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