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