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