##// END OF EJS Templates
hgweb: remove some use of wsgireq in hgwebdir...
Gregory Szorc -
r36905:fc4e3129 default
parent child Browse files
Show More
@@ -1,553 +1,554 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 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 from __future__ import absolute_import
10 10
11 11 import os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 cspvalues,
23 23 get_contact,
24 24 get_mtime,
25 25 ismember,
26 26 paritygen,
27 27 staticfile,
28 28 )
29 29
30 30 from .. import (
31 31 configitems,
32 32 encoding,
33 33 error,
34 34 hg,
35 35 profiling,
36 36 pycompat,
37 37 scmutil,
38 38 templater,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 request as requestmod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49 from ..utils import dateutil
50 50
51 51 def cleannames(items):
52 52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 53
54 54 def findrepos(paths):
55 55 repos = []
56 56 for prefix, root in cleannames(paths):
57 57 roothead, roottail = os.path.split(root)
58 58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 59 # /bar/ be served as as foo/N .
60 60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 62 try:
63 63 recurse = {'*': False, '**': True}[roottail]
64 64 except KeyError:
65 65 repos.append((prefix, root))
66 66 continue
67 67 roothead = os.path.normpath(os.path.abspath(roothead))
68 68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 69 repos.extend(urlrepos(prefix, roothead, paths))
70 70 return repos
71 71
72 72 def urlrepos(prefix, roothead, paths):
73 73 """yield url paths and filesystem paths from a list of repo paths
74 74
75 75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 80 """
81 81 for path in paths:
82 82 path = os.path.normpath(path)
83 83 yield (prefix + '/' +
84 84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 85
86 86 def geturlcgivars(baseurl, port):
87 87 """
88 88 Extract CGI variables from baseurl
89 89
90 90 >>> geturlcgivars(b"http://host.org/base", b"80")
91 91 ('host.org', '80', '/base')
92 92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
93 93 ('host.org', '8000', '/base')
94 94 >>> geturlcgivars(b'/base', 8000)
95 95 ('', '8000', '/base')
96 96 >>> geturlcgivars(b"base", b'8000')
97 97 ('', '8000', '/base')
98 98 >>> geturlcgivars(b"http://host", b'8000')
99 99 ('host', '8000', '/')
100 100 >>> geturlcgivars(b"http://host/", b'8000')
101 101 ('host', '8000', '/')
102 102 """
103 103 u = util.url(baseurl)
104 104 name = u.host or ''
105 105 if u.port:
106 106 port = u.port
107 107 path = u.path or ""
108 108 if not path.startswith('/'):
109 109 path = '/' + path
110 110
111 111 return name, pycompat.bytestr(port), path
112 112
113 113 class hgwebdir(object):
114 114 """HTTP server for multiple repositories.
115 115
116 116 Given a configuration, different repositories will be served depending
117 117 on the request path.
118 118
119 119 Instances are typically used as WSGI applications.
120 120 """
121 121 def __init__(self, conf, baseui=None):
122 122 self.conf = conf
123 123 self.baseui = baseui
124 124 self.ui = None
125 125 self.lastrefresh = 0
126 126 self.motd = None
127 127 self.refresh()
128 128
129 129 def refresh(self):
130 130 if self.ui:
131 131 refreshinterval = self.ui.configint('web', 'refreshinterval')
132 132 else:
133 133 item = configitems.coreitems['web']['refreshinterval']
134 134 refreshinterval = item.default
135 135
136 136 # refreshinterval <= 0 means to always refresh.
137 137 if (refreshinterval > 0 and
138 138 self.lastrefresh + refreshinterval > time.time()):
139 139 return
140 140
141 141 if self.baseui:
142 142 u = self.baseui.copy()
143 143 else:
144 144 u = uimod.ui.load()
145 145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
146 146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
147 147 # displaying bundling progress bar while serving feels wrong and may
148 148 # break some wsgi implementations.
149 149 u.setconfig('progress', 'disable', 'true', 'hgweb')
150 150
151 151 if not isinstance(self.conf, (dict, list, tuple)):
152 152 map = {'paths': 'hgweb-paths'}
153 153 if not os.path.exists(self.conf):
154 154 raise error.Abort(_('config file %s not found!') % self.conf)
155 155 u.readconfig(self.conf, remap=map, trust=True)
156 156 paths = []
157 157 for name, ignored in u.configitems('hgweb-paths'):
158 158 for path in u.configlist('hgweb-paths', name):
159 159 paths.append((name, path))
160 160 elif isinstance(self.conf, (list, tuple)):
161 161 paths = self.conf
162 162 elif isinstance(self.conf, dict):
163 163 paths = self.conf.items()
164 164
165 165 repos = findrepos(paths)
166 166 for prefix, root in u.configitems('collections'):
167 167 prefix = util.pconvert(prefix)
168 168 for path in scmutil.walkrepos(root, followsym=True):
169 169 repo = os.path.normpath(path)
170 170 name = util.pconvert(repo)
171 171 if name.startswith(prefix):
172 172 name = name[len(prefix):]
173 173 repos.append((name.lstrip('/'), repo))
174 174
175 175 self.repos = repos
176 176 self.ui = u
177 177 encoding.encoding = self.ui.config('web', 'encoding')
178 178 self.style = self.ui.config('web', 'style')
179 179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
180 180 self.stripecount = self.ui.config('web', 'stripes')
181 181 if self.stripecount:
182 182 self.stripecount = int(self.stripecount)
183 183 self._baseurl = self.ui.config('web', 'baseurl')
184 184 prefix = self.ui.config('web', 'prefix')
185 185 if prefix.startswith('/'):
186 186 prefix = prefix[1:]
187 187 if prefix.endswith('/'):
188 188 prefix = prefix[:-1]
189 189 self.prefix = prefix
190 190 self.lastrefresh = time.time()
191 191
192 192 def run(self):
193 193 if not encoding.environ.get('GATEWAY_INTERFACE',
194 194 '').startswith("CGI/1."):
195 195 raise RuntimeError("This function is only intended to be "
196 196 "called while running as a CGI script.")
197 197 wsgicgi.launch(self)
198 198
199 199 def __call__(self, env, respond):
200 200 wsgireq = requestmod.wsgirequest(env, respond)
201 201 return self.run_wsgi(wsgireq)
202 202
203 def read_allowed(self, ui, wsgireq):
203 def readallowed(self, ui, req):
204 204 """Check allow_read and deny_read config options of a repo's ui object
205 205 to determine user permissions. By default, with neither option set (or
206 206 both empty), allow all users to read the repo. There are two ways a
207 207 user can be denied read access: (1) deny_read is not empty, and the
208 208 user is unauthenticated or deny_read contains user (or *), and (2)
209 209 allow_read is not empty and the user is not in allow_read. Return True
210 210 if user is allowed to read the repo, else return False."""
211 211
212 user = wsgireq.env.get('REMOTE_USER')
212 user = req.remoteuser
213 213
214 214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
215 215 if deny_read and (not user or ismember(ui, user, deny_read)):
216 216 return False
217 217
218 218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
219 219 # by default, allow reading if no allow_read option has been set
220 220 if (not allow_read) or ismember(ui, user, allow_read):
221 221 return True
222 222
223 223 return False
224 224
225 225 def run_wsgi(self, wsgireq):
226 226 profile = self.ui.configbool('profiling', 'enabled')
227 227 with profiling.profile(self.ui, enabled=profile):
228 228 for r in self._runwsgi(wsgireq):
229 229 yield r
230 230
231 231 def _runwsgi(self, wsgireq):
232 232 req = wsgireq.req
233 233 res = wsgireq.res
234 234
235 235 try:
236 236 self.refresh()
237 237
238 238 csp, nonce = cspvalues(self.ui)
239 239 if csp:
240 240 res.headers['Content-Security-Policy'] = csp
241 241 wsgireq.headers.append(('Content-Security-Policy', csp))
242 242
243 243 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
244 244 tmpl = self.templater(wsgireq, nonce)
245 245 ctype = tmpl('mimetype', encoding=encoding.encoding)
246 246 ctype = templater.stringify(ctype)
247 247
248 248 # Global defaults. These can be overridden by any handler.
249 249 res.status = '200 Script output follows'
250 250 res.headers['Content-Type'] = ctype
251 251
252 252 # a static file
253 253 if virtual.startswith('static/') or 'static' in req.qsparams:
254 254 if virtual.startswith('static/'):
255 255 fname = virtual[7:]
256 256 else:
257 257 fname = req.qsparams['static']
258 258 static = self.ui.config("web", "static", None,
259 259 untrusted=False)
260 260 if not static:
261 261 tp = self.templatepath or templater.templatepaths()
262 262 if isinstance(tp, str):
263 263 tp = [tp]
264 264 static = [os.path.join(p, 'static') for p in tp]
265 265
266 266 staticfile(static, fname, res)
267 267 return res.sendresponse()
268 268
269 269 # top-level index
270 270
271 271 repos = dict(self.repos)
272 272
273 273 if (not virtual or virtual == 'index') and virtual not in repos:
274 274 wsgireq.respond(HTTP_OK, ctype)
275 275 return self.makeindex(wsgireq, tmpl)
276 276
277 277 # nested indexes and hgwebs
278 278
279 279 if virtual.endswith('/index') and virtual not in repos:
280 280 subdir = virtual[:-len('index')]
281 281 if any(r.startswith(subdir) for r in repos):
282 282 wsgireq.respond(HTTP_OK, ctype)
283 283 return self.makeindex(wsgireq, tmpl, subdir)
284 284
285 285 def _virtualdirs():
286 286 # Check the full virtual path, each parent, and the root ('')
287 287 if virtual != '':
288 288 yield virtual
289 289
290 290 for p in util.finddirs(virtual):
291 291 yield p
292 292
293 293 yield ''
294 294
295 295 for virtualrepo in _virtualdirs():
296 296 real = repos.get(virtualrepo)
297 297 if real:
298 298 wsgireq.env['REPO_NAME'] = virtualrepo
299 299 # We have to re-parse because of updated environment
300 300 # variable.
301 301 # TODO this is kind of hacky and we should have a better
302 302 # way of doing this than with REPO_NAME side-effects.
303 303 wsgireq.req = requestmod.parserequestfromenv(
304 304 wsgireq.env, wsgireq.req.bodyfh)
305 305 try:
306 306 # ensure caller gets private copy of ui
307 307 repo = hg.repository(self.ui.copy(), real)
308 308 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
309 309 except IOError as inst:
310 310 msg = encoding.strtolocal(inst.strerror)
311 311 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
312 312 except error.RepoError as inst:
313 313 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
314 314
315 315 # browse subdirectories
316 316 subdir = virtual + '/'
317 317 if [r for r in repos if r.startswith(subdir)]:
318 318 wsgireq.respond(HTTP_OK, ctype)
319 319 return self.makeindex(wsgireq, tmpl, subdir)
320 320
321 321 # prefixes not found
322 322 wsgireq.respond(HTTP_NOT_FOUND, ctype)
323 323 return tmpl("notfound", repo=virtual)
324 324
325 325 except ErrorResponse as err:
326 326 wsgireq.respond(err, ctype)
327 327 return tmpl('error', error=err.message or '')
328 328 finally:
329 329 tmpl = None
330 330
331 331 def makeindex(self, wsgireq, tmpl, subdir=""):
332 req = wsgireq.req
332 333
333 334 def archivelist(ui, nodeid, url):
334 335 allowed = ui.configlist("web", "allow_archive", untrusted=True)
335 336 archives = []
336 337 for typ, spec in hgweb_mod.archivespecs.iteritems():
337 338 if typ in allowed or ui.configbool("web", "allow" + typ,
338 339 untrusted=True):
339 340 archives.append({"type": typ, "extension": spec[2],
340 341 "node": nodeid, "url": url})
341 342 return archives
342 343
343 344 def rawentries(subdir="", **map):
344 345
345 346 descend = self.ui.configbool('web', 'descend')
346 347 collapse = self.ui.configbool('web', 'collapse')
347 348 seenrepos = set()
348 349 seendirs = set()
349 350 for name, path in self.repos:
350 351
351 352 if not name.startswith(subdir):
352 353 continue
353 354 name = name[len(subdir):]
354 355 directory = False
355 356
356 357 if '/' in name:
357 358 if not descend:
358 359 continue
359 360
360 361 nameparts = name.split('/')
361 362 rootname = nameparts[0]
362 363
363 364 if not collapse:
364 365 pass
365 366 elif rootname in seendirs:
366 367 continue
367 368 elif rootname in seenrepos:
368 369 pass
369 370 else:
370 371 directory = True
371 372 name = rootname
372 373
373 374 # redefine the path to refer to the directory
374 375 discarded = '/'.join(nameparts[1:])
375 376
376 377 # remove name parts plus accompanying slash
377 378 path = path[:-len(discarded) - 1]
378 379
379 380 try:
380 381 r = hg.repository(self.ui, path)
381 382 directory = False
382 383 except (IOError, error.RepoError):
383 384 pass
384 385
385 386 parts = [name]
386 387 parts.insert(0, '/' + subdir.rstrip('/'))
387 388 if wsgireq.env['SCRIPT_NAME']:
388 389 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
389 390 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
390 391
391 392 # show either a directory entry or a repository
392 393 if directory:
393 394 # get the directory's time information
394 395 try:
395 396 d = (get_mtime(path), dateutil.makedate()[1])
396 397 except OSError:
397 398 continue
398 399
399 400 # add '/' to the name to make it obvious that
400 401 # the entry is a directory, not a regular repository
401 402 row = {'contact': "",
402 403 'contact_sort': "",
403 404 'name': name + '/',
404 405 'name_sort': name,
405 406 'url': url,
406 407 'description': "",
407 408 'description_sort': "",
408 409 'lastchange': d,
409 410 'lastchange_sort': d[1]-d[0],
410 411 'archives': [],
411 412 'isdirectory': True,
412 413 'labels': [],
413 414 }
414 415
415 416 seendirs.add(name)
416 417 yield row
417 418 continue
418 419
419 420 u = self.ui.copy()
420 421 try:
421 422 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
422 423 except Exception as e:
423 424 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
424 425 continue
425 426 def get(section, name, default=uimod._unset):
426 427 return u.config(section, name, default, untrusted=True)
427 428
428 429 if u.configbool("web", "hidden", untrusted=True):
429 430 continue
430 431
431 if not self.read_allowed(u, wsgireq):
432 if not self.readallowed(u, req):
432 433 continue
433 434
434 435 # update time with local timezone
435 436 try:
436 437 r = hg.repository(self.ui, path)
437 438 except IOError:
438 439 u.warn(_('error accessing repository at %s\n') % path)
439 440 continue
440 441 except error.RepoError:
441 442 u.warn(_('error accessing repository at %s\n') % path)
442 443 continue
443 444 try:
444 445 d = (get_mtime(r.spath), dateutil.makedate()[1])
445 446 except OSError:
446 447 continue
447 448
448 449 contact = get_contact(get)
449 450 description = get("web", "description")
450 451 seenrepos.add(name)
451 452 name = get("web", "name", name)
452 453 row = {'contact': contact or "unknown",
453 454 'contact_sort': contact.upper() or "unknown",
454 455 'name': name,
455 456 'name_sort': name,
456 457 'url': url,
457 458 'description': description or "unknown",
458 459 'description_sort': description.upper() or "unknown",
459 460 'lastchange': d,
460 461 'lastchange_sort': d[1]-d[0],
461 462 'archives': archivelist(u, "tip", url),
462 463 'isdirectory': None,
463 464 'labels': u.configlist('web', 'labels', untrusted=True),
464 465 }
465 466
466 467 yield row
467 468
468 469 sortdefault = None, False
469 470 def entries(sortcolumn="", descending=False, subdir="", **map):
470 471 rows = rawentries(subdir=subdir, **map)
471 472
472 473 if sortcolumn and sortdefault != (sortcolumn, descending):
473 474 sortkey = '%s_sort' % sortcolumn
474 475 rows = sorted(rows, key=lambda x: x[sortkey],
475 476 reverse=descending)
476 477 for row, parity in zip(rows, paritygen(self.stripecount)):
477 478 row['parity'] = parity
478 479 yield row
479 480
480 481 self.refresh()
481 482 sortable = ["name", "description", "contact", "lastchange"]
482 483 sortcolumn, descending = sortdefault
483 if 'sort' in wsgireq.req.qsparams:
484 sortcolumn = wsgireq.req.qsparams['sort']
484 if 'sort' in req.qsparams:
485 sortcolumn = req.qsparams['sort']
485 486 descending = sortcolumn.startswith('-')
486 487 if descending:
487 488 sortcolumn = sortcolumn[1:]
488 489 if sortcolumn not in sortable:
489 490 sortcolumn = ""
490 491
491 492 sort = [("sort_%s" % column,
492 493 "%s%s" % ((not descending and column == sortcolumn)
493 494 and "-" or "", column))
494 495 for column in sortable]
495 496
496 497 self.refresh()
497 498 self.updatereqenv(wsgireq.env)
498 499
499 500 return tmpl("index", entries=entries, subdir=subdir,
500 501 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
501 502 sortcolumn=sortcolumn, descending=descending,
502 503 **dict(sort))
503 504
504 505 def templater(self, wsgireq, nonce):
505 506
506 507 def motd(**map):
507 508 if self.motd is not None:
508 509 yield self.motd
509 510 else:
510 511 yield config('web', 'motd')
511 512
512 513 def config(section, name, default=uimod._unset, untrusted=True):
513 514 return self.ui.config(section, name, default, untrusted)
514 515
515 516 self.updatereqenv(wsgireq.env)
516 517
517 518 url = wsgireq.env.get('SCRIPT_NAME', '')
518 519 if not url.endswith('/'):
519 520 url += '/'
520 521
521 522 vars = {}
522 523 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq.req, config,
523 524 self.templatepath)
524 525 if style == styles[0]:
525 526 vars['style'] = style
526 527
527 528 sessionvars = webutil.sessionvars(vars, r'?')
528 529 logourl = config('web', 'logourl')
529 530 logoimg = config('web', 'logoimg')
530 531 staticurl = config('web', 'staticurl') or url + 'static/'
531 532 if not staticurl.endswith('/'):
532 533 staticurl += '/'
533 534
534 535 defaults = {
535 536 "encoding": encoding.encoding,
536 537 "motd": motd,
537 538 "url": url,
538 539 "logourl": logourl,
539 540 "logoimg": logoimg,
540 541 "staticurl": staticurl,
541 542 "sessionvars": sessionvars,
542 543 "style": style,
543 544 "nonce": nonce,
544 545 }
545 546 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
546 547 return tmpl
547 548
548 549 def updatereqenv(self, env):
549 550 if self._baseurl is not None:
550 551 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
551 552 env['SERVER_NAME'] = name
552 553 env['SERVER_PORT'] = port
553 554 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now