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