##// END OF EJS Templates
hgweb: move templater instantiation to requestcontext...
Gregory Szorc -
r26183:bf1b2478 default
parent child Browse files
Show More
@@ -1,452 +1,446 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 import os
10 10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 11 from mercurial.templatefilters import websub
12 12 from mercurial.i18n import _
13 13 from common import get_stat, ErrorResponse, permhooks, caching
14 14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 16 from request import wsgirequest
17 17 import webcommands, protocol, webutil
18 18
19 19 perms = {
20 20 'changegroup': 'pull',
21 21 'changegroupsubset': 'pull',
22 22 'getbundle': 'pull',
23 23 'stream_out': 'pull',
24 24 'listkeys': 'pull',
25 25 'unbundle': 'push',
26 26 'pushkey': 'push',
27 27 }
28 28
29 29 ## Files of interest
30 30 # Used to check if the repository has changed looking at mtime and size of
31 31 # theses files. This should probably be relocated a bit higher in core.
32 32 foi = [('spath', '00changelog.i'),
33 33 ('spath', 'phaseroots'), # ! phase can change content at the same size
34 34 ('spath', 'obsstore'),
35 35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
36 36 ]
37 37
38 38 def makebreadcrumb(url, prefix=''):
39 39 '''Return a 'URL breadcrumb' list
40 40
41 41 A 'URL breadcrumb' is a list of URL-name pairs,
42 42 corresponding to each of the path items on a URL.
43 43 This can be used to create path navigation entries.
44 44 '''
45 45 if url.endswith('/'):
46 46 url = url[:-1]
47 47 if prefix:
48 48 url = '/' + prefix + url
49 49 relpath = url
50 50 if relpath.startswith('/'):
51 51 relpath = relpath[1:]
52 52
53 53 breadcrumb = []
54 54 urlel = url
55 55 pathitems = [''] + relpath.split('/')
56 56 for pathel in reversed(pathitems):
57 57 if not pathel or not urlel:
58 58 break
59 59 breadcrumb.append({'url': urlel, 'name': pathel})
60 60 urlel = os.path.dirname(urlel)
61 61 return reversed(breadcrumb)
62 62
63 63 class requestcontext(object):
64 64 """Holds state/context for an individual request.
65 65
66 66 Servers can be multi-threaded. Holding state on the WSGI application
67 67 is prone to race conditions. Instances of this class exist to hold
68 68 mutable and race-free state for requests.
69 69 """
70 70 def __init__(self, app):
71 71 object.__setattr__(self, 'app', app)
72 72 object.__setattr__(self, 'repo', app.repo)
73 73
74 74 object.__setattr__(self, 'archives', ('zip', 'gz', 'bz2'))
75 75
76 76 object.__setattr__(self, 'maxchanges',
77 77 self.configint('web', 'maxchanges', 10))
78 78 object.__setattr__(self, 'stripecount',
79 79 self.configint('web', 'stripes', 1))
80 80 object.__setattr__(self, 'maxshortchanges',
81 81 self.configint('web', 'maxshortchanges', 60))
82 82 object.__setattr__(self, 'maxfiles',
83 83 self.configint('web', 'maxfiles', 10))
84 84 object.__setattr__(self, 'allowpull',
85 85 self.configbool('web', 'allowpull', True))
86 86
87 87 # we use untrusted=False to prevent a repo owner from using
88 88 # web.templates in .hg/hgrc to get access to any file readable
89 89 # by the user running the CGI script
90 90 object.__setattr__(self, 'templatepath',
91 91 self.config('web', 'templates', untrusted=False))
92 92
93 93 # This object is more expensive to build than simple config values.
94 94 # It is shared across requests. The app will replace the object
95 95 # if it is updated. Since this is a reference and nothing should
96 96 # modify the underlying object, it should be constant for the lifetime
97 97 # of the request.
98 98 object.__setattr__(self, 'websubtable', app.websubtable)
99 99
100 100 # Proxy unknown reads and writes to the application instance
101 101 # until everything is moved to us.
102 102 def __getattr__(self, name):
103 103 return getattr(self.app, name)
104 104
105 105 def __setattr__(self, name, value):
106 106 return setattr(self.app, name, value)
107 107
108 108 # Servers are often run by a user different from the repo owner.
109 109 # Trust the settings from the .hg/hgrc files by default.
110 110 def config(self, section, name, default=None, untrusted=True):
111 111 return self.repo.ui.config(section, name, default,
112 112 untrusted=untrusted)
113 113
114 114 def configbool(self, section, name, default=False, untrusted=True):
115 115 return self.repo.ui.configbool(section, name, default,
116 116 untrusted=untrusted)
117 117
118 118 def configint(self, section, name, default=None, untrusted=True):
119 119 return self.repo.ui.configint(section, name, default,
120 120 untrusted=untrusted)
121 121
122 122 def configlist(self, section, name, default=None, untrusted=True):
123 123 return self.repo.ui.configlist(section, name, default,
124 124 untrusted=untrusted)
125 125
126 126 archivespecs = {
127 127 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
128 128 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
129 129 'zip': ('application/zip', 'zip', '.zip', None),
130 130 }
131 131
132 132 def archivelist(self, nodeid):
133 133 allowed = self.configlist('web', 'allow_archive')
134 134 for typ, spec in self.archivespecs.iteritems():
135 135 if typ in allowed or self.configbool('web', 'allow%s' % typ):
136 136 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
137 137
138 def templater(self, req):
139 # determine scheme, port and server name
140 # this is needed to create absolute urls
141
142 proto = req.env.get('wsgi.url_scheme')
143 if proto == 'https':
144 proto = 'https'
145 default_port = '443'
146 else:
147 proto = 'http'
148 default_port = '80'
149
150 port = req.env['SERVER_PORT']
151 port = port != default_port and (':' + port) or ''
152 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
153 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
154 logoimg = self.config('web', 'logoimg', 'hglogo.png')
155 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
156 if not staticurl.endswith('/'):
157 staticurl += '/'
158
159 # some functions for the templater
160
161 def motd(**map):
162 yield self.config('web', 'motd', '')
163
164 # figure out which style to use
165
166 vars = {}
167 styles = (
168 req.form.get('style', [None])[0],
169 self.config('web', 'style'),
170 'paper',
171 )
172 style, mapfile = templater.stylemap(styles, self.templatepath)
173 if style == styles[0]:
174 vars['style'] = style
175
176 start = req.url[-1] == '?' and '&' or '?'
177 sessionvars = webutil.sessionvars(vars, start)
178
179 if not self.reponame:
180 self.reponame = (self.config('web', 'name')
181 or req.env.get('REPO_NAME')
182 or req.url.strip('/') or self.repo.root)
183
184 def websubfilter(text):
185 return websub(text, self.websubtable)
186
187 # create the templater
188
189 tmpl = templater.templater(mapfile,
190 filters={'websub': websubfilter},
191 defaults={'url': req.url,
192 'logourl': logourl,
193 'logoimg': logoimg,
194 'staticurl': staticurl,
195 'urlbase': urlbase,
196 'repo': self.reponame,
197 'encoding': encoding.encoding,
198 'motd': motd,
199 'sessionvars': sessionvars,
200 'pathdef': makebreadcrumb(req.url),
201 'style': style,
202 })
203 return tmpl
204
205
138 206 class hgweb(object):
139 207 """HTTP server for individual repositories.
140 208
141 209 Instances of this class serve HTTP responses for a particular
142 210 repository.
143 211
144 212 Instances are typically used as WSGI applications.
145 213
146 214 Some servers are multi-threaded. On these servers, there may
147 215 be multiple active threads inside __call__.
148 216 """
149 217 def __init__(self, repo, name=None, baseui=None):
150 218 if isinstance(repo, str):
151 219 if baseui:
152 220 u = baseui.copy()
153 221 else:
154 222 u = ui.ui()
155 223 r = hg.repository(u, repo)
156 224 else:
157 225 # we trust caller to give us a private copy
158 226 r = repo
159 227
160 228 r = self._getview(r)
161 229 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
162 230 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
163 231 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
164 232 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
165 233 # displaying bundling progress bar while serving feel wrong and may
166 234 # break some wsgi implementation.
167 235 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
168 236 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
169 237 self.repo = r
170 238 hook.redirect(True)
171 239 self.repostate = None
172 240 self.mtime = -1
173 241 self.reponame = name
174 242 self.websubtable = webutil.getwebsubs(r)
175 243
176 # The CGI scripts are often run by a user different from the repo owner.
177 # Trust the settings from the .hg/hgrc files by default.
178 def config(self, section, name, default=None, untrusted=True):
179 return self.repo.ui.config(section, name, default,
180 untrusted=untrusted)
181
182 244 def _getview(self, repo):
183 245 """The 'web.view' config controls changeset filter to hgweb. Possible
184 246 values are ``served``, ``visible`` and ``all``. Default is ``served``.
185 247 The ``served`` filter only shows changesets that can be pulled from the
186 248 hgweb instance. The``visible`` filter includes secret changesets but
187 249 still excludes "hidden" one.
188 250
189 251 See the repoview module for details.
190 252
191 253 The option has been around undocumented since Mercurial 2.5, but no
192 254 user ever asked about it. So we better keep it undocumented for now."""
193 255 viewconfig = repo.ui.config('web', 'view', 'served',
194 256 untrusted=True)
195 257 if viewconfig == 'all':
196 258 return repo.unfiltered()
197 259 elif viewconfig in repoview.filtertable:
198 260 return repo.filtered(viewconfig)
199 261 else:
200 262 return repo.filtered('served')
201 263
202 264 def refresh(self):
203 265 repostate = []
204 266 mtime = 0
205 267 # file of interrests mtime and size
206 268 for meth, fname in foi:
207 269 prefix = getattr(self.repo, meth)
208 270 st = get_stat(prefix, fname)
209 271 repostate.append((st.st_mtime, st.st_size))
210 272 mtime = max(mtime, st.st_mtime)
211 273 repostate = tuple(repostate)
212 274 # we need to compare file size in addition to mtime to catch
213 275 # changes made less than a second ago
214 276 if repostate != self.repostate:
215 277 r = hg.repository(self.repo.baseui, self.repo.url())
216 278 self.repo = self._getview(r)
217 279 # update these last to avoid threads seeing empty settings
218 280 self.repostate = repostate
219 281 # mtime is needed for ETag
220 282 self.mtime = mtime
221 283
222 284 def run(self):
223 285 """Start a server from CGI environment.
224 286
225 287 Modern servers should be using WSGI and should avoid this
226 288 method, if possible.
227 289 """
228 290 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
229 291 raise RuntimeError("This function is only intended to be "
230 292 "called while running as a CGI script.")
231 293 import mercurial.hgweb.wsgicgi as wsgicgi
232 294 wsgicgi.launch(self)
233 295
234 296 def __call__(self, env, respond):
235 297 """Run the WSGI application.
236 298
237 299 This may be called by multiple threads.
238 300 """
239 301 req = wsgirequest(env, respond)
240 302 return self.run_wsgi(req)
241 303
242 304 def run_wsgi(self, req):
243 305 """Internal method to run the WSGI application.
244 306
245 307 This is typically only called by Mercurial. External consumers
246 308 should be using instances of this class as the WSGI application.
247 309 """
248 310 self.refresh()
249 311 rctx = requestcontext(self)
250 312
251 313 # This state is global across all threads.
252 314 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
253 315 rctx.repo.ui.environ = req.env
254 316
255 317 # work with CGI variables to create coherent structure
256 318 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
257 319
258 320 req.url = req.env['SCRIPT_NAME']
259 321 if not req.url.endswith('/'):
260 322 req.url += '/'
261 323 if 'REPO_NAME' in req.env:
262 324 req.url += req.env['REPO_NAME'] + '/'
263 325
264 326 if 'PATH_INFO' in req.env:
265 327 parts = req.env['PATH_INFO'].strip('/').split('/')
266 328 repo_parts = req.env.get('REPO_NAME', '').split('/')
267 329 if parts[:len(repo_parts)] == repo_parts:
268 330 parts = parts[len(repo_parts):]
269 331 query = '/'.join(parts)
270 332 else:
271 333 query = req.env['QUERY_STRING'].split('&', 1)[0]
272 334 query = query.split(';', 1)[0]
273 335
274 336 # process this if it's a protocol request
275 337 # protocol bits don't need to create any URLs
276 338 # and the clients always use the old URL structure
277 339
278 340 cmd = req.form.get('cmd', [''])[0]
279 341 if protocol.iscmd(cmd):
280 342 try:
281 343 if query:
282 344 raise ErrorResponse(HTTP_NOT_FOUND)
283 345 if cmd in perms:
284 346 self.check_perm(rctx, req, perms[cmd])
285 347 return protocol.call(self.repo, req, cmd)
286 348 except ErrorResponse as inst:
287 349 # A client that sends unbundle without 100-continue will
288 350 # break if we respond early.
289 351 if (cmd == 'unbundle' and
290 352 (req.env.get('HTTP_EXPECT',
291 353 '').lower() != '100-continue') or
292 354 req.env.get('X-HgHttp2', '')):
293 355 req.drain()
294 356 else:
295 357 req.headers.append(('Connection', 'Close'))
296 358 req.respond(inst, protocol.HGTYPE,
297 359 body='0\n%s\n' % inst.message)
298 360 return ''
299 361
300 362 # translate user-visible url structure to internal structure
301 363
302 364 args = query.split('/', 2)
303 365 if 'cmd' not in req.form and args and args[0]:
304 366
305 367 cmd = args.pop(0)
306 368 style = cmd.rfind('-')
307 369 if style != -1:
308 370 req.form['style'] = [cmd[:style]]
309 371 cmd = cmd[style + 1:]
310 372
311 373 # avoid accepting e.g. style parameter as command
312 374 if util.safehasattr(webcommands, cmd):
313 375 req.form['cmd'] = [cmd]
314 376
315 377 if cmd == 'static':
316 378 req.form['file'] = ['/'.join(args)]
317 379 else:
318 380 if args and args[0]:
319 381 node = args.pop(0).replace('%2F', '/')
320 382 req.form['node'] = [node]
321 383 if args:
322 384 req.form['file'] = args
323 385
324 386 ua = req.env.get('HTTP_USER_AGENT', '')
325 387 if cmd == 'rev' and 'mercurial' in ua:
326 388 req.form['style'] = ['raw']
327 389
328 390 if cmd == 'archive':
329 391 fn = req.form['node'][0]
330 392 for type_, spec in rctx.archivespecs.iteritems():
331 393 ext = spec[2]
332 394 if fn.endswith(ext):
333 395 req.form['node'] = [fn[:-len(ext)]]
334 396 req.form['type'] = [type_]
335 397
336 398 # process the web interface request
337 399
338 400 try:
339 tmpl = self.templater(req)
401 tmpl = rctx.templater(req)
340 402 ctype = tmpl('mimetype', encoding=encoding.encoding)
341 403 ctype = templater.stringify(ctype)
342 404
343 405 # check read permissions non-static content
344 406 if cmd != 'static':
345 407 self.check_perm(rctx, req, None)
346 408
347 409 if cmd == '':
348 410 req.form['cmd'] = [tmpl.cache['default']]
349 411 cmd = req.form['cmd'][0]
350 412
351 413 if rctx.configbool('web', 'cache', True):
352 414 caching(self, req) # sets ETag header or raises NOT_MODIFIED
353 415 if cmd not in webcommands.__all__:
354 416 msg = 'no such method: %s' % cmd
355 417 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
356 418 elif cmd == 'file' and 'raw' in req.form.get('style', []):
357 419 self.ctype = ctype
358 420 content = webcommands.rawfile(rctx, req, tmpl)
359 421 else:
360 422 content = getattr(webcommands, cmd)(rctx, req, tmpl)
361 423 req.respond(HTTP_OK, ctype)
362 424
363 425 return content
364 426
365 427 except (error.LookupError, error.RepoLookupError) as err:
366 428 req.respond(HTTP_NOT_FOUND, ctype)
367 429 msg = str(err)
368 430 if (util.safehasattr(err, 'name') and
369 431 not isinstance(err, error.ManifestLookupError)):
370 432 msg = 'revision not found: %s' % err.name
371 433 return tmpl('error', error=msg)
372 434 except (error.RepoError, error.RevlogError) as inst:
373 435 req.respond(HTTP_SERVER_ERROR, ctype)
374 436 return tmpl('error', error=str(inst))
375 437 except ErrorResponse as inst:
376 438 req.respond(inst, ctype)
377 439 if inst.code == HTTP_NOT_MODIFIED:
378 440 # Not allowed to return a body on a 304
379 441 return ['']
380 442 return tmpl('error', error=inst.message)
381 443
382 def templater(self, req):
383
384 # determine scheme, port and server name
385 # this is needed to create absolute urls
386
387 proto = req.env.get('wsgi.url_scheme')
388 if proto == 'https':
389 proto = 'https'
390 default_port = "443"
391 else:
392 proto = 'http'
393 default_port = "80"
394
395 port = req.env["SERVER_PORT"]
396 port = port != default_port and (":" + port) or ""
397 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
398 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
399 logoimg = self.config("web", "logoimg", "hglogo.png")
400 staticurl = self.config("web", "staticurl") or req.url + 'static/'
401 if not staticurl.endswith('/'):
402 staticurl += '/'
403
404 # some functions for the templater
405
406 def motd(**map):
407 yield self.config("web", "motd", "")
408
409 # figure out which style to use
410
411 vars = {}
412 styles = (
413 req.form.get('style', [None])[0],
414 self.config('web', 'style'),
415 'paper',
416 )
417 style, mapfile = templater.stylemap(styles, self.templatepath)
418 if style == styles[0]:
419 vars['style'] = style
420
421 start = req.url[-1] == '?' and '&' or '?'
422 sessionvars = webutil.sessionvars(vars, start)
423
424 if not self.reponame:
425 self.reponame = (self.config("web", "name")
426 or req.env.get('REPO_NAME')
427 or req.url.strip('/') or self.repo.root)
428
429 def websubfilter(text):
430 return websub(text, self.websubtable)
431
432 # create the templater
433
434 tmpl = templater.templater(mapfile,
435 filters={"websub": websubfilter},
436 defaults={"url": req.url,
437 "logourl": logourl,
438 "logoimg": logoimg,
439 "staticurl": staticurl,
440 "urlbase": urlbase,
441 "repo": self.reponame,
442 "encoding": encoding.encoding,
443 "motd": motd,
444 "sessionvars": sessionvars,
445 "pathdef": makebreadcrumb(req.url),
446 "style": style,
447 })
448 return tmpl
449
450 444 def check_perm(self, rctx, req, op):
451 445 for permhook in permhooks:
452 446 permhook(rctx, req, op)
General Comments 0
You need to be logged in to leave comments. Login now