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