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