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