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