##// END OF EJS Templates
hgweb: remove hgweb.configbool...
Gregory Szorc -
r26161:16d54bbd default
parent child Browse files
Show More
@@ -1,487 +1,483
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 def configbool(self, section, name, default=False, untrusted=True):
175 return self.repo.ui.configbool(section, name, default,
176 untrusted=untrusted)
177
178 174 def _getview(self, repo):
179 175 """The 'web.view' config controls changeset filter to hgweb. Possible
180 176 values are ``served``, ``visible`` and ``all``. Default is ``served``.
181 177 The ``served`` filter only shows changesets that can be pulled from the
182 178 hgweb instance. The``visible`` filter includes secret changesets but
183 179 still excludes "hidden" one.
184 180
185 181 See the repoview module for details.
186 182
187 183 The option has been around undocumented since Mercurial 2.5, but no
188 184 user ever asked about it. So we better keep it undocumented for now."""
189 185 viewconfig = repo.ui.config('web', 'view', 'served',
190 186 untrusted=True)
191 187 if viewconfig == 'all':
192 188 return repo.unfiltered()
193 189 elif viewconfig in repoview.filtertable:
194 190 return repo.filtered(viewconfig)
195 191 else:
196 192 return repo.filtered('served')
197 193
198 194 def refresh(self):
199 195 repostate = []
200 196 # file of interrests mtime and size
201 197 for meth, fname in foi:
202 198 prefix = getattr(self.repo, meth)
203 199 st = get_stat(prefix, fname)
204 200 repostate.append((st.st_mtime, st.st_size))
205 201 repostate = tuple(repostate)
206 202 # we need to compare file size in addition to mtime to catch
207 203 # changes made less than a second ago
208 204 if repostate != self.repostate:
209 205 r = hg.repository(self.repo.baseui, self.repo.url())
210 206 self.repo = self._getview(r)
211 207 # update these last to avoid threads seeing empty settings
212 208 self.repostate = repostate
213 209 # mtime is needed for ETag
214 210 self.mtime = st.st_mtime
215 211
216 212 def run(self):
217 213 """Start a server from CGI environment.
218 214
219 215 Modern servers should be using WSGI and should avoid this
220 216 method, if possible.
221 217 """
222 218 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
223 219 raise RuntimeError("This function is only intended to be "
224 220 "called while running as a CGI script.")
225 221 import mercurial.hgweb.wsgicgi as wsgicgi
226 222 wsgicgi.launch(self)
227 223
228 224 def __call__(self, env, respond):
229 225 """Run the WSGI application.
230 226
231 227 This may be called by multiple threads.
232 228 """
233 229 req = wsgirequest(env, respond)
234 230 return self.run_wsgi(req)
235 231
236 232 def run_wsgi(self, req):
237 233 """Internal method to run the WSGI application.
238 234
239 235 This is typically only called by Mercurial. External consumers
240 236 should be using instances of this class as the WSGI application.
241 237 """
242 238 self.refresh()
243 239 rctx = requestcontext(self)
244 240
245 241 # This state is global across all threads.
246 242 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
247 243 rctx.repo.ui.environ = req.env
248 244
249 245 # work with CGI variables to create coherent structure
250 246 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
251 247
252 248 req.url = req.env['SCRIPT_NAME']
253 249 if not req.url.endswith('/'):
254 250 req.url += '/'
255 251 if 'REPO_NAME' in req.env:
256 252 req.url += req.env['REPO_NAME'] + '/'
257 253
258 254 if 'PATH_INFO' in req.env:
259 255 parts = req.env['PATH_INFO'].strip('/').split('/')
260 256 repo_parts = req.env.get('REPO_NAME', '').split('/')
261 257 if parts[:len(repo_parts)] == repo_parts:
262 258 parts = parts[len(repo_parts):]
263 259 query = '/'.join(parts)
264 260 else:
265 261 query = req.env['QUERY_STRING'].split('&', 1)[0]
266 262 query = query.split(';', 1)[0]
267 263
268 264 # process this if it's a protocol request
269 265 # protocol bits don't need to create any URLs
270 266 # and the clients always use the old URL structure
271 267
272 268 cmd = req.form.get('cmd', [''])[0]
273 269 if protocol.iscmd(cmd):
274 270 try:
275 271 if query:
276 272 raise ErrorResponse(HTTP_NOT_FOUND)
277 273 if cmd in perms:
278 274 self.check_perm(rctx, req, perms[cmd])
279 275 return protocol.call(self.repo, req, cmd)
280 276 except ErrorResponse as inst:
281 277 # A client that sends unbundle without 100-continue will
282 278 # break if we respond early.
283 279 if (cmd == 'unbundle' and
284 280 (req.env.get('HTTP_EXPECT',
285 281 '').lower() != '100-continue') or
286 282 req.env.get('X-HgHttp2', '')):
287 283 req.drain()
288 284 else:
289 285 req.headers.append(('Connection', 'Close'))
290 286 req.respond(inst, protocol.HGTYPE,
291 287 body='0\n%s\n' % inst.message)
292 288 return ''
293 289
294 290 # translate user-visible url structure to internal structure
295 291
296 292 args = query.split('/', 2)
297 293 if 'cmd' not in req.form and args and args[0]:
298 294
299 295 cmd = args.pop(0)
300 296 style = cmd.rfind('-')
301 297 if style != -1:
302 298 req.form['style'] = [cmd[:style]]
303 299 cmd = cmd[style + 1:]
304 300
305 301 # avoid accepting e.g. style parameter as command
306 302 if util.safehasattr(webcommands, cmd):
307 303 req.form['cmd'] = [cmd]
308 304
309 305 if cmd == 'static':
310 306 req.form['file'] = ['/'.join(args)]
311 307 else:
312 308 if args and args[0]:
313 309 node = args.pop(0).replace('%2F', '/')
314 310 req.form['node'] = [node]
315 311 if args:
316 312 req.form['file'] = args
317 313
318 314 ua = req.env.get('HTTP_USER_AGENT', '')
319 315 if cmd == 'rev' and 'mercurial' in ua:
320 316 req.form['style'] = ['raw']
321 317
322 318 if cmd == 'archive':
323 319 fn = req.form['node'][0]
324 320 for type_, spec in rctx.archivespecs.iteritems():
325 321 ext = spec[2]
326 322 if fn.endswith(ext):
327 323 req.form['node'] = [fn[:-len(ext)]]
328 324 req.form['type'] = [type_]
329 325
330 326 # process the web interface request
331 327
332 328 try:
333 329 tmpl = self.templater(req)
334 330 ctype = tmpl('mimetype', encoding=encoding.encoding)
335 331 ctype = templater.stringify(ctype)
336 332
337 333 # check read permissions non-static content
338 334 if cmd != 'static':
339 335 self.check_perm(rctx, req, None)
340 336
341 337 if cmd == '':
342 338 req.form['cmd'] = [tmpl.cache['default']]
343 339 cmd = req.form['cmd'][0]
344 340
345 if self.configbool('web', 'cache', True):
341 if rctx.configbool('web', 'cache', True):
346 342 caching(self, req) # sets ETag header or raises NOT_MODIFIED
347 343 if cmd not in webcommands.__all__:
348 344 msg = 'no such method: %s' % cmd
349 345 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
350 346 elif cmd == 'file' and 'raw' in req.form.get('style', []):
351 347 self.ctype = ctype
352 348 content = webcommands.rawfile(rctx, req, tmpl)
353 349 else:
354 350 content = getattr(webcommands, cmd)(rctx, req, tmpl)
355 351 req.respond(HTTP_OK, ctype)
356 352
357 353 return content
358 354
359 355 except (error.LookupError, error.RepoLookupError) as err:
360 356 req.respond(HTTP_NOT_FOUND, ctype)
361 357 msg = str(err)
362 358 if (util.safehasattr(err, 'name') and
363 359 not isinstance(err, error.ManifestLookupError)):
364 360 msg = 'revision not found: %s' % err.name
365 361 return tmpl('error', error=msg)
366 362 except (error.RepoError, error.RevlogError) as inst:
367 363 req.respond(HTTP_SERVER_ERROR, ctype)
368 364 return tmpl('error', error=str(inst))
369 365 except ErrorResponse as inst:
370 366 req.respond(inst, ctype)
371 367 if inst.code == HTTP_NOT_MODIFIED:
372 368 # Not allowed to return a body on a 304
373 369 return ['']
374 370 return tmpl('error', error=inst.message)
375 371
376 372 def loadwebsub(self):
377 373 websubtable = []
378 374 websubdefs = self.repo.ui.configitems('websub')
379 375 # we must maintain interhg backwards compatibility
380 376 websubdefs += self.repo.ui.configitems('interhg')
381 377 for key, pattern in websubdefs:
382 378 # grab the delimiter from the character after the "s"
383 379 unesc = pattern[1]
384 380 delim = re.escape(unesc)
385 381
386 382 # identify portions of the pattern, taking care to avoid escaped
387 383 # delimiters. the replace format and flags are optional, but
388 384 # delimiters are required.
389 385 match = re.match(
390 386 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
391 387 % (delim, delim, delim), pattern)
392 388 if not match:
393 389 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
394 390 % (key, pattern))
395 391 continue
396 392
397 393 # we need to unescape the delimiter for regexp and format
398 394 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
399 395 regexp = delim_re.sub(unesc, match.group(1))
400 396 format = delim_re.sub(unesc, match.group(2))
401 397
402 398 # the pattern allows for 6 regexp flags, so set them if necessary
403 399 flagin = match.group(3)
404 400 flags = 0
405 401 if flagin:
406 402 for flag in flagin.upper():
407 403 flags |= re.__dict__[flag]
408 404
409 405 try:
410 406 regexp = re.compile(regexp, flags)
411 407 websubtable.append((regexp, format))
412 408 except re.error:
413 409 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
414 410 % (key, regexp))
415 411 return websubtable
416 412
417 413 def templater(self, req):
418 414
419 415 # determine scheme, port and server name
420 416 # this is needed to create absolute urls
421 417
422 418 proto = req.env.get('wsgi.url_scheme')
423 419 if proto == 'https':
424 420 proto = 'https'
425 421 default_port = "443"
426 422 else:
427 423 proto = 'http'
428 424 default_port = "80"
429 425
430 426 port = req.env["SERVER_PORT"]
431 427 port = port != default_port and (":" + port) or ""
432 428 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
433 429 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
434 430 logoimg = self.config("web", "logoimg", "hglogo.png")
435 431 staticurl = self.config("web", "staticurl") or req.url + 'static/'
436 432 if not staticurl.endswith('/'):
437 433 staticurl += '/'
438 434
439 435 # some functions for the templater
440 436
441 437 def motd(**map):
442 438 yield self.config("web", "motd", "")
443 439
444 440 # figure out which style to use
445 441
446 442 vars = {}
447 443 styles = (
448 444 req.form.get('style', [None])[0],
449 445 self.config('web', 'style'),
450 446 'paper',
451 447 )
452 448 style, mapfile = templater.stylemap(styles, self.templatepath)
453 449 if style == styles[0]:
454 450 vars['style'] = style
455 451
456 452 start = req.url[-1] == '?' and '&' or '?'
457 453 sessionvars = webutil.sessionvars(vars, start)
458 454
459 455 if not self.reponame:
460 456 self.reponame = (self.config("web", "name")
461 457 or req.env.get('REPO_NAME')
462 458 or req.url.strip('/') or self.repo.root)
463 459
464 460 def websubfilter(text):
465 461 return websub(text, self.websubtable)
466 462
467 463 # create the templater
468 464
469 465 tmpl = templater.templater(mapfile,
470 466 filters={"websub": websubfilter},
471 467 defaults={"url": req.url,
472 468 "logourl": logourl,
473 469 "logoimg": logoimg,
474 470 "staticurl": staticurl,
475 471 "urlbase": urlbase,
476 472 "repo": self.reponame,
477 473 "encoding": encoding.encoding,
478 474 "motd": motd,
479 475 "sessionvars": sessionvars,
480 476 "pathdef": makebreadcrumb(req.url),
481 477 "style": style,
482 478 })
483 479 return tmpl
484 480
485 481 def check_perm(self, rctx, req, op):
486 482 for permhook in permhooks:
487 483 permhook(rctx, req, op)
General Comments 0
You need to be logged in to leave comments. Login now