##// END OF EJS Templates
hgweb: use ui._unset to prevent a warning in configitems
David Demelier -
r33328:c8f212cb default
parent child Browse files
Show More
@@ -1,484 +1,484 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 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25 from .request import wsgirequest
25 from .request import wsgirequest
26
26
27 from .. import (
27 from .. import (
28 encoding,
28 encoding,
29 error,
29 error,
30 hg,
30 hg,
31 hook,
31 hook,
32 profiling,
32 profiling,
33 repoview,
33 repoview,
34 templatefilters,
34 templatefilters,
35 templater,
35 templater,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 )
38 )
39
39
40 from . import (
40 from . import (
41 protocol,
41 protocol,
42 webcommands,
42 webcommands,
43 webutil,
43 webutil,
44 wsgicgi,
44 wsgicgi,
45 )
45 )
46
46
47 perms = {
47 perms = {
48 'changegroup': 'pull',
48 'changegroup': 'pull',
49 'changegroupsubset': 'pull',
49 'changegroupsubset': 'pull',
50 'getbundle': 'pull',
50 'getbundle': 'pull',
51 'stream_out': 'pull',
51 'stream_out': 'pull',
52 'listkeys': 'pull',
52 'listkeys': 'pull',
53 'unbundle': 'push',
53 'unbundle': 'push',
54 'pushkey': 'push',
54 'pushkey': 'push',
55 }
55 }
56
56
57 archivespecs = util.sortdict((
57 archivespecs = util.sortdict((
58 ('zip', ('application/zip', 'zip', '.zip', None)),
58 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
59 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
60 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ))
61 ))
62
62
63 def makebreadcrumb(url, prefix=''):
63 def makebreadcrumb(url, prefix=''):
64 '''Return a 'URL breadcrumb' list
64 '''Return a 'URL breadcrumb' list
65
65
66 A 'URL breadcrumb' is a list of URL-name pairs,
66 A 'URL breadcrumb' is a list of URL-name pairs,
67 corresponding to each of the path items on a URL.
67 corresponding to each of the path items on a URL.
68 This can be used to create path navigation entries.
68 This can be used to create path navigation entries.
69 '''
69 '''
70 if url.endswith('/'):
70 if url.endswith('/'):
71 url = url[:-1]
71 url = url[:-1]
72 if prefix:
72 if prefix:
73 url = '/' + prefix + url
73 url = '/' + prefix + url
74 relpath = url
74 relpath = url
75 if relpath.startswith('/'):
75 if relpath.startswith('/'):
76 relpath = relpath[1:]
76 relpath = relpath[1:]
77
77
78 breadcrumb = []
78 breadcrumb = []
79 urlel = url
79 urlel = url
80 pathitems = [''] + relpath.split('/')
80 pathitems = [''] + relpath.split('/')
81 for pathel in reversed(pathitems):
81 for pathel in reversed(pathitems):
82 if not pathel or not urlel:
82 if not pathel or not urlel:
83 break
83 break
84 breadcrumb.append({'url': urlel, 'name': pathel})
84 breadcrumb.append({'url': urlel, 'name': pathel})
85 urlel = os.path.dirname(urlel)
85 urlel = os.path.dirname(urlel)
86 return reversed(breadcrumb)
86 return reversed(breadcrumb)
87
87
88 class requestcontext(object):
88 class requestcontext(object):
89 """Holds state/context for an individual request.
89 """Holds state/context for an individual request.
90
90
91 Servers can be multi-threaded. Holding state on the WSGI application
91 Servers can be multi-threaded. Holding state on the WSGI application
92 is prone to race conditions. Instances of this class exist to hold
92 is prone to race conditions. Instances of this class exist to hold
93 mutable and race-free state for requests.
93 mutable and race-free state for requests.
94 """
94 """
95 def __init__(self, app, repo):
95 def __init__(self, app, repo):
96 self.repo = repo
96 self.repo = repo
97 self.reponame = app.reponame
97 self.reponame = app.reponame
98
98
99 self.archivespecs = archivespecs
99 self.archivespecs = archivespecs
100
100
101 self.maxchanges = self.configint('web', 'maxchanges', 10)
101 self.maxchanges = self.configint('web', 'maxchanges', 10)
102 self.stripecount = self.configint('web', 'stripes', 1)
102 self.stripecount = self.configint('web', 'stripes', 1)
103 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
103 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
104 self.maxfiles = self.configint('web', 'maxfiles', 10)
104 self.maxfiles = self.configint('web', 'maxfiles', 10)
105 self.allowpull = self.configbool('web', 'allowpull', True)
105 self.allowpull = self.configbool('web', 'allowpull', True)
106
106
107 # we use untrusted=False to prevent a repo owner from using
107 # we use untrusted=False to prevent a repo owner from using
108 # web.templates in .hg/hgrc to get access to any file readable
108 # web.templates in .hg/hgrc to get access to any file readable
109 # by the user running the CGI script
109 # by the user running the CGI script
110 self.templatepath = self.config('web', 'templates', untrusted=False)
110 self.templatepath = self.config('web', 'templates', untrusted=False)
111
111
112 # This object is more expensive to build than simple config values.
112 # This object is more expensive to build than simple config values.
113 # It is shared across requests. The app will replace the object
113 # It is shared across requests. The app will replace the object
114 # if it is updated. Since this is a reference and nothing should
114 # if it is updated. Since this is a reference and nothing should
115 # modify the underlying object, it should be constant for the lifetime
115 # modify the underlying object, it should be constant for the lifetime
116 # of the request.
116 # of the request.
117 self.websubtable = app.websubtable
117 self.websubtable = app.websubtable
118
118
119 self.csp, self.nonce = cspvalues(self.repo.ui)
119 self.csp, self.nonce = cspvalues(self.repo.ui)
120
120
121 # Trust the settings from the .hg/hgrc files by default.
121 # Trust the settings from the .hg/hgrc files by default.
122 def config(self, section, name, default=None, untrusted=True):
122 def config(self, section, name, default=uimod._unset, untrusted=True):
123 return self.repo.ui.config(section, name, default,
123 return self.repo.ui.config(section, name, default,
124 untrusted=untrusted)
124 untrusted=untrusted)
125
125
126 def configbool(self, section, name, default=False, untrusted=True):
126 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 return self.repo.ui.configbool(section, name, default,
127 return self.repo.ui.configbool(section, name, default,
128 untrusted=untrusted)
128 untrusted=untrusted)
129
129
130 def configint(self, section, name, default=None, untrusted=True):
130 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 return self.repo.ui.configint(section, name, default,
131 return self.repo.ui.configint(section, name, default,
132 untrusted=untrusted)
132 untrusted=untrusted)
133
133
134 def configlist(self, section, name, default=None, untrusted=True):
134 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 return self.repo.ui.configlist(section, name, default,
135 return self.repo.ui.configlist(section, name, default,
136 untrusted=untrusted)
136 untrusted=untrusted)
137
137
138 def archivelist(self, nodeid):
138 def archivelist(self, nodeid):
139 allowed = self.configlist('web', 'allow_archive')
139 allowed = self.configlist('web', 'allow_archive')
140 for typ, spec in self.archivespecs.iteritems():
140 for typ, spec in self.archivespecs.iteritems():
141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143
143
144 def templater(self, req):
144 def templater(self, req):
145 # determine scheme, port and server name
145 # determine scheme, port and server name
146 # this is needed to create absolute urls
146 # this is needed to create absolute urls
147
147
148 proto = req.env.get('wsgi.url_scheme')
148 proto = req.env.get('wsgi.url_scheme')
149 if proto == 'https':
149 if proto == 'https':
150 proto = 'https'
150 proto = 'https'
151 default_port = '443'
151 default_port = '443'
152 else:
152 else:
153 proto = 'http'
153 proto = 'http'
154 default_port = '80'
154 default_port = '80'
155
155
156 port = req.env['SERVER_PORT']
156 port = req.env['SERVER_PORT']
157 port = port != default_port and (':' + port) or ''
157 port = port != default_port and (':' + port) or ''
158 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
158 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
159 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
159 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
160 logoimg = self.config('web', 'logoimg', 'hglogo.png')
160 logoimg = self.config('web', 'logoimg', 'hglogo.png')
161 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
161 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
162 if not staticurl.endswith('/'):
162 if not staticurl.endswith('/'):
163 staticurl += '/'
163 staticurl += '/'
164
164
165 # some functions for the templater
165 # some functions for the templater
166
166
167 def motd(**map):
167 def motd(**map):
168 yield self.config('web', 'motd', '')
168 yield self.config('web', 'motd', '')
169
169
170 # figure out which style to use
170 # figure out which style to use
171
171
172 vars = {}
172 vars = {}
173 styles = (
173 styles = (
174 req.form.get('style', [None])[0],
174 req.form.get('style', [None])[0],
175 self.config('web', 'style'),
175 self.config('web', 'style'),
176 'paper',
176 'paper',
177 )
177 )
178 style, mapfile = templater.stylemap(styles, self.templatepath)
178 style, mapfile = templater.stylemap(styles, self.templatepath)
179 if style == styles[0]:
179 if style == styles[0]:
180 vars['style'] = style
180 vars['style'] = style
181
181
182 start = req.url[-1] == '?' and '&' or '?'
182 start = req.url[-1] == '?' and '&' or '?'
183 sessionvars = webutil.sessionvars(vars, start)
183 sessionvars = webutil.sessionvars(vars, start)
184
184
185 if not self.reponame:
185 if not self.reponame:
186 self.reponame = (self.config('web', 'name')
186 self.reponame = (self.config('web', 'name')
187 or req.env.get('REPO_NAME')
187 or req.env.get('REPO_NAME')
188 or req.url.strip('/') or self.repo.root)
188 or req.url.strip('/') or self.repo.root)
189
189
190 def websubfilter(text):
190 def websubfilter(text):
191 return templatefilters.websub(text, self.websubtable)
191 return templatefilters.websub(text, self.websubtable)
192
192
193 # create the templater
193 # create the templater
194
194
195 defaults = {
195 defaults = {
196 'url': req.url,
196 'url': req.url,
197 'logourl': logourl,
197 'logourl': logourl,
198 'logoimg': logoimg,
198 'logoimg': logoimg,
199 'staticurl': staticurl,
199 'staticurl': staticurl,
200 'urlbase': urlbase,
200 'urlbase': urlbase,
201 'repo': self.reponame,
201 'repo': self.reponame,
202 'encoding': encoding.encoding,
202 'encoding': encoding.encoding,
203 'motd': motd,
203 'motd': motd,
204 'sessionvars': sessionvars,
204 'sessionvars': sessionvars,
205 'pathdef': makebreadcrumb(req.url),
205 'pathdef': makebreadcrumb(req.url),
206 'style': style,
206 'style': style,
207 'nonce': self.nonce,
207 'nonce': self.nonce,
208 }
208 }
209 tmpl = templater.templater.frommapfile(mapfile,
209 tmpl = templater.templater.frommapfile(mapfile,
210 filters={'websub': websubfilter},
210 filters={'websub': websubfilter},
211 defaults=defaults)
211 defaults=defaults)
212 return tmpl
212 return tmpl
213
213
214
214
215 class hgweb(object):
215 class hgweb(object):
216 """HTTP server for individual repositories.
216 """HTTP server for individual repositories.
217
217
218 Instances of this class serve HTTP responses for a particular
218 Instances of this class serve HTTP responses for a particular
219 repository.
219 repository.
220
220
221 Instances are typically used as WSGI applications.
221 Instances are typically used as WSGI applications.
222
222
223 Some servers are multi-threaded. On these servers, there may
223 Some servers are multi-threaded. On these servers, there may
224 be multiple active threads inside __call__.
224 be multiple active threads inside __call__.
225 """
225 """
226 def __init__(self, repo, name=None, baseui=None):
226 def __init__(self, repo, name=None, baseui=None):
227 if isinstance(repo, str):
227 if isinstance(repo, str):
228 if baseui:
228 if baseui:
229 u = baseui.copy()
229 u = baseui.copy()
230 else:
230 else:
231 u = uimod.ui.load()
231 u = uimod.ui.load()
232 r = hg.repository(u, repo)
232 r = hg.repository(u, repo)
233 else:
233 else:
234 # we trust caller to give us a private copy
234 # we trust caller to give us a private copy
235 r = repo
235 r = repo
236
236
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
241 # resolve file patterns relative to repo root
241 # resolve file patterns relative to repo root
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
244 # displaying bundling progress bar while serving feel wrong and may
244 # displaying bundling progress bar while serving feel wrong and may
245 # break some wsgi implementation.
245 # break some wsgi implementation.
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
249 self._lastrepo = self._repos[0]
249 self._lastrepo = self._repos[0]
250 hook.redirect(True)
250 hook.redirect(True)
251 self.reponame = name
251 self.reponame = name
252
252
253 def _webifyrepo(self, repo):
253 def _webifyrepo(self, repo):
254 repo = getwebview(repo)
254 repo = getwebview(repo)
255 self.websubtable = webutil.getwebsubs(repo)
255 self.websubtable = webutil.getwebsubs(repo)
256 return repo
256 return repo
257
257
258 @contextlib.contextmanager
258 @contextlib.contextmanager
259 def _obtainrepo(self):
259 def _obtainrepo(self):
260 """Obtain a repo unique to the caller.
260 """Obtain a repo unique to the caller.
261
261
262 Internally we maintain a stack of cachedlocalrepo instances
262 Internally we maintain a stack of cachedlocalrepo instances
263 to be handed out. If one is available, we pop it and return it,
263 to be handed out. If one is available, we pop it and return it,
264 ensuring it is up to date in the process. If one is not available,
264 ensuring it is up to date in the process. If one is not available,
265 we clone the most recently used repo instance and return it.
265 we clone the most recently used repo instance and return it.
266
266
267 It is currently possible for the stack to grow without bounds
267 It is currently possible for the stack to grow without bounds
268 if the server allows infinite threads. However, servers should
268 if the server allows infinite threads. However, servers should
269 have a thread limit, thus establishing our limit.
269 have a thread limit, thus establishing our limit.
270 """
270 """
271 if self._repos:
271 if self._repos:
272 cached = self._repos.pop()
272 cached = self._repos.pop()
273 r, created = cached.fetch()
273 r, created = cached.fetch()
274 else:
274 else:
275 cached = self._lastrepo.copy()
275 cached = self._lastrepo.copy()
276 r, created = cached.fetch()
276 r, created = cached.fetch()
277 if created:
277 if created:
278 r = self._webifyrepo(r)
278 r = self._webifyrepo(r)
279
279
280 self._lastrepo = cached
280 self._lastrepo = cached
281 self.mtime = cached.mtime
281 self.mtime = cached.mtime
282 try:
282 try:
283 yield r
283 yield r
284 finally:
284 finally:
285 self._repos.append(cached)
285 self._repos.append(cached)
286
286
287 def run(self):
287 def run(self):
288 """Start a server from CGI environment.
288 """Start a server from CGI environment.
289
289
290 Modern servers should be using WSGI and should avoid this
290 Modern servers should be using WSGI and should avoid this
291 method, if possible.
291 method, if possible.
292 """
292 """
293 if not encoding.environ.get('GATEWAY_INTERFACE',
293 if not encoding.environ.get('GATEWAY_INTERFACE',
294 '').startswith("CGI/1."):
294 '').startswith("CGI/1."):
295 raise RuntimeError("This function is only intended to be "
295 raise RuntimeError("This function is only intended to be "
296 "called while running as a CGI script.")
296 "called while running as a CGI script.")
297 wsgicgi.launch(self)
297 wsgicgi.launch(self)
298
298
299 def __call__(self, env, respond):
299 def __call__(self, env, respond):
300 """Run the WSGI application.
300 """Run the WSGI application.
301
301
302 This may be called by multiple threads.
302 This may be called by multiple threads.
303 """
303 """
304 req = wsgirequest(env, respond)
304 req = wsgirequest(env, respond)
305 return self.run_wsgi(req)
305 return self.run_wsgi(req)
306
306
307 def run_wsgi(self, req):
307 def run_wsgi(self, req):
308 """Internal method to run the WSGI application.
308 """Internal method to run the WSGI application.
309
309
310 This is typically only called by Mercurial. External consumers
310 This is typically only called by Mercurial. External consumers
311 should be using instances of this class as the WSGI application.
311 should be using instances of this class as the WSGI application.
312 """
312 """
313 with self._obtainrepo() as repo:
313 with self._obtainrepo() as repo:
314 profile = repo.ui.configbool('profiling', 'enabled')
314 profile = repo.ui.configbool('profiling', 'enabled')
315 with profiling.profile(repo.ui, enabled=profile):
315 with profiling.profile(repo.ui, enabled=profile):
316 for r in self._runwsgi(req, repo):
316 for r in self._runwsgi(req, repo):
317 yield r
317 yield r
318
318
319 def _runwsgi(self, req, repo):
319 def _runwsgi(self, req, repo):
320 rctx = requestcontext(self, repo)
320 rctx = requestcontext(self, repo)
321
321
322 # This state is global across all threads.
322 # This state is global across all threads.
323 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
323 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
324 rctx.repo.ui.environ = req.env
324 rctx.repo.ui.environ = req.env
325
325
326 if rctx.csp:
326 if rctx.csp:
327 # hgwebdir may have added CSP header. Since we generate our own,
327 # hgwebdir may have added CSP header. Since we generate our own,
328 # replace it.
328 # replace it.
329 req.headers = [h for h in req.headers
329 req.headers = [h for h in req.headers
330 if h[0] != 'Content-Security-Policy']
330 if h[0] != 'Content-Security-Policy']
331 req.headers.append(('Content-Security-Policy', rctx.csp))
331 req.headers.append(('Content-Security-Policy', rctx.csp))
332
332
333 # work with CGI variables to create coherent structure
333 # work with CGI variables to create coherent structure
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
335
335
336 req.url = req.env['SCRIPT_NAME']
336 req.url = req.env['SCRIPT_NAME']
337 if not req.url.endswith('/'):
337 if not req.url.endswith('/'):
338 req.url += '/'
338 req.url += '/'
339 if req.env.get('REPO_NAME'):
339 if req.env.get('REPO_NAME'):
340 req.url += req.env['REPO_NAME'] + '/'
340 req.url += req.env['REPO_NAME'] + '/'
341
341
342 if 'PATH_INFO' in req.env:
342 if 'PATH_INFO' in req.env:
343 parts = req.env['PATH_INFO'].strip('/').split('/')
343 parts = req.env['PATH_INFO'].strip('/').split('/')
344 repo_parts = req.env.get('REPO_NAME', '').split('/')
344 repo_parts = req.env.get('REPO_NAME', '').split('/')
345 if parts[:len(repo_parts)] == repo_parts:
345 if parts[:len(repo_parts)] == repo_parts:
346 parts = parts[len(repo_parts):]
346 parts = parts[len(repo_parts):]
347 query = '/'.join(parts)
347 query = '/'.join(parts)
348 else:
348 else:
349 query = req.env['QUERY_STRING'].partition('&')[0]
349 query = req.env['QUERY_STRING'].partition('&')[0]
350 query = query.partition(';')[0]
350 query = query.partition(';')[0]
351
351
352 # process this if it's a protocol request
352 # process this if it's a protocol request
353 # protocol bits don't need to create any URLs
353 # protocol bits don't need to create any URLs
354 # and the clients always use the old URL structure
354 # and the clients always use the old URL structure
355
355
356 cmd = req.form.get('cmd', [''])[0]
356 cmd = req.form.get('cmd', [''])[0]
357 if protocol.iscmd(cmd):
357 if protocol.iscmd(cmd):
358 try:
358 try:
359 if query:
359 if query:
360 raise ErrorResponse(HTTP_NOT_FOUND)
360 raise ErrorResponse(HTTP_NOT_FOUND)
361 if cmd in perms:
361 if cmd in perms:
362 self.check_perm(rctx, req, perms[cmd])
362 self.check_perm(rctx, req, perms[cmd])
363 return protocol.call(rctx.repo, req, cmd)
363 return protocol.call(rctx.repo, req, cmd)
364 except ErrorResponse as inst:
364 except ErrorResponse as inst:
365 # A client that sends unbundle without 100-continue will
365 # A client that sends unbundle without 100-continue will
366 # break if we respond early.
366 # break if we respond early.
367 if (cmd == 'unbundle' and
367 if (cmd == 'unbundle' and
368 (req.env.get('HTTP_EXPECT',
368 (req.env.get('HTTP_EXPECT',
369 '').lower() != '100-continue') or
369 '').lower() != '100-continue') or
370 req.env.get('X-HgHttp2', '')):
370 req.env.get('X-HgHttp2', '')):
371 req.drain()
371 req.drain()
372 else:
372 else:
373 req.headers.append(('Connection', 'Close'))
373 req.headers.append(('Connection', 'Close'))
374 req.respond(inst, protocol.HGTYPE,
374 req.respond(inst, protocol.HGTYPE,
375 body='0\n%s\n' % inst)
375 body='0\n%s\n' % inst)
376 return ''
376 return ''
377
377
378 # translate user-visible url structure to internal structure
378 # translate user-visible url structure to internal structure
379
379
380 args = query.split('/', 2)
380 args = query.split('/', 2)
381 if 'cmd' not in req.form and args and args[0]:
381 if 'cmd' not in req.form and args and args[0]:
382
382
383 cmd = args.pop(0)
383 cmd = args.pop(0)
384 style = cmd.rfind('-')
384 style = cmd.rfind('-')
385 if style != -1:
385 if style != -1:
386 req.form['style'] = [cmd[:style]]
386 req.form['style'] = [cmd[:style]]
387 cmd = cmd[style + 1:]
387 cmd = cmd[style + 1:]
388
388
389 # avoid accepting e.g. style parameter as command
389 # avoid accepting e.g. style parameter as command
390 if util.safehasattr(webcommands, cmd):
390 if util.safehasattr(webcommands, cmd):
391 req.form['cmd'] = [cmd]
391 req.form['cmd'] = [cmd]
392
392
393 if cmd == 'static':
393 if cmd == 'static':
394 req.form['file'] = ['/'.join(args)]
394 req.form['file'] = ['/'.join(args)]
395 else:
395 else:
396 if args and args[0]:
396 if args and args[0]:
397 node = args.pop(0).replace('%2F', '/')
397 node = args.pop(0).replace('%2F', '/')
398 req.form['node'] = [node]
398 req.form['node'] = [node]
399 if args:
399 if args:
400 req.form['file'] = args
400 req.form['file'] = args
401
401
402 ua = req.env.get('HTTP_USER_AGENT', '')
402 ua = req.env.get('HTTP_USER_AGENT', '')
403 if cmd == 'rev' and 'mercurial' in ua:
403 if cmd == 'rev' and 'mercurial' in ua:
404 req.form['style'] = ['raw']
404 req.form['style'] = ['raw']
405
405
406 if cmd == 'archive':
406 if cmd == 'archive':
407 fn = req.form['node'][0]
407 fn = req.form['node'][0]
408 for type_, spec in rctx.archivespecs.iteritems():
408 for type_, spec in rctx.archivespecs.iteritems():
409 ext = spec[2]
409 ext = spec[2]
410 if fn.endswith(ext):
410 if fn.endswith(ext):
411 req.form['node'] = [fn[:-len(ext)]]
411 req.form['node'] = [fn[:-len(ext)]]
412 req.form['type'] = [type_]
412 req.form['type'] = [type_]
413
413
414 # process the web interface request
414 # process the web interface request
415
415
416 try:
416 try:
417 tmpl = rctx.templater(req)
417 tmpl = rctx.templater(req)
418 ctype = tmpl('mimetype', encoding=encoding.encoding)
418 ctype = tmpl('mimetype', encoding=encoding.encoding)
419 ctype = templater.stringify(ctype)
419 ctype = templater.stringify(ctype)
420
420
421 # check read permissions non-static content
421 # check read permissions non-static content
422 if cmd != 'static':
422 if cmd != 'static':
423 self.check_perm(rctx, req, None)
423 self.check_perm(rctx, req, None)
424
424
425 if cmd == '':
425 if cmd == '':
426 req.form['cmd'] = [tmpl.cache['default']]
426 req.form['cmd'] = [tmpl.cache['default']]
427 cmd = req.form['cmd'][0]
427 cmd = req.form['cmd'][0]
428
428
429 # Don't enable caching if using a CSP nonce because then it wouldn't
429 # Don't enable caching if using a CSP nonce because then it wouldn't
430 # be a nonce.
430 # be a nonce.
431 if rctx.configbool('web', 'cache', True) and not rctx.nonce:
431 if rctx.configbool('web', 'cache', True) and not rctx.nonce:
432 caching(self, req) # sets ETag header or raises NOT_MODIFIED
432 caching(self, req) # sets ETag header or raises NOT_MODIFIED
433 if cmd not in webcommands.__all__:
433 if cmd not in webcommands.__all__:
434 msg = 'no such method: %s' % cmd
434 msg = 'no such method: %s' % cmd
435 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
435 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
436 elif cmd == 'file' and 'raw' in req.form.get('style', []):
436 elif cmd == 'file' and 'raw' in req.form.get('style', []):
437 rctx.ctype = ctype
437 rctx.ctype = ctype
438 content = webcommands.rawfile(rctx, req, tmpl)
438 content = webcommands.rawfile(rctx, req, tmpl)
439 else:
439 else:
440 content = getattr(webcommands, cmd)(rctx, req, tmpl)
440 content = getattr(webcommands, cmd)(rctx, req, tmpl)
441 req.respond(HTTP_OK, ctype)
441 req.respond(HTTP_OK, ctype)
442
442
443 return content
443 return content
444
444
445 except (error.LookupError, error.RepoLookupError) as err:
445 except (error.LookupError, error.RepoLookupError) as err:
446 req.respond(HTTP_NOT_FOUND, ctype)
446 req.respond(HTTP_NOT_FOUND, ctype)
447 msg = str(err)
447 msg = str(err)
448 if (util.safehasattr(err, 'name') and
448 if (util.safehasattr(err, 'name') and
449 not isinstance(err, error.ManifestLookupError)):
449 not isinstance(err, error.ManifestLookupError)):
450 msg = 'revision not found: %s' % err.name
450 msg = 'revision not found: %s' % err.name
451 return tmpl('error', error=msg)
451 return tmpl('error', error=msg)
452 except (error.RepoError, error.RevlogError) as inst:
452 except (error.RepoError, error.RevlogError) as inst:
453 req.respond(HTTP_SERVER_ERROR, ctype)
453 req.respond(HTTP_SERVER_ERROR, ctype)
454 return tmpl('error', error=str(inst))
454 return tmpl('error', error=str(inst))
455 except ErrorResponse as inst:
455 except ErrorResponse as inst:
456 req.respond(inst, ctype)
456 req.respond(inst, ctype)
457 if inst.code == HTTP_NOT_MODIFIED:
457 if inst.code == HTTP_NOT_MODIFIED:
458 # Not allowed to return a body on a 304
458 # Not allowed to return a body on a 304
459 return ['']
459 return ['']
460 return tmpl('error', error=str(inst))
460 return tmpl('error', error=str(inst))
461
461
462 def check_perm(self, rctx, req, op):
462 def check_perm(self, rctx, req, op):
463 for permhook in permhooks:
463 for permhook in permhooks:
464 permhook(rctx, req, op)
464 permhook(rctx, req, op)
465
465
466 def getwebview(repo):
466 def getwebview(repo):
467 """The 'web.view' config controls changeset filter to hgweb. Possible
467 """The 'web.view' config controls changeset filter to hgweb. Possible
468 values are ``served``, ``visible`` and ``all``. Default is ``served``.
468 values are ``served``, ``visible`` and ``all``. Default is ``served``.
469 The ``served`` filter only shows changesets that can be pulled from the
469 The ``served`` filter only shows changesets that can be pulled from the
470 hgweb instance. The``visible`` filter includes secret changesets but
470 hgweb instance. The``visible`` filter includes secret changesets but
471 still excludes "hidden" one.
471 still excludes "hidden" one.
472
472
473 See the repoview module for details.
473 See the repoview module for details.
474
474
475 The option has been around undocumented since Mercurial 2.5, but no
475 The option has been around undocumented since Mercurial 2.5, but no
476 user ever asked about it. So we better keep it undocumented for now."""
476 user ever asked about it. So we better keep it undocumented for now."""
477 viewconfig = repo.ui.config('web', 'view', 'served',
477 viewconfig = repo.ui.config('web', 'view', 'served',
478 untrusted=True)
478 untrusted=True)
479 if viewconfig == 'all':
479 if viewconfig == 'all':
480 return repo.unfiltered()
480 return repo.unfiltered()
481 elif viewconfig in repoview.filtertable:
481 elif viewconfig in repoview.filtertable:
482 return repo.filtered(viewconfig)
482 return repo.filtered(viewconfig)
483 else:
483 else:
484 return repo.filtered('served')
484 return repo.filtered('served')
@@ -1,540 +1,540 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 os
11 import os
12 import re
12 import re
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_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 cspvalues,
23 get_contact,
23 get_contact,
24 get_mtime,
24 get_mtime,
25 ismember,
25 ismember,
26 paritygen,
26 paritygen,
27 staticfile,
27 staticfile,
28 )
28 )
29 from .request import wsgirequest
29 from .request import wsgirequest
30
30
31 from .. import (
31 from .. import (
32 encoding,
32 encoding,
33 error,
33 error,
34 hg,
34 hg,
35 profiling,
35 profiling,
36 scmutil,
36 scmutil,
37 templater,
37 templater,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 hgweb_mod,
43 hgweb_mod,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 def cleannames(items):
48 def cleannames(items):
49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
50
50
51 def findrepos(paths):
51 def findrepos(paths):
52 repos = []
52 repos = []
53 for prefix, root in cleannames(paths):
53 for prefix, root in cleannames(paths):
54 roothead, roottail = os.path.split(root)
54 roothead, roottail = os.path.split(root)
55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
56 # /bar/ be served as as foo/N .
56 # /bar/ be served as as foo/N .
57 # '*' will not search inside dirs with .hg (except .hg/patches),
57 # '*' will not search inside dirs with .hg (except .hg/patches),
58 # '**' will search inside dirs with .hg (and thus also find subrepos).
58 # '**' will search inside dirs with .hg (and thus also find subrepos).
59 try:
59 try:
60 recurse = {'*': False, '**': True}[roottail]
60 recurse = {'*': False, '**': True}[roottail]
61 except KeyError:
61 except KeyError:
62 repos.append((prefix, root))
62 repos.append((prefix, root))
63 continue
63 continue
64 roothead = os.path.normpath(os.path.abspath(roothead))
64 roothead = os.path.normpath(os.path.abspath(roothead))
65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
66 repos.extend(urlrepos(prefix, roothead, paths))
66 repos.extend(urlrepos(prefix, roothead, paths))
67 return repos
67 return repos
68
68
69 def urlrepos(prefix, roothead, paths):
69 def urlrepos(prefix, roothead, paths):
70 """yield url paths and filesystem paths from a list of repo paths
70 """yield url paths and filesystem paths from a list of repo paths
71
71
72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
73 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
73 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
75 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
75 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
77 """
77 """
78 for path in paths:
78 for path in paths:
79 path = os.path.normpath(path)
79 path = os.path.normpath(path)
80 yield (prefix + '/' +
80 yield (prefix + '/' +
81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
82
82
83 def geturlcgivars(baseurl, port):
83 def geturlcgivars(baseurl, port):
84 """
84 """
85 Extract CGI variables from baseurl
85 Extract CGI variables from baseurl
86
86
87 >>> geturlcgivars("http://host.org/base", "80")
87 >>> geturlcgivars("http://host.org/base", "80")
88 ('host.org', '80', '/base')
88 ('host.org', '80', '/base')
89 >>> geturlcgivars("http://host.org:8000/base", "80")
89 >>> geturlcgivars("http://host.org:8000/base", "80")
90 ('host.org', '8000', '/base')
90 ('host.org', '8000', '/base')
91 >>> geturlcgivars('/base', 8000)
91 >>> geturlcgivars('/base', 8000)
92 ('', '8000', '/base')
92 ('', '8000', '/base')
93 >>> geturlcgivars("base", '8000')
93 >>> geturlcgivars("base", '8000')
94 ('', '8000', '/base')
94 ('', '8000', '/base')
95 >>> geturlcgivars("http://host", '8000')
95 >>> geturlcgivars("http://host", '8000')
96 ('host', '8000', '/')
96 ('host', '8000', '/')
97 >>> geturlcgivars("http://host/", '8000')
97 >>> geturlcgivars("http://host/", '8000')
98 ('host', '8000', '/')
98 ('host', '8000', '/')
99 """
99 """
100 u = util.url(baseurl)
100 u = util.url(baseurl)
101 name = u.host or ''
101 name = u.host or ''
102 if u.port:
102 if u.port:
103 port = u.port
103 port = u.port
104 path = u.path or ""
104 path = u.path or ""
105 if not path.startswith('/'):
105 if not path.startswith('/'):
106 path = '/' + path
106 path = '/' + path
107
107
108 return name, str(port), path
108 return name, str(port), path
109
109
110 class hgwebdir(object):
110 class hgwebdir(object):
111 """HTTP server for multiple repositories.
111 """HTTP server for multiple repositories.
112
112
113 Given a configuration, different repositories will be served depending
113 Given a configuration, different repositories will be served depending
114 on the request path.
114 on the request path.
115
115
116 Instances are typically used as WSGI applications.
116 Instances are typically used as WSGI applications.
117 """
117 """
118 def __init__(self, conf, baseui=None):
118 def __init__(self, conf, baseui=None):
119 self.conf = conf
119 self.conf = conf
120 self.baseui = baseui
120 self.baseui = baseui
121 self.ui = None
121 self.ui = None
122 self.lastrefresh = 0
122 self.lastrefresh = 0
123 self.motd = None
123 self.motd = None
124 self.refresh()
124 self.refresh()
125
125
126 def refresh(self):
126 def refresh(self):
127 refreshinterval = 20
127 refreshinterval = 20
128 if self.ui:
128 if self.ui:
129 refreshinterval = self.ui.configint('web', 'refreshinterval',
129 refreshinterval = self.ui.configint('web', 'refreshinterval',
130 refreshinterval)
130 refreshinterval)
131
131
132 # refreshinterval <= 0 means to always refresh.
132 # refreshinterval <= 0 means to always refresh.
133 if (refreshinterval > 0 and
133 if (refreshinterval > 0 and
134 self.lastrefresh + refreshinterval > time.time()):
134 self.lastrefresh + refreshinterval > time.time()):
135 return
135 return
136
136
137 if self.baseui:
137 if self.baseui:
138 u = self.baseui.copy()
138 u = self.baseui.copy()
139 else:
139 else:
140 u = uimod.ui.load()
140 u = uimod.ui.load()
141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
143 # displaying bundling progress bar while serving feels wrong and may
143 # displaying bundling progress bar while serving feels wrong and may
144 # break some wsgi implementations.
144 # break some wsgi implementations.
145 u.setconfig('progress', 'disable', 'true', 'hgweb')
145 u.setconfig('progress', 'disable', 'true', 'hgweb')
146
146
147 if not isinstance(self.conf, (dict, list, tuple)):
147 if not isinstance(self.conf, (dict, list, tuple)):
148 map = {'paths': 'hgweb-paths'}
148 map = {'paths': 'hgweb-paths'}
149 if not os.path.exists(self.conf):
149 if not os.path.exists(self.conf):
150 raise error.Abort(_('config file %s not found!') % self.conf)
150 raise error.Abort(_('config file %s not found!') % self.conf)
151 u.readconfig(self.conf, remap=map, trust=True)
151 u.readconfig(self.conf, remap=map, trust=True)
152 paths = []
152 paths = []
153 for name, ignored in u.configitems('hgweb-paths'):
153 for name, ignored in u.configitems('hgweb-paths'):
154 for path in u.configlist('hgweb-paths', name):
154 for path in u.configlist('hgweb-paths', name):
155 paths.append((name, path))
155 paths.append((name, path))
156 elif isinstance(self.conf, (list, tuple)):
156 elif isinstance(self.conf, (list, tuple)):
157 paths = self.conf
157 paths = self.conf
158 elif isinstance(self.conf, dict):
158 elif isinstance(self.conf, dict):
159 paths = self.conf.items()
159 paths = self.conf.items()
160
160
161 repos = findrepos(paths)
161 repos = findrepos(paths)
162 for prefix, root in u.configitems('collections'):
162 for prefix, root in u.configitems('collections'):
163 prefix = util.pconvert(prefix)
163 prefix = util.pconvert(prefix)
164 for path in scmutil.walkrepos(root, followsym=True):
164 for path in scmutil.walkrepos(root, followsym=True):
165 repo = os.path.normpath(path)
165 repo = os.path.normpath(path)
166 name = util.pconvert(repo)
166 name = util.pconvert(repo)
167 if name.startswith(prefix):
167 if name.startswith(prefix):
168 name = name[len(prefix):]
168 name = name[len(prefix):]
169 repos.append((name.lstrip('/'), repo))
169 repos.append((name.lstrip('/'), repo))
170
170
171 self.repos = repos
171 self.repos = repos
172 self.ui = u
172 self.ui = u
173 encoding.encoding = self.ui.config('web', 'encoding',
173 encoding.encoding = self.ui.config('web', 'encoding',
174 encoding.encoding)
174 encoding.encoding)
175 self.style = self.ui.config('web', 'style', 'paper')
175 self.style = self.ui.config('web', 'style', 'paper')
176 self.templatepath = self.ui.config('web', 'templates', None)
176 self.templatepath = self.ui.config('web', 'templates', None)
177 self.stripecount = self.ui.config('web', 'stripes', 1)
177 self.stripecount = self.ui.config('web', 'stripes', 1)
178 if self.stripecount:
178 if self.stripecount:
179 self.stripecount = int(self.stripecount)
179 self.stripecount = int(self.stripecount)
180 self._baseurl = self.ui.config('web', 'baseurl')
180 self._baseurl = self.ui.config('web', 'baseurl')
181 prefix = self.ui.config('web', 'prefix', '')
181 prefix = self.ui.config('web', 'prefix', '')
182 if prefix.startswith('/'):
182 if prefix.startswith('/'):
183 prefix = prefix[1:]
183 prefix = prefix[1:]
184 if prefix.endswith('/'):
184 if prefix.endswith('/'):
185 prefix = prefix[:-1]
185 prefix = prefix[:-1]
186 self.prefix = prefix
186 self.prefix = prefix
187 self.lastrefresh = time.time()
187 self.lastrefresh = time.time()
188
188
189 def run(self):
189 def run(self):
190 if not encoding.environ.get('GATEWAY_INTERFACE',
190 if not encoding.environ.get('GATEWAY_INTERFACE',
191 '').startswith("CGI/1."):
191 '').startswith("CGI/1."):
192 raise RuntimeError("This function is only intended to be "
192 raise RuntimeError("This function is only intended to be "
193 "called while running as a CGI script.")
193 "called while running as a CGI script.")
194 wsgicgi.launch(self)
194 wsgicgi.launch(self)
195
195
196 def __call__(self, env, respond):
196 def __call__(self, env, respond):
197 req = wsgirequest(env, respond)
197 req = wsgirequest(env, respond)
198 return self.run_wsgi(req)
198 return self.run_wsgi(req)
199
199
200 def read_allowed(self, ui, req):
200 def read_allowed(self, ui, req):
201 """Check allow_read and deny_read config options of a repo's ui object
201 """Check allow_read and deny_read config options of a repo's ui object
202 to determine user permissions. By default, with neither option set (or
202 to determine user permissions. By default, with neither option set (or
203 both empty), allow all users to read the repo. There are two ways a
203 both empty), allow all users to read the repo. There are two ways a
204 user can be denied read access: (1) deny_read is not empty, and the
204 user can be denied read access: (1) deny_read is not empty, and the
205 user is unauthenticated or deny_read contains user (or *), and (2)
205 user is unauthenticated or deny_read contains user (or *), and (2)
206 allow_read is not empty and the user is not in allow_read. Return True
206 allow_read is not empty and the user is not in allow_read. Return True
207 if user is allowed to read the repo, else return False."""
207 if user is allowed to read the repo, else return False."""
208
208
209 user = req.env.get('REMOTE_USER')
209 user = req.env.get('REMOTE_USER')
210
210
211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
212 if deny_read and (not user or ismember(ui, user, deny_read)):
212 if deny_read and (not user or ismember(ui, user, deny_read)):
213 return False
213 return False
214
214
215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
216 # by default, allow reading if no allow_read option has been set
216 # by default, allow reading if no allow_read option has been set
217 if (not allow_read) or ismember(ui, user, allow_read):
217 if (not allow_read) or ismember(ui, user, allow_read):
218 return True
218 return True
219
219
220 return False
220 return False
221
221
222 def run_wsgi(self, req):
222 def run_wsgi(self, req):
223 profile = self.ui.configbool('profiling', 'enabled')
223 profile = self.ui.configbool('profiling', 'enabled')
224 with profiling.profile(self.ui, enabled=profile):
224 with profiling.profile(self.ui, enabled=profile):
225 for r in self._runwsgi(req):
225 for r in self._runwsgi(req):
226 yield r
226 yield r
227
227
228 def _runwsgi(self, req):
228 def _runwsgi(self, req):
229 try:
229 try:
230 self.refresh()
230 self.refresh()
231
231
232 csp, nonce = cspvalues(self.ui)
232 csp, nonce = cspvalues(self.ui)
233 if csp:
233 if csp:
234 req.headers.append(('Content-Security-Policy', csp))
234 req.headers.append(('Content-Security-Policy', csp))
235
235
236 virtual = req.env.get("PATH_INFO", "").strip('/')
236 virtual = req.env.get("PATH_INFO", "").strip('/')
237 tmpl = self.templater(req, nonce)
237 tmpl = self.templater(req, nonce)
238 ctype = tmpl('mimetype', encoding=encoding.encoding)
238 ctype = tmpl('mimetype', encoding=encoding.encoding)
239 ctype = templater.stringify(ctype)
239 ctype = templater.stringify(ctype)
240
240
241 # a static file
241 # a static file
242 if virtual.startswith('static/') or 'static' in req.form:
242 if virtual.startswith('static/') or 'static' in req.form:
243 if virtual.startswith('static/'):
243 if virtual.startswith('static/'):
244 fname = virtual[7:]
244 fname = virtual[7:]
245 else:
245 else:
246 fname = req.form['static'][0]
246 fname = req.form['static'][0]
247 static = self.ui.config("web", "static", None,
247 static = self.ui.config("web", "static", None,
248 untrusted=False)
248 untrusted=False)
249 if not static:
249 if not static:
250 tp = self.templatepath or templater.templatepaths()
250 tp = self.templatepath or templater.templatepaths()
251 if isinstance(tp, str):
251 if isinstance(tp, str):
252 tp = [tp]
252 tp = [tp]
253 static = [os.path.join(p, 'static') for p in tp]
253 static = [os.path.join(p, 'static') for p in tp]
254 staticfile(static, fname, req)
254 staticfile(static, fname, req)
255 return []
255 return []
256
256
257 # top-level index
257 # top-level index
258
258
259 repos = dict(self.repos)
259 repos = dict(self.repos)
260
260
261 if (not virtual or virtual == 'index') and virtual not in repos:
261 if (not virtual or virtual == 'index') and virtual not in repos:
262 req.respond(HTTP_OK, ctype)
262 req.respond(HTTP_OK, ctype)
263 return self.makeindex(req, tmpl)
263 return self.makeindex(req, tmpl)
264
264
265 # nested indexes and hgwebs
265 # nested indexes and hgwebs
266
266
267 if virtual.endswith('/index') and virtual not in repos:
267 if virtual.endswith('/index') and virtual not in repos:
268 subdir = virtual[:-len('index')]
268 subdir = virtual[:-len('index')]
269 if any(r.startswith(subdir) for r in repos):
269 if any(r.startswith(subdir) for r in repos):
270 req.respond(HTTP_OK, ctype)
270 req.respond(HTTP_OK, ctype)
271 return self.makeindex(req, tmpl, subdir)
271 return self.makeindex(req, tmpl, subdir)
272
272
273 def _virtualdirs():
273 def _virtualdirs():
274 # Check the full virtual path, each parent, and the root ('')
274 # Check the full virtual path, each parent, and the root ('')
275 if virtual != '':
275 if virtual != '':
276 yield virtual
276 yield virtual
277
277
278 for p in util.finddirs(virtual):
278 for p in util.finddirs(virtual):
279 yield p
279 yield p
280
280
281 yield ''
281 yield ''
282
282
283 for virtualrepo in _virtualdirs():
283 for virtualrepo in _virtualdirs():
284 real = repos.get(virtualrepo)
284 real = repos.get(virtualrepo)
285 if real:
285 if real:
286 req.env['REPO_NAME'] = virtualrepo
286 req.env['REPO_NAME'] = virtualrepo
287 try:
287 try:
288 # ensure caller gets private copy of ui
288 # ensure caller gets private copy of ui
289 repo = hg.repository(self.ui.copy(), real)
289 repo = hg.repository(self.ui.copy(), real)
290 return hgweb_mod.hgweb(repo).run_wsgi(req)
290 return hgweb_mod.hgweb(repo).run_wsgi(req)
291 except IOError as inst:
291 except IOError as inst:
292 msg = inst.strerror
292 msg = inst.strerror
293 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
293 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
294 except error.RepoError as inst:
294 except error.RepoError as inst:
295 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
295 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
296
296
297 # browse subdirectories
297 # browse subdirectories
298 subdir = virtual + '/'
298 subdir = virtual + '/'
299 if [r for r in repos if r.startswith(subdir)]:
299 if [r for r in repos if r.startswith(subdir)]:
300 req.respond(HTTP_OK, ctype)
300 req.respond(HTTP_OK, ctype)
301 return self.makeindex(req, tmpl, subdir)
301 return self.makeindex(req, tmpl, subdir)
302
302
303 # prefixes not found
303 # prefixes not found
304 req.respond(HTTP_NOT_FOUND, ctype)
304 req.respond(HTTP_NOT_FOUND, ctype)
305 return tmpl("notfound", repo=virtual)
305 return tmpl("notfound", repo=virtual)
306
306
307 except ErrorResponse as err:
307 except ErrorResponse as err:
308 req.respond(err, ctype)
308 req.respond(err, ctype)
309 return tmpl('error', error=err.message or '')
309 return tmpl('error', error=err.message or '')
310 finally:
310 finally:
311 tmpl = None
311 tmpl = None
312
312
313 def makeindex(self, req, tmpl, subdir=""):
313 def makeindex(self, req, tmpl, subdir=""):
314
314
315 def archivelist(ui, nodeid, url):
315 def archivelist(ui, nodeid, url):
316 allowed = ui.configlist("web", "allow_archive", untrusted=True)
316 allowed = ui.configlist("web", "allow_archive", untrusted=True)
317 archives = []
317 archives = []
318 for typ, spec in hgweb_mod.archivespecs.iteritems():
318 for typ, spec in hgweb_mod.archivespecs.iteritems():
319 if typ in allowed or ui.configbool("web", "allow" + typ,
319 if typ in allowed or ui.configbool("web", "allow" + typ,
320 untrusted=True):
320 untrusted=True):
321 archives.append({"type" : typ, "extension": spec[2],
321 archives.append({"type" : typ, "extension": spec[2],
322 "node": nodeid, "url": url})
322 "node": nodeid, "url": url})
323 return archives
323 return archives
324
324
325 def rawentries(subdir="", **map):
325 def rawentries(subdir="", **map):
326
326
327 descend = self.ui.configbool('web', 'descend', True)
327 descend = self.ui.configbool('web', 'descend', True)
328 collapse = self.ui.configbool('web', 'collapse', False)
328 collapse = self.ui.configbool('web', 'collapse', False)
329 seenrepos = set()
329 seenrepos = set()
330 seendirs = set()
330 seendirs = set()
331 for name, path in self.repos:
331 for name, path in self.repos:
332
332
333 if not name.startswith(subdir):
333 if not name.startswith(subdir):
334 continue
334 continue
335 name = name[len(subdir):]
335 name = name[len(subdir):]
336 directory = False
336 directory = False
337
337
338 if '/' in name:
338 if '/' in name:
339 if not descend:
339 if not descend:
340 continue
340 continue
341
341
342 nameparts = name.split('/')
342 nameparts = name.split('/')
343 rootname = nameparts[0]
343 rootname = nameparts[0]
344
344
345 if not collapse:
345 if not collapse:
346 pass
346 pass
347 elif rootname in seendirs:
347 elif rootname in seendirs:
348 continue
348 continue
349 elif rootname in seenrepos:
349 elif rootname in seenrepos:
350 pass
350 pass
351 else:
351 else:
352 directory = True
352 directory = True
353 name = rootname
353 name = rootname
354
354
355 # redefine the path to refer to the directory
355 # redefine the path to refer to the directory
356 discarded = '/'.join(nameparts[1:])
356 discarded = '/'.join(nameparts[1:])
357
357
358 # remove name parts plus accompanying slash
358 # remove name parts plus accompanying slash
359 path = path[:-len(discarded) - 1]
359 path = path[:-len(discarded) - 1]
360
360
361 try:
361 try:
362 r = hg.repository(self.ui, path)
362 r = hg.repository(self.ui, path)
363 directory = False
363 directory = False
364 except (IOError, error.RepoError):
364 except (IOError, error.RepoError):
365 pass
365 pass
366
366
367 parts = [name]
367 parts = [name]
368 parts.insert(0, '/' + subdir.rstrip('/'))
368 parts.insert(0, '/' + subdir.rstrip('/'))
369 if req.env['SCRIPT_NAME']:
369 if req.env['SCRIPT_NAME']:
370 parts.insert(0, req.env['SCRIPT_NAME'])
370 parts.insert(0, req.env['SCRIPT_NAME'])
371 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
371 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
372
372
373 # show either a directory entry or a repository
373 # show either a directory entry or a repository
374 if directory:
374 if directory:
375 # get the directory's time information
375 # get the directory's time information
376 try:
376 try:
377 d = (get_mtime(path), util.makedate()[1])
377 d = (get_mtime(path), util.makedate()[1])
378 except OSError:
378 except OSError:
379 continue
379 continue
380
380
381 # add '/' to the name to make it obvious that
381 # add '/' to the name to make it obvious that
382 # the entry is a directory, not a regular repository
382 # the entry is a directory, not a regular repository
383 row = {'contact': "",
383 row = {'contact': "",
384 'contact_sort': "",
384 'contact_sort': "",
385 'name': name + '/',
385 'name': name + '/',
386 'name_sort': name,
386 'name_sort': name,
387 'url': url,
387 'url': url,
388 'description': "",
388 'description': "",
389 'description_sort': "",
389 'description_sort': "",
390 'lastchange': d,
390 'lastchange': d,
391 'lastchange_sort': d[1]-d[0],
391 'lastchange_sort': d[1]-d[0],
392 'archives': [],
392 'archives': [],
393 'isdirectory': True,
393 'isdirectory': True,
394 'labels': [],
394 'labels': [],
395 }
395 }
396
396
397 seendirs.add(name)
397 seendirs.add(name)
398 yield row
398 yield row
399 continue
399 continue
400
400
401 u = self.ui.copy()
401 u = self.ui.copy()
402 try:
402 try:
403 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
403 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
404 except Exception as e:
404 except Exception as e:
405 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
405 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
406 continue
406 continue
407 def get(section, name, default=None):
407 def get(section, name, default=uimod._unset):
408 return u.config(section, name, default, untrusted=True)
408 return u.config(section, name, default, untrusted=True)
409
409
410 if u.configbool("web", "hidden", untrusted=True):
410 if u.configbool("web", "hidden", untrusted=True):
411 continue
411 continue
412
412
413 if not self.read_allowed(u, req):
413 if not self.read_allowed(u, req):
414 continue
414 continue
415
415
416 # update time with local timezone
416 # update time with local timezone
417 try:
417 try:
418 r = hg.repository(self.ui, path)
418 r = hg.repository(self.ui, path)
419 except IOError:
419 except IOError:
420 u.warn(_('error accessing repository at %s\n') % path)
420 u.warn(_('error accessing repository at %s\n') % path)
421 continue
421 continue
422 except error.RepoError:
422 except error.RepoError:
423 u.warn(_('error accessing repository at %s\n') % path)
423 u.warn(_('error accessing repository at %s\n') % path)
424 continue
424 continue
425 try:
425 try:
426 d = (get_mtime(r.spath), util.makedate()[1])
426 d = (get_mtime(r.spath), util.makedate()[1])
427 except OSError:
427 except OSError:
428 continue
428 continue
429
429
430 contact = get_contact(get)
430 contact = get_contact(get)
431 description = get("web", "description", "")
431 description = get("web", "description", "")
432 seenrepos.add(name)
432 seenrepos.add(name)
433 name = get("web", "name", name)
433 name = get("web", "name", name)
434 row = {'contact': contact or "unknown",
434 row = {'contact': contact or "unknown",
435 'contact_sort': contact.upper() or "unknown",
435 'contact_sort': contact.upper() or "unknown",
436 'name': name,
436 'name': name,
437 'name_sort': name,
437 'name_sort': name,
438 'url': url,
438 'url': url,
439 'description': description or "unknown",
439 'description': description or "unknown",
440 'description_sort': description.upper() or "unknown",
440 'description_sort': description.upper() or "unknown",
441 'lastchange': d,
441 'lastchange': d,
442 'lastchange_sort': d[1]-d[0],
442 'lastchange_sort': d[1]-d[0],
443 'archives': archivelist(u, "tip", url),
443 'archives': archivelist(u, "tip", url),
444 'isdirectory': None,
444 'isdirectory': None,
445 'labels': u.configlist('web', 'labels', untrusted=True),
445 'labels': u.configlist('web', 'labels', untrusted=True),
446 }
446 }
447
447
448 yield row
448 yield row
449
449
450 sortdefault = None, False
450 sortdefault = None, False
451 def entries(sortcolumn="", descending=False, subdir="", **map):
451 def entries(sortcolumn="", descending=False, subdir="", **map):
452 rows = rawentries(subdir=subdir, **map)
452 rows = rawentries(subdir=subdir, **map)
453
453
454 if sortcolumn and sortdefault != (sortcolumn, descending):
454 if sortcolumn and sortdefault != (sortcolumn, descending):
455 sortkey = '%s_sort' % sortcolumn
455 sortkey = '%s_sort' % sortcolumn
456 rows = sorted(rows, key=lambda x: x[sortkey],
456 rows = sorted(rows, key=lambda x: x[sortkey],
457 reverse=descending)
457 reverse=descending)
458 for row, parity in zip(rows, paritygen(self.stripecount)):
458 for row, parity in zip(rows, paritygen(self.stripecount)):
459 row['parity'] = parity
459 row['parity'] = parity
460 yield row
460 yield row
461
461
462 self.refresh()
462 self.refresh()
463 sortable = ["name", "description", "contact", "lastchange"]
463 sortable = ["name", "description", "contact", "lastchange"]
464 sortcolumn, descending = sortdefault
464 sortcolumn, descending = sortdefault
465 if 'sort' in req.form:
465 if 'sort' in req.form:
466 sortcolumn = req.form['sort'][0]
466 sortcolumn = req.form['sort'][0]
467 descending = sortcolumn.startswith('-')
467 descending = sortcolumn.startswith('-')
468 if descending:
468 if descending:
469 sortcolumn = sortcolumn[1:]
469 sortcolumn = sortcolumn[1:]
470 if sortcolumn not in sortable:
470 if sortcolumn not in sortable:
471 sortcolumn = ""
471 sortcolumn = ""
472
472
473 sort = [("sort_%s" % column,
473 sort = [("sort_%s" % column,
474 "%s%s" % ((not descending and column == sortcolumn)
474 "%s%s" % ((not descending and column == sortcolumn)
475 and "-" or "", column))
475 and "-" or "", column))
476 for column in sortable]
476 for column in sortable]
477
477
478 self.refresh()
478 self.refresh()
479 self.updatereqenv(req.env)
479 self.updatereqenv(req.env)
480
480
481 return tmpl("index", entries=entries, subdir=subdir,
481 return tmpl("index", entries=entries, subdir=subdir,
482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
483 sortcolumn=sortcolumn, descending=descending,
483 sortcolumn=sortcolumn, descending=descending,
484 **dict(sort))
484 **dict(sort))
485
485
486 def templater(self, req, nonce):
486 def templater(self, req, nonce):
487
487
488 def motd(**map):
488 def motd(**map):
489 if self.motd is not None:
489 if self.motd is not None:
490 yield self.motd
490 yield self.motd
491 else:
491 else:
492 yield config('web', 'motd', '')
492 yield config('web', 'motd', '')
493
493
494 def config(section, name, default=None, untrusted=True):
494 def config(section, name, default=None, untrusted=True):
495 return self.ui.config(section, name, default, untrusted)
495 return self.ui.config(section, name, default, untrusted)
496
496
497 self.updatereqenv(req.env)
497 self.updatereqenv(req.env)
498
498
499 url = req.env.get('SCRIPT_NAME', '')
499 url = req.env.get('SCRIPT_NAME', '')
500 if not url.endswith('/'):
500 if not url.endswith('/'):
501 url += '/'
501 url += '/'
502
502
503 vars = {}
503 vars = {}
504 styles = (
504 styles = (
505 req.form.get('style', [None])[0],
505 req.form.get('style', [None])[0],
506 config('web', 'style'),
506 config('web', 'style'),
507 'paper'
507 'paper'
508 )
508 )
509 style, mapfile = templater.stylemap(styles, self.templatepath)
509 style, mapfile = templater.stylemap(styles, self.templatepath)
510 if style == styles[0]:
510 if style == styles[0]:
511 vars['style'] = style
511 vars['style'] = style
512
512
513 start = url[-1] == '?' and '&' or '?'
513 start = url[-1] == '?' and '&' or '?'
514 sessionvars = webutil.sessionvars(vars, start)
514 sessionvars = webutil.sessionvars(vars, start)
515 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
515 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
516 logoimg = config('web', 'logoimg', 'hglogo.png')
516 logoimg = config('web', 'logoimg', 'hglogo.png')
517 staticurl = config('web', 'staticurl') or url + 'static/'
517 staticurl = config('web', 'staticurl') or url + 'static/'
518 if not staticurl.endswith('/'):
518 if not staticurl.endswith('/'):
519 staticurl += '/'
519 staticurl += '/'
520
520
521 defaults = {
521 defaults = {
522 "encoding": encoding.encoding,
522 "encoding": encoding.encoding,
523 "motd": motd,
523 "motd": motd,
524 "url": url,
524 "url": url,
525 "logourl": logourl,
525 "logourl": logourl,
526 "logoimg": logoimg,
526 "logoimg": logoimg,
527 "staticurl": staticurl,
527 "staticurl": staticurl,
528 "sessionvars": sessionvars,
528 "sessionvars": sessionvars,
529 "style": style,
529 "style": style,
530 "nonce": nonce,
530 "nonce": nonce,
531 }
531 }
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
533 return tmpl
533 return tmpl
534
534
535 def updatereqenv(self, env):
535 def updatereqenv(self, env):
536 if self._baseurl is not None:
536 if self._baseurl is not None:
537 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
537 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
538 env['SERVER_NAME'] = name
538 env['SERVER_NAME'] = name
539 env['SERVER_PORT'] = port
539 env['SERVER_PORT'] = port
540 env['SCRIPT_NAME'] = path
540 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now