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