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