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