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