##// END OF EJS Templates
hgweb: use registrar to add "motd" template keyword...
Yuya Nishihara -
r38964:4167437a default
parent child Browse files
Show More
@@ -1,472 +1,471 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
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.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 cspvalues,
17 cspvalues,
18 permhooks,
18 permhooks,
19 statusmessage,
19 statusmessage,
20 )
20 )
21
21
22 from .. import (
22 from .. import (
23 encoding,
23 encoding,
24 error,
24 error,
25 formatter,
25 formatter,
26 hg,
26 hg,
27 hook,
27 hook,
28 profiling,
28 profiling,
29 pycompat,
29 pycompat,
30 registrar,
30 registrar,
31 repoview,
31 repoview,
32 templatefilters,
32 templatefilters,
33 templater,
33 templater,
34 templateutil,
34 templateutil,
35 ui as uimod,
35 ui as uimod,
36 util,
36 util,
37 wireprotoserver,
37 wireprotoserver,
38 )
38 )
39
39
40 from . import (
40 from . import (
41 request as requestmod,
41 request as requestmod,
42 webcommands,
42 webcommands,
43 webutil,
43 webutil,
44 wsgicgi,
44 wsgicgi,
45 )
45 )
46
46
47 def getstyle(req, configfn, templatepath):
47 def getstyle(req, configfn, templatepath):
48 styles = (
48 styles = (
49 req.qsparams.get('style', None),
49 req.qsparams.get('style', None),
50 configfn('web', 'style'),
50 configfn('web', 'style'),
51 'paper',
51 'paper',
52 )
52 )
53 return styles, templater.stylemap(styles, templatepath)
53 return styles, templater.stylemap(styles, templatepath)
54
54
55 def makebreadcrumb(url, prefix=''):
55 def makebreadcrumb(url, prefix=''):
56 '''Return a 'URL breadcrumb' list
56 '''Return a 'URL breadcrumb' list
57
57
58 A 'URL breadcrumb' is a list of URL-name pairs,
58 A 'URL breadcrumb' is a list of URL-name pairs,
59 corresponding to each of the path items on a URL.
59 corresponding to each of the path items on a URL.
60 This can be used to create path navigation entries.
60 This can be used to create path navigation entries.
61 '''
61 '''
62 if url.endswith('/'):
62 if url.endswith('/'):
63 url = url[:-1]
63 url = url[:-1]
64 if prefix:
64 if prefix:
65 url = '/' + prefix + url
65 url = '/' + prefix + url
66 relpath = url
66 relpath = url
67 if relpath.startswith('/'):
67 if relpath.startswith('/'):
68 relpath = relpath[1:]
68 relpath = relpath[1:]
69
69
70 breadcrumb = []
70 breadcrumb = []
71 urlel = url
71 urlel = url
72 pathitems = [''] + relpath.split('/')
72 pathitems = [''] + relpath.split('/')
73 for pathel in reversed(pathitems):
73 for pathel in reversed(pathitems):
74 if not pathel or not urlel:
74 if not pathel or not urlel:
75 break
75 break
76 breadcrumb.append({'url': urlel, 'name': pathel})
76 breadcrumb.append({'url': urlel, 'name': pathel})
77 urlel = os.path.dirname(urlel)
77 urlel = os.path.dirname(urlel)
78 return templateutil.mappinglist(reversed(breadcrumb))
78 return templateutil.mappinglist(reversed(breadcrumb))
79
79
80 class requestcontext(object):
80 class requestcontext(object):
81 """Holds state/context for an individual request.
81 """Holds state/context for an individual request.
82
82
83 Servers can be multi-threaded. Holding state on the WSGI application
83 Servers can be multi-threaded. Holding state on the WSGI application
84 is prone to race conditions. Instances of this class exist to hold
84 is prone to race conditions. Instances of this class exist to hold
85 mutable and race-free state for requests.
85 mutable and race-free state for requests.
86 """
86 """
87 def __init__(self, app, repo, req, res):
87 def __init__(self, app, repo, req, res):
88 self.repo = repo
88 self.repo = repo
89 self.reponame = app.reponame
89 self.reponame = app.reponame
90 self.req = req
90 self.req = req
91 self.res = res
91 self.res = res
92
92
93 self.maxchanges = self.configint('web', 'maxchanges')
93 self.maxchanges = self.configint('web', 'maxchanges')
94 self.stripecount = self.configint('web', 'stripes')
94 self.stripecount = self.configint('web', 'stripes')
95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
96 self.maxfiles = self.configint('web', 'maxfiles')
96 self.maxfiles = self.configint('web', 'maxfiles')
97 self.allowpull = self.configbool('web', 'allow-pull')
97 self.allowpull = self.configbool('web', 'allow-pull')
98
98
99 # we use untrusted=False to prevent a repo owner from using
99 # we use untrusted=False to prevent a repo owner from using
100 # web.templates in .hg/hgrc to get access to any file readable
100 # web.templates in .hg/hgrc to get access to any file readable
101 # by the user running the CGI script
101 # by the user running the CGI script
102 self.templatepath = self.config('web', 'templates', untrusted=False)
102 self.templatepath = self.config('web', 'templates', untrusted=False)
103
103
104 # This object is more expensive to build than simple config values.
104 # This object is more expensive to build than simple config values.
105 # It is shared across requests. The app will replace the object
105 # It is shared across requests. The app will replace the object
106 # if it is updated. Since this is a reference and nothing should
106 # if it is updated. Since this is a reference and nothing should
107 # modify the underlying object, it should be constant for the lifetime
107 # modify the underlying object, it should be constant for the lifetime
108 # of the request.
108 # of the request.
109 self.websubtable = app.websubtable
109 self.websubtable = app.websubtable
110
110
111 self.csp, self.nonce = cspvalues(self.repo.ui)
111 self.csp, self.nonce = cspvalues(self.repo.ui)
112
112
113 # Trust the settings from the .hg/hgrc files by default.
113 # Trust the settings from the .hg/hgrc files by default.
114 def config(self, section, name, default=uimod._unset, untrusted=True):
114 def config(self, section, name, default=uimod._unset, untrusted=True):
115 return self.repo.ui.config(section, name, default,
115 return self.repo.ui.config(section, name, default,
116 untrusted=untrusted)
116 untrusted=untrusted)
117
117
118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
119 return self.repo.ui.configbool(section, name, default,
119 return self.repo.ui.configbool(section, name, default,
120 untrusted=untrusted)
120 untrusted=untrusted)
121
121
122 def configint(self, section, name, default=uimod._unset, untrusted=True):
122 def configint(self, section, name, default=uimod._unset, untrusted=True):
123 return self.repo.ui.configint(section, name, default,
123 return self.repo.ui.configint(section, name, default,
124 untrusted=untrusted)
124 untrusted=untrusted)
125
125
126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
127 return self.repo.ui.configlist(section, name, default,
127 return self.repo.ui.configlist(section, name, default,
128 untrusted=untrusted)
128 untrusted=untrusted)
129
129
130 def archivelist(self, nodeid):
130 def archivelist(self, nodeid):
131 return webutil.archivelist(self.repo.ui, nodeid)
131 return webutil.archivelist(self.repo.ui, nodeid)
132
132
133 def templater(self, req):
133 def templater(self, req):
134 # determine scheme, port and server name
134 # determine scheme, port and server name
135 # this is needed to create absolute urls
135 # this is needed to create absolute urls
136 logourl = self.config('web', 'logourl')
136 logourl = self.config('web', 'logourl')
137 logoimg = self.config('web', 'logoimg')
137 logoimg = self.config('web', 'logoimg')
138 staticurl = (self.config('web', 'staticurl')
138 staticurl = (self.config('web', 'staticurl')
139 or req.apppath.rstrip('/') + '/static/')
139 or req.apppath.rstrip('/') + '/static/')
140 if not staticurl.endswith('/'):
140 if not staticurl.endswith('/'):
141 staticurl += '/'
141 staticurl += '/'
142
142
143 # some functions for the templater
144
145 def motd(**map):
146 yield self.config('web', 'motd')
147
148 # figure out which style to use
143 # figure out which style to use
149
144
150 vars = {}
145 vars = {}
151 styles, (style, mapfile) = getstyle(req, self.config,
146 styles, (style, mapfile) = getstyle(req, self.config,
152 self.templatepath)
147 self.templatepath)
153 if style == styles[0]:
148 if style == styles[0]:
154 vars['style'] = style
149 vars['style'] = style
155
150
156 sessionvars = webutil.sessionvars(vars, '?')
151 sessionvars = webutil.sessionvars(vars, '?')
157
152
158 if not self.reponame:
153 if not self.reponame:
159 self.reponame = (self.config('web', 'name', '')
154 self.reponame = (self.config('web', 'name', '')
160 or req.reponame
155 or req.reponame
161 or req.apppath
156 or req.apppath
162 or self.repo.root)
157 or self.repo.root)
163
158
164 filters = {}
159 filters = {}
165 templatefilter = registrar.templatefilter(filters)
160 templatefilter = registrar.templatefilter(filters)
166 @templatefilter('websub', intype=bytes)
161 @templatefilter('websub', intype=bytes)
167 def websubfilter(text):
162 def websubfilter(text):
168 return templatefilters.websub(text, self.websubtable)
163 return templatefilters.websub(text, self.websubtable)
169
164
170 # create the templater
165 # create the templater
171 # TODO: export all keywords: defaults = templatekw.keywords.copy()
166 # TODO: export all keywords: defaults = templatekw.keywords.copy()
172 defaults = {
167 defaults = {
173 'url': req.apppath + '/',
168 'url': req.apppath + '/',
174 'logourl': logourl,
169 'logourl': logourl,
175 'logoimg': logoimg,
170 'logoimg': logoimg,
176 'staticurl': staticurl,
171 'staticurl': staticurl,
177 'urlbase': req.advertisedbaseurl,
172 'urlbase': req.advertisedbaseurl,
178 'repo': self.reponame,
173 'repo': self.reponame,
179 'encoding': encoding.encoding,
174 'encoding': encoding.encoding,
180 'motd': motd,
181 'sessionvars': sessionvars,
175 'sessionvars': sessionvars,
182 'pathdef': makebreadcrumb(req.apppath),
176 'pathdef': makebreadcrumb(req.apppath),
183 'style': style,
177 'style': style,
184 'nonce': self.nonce,
178 'nonce': self.nonce,
185 }
179 }
180 templatekeyword = registrar.templatekeyword(defaults)
181 @templatekeyword('motd', requires=())
182 def motd(context, mapping):
183 yield self.config('web', 'motd')
184
186 tres = formatter.templateresources(self.repo.ui, self.repo)
185 tres = formatter.templateresources(self.repo.ui, self.repo)
187 tmpl = templater.templater.frommapfile(mapfile,
186 tmpl = templater.templater.frommapfile(mapfile,
188 filters=filters,
187 filters=filters,
189 defaults=defaults,
188 defaults=defaults,
190 resources=tres)
189 resources=tres)
191 return tmpl
190 return tmpl
192
191
193 def sendtemplate(self, name, **kwargs):
192 def sendtemplate(self, name, **kwargs):
194 """Helper function to send a response generated from a template."""
193 """Helper function to send a response generated from a template."""
195 kwargs = pycompat.byteskwargs(kwargs)
194 kwargs = pycompat.byteskwargs(kwargs)
196 self.res.setbodygen(self.tmpl.generate(name, kwargs))
195 self.res.setbodygen(self.tmpl.generate(name, kwargs))
197 return self.res.sendresponse()
196 return self.res.sendresponse()
198
197
199 class hgweb(object):
198 class hgweb(object):
200 """HTTP server for individual repositories.
199 """HTTP server for individual repositories.
201
200
202 Instances of this class serve HTTP responses for a particular
201 Instances of this class serve HTTP responses for a particular
203 repository.
202 repository.
204
203
205 Instances are typically used as WSGI applications.
204 Instances are typically used as WSGI applications.
206
205
207 Some servers are multi-threaded. On these servers, there may
206 Some servers are multi-threaded. On these servers, there may
208 be multiple active threads inside __call__.
207 be multiple active threads inside __call__.
209 """
208 """
210 def __init__(self, repo, name=None, baseui=None):
209 def __init__(self, repo, name=None, baseui=None):
211 if isinstance(repo, bytes):
210 if isinstance(repo, bytes):
212 if baseui:
211 if baseui:
213 u = baseui.copy()
212 u = baseui.copy()
214 else:
213 else:
215 u = uimod.ui.load()
214 u = uimod.ui.load()
216 r = hg.repository(u, repo)
215 r = hg.repository(u, repo)
217 else:
216 else:
218 # we trust caller to give us a private copy
217 # we trust caller to give us a private copy
219 r = repo
218 r = repo
220
219
221 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
220 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
222 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
221 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
223 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
222 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
224 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
223 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
225 # resolve file patterns relative to repo root
224 # resolve file patterns relative to repo root
226 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
225 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
227 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
226 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
228 # it's unlikely that we can replace signal handlers in WSGI server,
227 # it's unlikely that we can replace signal handlers in WSGI server,
229 # and mod_wsgi issues a big warning. a plain hgweb process (with no
228 # and mod_wsgi issues a big warning. a plain hgweb process (with no
230 # threading) could replace signal handlers, but we don't bother
229 # threading) could replace signal handlers, but we don't bother
231 # conditionally enabling it.
230 # conditionally enabling it.
232 r.ui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
231 r.ui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
233 r.baseui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
232 r.baseui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
234 # displaying bundling progress bar while serving feel wrong and may
233 # displaying bundling progress bar while serving feel wrong and may
235 # break some wsgi implementation.
234 # break some wsgi implementation.
236 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
237 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
236 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
238 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
237 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
239 self._lastrepo = self._repos[0]
238 self._lastrepo = self._repos[0]
240 hook.redirect(True)
239 hook.redirect(True)
241 self.reponame = name
240 self.reponame = name
242
241
243 def _webifyrepo(self, repo):
242 def _webifyrepo(self, repo):
244 repo = getwebview(repo)
243 repo = getwebview(repo)
245 self.websubtable = webutil.getwebsubs(repo)
244 self.websubtable = webutil.getwebsubs(repo)
246 return repo
245 return repo
247
246
248 @contextlib.contextmanager
247 @contextlib.contextmanager
249 def _obtainrepo(self):
248 def _obtainrepo(self):
250 """Obtain a repo unique to the caller.
249 """Obtain a repo unique to the caller.
251
250
252 Internally we maintain a stack of cachedlocalrepo instances
251 Internally we maintain a stack of cachedlocalrepo instances
253 to be handed out. If one is available, we pop it and return it,
252 to be handed out. If one is available, we pop it and return it,
254 ensuring it is up to date in the process. If one is not available,
253 ensuring it is up to date in the process. If one is not available,
255 we clone the most recently used repo instance and return it.
254 we clone the most recently used repo instance and return it.
256
255
257 It is currently possible for the stack to grow without bounds
256 It is currently possible for the stack to grow without bounds
258 if the server allows infinite threads. However, servers should
257 if the server allows infinite threads. However, servers should
259 have a thread limit, thus establishing our limit.
258 have a thread limit, thus establishing our limit.
260 """
259 """
261 if self._repos:
260 if self._repos:
262 cached = self._repos.pop()
261 cached = self._repos.pop()
263 r, created = cached.fetch()
262 r, created = cached.fetch()
264 else:
263 else:
265 cached = self._lastrepo.copy()
264 cached = self._lastrepo.copy()
266 r, created = cached.fetch()
265 r, created = cached.fetch()
267 if created:
266 if created:
268 r = self._webifyrepo(r)
267 r = self._webifyrepo(r)
269
268
270 self._lastrepo = cached
269 self._lastrepo = cached
271 self.mtime = cached.mtime
270 self.mtime = cached.mtime
272 try:
271 try:
273 yield r
272 yield r
274 finally:
273 finally:
275 self._repos.append(cached)
274 self._repos.append(cached)
276
275
277 def run(self):
276 def run(self):
278 """Start a server from CGI environment.
277 """Start a server from CGI environment.
279
278
280 Modern servers should be using WSGI and should avoid this
279 Modern servers should be using WSGI and should avoid this
281 method, if possible.
280 method, if possible.
282 """
281 """
283 if not encoding.environ.get('GATEWAY_INTERFACE',
282 if not encoding.environ.get('GATEWAY_INTERFACE',
284 '').startswith("CGI/1."):
283 '').startswith("CGI/1."):
285 raise RuntimeError("This function is only intended to be "
284 raise RuntimeError("This function is only intended to be "
286 "called while running as a CGI script.")
285 "called while running as a CGI script.")
287 wsgicgi.launch(self)
286 wsgicgi.launch(self)
288
287
289 def __call__(self, env, respond):
288 def __call__(self, env, respond):
290 """Run the WSGI application.
289 """Run the WSGI application.
291
290
292 This may be called by multiple threads.
291 This may be called by multiple threads.
293 """
292 """
294 req = requestmod.parserequestfromenv(env)
293 req = requestmod.parserequestfromenv(env)
295 res = requestmod.wsgiresponse(req, respond)
294 res = requestmod.wsgiresponse(req, respond)
296
295
297 return self.run_wsgi(req, res)
296 return self.run_wsgi(req, res)
298
297
299 def run_wsgi(self, req, res):
298 def run_wsgi(self, req, res):
300 """Internal method to run the WSGI application.
299 """Internal method to run the WSGI application.
301
300
302 This is typically only called by Mercurial. External consumers
301 This is typically only called by Mercurial. External consumers
303 should be using instances of this class as the WSGI application.
302 should be using instances of this class as the WSGI application.
304 """
303 """
305 with self._obtainrepo() as repo:
304 with self._obtainrepo() as repo:
306 profile = repo.ui.configbool('profiling', 'enabled')
305 profile = repo.ui.configbool('profiling', 'enabled')
307 with profiling.profile(repo.ui, enabled=profile):
306 with profiling.profile(repo.ui, enabled=profile):
308 for r in self._runwsgi(req, res, repo):
307 for r in self._runwsgi(req, res, repo):
309 yield r
308 yield r
310
309
311 def _runwsgi(self, req, res, repo):
310 def _runwsgi(self, req, res, repo):
312 rctx = requestcontext(self, repo, req, res)
311 rctx = requestcontext(self, repo, req, res)
313
312
314 # This state is global across all threads.
313 # This state is global across all threads.
315 encoding.encoding = rctx.config('web', 'encoding')
314 encoding.encoding = rctx.config('web', 'encoding')
316 rctx.repo.ui.environ = req.rawenv
315 rctx.repo.ui.environ = req.rawenv
317
316
318 if rctx.csp:
317 if rctx.csp:
319 # hgwebdir may have added CSP header. Since we generate our own,
318 # hgwebdir may have added CSP header. Since we generate our own,
320 # replace it.
319 # replace it.
321 res.headers['Content-Security-Policy'] = rctx.csp
320 res.headers['Content-Security-Policy'] = rctx.csp
322
321
323 # /api/* is reserved for various API implementations. Dispatch
322 # /api/* is reserved for various API implementations. Dispatch
324 # accordingly. But URL paths can conflict with subrepos and virtual
323 # accordingly. But URL paths can conflict with subrepos and virtual
325 # repos in hgwebdir. So until we have a workaround for this, only
324 # repos in hgwebdir. So until we have a workaround for this, only
326 # expose the URLs if the feature is enabled.
325 # expose the URLs if the feature is enabled.
327 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
326 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
328 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
327 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
329 wireprotoserver.handlewsgiapirequest(rctx, req, res,
328 wireprotoserver.handlewsgiapirequest(rctx, req, res,
330 self.check_perm)
329 self.check_perm)
331 return res.sendresponse()
330 return res.sendresponse()
332
331
333 handled = wireprotoserver.handlewsgirequest(
332 handled = wireprotoserver.handlewsgirequest(
334 rctx, req, res, self.check_perm)
333 rctx, req, res, self.check_perm)
335 if handled:
334 if handled:
336 return res.sendresponse()
335 return res.sendresponse()
337
336
338 # Old implementations of hgweb supported dispatching the request via
337 # Old implementations of hgweb supported dispatching the request via
339 # the initial query string parameter instead of using PATH_INFO.
338 # the initial query string parameter instead of using PATH_INFO.
340 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
339 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
341 # a value), we use it. Otherwise fall back to the query string.
340 # a value), we use it. Otherwise fall back to the query string.
342 if req.dispatchpath is not None:
341 if req.dispatchpath is not None:
343 query = req.dispatchpath
342 query = req.dispatchpath
344 else:
343 else:
345 query = req.querystring.partition('&')[0].partition(';')[0]
344 query = req.querystring.partition('&')[0].partition(';')[0]
346
345
347 # translate user-visible url structure to internal structure
346 # translate user-visible url structure to internal structure
348
347
349 args = query.split('/', 2)
348 args = query.split('/', 2)
350 if 'cmd' not in req.qsparams and args and args[0]:
349 if 'cmd' not in req.qsparams and args and args[0]:
351 cmd = args.pop(0)
350 cmd = args.pop(0)
352 style = cmd.rfind('-')
351 style = cmd.rfind('-')
353 if style != -1:
352 if style != -1:
354 req.qsparams['style'] = cmd[:style]
353 req.qsparams['style'] = cmd[:style]
355 cmd = cmd[style + 1:]
354 cmd = cmd[style + 1:]
356
355
357 # avoid accepting e.g. style parameter as command
356 # avoid accepting e.g. style parameter as command
358 if util.safehasattr(webcommands, cmd):
357 if util.safehasattr(webcommands, cmd):
359 req.qsparams['cmd'] = cmd
358 req.qsparams['cmd'] = cmd
360
359
361 if cmd == 'static':
360 if cmd == 'static':
362 req.qsparams['file'] = '/'.join(args)
361 req.qsparams['file'] = '/'.join(args)
363 else:
362 else:
364 if args and args[0]:
363 if args and args[0]:
365 node = args.pop(0).replace('%2F', '/')
364 node = args.pop(0).replace('%2F', '/')
366 req.qsparams['node'] = node
365 req.qsparams['node'] = node
367 if args:
366 if args:
368 if 'file' in req.qsparams:
367 if 'file' in req.qsparams:
369 del req.qsparams['file']
368 del req.qsparams['file']
370 for a in args:
369 for a in args:
371 req.qsparams.add('file', a)
370 req.qsparams.add('file', a)
372
371
373 ua = req.headers.get('User-Agent', '')
372 ua = req.headers.get('User-Agent', '')
374 if cmd == 'rev' and 'mercurial' in ua:
373 if cmd == 'rev' and 'mercurial' in ua:
375 req.qsparams['style'] = 'raw'
374 req.qsparams['style'] = 'raw'
376
375
377 if cmd == 'archive':
376 if cmd == 'archive':
378 fn = req.qsparams['node']
377 fn = req.qsparams['node']
379 for type_, spec in webutil.archivespecs.iteritems():
378 for type_, spec in webutil.archivespecs.iteritems():
380 ext = spec[2]
379 ext = spec[2]
381 if fn.endswith(ext):
380 if fn.endswith(ext):
382 req.qsparams['node'] = fn[:-len(ext)]
381 req.qsparams['node'] = fn[:-len(ext)]
383 req.qsparams['type'] = type_
382 req.qsparams['type'] = type_
384 else:
383 else:
385 cmd = req.qsparams.get('cmd', '')
384 cmd = req.qsparams.get('cmd', '')
386
385
387 # process the web interface request
386 # process the web interface request
388
387
389 try:
388 try:
390 rctx.tmpl = rctx.templater(req)
389 rctx.tmpl = rctx.templater(req)
391 ctype = rctx.tmpl.render('mimetype',
390 ctype = rctx.tmpl.render('mimetype',
392 {'encoding': encoding.encoding})
391 {'encoding': encoding.encoding})
393
392
394 # check read permissions non-static content
393 # check read permissions non-static content
395 if cmd != 'static':
394 if cmd != 'static':
396 self.check_perm(rctx, req, None)
395 self.check_perm(rctx, req, None)
397
396
398 if cmd == '':
397 if cmd == '':
399 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
398 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
400 cmd = req.qsparams['cmd']
399 cmd = req.qsparams['cmd']
401
400
402 # Don't enable caching if using a CSP nonce because then it wouldn't
401 # Don't enable caching if using a CSP nonce because then it wouldn't
403 # be a nonce.
402 # be a nonce.
404 if rctx.configbool('web', 'cache') and not rctx.nonce:
403 if rctx.configbool('web', 'cache') and not rctx.nonce:
405 tag = 'W/"%d"' % self.mtime
404 tag = 'W/"%d"' % self.mtime
406 if req.headers.get('If-None-Match') == tag:
405 if req.headers.get('If-None-Match') == tag:
407 res.status = '304 Not Modified'
406 res.status = '304 Not Modified'
408 # Content-Type may be defined globally. It isn't valid on a
407 # Content-Type may be defined globally. It isn't valid on a
409 # 304, so discard it.
408 # 304, so discard it.
410 try:
409 try:
411 del res.headers[b'Content-Type']
410 del res.headers[b'Content-Type']
412 except KeyError:
411 except KeyError:
413 pass
412 pass
414 # Response body not allowed on 304.
413 # Response body not allowed on 304.
415 res.setbodybytes('')
414 res.setbodybytes('')
416 return res.sendresponse()
415 return res.sendresponse()
417
416
418 res.headers['ETag'] = tag
417 res.headers['ETag'] = tag
419
418
420 if cmd not in webcommands.__all__:
419 if cmd not in webcommands.__all__:
421 msg = 'no such method: %s' % cmd
420 msg = 'no such method: %s' % cmd
422 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
421 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
423 else:
422 else:
424 # Set some globals appropriate for web handlers. Commands can
423 # Set some globals appropriate for web handlers. Commands can
425 # override easily enough.
424 # override easily enough.
426 res.status = '200 Script output follows'
425 res.status = '200 Script output follows'
427 res.headers['Content-Type'] = ctype
426 res.headers['Content-Type'] = ctype
428 return getattr(webcommands, cmd)(rctx)
427 return getattr(webcommands, cmd)(rctx)
429
428
430 except (error.LookupError, error.RepoLookupError) as err:
429 except (error.LookupError, error.RepoLookupError) as err:
431 msg = pycompat.bytestr(err)
430 msg = pycompat.bytestr(err)
432 if (util.safehasattr(err, 'name') and
431 if (util.safehasattr(err, 'name') and
433 not isinstance(err, error.ManifestLookupError)):
432 not isinstance(err, error.ManifestLookupError)):
434 msg = 'revision not found: %s' % err.name
433 msg = 'revision not found: %s' % err.name
435
434
436 res.status = '404 Not Found'
435 res.status = '404 Not Found'
437 res.headers['Content-Type'] = ctype
436 res.headers['Content-Type'] = ctype
438 return rctx.sendtemplate('error', error=msg)
437 return rctx.sendtemplate('error', error=msg)
439 except (error.RepoError, error.RevlogError) as e:
438 except (error.RepoError, error.RevlogError) as e:
440 res.status = '500 Internal Server Error'
439 res.status = '500 Internal Server Error'
441 res.headers['Content-Type'] = ctype
440 res.headers['Content-Type'] = ctype
442 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
441 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
443 except ErrorResponse as e:
442 except ErrorResponse as e:
444 for k, v in e.headers:
443 for k, v in e.headers:
445 res.headers[k] = v
444 res.headers[k] = v
446 res.status = statusmessage(e.code, pycompat.bytestr(e))
445 res.status = statusmessage(e.code, pycompat.bytestr(e))
447 res.headers['Content-Type'] = ctype
446 res.headers['Content-Type'] = ctype
448 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
447 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
449
448
450 def check_perm(self, rctx, req, op):
449 def check_perm(self, rctx, req, op):
451 for permhook in permhooks:
450 for permhook in permhooks:
452 permhook(rctx, req, op)
451 permhook(rctx, req, op)
453
452
454 def getwebview(repo):
453 def getwebview(repo):
455 """The 'web.view' config controls changeset filter to hgweb. Possible
454 """The 'web.view' config controls changeset filter to hgweb. Possible
456 values are ``served``, ``visible`` and ``all``. Default is ``served``.
455 values are ``served``, ``visible`` and ``all``. Default is ``served``.
457 The ``served`` filter only shows changesets that can be pulled from the
456 The ``served`` filter only shows changesets that can be pulled from the
458 hgweb instance. The``visible`` filter includes secret changesets but
457 hgweb instance. The``visible`` filter includes secret changesets but
459 still excludes "hidden" one.
458 still excludes "hidden" one.
460
459
461 See the repoview module for details.
460 See the repoview module for details.
462
461
463 The option has been around undocumented since Mercurial 2.5, but no
462 The option has been around undocumented since Mercurial 2.5, but no
464 user ever asked about it. So we better keep it undocumented for now."""
463 user ever asked about it. So we better keep it undocumented for now."""
465 # experimental config: web.view
464 # experimental config: web.view
466 viewconfig = repo.ui.config('web', 'view', untrusted=True)
465 viewconfig = repo.ui.config('web', 'view', untrusted=True)
467 if viewconfig == 'all':
466 if viewconfig == 'all':
468 return repo.unfiltered()
467 return repo.unfiltered()
469 elif viewconfig in repoview.filtertable:
468 elif viewconfig in repoview.filtertable:
470 return repo.filtered(viewconfig)
469 return repo.filtered(viewconfig)
471 else:
470 else:
472 return repo.filtered('served')
471 return repo.filtered('served')
@@ -1,533 +1,535 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
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.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import gc
11 import gc
12 import os
12 import os
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_SERVER_ERROR,
19 HTTP_SERVER_ERROR,
20 cspvalues,
20 cspvalues,
21 get_contact,
21 get_contact,
22 get_mtime,
22 get_mtime,
23 ismember,
23 ismember,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 statusmessage,
26 statusmessage,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 configitems,
30 configitems,
31 encoding,
31 encoding,
32 error,
32 error,
33 hg,
33 hg,
34 profiling,
34 profiling,
35 pycompat,
35 pycompat,
36 registrar,
36 scmutil,
37 scmutil,
37 templater,
38 templater,
38 templateutil,
39 templateutil,
39 ui as uimod,
40 ui as uimod,
40 util,
41 util,
41 )
42 )
42
43
43 from . import (
44 from . import (
44 hgweb_mod,
45 hgweb_mod,
45 request as requestmod,
46 request as requestmod,
46 webutil,
47 webutil,
47 wsgicgi,
48 wsgicgi,
48 )
49 )
49 from ..utils import dateutil
50 from ..utils import dateutil
50
51
51 def cleannames(items):
52 def cleannames(items):
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53
54
54 def findrepos(paths):
55 def findrepos(paths):
55 repos = []
56 repos = []
56 for prefix, root in cleannames(paths):
57 for prefix, root in cleannames(paths):
57 roothead, roottail = os.path.split(root)
58 roothead, roottail = os.path.split(root)
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 # /bar/ be served as as foo/N .
60 # /bar/ be served as as foo/N .
60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 # '*' will not search inside dirs with .hg (except .hg/patches),
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 try:
63 try:
63 recurse = {'*': False, '**': True}[roottail]
64 recurse = {'*': False, '**': True}[roottail]
64 except KeyError:
65 except KeyError:
65 repos.append((prefix, root))
66 repos.append((prefix, root))
66 continue
67 continue
67 roothead = os.path.normpath(os.path.abspath(roothead))
68 roothead = os.path.normpath(os.path.abspath(roothead))
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 repos.extend(urlrepos(prefix, roothead, paths))
70 repos.extend(urlrepos(prefix, roothead, paths))
70 return repos
71 return repos
71
72
72 def urlrepos(prefix, roothead, paths):
73 def urlrepos(prefix, roothead, paths):
73 """yield url paths and filesystem paths from a list of repo paths
74 """yield url paths and filesystem paths from a list of repo paths
74
75
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 """
81 """
81 for path in paths:
82 for path in paths:
82 path = os.path.normpath(path)
83 path = os.path.normpath(path)
83 yield (prefix + '/' +
84 yield (prefix + '/' +
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85
86
86 def readallowed(ui, req):
87 def readallowed(ui, req):
87 """Check allow_read and deny_read config options of a repo's ui object
88 """Check allow_read and deny_read config options of a repo's ui object
88 to determine user permissions. By default, with neither option set (or
89 to determine user permissions. By default, with neither option set (or
89 both empty), allow all users to read the repo. There are two ways a
90 both empty), allow all users to read the repo. There are two ways a
90 user can be denied read access: (1) deny_read is not empty, and the
91 user can be denied read access: (1) deny_read is not empty, and the
91 user is unauthenticated or deny_read contains user (or *), and (2)
92 user is unauthenticated or deny_read contains user (or *), and (2)
92 allow_read is not empty and the user is not in allow_read. Return True
93 allow_read is not empty and the user is not in allow_read. Return True
93 if user is allowed to read the repo, else return False."""
94 if user is allowed to read the repo, else return False."""
94
95
95 user = req.remoteuser
96 user = req.remoteuser
96
97
97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 if deny_read and (not user or ismember(ui, user, deny_read)):
99 if deny_read and (not user or ismember(ui, user, deny_read)):
99 return False
100 return False
100
101
101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 # by default, allow reading if no allow_read option has been set
103 # by default, allow reading if no allow_read option has been set
103 if not allow_read or ismember(ui, user, allow_read):
104 if not allow_read or ismember(ui, user, allow_read):
104 return True
105 return True
105
106
106 return False
107 return False
107
108
108 def rawindexentries(ui, repos, req, subdir=''):
109 def rawindexentries(ui, repos, req, subdir=''):
109 descend = ui.configbool('web', 'descend')
110 descend = ui.configbool('web', 'descend')
110 collapse = ui.configbool('web', 'collapse')
111 collapse = ui.configbool('web', 'collapse')
111 seenrepos = set()
112 seenrepos = set()
112 seendirs = set()
113 seendirs = set()
113 for name, path in repos:
114 for name, path in repos:
114
115
115 if not name.startswith(subdir):
116 if not name.startswith(subdir):
116 continue
117 continue
117 name = name[len(subdir):]
118 name = name[len(subdir):]
118 directory = False
119 directory = False
119
120
120 if '/' in name:
121 if '/' in name:
121 if not descend:
122 if not descend:
122 continue
123 continue
123
124
124 nameparts = name.split('/')
125 nameparts = name.split('/')
125 rootname = nameparts[0]
126 rootname = nameparts[0]
126
127
127 if not collapse:
128 if not collapse:
128 pass
129 pass
129 elif rootname in seendirs:
130 elif rootname in seendirs:
130 continue
131 continue
131 elif rootname in seenrepos:
132 elif rootname in seenrepos:
132 pass
133 pass
133 else:
134 else:
134 directory = True
135 directory = True
135 name = rootname
136 name = rootname
136
137
137 # redefine the path to refer to the directory
138 # redefine the path to refer to the directory
138 discarded = '/'.join(nameparts[1:])
139 discarded = '/'.join(nameparts[1:])
139
140
140 # remove name parts plus accompanying slash
141 # remove name parts plus accompanying slash
141 path = path[:-len(discarded) - 1]
142 path = path[:-len(discarded) - 1]
142
143
143 try:
144 try:
144 r = hg.repository(ui, path)
145 r = hg.repository(ui, path)
145 directory = False
146 directory = False
146 except (IOError, error.RepoError):
147 except (IOError, error.RepoError):
147 pass
148 pass
148
149
149 parts = [
150 parts = [
150 req.apppath.strip('/'),
151 req.apppath.strip('/'),
151 subdir.strip('/'),
152 subdir.strip('/'),
152 name.strip('/'),
153 name.strip('/'),
153 ]
154 ]
154 url = '/' + '/'.join(p for p in parts if p) + '/'
155 url = '/' + '/'.join(p for p in parts if p) + '/'
155
156
156 # show either a directory entry or a repository
157 # show either a directory entry or a repository
157 if directory:
158 if directory:
158 # get the directory's time information
159 # get the directory's time information
159 try:
160 try:
160 d = (get_mtime(path), dateutil.makedate()[1])
161 d = (get_mtime(path), dateutil.makedate()[1])
161 except OSError:
162 except OSError:
162 continue
163 continue
163
164
164 # add '/' to the name to make it obvious that
165 # add '/' to the name to make it obvious that
165 # the entry is a directory, not a regular repository
166 # the entry is a directory, not a regular repository
166 row = {'contact': "",
167 row = {'contact': "",
167 'contact_sort': "",
168 'contact_sort': "",
168 'name': name + '/',
169 'name': name + '/',
169 'name_sort': name,
170 'name_sort': name,
170 'url': url,
171 'url': url,
171 'description': "",
172 'description': "",
172 'description_sort': "",
173 'description_sort': "",
173 'lastchange': d,
174 'lastchange': d,
174 'lastchange_sort': d[1] - d[0],
175 'lastchange_sort': d[1] - d[0],
175 'archives': templateutil.mappinglist([]),
176 'archives': templateutil.mappinglist([]),
176 'isdirectory': True,
177 'isdirectory': True,
177 'labels': templateutil.hybridlist([], name='label'),
178 'labels': templateutil.hybridlist([], name='label'),
178 }
179 }
179
180
180 seendirs.add(name)
181 seendirs.add(name)
181 yield row
182 yield row
182 continue
183 continue
183
184
184 u = ui.copy()
185 u = ui.copy()
185 try:
186 try:
186 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
187 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
187 except Exception as e:
188 except Exception as e:
188 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
189 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
189 continue
190 continue
190
191
191 def get(section, name, default=uimod._unset):
192 def get(section, name, default=uimod._unset):
192 return u.config(section, name, default, untrusted=True)
193 return u.config(section, name, default, untrusted=True)
193
194
194 if u.configbool("web", "hidden", untrusted=True):
195 if u.configbool("web", "hidden", untrusted=True):
195 continue
196 continue
196
197
197 if not readallowed(u, req):
198 if not readallowed(u, req):
198 continue
199 continue
199
200
200 # update time with local timezone
201 # update time with local timezone
201 try:
202 try:
202 r = hg.repository(ui, path)
203 r = hg.repository(ui, path)
203 except IOError:
204 except IOError:
204 u.warn(_('error accessing repository at %s\n') % path)
205 u.warn(_('error accessing repository at %s\n') % path)
205 continue
206 continue
206 except error.RepoError:
207 except error.RepoError:
207 u.warn(_('error accessing repository at %s\n') % path)
208 u.warn(_('error accessing repository at %s\n') % path)
208 continue
209 continue
209 try:
210 try:
210 d = (get_mtime(r.spath), dateutil.makedate()[1])
211 d = (get_mtime(r.spath), dateutil.makedate()[1])
211 except OSError:
212 except OSError:
212 continue
213 continue
213
214
214 contact = get_contact(get)
215 contact = get_contact(get)
215 description = get("web", "description")
216 description = get("web", "description")
216 seenrepos.add(name)
217 seenrepos.add(name)
217 name = get("web", "name", name)
218 name = get("web", "name", name)
218 labels = u.configlist('web', 'labels', untrusted=True)
219 labels = u.configlist('web', 'labels', untrusted=True)
219 row = {'contact': contact or "unknown",
220 row = {'contact': contact or "unknown",
220 'contact_sort': contact.upper() or "unknown",
221 'contact_sort': contact.upper() or "unknown",
221 'name': name,
222 'name': name,
222 'name_sort': name,
223 'name_sort': name,
223 'url': url,
224 'url': url,
224 'description': description or "unknown",
225 'description': description or "unknown",
225 'description_sort': description.upper() or "unknown",
226 'description_sort': description.upper() or "unknown",
226 'lastchange': d,
227 'lastchange': d,
227 'lastchange_sort': d[1] - d[0],
228 'lastchange_sort': d[1] - d[0],
228 'archives': webutil.archivelist(u, "tip", url),
229 'archives': webutil.archivelist(u, "tip", url),
229 'isdirectory': None,
230 'isdirectory': None,
230 'labels': templateutil.hybridlist(labels, name='label'),
231 'labels': templateutil.hybridlist(labels, name='label'),
231 }
232 }
232
233
233 yield row
234 yield row
234
235
235 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
236 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
236 descending, subdir):
237 descending, subdir):
237 rows = rawindexentries(ui, repos, req, subdir=subdir)
238 rows = rawindexentries(ui, repos, req, subdir=subdir)
238
239
239 sortdefault = None, False
240 sortdefault = None, False
240
241
241 if sortcolumn and sortdefault != (sortcolumn, descending):
242 if sortcolumn and sortdefault != (sortcolumn, descending):
242 sortkey = '%s_sort' % sortcolumn
243 sortkey = '%s_sort' % sortcolumn
243 rows = sorted(rows, key=lambda x: x[sortkey],
244 rows = sorted(rows, key=lambda x: x[sortkey],
244 reverse=descending)
245 reverse=descending)
245
246
246 for row, parity in zip(rows, paritygen(stripecount)):
247 for row, parity in zip(rows, paritygen(stripecount)):
247 row['parity'] = parity
248 row['parity'] = parity
248 yield row
249 yield row
249
250
250 def indexentries(ui, repos, req, stripecount, sortcolumn='',
251 def indexentries(ui, repos, req, stripecount, sortcolumn='',
251 descending=False, subdir=''):
252 descending=False, subdir=''):
252 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
253 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
253 return templateutil.mappinggenerator(_indexentriesgen, args=args)
254 return templateutil.mappinggenerator(_indexentriesgen, args=args)
254
255
255 class hgwebdir(object):
256 class hgwebdir(object):
256 """HTTP server for multiple repositories.
257 """HTTP server for multiple repositories.
257
258
258 Given a configuration, different repositories will be served depending
259 Given a configuration, different repositories will be served depending
259 on the request path.
260 on the request path.
260
261
261 Instances are typically used as WSGI applications.
262 Instances are typically used as WSGI applications.
262 """
263 """
263 def __init__(self, conf, baseui=None):
264 def __init__(self, conf, baseui=None):
264 self.conf = conf
265 self.conf = conf
265 self.baseui = baseui
266 self.baseui = baseui
266 self.ui = None
267 self.ui = None
267 self.lastrefresh = 0
268 self.lastrefresh = 0
268 self.motd = None
269 self.motd = None
269 self.refresh()
270 self.refresh()
270
271
271 def refresh(self):
272 def refresh(self):
272 if self.ui:
273 if self.ui:
273 refreshinterval = self.ui.configint('web', 'refreshinterval')
274 refreshinterval = self.ui.configint('web', 'refreshinterval')
274 else:
275 else:
275 item = configitems.coreitems['web']['refreshinterval']
276 item = configitems.coreitems['web']['refreshinterval']
276 refreshinterval = item.default
277 refreshinterval = item.default
277
278
278 # refreshinterval <= 0 means to always refresh.
279 # refreshinterval <= 0 means to always refresh.
279 if (refreshinterval > 0 and
280 if (refreshinterval > 0 and
280 self.lastrefresh + refreshinterval > time.time()):
281 self.lastrefresh + refreshinterval > time.time()):
281 return
282 return
282
283
283 if self.baseui:
284 if self.baseui:
284 u = self.baseui.copy()
285 u = self.baseui.copy()
285 else:
286 else:
286 u = uimod.ui.load()
287 u = uimod.ui.load()
287 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
288 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
288 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
289 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
289 # displaying bundling progress bar while serving feels wrong and may
290 # displaying bundling progress bar while serving feels wrong and may
290 # break some wsgi implementations.
291 # break some wsgi implementations.
291 u.setconfig('progress', 'disable', 'true', 'hgweb')
292 u.setconfig('progress', 'disable', 'true', 'hgweb')
292
293
293 if not isinstance(self.conf, (dict, list, tuple)):
294 if not isinstance(self.conf, (dict, list, tuple)):
294 map = {'paths': 'hgweb-paths'}
295 map = {'paths': 'hgweb-paths'}
295 if not os.path.exists(self.conf):
296 if not os.path.exists(self.conf):
296 raise error.Abort(_('config file %s not found!') % self.conf)
297 raise error.Abort(_('config file %s not found!') % self.conf)
297 u.readconfig(self.conf, remap=map, trust=True)
298 u.readconfig(self.conf, remap=map, trust=True)
298 paths = []
299 paths = []
299 for name, ignored in u.configitems('hgweb-paths'):
300 for name, ignored in u.configitems('hgweb-paths'):
300 for path in u.configlist('hgweb-paths', name):
301 for path in u.configlist('hgweb-paths', name):
301 paths.append((name, path))
302 paths.append((name, path))
302 elif isinstance(self.conf, (list, tuple)):
303 elif isinstance(self.conf, (list, tuple)):
303 paths = self.conf
304 paths = self.conf
304 elif isinstance(self.conf, dict):
305 elif isinstance(self.conf, dict):
305 paths = self.conf.items()
306 paths = self.conf.items()
306
307
307 repos = findrepos(paths)
308 repos = findrepos(paths)
308 for prefix, root in u.configitems('collections'):
309 for prefix, root in u.configitems('collections'):
309 prefix = util.pconvert(prefix)
310 prefix = util.pconvert(prefix)
310 for path in scmutil.walkrepos(root, followsym=True):
311 for path in scmutil.walkrepos(root, followsym=True):
311 repo = os.path.normpath(path)
312 repo = os.path.normpath(path)
312 name = util.pconvert(repo)
313 name = util.pconvert(repo)
313 if name.startswith(prefix):
314 if name.startswith(prefix):
314 name = name[len(prefix):]
315 name = name[len(prefix):]
315 repos.append((name.lstrip('/'), repo))
316 repos.append((name.lstrip('/'), repo))
316
317
317 self.repos = repos
318 self.repos = repos
318 self.ui = u
319 self.ui = u
319 encoding.encoding = self.ui.config('web', 'encoding')
320 encoding.encoding = self.ui.config('web', 'encoding')
320 self.style = self.ui.config('web', 'style')
321 self.style = self.ui.config('web', 'style')
321 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
322 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
322 self.stripecount = self.ui.config('web', 'stripes')
323 self.stripecount = self.ui.config('web', 'stripes')
323 if self.stripecount:
324 if self.stripecount:
324 self.stripecount = int(self.stripecount)
325 self.stripecount = int(self.stripecount)
325 prefix = self.ui.config('web', 'prefix')
326 prefix = self.ui.config('web', 'prefix')
326 if prefix.startswith('/'):
327 if prefix.startswith('/'):
327 prefix = prefix[1:]
328 prefix = prefix[1:]
328 if prefix.endswith('/'):
329 if prefix.endswith('/'):
329 prefix = prefix[:-1]
330 prefix = prefix[:-1]
330 self.prefix = prefix
331 self.prefix = prefix
331 self.lastrefresh = time.time()
332 self.lastrefresh = time.time()
332
333
333 def run(self):
334 def run(self):
334 if not encoding.environ.get('GATEWAY_INTERFACE',
335 if not encoding.environ.get('GATEWAY_INTERFACE',
335 '').startswith("CGI/1."):
336 '').startswith("CGI/1."):
336 raise RuntimeError("This function is only intended to be "
337 raise RuntimeError("This function is only intended to be "
337 "called while running as a CGI script.")
338 "called while running as a CGI script.")
338 wsgicgi.launch(self)
339 wsgicgi.launch(self)
339
340
340 def __call__(self, env, respond):
341 def __call__(self, env, respond):
341 baseurl = self.ui.config('web', 'baseurl')
342 baseurl = self.ui.config('web', 'baseurl')
342 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
343 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
343 res = requestmod.wsgiresponse(req, respond)
344 res = requestmod.wsgiresponse(req, respond)
344
345
345 return self.run_wsgi(req, res)
346 return self.run_wsgi(req, res)
346
347
347 def run_wsgi(self, req, res):
348 def run_wsgi(self, req, res):
348 profile = self.ui.configbool('profiling', 'enabled')
349 profile = self.ui.configbool('profiling', 'enabled')
349 with profiling.profile(self.ui, enabled=profile):
350 with profiling.profile(self.ui, enabled=profile):
350 try:
351 try:
351 for r in self._runwsgi(req, res):
352 for r in self._runwsgi(req, res):
352 yield r
353 yield r
353 finally:
354 finally:
354 # There are known cycles in localrepository that prevent
355 # There are known cycles in localrepository that prevent
355 # those objects (and tons of held references) from being
356 # those objects (and tons of held references) from being
356 # collected through normal refcounting. We mitigate those
357 # collected through normal refcounting. We mitigate those
357 # leaks by performing an explicit GC on every request.
358 # leaks by performing an explicit GC on every request.
358 # TODO remove this once leaks are fixed.
359 # TODO remove this once leaks are fixed.
359 # TODO only run this on requests that create localrepository
360 # TODO only run this on requests that create localrepository
360 # instances instead of every request.
361 # instances instead of every request.
361 gc.collect()
362 gc.collect()
362
363
363 def _runwsgi(self, req, res):
364 def _runwsgi(self, req, res):
364 try:
365 try:
365 self.refresh()
366 self.refresh()
366
367
367 csp, nonce = cspvalues(self.ui)
368 csp, nonce = cspvalues(self.ui)
368 if csp:
369 if csp:
369 res.headers['Content-Security-Policy'] = csp
370 res.headers['Content-Security-Policy'] = csp
370
371
371 virtual = req.dispatchpath.strip('/')
372 virtual = req.dispatchpath.strip('/')
372 tmpl = self.templater(req, nonce)
373 tmpl = self.templater(req, nonce)
373 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
374 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
374
375
375 # Global defaults. These can be overridden by any handler.
376 # Global defaults. These can be overridden by any handler.
376 res.status = '200 Script output follows'
377 res.status = '200 Script output follows'
377 res.headers['Content-Type'] = ctype
378 res.headers['Content-Type'] = ctype
378
379
379 # a static file
380 # a static file
380 if virtual.startswith('static/') or 'static' in req.qsparams:
381 if virtual.startswith('static/') or 'static' in req.qsparams:
381 if virtual.startswith('static/'):
382 if virtual.startswith('static/'):
382 fname = virtual[7:]
383 fname = virtual[7:]
383 else:
384 else:
384 fname = req.qsparams['static']
385 fname = req.qsparams['static']
385 static = self.ui.config("web", "static", None,
386 static = self.ui.config("web", "static", None,
386 untrusted=False)
387 untrusted=False)
387 if not static:
388 if not static:
388 tp = self.templatepath or templater.templatepaths()
389 tp = self.templatepath or templater.templatepaths()
389 if isinstance(tp, str):
390 if isinstance(tp, str):
390 tp = [tp]
391 tp = [tp]
391 static = [os.path.join(p, 'static') for p in tp]
392 static = [os.path.join(p, 'static') for p in tp]
392
393
393 staticfile(static, fname, res)
394 staticfile(static, fname, res)
394 return res.sendresponse()
395 return res.sendresponse()
395
396
396 # top-level index
397 # top-level index
397
398
398 repos = dict(self.repos)
399 repos = dict(self.repos)
399
400
400 if (not virtual or virtual == 'index') and virtual not in repos:
401 if (not virtual or virtual == 'index') and virtual not in repos:
401 return self.makeindex(req, res, tmpl)
402 return self.makeindex(req, res, tmpl)
402
403
403 # nested indexes and hgwebs
404 # nested indexes and hgwebs
404
405
405 if virtual.endswith('/index') and virtual not in repos:
406 if virtual.endswith('/index') and virtual not in repos:
406 subdir = virtual[:-len('index')]
407 subdir = virtual[:-len('index')]
407 if any(r.startswith(subdir) for r in repos):
408 if any(r.startswith(subdir) for r in repos):
408 return self.makeindex(req, res, tmpl, subdir)
409 return self.makeindex(req, res, tmpl, subdir)
409
410
410 def _virtualdirs():
411 def _virtualdirs():
411 # Check the full virtual path, each parent, and the root ('')
412 # Check the full virtual path, each parent, and the root ('')
412 if virtual != '':
413 if virtual != '':
413 yield virtual
414 yield virtual
414
415
415 for p in util.finddirs(virtual):
416 for p in util.finddirs(virtual):
416 yield p
417 yield p
417
418
418 yield ''
419 yield ''
419
420
420 for virtualrepo in _virtualdirs():
421 for virtualrepo in _virtualdirs():
421 real = repos.get(virtualrepo)
422 real = repos.get(virtualrepo)
422 if real:
423 if real:
423 # Re-parse the WSGI environment to take into account our
424 # Re-parse the WSGI environment to take into account our
424 # repository path component.
425 # repository path component.
425 uenv = req.rawenv
426 uenv = req.rawenv
426 if pycompat.ispy3:
427 if pycompat.ispy3:
427 uenv = {k.decode('latin1'): v for k, v in
428 uenv = {k.decode('latin1'): v for k, v in
428 uenv.iteritems()}
429 uenv.iteritems()}
429 req = requestmod.parserequestfromenv(
430 req = requestmod.parserequestfromenv(
430 uenv, reponame=virtualrepo,
431 uenv, reponame=virtualrepo,
431 altbaseurl=self.ui.config('web', 'baseurl'),
432 altbaseurl=self.ui.config('web', 'baseurl'),
432 # Reuse wrapped body file object otherwise state
433 # Reuse wrapped body file object otherwise state
433 # tracking can get confused.
434 # tracking can get confused.
434 bodyfh=req.bodyfh)
435 bodyfh=req.bodyfh)
435 try:
436 try:
436 # ensure caller gets private copy of ui
437 # ensure caller gets private copy of ui
437 repo = hg.repository(self.ui.copy(), real)
438 repo = hg.repository(self.ui.copy(), real)
438 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
439 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
439 except IOError as inst:
440 except IOError as inst:
440 msg = encoding.strtolocal(inst.strerror)
441 msg = encoding.strtolocal(inst.strerror)
441 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
442 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
442 except error.RepoError as inst:
443 except error.RepoError as inst:
443 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
444 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
444
445
445 # browse subdirectories
446 # browse subdirectories
446 subdir = virtual + '/'
447 subdir = virtual + '/'
447 if [r for r in repos if r.startswith(subdir)]:
448 if [r for r in repos if r.startswith(subdir)]:
448 return self.makeindex(req, res, tmpl, subdir)
449 return self.makeindex(req, res, tmpl, subdir)
449
450
450 # prefixes not found
451 # prefixes not found
451 res.status = '404 Not Found'
452 res.status = '404 Not Found'
452 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
453 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
453 return res.sendresponse()
454 return res.sendresponse()
454
455
455 except ErrorResponse as e:
456 except ErrorResponse as e:
456 res.status = statusmessage(e.code, pycompat.bytestr(e))
457 res.status = statusmessage(e.code, pycompat.bytestr(e))
457 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
458 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
458 return res.sendresponse()
459 return res.sendresponse()
459 finally:
460 finally:
460 tmpl = None
461 tmpl = None
461
462
462 def makeindex(self, req, res, tmpl, subdir=""):
463 def makeindex(self, req, res, tmpl, subdir=""):
463 self.refresh()
464 self.refresh()
464 sortable = ["name", "description", "contact", "lastchange"]
465 sortable = ["name", "description", "contact", "lastchange"]
465 sortcolumn, descending = None, False
466 sortcolumn, descending = None, False
466 if 'sort' in req.qsparams:
467 if 'sort' in req.qsparams:
467 sortcolumn = req.qsparams['sort']
468 sortcolumn = req.qsparams['sort']
468 descending = sortcolumn.startswith('-')
469 descending = sortcolumn.startswith('-')
469 if descending:
470 if descending:
470 sortcolumn = sortcolumn[1:]
471 sortcolumn = sortcolumn[1:]
471 if sortcolumn not in sortable:
472 if sortcolumn not in sortable:
472 sortcolumn = ""
473 sortcolumn = ""
473
474
474 sort = [("sort_%s" % column,
475 sort = [("sort_%s" % column,
475 "%s%s" % ((not descending and column == sortcolumn)
476 "%s%s" % ((not descending and column == sortcolumn)
476 and "-" or "", column))
477 and "-" or "", column))
477 for column in sortable]
478 for column in sortable]
478
479
479 self.refresh()
480 self.refresh()
480
481
481 entries = indexentries(self.ui, self.repos, req,
482 entries = indexentries(self.ui, self.repos, req,
482 self.stripecount, sortcolumn=sortcolumn,
483 self.stripecount, sortcolumn=sortcolumn,
483 descending=descending, subdir=subdir)
484 descending=descending, subdir=subdir)
484
485
485 mapping = {
486 mapping = {
486 'entries': entries,
487 'entries': entries,
487 'subdir': subdir,
488 'subdir': subdir,
488 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
489 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
489 'sortcolumn': sortcolumn,
490 'sortcolumn': sortcolumn,
490 'descending': descending,
491 'descending': descending,
491 }
492 }
492 mapping.update(sort)
493 mapping.update(sort)
493 res.setbodygen(tmpl.generate('index', mapping))
494 res.setbodygen(tmpl.generate('index', mapping))
494 return res.sendresponse()
495 return res.sendresponse()
495
496
496 def templater(self, req, nonce):
497 def templater(self, req, nonce):
497
498
498 def motd(**map):
499 if self.motd is not None:
500 yield self.motd
501 else:
502 yield config('web', 'motd')
503
504 def config(section, name, default=uimod._unset, untrusted=True):
499 def config(section, name, default=uimod._unset, untrusted=True):
505 return self.ui.config(section, name, default, untrusted)
500 return self.ui.config(section, name, default, untrusted)
506
501
507 vars = {}
502 vars = {}
508 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
503 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
509 self.templatepath)
504 self.templatepath)
510 if style == styles[0]:
505 if style == styles[0]:
511 vars['style'] = style
506 vars['style'] = style
512
507
513 sessionvars = webutil.sessionvars(vars, r'?')
508 sessionvars = webutil.sessionvars(vars, r'?')
514 logourl = config('web', 'logourl')
509 logourl = config('web', 'logourl')
515 logoimg = config('web', 'logoimg')
510 logoimg = config('web', 'logoimg')
516 staticurl = (config('web', 'staticurl')
511 staticurl = (config('web', 'staticurl')
517 or req.apppath.rstrip('/') + '/static/')
512 or req.apppath.rstrip('/') + '/static/')
518 if not staticurl.endswith('/'):
513 if not staticurl.endswith('/'):
519 staticurl += '/'
514 staticurl += '/'
520
515
521 defaults = {
516 defaults = {
522 "encoding": encoding.encoding,
517 "encoding": encoding.encoding,
523 "motd": motd,
524 "url": req.apppath + '/',
518 "url": req.apppath + '/',
525 "logourl": logourl,
519 "logourl": logourl,
526 "logoimg": logoimg,
520 "logoimg": logoimg,
527 "staticurl": staticurl,
521 "staticurl": staticurl,
528 "sessionvars": sessionvars,
522 "sessionvars": sessionvars,
529 "style": style,
523 "style": style,
530 "nonce": nonce,
524 "nonce": nonce,
531 }
525 }
526 templatekeyword = registrar.templatekeyword(defaults)
527 @templatekeyword('motd', requires=())
528 def motd(context, mapping):
529 if self.motd is not None:
530 yield self.motd
531 else:
532 yield config('web', 'motd')
533
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
534 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
533 return tmpl
535 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now