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