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