##// END OF EJS Templates
hgweb: evaluate the "default" value as template...
Yuya Nishihara -
r37038:16bbb154 default
parent child Browse files
Show More
@@ -1,454 +1,454 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 repoview,
30 repoview,
31 templatefilters,
31 templatefilters,
32 templater,
32 templater,
33 ui as uimod,
33 ui as uimod,
34 util,
34 util,
35 wireprotoserver,
35 wireprotoserver,
36 )
36 )
37
37
38 from . import (
38 from . import (
39 request as requestmod,
39 request as requestmod,
40 webcommands,
40 webcommands,
41 webutil,
41 webutil,
42 wsgicgi,
42 wsgicgi,
43 )
43 )
44
44
45 archivespecs = util.sortdict((
45 archivespecs = util.sortdict((
46 ('zip', ('application/zip', 'zip', '.zip', None)),
46 ('zip', ('application/zip', 'zip', '.zip', None)),
47 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
47 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
48 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
48 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
49 ))
49 ))
50
50
51 def getstyle(req, configfn, templatepath):
51 def getstyle(req, configfn, templatepath):
52 styles = (
52 styles = (
53 req.qsparams.get('style', None),
53 req.qsparams.get('style', None),
54 configfn('web', 'style'),
54 configfn('web', 'style'),
55 'paper',
55 'paper',
56 )
56 )
57 return styles, templater.stylemap(styles, templatepath)
57 return styles, templater.stylemap(styles, templatepath)
58
58
59 def makebreadcrumb(url, prefix=''):
59 def makebreadcrumb(url, prefix=''):
60 '''Return a 'URL breadcrumb' list
60 '''Return a 'URL breadcrumb' list
61
61
62 A 'URL breadcrumb' is a list of URL-name pairs,
62 A 'URL breadcrumb' is a list of URL-name pairs,
63 corresponding to each of the path items on a URL.
63 corresponding to each of the path items on a URL.
64 This can be used to create path navigation entries.
64 This can be used to create path navigation entries.
65 '''
65 '''
66 if url.endswith('/'):
66 if url.endswith('/'):
67 url = url[:-1]
67 url = url[:-1]
68 if prefix:
68 if prefix:
69 url = '/' + prefix + url
69 url = '/' + prefix + url
70 relpath = url
70 relpath = url
71 if relpath.startswith('/'):
71 if relpath.startswith('/'):
72 relpath = relpath[1:]
72 relpath = relpath[1:]
73
73
74 breadcrumb = []
74 breadcrumb = []
75 urlel = url
75 urlel = url
76 pathitems = [''] + relpath.split('/')
76 pathitems = [''] + relpath.split('/')
77 for pathel in reversed(pathitems):
77 for pathel in reversed(pathitems):
78 if not pathel or not urlel:
78 if not pathel or not urlel:
79 break
79 break
80 breadcrumb.append({'url': urlel, 'name': pathel})
80 breadcrumb.append({'url': urlel, 'name': pathel})
81 urlel = os.path.dirname(urlel)
81 urlel = os.path.dirname(urlel)
82 return reversed(breadcrumb)
82 return reversed(breadcrumb)
83
83
84 class requestcontext(object):
84 class requestcontext(object):
85 """Holds state/context for an individual request.
85 """Holds state/context for an individual request.
86
86
87 Servers can be multi-threaded. Holding state on the WSGI application
87 Servers can be multi-threaded. Holding state on the WSGI application
88 is prone to race conditions. Instances of this class exist to hold
88 is prone to race conditions. Instances of this class exist to hold
89 mutable and race-free state for requests.
89 mutable and race-free state for requests.
90 """
90 """
91 def __init__(self, app, repo, req, res):
91 def __init__(self, app, repo, req, res):
92 self.repo = repo
92 self.repo = repo
93 self.reponame = app.reponame
93 self.reponame = app.reponame
94 self.req = req
94 self.req = req
95 self.res = res
95 self.res = res
96
96
97 self.archivespecs = archivespecs
97 self.archivespecs = archivespecs
98
98
99 self.maxchanges = self.configint('web', 'maxchanges')
99 self.maxchanges = self.configint('web', 'maxchanges')
100 self.stripecount = self.configint('web', 'stripes')
100 self.stripecount = self.configint('web', 'stripes')
101 self.maxshortchanges = self.configint('web', 'maxshortchanges')
101 self.maxshortchanges = self.configint('web', 'maxshortchanges')
102 self.maxfiles = self.configint('web', 'maxfiles')
102 self.maxfiles = self.configint('web', 'maxfiles')
103 self.allowpull = self.configbool('web', 'allow-pull')
103 self.allowpull = self.configbool('web', 'allow-pull')
104
104
105 # we use untrusted=False to prevent a repo owner from using
105 # we use untrusted=False to prevent a repo owner from using
106 # web.templates in .hg/hgrc to get access to any file readable
106 # web.templates in .hg/hgrc to get access to any file readable
107 # by the user running the CGI script
107 # by the user running the CGI script
108 self.templatepath = self.config('web', 'templates', untrusted=False)
108 self.templatepath = self.config('web', 'templates', untrusted=False)
109
109
110 # This object is more expensive to build than simple config values.
110 # This object is more expensive to build than simple config values.
111 # It is shared across requests. The app will replace the object
111 # It is shared across requests. The app will replace the object
112 # if it is updated. Since this is a reference and nothing should
112 # if it is updated. Since this is a reference and nothing should
113 # modify the underlying object, it should be constant for the lifetime
113 # modify the underlying object, it should be constant for the lifetime
114 # of the request.
114 # of the request.
115 self.websubtable = app.websubtable
115 self.websubtable = app.websubtable
116
116
117 self.csp, self.nonce = cspvalues(self.repo.ui)
117 self.csp, self.nonce = cspvalues(self.repo.ui)
118
118
119 # Trust the settings from the .hg/hgrc files by default.
119 # Trust the settings from the .hg/hgrc files by default.
120 def config(self, section, name, default=uimod._unset, untrusted=True):
120 def config(self, section, name, default=uimod._unset, untrusted=True):
121 return self.repo.ui.config(section, name, default,
121 return self.repo.ui.config(section, name, default,
122 untrusted=untrusted)
122 untrusted=untrusted)
123
123
124 def configbool(self, section, name, default=uimod._unset, untrusted=True):
124 def configbool(self, section, name, default=uimod._unset, untrusted=True):
125 return self.repo.ui.configbool(section, name, default,
125 return self.repo.ui.configbool(section, name, default,
126 untrusted=untrusted)
126 untrusted=untrusted)
127
127
128 def configint(self, section, name, default=uimod._unset, untrusted=True):
128 def configint(self, section, name, default=uimod._unset, untrusted=True):
129 return self.repo.ui.configint(section, name, default,
129 return self.repo.ui.configint(section, name, default,
130 untrusted=untrusted)
130 untrusted=untrusted)
131
131
132 def configlist(self, section, name, default=uimod._unset, untrusted=True):
132 def configlist(self, section, name, default=uimod._unset, untrusted=True):
133 return self.repo.ui.configlist(section, name, default,
133 return self.repo.ui.configlist(section, name, default,
134 untrusted=untrusted)
134 untrusted=untrusted)
135
135
136 def archivelist(self, nodeid):
136 def archivelist(self, nodeid):
137 allowed = self.configlist('web', 'allow_archive')
137 allowed = self.configlist('web', 'allow_archive')
138 for typ, spec in self.archivespecs.iteritems():
138 for typ, spec in self.archivespecs.iteritems():
139 if typ in allowed or self.configbool('web', 'allow%s' % typ):
139 if typ in allowed or self.configbool('web', 'allow%s' % typ):
140 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
140 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
141
141
142 def templater(self, req):
142 def templater(self, req):
143 # determine scheme, port and server name
143 # determine scheme, port and server name
144 # this is needed to create absolute urls
144 # this is needed to create absolute urls
145 logourl = self.config('web', 'logourl')
145 logourl = self.config('web', 'logourl')
146 logoimg = self.config('web', 'logoimg')
146 logoimg = self.config('web', 'logoimg')
147 staticurl = (self.config('web', 'staticurl')
147 staticurl = (self.config('web', 'staticurl')
148 or req.apppath + '/static/')
148 or req.apppath + '/static/')
149 if not staticurl.endswith('/'):
149 if not staticurl.endswith('/'):
150 staticurl += '/'
150 staticurl += '/'
151
151
152 # some functions for the templater
152 # some functions for the templater
153
153
154 def motd(**map):
154 def motd(**map):
155 yield self.config('web', 'motd')
155 yield self.config('web', 'motd')
156
156
157 # figure out which style to use
157 # figure out which style to use
158
158
159 vars = {}
159 vars = {}
160 styles, (style, mapfile) = getstyle(req, self.config,
160 styles, (style, mapfile) = getstyle(req, self.config,
161 self.templatepath)
161 self.templatepath)
162 if style == styles[0]:
162 if style == styles[0]:
163 vars['style'] = style
163 vars['style'] = style
164
164
165 sessionvars = webutil.sessionvars(vars, '?')
165 sessionvars = webutil.sessionvars(vars, '?')
166
166
167 if not self.reponame:
167 if not self.reponame:
168 self.reponame = (self.config('web', 'name', '')
168 self.reponame = (self.config('web', 'name', '')
169 or req.reponame
169 or req.reponame
170 or req.apppath
170 or req.apppath
171 or self.repo.root)
171 or self.repo.root)
172
172
173 def websubfilter(text):
173 def websubfilter(text):
174 return templatefilters.websub(text, self.websubtable)
174 return templatefilters.websub(text, self.websubtable)
175
175
176 # create the templater
176 # create the templater
177 # TODO: export all keywords: defaults = templatekw.keywords.copy()
177 # TODO: export all keywords: defaults = templatekw.keywords.copy()
178 defaults = {
178 defaults = {
179 'url': req.apppath + '/',
179 'url': req.apppath + '/',
180 'logourl': logourl,
180 'logourl': logourl,
181 'logoimg': logoimg,
181 'logoimg': logoimg,
182 'staticurl': staticurl,
182 'staticurl': staticurl,
183 'urlbase': req.advertisedbaseurl,
183 'urlbase': req.advertisedbaseurl,
184 'repo': self.reponame,
184 'repo': self.reponame,
185 'encoding': encoding.encoding,
185 'encoding': encoding.encoding,
186 'motd': motd,
186 'motd': motd,
187 'sessionvars': sessionvars,
187 'sessionvars': sessionvars,
188 'pathdef': makebreadcrumb(req.apppath),
188 'pathdef': makebreadcrumb(req.apppath),
189 'style': style,
189 'style': style,
190 'nonce': self.nonce,
190 'nonce': self.nonce,
191 }
191 }
192 tres = formatter.templateresources(self.repo.ui, self.repo)
192 tres = formatter.templateresources(self.repo.ui, self.repo)
193 tmpl = templater.templater.frommapfile(mapfile,
193 tmpl = templater.templater.frommapfile(mapfile,
194 filters={'websub': websubfilter},
194 filters={'websub': websubfilter},
195 defaults=defaults,
195 defaults=defaults,
196 resources=tres)
196 resources=tres)
197 return tmpl
197 return tmpl
198
198
199 def sendtemplate(self, name, **kwargs):
199 def sendtemplate(self, name, **kwargs):
200 """Helper function to send a response generated from a template."""
200 """Helper function to send a response generated from a template."""
201 kwargs = pycompat.byteskwargs(kwargs)
201 kwargs = pycompat.byteskwargs(kwargs)
202 self.res.setbodygen(self.tmpl.generate(name, kwargs))
202 self.res.setbodygen(self.tmpl.generate(name, kwargs))
203 return self.res.sendresponse()
203 return self.res.sendresponse()
204
204
205 class hgweb(object):
205 class hgweb(object):
206 """HTTP server for individual repositories.
206 """HTTP server for individual repositories.
207
207
208 Instances of this class serve HTTP responses for a particular
208 Instances of this class serve HTTP responses for a particular
209 repository.
209 repository.
210
210
211 Instances are typically used as WSGI applications.
211 Instances are typically used as WSGI applications.
212
212
213 Some servers are multi-threaded. On these servers, there may
213 Some servers are multi-threaded. On these servers, there may
214 be multiple active threads inside __call__.
214 be multiple active threads inside __call__.
215 """
215 """
216 def __init__(self, repo, name=None, baseui=None):
216 def __init__(self, repo, name=None, baseui=None):
217 if isinstance(repo, str):
217 if isinstance(repo, str):
218 if baseui:
218 if baseui:
219 u = baseui.copy()
219 u = baseui.copy()
220 else:
220 else:
221 u = uimod.ui.load()
221 u = uimod.ui.load()
222 r = hg.repository(u, repo)
222 r = hg.repository(u, repo)
223 else:
223 else:
224 # we trust caller to give us a private copy
224 # we trust caller to give us a private copy
225 r = repo
225 r = repo
226
226
227 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
227 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
228 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
228 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
229 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
229 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
230 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
230 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
231 # resolve file patterns relative to repo root
231 # resolve file patterns relative to repo root
232 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
232 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
233 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
233 r.baseui.setconfig('ui', 'forcecwd', r.root, '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 handled = wireprotoserver.handlewsgirequest(
323 handled = wireprotoserver.handlewsgirequest(
324 rctx, req, res, self.check_perm)
324 rctx, req, res, self.check_perm)
325 if handled:
325 if handled:
326 return res.sendresponse()
326 return res.sendresponse()
327
327
328 # Old implementations of hgweb supported dispatching the request via
328 # Old implementations of hgweb supported dispatching the request via
329 # the initial query string parameter instead of using PATH_INFO.
329 # the initial query string parameter instead of using PATH_INFO.
330 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
330 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
331 # a value), we use it. Otherwise fall back to the query string.
331 # a value), we use it. Otherwise fall back to the query string.
332 if req.dispatchpath is not None:
332 if req.dispatchpath is not None:
333 query = req.dispatchpath
333 query = req.dispatchpath
334 else:
334 else:
335 query = req.querystring.partition('&')[0].partition(';')[0]
335 query = req.querystring.partition('&')[0].partition(';')[0]
336
336
337 # translate user-visible url structure to internal structure
337 # translate user-visible url structure to internal structure
338
338
339 args = query.split('/', 2)
339 args = query.split('/', 2)
340 if 'cmd' not in req.qsparams and args and args[0]:
340 if 'cmd' not in req.qsparams and args and args[0]:
341 cmd = args.pop(0)
341 cmd = args.pop(0)
342 style = cmd.rfind('-')
342 style = cmd.rfind('-')
343 if style != -1:
343 if style != -1:
344 req.qsparams['style'] = cmd[:style]
344 req.qsparams['style'] = cmd[:style]
345 cmd = cmd[style + 1:]
345 cmd = cmd[style + 1:]
346
346
347 # avoid accepting e.g. style parameter as command
347 # avoid accepting e.g. style parameter as command
348 if util.safehasattr(webcommands, cmd):
348 if util.safehasattr(webcommands, cmd):
349 req.qsparams['cmd'] = cmd
349 req.qsparams['cmd'] = cmd
350
350
351 if cmd == 'static':
351 if cmd == 'static':
352 req.qsparams['file'] = '/'.join(args)
352 req.qsparams['file'] = '/'.join(args)
353 else:
353 else:
354 if args and args[0]:
354 if args and args[0]:
355 node = args.pop(0).replace('%2F', '/')
355 node = args.pop(0).replace('%2F', '/')
356 req.qsparams['node'] = node
356 req.qsparams['node'] = node
357 if args:
357 if args:
358 if 'file' in req.qsparams:
358 if 'file' in req.qsparams:
359 del req.qsparams['file']
359 del req.qsparams['file']
360 for a in args:
360 for a in args:
361 req.qsparams.add('file', a)
361 req.qsparams.add('file', a)
362
362
363 ua = req.headers.get('User-Agent', '')
363 ua = req.headers.get('User-Agent', '')
364 if cmd == 'rev' and 'mercurial' in ua:
364 if cmd == 'rev' and 'mercurial' in ua:
365 req.qsparams['style'] = 'raw'
365 req.qsparams['style'] = 'raw'
366
366
367 if cmd == 'archive':
367 if cmd == 'archive':
368 fn = req.qsparams['node']
368 fn = req.qsparams['node']
369 for type_, spec in rctx.archivespecs.iteritems():
369 for type_, spec in rctx.archivespecs.iteritems():
370 ext = spec[2]
370 ext = spec[2]
371 if fn.endswith(ext):
371 if fn.endswith(ext):
372 req.qsparams['node'] = fn[:-len(ext)]
372 req.qsparams['node'] = fn[:-len(ext)]
373 req.qsparams['type'] = type_
373 req.qsparams['type'] = type_
374 else:
374 else:
375 cmd = req.qsparams.get('cmd', '')
375 cmd = req.qsparams.get('cmd', '')
376
376
377 # process the web interface request
377 # process the web interface request
378
378
379 try:
379 try:
380 rctx.tmpl = rctx.templater(req)
380 rctx.tmpl = rctx.templater(req)
381 ctype = rctx.tmpl.render('mimetype',
381 ctype = rctx.tmpl.render('mimetype',
382 {'encoding': encoding.encoding})
382 {'encoding': encoding.encoding})
383
383
384 # check read permissions non-static content
384 # check read permissions non-static content
385 if cmd != 'static':
385 if cmd != 'static':
386 self.check_perm(rctx, req, None)
386 self.check_perm(rctx, req, None)
387
387
388 if cmd == '':
388 if cmd == '':
389 req.qsparams['cmd'] = rctx.tmpl.cache['default']
389 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
390 cmd = req.qsparams['cmd']
390 cmd = req.qsparams['cmd']
391
391
392 # Don't enable caching if using a CSP nonce because then it wouldn't
392 # Don't enable caching if using a CSP nonce because then it wouldn't
393 # be a nonce.
393 # be a nonce.
394 if rctx.configbool('web', 'cache') and not rctx.nonce:
394 if rctx.configbool('web', 'cache') and not rctx.nonce:
395 tag = 'W/"%d"' % self.mtime
395 tag = 'W/"%d"' % self.mtime
396 if req.headers.get('If-None-Match') == tag:
396 if req.headers.get('If-None-Match') == tag:
397 res.status = '304 Not Modified'
397 res.status = '304 Not Modified'
398 # Response body not allowed on 304.
398 # Response body not allowed on 304.
399 res.setbodybytes('')
399 res.setbodybytes('')
400 return res.sendresponse()
400 return res.sendresponse()
401
401
402 res.headers['ETag'] = tag
402 res.headers['ETag'] = tag
403
403
404 if cmd not in webcommands.__all__:
404 if cmd not in webcommands.__all__:
405 msg = 'no such method: %s' % cmd
405 msg = 'no such method: %s' % cmd
406 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
406 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
407 else:
407 else:
408 # Set some globals appropriate for web handlers. Commands can
408 # Set some globals appropriate for web handlers. Commands can
409 # override easily enough.
409 # override easily enough.
410 res.status = '200 Script output follows'
410 res.status = '200 Script output follows'
411 res.headers['Content-Type'] = ctype
411 res.headers['Content-Type'] = ctype
412 return getattr(webcommands, cmd)(rctx)
412 return getattr(webcommands, cmd)(rctx)
413
413
414 except (error.LookupError, error.RepoLookupError) as err:
414 except (error.LookupError, error.RepoLookupError) as err:
415 msg = pycompat.bytestr(err)
415 msg = pycompat.bytestr(err)
416 if (util.safehasattr(err, 'name') and
416 if (util.safehasattr(err, 'name') and
417 not isinstance(err, error.ManifestLookupError)):
417 not isinstance(err, error.ManifestLookupError)):
418 msg = 'revision not found: %s' % err.name
418 msg = 'revision not found: %s' % err.name
419
419
420 res.status = '404 Not Found'
420 res.status = '404 Not Found'
421 res.headers['Content-Type'] = ctype
421 res.headers['Content-Type'] = ctype
422 return rctx.sendtemplate('error', error=msg)
422 return rctx.sendtemplate('error', error=msg)
423 except (error.RepoError, error.RevlogError) as e:
423 except (error.RepoError, error.RevlogError) as e:
424 res.status = '500 Internal Server Error'
424 res.status = '500 Internal Server Error'
425 res.headers['Content-Type'] = ctype
425 res.headers['Content-Type'] = ctype
426 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
426 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
427 except ErrorResponse as e:
427 except ErrorResponse as e:
428 res.status = statusmessage(e.code, pycompat.bytestr(e))
428 res.status = statusmessage(e.code, pycompat.bytestr(e))
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
431
432 def check_perm(self, rctx, req, op):
432 def check_perm(self, rctx, req, op):
433 for permhook in permhooks:
433 for permhook in permhooks:
434 permhook(rctx, req, op)
434 permhook(rctx, req, op)
435
435
436 def getwebview(repo):
436 def getwebview(repo):
437 """The 'web.view' config controls changeset filter to hgweb. Possible
437 """The 'web.view' config controls changeset filter to hgweb. Possible
438 values are ``served``, ``visible`` and ``all``. Default is ``served``.
438 values are ``served``, ``visible`` and ``all``. Default is ``served``.
439 The ``served`` filter only shows changesets that can be pulled from the
439 The ``served`` filter only shows changesets that can be pulled from the
440 hgweb instance. The``visible`` filter includes secret changesets but
440 hgweb instance. The``visible`` filter includes secret changesets but
441 still excludes "hidden" one.
441 still excludes "hidden" one.
442
442
443 See the repoview module for details.
443 See the repoview module for details.
444
444
445 The option has been around undocumented since Mercurial 2.5, but no
445 The option has been around undocumented since Mercurial 2.5, but no
446 user ever asked about it. So we better keep it undocumented for now."""
446 user ever asked about it. So we better keep it undocumented for now."""
447 # experimental config: web.view
447 # experimental config: web.view
448 viewconfig = repo.ui.config('web', 'view', untrusted=True)
448 viewconfig = repo.ui.config('web', 'view', untrusted=True)
449 if viewconfig == 'all':
449 if viewconfig == 'all':
450 return repo.unfiltered()
450 return repo.unfiltered()
451 elif viewconfig in repoview.filtertable:
451 elif viewconfig in repoview.filtertable:
452 return repo.filtered(viewconfig)
452 return repo.filtered(viewconfig)
453 else:
453 else:
454 return repo.filtered('served')
454 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now