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