PageRenderTime 64ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/hgweb/hgwebdir_mod.py

https://bitbucket.org/mirror/mercurial/
Python | 463 lines | 445 code | 4 blank | 14 comment | 11 complexity | ea6e73e9bb93470e8e64cabfa7bba112 MD5 | raw file
Possible License(s): GPL-2.0
  1. # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
  2. #
  3. # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
  4. # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
  5. #
  6. # This software may be used and distributed according to the terms of the
  7. # GNU General Public License version 2 or any later version.
  8. import os, re, time
  9. from mercurial.i18n import _
  10. from mercurial import ui, hg, scmutil, util, templater
  11. from mercurial import error, encoding
  12. from common import ErrorResponse, get_mtime, staticfile, paritygen, ismember, \
  13. get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
  14. from hgweb_mod import hgweb, makebreadcrumb
  15. from request import wsgirequest
  16. import webutil
  17. def cleannames(items):
  18. return [(util.pconvert(name).strip('/'), path) for name, path in items]
  19. def findrepos(paths):
  20. repos = []
  21. for prefix, root in cleannames(paths):
  22. roothead, roottail = os.path.split(root)
  23. # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
  24. # /bar/ be served as as foo/N .
  25. # '*' will not search inside dirs with .hg (except .hg/patches),
  26. # '**' will search inside dirs with .hg (and thus also find subrepos).
  27. try:
  28. recurse = {'*': False, '**': True}[roottail]
  29. except KeyError:
  30. repos.append((prefix, root))
  31. continue
  32. roothead = os.path.normpath(os.path.abspath(roothead))
  33. paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
  34. repos.extend(urlrepos(prefix, roothead, paths))
  35. return repos
  36. def urlrepos(prefix, roothead, paths):
  37. """yield url paths and filesystem paths from a list of repo paths
  38. >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
  39. >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
  40. [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
  41. >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
  42. [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
  43. """
  44. for path in paths:
  45. path = os.path.normpath(path)
  46. yield (prefix + '/' +
  47. util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
  48. def geturlcgivars(baseurl, port):
  49. """
  50. Extract CGI variables from baseurl
  51. >>> geturlcgivars("http://host.org/base", "80")
  52. ('host.org', '80', '/base')
  53. >>> geturlcgivars("http://host.org:8000/base", "80")
  54. ('host.org', '8000', '/base')
  55. >>> geturlcgivars('/base', 8000)
  56. ('', '8000', '/base')
  57. >>> geturlcgivars("base", '8000')
  58. ('', '8000', '/base')
  59. >>> geturlcgivars("http://host", '8000')
  60. ('host', '8000', '/')
  61. >>> geturlcgivars("http://host/", '8000')
  62. ('host', '8000', '/')
  63. """
  64. u = util.url(baseurl)
  65. name = u.host or ''
  66. if u.port:
  67. port = u.port
  68. path = u.path or ""
  69. if not path.startswith('/'):
  70. path = '/' + path
  71. return name, str(port), path
  72. class hgwebdir(object):
  73. refreshinterval = 20
  74. def __init__(self, conf, baseui=None):
  75. self.conf = conf
  76. self.baseui = baseui
  77. self.lastrefresh = 0
  78. self.motd = None
  79. self.refresh()
  80. def refresh(self):
  81. if self.lastrefresh + self.refreshinterval > time.time():
  82. return
  83. if self.baseui:
  84. u = self.baseui.copy()
  85. else:
  86. u = ui.ui()
  87. u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
  88. u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
  89. if not isinstance(self.conf, (dict, list, tuple)):
  90. map = {'paths': 'hgweb-paths'}
  91. if not os.path.exists(self.conf):
  92. raise util.Abort(_('config file %s not found!') % self.conf)
  93. u.readconfig(self.conf, remap=map, trust=True)
  94. paths = []
  95. for name, ignored in u.configitems('hgweb-paths'):
  96. for path in u.configlist('hgweb-paths', name):
  97. paths.append((name, path))
  98. elif isinstance(self.conf, (list, tuple)):
  99. paths = self.conf
  100. elif isinstance(self.conf, dict):
  101. paths = self.conf.items()
  102. repos = findrepos(paths)
  103. for prefix, root in u.configitems('collections'):
  104. prefix = util.pconvert(prefix)
  105. for path in scmutil.walkrepos(root, followsym=True):
  106. repo = os.path.normpath(path)
  107. name = util.pconvert(repo)
  108. if name.startswith(prefix):
  109. name = name[len(prefix):]
  110. repos.append((name.lstrip('/'), repo))
  111. self.repos = repos
  112. self.ui = u
  113. encoding.encoding = self.ui.config('web', 'encoding',
  114. encoding.encoding)
  115. self.style = self.ui.config('web', 'style', 'paper')
  116. self.templatepath = self.ui.config('web', 'templates', None)
  117. self.stripecount = self.ui.config('web', 'stripes', 1)
  118. if self.stripecount:
  119. self.stripecount = int(self.stripecount)
  120. self._baseurl = self.ui.config('web', 'baseurl')
  121. prefix = self.ui.config('web', 'prefix', '')
  122. if prefix.startswith('/'):
  123. prefix = prefix[1:]
  124. if prefix.endswith('/'):
  125. prefix = prefix[:-1]
  126. self.prefix = prefix
  127. self.lastrefresh = time.time()
  128. def run(self):
  129. if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
  130. raise RuntimeError("This function is only intended to be "
  131. "called while running as a CGI script.")
  132. import mercurial.hgweb.wsgicgi as wsgicgi
  133. wsgicgi.launch(self)
  134. def __call__(self, env, respond):
  135. req = wsgirequest(env, respond)
  136. return self.run_wsgi(req)
  137. def read_allowed(self, ui, req):
  138. """Check allow_read and deny_read config options of a repo's ui object
  139. to determine user permissions. By default, with neither option set (or
  140. both empty), allow all users to read the repo. There are two ways a
  141. user can be denied read access: (1) deny_read is not empty, and the
  142. user is unauthenticated or deny_read contains user (or *), and (2)
  143. allow_read is not empty and the user is not in allow_read. Return True
  144. if user is allowed to read the repo, else return False."""
  145. user = req.env.get('REMOTE_USER')
  146. deny_read = ui.configlist('web', 'deny_read', untrusted=True)
  147. if deny_read and (not user or ismember(ui, user, deny_read)):
  148. return False
  149. allow_read = ui.configlist('web', 'allow_read', untrusted=True)
  150. # by default, allow reading if no allow_read option has been set
  151. if (not allow_read) or ismember(ui, user, allow_read):
  152. return True
  153. return False
  154. def run_wsgi(self, req):
  155. try:
  156. try:
  157. self.refresh()
  158. virtual = req.env.get("PATH_INFO", "").strip('/')
  159. tmpl = self.templater(req)
  160. ctype = tmpl('mimetype', encoding=encoding.encoding)
  161. ctype = templater.stringify(ctype)
  162. # a static file
  163. if virtual.startswith('static/') or 'static' in req.form:
  164. if virtual.startswith('static/'):
  165. fname = virtual[7:]
  166. else:
  167. fname = req.form['static'][0]
  168. static = self.ui.config("web", "static", None,
  169. untrusted=False)
  170. if not static:
  171. tp = self.templatepath or templater.templatepath()
  172. if isinstance(tp, str):
  173. tp = [tp]
  174. static = [os.path.join(p, 'static') for p in tp]
  175. staticfile(static, fname, req)
  176. return []
  177. # top-level index
  178. elif not virtual:
  179. req.respond(HTTP_OK, ctype)
  180. return self.makeindex(req, tmpl)
  181. # nested indexes and hgwebs
  182. repos = dict(self.repos)
  183. virtualrepo = virtual
  184. while virtualrepo:
  185. real = repos.get(virtualrepo)
  186. if real:
  187. req.env['REPO_NAME'] = virtualrepo
  188. try:
  189. repo = hg.repository(self.ui, real)
  190. return hgweb(repo).run_wsgi(req)
  191. except IOError, inst:
  192. msg = inst.strerror
  193. raise ErrorResponse(HTTP_SERVER_ERROR, msg)
  194. except error.RepoError, inst:
  195. raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
  196. up = virtualrepo.rfind('/')
  197. if up < 0:
  198. break
  199. virtualrepo = virtualrepo[:up]
  200. # browse subdirectories
  201. subdir = virtual + '/'
  202. if [r for r in repos if r.startswith(subdir)]:
  203. req.respond(HTTP_OK, ctype)
  204. return self.makeindex(req, tmpl, subdir)
  205. # prefixes not found
  206. req.respond(HTTP_NOT_FOUND, ctype)
  207. return tmpl("notfound", repo=virtual)
  208. except ErrorResponse, err:
  209. req.respond(err, ctype)
  210. return tmpl('error', error=err.message or '')
  211. finally:
  212. tmpl = None
  213. def makeindex(self, req, tmpl, subdir=""):
  214. def archivelist(ui, nodeid, url):
  215. allowed = ui.configlist("web", "allow_archive", untrusted=True)
  216. archives = []
  217. for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
  218. if i[0] in allowed or ui.configbool("web", "allow" + i[0],
  219. untrusted=True):
  220. archives.append({"type" : i[0], "extension": i[1],
  221. "node": nodeid, "url": url})
  222. return archives
  223. def rawentries(subdir="", **map):
  224. descend = self.ui.configbool('web', 'descend', True)
  225. collapse = self.ui.configbool('web', 'collapse', False)
  226. seenrepos = set()
  227. seendirs = set()
  228. for name, path in self.repos:
  229. if not name.startswith(subdir):
  230. continue
  231. name = name[len(subdir):]
  232. directory = False
  233. if '/' in name:
  234. if not descend:
  235. continue
  236. nameparts = name.split('/')
  237. rootname = nameparts[0]
  238. if not collapse:
  239. pass
  240. elif rootname in seendirs:
  241. continue
  242. elif rootname in seenrepos:
  243. pass
  244. else:
  245. directory = True
  246. name = rootname
  247. # redefine the path to refer to the directory
  248. discarded = '/'.join(nameparts[1:])
  249. # remove name parts plus accompanying slash
  250. path = path[:-len(discarded) - 1]
  251. parts = [name]
  252. if 'PATH_INFO' in req.env:
  253. parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
  254. if req.env['SCRIPT_NAME']:
  255. parts.insert(0, req.env['SCRIPT_NAME'])
  256. url = re.sub(r'/+', '/', '/'.join(parts) + '/')
  257. # show either a directory entry or a repository
  258. if directory:
  259. # get the directory's time information
  260. try:
  261. d = (get_mtime(path), util.makedate()[1])
  262. except OSError:
  263. continue
  264. # add '/' to the name to make it obvious that
  265. # the entry is a directory, not a regular repository
  266. row = {'contact': "",
  267. 'contact_sort': "",
  268. 'name': name + '/',
  269. 'name_sort': name,
  270. 'url': url,
  271. 'description': "",
  272. 'description_sort': "",
  273. 'lastchange': d,
  274. 'lastchange_sort': d[1]-d[0],
  275. 'archives': [],
  276. 'isdirectory': True}
  277. seendirs.add(name)
  278. yield row
  279. continue
  280. u = self.ui.copy()
  281. try:
  282. u.readconfig(os.path.join(path, '.hg', 'hgrc'))
  283. except Exception, e:
  284. u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
  285. continue
  286. def get(section, name, default=None):
  287. return u.config(section, name, default, untrusted=True)
  288. if u.configbool("web", "hidden", untrusted=True):
  289. continue
  290. if not self.read_allowed(u, req):
  291. continue
  292. # update time with local timezone
  293. try:
  294. r = hg.repository(self.ui, path)
  295. except IOError:
  296. u.warn(_('error accessing repository at %s\n') % path)
  297. continue
  298. except error.RepoError:
  299. u.warn(_('error accessing repository at %s\n') % path)
  300. continue
  301. try:
  302. d = (get_mtime(r.spath), util.makedate()[1])
  303. except OSError:
  304. continue
  305. contact = get_contact(get)
  306. description = get("web", "description", "")
  307. name = get("web", "name", name)
  308. row = {'contact': contact or "unknown",
  309. 'contact_sort': contact.upper() or "unknown",
  310. 'name': name,
  311. 'name_sort': name,
  312. 'url': url,
  313. 'description': description or "unknown",
  314. 'description_sort': description.upper() or "unknown",
  315. 'lastchange': d,
  316. 'lastchange_sort': d[1]-d[0],
  317. 'archives': archivelist(u, "tip", url),
  318. 'isdirectory': None,
  319. }
  320. seenrepos.add(name)
  321. yield row
  322. sortdefault = None, False
  323. def entries(sortcolumn="", descending=False, subdir="", **map):
  324. rows = rawentries(subdir=subdir, **map)
  325. if sortcolumn and sortdefault != (sortcolumn, descending):
  326. sortkey = '%s_sort' % sortcolumn
  327. rows = sorted(rows, key=lambda x: x[sortkey],
  328. reverse=descending)
  329. for row, parity in zip(rows, paritygen(self.stripecount)):
  330. row['parity'] = parity
  331. yield row
  332. self.refresh()
  333. sortable = ["name", "description", "contact", "lastchange"]
  334. sortcolumn, descending = sortdefault
  335. if 'sort' in req.form:
  336. sortcolumn = req.form['sort'][0]
  337. descending = sortcolumn.startswith('-')
  338. if descending:
  339. sortcolumn = sortcolumn[1:]
  340. if sortcolumn not in sortable:
  341. sortcolumn = ""
  342. sort = [("sort_%s" % column,
  343. "%s%s" % ((not descending and column == sortcolumn)
  344. and "-" or "", column))
  345. for column in sortable]
  346. self.refresh()
  347. self.updatereqenv(req.env)
  348. return tmpl("index", entries=entries, subdir=subdir,
  349. pathdef=makebreadcrumb('/' + subdir, self.prefix),
  350. sortcolumn=sortcolumn, descending=descending,
  351. **dict(sort))
  352. def templater(self, req):
  353. def motd(**map):
  354. if self.motd is not None:
  355. yield self.motd
  356. else:
  357. yield config('web', 'motd', '')
  358. def config(section, name, default=None, untrusted=True):
  359. return self.ui.config(section, name, default, untrusted)
  360. self.updatereqenv(req.env)
  361. url = req.env.get('SCRIPT_NAME', '')
  362. if not url.endswith('/'):
  363. url += '/'
  364. vars = {}
  365. styles = (
  366. req.form.get('style', [None])[0],
  367. config('web', 'style'),
  368. 'paper'
  369. )
  370. style, mapfile = templater.stylemap(styles, self.templatepath)
  371. if style == styles[0]:
  372. vars['style'] = style
  373. start = url[-1] == '?' and '&' or '?'
  374. sessionvars = webutil.sessionvars(vars, start)
  375. logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
  376. logoimg = config('web', 'logoimg', 'hglogo.png')
  377. staticurl = config('web', 'staticurl') or url + 'static/'
  378. if not staticurl.endswith('/'):
  379. staticurl += '/'
  380. tmpl = templater.templater(mapfile,
  381. defaults={"encoding": encoding.encoding,
  382. "motd": motd,
  383. "url": url,
  384. "logourl": logourl,
  385. "logoimg": logoimg,
  386. "staticurl": staticurl,
  387. "sessionvars": sessionvars,
  388. "style": style,
  389. })
  390. return tmpl
  391. def updatereqenv(self, env):
  392. if self._baseurl is not None:
  393. name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
  394. env['SERVER_NAME'] = name
  395. env['SERVER_PORT'] = port
  396. env['SCRIPT_NAME'] = path