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