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