mercurial/hgweb/hgweb_mod.py
changeset 43076 2372284d9457
parent 40729 c93d046d4300
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    43     webcommands,
    43     webcommands,
    44     webutil,
    44     webutil,
    45     wsgicgi,
    45     wsgicgi,
    46 )
    46 )
    47 
    47 
       
    48 
    48 def getstyle(req, configfn, templatepath):
    49 def getstyle(req, configfn, templatepath):
    49     styles = (
    50     styles = (
    50         req.qsparams.get('style', None),
    51         req.qsparams.get('style', None),
    51         configfn('web', 'style'),
    52         configfn('web', 'style'),
    52         'paper',
    53         'paper',
    53     )
    54     )
    54     return styles, templater.stylemap(styles, templatepath)
    55     return styles, templater.stylemap(styles, templatepath)
       
    56 
    55 
    57 
    56 def makebreadcrumb(url, prefix=''):
    58 def makebreadcrumb(url, prefix=''):
    57     '''Return a 'URL breadcrumb' list
    59     '''Return a 'URL breadcrumb' list
    58 
    60 
    59     A 'URL breadcrumb' is a list of URL-name pairs,
    61     A 'URL breadcrumb' is a list of URL-name pairs,
    76             break
    78             break
    77         breadcrumb.append({'url': urlel, 'name': pathel})
    79         breadcrumb.append({'url': urlel, 'name': pathel})
    78         urlel = os.path.dirname(urlel)
    80         urlel = os.path.dirname(urlel)
    79     return templateutil.mappinglist(reversed(breadcrumb))
    81     return templateutil.mappinglist(reversed(breadcrumb))
    80 
    82 
       
    83 
    81 class requestcontext(object):
    84 class requestcontext(object):
    82     """Holds state/context for an individual request.
    85     """Holds state/context for an individual request.
    83 
    86 
    84     Servers can be multi-threaded. Holding state on the WSGI application
    87     Servers can be multi-threaded. Holding state on the WSGI application
    85     is prone to race conditions. Instances of this class exist to hold
    88     is prone to race conditions. Instances of this class exist to hold
    86     mutable and race-free state for requests.
    89     mutable and race-free state for requests.
    87     """
    90     """
       
    91 
    88     def __init__(self, app, repo, req, res):
    92     def __init__(self, app, repo, req, res):
    89         self.repo = repo
    93         self.repo = repo
    90         self.reponame = app.reponame
    94         self.reponame = app.reponame
    91         self.req = req
    95         self.req = req
    92         self.res = res
    96         self.res = res
   111 
   115 
   112         self.csp, self.nonce = cspvalues(self.repo.ui)
   116         self.csp, self.nonce = cspvalues(self.repo.ui)
   113 
   117 
   114     # Trust the settings from the .hg/hgrc files by default.
   118     # Trust the settings from the .hg/hgrc files by default.
   115     def config(self, section, name, default=uimod._unset, untrusted=True):
   119     def config(self, section, name, default=uimod._unset, untrusted=True):
   116         return self.repo.ui.config(section, name, default,
   120         return self.repo.ui.config(section, name, default, untrusted=untrusted)
   117                                    untrusted=untrusted)
       
   118 
   121 
   119     def configbool(self, section, name, default=uimod._unset, untrusted=True):
   122     def configbool(self, section, name, default=uimod._unset, untrusted=True):
   120         return self.repo.ui.configbool(section, name, default,
   123         return self.repo.ui.configbool(
   121                                        untrusted=untrusted)
   124             section, name, default, untrusted=untrusted
       
   125         )
   122 
   126 
   123     def configint(self, section, name, default=uimod._unset, untrusted=True):
   127     def configint(self, section, name, default=uimod._unset, untrusted=True):
   124         return self.repo.ui.configint(section, name, default,
   128         return self.repo.ui.configint(
   125                                       untrusted=untrusted)
   129             section, name, default, untrusted=untrusted
       
   130         )
   126 
   131 
   127     def configlist(self, section, name, default=uimod._unset, untrusted=True):
   132     def configlist(self, section, name, default=uimod._unset, untrusted=True):
   128         return self.repo.ui.configlist(section, name, default,
   133         return self.repo.ui.configlist(
   129                                        untrusted=untrusted)
   134             section, name, default, untrusted=untrusted
       
   135         )
   130 
   136 
   131     def archivelist(self, nodeid):
   137     def archivelist(self, nodeid):
   132         return webutil.archivelist(self.repo.ui, nodeid)
   138         return webutil.archivelist(self.repo.ui, nodeid)
   133 
   139 
   134     def templater(self, req):
   140     def templater(self, req):
   135         # determine scheme, port and server name
   141         # determine scheme, port and server name
   136         # this is needed to create absolute urls
   142         # this is needed to create absolute urls
   137         logourl = self.config('web', 'logourl')
   143         logourl = self.config('web', 'logourl')
   138         logoimg = self.config('web', 'logoimg')
   144         logoimg = self.config('web', 'logoimg')
   139         staticurl = (self.config('web', 'staticurl')
   145         staticurl = (
   140                      or req.apppath.rstrip('/') + '/static/')
   146             self.config('web', 'staticurl')
       
   147             or req.apppath.rstrip('/') + '/static/'
       
   148         )
   141         if not staticurl.endswith('/'):
   149         if not staticurl.endswith('/'):
   142             staticurl += '/'
   150             staticurl += '/'
   143 
   151 
   144         # figure out which style to use
   152         # figure out which style to use
   145 
   153 
   146         vars = {}
   154         vars = {}
   147         styles, (style, mapfile) = getstyle(req, self.config,
   155         styles, (style, mapfile) = getstyle(req, self.config, self.templatepath)
   148                                             self.templatepath)
       
   149         if style == styles[0]:
   156         if style == styles[0]:
   150             vars['style'] = style
   157             vars['style'] = style
   151 
   158 
   152         sessionvars = webutil.sessionvars(vars, '?')
   159         sessionvars = webutil.sessionvars(vars, '?')
   153 
   160 
   154         if not self.reponame:
   161         if not self.reponame:
   155             self.reponame = (self.config('web', 'name', '')
   162             self.reponame = (
   156                              or req.reponame
   163                 self.config('web', 'name', '')
   157                              or req.apppath
   164                 or req.reponame
   158                              or self.repo.root)
   165                 or req.apppath
       
   166                 or self.repo.root
       
   167             )
   159 
   168 
   160         filters = {}
   169         filters = {}
   161         templatefilter = registrar.templatefilter(filters)
   170         templatefilter = registrar.templatefilter(filters)
       
   171 
   162         @templatefilter('websub', intype=bytes)
   172         @templatefilter('websub', intype=bytes)
   163         def websubfilter(text):
   173         def websubfilter(text):
   164             return templatefilters.websub(text, self.websubtable)
   174             return templatefilters.websub(text, self.websubtable)
   165 
   175 
   166         # create the templater
   176         # create the templater
   177             'pathdef': makebreadcrumb(req.apppath),
   187             'pathdef': makebreadcrumb(req.apppath),
   178             'style': style,
   188             'style': style,
   179             'nonce': self.nonce,
   189             'nonce': self.nonce,
   180         }
   190         }
   181         templatekeyword = registrar.templatekeyword(defaults)
   191         templatekeyword = registrar.templatekeyword(defaults)
       
   192 
   182         @templatekeyword('motd', requires=())
   193         @templatekeyword('motd', requires=())
   183         def motd(context, mapping):
   194         def motd(context, mapping):
   184             yield self.config('web', 'motd')
   195             yield self.config('web', 'motd')
   185 
   196 
   186         tres = formatter.templateresources(self.repo.ui, self.repo)
   197         tres = formatter.templateresources(self.repo.ui, self.repo)
   187         tmpl = templater.templater.frommapfile(mapfile,
   198         tmpl = templater.templater.frommapfile(
   188                                                filters=filters,
   199             mapfile, filters=filters, defaults=defaults, resources=tres
   189                                                defaults=defaults,
   200         )
   190                                                resources=tres)
       
   191         return tmpl
   201         return tmpl
   192 
   202 
   193     def sendtemplate(self, name, **kwargs):
   203     def sendtemplate(self, name, **kwargs):
   194         """Helper function to send a response generated from a template."""
   204         """Helper function to send a response generated from a template."""
   195         kwargs = pycompat.byteskwargs(kwargs)
   205         kwargs = pycompat.byteskwargs(kwargs)
   196         self.res.setbodygen(self.tmpl.generate(name, kwargs))
   206         self.res.setbodygen(self.tmpl.generate(name, kwargs))
   197         return self.res.sendresponse()
   207         return self.res.sendresponse()
   198 
   208 
       
   209 
   199 class hgweb(object):
   210 class hgweb(object):
   200     """HTTP server for individual repositories.
   211     """HTTP server for individual repositories.
   201 
   212 
   202     Instances of this class serve HTTP responses for a particular
   213     Instances of this class serve HTTP responses for a particular
   203     repository.
   214     repository.
   205     Instances are typically used as WSGI applications.
   216     Instances are typically used as WSGI applications.
   206 
   217 
   207     Some servers are multi-threaded. On these servers, there may
   218     Some servers are multi-threaded. On these servers, there may
   208     be multiple active threads inside __call__.
   219     be multiple active threads inside __call__.
   209     """
   220     """
       
   221 
   210     def __init__(self, repo, name=None, baseui=None):
   222     def __init__(self, repo, name=None, baseui=None):
   211         if isinstance(repo, bytes):
   223         if isinstance(repo, bytes):
   212             if baseui:
   224             if baseui:
   213                 u = baseui.copy()
   225                 u = baseui.copy()
   214             else:
   226             else:
   280         """Start a server from CGI environment.
   292         """Start a server from CGI environment.
   281 
   293 
   282         Modern servers should be using WSGI and should avoid this
   294         Modern servers should be using WSGI and should avoid this
   283         method, if possible.
   295         method, if possible.
   284         """
   296         """
   285         if not encoding.environ.get('GATEWAY_INTERFACE',
   297         if not encoding.environ.get('GATEWAY_INTERFACE', '').startswith(
   286                                     '').startswith("CGI/1."):
   298             "CGI/1."
   287             raise RuntimeError("This function is only intended to be "
   299         ):
   288                                "called while running as a CGI script.")
   300             raise RuntimeError(
       
   301                 "This function is only intended to be "
       
   302                 "called while running as a CGI script."
       
   303             )
   289         wsgicgi.launch(self)
   304         wsgicgi.launch(self)
   290 
   305 
   291     def __call__(self, env, respond):
   306     def __call__(self, env, respond):
   292         """Run the WSGI application.
   307         """Run the WSGI application.
   293 
   308 
   326         # accordingly. But URL paths can conflict with subrepos and virtual
   341         # accordingly. But URL paths can conflict with subrepos and virtual
   327         # repos in hgwebdir. So until we have a workaround for this, only
   342         # repos in hgwebdir. So until we have a workaround for this, only
   328         # expose the URLs if the feature is enabled.
   343         # expose the URLs if the feature is enabled.
   329         apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
   344         apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
   330         if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
   345         if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
   331             wireprotoserver.handlewsgiapirequest(rctx, req, res,
   346             wireprotoserver.handlewsgiapirequest(
   332                                                  self.check_perm)
   347                 rctx, req, res, self.check_perm
       
   348             )
   333             return res.sendresponse()
   349             return res.sendresponse()
   334 
   350 
   335         handled = wireprotoserver.handlewsgirequest(
   351         handled = wireprotoserver.handlewsgirequest(
   336             rctx, req, res, self.check_perm)
   352             rctx, req, res, self.check_perm
       
   353         )
   337         if handled:
   354         if handled:
   338             return res.sendresponse()
   355             return res.sendresponse()
   339 
   356 
   340         # Old implementations of hgweb supported dispatching the request via
   357         # Old implementations of hgweb supported dispatching the request via
   341         # the initial query string parameter instead of using PATH_INFO.
   358         # the initial query string parameter instead of using PATH_INFO.
   352         if 'cmd' not in req.qsparams and args and args[0]:
   369         if 'cmd' not in req.qsparams and args and args[0]:
   353             cmd = args.pop(0)
   370             cmd = args.pop(0)
   354             style = cmd.rfind('-')
   371             style = cmd.rfind('-')
   355             if style != -1:
   372             if style != -1:
   356                 req.qsparams['style'] = cmd[:style]
   373                 req.qsparams['style'] = cmd[:style]
   357                 cmd = cmd[style + 1:]
   374                 cmd = cmd[style + 1 :]
   358 
   375 
   359             # avoid accepting e.g. style parameter as command
   376             # avoid accepting e.g. style parameter as command
   360             if util.safehasattr(webcommands, cmd):
   377             if util.safehasattr(webcommands, cmd):
   361                 req.qsparams['cmd'] = cmd
   378                 req.qsparams['cmd'] = cmd
   362 
   379 
   379             if cmd == 'archive':
   396             if cmd == 'archive':
   380                 fn = req.qsparams['node']
   397                 fn = req.qsparams['node']
   381                 for type_, spec in webutil.archivespecs.iteritems():
   398                 for type_, spec in webutil.archivespecs.iteritems():
   382                     ext = spec[2]
   399                     ext = spec[2]
   383                     if fn.endswith(ext):
   400                     if fn.endswith(ext):
   384                         req.qsparams['node'] = fn[:-len(ext)]
   401                         req.qsparams['node'] = fn[: -len(ext)]
   385                         req.qsparams['type'] = type_
   402                         req.qsparams['type'] = type_
   386         else:
   403         else:
   387             cmd = req.qsparams.get('cmd', '')
   404             cmd = req.qsparams.get('cmd', '')
   388 
   405 
   389         # process the web interface request
   406         # process the web interface request
   390 
   407 
   391         try:
   408         try:
   392             rctx.tmpl = rctx.templater(req)
   409             rctx.tmpl = rctx.templater(req)
   393             ctype = rctx.tmpl.render('mimetype',
   410             ctype = rctx.tmpl.render(
   394                                      {'encoding': encoding.encoding})
   411                 'mimetype', {'encoding': encoding.encoding}
       
   412             )
   395 
   413 
   396             # check read permissions non-static content
   414             # check read permissions non-static content
   397             if cmd != 'static':
   415             if cmd != 'static':
   398                 self.check_perm(rctx, req, None)
   416                 self.check_perm(rctx, req, None)
   399 
   417 
   429                 res.headers['Content-Type'] = ctype
   447                 res.headers['Content-Type'] = ctype
   430                 return getattr(webcommands, cmd)(rctx)
   448                 return getattr(webcommands, cmd)(rctx)
   431 
   449 
   432         except (error.LookupError, error.RepoLookupError) as err:
   450         except (error.LookupError, error.RepoLookupError) as err:
   433             msg = pycompat.bytestr(err)
   451             msg = pycompat.bytestr(err)
   434             if (util.safehasattr(err, 'name') and
   452             if util.safehasattr(err, 'name') and not isinstance(
   435                 not isinstance(err,  error.ManifestLookupError)):
   453                 err, error.ManifestLookupError
       
   454             ):
   436                 msg = 'revision not found: %s' % err.name
   455                 msg = 'revision not found: %s' % err.name
   437 
   456 
   438             res.status = '404 Not Found'
   457             res.status = '404 Not Found'
   439             res.headers['Content-Type'] = ctype
   458             res.headers['Content-Type'] = ctype
   440             return rctx.sendtemplate('error', error=msg)
   459             return rctx.sendtemplate('error', error=msg)
   455 
   474 
   456     def check_perm(self, rctx, req, op):
   475     def check_perm(self, rctx, req, op):
   457         for permhook in permhooks:
   476         for permhook in permhooks:
   458             permhook(rctx, req, op)
   477             permhook(rctx, req, op)
   459 
   478 
       
   479 
   460 def getwebview(repo):
   480 def getwebview(repo):
   461     """The 'web.view' config controls changeset filter to hgweb. Possible
   481     """The 'web.view' config controls changeset filter to hgweb. Possible
   462     values are ``served``, ``visible`` and ``all``. Default is ``served``.
   482     values are ``served``, ``visible`` and ``all``. Default is ``served``.
   463     The ``served`` filter only shows changesets that can be pulled from the
   483     The ``served`` filter only shows changesets that can be pulled from the
   464     hgweb instance.  The``visible`` filter includes secret changesets but
   484     hgweb instance.  The``visible`` filter includes secret changesets but