##// END OF EJS Templates
hgweb: extract function for loading style from request context...
Augie Fackler -
r34516:8afc25e7 default
parent child Browse files
Show More
@@ -1,484 +1,492
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 repoview,
34 repoview,
34 templatefilters,
35 templatefilters,
35 templater,
36 templater,
36 ui as uimod,
37 ui as uimod,
37 util,
38 util,
38 )
39 )
39
40
40 from . import (
41 from . import (
41 protocol,
42 protocol,
42 webcommands,
43 webcommands,
43 webutil,
44 webutil,
44 wsgicgi,
45 wsgicgi,
45 )
46 )
46
47
47 perms = {
48 perms = {
48 'changegroup': 'pull',
49 'changegroup': 'pull',
49 'changegroupsubset': 'pull',
50 'changegroupsubset': 'pull',
50 'getbundle': 'pull',
51 'getbundle': 'pull',
51 'stream_out': 'pull',
52 'stream_out': 'pull',
52 'listkeys': 'pull',
53 'listkeys': 'pull',
53 'unbundle': 'push',
54 'unbundle': 'push',
54 'pushkey': 'push',
55 'pushkey': 'push',
55 }
56 }
56
57
57 archivespecs = util.sortdict((
58 archivespecs = util.sortdict((
58 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ))
62 ))
62
63
64 def getstyle(req, configfn, templatepath):
65 fromreq = req.form.get('style', [None])[0]
66 if fromreq is not None:
67 fromreq = pycompat.sysbytes(fromreq)
68 styles = (
69 fromreq,
70 configfn('web', 'style'),
71 'paper',
72 )
73 return styles, templater.stylemap(styles, templatepath)
74
63 def makebreadcrumb(url, prefix=''):
75 def makebreadcrumb(url, prefix=''):
64 '''Return a 'URL breadcrumb' list
76 '''Return a 'URL breadcrumb' list
65
77
66 A 'URL breadcrumb' is a list of URL-name pairs,
78 A 'URL breadcrumb' is a list of URL-name pairs,
67 corresponding to each of the path items on a URL.
79 corresponding to each of the path items on a URL.
68 This can be used to create path navigation entries.
80 This can be used to create path navigation entries.
69 '''
81 '''
70 if url.endswith('/'):
82 if url.endswith('/'):
71 url = url[:-1]
83 url = url[:-1]
72 if prefix:
84 if prefix:
73 url = '/' + prefix + url
85 url = '/' + prefix + url
74 relpath = url
86 relpath = url
75 if relpath.startswith('/'):
87 if relpath.startswith('/'):
76 relpath = relpath[1:]
88 relpath = relpath[1:]
77
89
78 breadcrumb = []
90 breadcrumb = []
79 urlel = url
91 urlel = url
80 pathitems = [''] + relpath.split('/')
92 pathitems = [''] + relpath.split('/')
81 for pathel in reversed(pathitems):
93 for pathel in reversed(pathitems):
82 if not pathel or not urlel:
94 if not pathel or not urlel:
83 break
95 break
84 breadcrumb.append({'url': urlel, 'name': pathel})
96 breadcrumb.append({'url': urlel, 'name': pathel})
85 urlel = os.path.dirname(urlel)
97 urlel = os.path.dirname(urlel)
86 return reversed(breadcrumb)
98 return reversed(breadcrumb)
87
99
88 class requestcontext(object):
100 class requestcontext(object):
89 """Holds state/context for an individual request.
101 """Holds state/context for an individual request.
90
102
91 Servers can be multi-threaded. Holding state on the WSGI application
103 Servers can be multi-threaded. Holding state on the WSGI application
92 is prone to race conditions. Instances of this class exist to hold
104 is prone to race conditions. Instances of this class exist to hold
93 mutable and race-free state for requests.
105 mutable and race-free state for requests.
94 """
106 """
95 def __init__(self, app, repo):
107 def __init__(self, app, repo):
96 self.repo = repo
108 self.repo = repo
97 self.reponame = app.reponame
109 self.reponame = app.reponame
98
110
99 self.archivespecs = archivespecs
111 self.archivespecs = archivespecs
100
112
101 self.maxchanges = self.configint('web', 'maxchanges', 10)
113 self.maxchanges = self.configint('web', 'maxchanges', 10)
102 self.stripecount = self.configint('web', 'stripes')
114 self.stripecount = self.configint('web', 'stripes')
103 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
115 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
104 self.maxfiles = self.configint('web', 'maxfiles', 10)
116 self.maxfiles = self.configint('web', 'maxfiles', 10)
105 self.allowpull = self.configbool('web', 'allowpull', True)
117 self.allowpull = self.configbool('web', 'allowpull', True)
106
118
107 # we use untrusted=False to prevent a repo owner from using
119 # we use untrusted=False to prevent a repo owner from using
108 # web.templates in .hg/hgrc to get access to any file readable
120 # web.templates in .hg/hgrc to get access to any file readable
109 # by the user running the CGI script
121 # by the user running the CGI script
110 self.templatepath = self.config('web', 'templates', untrusted=False)
122 self.templatepath = self.config('web', 'templates', untrusted=False)
111
123
112 # This object is more expensive to build than simple config values.
124 # This object is more expensive to build than simple config values.
113 # It is shared across requests. The app will replace the object
125 # It is shared across requests. The app will replace the object
114 # if it is updated. Since this is a reference and nothing should
126 # if it is updated. Since this is a reference and nothing should
115 # modify the underlying object, it should be constant for the lifetime
127 # modify the underlying object, it should be constant for the lifetime
116 # of the request.
128 # of the request.
117 self.websubtable = app.websubtable
129 self.websubtable = app.websubtable
118
130
119 self.csp, self.nonce = cspvalues(self.repo.ui)
131 self.csp, self.nonce = cspvalues(self.repo.ui)
120
132
121 # Trust the settings from the .hg/hgrc files by default.
133 # Trust the settings from the .hg/hgrc files by default.
122 def config(self, section, name, default=uimod._unset, untrusted=True):
134 def config(self, section, name, default=uimod._unset, untrusted=True):
123 return self.repo.ui.config(section, name, default,
135 return self.repo.ui.config(section, name, default,
124 untrusted=untrusted)
136 untrusted=untrusted)
125
137
126 def configbool(self, section, name, default=uimod._unset, untrusted=True):
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 return self.repo.ui.configbool(section, name, default,
139 return self.repo.ui.configbool(section, name, default,
128 untrusted=untrusted)
140 untrusted=untrusted)
129
141
130 def configint(self, section, name, default=uimod._unset, untrusted=True):
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 return self.repo.ui.configint(section, name, default,
143 return self.repo.ui.configint(section, name, default,
132 untrusted=untrusted)
144 untrusted=untrusted)
133
145
134 def configlist(self, section, name, default=uimod._unset, untrusted=True):
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 return self.repo.ui.configlist(section, name, default,
147 return self.repo.ui.configlist(section, name, default,
136 untrusted=untrusted)
148 untrusted=untrusted)
137
149
138 def archivelist(self, nodeid):
150 def archivelist(self, nodeid):
139 allowed = self.configlist('web', 'allow_archive')
151 allowed = self.configlist('web', 'allow_archive')
140 for typ, spec in self.archivespecs.iteritems():
152 for typ, spec in self.archivespecs.iteritems():
141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143
155
144 def templater(self, req):
156 def templater(self, req):
145 # determine scheme, port and server name
157 # determine scheme, port and server name
146 # this is needed to create absolute urls
158 # this is needed to create absolute urls
147
159
148 proto = req.env.get('wsgi.url_scheme')
160 proto = req.env.get('wsgi.url_scheme')
149 if proto == 'https':
161 if proto == 'https':
150 proto = 'https'
162 proto = 'https'
151 default_port = '443'
163 default_port = '443'
152 else:
164 else:
153 proto = 'http'
165 proto = 'http'
154 default_port = '80'
166 default_port = '80'
155
167
156 port = req.env['SERVER_PORT']
168 port = req.env['SERVER_PORT']
157 port = port != default_port and (':' + port) or ''
169 port = port != default_port and (':' + port) or ''
158 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
170 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
159 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
171 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
160 logoimg = self.config('web', 'logoimg', 'hglogo.png')
172 logoimg = self.config('web', 'logoimg', 'hglogo.png')
161 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
162 if not staticurl.endswith('/'):
174 if not staticurl.endswith('/'):
163 staticurl += '/'
175 staticurl += '/'
164
176
165 # some functions for the templater
177 # some functions for the templater
166
178
167 def motd(**map):
179 def motd(**map):
168 yield self.config('web', 'motd', '')
180 yield self.config('web', 'motd', '')
169
181
170 # figure out which style to use
182 # figure out which style to use
171
183
172 vars = {}
184 vars = {}
173 styles = (
185 styles, (style, mapfile) = getstyle(req, self.config,
174 req.form.get('style', [None])[0],
186 self.templatepath)
175 self.config('web', 'style'),
176 'paper',
177 )
178 style, mapfile = templater.stylemap(styles, self.templatepath)
179 if style == styles[0]:
187 if style == styles[0]:
180 vars['style'] = style
188 vars['style'] = style
181
189
182 start = req.url[-1] == '?' and '&' or '?'
190 start = req.url[-1] == '?' and '&' or '?'
183 sessionvars = webutil.sessionvars(vars, start)
191 sessionvars = webutil.sessionvars(vars, start)
184
192
185 if not self.reponame:
193 if not self.reponame:
186 self.reponame = (self.config('web', 'name')
194 self.reponame = (self.config('web', 'name')
187 or req.env.get('REPO_NAME')
195 or req.env.get('REPO_NAME')
188 or req.url.strip('/') or self.repo.root)
196 or req.url.strip('/') or self.repo.root)
189
197
190 def websubfilter(text):
198 def websubfilter(text):
191 return templatefilters.websub(text, self.websubtable)
199 return templatefilters.websub(text, self.websubtable)
192
200
193 # create the templater
201 # create the templater
194
202
195 defaults = {
203 defaults = {
196 'url': req.url,
204 'url': req.url,
197 'logourl': logourl,
205 'logourl': logourl,
198 'logoimg': logoimg,
206 'logoimg': logoimg,
199 'staticurl': staticurl,
207 'staticurl': staticurl,
200 'urlbase': urlbase,
208 'urlbase': urlbase,
201 'repo': self.reponame,
209 'repo': self.reponame,
202 'encoding': encoding.encoding,
210 'encoding': encoding.encoding,
203 'motd': motd,
211 'motd': motd,
204 'sessionvars': sessionvars,
212 'sessionvars': sessionvars,
205 'pathdef': makebreadcrumb(req.url),
213 'pathdef': makebreadcrumb(req.url),
206 'style': style,
214 'style': style,
207 'nonce': self.nonce,
215 'nonce': self.nonce,
208 }
216 }
209 tmpl = templater.templater.frommapfile(mapfile,
217 tmpl = templater.templater.frommapfile(mapfile,
210 filters={'websub': websubfilter},
218 filters={'websub': websubfilter},
211 defaults=defaults)
219 defaults=defaults)
212 return tmpl
220 return tmpl
213
221
214
222
215 class hgweb(object):
223 class hgweb(object):
216 """HTTP server for individual repositories.
224 """HTTP server for individual repositories.
217
225
218 Instances of this class serve HTTP responses for a particular
226 Instances of this class serve HTTP responses for a particular
219 repository.
227 repository.
220
228
221 Instances are typically used as WSGI applications.
229 Instances are typically used as WSGI applications.
222
230
223 Some servers are multi-threaded. On these servers, there may
231 Some servers are multi-threaded. On these servers, there may
224 be multiple active threads inside __call__.
232 be multiple active threads inside __call__.
225 """
233 """
226 def __init__(self, repo, name=None, baseui=None):
234 def __init__(self, repo, name=None, baseui=None):
227 if isinstance(repo, str):
235 if isinstance(repo, str):
228 if baseui:
236 if baseui:
229 u = baseui.copy()
237 u = baseui.copy()
230 else:
238 else:
231 u = uimod.ui.load()
239 u = uimod.ui.load()
232 r = hg.repository(u, repo)
240 r = hg.repository(u, repo)
233 else:
241 else:
234 # we trust caller to give us a private copy
242 # we trust caller to give us a private copy
235 r = repo
243 r = repo
236
244
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
241 # resolve file patterns relative to repo root
249 # resolve file patterns relative to repo root
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
244 # displaying bundling progress bar while serving feel wrong and may
252 # displaying bundling progress bar while serving feel wrong and may
245 # break some wsgi implementation.
253 # break some wsgi implementation.
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
249 self._lastrepo = self._repos[0]
257 self._lastrepo = self._repos[0]
250 hook.redirect(True)
258 hook.redirect(True)
251 self.reponame = name
259 self.reponame = name
252
260
253 def _webifyrepo(self, repo):
261 def _webifyrepo(self, repo):
254 repo = getwebview(repo)
262 repo = getwebview(repo)
255 self.websubtable = webutil.getwebsubs(repo)
263 self.websubtable = webutil.getwebsubs(repo)
256 return repo
264 return repo
257
265
258 @contextlib.contextmanager
266 @contextlib.contextmanager
259 def _obtainrepo(self):
267 def _obtainrepo(self):
260 """Obtain a repo unique to the caller.
268 """Obtain a repo unique to the caller.
261
269
262 Internally we maintain a stack of cachedlocalrepo instances
270 Internally we maintain a stack of cachedlocalrepo instances
263 to be handed out. If one is available, we pop it and return it,
271 to be handed out. If one is available, we pop it and return it,
264 ensuring it is up to date in the process. If one is not available,
272 ensuring it is up to date in the process. If one is not available,
265 we clone the most recently used repo instance and return it.
273 we clone the most recently used repo instance and return it.
266
274
267 It is currently possible for the stack to grow without bounds
275 It is currently possible for the stack to grow without bounds
268 if the server allows infinite threads. However, servers should
276 if the server allows infinite threads. However, servers should
269 have a thread limit, thus establishing our limit.
277 have a thread limit, thus establishing our limit.
270 """
278 """
271 if self._repos:
279 if self._repos:
272 cached = self._repos.pop()
280 cached = self._repos.pop()
273 r, created = cached.fetch()
281 r, created = cached.fetch()
274 else:
282 else:
275 cached = self._lastrepo.copy()
283 cached = self._lastrepo.copy()
276 r, created = cached.fetch()
284 r, created = cached.fetch()
277 if created:
285 if created:
278 r = self._webifyrepo(r)
286 r = self._webifyrepo(r)
279
287
280 self._lastrepo = cached
288 self._lastrepo = cached
281 self.mtime = cached.mtime
289 self.mtime = cached.mtime
282 try:
290 try:
283 yield r
291 yield r
284 finally:
292 finally:
285 self._repos.append(cached)
293 self._repos.append(cached)
286
294
287 def run(self):
295 def run(self):
288 """Start a server from CGI environment.
296 """Start a server from CGI environment.
289
297
290 Modern servers should be using WSGI and should avoid this
298 Modern servers should be using WSGI and should avoid this
291 method, if possible.
299 method, if possible.
292 """
300 """
293 if not encoding.environ.get('GATEWAY_INTERFACE',
301 if not encoding.environ.get('GATEWAY_INTERFACE',
294 '').startswith("CGI/1."):
302 '').startswith("CGI/1."):
295 raise RuntimeError("This function is only intended to be "
303 raise RuntimeError("This function is only intended to be "
296 "called while running as a CGI script.")
304 "called while running as a CGI script.")
297 wsgicgi.launch(self)
305 wsgicgi.launch(self)
298
306
299 def __call__(self, env, respond):
307 def __call__(self, env, respond):
300 """Run the WSGI application.
308 """Run the WSGI application.
301
309
302 This may be called by multiple threads.
310 This may be called by multiple threads.
303 """
311 """
304 req = wsgirequest(env, respond)
312 req = wsgirequest(env, respond)
305 return self.run_wsgi(req)
313 return self.run_wsgi(req)
306
314
307 def run_wsgi(self, req):
315 def run_wsgi(self, req):
308 """Internal method to run the WSGI application.
316 """Internal method to run the WSGI application.
309
317
310 This is typically only called by Mercurial. External consumers
318 This is typically only called by Mercurial. External consumers
311 should be using instances of this class as the WSGI application.
319 should be using instances of this class as the WSGI application.
312 """
320 """
313 with self._obtainrepo() as repo:
321 with self._obtainrepo() as repo:
314 profile = repo.ui.configbool('profiling', 'enabled')
322 profile = repo.ui.configbool('profiling', 'enabled')
315 with profiling.profile(repo.ui, enabled=profile):
323 with profiling.profile(repo.ui, enabled=profile):
316 for r in self._runwsgi(req, repo):
324 for r in self._runwsgi(req, repo):
317 yield r
325 yield r
318
326
319 def _runwsgi(self, req, repo):
327 def _runwsgi(self, req, repo):
320 rctx = requestcontext(self, repo)
328 rctx = requestcontext(self, repo)
321
329
322 # This state is global across all threads.
330 # This state is global across all threads.
323 encoding.encoding = rctx.config('web', 'encoding')
331 encoding.encoding = rctx.config('web', 'encoding')
324 rctx.repo.ui.environ = req.env
332 rctx.repo.ui.environ = req.env
325
333
326 if rctx.csp:
334 if rctx.csp:
327 # hgwebdir may have added CSP header. Since we generate our own,
335 # hgwebdir may have added CSP header. Since we generate our own,
328 # replace it.
336 # replace it.
329 req.headers = [h for h in req.headers
337 req.headers = [h for h in req.headers
330 if h[0] != 'Content-Security-Policy']
338 if h[0] != 'Content-Security-Policy']
331 req.headers.append(('Content-Security-Policy', rctx.csp))
339 req.headers.append(('Content-Security-Policy', rctx.csp))
332
340
333 # work with CGI variables to create coherent structure
341 # work with CGI variables to create coherent structure
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
335
343
336 req.url = req.env['SCRIPT_NAME']
344 req.url = req.env['SCRIPT_NAME']
337 if not req.url.endswith('/'):
345 if not req.url.endswith('/'):
338 req.url += '/'
346 req.url += '/'
339 if req.env.get('REPO_NAME'):
347 if req.env.get('REPO_NAME'):
340 req.url += req.env['REPO_NAME'] + '/'
348 req.url += req.env['REPO_NAME'] + '/'
341
349
342 if 'PATH_INFO' in req.env:
350 if 'PATH_INFO' in req.env:
343 parts = req.env['PATH_INFO'].strip('/').split('/')
351 parts = req.env['PATH_INFO'].strip('/').split('/')
344 repo_parts = req.env.get('REPO_NAME', '').split('/')
352 repo_parts = req.env.get('REPO_NAME', '').split('/')
345 if parts[:len(repo_parts)] == repo_parts:
353 if parts[:len(repo_parts)] == repo_parts:
346 parts = parts[len(repo_parts):]
354 parts = parts[len(repo_parts):]
347 query = '/'.join(parts)
355 query = '/'.join(parts)
348 else:
356 else:
349 query = req.env['QUERY_STRING'].partition('&')[0]
357 query = req.env['QUERY_STRING'].partition('&')[0]
350 query = query.partition(';')[0]
358 query = query.partition(';')[0]
351
359
352 # process this if it's a protocol request
360 # process this if it's a protocol request
353 # protocol bits don't need to create any URLs
361 # protocol bits don't need to create any URLs
354 # and the clients always use the old URL structure
362 # and the clients always use the old URL structure
355
363
356 cmd = req.form.get('cmd', [''])[0]
364 cmd = req.form.get('cmd', [''])[0]
357 if protocol.iscmd(cmd):
365 if protocol.iscmd(cmd):
358 try:
366 try:
359 if query:
367 if query:
360 raise ErrorResponse(HTTP_NOT_FOUND)
368 raise ErrorResponse(HTTP_NOT_FOUND)
361 if cmd in perms:
369 if cmd in perms:
362 self.check_perm(rctx, req, perms[cmd])
370 self.check_perm(rctx, req, perms[cmd])
363 return protocol.call(rctx.repo, req, cmd)
371 return protocol.call(rctx.repo, req, cmd)
364 except ErrorResponse as inst:
372 except ErrorResponse as inst:
365 # A client that sends unbundle without 100-continue will
373 # A client that sends unbundle without 100-continue will
366 # break if we respond early.
374 # break if we respond early.
367 if (cmd == 'unbundle' and
375 if (cmd == 'unbundle' and
368 (req.env.get('HTTP_EXPECT',
376 (req.env.get('HTTP_EXPECT',
369 '').lower() != '100-continue') or
377 '').lower() != '100-continue') or
370 req.env.get('X-HgHttp2', '')):
378 req.env.get('X-HgHttp2', '')):
371 req.drain()
379 req.drain()
372 else:
380 else:
373 req.headers.append(('Connection', 'Close'))
381 req.headers.append(('Connection', 'Close'))
374 req.respond(inst, protocol.HGTYPE,
382 req.respond(inst, protocol.HGTYPE,
375 body='0\n%s\n' % inst)
383 body='0\n%s\n' % inst)
376 return ''
384 return ''
377
385
378 # translate user-visible url structure to internal structure
386 # translate user-visible url structure to internal structure
379
387
380 args = query.split('/', 2)
388 args = query.split('/', 2)
381 if 'cmd' not in req.form and args and args[0]:
389 if 'cmd' not in req.form and args and args[0]:
382
390
383 cmd = args.pop(0)
391 cmd = args.pop(0)
384 style = cmd.rfind('-')
392 style = cmd.rfind('-')
385 if style != -1:
393 if style != -1:
386 req.form['style'] = [cmd[:style]]
394 req.form['style'] = [cmd[:style]]
387 cmd = cmd[style + 1:]
395 cmd = cmd[style + 1:]
388
396
389 # avoid accepting e.g. style parameter as command
397 # avoid accepting e.g. style parameter as command
390 if util.safehasattr(webcommands, cmd):
398 if util.safehasattr(webcommands, cmd):
391 req.form['cmd'] = [cmd]
399 req.form['cmd'] = [cmd]
392
400
393 if cmd == 'static':
401 if cmd == 'static':
394 req.form['file'] = ['/'.join(args)]
402 req.form['file'] = ['/'.join(args)]
395 else:
403 else:
396 if args and args[0]:
404 if args and args[0]:
397 node = args.pop(0).replace('%2F', '/')
405 node = args.pop(0).replace('%2F', '/')
398 req.form['node'] = [node]
406 req.form['node'] = [node]
399 if args:
407 if args:
400 req.form['file'] = args
408 req.form['file'] = args
401
409
402 ua = req.env.get('HTTP_USER_AGENT', '')
410 ua = req.env.get('HTTP_USER_AGENT', '')
403 if cmd == 'rev' and 'mercurial' in ua:
411 if cmd == 'rev' and 'mercurial' in ua:
404 req.form['style'] = ['raw']
412 req.form['style'] = ['raw']
405
413
406 if cmd == 'archive':
414 if cmd == 'archive':
407 fn = req.form['node'][0]
415 fn = req.form['node'][0]
408 for type_, spec in rctx.archivespecs.iteritems():
416 for type_, spec in rctx.archivespecs.iteritems():
409 ext = spec[2]
417 ext = spec[2]
410 if fn.endswith(ext):
418 if fn.endswith(ext):
411 req.form['node'] = [fn[:-len(ext)]]
419 req.form['node'] = [fn[:-len(ext)]]
412 req.form['type'] = [type_]
420 req.form['type'] = [type_]
413
421
414 # process the web interface request
422 # process the web interface request
415
423
416 try:
424 try:
417 tmpl = rctx.templater(req)
425 tmpl = rctx.templater(req)
418 ctype = tmpl('mimetype', encoding=encoding.encoding)
426 ctype = tmpl('mimetype', encoding=encoding.encoding)
419 ctype = templater.stringify(ctype)
427 ctype = templater.stringify(ctype)
420
428
421 # check read permissions non-static content
429 # check read permissions non-static content
422 if cmd != 'static':
430 if cmd != 'static':
423 self.check_perm(rctx, req, None)
431 self.check_perm(rctx, req, None)
424
432
425 if cmd == '':
433 if cmd == '':
426 req.form['cmd'] = [tmpl.cache['default']]
434 req.form['cmd'] = [tmpl.cache['default']]
427 cmd = req.form['cmd'][0]
435 cmd = req.form['cmd'][0]
428
436
429 # Don't enable caching if using a CSP nonce because then it wouldn't
437 # Don't enable caching if using a CSP nonce because then it wouldn't
430 # be a nonce.
438 # be a nonce.
431 if rctx.configbool('web', 'cache', True) and not rctx.nonce:
439 if rctx.configbool('web', 'cache', True) and not rctx.nonce:
432 caching(self, req) # sets ETag header or raises NOT_MODIFIED
440 caching(self, req) # sets ETag header or raises NOT_MODIFIED
433 if cmd not in webcommands.__all__:
441 if cmd not in webcommands.__all__:
434 msg = 'no such method: %s' % cmd
442 msg = 'no such method: %s' % cmd
435 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
443 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
436 elif cmd == 'file' and 'raw' in req.form.get('style', []):
444 elif cmd == 'file' and 'raw' in req.form.get('style', []):
437 rctx.ctype = ctype
445 rctx.ctype = ctype
438 content = webcommands.rawfile(rctx, req, tmpl)
446 content = webcommands.rawfile(rctx, req, tmpl)
439 else:
447 else:
440 content = getattr(webcommands, cmd)(rctx, req, tmpl)
448 content = getattr(webcommands, cmd)(rctx, req, tmpl)
441 req.respond(HTTP_OK, ctype)
449 req.respond(HTTP_OK, ctype)
442
450
443 return content
451 return content
444
452
445 except (error.LookupError, error.RepoLookupError) as err:
453 except (error.LookupError, error.RepoLookupError) as err:
446 req.respond(HTTP_NOT_FOUND, ctype)
454 req.respond(HTTP_NOT_FOUND, ctype)
447 msg = str(err)
455 msg = str(err)
448 if (util.safehasattr(err, 'name') and
456 if (util.safehasattr(err, 'name') and
449 not isinstance(err, error.ManifestLookupError)):
457 not isinstance(err, error.ManifestLookupError)):
450 msg = 'revision not found: %s' % err.name
458 msg = 'revision not found: %s' % err.name
451 return tmpl('error', error=msg)
459 return tmpl('error', error=msg)
452 except (error.RepoError, error.RevlogError) as inst:
460 except (error.RepoError, error.RevlogError) as inst:
453 req.respond(HTTP_SERVER_ERROR, ctype)
461 req.respond(HTTP_SERVER_ERROR, ctype)
454 return tmpl('error', error=str(inst))
462 return tmpl('error', error=str(inst))
455 except ErrorResponse as inst:
463 except ErrorResponse as inst:
456 req.respond(inst, ctype)
464 req.respond(inst, ctype)
457 if inst.code == HTTP_NOT_MODIFIED:
465 if inst.code == HTTP_NOT_MODIFIED:
458 # Not allowed to return a body on a 304
466 # Not allowed to return a body on a 304
459 return ['']
467 return ['']
460 return tmpl('error', error=str(inst))
468 return tmpl('error', error=str(inst))
461
469
462 def check_perm(self, rctx, req, op):
470 def check_perm(self, rctx, req, op):
463 for permhook in permhooks:
471 for permhook in permhooks:
464 permhook(rctx, req, op)
472 permhook(rctx, req, op)
465
473
466 def getwebview(repo):
474 def getwebview(repo):
467 """The 'web.view' config controls changeset filter to hgweb. Possible
475 """The 'web.view' config controls changeset filter to hgweb. Possible
468 values are ``served``, ``visible`` and ``all``. Default is ``served``.
476 values are ``served``, ``visible`` and ``all``. Default is ``served``.
469 The ``served`` filter only shows changesets that can be pulled from the
477 The ``served`` filter only shows changesets that can be pulled from the
470 hgweb instance. The``visible`` filter includes secret changesets but
478 hgweb instance. The``visible`` filter includes secret changesets but
471 still excludes "hidden" one.
479 still excludes "hidden" one.
472
480
473 See the repoview module for details.
481 See the repoview module for details.
474
482
475 The option has been around undocumented since Mercurial 2.5, but no
483 The option has been around undocumented since Mercurial 2.5, but no
476 user ever asked about it. So we better keep it undocumented for now."""
484 user ever asked about it. So we better keep it undocumented for now."""
477 viewconfig = repo.ui.config('web', 'view', 'served',
485 viewconfig = repo.ui.config('web', 'view', 'served',
478 untrusted=True)
486 untrusted=True)
479 if viewconfig == 'all':
487 if viewconfig == 'all':
480 return repo.unfiltered()
488 return repo.unfiltered()
481 elif viewconfig in repoview.filtertable:
489 elif viewconfig in repoview.filtertable:
482 return repo.filtered(viewconfig)
490 return repo.filtered(viewconfig)
483 else:
491 else:
484 return repo.filtered('served')
492 return repo.filtered('served')
@@ -1,542 +1,538
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 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 os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 cspvalues,
23 get_contact,
23 get_contact,
24 get_mtime,
24 get_mtime,
25 ismember,
25 ismember,
26 paritygen,
26 paritygen,
27 staticfile,
27 staticfile,
28 )
28 )
29 from .request import wsgirequest
29 from .request import wsgirequest
30
30
31 from .. import (
31 from .. import (
32 configitems,
32 configitems,
33 encoding,
33 encoding,
34 error,
34 error,
35 hg,
35 hg,
36 profiling,
36 profiling,
37 pycompat,
37 pycompat,
38 scmutil,
38 scmutil,
39 templater,
39 templater,
40 ui as uimod,
40 ui as uimod,
41 util,
41 util,
42 )
42 )
43
43
44 from . import (
44 from . import (
45 hgweb_mod,
45 hgweb_mod,
46 webutil,
46 webutil,
47 wsgicgi,
47 wsgicgi,
48 )
48 )
49
49
50 def cleannames(items):
50 def cleannames(items):
51 return [(util.pconvert(name).strip('/'), path) for name, path in items]
51 return [(util.pconvert(name).strip('/'), path) for name, path in items]
52
52
53 def findrepos(paths):
53 def findrepos(paths):
54 repos = []
54 repos = []
55 for prefix, root in cleannames(paths):
55 for prefix, root in cleannames(paths):
56 roothead, roottail = os.path.split(root)
56 roothead, roottail = os.path.split(root)
57 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
57 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
58 # /bar/ be served as as foo/N .
58 # /bar/ be served as as foo/N .
59 # '*' will not search inside dirs with .hg (except .hg/patches),
59 # '*' will not search inside dirs with .hg (except .hg/patches),
60 # '**' will search inside dirs with .hg (and thus also find subrepos).
60 # '**' will search inside dirs with .hg (and thus also find subrepos).
61 try:
61 try:
62 recurse = {'*': False, '**': True}[roottail]
62 recurse = {'*': False, '**': True}[roottail]
63 except KeyError:
63 except KeyError:
64 repos.append((prefix, root))
64 repos.append((prefix, root))
65 continue
65 continue
66 roothead = os.path.normpath(os.path.abspath(roothead))
66 roothead = os.path.normpath(os.path.abspath(roothead))
67 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
67 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
68 repos.extend(urlrepos(prefix, roothead, paths))
68 repos.extend(urlrepos(prefix, roothead, paths))
69 return repos
69 return repos
70
70
71 def urlrepos(prefix, roothead, paths):
71 def urlrepos(prefix, roothead, paths):
72 """yield url paths and filesystem paths from a list of repo paths
72 """yield url paths and filesystem paths from a list of repo paths
73
73
74 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
74 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
75 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
75 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
76 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
77 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
78 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
79 """
79 """
80 for path in paths:
80 for path in paths:
81 path = os.path.normpath(path)
81 path = os.path.normpath(path)
82 yield (prefix + '/' +
82 yield (prefix + '/' +
83 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
83 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
84
84
85 def geturlcgivars(baseurl, port):
85 def geturlcgivars(baseurl, port):
86 """
86 """
87 Extract CGI variables from baseurl
87 Extract CGI variables from baseurl
88
88
89 >>> geturlcgivars(b"http://host.org/base", b"80")
89 >>> geturlcgivars(b"http://host.org/base", b"80")
90 ('host.org', '80', '/base')
90 ('host.org', '80', '/base')
91 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
91 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
92 ('host.org', '8000', '/base')
92 ('host.org', '8000', '/base')
93 >>> geturlcgivars(b'/base', 8000)
93 >>> geturlcgivars(b'/base', 8000)
94 ('', '8000', '/base')
94 ('', '8000', '/base')
95 >>> geturlcgivars(b"base", b'8000')
95 >>> geturlcgivars(b"base", b'8000')
96 ('', '8000', '/base')
96 ('', '8000', '/base')
97 >>> geturlcgivars(b"http://host", b'8000')
97 >>> geturlcgivars(b"http://host", b'8000')
98 ('host', '8000', '/')
98 ('host', '8000', '/')
99 >>> geturlcgivars(b"http://host/", b'8000')
99 >>> geturlcgivars(b"http://host/", b'8000')
100 ('host', '8000', '/')
100 ('host', '8000', '/')
101 """
101 """
102 u = util.url(baseurl)
102 u = util.url(baseurl)
103 name = u.host or ''
103 name = u.host or ''
104 if u.port:
104 if u.port:
105 port = u.port
105 port = u.port
106 path = u.path or ""
106 path = u.path or ""
107 if not path.startswith('/'):
107 if not path.startswith('/'):
108 path = '/' + path
108 path = '/' + path
109
109
110 return name, pycompat.bytestr(port), path
110 return name, pycompat.bytestr(port), path
111
111
112 class hgwebdir(object):
112 class hgwebdir(object):
113 """HTTP server for multiple repositories.
113 """HTTP server for multiple repositories.
114
114
115 Given a configuration, different repositories will be served depending
115 Given a configuration, different repositories will be served depending
116 on the request path.
116 on the request path.
117
117
118 Instances are typically used as WSGI applications.
118 Instances are typically used as WSGI applications.
119 """
119 """
120 def __init__(self, conf, baseui=None):
120 def __init__(self, conf, baseui=None):
121 self.conf = conf
121 self.conf = conf
122 self.baseui = baseui
122 self.baseui = baseui
123 self.ui = None
123 self.ui = None
124 self.lastrefresh = 0
124 self.lastrefresh = 0
125 self.motd = None
125 self.motd = None
126 self.refresh()
126 self.refresh()
127
127
128 def refresh(self):
128 def refresh(self):
129 if self.ui:
129 if self.ui:
130 refreshinterval = self.ui.configint('web', 'refreshinterval')
130 refreshinterval = self.ui.configint('web', 'refreshinterval')
131 else:
131 else:
132 item = configitems.coreitems['web']['refreshinterval']
132 item = configitems.coreitems['web']['refreshinterval']
133 refreshinterval = item.default
133 refreshinterval = item.default
134
134
135 # refreshinterval <= 0 means to always refresh.
135 # refreshinterval <= 0 means to always refresh.
136 if (refreshinterval > 0 and
136 if (refreshinterval > 0 and
137 self.lastrefresh + refreshinterval > time.time()):
137 self.lastrefresh + refreshinterval > time.time()):
138 return
138 return
139
139
140 if self.baseui:
140 if self.baseui:
141 u = self.baseui.copy()
141 u = self.baseui.copy()
142 else:
142 else:
143 u = uimod.ui.load()
143 u = uimod.ui.load()
144 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
144 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
145 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
145 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
146 # displaying bundling progress bar while serving feels wrong and may
146 # displaying bundling progress bar while serving feels wrong and may
147 # break some wsgi implementations.
147 # break some wsgi implementations.
148 u.setconfig('progress', 'disable', 'true', 'hgweb')
148 u.setconfig('progress', 'disable', 'true', 'hgweb')
149
149
150 if not isinstance(self.conf, (dict, list, tuple)):
150 if not isinstance(self.conf, (dict, list, tuple)):
151 map = {'paths': 'hgweb-paths'}
151 map = {'paths': 'hgweb-paths'}
152 if not os.path.exists(self.conf):
152 if not os.path.exists(self.conf):
153 raise error.Abort(_('config file %s not found!') % self.conf)
153 raise error.Abort(_('config file %s not found!') % self.conf)
154 u.readconfig(self.conf, remap=map, trust=True)
154 u.readconfig(self.conf, remap=map, trust=True)
155 paths = []
155 paths = []
156 for name, ignored in u.configitems('hgweb-paths'):
156 for name, ignored in u.configitems('hgweb-paths'):
157 for path in u.configlist('hgweb-paths', name):
157 for path in u.configlist('hgweb-paths', name):
158 paths.append((name, path))
158 paths.append((name, path))
159 elif isinstance(self.conf, (list, tuple)):
159 elif isinstance(self.conf, (list, tuple)):
160 paths = self.conf
160 paths = self.conf
161 elif isinstance(self.conf, dict):
161 elif isinstance(self.conf, dict):
162 paths = self.conf.items()
162 paths = self.conf.items()
163
163
164 repos = findrepos(paths)
164 repos = findrepos(paths)
165 for prefix, root in u.configitems('collections'):
165 for prefix, root in u.configitems('collections'):
166 prefix = util.pconvert(prefix)
166 prefix = util.pconvert(prefix)
167 for path in scmutil.walkrepos(root, followsym=True):
167 for path in scmutil.walkrepos(root, followsym=True):
168 repo = os.path.normpath(path)
168 repo = os.path.normpath(path)
169 name = util.pconvert(repo)
169 name = util.pconvert(repo)
170 if name.startswith(prefix):
170 if name.startswith(prefix):
171 name = name[len(prefix):]
171 name = name[len(prefix):]
172 repos.append((name.lstrip('/'), repo))
172 repos.append((name.lstrip('/'), repo))
173
173
174 self.repos = repos
174 self.repos = repos
175 self.ui = u
175 self.ui = u
176 encoding.encoding = self.ui.config('web', 'encoding')
176 encoding.encoding = self.ui.config('web', 'encoding')
177 self.style = self.ui.config('web', 'style')
177 self.style = self.ui.config('web', 'style')
178 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
178 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
179 self.stripecount = self.ui.config('web', 'stripes')
179 self.stripecount = self.ui.config('web', 'stripes')
180 if self.stripecount:
180 if self.stripecount:
181 self.stripecount = int(self.stripecount)
181 self.stripecount = int(self.stripecount)
182 self._baseurl = self.ui.config('web', 'baseurl')
182 self._baseurl = self.ui.config('web', 'baseurl')
183 prefix = self.ui.config('web', 'prefix')
183 prefix = self.ui.config('web', 'prefix')
184 if prefix.startswith('/'):
184 if prefix.startswith('/'):
185 prefix = prefix[1:]
185 prefix = prefix[1:]
186 if prefix.endswith('/'):
186 if prefix.endswith('/'):
187 prefix = prefix[:-1]
187 prefix = prefix[:-1]
188 self.prefix = prefix
188 self.prefix = prefix
189 self.lastrefresh = time.time()
189 self.lastrefresh = time.time()
190
190
191 def run(self):
191 def run(self):
192 if not encoding.environ.get('GATEWAY_INTERFACE',
192 if not encoding.environ.get('GATEWAY_INTERFACE',
193 '').startswith("CGI/1."):
193 '').startswith("CGI/1."):
194 raise RuntimeError("This function is only intended to be "
194 raise RuntimeError("This function is only intended to be "
195 "called while running as a CGI script.")
195 "called while running as a CGI script.")
196 wsgicgi.launch(self)
196 wsgicgi.launch(self)
197
197
198 def __call__(self, env, respond):
198 def __call__(self, env, respond):
199 req = wsgirequest(env, respond)
199 req = wsgirequest(env, respond)
200 return self.run_wsgi(req)
200 return self.run_wsgi(req)
201
201
202 def read_allowed(self, ui, req):
202 def read_allowed(self, ui, req):
203 """Check allow_read and deny_read config options of a repo's ui object
203 """Check allow_read and deny_read config options of a repo's ui object
204 to determine user permissions. By default, with neither option set (or
204 to determine user permissions. By default, with neither option set (or
205 both empty), allow all users to read the repo. There are two ways a
205 both empty), allow all users to read the repo. There are two ways a
206 user can be denied read access: (1) deny_read is not empty, and the
206 user can be denied read access: (1) deny_read is not empty, and the
207 user is unauthenticated or deny_read contains user (or *), and (2)
207 user is unauthenticated or deny_read contains user (or *), and (2)
208 allow_read is not empty and the user is not in allow_read. Return True
208 allow_read is not empty and the user is not in allow_read. Return True
209 if user is allowed to read the repo, else return False."""
209 if user is allowed to read the repo, else return False."""
210
210
211 user = req.env.get('REMOTE_USER')
211 user = req.env.get('REMOTE_USER')
212
212
213 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
213 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
214 if deny_read and (not user or ismember(ui, user, deny_read)):
214 if deny_read and (not user or ismember(ui, user, deny_read)):
215 return False
215 return False
216
216
217 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
217 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
218 # by default, allow reading if no allow_read option has been set
218 # by default, allow reading if no allow_read option has been set
219 if (not allow_read) or ismember(ui, user, allow_read):
219 if (not allow_read) or ismember(ui, user, allow_read):
220 return True
220 return True
221
221
222 return False
222 return False
223
223
224 def run_wsgi(self, req):
224 def run_wsgi(self, req):
225 profile = self.ui.configbool('profiling', 'enabled')
225 profile = self.ui.configbool('profiling', 'enabled')
226 with profiling.profile(self.ui, enabled=profile):
226 with profiling.profile(self.ui, enabled=profile):
227 for r in self._runwsgi(req):
227 for r in self._runwsgi(req):
228 yield r
228 yield r
229
229
230 def _runwsgi(self, req):
230 def _runwsgi(self, req):
231 try:
231 try:
232 self.refresh()
232 self.refresh()
233
233
234 csp, nonce = cspvalues(self.ui)
234 csp, nonce = cspvalues(self.ui)
235 if csp:
235 if csp:
236 req.headers.append(('Content-Security-Policy', csp))
236 req.headers.append(('Content-Security-Policy', csp))
237
237
238 virtual = req.env.get("PATH_INFO", "").strip('/')
238 virtual = req.env.get("PATH_INFO", "").strip('/')
239 tmpl = self.templater(req, nonce)
239 tmpl = self.templater(req, nonce)
240 ctype = tmpl('mimetype', encoding=encoding.encoding)
240 ctype = tmpl('mimetype', encoding=encoding.encoding)
241 ctype = templater.stringify(ctype)
241 ctype = templater.stringify(ctype)
242
242
243 # a static file
243 # a static file
244 if virtual.startswith('static/') or 'static' in req.form:
244 if virtual.startswith('static/') or 'static' in req.form:
245 if virtual.startswith('static/'):
245 if virtual.startswith('static/'):
246 fname = virtual[7:]
246 fname = virtual[7:]
247 else:
247 else:
248 fname = req.form['static'][0]
248 fname = req.form['static'][0]
249 static = self.ui.config("web", "static", None,
249 static = self.ui.config("web", "static", None,
250 untrusted=False)
250 untrusted=False)
251 if not static:
251 if not static:
252 tp = self.templatepath or templater.templatepaths()
252 tp = self.templatepath or templater.templatepaths()
253 if isinstance(tp, str):
253 if isinstance(tp, str):
254 tp = [tp]
254 tp = [tp]
255 static = [os.path.join(p, 'static') for p in tp]
255 static = [os.path.join(p, 'static') for p in tp]
256 staticfile(static, fname, req)
256 staticfile(static, fname, req)
257 return []
257 return []
258
258
259 # top-level index
259 # top-level index
260
260
261 repos = dict(self.repos)
261 repos = dict(self.repos)
262
262
263 if (not virtual or virtual == 'index') and virtual not in repos:
263 if (not virtual or virtual == 'index') and virtual not in repos:
264 req.respond(HTTP_OK, ctype)
264 req.respond(HTTP_OK, ctype)
265 return self.makeindex(req, tmpl)
265 return self.makeindex(req, tmpl)
266
266
267 # nested indexes and hgwebs
267 # nested indexes and hgwebs
268
268
269 if virtual.endswith('/index') and virtual not in repos:
269 if virtual.endswith('/index') and virtual not in repos:
270 subdir = virtual[:-len('index')]
270 subdir = virtual[:-len('index')]
271 if any(r.startswith(subdir) for r in repos):
271 if any(r.startswith(subdir) for r in repos):
272 req.respond(HTTP_OK, ctype)
272 req.respond(HTTP_OK, ctype)
273 return self.makeindex(req, tmpl, subdir)
273 return self.makeindex(req, tmpl, subdir)
274
274
275 def _virtualdirs():
275 def _virtualdirs():
276 # Check the full virtual path, each parent, and the root ('')
276 # Check the full virtual path, each parent, and the root ('')
277 if virtual != '':
277 if virtual != '':
278 yield virtual
278 yield virtual
279
279
280 for p in util.finddirs(virtual):
280 for p in util.finddirs(virtual):
281 yield p
281 yield p
282
282
283 yield ''
283 yield ''
284
284
285 for virtualrepo in _virtualdirs():
285 for virtualrepo in _virtualdirs():
286 real = repos.get(virtualrepo)
286 real = repos.get(virtualrepo)
287 if real:
287 if real:
288 req.env['REPO_NAME'] = virtualrepo
288 req.env['REPO_NAME'] = virtualrepo
289 try:
289 try:
290 # ensure caller gets private copy of ui
290 # ensure caller gets private copy of ui
291 repo = hg.repository(self.ui.copy(), real)
291 repo = hg.repository(self.ui.copy(), real)
292 return hgweb_mod.hgweb(repo).run_wsgi(req)
292 return hgweb_mod.hgweb(repo).run_wsgi(req)
293 except IOError as inst:
293 except IOError as inst:
294 msg = encoding.strtolocal(inst.strerror)
294 msg = encoding.strtolocal(inst.strerror)
295 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
295 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
296 except error.RepoError as inst:
296 except error.RepoError as inst:
297 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
297 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
298
298
299 # browse subdirectories
299 # browse subdirectories
300 subdir = virtual + '/'
300 subdir = virtual + '/'
301 if [r for r in repos if r.startswith(subdir)]:
301 if [r for r in repos if r.startswith(subdir)]:
302 req.respond(HTTP_OK, ctype)
302 req.respond(HTTP_OK, ctype)
303 return self.makeindex(req, tmpl, subdir)
303 return self.makeindex(req, tmpl, subdir)
304
304
305 # prefixes not found
305 # prefixes not found
306 req.respond(HTTP_NOT_FOUND, ctype)
306 req.respond(HTTP_NOT_FOUND, ctype)
307 return tmpl("notfound", repo=virtual)
307 return tmpl("notfound", repo=virtual)
308
308
309 except ErrorResponse as err:
309 except ErrorResponse as err:
310 req.respond(err, ctype)
310 req.respond(err, ctype)
311 return tmpl('error', error=err.message or '')
311 return tmpl('error', error=err.message or '')
312 finally:
312 finally:
313 tmpl = None
313 tmpl = None
314
314
315 def makeindex(self, req, tmpl, subdir=""):
315 def makeindex(self, req, tmpl, subdir=""):
316
316
317 def archivelist(ui, nodeid, url):
317 def archivelist(ui, nodeid, url):
318 allowed = ui.configlist("web", "allow_archive", untrusted=True)
318 allowed = ui.configlist("web", "allow_archive", untrusted=True)
319 archives = []
319 archives = []
320 for typ, spec in hgweb_mod.archivespecs.iteritems():
320 for typ, spec in hgweb_mod.archivespecs.iteritems():
321 if typ in allowed or ui.configbool("web", "allow" + typ,
321 if typ in allowed or ui.configbool("web", "allow" + typ,
322 untrusted=True):
322 untrusted=True):
323 archives.append({"type": typ, "extension": spec[2],
323 archives.append({"type": typ, "extension": spec[2],
324 "node": nodeid, "url": url})
324 "node": nodeid, "url": url})
325 return archives
325 return archives
326
326
327 def rawentries(subdir="", **map):
327 def rawentries(subdir="", **map):
328
328
329 descend = self.ui.configbool('web', 'descend')
329 descend = self.ui.configbool('web', 'descend')
330 collapse = self.ui.configbool('web', 'collapse')
330 collapse = self.ui.configbool('web', 'collapse')
331 seenrepos = set()
331 seenrepos = set()
332 seendirs = set()
332 seendirs = set()
333 for name, path in self.repos:
333 for name, path in self.repos:
334
334
335 if not name.startswith(subdir):
335 if not name.startswith(subdir):
336 continue
336 continue
337 name = name[len(subdir):]
337 name = name[len(subdir):]
338 directory = False
338 directory = False
339
339
340 if '/' in name:
340 if '/' in name:
341 if not descend:
341 if not descend:
342 continue
342 continue
343
343
344 nameparts = name.split('/')
344 nameparts = name.split('/')
345 rootname = nameparts[0]
345 rootname = nameparts[0]
346
346
347 if not collapse:
347 if not collapse:
348 pass
348 pass
349 elif rootname in seendirs:
349 elif rootname in seendirs:
350 continue
350 continue
351 elif rootname in seenrepos:
351 elif rootname in seenrepos:
352 pass
352 pass
353 else:
353 else:
354 directory = True
354 directory = True
355 name = rootname
355 name = rootname
356
356
357 # redefine the path to refer to the directory
357 # redefine the path to refer to the directory
358 discarded = '/'.join(nameparts[1:])
358 discarded = '/'.join(nameparts[1:])
359
359
360 # remove name parts plus accompanying slash
360 # remove name parts plus accompanying slash
361 path = path[:-len(discarded) - 1]
361 path = path[:-len(discarded) - 1]
362
362
363 try:
363 try:
364 r = hg.repository(self.ui, path)
364 r = hg.repository(self.ui, path)
365 directory = False
365 directory = False
366 except (IOError, error.RepoError):
366 except (IOError, error.RepoError):
367 pass
367 pass
368
368
369 parts = [name]
369 parts = [name]
370 parts.insert(0, '/' + subdir.rstrip('/'))
370 parts.insert(0, '/' + subdir.rstrip('/'))
371 if req.env['SCRIPT_NAME']:
371 if req.env['SCRIPT_NAME']:
372 parts.insert(0, req.env['SCRIPT_NAME'])
372 parts.insert(0, req.env['SCRIPT_NAME'])
373 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
373 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
374
374
375 # show either a directory entry or a repository
375 # show either a directory entry or a repository
376 if directory:
376 if directory:
377 # get the directory's time information
377 # get the directory's time information
378 try:
378 try:
379 d = (get_mtime(path), util.makedate()[1])
379 d = (get_mtime(path), util.makedate()[1])
380 except OSError:
380 except OSError:
381 continue
381 continue
382
382
383 # add '/' to the name to make it obvious that
383 # add '/' to the name to make it obvious that
384 # the entry is a directory, not a regular repository
384 # the entry is a directory, not a regular repository
385 row = {'contact': "",
385 row = {'contact': "",
386 'contact_sort': "",
386 'contact_sort': "",
387 'name': name + '/',
387 'name': name + '/',
388 'name_sort': name,
388 'name_sort': name,
389 'url': url,
389 'url': url,
390 'description': "",
390 'description': "",
391 'description_sort': "",
391 'description_sort': "",
392 'lastchange': d,
392 'lastchange': d,
393 'lastchange_sort': d[1]-d[0],
393 'lastchange_sort': d[1]-d[0],
394 'archives': [],
394 'archives': [],
395 'isdirectory': True,
395 'isdirectory': True,
396 'labels': [],
396 'labels': [],
397 }
397 }
398
398
399 seendirs.add(name)
399 seendirs.add(name)
400 yield row
400 yield row
401 continue
401 continue
402
402
403 u = self.ui.copy()
403 u = self.ui.copy()
404 try:
404 try:
405 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
405 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
406 except Exception as e:
406 except Exception as e:
407 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
407 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
408 continue
408 continue
409 def get(section, name, default=uimod._unset):
409 def get(section, name, default=uimod._unset):
410 return u.config(section, name, default, untrusted=True)
410 return u.config(section, name, default, untrusted=True)
411
411
412 if u.configbool("web", "hidden", untrusted=True):
412 if u.configbool("web", "hidden", untrusted=True):
413 continue
413 continue
414
414
415 if not self.read_allowed(u, req):
415 if not self.read_allowed(u, req):
416 continue
416 continue
417
417
418 # update time with local timezone
418 # update time with local timezone
419 try:
419 try:
420 r = hg.repository(self.ui, path)
420 r = hg.repository(self.ui, path)
421 except IOError:
421 except IOError:
422 u.warn(_('error accessing repository at %s\n') % path)
422 u.warn(_('error accessing repository at %s\n') % path)
423 continue
423 continue
424 except error.RepoError:
424 except error.RepoError:
425 u.warn(_('error accessing repository at %s\n') % path)
425 u.warn(_('error accessing repository at %s\n') % path)
426 continue
426 continue
427 try:
427 try:
428 d = (get_mtime(r.spath), util.makedate()[1])
428 d = (get_mtime(r.spath), util.makedate()[1])
429 except OSError:
429 except OSError:
430 continue
430 continue
431
431
432 contact = get_contact(get)
432 contact = get_contact(get)
433 description = get("web", "description")
433 description = get("web", "description")
434 seenrepos.add(name)
434 seenrepos.add(name)
435 name = get("web", "name", name)
435 name = get("web", "name", name)
436 row = {'contact': contact or "unknown",
436 row = {'contact': contact or "unknown",
437 'contact_sort': contact.upper() or "unknown",
437 'contact_sort': contact.upper() or "unknown",
438 'name': name,
438 'name': name,
439 'name_sort': name,
439 'name_sort': name,
440 'url': url,
440 'url': url,
441 'description': description or "unknown",
441 'description': description or "unknown",
442 'description_sort': description.upper() or "unknown",
442 'description_sort': description.upper() or "unknown",
443 'lastchange': d,
443 'lastchange': d,
444 'lastchange_sort': d[1]-d[0],
444 'lastchange_sort': d[1]-d[0],
445 'archives': archivelist(u, "tip", url),
445 'archives': archivelist(u, "tip", url),
446 'isdirectory': None,
446 'isdirectory': None,
447 'labels': u.configlist('web', 'labels', untrusted=True),
447 'labels': u.configlist('web', 'labels', untrusted=True),
448 }
448 }
449
449
450 yield row
450 yield row
451
451
452 sortdefault = None, False
452 sortdefault = None, False
453 def entries(sortcolumn="", descending=False, subdir="", **map):
453 def entries(sortcolumn="", descending=False, subdir="", **map):
454 rows = rawentries(subdir=subdir, **map)
454 rows = rawentries(subdir=subdir, **map)
455
455
456 if sortcolumn and sortdefault != (sortcolumn, descending):
456 if sortcolumn and sortdefault != (sortcolumn, descending):
457 sortkey = '%s_sort' % sortcolumn
457 sortkey = '%s_sort' % sortcolumn
458 rows = sorted(rows, key=lambda x: x[sortkey],
458 rows = sorted(rows, key=lambda x: x[sortkey],
459 reverse=descending)
459 reverse=descending)
460 for row, parity in zip(rows, paritygen(self.stripecount)):
460 for row, parity in zip(rows, paritygen(self.stripecount)):
461 row['parity'] = parity
461 row['parity'] = parity
462 yield row
462 yield row
463
463
464 self.refresh()
464 self.refresh()
465 sortable = ["name", "description", "contact", "lastchange"]
465 sortable = ["name", "description", "contact", "lastchange"]
466 sortcolumn, descending = sortdefault
466 sortcolumn, descending = sortdefault
467 if 'sort' in req.form:
467 if 'sort' in req.form:
468 sortcolumn = req.form['sort'][0]
468 sortcolumn = req.form['sort'][0]
469 descending = sortcolumn.startswith('-')
469 descending = sortcolumn.startswith('-')
470 if descending:
470 if descending:
471 sortcolumn = sortcolumn[1:]
471 sortcolumn = sortcolumn[1:]
472 if sortcolumn not in sortable:
472 if sortcolumn not in sortable:
473 sortcolumn = ""
473 sortcolumn = ""
474
474
475 sort = [("sort_%s" % column,
475 sort = [("sort_%s" % column,
476 "%s%s" % ((not descending and column == sortcolumn)
476 "%s%s" % ((not descending and column == sortcolumn)
477 and "-" or "", column))
477 and "-" or "", column))
478 for column in sortable]
478 for column in sortable]
479
479
480 self.refresh()
480 self.refresh()
481 self.updatereqenv(req.env)
481 self.updatereqenv(req.env)
482
482
483 return tmpl("index", entries=entries, subdir=subdir,
483 return tmpl("index", entries=entries, subdir=subdir,
484 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
484 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
485 sortcolumn=sortcolumn, descending=descending,
485 sortcolumn=sortcolumn, descending=descending,
486 **dict(sort))
486 **dict(sort))
487
487
488 def templater(self, req, nonce):
488 def templater(self, req, nonce):
489
489
490 def motd(**map):
490 def motd(**map):
491 if self.motd is not None:
491 if self.motd is not None:
492 yield self.motd
492 yield self.motd
493 else:
493 else:
494 yield config('web', 'motd', '')
494 yield config('web', 'motd', '')
495
495
496 def config(section, name, default=uimod._unset, untrusted=True):
496 def config(section, name, default=uimod._unset, untrusted=True):
497 return self.ui.config(section, name, default, untrusted)
497 return self.ui.config(section, name, default, untrusted)
498
498
499 self.updatereqenv(req.env)
499 self.updatereqenv(req.env)
500
500
501 url = req.env.get('SCRIPT_NAME', '')
501 url = req.env.get('SCRIPT_NAME', '')
502 if not url.endswith('/'):
502 if not url.endswith('/'):
503 url += '/'
503 url += '/'
504
504
505 vars = {}
505 vars = {}
506 styles = (
506 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
507 req.form.get('style', [None])[0],
507 self.templatepath)
508 config('web', 'style'),
509 'paper'
510 )
511 style, mapfile = templater.stylemap(styles, self.templatepath)
512 if style == styles[0]:
508 if style == styles[0]:
513 vars['style'] = style
509 vars['style'] = style
514
510
515 start = url[-1] == '?' and '&' or '?'
511 start = url[-1] == '?' and '&' or '?'
516 sessionvars = webutil.sessionvars(vars, start)
512 sessionvars = webutil.sessionvars(vars, start)
517 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
513 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
518 logoimg = config('web', 'logoimg', 'hglogo.png')
514 logoimg = config('web', 'logoimg', 'hglogo.png')
519 staticurl = config('web', 'staticurl') or url + 'static/'
515 staticurl = config('web', 'staticurl') or url + 'static/'
520 if not staticurl.endswith('/'):
516 if not staticurl.endswith('/'):
521 staticurl += '/'
517 staticurl += '/'
522
518
523 defaults = {
519 defaults = {
524 "encoding": encoding.encoding,
520 "encoding": encoding.encoding,
525 "motd": motd,
521 "motd": motd,
526 "url": url,
522 "url": url,
527 "logourl": logourl,
523 "logourl": logourl,
528 "logoimg": logoimg,
524 "logoimg": logoimg,
529 "staticurl": staticurl,
525 "staticurl": staticurl,
530 "sessionvars": sessionvars,
526 "sessionvars": sessionvars,
531 "style": style,
527 "style": style,
532 "nonce": nonce,
528 "nonce": nonce,
533 }
529 }
534 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
530 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
535 return tmpl
531 return tmpl
536
532
537 def updatereqenv(self, env):
533 def updatereqenv(self, env):
538 if self._baseurl is not None:
534 if self._baseurl is not None:
539 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
535 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
540 env['SERVER_NAME'] = name
536 env['SERVER_NAME'] = name
541 env['SERVER_PORT'] = port
537 env['SERVER_PORT'] = port
542 env['SCRIPT_NAME'] = path
538 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now