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