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