eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/hgweb/hgwebdir_mod.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     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 
       
     9 import os, re, time, urlparse
       
    10 from mercurial.i18n import _
       
    11 from mercurial import ui, hg, util, templater
       
    12 from mercurial import error, encoding
       
    13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
       
    14                    get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
       
    15 from hgweb_mod import hgweb
       
    16 from request import wsgirequest
       
    17 import webutil
       
    18 
       
    19 def cleannames(items):
       
    20     return [(util.pconvert(name).strip('/'), path) for name, path in items]
       
    21 
       
    22 def findrepos(paths):
       
    23     repos = []
       
    24     for prefix, root in cleannames(paths):
       
    25         roothead, roottail = os.path.split(root)
       
    26         # "foo = /bar/*" makes every subrepo of /bar/ to be
       
    27         # mounted as foo/subrepo
       
    28         # and "foo = /bar/**" also recurses into the subdirectories,
       
    29         # remember to use it without working dir.
       
    30         try:
       
    31             recurse = {'*': False, '**': True}[roottail]
       
    32         except KeyError:
       
    33             repos.append((prefix, root))
       
    34             continue
       
    35         roothead = os.path.normpath(os.path.abspath(roothead))
       
    36         for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
       
    37             path = os.path.normpath(path)
       
    38             name = util.pconvert(path[len(roothead):]).strip('/')
       
    39             if prefix:
       
    40                 name = prefix + '/' + name
       
    41             repos.append((name, path))
       
    42     return repos
       
    43 
       
    44 class hgwebdir(object):
       
    45     refreshinterval = 20
       
    46 
       
    47     def __init__(self, conf, baseui=None):
       
    48         self.conf = conf
       
    49         self.baseui = baseui
       
    50         self.lastrefresh = 0
       
    51         self.motd = None
       
    52         self.refresh()
       
    53 
       
    54     def refresh(self):
       
    55         if self.lastrefresh + self.refreshinterval > time.time():
       
    56             return
       
    57 
       
    58         if self.baseui:
       
    59             u = self.baseui.copy()
       
    60         else:
       
    61             u = ui.ui()
       
    62             u.setconfig('ui', 'report_untrusted', 'off')
       
    63             u.setconfig('ui', 'interactive', 'off')
       
    64 
       
    65         if not isinstance(self.conf, (dict, list, tuple)):
       
    66             map = {'paths': 'hgweb-paths'}
       
    67             if not os.path.exists(self.conf):
       
    68                 raise util.Abort(_('config file %s not found!') % self.conf)
       
    69             u.readconfig(self.conf, remap=map, trust=True)
       
    70             paths = u.configitems('hgweb-paths')
       
    71         elif isinstance(self.conf, (list, tuple)):
       
    72             paths = self.conf
       
    73         elif isinstance(self.conf, dict):
       
    74             paths = self.conf.items()
       
    75 
       
    76         repos = findrepos(paths)
       
    77         for prefix, root in u.configitems('collections'):
       
    78             prefix = util.pconvert(prefix)
       
    79             for path in util.walkrepos(root, followsym=True):
       
    80                 repo = os.path.normpath(path)
       
    81                 name = util.pconvert(repo)
       
    82                 if name.startswith(prefix):
       
    83                     name = name[len(prefix):]
       
    84                 repos.append((name.lstrip('/'), repo))
       
    85 
       
    86         self.repos = repos
       
    87         self.ui = u
       
    88         encoding.encoding = self.ui.config('web', 'encoding',
       
    89                                            encoding.encoding)
       
    90         self.style = self.ui.config('web', 'style', 'paper')
       
    91         self.templatepath = self.ui.config('web', 'templates', None)
       
    92         self.stripecount = self.ui.config('web', 'stripes', 1)
       
    93         if self.stripecount:
       
    94             self.stripecount = int(self.stripecount)
       
    95         self._baseurl = self.ui.config('web', 'baseurl')
       
    96         self.lastrefresh = time.time()
       
    97 
       
    98     def run(self):
       
    99         if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
       
   100             raise RuntimeError("This function is only intended to be "
       
   101                                "called while running as a CGI script.")
       
   102         import mercurial.hgweb.wsgicgi as wsgicgi
       
   103         wsgicgi.launch(self)
       
   104 
       
   105     def __call__(self, env, respond):
       
   106         req = wsgirequest(env, respond)
       
   107         return self.run_wsgi(req)
       
   108 
       
   109     def read_allowed(self, ui, req):
       
   110         """Check allow_read and deny_read config options of a repo's ui object
       
   111         to determine user permissions.  By default, with neither option set (or
       
   112         both empty), allow all users to read the repo.  There are two ways a
       
   113         user can be denied read access:  (1) deny_read is not empty, and the
       
   114         user is unauthenticated or deny_read contains user (or *), and (2)
       
   115         allow_read is not empty and the user is not in allow_read.  Return True
       
   116         if user is allowed to read the repo, else return False."""
       
   117 
       
   118         user = req.env.get('REMOTE_USER')
       
   119 
       
   120         deny_read = ui.configlist('web', 'deny_read', untrusted=True)
       
   121         if deny_read and (not user or deny_read == ['*'] or user in deny_read):
       
   122             return False
       
   123 
       
   124         allow_read = ui.configlist('web', 'allow_read', untrusted=True)
       
   125         # by default, allow reading if no allow_read option has been set
       
   126         if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
       
   127             return True
       
   128 
       
   129         return False
       
   130 
       
   131     def run_wsgi(self, req):
       
   132         try:
       
   133             try:
       
   134                 self.refresh()
       
   135 
       
   136                 virtual = req.env.get("PATH_INFO", "").strip('/')
       
   137                 tmpl = self.templater(req)
       
   138                 ctype = tmpl('mimetype', encoding=encoding.encoding)
       
   139                 ctype = templater.stringify(ctype)
       
   140 
       
   141                 # a static file
       
   142                 if virtual.startswith('static/') or 'static' in req.form:
       
   143                     if virtual.startswith('static/'):
       
   144                         fname = virtual[7:]
       
   145                     else:
       
   146                         fname = req.form['static'][0]
       
   147                     static = templater.templatepath('static')
       
   148                     return (staticfile(static, fname, req),)
       
   149 
       
   150                 # top-level index
       
   151                 elif not virtual:
       
   152                     req.respond(HTTP_OK, ctype)
       
   153                     return self.makeindex(req, tmpl)
       
   154 
       
   155                 # nested indexes and hgwebs
       
   156 
       
   157                 repos = dict(self.repos)
       
   158                 virtualrepo = virtual
       
   159                 while virtualrepo:
       
   160                     real = repos.get(virtualrepo)
       
   161                     if real:
       
   162                         req.env['REPO_NAME'] = virtualrepo
       
   163                         try:
       
   164                             repo = hg.repository(self.ui, real)
       
   165                             return hgweb(repo).run_wsgi(req)
       
   166                         except IOError, inst:
       
   167                             msg = inst.strerror
       
   168                             raise ErrorResponse(HTTP_SERVER_ERROR, msg)
       
   169                         except error.RepoError, inst:
       
   170                             raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
       
   171 
       
   172                     up = virtualrepo.rfind('/')
       
   173                     if up < 0:
       
   174                         break
       
   175                     virtualrepo = virtualrepo[:up]
       
   176 
       
   177                 # browse subdirectories
       
   178                 subdir = virtual + '/'
       
   179                 if [r for r in repos if r.startswith(subdir)]:
       
   180                     req.respond(HTTP_OK, ctype)
       
   181                     return self.makeindex(req, tmpl, subdir)
       
   182 
       
   183                 # prefixes not found
       
   184                 req.respond(HTTP_NOT_FOUND, ctype)
       
   185                 return tmpl("notfound", repo=virtual)
       
   186 
       
   187             except ErrorResponse, err:
       
   188                 req.respond(err, ctype)
       
   189                 return tmpl('error', error=err.message or '')
       
   190         finally:
       
   191             tmpl = None
       
   192 
       
   193     def makeindex(self, req, tmpl, subdir=""):
       
   194 
       
   195         def archivelist(ui, nodeid, url):
       
   196             allowed = ui.configlist("web", "allow_archive", untrusted=True)
       
   197             for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
       
   198                 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
       
   199                                                     untrusted=True):
       
   200                     yield {"type" : i[0], "extension": i[1],
       
   201                            "node": nodeid, "url": url}
       
   202 
       
   203         def rawentries(subdir="", **map):
       
   204 
       
   205             descend = self.ui.configbool('web', 'descend', True)
       
   206             for name, path in self.repos:
       
   207 
       
   208                 if not name.startswith(subdir):
       
   209                     continue
       
   210                 name = name[len(subdir):]
       
   211                 if not descend and '/' in name:
       
   212                     continue
       
   213 
       
   214                 u = self.ui.copy()
       
   215                 try:
       
   216                     u.readconfig(os.path.join(path, '.hg', 'hgrc'))
       
   217                 except Exception, e:
       
   218                     u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
       
   219                     continue
       
   220                 def get(section, name, default=None):
       
   221                     return u.config(section, name, default, untrusted=True)
       
   222 
       
   223                 if u.configbool("web", "hidden", untrusted=True):
       
   224                     continue
       
   225 
       
   226                 if not self.read_allowed(u, req):
       
   227                     continue
       
   228 
       
   229                 parts = [name]
       
   230                 if 'PATH_INFO' in req.env:
       
   231                     parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
       
   232                 if req.env['SCRIPT_NAME']:
       
   233                     parts.insert(0, req.env['SCRIPT_NAME'])
       
   234                 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
       
   235 
       
   236                 # update time with local timezone
       
   237                 try:
       
   238                     r = hg.repository(self.ui, path)
       
   239                 except error.RepoError:
       
   240                     u.warn(_('error accessing repository at %s\n') % path)
       
   241                     continue
       
   242                 try:
       
   243                     d = (get_mtime(r.spath), util.makedate()[1])
       
   244                 except OSError:
       
   245                     continue
       
   246 
       
   247                 contact = get_contact(get)
       
   248                 description = get("web", "description", "")
       
   249                 name = get("web", "name", name)
       
   250                 row = dict(contact=contact or "unknown",
       
   251                            contact_sort=contact.upper() or "unknown",
       
   252                            name=name,
       
   253                            name_sort=name,
       
   254                            url=url,
       
   255                            description=description or "unknown",
       
   256                            description_sort=description.upper() or "unknown",
       
   257                            lastchange=d,
       
   258                            lastchange_sort=d[1]-d[0],
       
   259                            archives=archivelist(u, "tip", url))
       
   260                 yield row
       
   261 
       
   262         sortdefault = None, False
       
   263         def entries(sortcolumn="", descending=False, subdir="", **map):
       
   264             rows = rawentries(subdir=subdir, **map)
       
   265 
       
   266             if sortcolumn and sortdefault != (sortcolumn, descending):
       
   267                 sortkey = '%s_sort' % sortcolumn
       
   268                 rows = sorted(rows, key=lambda x: x[sortkey],
       
   269                               reverse=descending)
       
   270             for row, parity in zip(rows, paritygen(self.stripecount)):
       
   271                 row['parity'] = parity
       
   272                 yield row
       
   273 
       
   274         self.refresh()
       
   275         sortable = ["name", "description", "contact", "lastchange"]
       
   276         sortcolumn, descending = sortdefault
       
   277         if 'sort' in req.form:
       
   278             sortcolumn = req.form['sort'][0]
       
   279             descending = sortcolumn.startswith('-')
       
   280             if descending:
       
   281                 sortcolumn = sortcolumn[1:]
       
   282             if sortcolumn not in sortable:
       
   283                 sortcolumn = ""
       
   284 
       
   285         sort = [("sort_%s" % column,
       
   286                  "%s%s" % ((not descending and column == sortcolumn)
       
   287                             and "-" or "", column))
       
   288                 for column in sortable]
       
   289 
       
   290         self.refresh()
       
   291         self.updatereqenv(req.env)
       
   292 
       
   293         return tmpl("index", entries=entries, subdir=subdir,
       
   294                     sortcolumn=sortcolumn, descending=descending,
       
   295                     **dict(sort))
       
   296 
       
   297     def templater(self, req):
       
   298 
       
   299         def header(**map):
       
   300             yield tmpl('header', encoding=encoding.encoding, **map)
       
   301 
       
   302         def footer(**map):
       
   303             yield tmpl("footer", **map)
       
   304 
       
   305         def motd(**map):
       
   306             if self.motd is not None:
       
   307                 yield self.motd
       
   308             else:
       
   309                 yield config('web', 'motd', '')
       
   310 
       
   311         def config(section, name, default=None, untrusted=True):
       
   312             return self.ui.config(section, name, default, untrusted)
       
   313 
       
   314         self.updatereqenv(req.env)
       
   315 
       
   316         url = req.env.get('SCRIPT_NAME', '')
       
   317         if not url.endswith('/'):
       
   318             url += '/'
       
   319 
       
   320         vars = {}
       
   321         styles = (
       
   322             req.form.get('style', [None])[0],
       
   323             config('web', 'style'),
       
   324             'paper'
       
   325         )
       
   326         style, mapfile = templater.stylemap(styles, self.templatepath)
       
   327         if style == styles[0]:
       
   328             vars['style'] = style
       
   329 
       
   330         start = url[-1] == '?' and '&' or '?'
       
   331         sessionvars = webutil.sessionvars(vars, start)
       
   332         staticurl = config('web', 'staticurl') or url + 'static/'
       
   333         if not staticurl.endswith('/'):
       
   334             staticurl += '/'
       
   335 
       
   336         tmpl = templater.templater(mapfile,
       
   337                                    defaults={"header": header,
       
   338                                              "footer": footer,
       
   339                                              "motd": motd,
       
   340                                              "url": url,
       
   341                                              "staticurl": staticurl,
       
   342                                              "sessionvars": sessionvars})
       
   343         return tmpl
       
   344 
       
   345     def updatereqenv(self, env):
       
   346         def splitnetloc(netloc):
       
   347             if ':' in netloc:
       
   348                 return netloc.split(':', 1)
       
   349             else:
       
   350                 return (netloc, None)
       
   351 
       
   352         if self._baseurl is not None:
       
   353             urlcomp = urlparse.urlparse(self._baseurl)
       
   354             host, port = splitnetloc(urlcomp[1])
       
   355             path = urlcomp[2]
       
   356             env['SERVER_NAME'] = host
       
   357             if port:
       
   358                 env['SERVER_PORT'] = port
       
   359             env['SCRIPT_NAME'] = path