##// END OF EJS Templates
hgweb_mod: inform hgweb class about paths actually being bytes...
Augie Fackler -
r37764:b29f490e default
parent child Browse files
Show More
@@ -1,458 +1,458 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, str):
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 # displaying bundling progress bar while serving feel wrong and may
228 # displaying bundling progress bar while serving feel wrong and may
229 # break some wsgi implementation.
229 # break some wsgi implementation.
230 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
230 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
231 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
231 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
232 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
232 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
233 self._lastrepo = self._repos[0]
233 self._lastrepo = self._repos[0]
234 hook.redirect(True)
234 hook.redirect(True)
235 self.reponame = name
235 self.reponame = name
236
236
237 def _webifyrepo(self, repo):
237 def _webifyrepo(self, repo):
238 repo = getwebview(repo)
238 repo = getwebview(repo)
239 self.websubtable = webutil.getwebsubs(repo)
239 self.websubtable = webutil.getwebsubs(repo)
240 return repo
240 return repo
241
241
242 @contextlib.contextmanager
242 @contextlib.contextmanager
243 def _obtainrepo(self):
243 def _obtainrepo(self):
244 """Obtain a repo unique to the caller.
244 """Obtain a repo unique to the caller.
245
245
246 Internally we maintain a stack of cachedlocalrepo instances
246 Internally we maintain a stack of cachedlocalrepo instances
247 to be handed out. If one is available, we pop it and return it,
247 to be handed out. If one is available, we pop it and return it,
248 ensuring it is up to date in the process. If one is not available,
248 ensuring it is up to date in the process. If one is not available,
249 we clone the most recently used repo instance and return it.
249 we clone the most recently used repo instance and return it.
250
250
251 It is currently possible for the stack to grow without bounds
251 It is currently possible for the stack to grow without bounds
252 if the server allows infinite threads. However, servers should
252 if the server allows infinite threads. However, servers should
253 have a thread limit, thus establishing our limit.
253 have a thread limit, thus establishing our limit.
254 """
254 """
255 if self._repos:
255 if self._repos:
256 cached = self._repos.pop()
256 cached = self._repos.pop()
257 r, created = cached.fetch()
257 r, created = cached.fetch()
258 else:
258 else:
259 cached = self._lastrepo.copy()
259 cached = self._lastrepo.copy()
260 r, created = cached.fetch()
260 r, created = cached.fetch()
261 if created:
261 if created:
262 r = self._webifyrepo(r)
262 r = self._webifyrepo(r)
263
263
264 self._lastrepo = cached
264 self._lastrepo = cached
265 self.mtime = cached.mtime
265 self.mtime = cached.mtime
266 try:
266 try:
267 yield r
267 yield r
268 finally:
268 finally:
269 self._repos.append(cached)
269 self._repos.append(cached)
270
270
271 def run(self):
271 def run(self):
272 """Start a server from CGI environment.
272 """Start a server from CGI environment.
273
273
274 Modern servers should be using WSGI and should avoid this
274 Modern servers should be using WSGI and should avoid this
275 method, if possible.
275 method, if possible.
276 """
276 """
277 if not encoding.environ.get('GATEWAY_INTERFACE',
277 if not encoding.environ.get('GATEWAY_INTERFACE',
278 '').startswith("CGI/1."):
278 '').startswith("CGI/1."):
279 raise RuntimeError("This function is only intended to be "
279 raise RuntimeError("This function is only intended to be "
280 "called while running as a CGI script.")
280 "called while running as a CGI script.")
281 wsgicgi.launch(self)
281 wsgicgi.launch(self)
282
282
283 def __call__(self, env, respond):
283 def __call__(self, env, respond):
284 """Run the WSGI application.
284 """Run the WSGI application.
285
285
286 This may be called by multiple threads.
286 This may be called by multiple threads.
287 """
287 """
288 req = requestmod.parserequestfromenv(env)
288 req = requestmod.parserequestfromenv(env)
289 res = requestmod.wsgiresponse(req, respond)
289 res = requestmod.wsgiresponse(req, respond)
290
290
291 return self.run_wsgi(req, res)
291 return self.run_wsgi(req, res)
292
292
293 def run_wsgi(self, req, res):
293 def run_wsgi(self, req, res):
294 """Internal method to run the WSGI application.
294 """Internal method to run the WSGI application.
295
295
296 This is typically only called by Mercurial. External consumers
296 This is typically only called by Mercurial. External consumers
297 should be using instances of this class as the WSGI application.
297 should be using instances of this class as the WSGI application.
298 """
298 """
299 with self._obtainrepo() as repo:
299 with self._obtainrepo() as repo:
300 profile = repo.ui.configbool('profiling', 'enabled')
300 profile = repo.ui.configbool('profiling', 'enabled')
301 with profiling.profile(repo.ui, enabled=profile):
301 with profiling.profile(repo.ui, enabled=profile):
302 for r in self._runwsgi(req, res, repo):
302 for r in self._runwsgi(req, res, repo):
303 yield r
303 yield r
304
304
305 def _runwsgi(self, req, res, repo):
305 def _runwsgi(self, req, res, repo):
306 rctx = requestcontext(self, repo, req, res)
306 rctx = requestcontext(self, repo, req, res)
307
307
308 # This state is global across all threads.
308 # This state is global across all threads.
309 encoding.encoding = rctx.config('web', 'encoding')
309 encoding.encoding = rctx.config('web', 'encoding')
310 rctx.repo.ui.environ = req.rawenv
310 rctx.repo.ui.environ = req.rawenv
311
311
312 if rctx.csp:
312 if rctx.csp:
313 # hgwebdir may have added CSP header. Since we generate our own,
313 # hgwebdir may have added CSP header. Since we generate our own,
314 # replace it.
314 # replace it.
315 res.headers['Content-Security-Policy'] = rctx.csp
315 res.headers['Content-Security-Policy'] = rctx.csp
316
316
317 # /api/* is reserved for various API implementations. Dispatch
317 # /api/* is reserved for various API implementations. Dispatch
318 # accordingly. But URL paths can conflict with subrepos and virtual
318 # accordingly. But URL paths can conflict with subrepos and virtual
319 # repos in hgwebdir. So until we have a workaround for this, only
319 # repos in hgwebdir. So until we have a workaround for this, only
320 # expose the URLs if the feature is enabled.
320 # expose the URLs if the feature is enabled.
321 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
321 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
322 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
322 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
323 wireprotoserver.handlewsgiapirequest(rctx, req, res,
323 wireprotoserver.handlewsgiapirequest(rctx, req, res,
324 self.check_perm)
324 self.check_perm)
325 return res.sendresponse()
325 return res.sendresponse()
326
326
327 handled = wireprotoserver.handlewsgirequest(
327 handled = wireprotoserver.handlewsgirequest(
328 rctx, req, res, self.check_perm)
328 rctx, req, res, self.check_perm)
329 if handled:
329 if handled:
330 return res.sendresponse()
330 return res.sendresponse()
331
331
332 # Old implementations of hgweb supported dispatching the request via
332 # Old implementations of hgweb supported dispatching the request via
333 # the initial query string parameter instead of using PATH_INFO.
333 # the initial query string parameter instead of using PATH_INFO.
334 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
334 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
335 # a value), we use it. Otherwise fall back to the query string.
335 # a value), we use it. Otherwise fall back to the query string.
336 if req.dispatchpath is not None:
336 if req.dispatchpath is not None:
337 query = req.dispatchpath
337 query = req.dispatchpath
338 else:
338 else:
339 query = req.querystring.partition('&')[0].partition(';')[0]
339 query = req.querystring.partition('&')[0].partition(';')[0]
340
340
341 # translate user-visible url structure to internal structure
341 # translate user-visible url structure to internal structure
342
342
343 args = query.split('/', 2)
343 args = query.split('/', 2)
344 if 'cmd' not in req.qsparams and args and args[0]:
344 if 'cmd' not in req.qsparams and args and args[0]:
345 cmd = args.pop(0)
345 cmd = args.pop(0)
346 style = cmd.rfind('-')
346 style = cmd.rfind('-')
347 if style != -1:
347 if style != -1:
348 req.qsparams['style'] = cmd[:style]
348 req.qsparams['style'] = cmd[:style]
349 cmd = cmd[style + 1:]
349 cmd = cmd[style + 1:]
350
350
351 # avoid accepting e.g. style parameter as command
351 # avoid accepting e.g. style parameter as command
352 if util.safehasattr(webcommands, cmd):
352 if util.safehasattr(webcommands, cmd):
353 req.qsparams['cmd'] = cmd
353 req.qsparams['cmd'] = cmd
354
354
355 if cmd == 'static':
355 if cmd == 'static':
356 req.qsparams['file'] = '/'.join(args)
356 req.qsparams['file'] = '/'.join(args)
357 else:
357 else:
358 if args and args[0]:
358 if args and args[0]:
359 node = args.pop(0).replace('%2F', '/')
359 node = args.pop(0).replace('%2F', '/')
360 req.qsparams['node'] = node
360 req.qsparams['node'] = node
361 if args:
361 if args:
362 if 'file' in req.qsparams:
362 if 'file' in req.qsparams:
363 del req.qsparams['file']
363 del req.qsparams['file']
364 for a in args:
364 for a in args:
365 req.qsparams.add('file', a)
365 req.qsparams.add('file', a)
366
366
367 ua = req.headers.get('User-Agent', '')
367 ua = req.headers.get('User-Agent', '')
368 if cmd == 'rev' and 'mercurial' in ua:
368 if cmd == 'rev' and 'mercurial' in ua:
369 req.qsparams['style'] = 'raw'
369 req.qsparams['style'] = 'raw'
370
370
371 if cmd == 'archive':
371 if cmd == 'archive':
372 fn = req.qsparams['node']
372 fn = req.qsparams['node']
373 for type_, spec in webutil.archivespecs.iteritems():
373 for type_, spec in webutil.archivespecs.iteritems():
374 ext = spec[2]
374 ext = spec[2]
375 if fn.endswith(ext):
375 if fn.endswith(ext):
376 req.qsparams['node'] = fn[:-len(ext)]
376 req.qsparams['node'] = fn[:-len(ext)]
377 req.qsparams['type'] = type_
377 req.qsparams['type'] = type_
378 else:
378 else:
379 cmd = req.qsparams.get('cmd', '')
379 cmd = req.qsparams.get('cmd', '')
380
380
381 # process the web interface request
381 # process the web interface request
382
382
383 try:
383 try:
384 rctx.tmpl = rctx.templater(req)
384 rctx.tmpl = rctx.templater(req)
385 ctype = rctx.tmpl.render('mimetype',
385 ctype = rctx.tmpl.render('mimetype',
386 {'encoding': encoding.encoding})
386 {'encoding': encoding.encoding})
387
387
388 # check read permissions non-static content
388 # check read permissions non-static content
389 if cmd != 'static':
389 if cmd != 'static':
390 self.check_perm(rctx, req, None)
390 self.check_perm(rctx, req, None)
391
391
392 if cmd == '':
392 if cmd == '':
393 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
393 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
394 cmd = req.qsparams['cmd']
394 cmd = req.qsparams['cmd']
395
395
396 # Don't enable caching if using a CSP nonce because then it wouldn't
396 # Don't enable caching if using a CSP nonce because then it wouldn't
397 # be a nonce.
397 # be a nonce.
398 if rctx.configbool('web', 'cache') and not rctx.nonce:
398 if rctx.configbool('web', 'cache') and not rctx.nonce:
399 tag = 'W/"%d"' % self.mtime
399 tag = 'W/"%d"' % self.mtime
400 if req.headers.get('If-None-Match') == tag:
400 if req.headers.get('If-None-Match') == tag:
401 res.status = '304 Not Modified'
401 res.status = '304 Not Modified'
402 # Response body not allowed on 304.
402 # Response body not allowed on 304.
403 res.setbodybytes('')
403 res.setbodybytes('')
404 return res.sendresponse()
404 return res.sendresponse()
405
405
406 res.headers['ETag'] = tag
406 res.headers['ETag'] = tag
407
407
408 if cmd not in webcommands.__all__:
408 if cmd not in webcommands.__all__:
409 msg = 'no such method: %s' % cmd
409 msg = 'no such method: %s' % cmd
410 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
410 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
411 else:
411 else:
412 # Set some globals appropriate for web handlers. Commands can
412 # Set some globals appropriate for web handlers. Commands can
413 # override easily enough.
413 # override easily enough.
414 res.status = '200 Script output follows'
414 res.status = '200 Script output follows'
415 res.headers['Content-Type'] = ctype
415 res.headers['Content-Type'] = ctype
416 return getattr(webcommands, cmd)(rctx)
416 return getattr(webcommands, cmd)(rctx)
417
417
418 except (error.LookupError, error.RepoLookupError) as err:
418 except (error.LookupError, error.RepoLookupError) as err:
419 msg = pycompat.bytestr(err)
419 msg = pycompat.bytestr(err)
420 if (util.safehasattr(err, 'name') and
420 if (util.safehasattr(err, 'name') and
421 not isinstance(err, error.ManifestLookupError)):
421 not isinstance(err, error.ManifestLookupError)):
422 msg = 'revision not found: %s' % err.name
422 msg = 'revision not found: %s' % err.name
423
423
424 res.status = '404 Not Found'
424 res.status = '404 Not Found'
425 res.headers['Content-Type'] = ctype
425 res.headers['Content-Type'] = ctype
426 return rctx.sendtemplate('error', error=msg)
426 return rctx.sendtemplate('error', error=msg)
427 except (error.RepoError, error.RevlogError) as e:
427 except (error.RepoError, error.RevlogError) as e:
428 res.status = '500 Internal Server Error'
428 res.status = '500 Internal Server Error'
429 res.headers['Content-Type'] = ctype
429 res.headers['Content-Type'] = ctype
430 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
430 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
431 except ErrorResponse as e:
431 except ErrorResponse as e:
432 res.status = statusmessage(e.code, pycompat.bytestr(e))
432 res.status = statusmessage(e.code, pycompat.bytestr(e))
433 res.headers['Content-Type'] = ctype
433 res.headers['Content-Type'] = ctype
434 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
434 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
435
435
436 def check_perm(self, rctx, req, op):
436 def check_perm(self, rctx, req, op):
437 for permhook in permhooks:
437 for permhook in permhooks:
438 permhook(rctx, req, op)
438 permhook(rctx, req, op)
439
439
440 def getwebview(repo):
440 def getwebview(repo):
441 """The 'web.view' config controls changeset filter to hgweb. Possible
441 """The 'web.view' config controls changeset filter to hgweb. Possible
442 values are ``served``, ``visible`` and ``all``. Default is ``served``.
442 values are ``served``, ``visible`` and ``all``. Default is ``served``.
443 The ``served`` filter only shows changesets that can be pulled from the
443 The ``served`` filter only shows changesets that can be pulled from the
444 hgweb instance. The``visible`` filter includes secret changesets but
444 hgweb instance. The``visible`` filter includes secret changesets but
445 still excludes "hidden" one.
445 still excludes "hidden" one.
446
446
447 See the repoview module for details.
447 See the repoview module for details.
448
448
449 The option has been around undocumented since Mercurial 2.5, but no
449 The option has been around undocumented since Mercurial 2.5, but no
450 user ever asked about it. So we better keep it undocumented for now."""
450 user ever asked about it. So we better keep it undocumented for now."""
451 # experimental config: web.view
451 # experimental config: web.view
452 viewconfig = repo.ui.config('web', 'view', untrusted=True)
452 viewconfig = repo.ui.config('web', 'view', untrusted=True)
453 if viewconfig == 'all':
453 if viewconfig == 'all':
454 return repo.unfiltered()
454 return repo.unfiltered()
455 elif viewconfig in repoview.filtertable:
455 elif viewconfig in repoview.filtertable:
456 return repo.filtered(viewconfig)
456 return repo.filtered(viewconfig)
457 else:
457 else:
458 return repo.filtered('served')
458 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now