##// END OF EJS Templates
hgweb: make code to join url path simpler...
Yuya Nishihara -
r10675:3c05ecff stable
parent child Browse files
Show More
@@ -1,355 +1,353 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 import os, re, time, urlparse
9 import os, re, time, urlparse
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater
11 from mercurial import ui, hg, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17 import webutil
17 import webutil
18
18
19 def cleannames(items):
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21
21
22 def findrepos(paths):
22 def findrepos(paths):
23 repos = []
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
29 # remember to use it without working dir.
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos.append((prefix, root))
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(roothead)
35 roothead = os.path.normpath(roothead)
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
39 if prefix:
40 name = prefix + '/' + name
40 name = prefix + '/' + name
41 repos.append((name, path))
41 repos.append((name, path))
42 return repos
42 return repos
43
43
44 class hgwebdir(object):
44 class hgwebdir(object):
45 refreshinterval = 20
45 refreshinterval = 20
46
46
47 def __init__(self, conf, baseui=None):
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
48 self.conf = conf
49 self.baseui = baseui
49 self.baseui = baseui
50 self.lastrefresh = 0
50 self.lastrefresh = 0
51 self.motd = None
51 self.motd = None
52 self.refresh()
52 self.refresh()
53
53
54 def refresh(self):
54 def refresh(self):
55 if self.lastrefresh + self.refreshinterval > time.time():
55 if self.lastrefresh + self.refreshinterval > time.time():
56 return
56 return
57
57
58 if self.baseui:
58 if self.baseui:
59 self.ui = self.baseui.copy()
59 self.ui = self.baseui.copy()
60 else:
60 else:
61 self.ui = ui.ui()
61 self.ui = ui.ui()
62 self.ui.setconfig('ui', 'report_untrusted', 'off')
62 self.ui.setconfig('ui', 'report_untrusted', 'off')
63 self.ui.setconfig('ui', 'interactive', 'off')
63 self.ui.setconfig('ui', 'interactive', 'off')
64
64
65 if not isinstance(self.conf, (dict, list, tuple)):
65 if not isinstance(self.conf, (dict, list, tuple)):
66 map = {'paths': 'hgweb-paths'}
66 map = {'paths': 'hgweb-paths'}
67 self.ui.readconfig(self.conf, remap=map, trust=True)
67 self.ui.readconfig(self.conf, remap=map, trust=True)
68 paths = self.ui.configitems('hgweb-paths')
68 paths = self.ui.configitems('hgweb-paths')
69 elif isinstance(self.conf, (list, tuple)):
69 elif isinstance(self.conf, (list, tuple)):
70 paths = self.conf
70 paths = self.conf
71 elif isinstance(self.conf, dict):
71 elif isinstance(self.conf, dict):
72 paths = self.conf.items()
72 paths = self.conf.items()
73
73
74 encoding.encoding = self.ui.config('web', 'encoding',
74 encoding.encoding = self.ui.config('web', 'encoding',
75 encoding.encoding)
75 encoding.encoding)
76 self.style = self.ui.config('web', 'style', 'paper')
76 self.style = self.ui.config('web', 'style', 'paper')
77 self.stripecount = self.ui.config('web', 'stripes', 1)
77 self.stripecount = self.ui.config('web', 'stripes', 1)
78 if self.stripecount:
78 if self.stripecount:
79 self.stripecount = int(self.stripecount)
79 self.stripecount = int(self.stripecount)
80 self._baseurl = self.ui.config('web', 'baseurl')
80 self._baseurl = self.ui.config('web', 'baseurl')
81
81
82 self.repos = findrepos(paths)
82 self.repos = findrepos(paths)
83 for prefix, root in self.ui.configitems('collections'):
83 for prefix, root in self.ui.configitems('collections'):
84 prefix = util.pconvert(prefix)
84 prefix = util.pconvert(prefix)
85 for path in util.walkrepos(root, followsym=True):
85 for path in util.walkrepos(root, followsym=True):
86 repo = os.path.normpath(path)
86 repo = os.path.normpath(path)
87 name = util.pconvert(repo)
87 name = util.pconvert(repo)
88 if name.startswith(prefix):
88 if name.startswith(prefix):
89 name = name[len(prefix):]
89 name = name[len(prefix):]
90 self.repos.append((name.lstrip('/'), repo))
90 self.repos.append((name.lstrip('/'), repo))
91
91
92 self.lastrefresh = time.time()
92 self.lastrefresh = time.time()
93
93
94 def run(self):
94 def run(self):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
96 raise RuntimeError("This function is only intended to be "
96 raise RuntimeError("This function is only intended to be "
97 "called while running as a CGI script.")
97 "called while running as a CGI script.")
98 import mercurial.hgweb.wsgicgi as wsgicgi
98 import mercurial.hgweb.wsgicgi as wsgicgi
99 wsgicgi.launch(self)
99 wsgicgi.launch(self)
100
100
101 def __call__(self, env, respond):
101 def __call__(self, env, respond):
102 req = wsgirequest(env, respond)
102 req = wsgirequest(env, respond)
103 return self.run_wsgi(req)
103 return self.run_wsgi(req)
104
104
105 def read_allowed(self, ui, req):
105 def read_allowed(self, ui, req):
106 """Check allow_read and deny_read config options of a repo's ui object
106 """Check allow_read and deny_read config options of a repo's ui object
107 to determine user permissions. By default, with neither option set (or
107 to determine user permissions. By default, with neither option set (or
108 both empty), allow all users to read the repo. There are two ways a
108 both empty), allow all users to read the repo. There are two ways a
109 user can be denied read access: (1) deny_read is not empty, and the
109 user can be denied read access: (1) deny_read is not empty, and the
110 user is unauthenticated or deny_read contains user (or *), and (2)
110 user is unauthenticated or deny_read contains user (or *), and (2)
111 allow_read is not empty and the user is not in allow_read. Return True
111 allow_read is not empty and the user is not in allow_read. Return True
112 if user is allowed to read the repo, else return False."""
112 if user is allowed to read the repo, else return False."""
113
113
114 user = req.env.get('REMOTE_USER')
114 user = req.env.get('REMOTE_USER')
115
115
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
118 return False
118 return False
119
119
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
121 # by default, allow reading if no allow_read option has been set
121 # by default, allow reading if no allow_read option has been set
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
123 return True
123 return True
124
124
125 return False
125 return False
126
126
127 def run_wsgi(self, req):
127 def run_wsgi(self, req):
128 try:
128 try:
129 try:
129 try:
130 self.refresh()
130 self.refresh()
131
131
132 virtual = req.env.get("PATH_INFO", "").strip('/')
132 virtual = req.env.get("PATH_INFO", "").strip('/')
133 tmpl = self.templater(req)
133 tmpl = self.templater(req)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
135 ctype = templater.stringify(ctype)
135 ctype = templater.stringify(ctype)
136
136
137 # a static file
137 # a static file
138 if virtual.startswith('static/') or 'static' in req.form:
138 if virtual.startswith('static/') or 'static' in req.form:
139 if virtual.startswith('static/'):
139 if virtual.startswith('static/'):
140 fname = virtual[7:]
140 fname = virtual[7:]
141 else:
141 else:
142 fname = req.form['static'][0]
142 fname = req.form['static'][0]
143 static = templater.templatepath('static')
143 static = templater.templatepath('static')
144 return (staticfile(static, fname, req),)
144 return (staticfile(static, fname, req),)
145
145
146 # top-level index
146 # top-level index
147 elif not virtual:
147 elif not virtual:
148 req.respond(HTTP_OK, ctype)
148 req.respond(HTTP_OK, ctype)
149 return self.makeindex(req, tmpl)
149 return self.makeindex(req, tmpl)
150
150
151 # nested indexes and hgwebs
151 # nested indexes and hgwebs
152
152
153 repos = dict(self.repos)
153 repos = dict(self.repos)
154 while virtual:
154 while virtual:
155 real = repos.get(virtual)
155 real = repos.get(virtual)
156 if real:
156 if real:
157 req.env['REPO_NAME'] = virtual
157 req.env['REPO_NAME'] = virtual
158 try:
158 try:
159 repo = hg.repository(self.ui, real)
159 repo = hg.repository(self.ui, real)
160 return hgweb(repo).run_wsgi(req)
160 return hgweb(repo).run_wsgi(req)
161 except IOError, inst:
161 except IOError, inst:
162 msg = inst.strerror
162 msg = inst.strerror
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
164 except error.RepoError, inst:
164 except error.RepoError, inst:
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
166
166
167 # browse subdirectories
167 # browse subdirectories
168 subdir = virtual + '/'
168 subdir = virtual + '/'
169 if [r for r in repos if r.startswith(subdir)]:
169 if [r for r in repos if r.startswith(subdir)]:
170 req.respond(HTTP_OK, ctype)
170 req.respond(HTTP_OK, ctype)
171 return self.makeindex(req, tmpl, subdir)
171 return self.makeindex(req, tmpl, subdir)
172
172
173 up = virtual.rfind('/')
173 up = virtual.rfind('/')
174 if up < 0:
174 if up < 0:
175 break
175 break
176 virtual = virtual[:up]
176 virtual = virtual[:up]
177
177
178 # prefixes not found
178 # prefixes not found
179 req.respond(HTTP_NOT_FOUND, ctype)
179 req.respond(HTTP_NOT_FOUND, ctype)
180 return tmpl("notfound", repo=virtual)
180 return tmpl("notfound", repo=virtual)
181
181
182 except ErrorResponse, err:
182 except ErrorResponse, err:
183 req.respond(err, ctype)
183 req.respond(err, ctype)
184 return tmpl('error', error=err.message or '')
184 return tmpl('error', error=err.message or '')
185 finally:
185 finally:
186 tmpl = None
186 tmpl = None
187
187
188 def makeindex(self, req, tmpl, subdir=""):
188 def makeindex(self, req, tmpl, subdir=""):
189
189
190 def archivelist(ui, nodeid, url):
190 def archivelist(ui, nodeid, url):
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
194 untrusted=True):
194 untrusted=True):
195 yield {"type" : i[0], "extension": i[1],
195 yield {"type" : i[0], "extension": i[1],
196 "node": nodeid, "url": url}
196 "node": nodeid, "url": url}
197
197
198 sortdefault = None, False
198 sortdefault = None, False
199 def entries(sortcolumn="", descending=False, subdir="", **map):
199 def entries(sortcolumn="", descending=False, subdir="", **map):
200
200
201 rows = []
201 rows = []
202 parity = paritygen(self.stripecount)
202 parity = paritygen(self.stripecount)
203 descend = self.ui.configbool('web', 'descend', True)
203 descend = self.ui.configbool('web', 'descend', True)
204 for name, path in self.repos:
204 for name, path in self.repos:
205
205
206 if not name.startswith(subdir):
206 if not name.startswith(subdir):
207 continue
207 continue
208 name = name[len(subdir):]
208 name = name[len(subdir):]
209 if not descend and '/' in name:
209 if not descend and '/' in name:
210 continue
210 continue
211
211
212 u = self.ui.copy()
212 u = self.ui.copy()
213 try:
213 try:
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 except Exception, e:
215 except Exception, e:
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 continue
217 continue
218 def get(section, name, default=None):
218 def get(section, name, default=None):
219 return u.config(section, name, default, untrusted=True)
219 return u.config(section, name, default, untrusted=True)
220
220
221 if u.configbool("web", "hidden", untrusted=True):
221 if u.configbool("web", "hidden", untrusted=True):
222 continue
222 continue
223
223
224 if not self.read_allowed(u, req):
224 if not self.read_allowed(u, req):
225 continue
225 continue
226
226
227 parts = [name]
227 parts = [name]
228 if 'PATH_INFO' in req.env:
228 if 'PATH_INFO' in req.env:
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 if req.env['SCRIPT_NAME']:
230 if req.env['SCRIPT_NAME']:
231 parts.insert(0, req.env['SCRIPT_NAME'])
231 parts.insert(0, req.env['SCRIPT_NAME'])
232 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
232 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
233 # squish repeated slashes out of the path component
234 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
235
233
236 # update time with local timezone
234 # update time with local timezone
237 try:
235 try:
238 r = hg.repository(self.ui, path)
236 r = hg.repository(self.ui, path)
239 d = (get_mtime(r.spath), util.makedate()[1])
237 d = (get_mtime(r.spath), util.makedate()[1])
240 except OSError:
238 except OSError:
241 continue
239 continue
242
240
243 contact = get_contact(get)
241 contact = get_contact(get)
244 description = get("web", "description", "")
242 description = get("web", "description", "")
245 name = get("web", "name", name)
243 name = get("web", "name", name)
246 row = dict(contact=contact or "unknown",
244 row = dict(contact=contact or "unknown",
247 contact_sort=contact.upper() or "unknown",
245 contact_sort=contact.upper() or "unknown",
248 name=name,
246 name=name,
249 name_sort=name,
247 name_sort=name,
250 url=url,
248 url=url,
251 description=description or "unknown",
249 description=description or "unknown",
252 description_sort=description.upper() or "unknown",
250 description_sort=description.upper() or "unknown",
253 lastchange=d,
251 lastchange=d,
254 lastchange_sort=d[1]-d[0],
252 lastchange_sort=d[1]-d[0],
255 archives=archivelist(u, "tip", url))
253 archives=archivelist(u, "tip", url))
256 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
254 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
257 # fast path for unsorted output
255 # fast path for unsorted output
258 row['parity'] = parity.next()
256 row['parity'] = parity.next()
259 yield row
257 yield row
260 else:
258 else:
261 rows.append((row["%s_sort" % sortcolumn], row))
259 rows.append((row["%s_sort" % sortcolumn], row))
262 if rows:
260 if rows:
263 rows.sort()
261 rows.sort()
264 if descending:
262 if descending:
265 rows.reverse()
263 rows.reverse()
266 for key, row in rows:
264 for key, row in rows:
267 row['parity'] = parity.next()
265 row['parity'] = parity.next()
268 yield row
266 yield row
269
267
270 self.refresh()
268 self.refresh()
271 sortable = ["name", "description", "contact", "lastchange"]
269 sortable = ["name", "description", "contact", "lastchange"]
272 sortcolumn, descending = sortdefault
270 sortcolumn, descending = sortdefault
273 if 'sort' in req.form:
271 if 'sort' in req.form:
274 sortcolumn = req.form['sort'][0]
272 sortcolumn = req.form['sort'][0]
275 descending = sortcolumn.startswith('-')
273 descending = sortcolumn.startswith('-')
276 if descending:
274 if descending:
277 sortcolumn = sortcolumn[1:]
275 sortcolumn = sortcolumn[1:]
278 if sortcolumn not in sortable:
276 if sortcolumn not in sortable:
279 sortcolumn = ""
277 sortcolumn = ""
280
278
281 sort = [("sort_%s" % column,
279 sort = [("sort_%s" % column,
282 "%s%s" % ((not descending and column == sortcolumn)
280 "%s%s" % ((not descending and column == sortcolumn)
283 and "-" or "", column))
281 and "-" or "", column))
284 for column in sortable]
282 for column in sortable]
285
283
286 self.refresh()
284 self.refresh()
287 self.updatereqenv(req.env)
285 self.updatereqenv(req.env)
288
286
289 return tmpl("index", entries=entries, subdir=subdir,
287 return tmpl("index", entries=entries, subdir=subdir,
290 sortcolumn=sortcolumn, descending=descending,
288 sortcolumn=sortcolumn, descending=descending,
291 **dict(sort))
289 **dict(sort))
292
290
293 def templater(self, req):
291 def templater(self, req):
294
292
295 def header(**map):
293 def header(**map):
296 yield tmpl('header', encoding=encoding.encoding, **map)
294 yield tmpl('header', encoding=encoding.encoding, **map)
297
295
298 def footer(**map):
296 def footer(**map):
299 yield tmpl("footer", **map)
297 yield tmpl("footer", **map)
300
298
301 def motd(**map):
299 def motd(**map):
302 if self.motd is not None:
300 if self.motd is not None:
303 yield self.motd
301 yield self.motd
304 else:
302 else:
305 yield config('web', 'motd', '')
303 yield config('web', 'motd', '')
306
304
307 def config(section, name, default=None, untrusted=True):
305 def config(section, name, default=None, untrusted=True):
308 return self.ui.config(section, name, default, untrusted)
306 return self.ui.config(section, name, default, untrusted)
309
307
310 self.updatereqenv(req.env)
308 self.updatereqenv(req.env)
311
309
312 url = req.env.get('SCRIPT_NAME', '')
310 url = req.env.get('SCRIPT_NAME', '')
313 if not url.endswith('/'):
311 if not url.endswith('/'):
314 url += '/'
312 url += '/'
315
313
316 vars = {}
314 vars = {}
317 styles = (
315 styles = (
318 req.form.get('style', [None])[0],
316 req.form.get('style', [None])[0],
319 config('web', 'style'),
317 config('web', 'style'),
320 'paper'
318 'paper'
321 )
319 )
322 style, mapfile = templater.stylemap(styles)
320 style, mapfile = templater.stylemap(styles)
323 if style == styles[0]:
321 if style == styles[0]:
324 vars['style'] = style
322 vars['style'] = style
325
323
326 start = url[-1] == '?' and '&' or '?'
324 start = url[-1] == '?' and '&' or '?'
327 sessionvars = webutil.sessionvars(vars, start)
325 sessionvars = webutil.sessionvars(vars, start)
328 staticurl = config('web', 'staticurl') or url + 'static/'
326 staticurl = config('web', 'staticurl') or url + 'static/'
329 if not staticurl.endswith('/'):
327 if not staticurl.endswith('/'):
330 staticurl += '/'
328 staticurl += '/'
331
329
332 tmpl = templater.templater(mapfile,
330 tmpl = templater.templater(mapfile,
333 defaults={"header": header,
331 defaults={"header": header,
334 "footer": footer,
332 "footer": footer,
335 "motd": motd,
333 "motd": motd,
336 "url": url,
334 "url": url,
337 "staticurl": staticurl,
335 "staticurl": staticurl,
338 "sessionvars": sessionvars})
336 "sessionvars": sessionvars})
339 return tmpl
337 return tmpl
340
338
341 def updatereqenv(self, env):
339 def updatereqenv(self, env):
342 def splitnetloc(netloc):
340 def splitnetloc(netloc):
343 if ':' in netloc:
341 if ':' in netloc:
344 return netloc.split(':', 1)
342 return netloc.split(':', 1)
345 else:
343 else:
346 return (netloc, None)
344 return (netloc, None)
347
345
348 if self._baseurl is not None:
346 if self._baseurl is not None:
349 urlcomp = urlparse.urlparse(self._baseurl)
347 urlcomp = urlparse.urlparse(self._baseurl)
350 host, port = splitnetloc(urlcomp[1])
348 host, port = splitnetloc(urlcomp[1])
351 path = urlcomp[2]
349 path = urlcomp[2]
352 env['SERVER_NAME'] = host
350 env['SERVER_NAME'] = host
353 if port:
351 if port:
354 env['SERVER_PORT'] = port
352 env['SERVER_PORT'] = port
355 env['SCRIPT_NAME'] = path
353 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now