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