##// END OF EJS Templates
hgweb: don't use raw string for session vars...
Gregory Szorc -
r41349:46781c1b default
parent child Browse files
Show More
@@ -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 gc
11 import gc
12 import os
12 import os
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_SERVER_ERROR,
19 HTTP_SERVER_ERROR,
20 cspvalues,
20 cspvalues,
21 get_contact,
21 get_contact,
22 get_mtime,
22 get_mtime,
23 ismember,
23 ismember,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 statusmessage,
26 statusmessage,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 configitems,
30 configitems,
31 encoding,
31 encoding,
32 error,
32 error,
33 extensions,
33 extensions,
34 hg,
34 hg,
35 profiling,
35 profiling,
36 pycompat,
36 pycompat,
37 registrar,
37 registrar,
38 scmutil,
38 scmutil,
39 templater,
39 templater,
40 templateutil,
40 templateutil,
41 ui as uimod,
41 ui as uimod,
42 util,
42 util,
43 )
43 )
44
44
45 from . import (
45 from . import (
46 hgweb_mod,
46 hgweb_mod,
47 request as requestmod,
47 request as requestmod,
48 webutil,
48 webutil,
49 wsgicgi,
49 wsgicgi,
50 )
50 )
51 from ..utils import dateutil
51 from ..utils import dateutil
52
52
53 def cleannames(items):
53 def cleannames(items):
54 return [(util.pconvert(name).strip('/'), path) for name, path in items]
54 return [(util.pconvert(name).strip('/'), path) for name, path in items]
55
55
56 def findrepos(paths):
56 def findrepos(paths):
57 repos = []
57 repos = []
58 for prefix, root in cleannames(paths):
58 for prefix, root in cleannames(paths):
59 roothead, roottail = os.path.split(root)
59 roothead, roottail = os.path.split(root)
60 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
60 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
61 # /bar/ be served as as foo/N .
61 # /bar/ be served as as foo/N .
62 # '*' will not search inside dirs with .hg (except .hg/patches),
62 # '*' will not search inside dirs with .hg (except .hg/patches),
63 # '**' will search inside dirs with .hg (and thus also find subrepos).
63 # '**' will search inside dirs with .hg (and thus also find subrepos).
64 try:
64 try:
65 recurse = {'*': False, '**': True}[roottail]
65 recurse = {'*': False, '**': True}[roottail]
66 except KeyError:
66 except KeyError:
67 repos.append((prefix, root))
67 repos.append((prefix, root))
68 continue
68 continue
69 roothead = os.path.normpath(os.path.abspath(roothead))
69 roothead = os.path.normpath(os.path.abspath(roothead))
70 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
70 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
71 repos.extend(urlrepos(prefix, roothead, paths))
71 repos.extend(urlrepos(prefix, roothead, paths))
72 return repos
72 return repos
73
73
74 def urlrepos(prefix, roothead, paths):
74 def urlrepos(prefix, roothead, paths):
75 """yield url paths and filesystem paths from a list of repo paths
75 """yield url paths and filesystem paths from a list of repo paths
76
76
77 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
77 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
78 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
79 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
80 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
80 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
81 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
81 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
82 """
82 """
83 for path in paths:
83 for path in paths:
84 path = os.path.normpath(path)
84 path = os.path.normpath(path)
85 yield (prefix + '/' +
85 yield (prefix + '/' +
86 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
86 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
87
87
88 def readallowed(ui, req):
88 def readallowed(ui, req):
89 """Check allow_read and deny_read config options of a repo's ui object
89 """Check allow_read and deny_read config options of a repo's ui object
90 to determine user permissions. By default, with neither option set (or
90 to determine user permissions. By default, with neither option set (or
91 both empty), allow all users to read the repo. There are two ways a
91 both empty), allow all users to read the repo. There are two ways a
92 user can be denied read access: (1) deny_read is not empty, and the
92 user can be denied read access: (1) deny_read is not empty, and the
93 user is unauthenticated or deny_read contains user (or *), and (2)
93 user is unauthenticated or deny_read contains user (or *), and (2)
94 allow_read is not empty and the user is not in allow_read. Return True
94 allow_read is not empty and the user is not in allow_read. Return True
95 if user is allowed to read the repo, else return False."""
95 if user is allowed to read the repo, else return False."""
96
96
97 user = req.remoteuser
97 user = req.remoteuser
98
98
99 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
99 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
100 if deny_read and (not user or ismember(ui, user, deny_read)):
100 if deny_read and (not user or ismember(ui, user, deny_read)):
101 return False
101 return False
102
102
103 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
103 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
104 # by default, allow reading if no allow_read option has been set
104 # by default, allow reading if no allow_read option has been set
105 if not allow_read or ismember(ui, user, allow_read):
105 if not allow_read or ismember(ui, user, allow_read):
106 return True
106 return True
107
107
108 return False
108 return False
109
109
110 def rawindexentries(ui, repos, req, subdir=''):
110 def rawindexentries(ui, repos, req, subdir=''):
111 descend = ui.configbool('web', 'descend')
111 descend = ui.configbool('web', 'descend')
112 collapse = ui.configbool('web', 'collapse')
112 collapse = ui.configbool('web', 'collapse')
113 seenrepos = set()
113 seenrepos = set()
114 seendirs = set()
114 seendirs = set()
115 for name, path in repos:
115 for name, path in repos:
116
116
117 if not name.startswith(subdir):
117 if not name.startswith(subdir):
118 continue
118 continue
119 name = name[len(subdir):]
119 name = name[len(subdir):]
120 directory = False
120 directory = False
121
121
122 if '/' in name:
122 if '/' in name:
123 if not descend:
123 if not descend:
124 continue
124 continue
125
125
126 nameparts = name.split('/')
126 nameparts = name.split('/')
127 rootname = nameparts[0]
127 rootname = nameparts[0]
128
128
129 if not collapse:
129 if not collapse:
130 pass
130 pass
131 elif rootname in seendirs:
131 elif rootname in seendirs:
132 continue
132 continue
133 elif rootname in seenrepos:
133 elif rootname in seenrepos:
134 pass
134 pass
135 else:
135 else:
136 directory = True
136 directory = True
137 name = rootname
137 name = rootname
138
138
139 # redefine the path to refer to the directory
139 # redefine the path to refer to the directory
140 discarded = '/'.join(nameparts[1:])
140 discarded = '/'.join(nameparts[1:])
141
141
142 # remove name parts plus accompanying slash
142 # remove name parts plus accompanying slash
143 path = path[:-len(discarded) - 1]
143 path = path[:-len(discarded) - 1]
144
144
145 try:
145 try:
146 r = hg.repository(ui, path)
146 r = hg.repository(ui, path)
147 directory = False
147 directory = False
148 except (IOError, error.RepoError):
148 except (IOError, error.RepoError):
149 pass
149 pass
150
150
151 parts = [
151 parts = [
152 req.apppath.strip('/'),
152 req.apppath.strip('/'),
153 subdir.strip('/'),
153 subdir.strip('/'),
154 name.strip('/'),
154 name.strip('/'),
155 ]
155 ]
156 url = '/' + '/'.join(p for p in parts if p) + '/'
156 url = '/' + '/'.join(p for p in parts if p) + '/'
157
157
158 # show either a directory entry or a repository
158 # show either a directory entry or a repository
159 if directory:
159 if directory:
160 # get the directory's time information
160 # get the directory's time information
161 try:
161 try:
162 d = (get_mtime(path), dateutil.makedate()[1])
162 d = (get_mtime(path), dateutil.makedate()[1])
163 except OSError:
163 except OSError:
164 continue
164 continue
165
165
166 # add '/' to the name to make it obvious that
166 # add '/' to the name to make it obvious that
167 # the entry is a directory, not a regular repository
167 # the entry is a directory, not a regular repository
168 row = {'contact': "",
168 row = {'contact': "",
169 'contact_sort': "",
169 'contact_sort': "",
170 'name': name + '/',
170 'name': name + '/',
171 'name_sort': name,
171 'name_sort': name,
172 'url': url,
172 'url': url,
173 'description': "",
173 'description': "",
174 'description_sort': "",
174 'description_sort': "",
175 'lastchange': d,
175 'lastchange': d,
176 'lastchange_sort': d[1] - d[0],
176 'lastchange_sort': d[1] - d[0],
177 'archives': templateutil.mappinglist([]),
177 'archives': templateutil.mappinglist([]),
178 'isdirectory': True,
178 'isdirectory': True,
179 'labels': templateutil.hybridlist([], name='label'),
179 'labels': templateutil.hybridlist([], name='label'),
180 }
180 }
181
181
182 seendirs.add(name)
182 seendirs.add(name)
183 yield row
183 yield row
184 continue
184 continue
185
185
186 u = ui.copy()
186 u = ui.copy()
187 try:
187 try:
188 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
188 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
189 except Exception as e:
189 except Exception as e:
190 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
190 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
191 continue
191 continue
192
192
193 def get(section, name, default=uimod._unset):
193 def get(section, name, default=uimod._unset):
194 return u.config(section, name, default, untrusted=True)
194 return u.config(section, name, default, untrusted=True)
195
195
196 if u.configbool("web", "hidden", untrusted=True):
196 if u.configbool("web", "hidden", untrusted=True):
197 continue
197 continue
198
198
199 if not readallowed(u, req):
199 if not readallowed(u, req):
200 continue
200 continue
201
201
202 # update time with local timezone
202 # update time with local timezone
203 try:
203 try:
204 r = hg.repository(ui, path)
204 r = hg.repository(ui, path)
205 except IOError:
205 except IOError:
206 u.warn(_('error accessing repository at %s\n') % path)
206 u.warn(_('error accessing repository at %s\n') % path)
207 continue
207 continue
208 except error.RepoError:
208 except error.RepoError:
209 u.warn(_('error accessing repository at %s\n') % path)
209 u.warn(_('error accessing repository at %s\n') % path)
210 continue
210 continue
211 try:
211 try:
212 d = (get_mtime(r.spath), dateutil.makedate()[1])
212 d = (get_mtime(r.spath), dateutil.makedate()[1])
213 except OSError:
213 except OSError:
214 continue
214 continue
215
215
216 contact = get_contact(get)
216 contact = get_contact(get)
217 description = get("web", "description")
217 description = get("web", "description")
218 seenrepos.add(name)
218 seenrepos.add(name)
219 name = get("web", "name", name)
219 name = get("web", "name", name)
220 labels = u.configlist('web', 'labels', untrusted=True)
220 labels = u.configlist('web', 'labels', untrusted=True)
221 row = {'contact': contact or "unknown",
221 row = {'contact': contact or "unknown",
222 'contact_sort': contact.upper() or "unknown",
222 'contact_sort': contact.upper() or "unknown",
223 'name': name,
223 'name': name,
224 'name_sort': name,
224 'name_sort': name,
225 'url': url,
225 'url': url,
226 'description': description or "unknown",
226 'description': description or "unknown",
227 'description_sort': description.upper() or "unknown",
227 'description_sort': description.upper() or "unknown",
228 'lastchange': d,
228 'lastchange': d,
229 'lastchange_sort': d[1] - d[0],
229 'lastchange_sort': d[1] - d[0],
230 'archives': webutil.archivelist(u, "tip", url),
230 'archives': webutil.archivelist(u, "tip", url),
231 'isdirectory': None,
231 'isdirectory': None,
232 'labels': templateutil.hybridlist(labels, name='label'),
232 'labels': templateutil.hybridlist(labels, name='label'),
233 }
233 }
234
234
235 yield row
235 yield row
236
236
237 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
237 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
238 descending, subdir):
238 descending, subdir):
239 rows = rawindexentries(ui, repos, req, subdir=subdir)
239 rows = rawindexentries(ui, repos, req, subdir=subdir)
240
240
241 sortdefault = None, False
241 sortdefault = None, False
242
242
243 if sortcolumn and sortdefault != (sortcolumn, descending):
243 if sortcolumn and sortdefault != (sortcolumn, descending):
244 sortkey = '%s_sort' % sortcolumn
244 sortkey = '%s_sort' % sortcolumn
245 rows = sorted(rows, key=lambda x: x[sortkey],
245 rows = sorted(rows, key=lambda x: x[sortkey],
246 reverse=descending)
246 reverse=descending)
247
247
248 for row, parity in zip(rows, paritygen(stripecount)):
248 for row, parity in zip(rows, paritygen(stripecount)):
249 row['parity'] = parity
249 row['parity'] = parity
250 yield row
250 yield row
251
251
252 def indexentries(ui, repos, req, stripecount, sortcolumn='',
252 def indexentries(ui, repos, req, stripecount, sortcolumn='',
253 descending=False, subdir=''):
253 descending=False, subdir=''):
254 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
254 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
255 return templateutil.mappinggenerator(_indexentriesgen, args=args)
255 return templateutil.mappinggenerator(_indexentriesgen, args=args)
256
256
257 class hgwebdir(object):
257 class hgwebdir(object):
258 """HTTP server for multiple repositories.
258 """HTTP server for multiple repositories.
259
259
260 Given a configuration, different repositories will be served depending
260 Given a configuration, different repositories will be served depending
261 on the request path.
261 on the request path.
262
262
263 Instances are typically used as WSGI applications.
263 Instances are typically used as WSGI applications.
264 """
264 """
265 def __init__(self, conf, baseui=None):
265 def __init__(self, conf, baseui=None):
266 self.conf = conf
266 self.conf = conf
267 self.baseui = baseui
267 self.baseui = baseui
268 self.ui = None
268 self.ui = None
269 self.lastrefresh = 0
269 self.lastrefresh = 0
270 self.motd = None
270 self.motd = None
271 self.refresh()
271 self.refresh()
272 if not baseui:
272 if not baseui:
273 # set up environment for new ui
273 # set up environment for new ui
274 extensions.loadall(self.ui)
274 extensions.loadall(self.ui)
275 extensions.populateui(self.ui)
275 extensions.populateui(self.ui)
276
276
277 def refresh(self):
277 def refresh(self):
278 if self.ui:
278 if self.ui:
279 refreshinterval = self.ui.configint('web', 'refreshinterval')
279 refreshinterval = self.ui.configint('web', 'refreshinterval')
280 else:
280 else:
281 item = configitems.coreitems['web']['refreshinterval']
281 item = configitems.coreitems['web']['refreshinterval']
282 refreshinterval = item.default
282 refreshinterval = item.default
283
283
284 # refreshinterval <= 0 means to always refresh.
284 # refreshinterval <= 0 means to always refresh.
285 if (refreshinterval > 0 and
285 if (refreshinterval > 0 and
286 self.lastrefresh + refreshinterval > time.time()):
286 self.lastrefresh + refreshinterval > time.time()):
287 return
287 return
288
288
289 if self.baseui:
289 if self.baseui:
290 u = self.baseui.copy()
290 u = self.baseui.copy()
291 else:
291 else:
292 u = uimod.ui.load()
292 u = uimod.ui.load()
293 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
293 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
294 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
294 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
295 # displaying bundling progress bar while serving feels wrong and may
295 # displaying bundling progress bar while serving feels wrong and may
296 # break some wsgi implementations.
296 # break some wsgi implementations.
297 u.setconfig('progress', 'disable', 'true', 'hgweb')
297 u.setconfig('progress', 'disable', 'true', 'hgweb')
298
298
299 if not isinstance(self.conf, (dict, list, tuple)):
299 if not isinstance(self.conf, (dict, list, tuple)):
300 map = {'paths': 'hgweb-paths'}
300 map = {'paths': 'hgweb-paths'}
301 if not os.path.exists(self.conf):
301 if not os.path.exists(self.conf):
302 raise error.Abort(_('config file %s not found!') % self.conf)
302 raise error.Abort(_('config file %s not found!') % self.conf)
303 u.readconfig(self.conf, remap=map, trust=True)
303 u.readconfig(self.conf, remap=map, trust=True)
304 paths = []
304 paths = []
305 for name, ignored in u.configitems('hgweb-paths'):
305 for name, ignored in u.configitems('hgweb-paths'):
306 for path in u.configlist('hgweb-paths', name):
306 for path in u.configlist('hgweb-paths', name):
307 paths.append((name, path))
307 paths.append((name, path))
308 elif isinstance(self.conf, (list, tuple)):
308 elif isinstance(self.conf, (list, tuple)):
309 paths = self.conf
309 paths = self.conf
310 elif isinstance(self.conf, dict):
310 elif isinstance(self.conf, dict):
311 paths = self.conf.items()
311 paths = self.conf.items()
312 extensions.populateui(u)
312 extensions.populateui(u)
313
313
314 repos = findrepos(paths)
314 repos = findrepos(paths)
315 for prefix, root in u.configitems('collections'):
315 for prefix, root in u.configitems('collections'):
316 prefix = util.pconvert(prefix)
316 prefix = util.pconvert(prefix)
317 for path in scmutil.walkrepos(root, followsym=True):
317 for path in scmutil.walkrepos(root, followsym=True):
318 repo = os.path.normpath(path)
318 repo = os.path.normpath(path)
319 name = util.pconvert(repo)
319 name = util.pconvert(repo)
320 if name.startswith(prefix):
320 if name.startswith(prefix):
321 name = name[len(prefix):]
321 name = name[len(prefix):]
322 repos.append((name.lstrip('/'), repo))
322 repos.append((name.lstrip('/'), repo))
323
323
324 self.repos = repos
324 self.repos = repos
325 self.ui = u
325 self.ui = u
326 encoding.encoding = self.ui.config('web', 'encoding')
326 encoding.encoding = self.ui.config('web', 'encoding')
327 self.style = self.ui.config('web', 'style')
327 self.style = self.ui.config('web', 'style')
328 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
328 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
329 self.stripecount = self.ui.config('web', 'stripes')
329 self.stripecount = self.ui.config('web', 'stripes')
330 if self.stripecount:
330 if self.stripecount:
331 self.stripecount = int(self.stripecount)
331 self.stripecount = int(self.stripecount)
332 prefix = self.ui.config('web', 'prefix')
332 prefix = self.ui.config('web', 'prefix')
333 if prefix.startswith('/'):
333 if prefix.startswith('/'):
334 prefix = prefix[1:]
334 prefix = prefix[1:]
335 if prefix.endswith('/'):
335 if prefix.endswith('/'):
336 prefix = prefix[:-1]
336 prefix = prefix[:-1]
337 self.prefix = prefix
337 self.prefix = prefix
338 self.lastrefresh = time.time()
338 self.lastrefresh = time.time()
339
339
340 def run(self):
340 def run(self):
341 if not encoding.environ.get('GATEWAY_INTERFACE',
341 if not encoding.environ.get('GATEWAY_INTERFACE',
342 '').startswith("CGI/1."):
342 '').startswith("CGI/1."):
343 raise RuntimeError("This function is only intended to be "
343 raise RuntimeError("This function is only intended to be "
344 "called while running as a CGI script.")
344 "called while running as a CGI script.")
345 wsgicgi.launch(self)
345 wsgicgi.launch(self)
346
346
347 def __call__(self, env, respond):
347 def __call__(self, env, respond):
348 baseurl = self.ui.config('web', 'baseurl')
348 baseurl = self.ui.config('web', 'baseurl')
349 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
349 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
350 res = requestmod.wsgiresponse(req, respond)
350 res = requestmod.wsgiresponse(req, respond)
351
351
352 return self.run_wsgi(req, res)
352 return self.run_wsgi(req, res)
353
353
354 def run_wsgi(self, req, res):
354 def run_wsgi(self, req, res):
355 profile = self.ui.configbool('profiling', 'enabled')
355 profile = self.ui.configbool('profiling', 'enabled')
356 with profiling.profile(self.ui, enabled=profile):
356 with profiling.profile(self.ui, enabled=profile):
357 try:
357 try:
358 for r in self._runwsgi(req, res):
358 for r in self._runwsgi(req, res):
359 yield r
359 yield r
360 finally:
360 finally:
361 # There are known cycles in localrepository that prevent
361 # There are known cycles in localrepository that prevent
362 # those objects (and tons of held references) from being
362 # those objects (and tons of held references) from being
363 # collected through normal refcounting. We mitigate those
363 # collected through normal refcounting. We mitigate those
364 # leaks by performing an explicit GC on every request.
364 # leaks by performing an explicit GC on every request.
365 # TODO remove this once leaks are fixed.
365 # TODO remove this once leaks are fixed.
366 # TODO only run this on requests that create localrepository
366 # TODO only run this on requests that create localrepository
367 # instances instead of every request.
367 # instances instead of every request.
368 gc.collect()
368 gc.collect()
369
369
370 def _runwsgi(self, req, res):
370 def _runwsgi(self, req, res):
371 try:
371 try:
372 self.refresh()
372 self.refresh()
373
373
374 csp, nonce = cspvalues(self.ui)
374 csp, nonce = cspvalues(self.ui)
375 if csp:
375 if csp:
376 res.headers['Content-Security-Policy'] = csp
376 res.headers['Content-Security-Policy'] = csp
377
377
378 virtual = req.dispatchpath.strip('/')
378 virtual = req.dispatchpath.strip('/')
379 tmpl = self.templater(req, nonce)
379 tmpl = self.templater(req, nonce)
380 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
380 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
381
381
382 # Global defaults. These can be overridden by any handler.
382 # Global defaults. These can be overridden by any handler.
383 res.status = '200 Script output follows'
383 res.status = '200 Script output follows'
384 res.headers['Content-Type'] = ctype
384 res.headers['Content-Type'] = ctype
385
385
386 # a static file
386 # a static file
387 if virtual.startswith('static/') or 'static' in req.qsparams:
387 if virtual.startswith('static/') or 'static' in req.qsparams:
388 if virtual.startswith('static/'):
388 if virtual.startswith('static/'):
389 fname = virtual[7:]
389 fname = virtual[7:]
390 else:
390 else:
391 fname = req.qsparams['static']
391 fname = req.qsparams['static']
392 static = self.ui.config("web", "static", untrusted=False)
392 static = self.ui.config("web", "static", untrusted=False)
393 if not static:
393 if not static:
394 tp = self.templatepath or templater.templatepaths()
394 tp = self.templatepath or templater.templatepaths()
395 if isinstance(tp, str):
395 if isinstance(tp, str):
396 tp = [tp]
396 tp = [tp]
397 static = [os.path.join(p, 'static') for p in tp]
397 static = [os.path.join(p, 'static') for p in tp]
398
398
399 staticfile(static, fname, res)
399 staticfile(static, fname, res)
400 return res.sendresponse()
400 return res.sendresponse()
401
401
402 # top-level index
402 # top-level index
403
403
404 repos = dict(self.repos)
404 repos = dict(self.repos)
405
405
406 if (not virtual or virtual == 'index') and virtual not in repos:
406 if (not virtual or virtual == 'index') and virtual not in repos:
407 return self.makeindex(req, res, tmpl)
407 return self.makeindex(req, res, tmpl)
408
408
409 # nested indexes and hgwebs
409 # nested indexes and hgwebs
410
410
411 if virtual.endswith('/index') and virtual not in repos:
411 if virtual.endswith('/index') and virtual not in repos:
412 subdir = virtual[:-len('index')]
412 subdir = virtual[:-len('index')]
413 if any(r.startswith(subdir) for r in repos):
413 if any(r.startswith(subdir) for r in repos):
414 return self.makeindex(req, res, tmpl, subdir)
414 return self.makeindex(req, res, tmpl, subdir)
415
415
416 def _virtualdirs():
416 def _virtualdirs():
417 # Check the full virtual path, each parent, and the root ('')
417 # Check the full virtual path, each parent, and the root ('')
418 if virtual != '':
418 if virtual != '':
419 yield virtual
419 yield virtual
420
420
421 for p in util.finddirs(virtual):
421 for p in util.finddirs(virtual):
422 yield p
422 yield p
423
423
424 yield ''
424 yield ''
425
425
426 for virtualrepo in _virtualdirs():
426 for virtualrepo in _virtualdirs():
427 real = repos.get(virtualrepo)
427 real = repos.get(virtualrepo)
428 if real:
428 if real:
429 # Re-parse the WSGI environment to take into account our
429 # Re-parse the WSGI environment to take into account our
430 # repository path component.
430 # repository path component.
431 uenv = req.rawenv
431 uenv = req.rawenv
432 if pycompat.ispy3:
432 if pycompat.ispy3:
433 uenv = {k.decode('latin1'): v for k, v in
433 uenv = {k.decode('latin1'): v for k, v in
434 uenv.iteritems()}
434 uenv.iteritems()}
435 req = requestmod.parserequestfromenv(
435 req = requestmod.parserequestfromenv(
436 uenv, reponame=virtualrepo,
436 uenv, reponame=virtualrepo,
437 altbaseurl=self.ui.config('web', 'baseurl'),
437 altbaseurl=self.ui.config('web', 'baseurl'),
438 # Reuse wrapped body file object otherwise state
438 # Reuse wrapped body file object otherwise state
439 # tracking can get confused.
439 # tracking can get confused.
440 bodyfh=req.bodyfh)
440 bodyfh=req.bodyfh)
441 try:
441 try:
442 # ensure caller gets private copy of ui
442 # ensure caller gets private copy of ui
443 repo = hg.repository(self.ui.copy(), real)
443 repo = hg.repository(self.ui.copy(), real)
444 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
444 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
445 except IOError as inst:
445 except IOError as inst:
446 msg = encoding.strtolocal(inst.strerror)
446 msg = encoding.strtolocal(inst.strerror)
447 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
447 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
448 except error.RepoError as inst:
448 except error.RepoError as inst:
449 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
449 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
450
450
451 # browse subdirectories
451 # browse subdirectories
452 subdir = virtual + '/'
452 subdir = virtual + '/'
453 if [r for r in repos if r.startswith(subdir)]:
453 if [r for r in repos if r.startswith(subdir)]:
454 return self.makeindex(req, res, tmpl, subdir)
454 return self.makeindex(req, res, tmpl, subdir)
455
455
456 # prefixes not found
456 # prefixes not found
457 res.status = '404 Not Found'
457 res.status = '404 Not Found'
458 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
458 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
459 return res.sendresponse()
459 return res.sendresponse()
460
460
461 except ErrorResponse as e:
461 except ErrorResponse as e:
462 res.status = statusmessage(e.code, pycompat.bytestr(e))
462 res.status = statusmessage(e.code, pycompat.bytestr(e))
463 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
463 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
464 return res.sendresponse()
464 return res.sendresponse()
465 finally:
465 finally:
466 tmpl = None
466 tmpl = None
467
467
468 def makeindex(self, req, res, tmpl, subdir=""):
468 def makeindex(self, req, res, tmpl, subdir=""):
469 self.refresh()
469 self.refresh()
470 sortable = ["name", "description", "contact", "lastchange"]
470 sortable = ["name", "description", "contact", "lastchange"]
471 sortcolumn, descending = None, False
471 sortcolumn, descending = None, False
472 if 'sort' in req.qsparams:
472 if 'sort' in req.qsparams:
473 sortcolumn = req.qsparams['sort']
473 sortcolumn = req.qsparams['sort']
474 descending = sortcolumn.startswith('-')
474 descending = sortcolumn.startswith('-')
475 if descending:
475 if descending:
476 sortcolumn = sortcolumn[1:]
476 sortcolumn = sortcolumn[1:]
477 if sortcolumn not in sortable:
477 if sortcolumn not in sortable:
478 sortcolumn = ""
478 sortcolumn = ""
479
479
480 sort = [("sort_%s" % column,
480 sort = [("sort_%s" % column,
481 "%s%s" % ((not descending and column == sortcolumn)
481 "%s%s" % ((not descending and column == sortcolumn)
482 and "-" or "", column))
482 and "-" or "", column))
483 for column in sortable]
483 for column in sortable]
484
484
485 self.refresh()
485 self.refresh()
486
486
487 entries = indexentries(self.ui, self.repos, req,
487 entries = indexentries(self.ui, self.repos, req,
488 self.stripecount, sortcolumn=sortcolumn,
488 self.stripecount, sortcolumn=sortcolumn,
489 descending=descending, subdir=subdir)
489 descending=descending, subdir=subdir)
490
490
491 mapping = {
491 mapping = {
492 'entries': entries,
492 'entries': entries,
493 'subdir': subdir,
493 'subdir': subdir,
494 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
494 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
495 'sortcolumn': sortcolumn,
495 'sortcolumn': sortcolumn,
496 'descending': descending,
496 'descending': descending,
497 }
497 }
498 mapping.update(sort)
498 mapping.update(sort)
499 res.setbodygen(tmpl.generate('index', mapping))
499 res.setbodygen(tmpl.generate('index', mapping))
500 return res.sendresponse()
500 return res.sendresponse()
501
501
502 def templater(self, req, nonce):
502 def templater(self, req, nonce):
503
503
504 def config(section, name, default=uimod._unset, untrusted=True):
504 def config(section, name, default=uimod._unset, untrusted=True):
505 return self.ui.config(section, name, default, untrusted)
505 return self.ui.config(section, name, default, untrusted)
506
506
507 vars = {}
507 vars = {}
508 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
508 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
509 self.templatepath)
509 self.templatepath)
510 if style == styles[0]:
510 if style == styles[0]:
511 vars['style'] = style
511 vars['style'] = style
512
512
513 sessionvars = webutil.sessionvars(vars, r'?')
513 sessionvars = webutil.sessionvars(vars, '?')
514 logourl = config('web', 'logourl')
514 logourl = config('web', 'logourl')
515 logoimg = config('web', 'logoimg')
515 logoimg = config('web', 'logoimg')
516 staticurl = (config('web', 'staticurl')
516 staticurl = (config('web', 'staticurl')
517 or req.apppath.rstrip('/') + '/static/')
517 or req.apppath.rstrip('/') + '/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 "url": req.apppath + '/',
523 "url": req.apppath + '/',
524 "logourl": logourl,
524 "logourl": logourl,
525 "logoimg": logoimg,
525 "logoimg": logoimg,
526 "staticurl": staticurl,
526 "staticurl": staticurl,
527 "sessionvars": sessionvars,
527 "sessionvars": sessionvars,
528 "style": style,
528 "style": style,
529 "nonce": nonce,
529 "nonce": nonce,
530 }
530 }
531 templatekeyword = registrar.templatekeyword(defaults)
531 templatekeyword = registrar.templatekeyword(defaults)
532 @templatekeyword('motd', requires=())
532 @templatekeyword('motd', requires=())
533 def motd(context, mapping):
533 def motd(context, mapping):
534 if self.motd is not None:
534 if self.motd is not None:
535 yield self.motd
535 yield self.motd
536 else:
536 else:
537 yield config('web', 'motd')
537 yield config('web', 'motd')
538
538
539 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
539 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
540 return tmpl
540 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now