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