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