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