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