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