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