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