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