##// END OF EJS Templates
hgwebmod: use sysstr to check for attribute presence...
marmoute -
r51789:b2b8c25f default
parent child Browse files
Show More
@@ -1,527 +1,527 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9
9
10 import contextlib
10 import contextlib
11 import os
11 import os
12
12
13 from .common import (
13 from .common import (
14 ErrorResponse,
14 ErrorResponse,
15 HTTP_BAD_REQUEST,
15 HTTP_BAD_REQUEST,
16 cspvalues,
16 cspvalues,
17 permhooks,
17 permhooks,
18 statusmessage,
18 statusmessage,
19 )
19 )
20 from ..pycompat import getattr
20 from ..pycompat import getattr
21
21
22 from .. import (
22 from .. import (
23 encoding,
23 encoding,
24 error,
24 error,
25 extensions,
25 extensions,
26 formatter,
26 formatter,
27 hg,
27 hg,
28 hook,
28 hook,
29 profiling,
29 profiling,
30 pycompat,
30 pycompat,
31 registrar,
31 registrar,
32 repoview,
32 repoview,
33 templatefilters,
33 templatefilters,
34 templater,
34 templater,
35 templateutil,
35 templateutil,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 wireprotoserver,
38 wireprotoserver,
39 )
39 )
40
40
41 from . import (
41 from . import (
42 common,
42 common,
43 request as requestmod,
43 request as requestmod,
44 webcommands,
44 webcommands,
45 webutil,
45 webutil,
46 wsgicgi,
46 wsgicgi,
47 )
47 )
48
48
49
49
50 def getstyle(req, configfn, templatepath):
50 def getstyle(req, configfn, templatepath):
51 styles = (
51 styles = (
52 req.qsparams.get(b'style', None),
52 req.qsparams.get(b'style', None),
53 configfn(b'web', b'style'),
53 configfn(b'web', b'style'),
54 b'paper',
54 b'paper',
55 )
55 )
56 return styles, _stylemap(styles, templatepath)
56 return styles, _stylemap(styles, templatepath)
57
57
58
58
59 def _stylemap(styles, path=None):
59 def _stylemap(styles, path=None):
60 """Return path to mapfile for a given style.
60 """Return path to mapfile for a given style.
61
61
62 Searches mapfile in the following locations:
62 Searches mapfile in the following locations:
63 1. templatepath/style/map
63 1. templatepath/style/map
64 2. templatepath/map-style
64 2. templatepath/map-style
65 3. templatepath/map
65 3. templatepath/map
66 """
66 """
67
67
68 for style in styles:
68 for style in styles:
69 # only plain name is allowed to honor template paths
69 # only plain name is allowed to honor template paths
70 if (
70 if (
71 not style
71 not style
72 or style in (pycompat.oscurdir, pycompat.ospardir)
72 or style in (pycompat.oscurdir, pycompat.ospardir)
73 or pycompat.ossep in style
73 or pycompat.ossep in style
74 or pycompat.osaltsep
74 or pycompat.osaltsep
75 and pycompat.osaltsep in style
75 and pycompat.osaltsep in style
76 ):
76 ):
77 continue
77 continue
78 locations = (os.path.join(style, b'map'), b'map-' + style, b'map')
78 locations = (os.path.join(style, b'map'), b'map-' + style, b'map')
79
79
80 for location in locations:
80 for location in locations:
81 mapfile, fp = templater.try_open_template(location, path)
81 mapfile, fp = templater.try_open_template(location, path)
82 if mapfile:
82 if mapfile:
83 return style, mapfile, fp
83 return style, mapfile, fp
84
84
85 raise RuntimeError(b"No hgweb templates found in %r" % path)
85 raise RuntimeError(b"No hgweb templates found in %r" % path)
86
86
87
87
88 def makebreadcrumb(url, prefix=b''):
88 def makebreadcrumb(url, prefix=b''):
89 """Return a 'URL breadcrumb' list
89 """Return a 'URL breadcrumb' list
90
90
91 A 'URL breadcrumb' is a list of URL-name pairs,
91 A 'URL breadcrumb' is a list of URL-name pairs,
92 corresponding to each of the path items on a URL.
92 corresponding to each of the path items on a URL.
93 This can be used to create path navigation entries.
93 This can be used to create path navigation entries.
94 """
94 """
95 if url.endswith(b'/'):
95 if url.endswith(b'/'):
96 url = url[:-1]
96 url = url[:-1]
97 if prefix:
97 if prefix:
98 url = b'/' + prefix + url
98 url = b'/' + prefix + url
99 relpath = url
99 relpath = url
100 if relpath.startswith(b'/'):
100 if relpath.startswith(b'/'):
101 relpath = relpath[1:]
101 relpath = relpath[1:]
102
102
103 breadcrumb = []
103 breadcrumb = []
104 urlel = url
104 urlel = url
105 pathitems = [b''] + relpath.split(b'/')
105 pathitems = [b''] + relpath.split(b'/')
106 for pathel in reversed(pathitems):
106 for pathel in reversed(pathitems):
107 if not pathel or not urlel:
107 if not pathel or not urlel:
108 break
108 break
109 breadcrumb.append({b'url': urlel, b'name': pathel})
109 breadcrumb.append({b'url': urlel, b'name': pathel})
110 urlel = os.path.dirname(urlel)
110 urlel = os.path.dirname(urlel)
111 return templateutil.mappinglist(reversed(breadcrumb))
111 return templateutil.mappinglist(reversed(breadcrumb))
112
112
113
113
114 class requestcontext:
114 class requestcontext:
115 """Holds state/context for an individual request.
115 """Holds state/context for an individual request.
116
116
117 Servers can be multi-threaded. Holding state on the WSGI application
117 Servers can be multi-threaded. Holding state on the WSGI application
118 is prone to race conditions. Instances of this class exist to hold
118 is prone to race conditions. Instances of this class exist to hold
119 mutable and race-free state for requests.
119 mutable and race-free state for requests.
120 """
120 """
121
121
122 def __init__(self, app, repo, req, res):
122 def __init__(self, app, repo, req, res):
123 self.repo = repo
123 self.repo = repo
124 self.reponame = app.reponame
124 self.reponame = app.reponame
125 self.req = req
125 self.req = req
126 self.res = res
126 self.res = res
127
127
128 # Only works if the filter actually support being upgraded to show
128 # Only works if the filter actually support being upgraded to show
129 # visible changesets
129 # visible changesets
130 current_filter = repo.filtername
130 current_filter = repo.filtername
131 if (
131 if (
132 common.hashiddenaccess(repo, req)
132 common.hashiddenaccess(repo, req)
133 and current_filter is not None
133 and current_filter is not None
134 and current_filter + b'.hidden' in repoview.filtertable
134 and current_filter + b'.hidden' in repoview.filtertable
135 ):
135 ):
136 self.repo = self.repo.filtered(repo.filtername + b'.hidden')
136 self.repo = self.repo.filtered(repo.filtername + b'.hidden')
137
137
138 self.maxchanges = self.configint(b'web', b'maxchanges')
138 self.maxchanges = self.configint(b'web', b'maxchanges')
139 self.stripecount = self.configint(b'web', b'stripes')
139 self.stripecount = self.configint(b'web', b'stripes')
140 self.maxshortchanges = self.configint(b'web', b'maxshortchanges')
140 self.maxshortchanges = self.configint(b'web', b'maxshortchanges')
141 self.maxfiles = self.configint(b'web', b'maxfiles')
141 self.maxfiles = self.configint(b'web', b'maxfiles')
142 self.allowpull = self.configbool(b'web', b'allow-pull')
142 self.allowpull = self.configbool(b'web', b'allow-pull')
143
143
144 # we use untrusted=False to prevent a repo owner from using
144 # we use untrusted=False to prevent a repo owner from using
145 # web.templates in .hg/hgrc to get access to any file readable
145 # web.templates in .hg/hgrc to get access to any file readable
146 # by the user running the CGI script
146 # by the user running the CGI script
147 self.templatepath = self.config(b'web', b'templates', untrusted=False)
147 self.templatepath = self.config(b'web', b'templates', untrusted=False)
148
148
149 # This object is more expensive to build than simple config values.
149 # This object is more expensive to build than simple config values.
150 # It is shared across requests. The app will replace the object
150 # It is shared across requests. The app will replace the object
151 # if it is updated. Since this is a reference and nothing should
151 # if it is updated. Since this is a reference and nothing should
152 # modify the underlying object, it should be constant for the lifetime
152 # modify the underlying object, it should be constant for the lifetime
153 # of the request.
153 # of the request.
154 self.websubtable = app.websubtable
154 self.websubtable = app.websubtable
155
155
156 self.csp, self.nonce = cspvalues(self.repo.ui)
156 self.csp, self.nonce = cspvalues(self.repo.ui)
157
157
158 # Trust the settings from the .hg/hgrc files by default.
158 # Trust the settings from the .hg/hgrc files by default.
159 def config(self, *args, **kwargs):
159 def config(self, *args, **kwargs):
160 kwargs.setdefault('untrusted', True)
160 kwargs.setdefault('untrusted', True)
161 return self.repo.ui.config(*args, **kwargs)
161 return self.repo.ui.config(*args, **kwargs)
162
162
163 def configbool(self, *args, **kwargs):
163 def configbool(self, *args, **kwargs):
164 kwargs.setdefault('untrusted', True)
164 kwargs.setdefault('untrusted', True)
165 return self.repo.ui.configbool(*args, **kwargs)
165 return self.repo.ui.configbool(*args, **kwargs)
166
166
167 def configint(self, *args, **kwargs):
167 def configint(self, *args, **kwargs):
168 kwargs.setdefault('untrusted', True)
168 kwargs.setdefault('untrusted', True)
169 return self.repo.ui.configint(*args, **kwargs)
169 return self.repo.ui.configint(*args, **kwargs)
170
170
171 def configlist(self, *args, **kwargs):
171 def configlist(self, *args, **kwargs):
172 kwargs.setdefault('untrusted', True)
172 kwargs.setdefault('untrusted', True)
173 return self.repo.ui.configlist(*args, **kwargs)
173 return self.repo.ui.configlist(*args, **kwargs)
174
174
175 def archivelist(self, nodeid):
175 def archivelist(self, nodeid):
176 return webutil.archivelist(self.repo.ui, nodeid)
176 return webutil.archivelist(self.repo.ui, nodeid)
177
177
178 def templater(self, req):
178 def templater(self, req):
179 # determine scheme, port and server name
179 # determine scheme, port and server name
180 # this is needed to create absolute urls
180 # this is needed to create absolute urls
181 logourl = self.config(b'web', b'logourl')
181 logourl = self.config(b'web', b'logourl')
182 logoimg = self.config(b'web', b'logoimg')
182 logoimg = self.config(b'web', b'logoimg')
183 staticurl = (
183 staticurl = (
184 self.config(b'web', b'staticurl')
184 self.config(b'web', b'staticurl')
185 or req.apppath.rstrip(b'/') + b'/static/'
185 or req.apppath.rstrip(b'/') + b'/static/'
186 )
186 )
187 if not staticurl.endswith(b'/'):
187 if not staticurl.endswith(b'/'):
188 staticurl += b'/'
188 staticurl += b'/'
189
189
190 # figure out which style to use
190 # figure out which style to use
191
191
192 vars = {}
192 vars = {}
193 styles, (style, mapfile, fp) = getstyle(
193 styles, (style, mapfile, fp) = getstyle(
194 req, self.config, self.templatepath
194 req, self.config, self.templatepath
195 )
195 )
196 if style == styles[0]:
196 if style == styles[0]:
197 vars[b'style'] = style
197 vars[b'style'] = style
198
198
199 sessionvars = webutil.sessionvars(vars, b'?')
199 sessionvars = webutil.sessionvars(vars, b'?')
200
200
201 if not self.reponame:
201 if not self.reponame:
202 self.reponame = (
202 self.reponame = (
203 self.config(b'web', b'name', b'')
203 self.config(b'web', b'name', b'')
204 or req.reponame
204 or req.reponame
205 or req.apppath
205 or req.apppath
206 or self.repo.root
206 or self.repo.root
207 )
207 )
208
208
209 filters = {}
209 filters = {}
210 templatefilter = registrar.templatefilter(filters)
210 templatefilter = registrar.templatefilter(filters)
211
211
212 @templatefilter(b'websub', intype=bytes)
212 @templatefilter(b'websub', intype=bytes)
213 def websubfilter(text):
213 def websubfilter(text):
214 return templatefilters.websub(text, self.websubtable)
214 return templatefilters.websub(text, self.websubtable)
215
215
216 # create the templater
216 # create the templater
217 # TODO: export all keywords: defaults = templatekw.keywords.copy()
217 # TODO: export all keywords: defaults = templatekw.keywords.copy()
218 defaults = {
218 defaults = {
219 b'url': req.apppath + b'/',
219 b'url': req.apppath + b'/',
220 b'logourl': logourl,
220 b'logourl': logourl,
221 b'logoimg': logoimg,
221 b'logoimg': logoimg,
222 b'staticurl': staticurl,
222 b'staticurl': staticurl,
223 b'urlbase': req.advertisedbaseurl,
223 b'urlbase': req.advertisedbaseurl,
224 b'repo': self.reponame,
224 b'repo': self.reponame,
225 b'encoding': encoding.encoding,
225 b'encoding': encoding.encoding,
226 b'sessionvars': sessionvars,
226 b'sessionvars': sessionvars,
227 b'pathdef': makebreadcrumb(req.apppath),
227 b'pathdef': makebreadcrumb(req.apppath),
228 b'style': style,
228 b'style': style,
229 b'nonce': self.nonce,
229 b'nonce': self.nonce,
230 }
230 }
231 templatekeyword = registrar.templatekeyword(defaults)
231 templatekeyword = registrar.templatekeyword(defaults)
232
232
233 @templatekeyword(b'motd', requires=())
233 @templatekeyword(b'motd', requires=())
234 def motd(context, mapping):
234 def motd(context, mapping):
235 yield self.config(b'web', b'motd')
235 yield self.config(b'web', b'motd')
236
236
237 tres = formatter.templateresources(self.repo.ui, self.repo)
237 tres = formatter.templateresources(self.repo.ui, self.repo)
238 return templater.templater.frommapfile(
238 return templater.templater.frommapfile(
239 mapfile, fp=fp, filters=filters, defaults=defaults, resources=tres
239 mapfile, fp=fp, filters=filters, defaults=defaults, resources=tres
240 )
240 )
241
241
242 def sendtemplate(self, name, **kwargs):
242 def sendtemplate(self, name, **kwargs):
243 """Helper function to send a response generated from a template."""
243 """Helper function to send a response generated from a template."""
244 if self.req.method != b'HEAD':
244 if self.req.method != b'HEAD':
245 kwargs = pycompat.byteskwargs(kwargs)
245 kwargs = pycompat.byteskwargs(kwargs)
246 self.res.setbodygen(self.tmpl.generate(name, kwargs))
246 self.res.setbodygen(self.tmpl.generate(name, kwargs))
247 return self.res.sendresponse()
247 return self.res.sendresponse()
248
248
249
249
250 class hgweb:
250 class hgweb:
251 """HTTP server for individual repositories.
251 """HTTP server for individual repositories.
252
252
253 Instances of this class serve HTTP responses for a particular
253 Instances of this class serve HTTP responses for a particular
254 repository.
254 repository.
255
255
256 Instances are typically used as WSGI applications.
256 Instances are typically used as WSGI applications.
257
257
258 Some servers are multi-threaded. On these servers, there may
258 Some servers are multi-threaded. On these servers, there may
259 be multiple active threads inside __call__.
259 be multiple active threads inside __call__.
260 """
260 """
261
261
262 def __init__(self, repo, name=None, baseui=None):
262 def __init__(self, repo, name=None, baseui=None):
263 if isinstance(repo, bytes):
263 if isinstance(repo, bytes):
264 if baseui:
264 if baseui:
265 u = baseui.copy()
265 u = baseui.copy()
266 else:
266 else:
267 u = uimod.ui.load()
267 u = uimod.ui.load()
268 extensions.loadall(u)
268 extensions.loadall(u)
269 extensions.populateui(u)
269 extensions.populateui(u)
270 r = hg.repository(u, repo)
270 r = hg.repository(u, repo)
271 else:
271 else:
272 # we trust caller to give us a private copy
272 # we trust caller to give us a private copy
273 r = repo
273 r = repo
274
274
275 r.ui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
275 r.ui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
276 r.baseui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
276 r.baseui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
277 r.ui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
277 r.ui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
278 r.baseui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
278 r.baseui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
279 # resolve file patterns relative to repo root
279 # resolve file patterns relative to repo root
280 r.ui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
280 r.ui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
281 r.baseui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
281 r.baseui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
282 # it's unlikely that we can replace signal handlers in WSGI server,
282 # it's unlikely that we can replace signal handlers in WSGI server,
283 # and mod_wsgi issues a big warning. a plain hgweb process (with no
283 # and mod_wsgi issues a big warning. a plain hgweb process (with no
284 # threading) could replace signal handlers, but we don't bother
284 # threading) could replace signal handlers, but we don't bother
285 # conditionally enabling it.
285 # conditionally enabling it.
286 r.ui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
286 r.ui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
287 r.baseui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
287 r.baseui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
288 # displaying bundling progress bar while serving feel wrong and may
288 # displaying bundling progress bar while serving feel wrong and may
289 # break some wsgi implementation.
289 # break some wsgi implementation.
290 r.ui.setconfig(b'progress', b'disable', b'true', b'hgweb')
290 r.ui.setconfig(b'progress', b'disable', b'true', b'hgweb')
291 r.baseui.setconfig(b'progress', b'disable', b'true', b'hgweb')
291 r.baseui.setconfig(b'progress', b'disable', b'true', b'hgweb')
292 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
292 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
293 self._lastrepo = self._repos[0]
293 self._lastrepo = self._repos[0]
294 hook.redirect(True)
294 hook.redirect(True)
295 self.reponame = name
295 self.reponame = name
296
296
297 def _webifyrepo(self, repo):
297 def _webifyrepo(self, repo):
298 repo = getwebview(repo)
298 repo = getwebview(repo)
299 self.websubtable = webutil.getwebsubs(repo)
299 self.websubtable = webutil.getwebsubs(repo)
300 return repo
300 return repo
301
301
302 @contextlib.contextmanager
302 @contextlib.contextmanager
303 def _obtainrepo(self):
303 def _obtainrepo(self):
304 """Obtain a repo unique to the caller.
304 """Obtain a repo unique to the caller.
305
305
306 Internally we maintain a stack of cachedlocalrepo instances
306 Internally we maintain a stack of cachedlocalrepo instances
307 to be handed out. If one is available, we pop it and return it,
307 to be handed out. If one is available, we pop it and return it,
308 ensuring it is up to date in the process. If one is not available,
308 ensuring it is up to date in the process. If one is not available,
309 we clone the most recently used repo instance and return it.
309 we clone the most recently used repo instance and return it.
310
310
311 It is currently possible for the stack to grow without bounds
311 It is currently possible for the stack to grow without bounds
312 if the server allows infinite threads. However, servers should
312 if the server allows infinite threads. However, servers should
313 have a thread limit, thus establishing our limit.
313 have a thread limit, thus establishing our limit.
314 """
314 """
315 if self._repos:
315 if self._repos:
316 cached = self._repos.pop()
316 cached = self._repos.pop()
317 r, created = cached.fetch()
317 r, created = cached.fetch()
318 else:
318 else:
319 cached = self._lastrepo.copy()
319 cached = self._lastrepo.copy()
320 r, created = cached.fetch()
320 r, created = cached.fetch()
321 if created:
321 if created:
322 r = self._webifyrepo(r)
322 r = self._webifyrepo(r)
323
323
324 self._lastrepo = cached
324 self._lastrepo = cached
325 self.mtime = cached.mtime
325 self.mtime = cached.mtime
326 try:
326 try:
327 yield r
327 yield r
328 finally:
328 finally:
329 self._repos.append(cached)
329 self._repos.append(cached)
330
330
331 def run(self):
331 def run(self):
332 """Start a server from CGI environment.
332 """Start a server from CGI environment.
333
333
334 Modern servers should be using WSGI and should avoid this
334 Modern servers should be using WSGI and should avoid this
335 method, if possible.
335 method, if possible.
336 """
336 """
337 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
337 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
338 b"CGI/1."
338 b"CGI/1."
339 ):
339 ):
340 raise RuntimeError(
340 raise RuntimeError(
341 b"This function is only intended to be "
341 b"This function is only intended to be "
342 b"called while running as a CGI script."
342 b"called while running as a CGI script."
343 )
343 )
344 wsgicgi.launch(self)
344 wsgicgi.launch(self)
345
345
346 def __call__(self, env, respond):
346 def __call__(self, env, respond):
347 """Run the WSGI application.
347 """Run the WSGI application.
348
348
349 This may be called by multiple threads.
349 This may be called by multiple threads.
350 """
350 """
351 req = requestmod.parserequestfromenv(env)
351 req = requestmod.parserequestfromenv(env)
352 res = requestmod.wsgiresponse(req, respond)
352 res = requestmod.wsgiresponse(req, respond)
353
353
354 return self.run_wsgi(req, res)
354 return self.run_wsgi(req, res)
355
355
356 def run_wsgi(self, req, res):
356 def run_wsgi(self, req, res):
357 """Internal method to run the WSGI application.
357 """Internal method to run the WSGI application.
358
358
359 This is typically only called by Mercurial. External consumers
359 This is typically only called by Mercurial. External consumers
360 should be using instances of this class as the WSGI application.
360 should be using instances of this class as the WSGI application.
361 """
361 """
362 with self._obtainrepo() as repo:
362 with self._obtainrepo() as repo:
363 profile = repo.ui.configbool(b'profiling', b'enabled')
363 profile = repo.ui.configbool(b'profiling', b'enabled')
364 with profiling.profile(repo.ui, enabled=profile):
364 with profiling.profile(repo.ui, enabled=profile):
365 for r in self._runwsgi(req, res, repo):
365 for r in self._runwsgi(req, res, repo):
366 yield r
366 yield r
367
367
368 def _runwsgi(self, req, res, repo):
368 def _runwsgi(self, req, res, repo):
369 rctx = requestcontext(self, repo, req, res)
369 rctx = requestcontext(self, repo, req, res)
370
370
371 # This state is global across all threads.
371 # This state is global across all threads.
372 encoding.encoding = rctx.config(b'web', b'encoding')
372 encoding.encoding = rctx.config(b'web', b'encoding')
373 rctx.repo.ui.environ = req.rawenv
373 rctx.repo.ui.environ = req.rawenv
374
374
375 if rctx.csp:
375 if rctx.csp:
376 # hgwebdir may have added CSP header. Since we generate our own,
376 # hgwebdir may have added CSP header. Since we generate our own,
377 # replace it.
377 # replace it.
378 res.headers[b'Content-Security-Policy'] = rctx.csp
378 res.headers[b'Content-Security-Policy'] = rctx.csp
379
379
380 handled = wireprotoserver.handlewsgirequest(
380 handled = wireprotoserver.handlewsgirequest(
381 rctx, req, res, self.check_perm
381 rctx, req, res, self.check_perm
382 )
382 )
383 if handled:
383 if handled:
384 return res.sendresponse()
384 return res.sendresponse()
385
385
386 # Old implementations of hgweb supported dispatching the request via
386 # Old implementations of hgweb supported dispatching the request via
387 # the initial query string parameter instead of using PATH_INFO.
387 # the initial query string parameter instead of using PATH_INFO.
388 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
388 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
389 # a value), we use it. Otherwise fall back to the query string.
389 # a value), we use it. Otherwise fall back to the query string.
390 if req.dispatchpath is not None:
390 if req.dispatchpath is not None:
391 query = req.dispatchpath
391 query = req.dispatchpath
392 else:
392 else:
393 query = req.querystring.partition(b'&')[0].partition(b';')[0]
393 query = req.querystring.partition(b'&')[0].partition(b';')[0]
394
394
395 # translate user-visible url structure to internal structure
395 # translate user-visible url structure to internal structure
396
396
397 args = query.split(b'/', 2)
397 args = query.split(b'/', 2)
398 if b'cmd' not in req.qsparams and args and args[0]:
398 if b'cmd' not in req.qsparams and args and args[0]:
399 cmd = args.pop(0)
399 cmd = args.pop(0)
400 style = cmd.rfind(b'-')
400 style = cmd.rfind(b'-')
401 if style != -1:
401 if style != -1:
402 req.qsparams[b'style'] = cmd[:style]
402 req.qsparams[b'style'] = cmd[:style]
403 cmd = cmd[style + 1 :]
403 cmd = cmd[style + 1 :]
404
404
405 # avoid accepting e.g. style parameter as command
405 # avoid accepting e.g. style parameter as command
406 if util.safehasattr(webcommands, cmd):
406 if util.safehasattr(webcommands, pycompat.sysstr(cmd)):
407 req.qsparams[b'cmd'] = cmd
407 req.qsparams[b'cmd'] = cmd
408
408
409 if cmd == b'static':
409 if cmd == b'static':
410 req.qsparams[b'file'] = b'/'.join(args)
410 req.qsparams[b'file'] = b'/'.join(args)
411 else:
411 else:
412 if args and args[0]:
412 if args and args[0]:
413 node = args.pop(0).replace(b'%2F', b'/')
413 node = args.pop(0).replace(b'%2F', b'/')
414 req.qsparams[b'node'] = node
414 req.qsparams[b'node'] = node
415 if args:
415 if args:
416 if b'file' in req.qsparams:
416 if b'file' in req.qsparams:
417 del req.qsparams[b'file']
417 del req.qsparams[b'file']
418 for a in args:
418 for a in args:
419 req.qsparams.add(b'file', a)
419 req.qsparams.add(b'file', a)
420
420
421 ua = req.headers.get(b'User-Agent', b'')
421 ua = req.headers.get(b'User-Agent', b'')
422 if cmd == b'rev' and b'mercurial' in ua:
422 if cmd == b'rev' and b'mercurial' in ua:
423 req.qsparams[b'style'] = b'raw'
423 req.qsparams[b'style'] = b'raw'
424
424
425 if cmd == b'archive':
425 if cmd == b'archive':
426 fn = req.qsparams[b'node']
426 fn = req.qsparams[b'node']
427 for type_, spec in webutil.archivespecs.items():
427 for type_, spec in webutil.archivespecs.items():
428 ext = spec[2]
428 ext = spec[2]
429 if fn.endswith(ext):
429 if fn.endswith(ext):
430 req.qsparams[b'node'] = fn[: -len(ext)]
430 req.qsparams[b'node'] = fn[: -len(ext)]
431 req.qsparams[b'type'] = type_
431 req.qsparams[b'type'] = type_
432 else:
432 else:
433 cmd = req.qsparams.get(b'cmd', b'')
433 cmd = req.qsparams.get(b'cmd', b'')
434
434
435 # process the web interface request
435 # process the web interface request
436
436
437 try:
437 try:
438 rctx.tmpl = rctx.templater(req)
438 rctx.tmpl = rctx.templater(req)
439 ctype = rctx.tmpl.render(
439 ctype = rctx.tmpl.render(
440 b'mimetype', {b'encoding': encoding.encoding}
440 b'mimetype', {b'encoding': encoding.encoding}
441 )
441 )
442
442
443 # check read permissions non-static content
443 # check read permissions non-static content
444 if cmd != b'static':
444 if cmd != b'static':
445 self.check_perm(rctx, req, None)
445 self.check_perm(rctx, req, None)
446
446
447 if cmd == b'':
447 if cmd == b'':
448 req.qsparams[b'cmd'] = rctx.tmpl.render(b'default', {})
448 req.qsparams[b'cmd'] = rctx.tmpl.render(b'default', {})
449 cmd = req.qsparams[b'cmd']
449 cmd = req.qsparams[b'cmd']
450
450
451 # Don't enable caching if using a CSP nonce because then it wouldn't
451 # Don't enable caching if using a CSP nonce because then it wouldn't
452 # be a nonce.
452 # be a nonce.
453 if rctx.configbool(b'web', b'cache') and not rctx.nonce:
453 if rctx.configbool(b'web', b'cache') and not rctx.nonce:
454 tag = b'W/"%d"' % self.mtime
454 tag = b'W/"%d"' % self.mtime
455 if req.headers.get(b'If-None-Match') == tag:
455 if req.headers.get(b'If-None-Match') == tag:
456 res.status = b'304 Not Modified'
456 res.status = b'304 Not Modified'
457 # Content-Type may be defined globally. It isn't valid on a
457 # Content-Type may be defined globally. It isn't valid on a
458 # 304, so discard it.
458 # 304, so discard it.
459 try:
459 try:
460 del res.headers[b'Content-Type']
460 del res.headers[b'Content-Type']
461 except KeyError:
461 except KeyError:
462 pass
462 pass
463 # Response body not allowed on 304.
463 # Response body not allowed on 304.
464 res.setbodybytes(b'')
464 res.setbodybytes(b'')
465 return res.sendresponse()
465 return res.sendresponse()
466
466
467 res.headers[b'ETag'] = tag
467 res.headers[b'ETag'] = tag
468
468
469 if cmd not in webcommands.__all__:
469 if cmd not in webcommands.__all__:
470 msg = b'no such method: %s' % cmd
470 msg = b'no such method: %s' % cmd
471 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
471 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
472 else:
472 else:
473 # Set some globals appropriate for web handlers. Commands can
473 # Set some globals appropriate for web handlers. Commands can
474 # override easily enough.
474 # override easily enough.
475 res.status = b'200 Script output follows'
475 res.status = b'200 Script output follows'
476 res.headers[b'Content-Type'] = ctype
476 res.headers[b'Content-Type'] = ctype
477 return getattr(webcommands, cmd)(rctx)
477 return getattr(webcommands, pycompat.sysstr(cmd))(rctx)
478
478
479 except (error.LookupError, error.RepoLookupError) as err:
479 except (error.LookupError, error.RepoLookupError) as err:
480 msg = pycompat.bytestr(err)
480 msg = pycompat.bytestr(err)
481 if util.safehasattr(err, 'name') and not isinstance(
481 if util.safehasattr(err, 'name') and not isinstance(
482 err, error.ManifestLookupError
482 err, error.ManifestLookupError
483 ):
483 ):
484 msg = b'revision not found: %s' % err.name
484 msg = b'revision not found: %s' % err.name
485
485
486 res.status = b'404 Not Found'
486 res.status = b'404 Not Found'
487 res.headers[b'Content-Type'] = ctype
487 res.headers[b'Content-Type'] = ctype
488 return rctx.sendtemplate(b'error', error=msg)
488 return rctx.sendtemplate(b'error', error=msg)
489 except (error.RepoError, error.StorageError) as e:
489 except (error.RepoError, error.StorageError) as e:
490 res.status = b'500 Internal Server Error'
490 res.status = b'500 Internal Server Error'
491 res.headers[b'Content-Type'] = ctype
491 res.headers[b'Content-Type'] = ctype
492 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
492 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
493 except error.Abort as e:
493 except error.Abort as e:
494 res.status = b'403 Forbidden'
494 res.status = b'403 Forbidden'
495 res.headers[b'Content-Type'] = ctype
495 res.headers[b'Content-Type'] = ctype
496 return rctx.sendtemplate(b'error', error=e.message)
496 return rctx.sendtemplate(b'error', error=e.message)
497 except ErrorResponse as e:
497 except ErrorResponse as e:
498 for k, v in e.headers:
498 for k, v in e.headers:
499 res.headers[k] = v
499 res.headers[k] = v
500 res.status = statusmessage(e.code, pycompat.bytestr(e))
500 res.status = statusmessage(e.code, pycompat.bytestr(e))
501 res.headers[b'Content-Type'] = ctype
501 res.headers[b'Content-Type'] = ctype
502 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
502 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
503
503
504 def check_perm(self, rctx, req, op):
504 def check_perm(self, rctx, req, op):
505 for permhook in permhooks:
505 for permhook in permhooks:
506 permhook(rctx, req, op)
506 permhook(rctx, req, op)
507
507
508
508
509 def getwebview(repo):
509 def getwebview(repo):
510 """The 'web.view' config controls changeset filter to hgweb. Possible
510 """The 'web.view' config controls changeset filter to hgweb. Possible
511 values are ``served``, ``visible`` and ``all``. Default is ``served``.
511 values are ``served``, ``visible`` and ``all``. Default is ``served``.
512 The ``served`` filter only shows changesets that can be pulled from the
512 The ``served`` filter only shows changesets that can be pulled from the
513 hgweb instance. The``visible`` filter includes secret changesets but
513 hgweb instance. The``visible`` filter includes secret changesets but
514 still excludes "hidden" one.
514 still excludes "hidden" one.
515
515
516 See the repoview module for details.
516 See the repoview module for details.
517
517
518 The option has been around undocumented since Mercurial 2.5, but no
518 The option has been around undocumented since Mercurial 2.5, but no
519 user ever asked about it. So we better keep it undocumented for now."""
519 user ever asked about it. So we better keep it undocumented for now."""
520 # experimental config: web.view
520 # experimental config: web.view
521 viewconfig = repo.ui.config(b'web', b'view', untrusted=True)
521 viewconfig = repo.ui.config(b'web', b'view', untrusted=True)
522 if viewconfig == b'all':
522 if viewconfig == b'all':
523 return repo.unfiltered()
523 return repo.unfiltered()
524 elif viewconfig in repoview.filtertable:
524 elif viewconfig in repoview.filtertable:
525 return repo.filtered(viewconfig)
525 return repo.filtered(viewconfig)
526 else:
526 else:
527 return repo.filtered(b'served')
527 return repo.filtered(b'served')
General Comments 0
You need to be logged in to leave comments. Login now