##// END OF EJS Templates
hgwebdir: honor web.templates and web.static for static files (issue3734)
Matt Mackall -
r18191:e4f17956 stable
parent child Browse files
Show More
@@ -1,451 +1,457 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
10 10 from mercurial.i18n import _
11 11 from mercurial import ui, hg, scmutil, 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/*" or "foo = /bar/**" lets every repo /bar/N in or below
27 27 # /bar/ be served as as foo/N .
28 28 # '*' will not search inside dirs with .hg (except .hg/patches),
29 29 # '**' will search inside dirs with .hg (and thus also find subrepos).
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(os.path.abspath(roothead))
36 36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 37 repos.extend(urlrepos(prefix, roothead, paths))
38 38 return repos
39 39
40 40 def urlrepos(prefix, roothead, paths):
41 41 """yield url paths and filesystem paths from a list of repo paths
42 42
43 43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 48 """
49 49 for path in paths:
50 50 path = os.path.normpath(path)
51 51 yield (prefix + '/' +
52 52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53 53
54 54 def geturlcgivars(baseurl, port):
55 55 """
56 56 Extract CGI variables from baseurl
57 57
58 58 >>> geturlcgivars("http://host.org/base", "80")
59 59 ('host.org', '80', '/base')
60 60 >>> geturlcgivars("http://host.org:8000/base", "80")
61 61 ('host.org', '8000', '/base')
62 62 >>> geturlcgivars('/base', 8000)
63 63 ('', '8000', '/base')
64 64 >>> geturlcgivars("base", '8000')
65 65 ('', '8000', '/base')
66 66 >>> geturlcgivars("http://host", '8000')
67 67 ('host', '8000', '/')
68 68 >>> geturlcgivars("http://host/", '8000')
69 69 ('host', '8000', '/')
70 70 """
71 71 u = util.url(baseurl)
72 72 name = u.host or ''
73 73 if u.port:
74 74 port = u.port
75 75 path = u.path or ""
76 76 if not path.startswith('/'):
77 77 path = '/' + path
78 78
79 79 return name, str(port), path
80 80
81 81 class hgwebdir(object):
82 82 refreshinterval = 20
83 83
84 84 def __init__(self, conf, baseui=None):
85 85 self.conf = conf
86 86 self.baseui = baseui
87 87 self.lastrefresh = 0
88 88 self.motd = None
89 89 self.refresh()
90 90
91 91 def refresh(self):
92 92 if self.lastrefresh + self.refreshinterval > time.time():
93 93 return
94 94
95 95 if self.baseui:
96 96 u = self.baseui.copy()
97 97 else:
98 98 u = ui.ui()
99 99 u.setconfig('ui', 'report_untrusted', 'off')
100 100 u.setconfig('ui', 'nontty', 'true')
101 101
102 102 if not isinstance(self.conf, (dict, list, tuple)):
103 103 map = {'paths': 'hgweb-paths'}
104 104 if not os.path.exists(self.conf):
105 105 raise util.Abort(_('config file %s not found!') % self.conf)
106 106 u.readconfig(self.conf, remap=map, trust=True)
107 107 paths = []
108 108 for name, ignored in u.configitems('hgweb-paths'):
109 109 for path in u.configlist('hgweb-paths', name):
110 110 paths.append((name, path))
111 111 elif isinstance(self.conf, (list, tuple)):
112 112 paths = self.conf
113 113 elif isinstance(self.conf, dict):
114 114 paths = self.conf.items()
115 115
116 116 repos = findrepos(paths)
117 117 for prefix, root in u.configitems('collections'):
118 118 prefix = util.pconvert(prefix)
119 119 for path in scmutil.walkrepos(root, followsym=True):
120 120 repo = os.path.normpath(path)
121 121 name = util.pconvert(repo)
122 122 if name.startswith(prefix):
123 123 name = name[len(prefix):]
124 124 repos.append((name.lstrip('/'), repo))
125 125
126 126 self.repos = repos
127 127 self.ui = u
128 128 encoding.encoding = self.ui.config('web', 'encoding',
129 129 encoding.encoding)
130 130 self.style = self.ui.config('web', 'style', 'paper')
131 131 self.templatepath = self.ui.config('web', 'templates', None)
132 132 self.stripecount = self.ui.config('web', 'stripes', 1)
133 133 if self.stripecount:
134 134 self.stripecount = int(self.stripecount)
135 135 self._baseurl = self.ui.config('web', 'baseurl')
136 136 self.lastrefresh = time.time()
137 137
138 138 def run(self):
139 139 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
140 140 raise RuntimeError("This function is only intended to be "
141 141 "called while running as a CGI script.")
142 142 import mercurial.hgweb.wsgicgi as wsgicgi
143 143 wsgicgi.launch(self)
144 144
145 145 def __call__(self, env, respond):
146 146 req = wsgirequest(env, respond)
147 147 return self.run_wsgi(req)
148 148
149 149 def read_allowed(self, ui, req):
150 150 """Check allow_read and deny_read config options of a repo's ui object
151 151 to determine user permissions. By default, with neither option set (or
152 152 both empty), allow all users to read the repo. There are two ways a
153 153 user can be denied read access: (1) deny_read is not empty, and the
154 154 user is unauthenticated or deny_read contains user (or *), and (2)
155 155 allow_read is not empty and the user is not in allow_read. Return True
156 156 if user is allowed to read the repo, else return False."""
157 157
158 158 user = req.env.get('REMOTE_USER')
159 159
160 160 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
161 161 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
162 162 return False
163 163
164 164 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
165 165 # by default, allow reading if no allow_read option has been set
166 166 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
167 167 return True
168 168
169 169 return False
170 170
171 171 def run_wsgi(self, req):
172 172 try:
173 173 try:
174 174 self.refresh()
175 175
176 176 virtual = req.env.get("PATH_INFO", "").strip('/')
177 177 tmpl = self.templater(req)
178 178 ctype = tmpl('mimetype', encoding=encoding.encoding)
179 179 ctype = templater.stringify(ctype)
180 180
181 181 # a static file
182 182 if virtual.startswith('static/') or 'static' in req.form:
183 183 if virtual.startswith('static/'):
184 184 fname = virtual[7:]
185 185 else:
186 186 fname = req.form['static'][0]
187 static = templater.templatepath('static')
187 static = self.ui.config("web", "static", None,
188 untrusted=False)
189 if not static:
190 tp = self.templatepath or templater.templatepath()
191 if isinstance(tp, str):
192 tp = [tp]
193 static = [os.path.join(p, 'static') for p in tp]
188 194 return (staticfile(static, fname, req),)
189 195
190 196 # top-level index
191 197 elif not virtual:
192 198 req.respond(HTTP_OK, ctype)
193 199 return self.makeindex(req, tmpl)
194 200
195 201 # nested indexes and hgwebs
196 202
197 203 repos = dict(self.repos)
198 204 virtualrepo = virtual
199 205 while virtualrepo:
200 206 real = repos.get(virtualrepo)
201 207 if real:
202 208 req.env['REPO_NAME'] = virtualrepo
203 209 try:
204 210 repo = hg.repository(self.ui, real)
205 211 return hgweb(repo).run_wsgi(req)
206 212 except IOError, inst:
207 213 msg = inst.strerror
208 214 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
209 215 except error.RepoError, inst:
210 216 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
211 217
212 218 up = virtualrepo.rfind('/')
213 219 if up < 0:
214 220 break
215 221 virtualrepo = virtualrepo[:up]
216 222
217 223 # browse subdirectories
218 224 subdir = virtual + '/'
219 225 if [r for r in repos if r.startswith(subdir)]:
220 226 req.respond(HTTP_OK, ctype)
221 227 return self.makeindex(req, tmpl, subdir)
222 228
223 229 # prefixes not found
224 230 req.respond(HTTP_NOT_FOUND, ctype)
225 231 return tmpl("notfound", repo=virtual)
226 232
227 233 except ErrorResponse, err:
228 234 req.respond(err, ctype)
229 235 return tmpl('error', error=err.message or '')
230 236 finally:
231 237 tmpl = None
232 238
233 239 def makeindex(self, req, tmpl, subdir=""):
234 240
235 241 def archivelist(ui, nodeid, url):
236 242 allowed = ui.configlist("web", "allow_archive", untrusted=True)
237 243 archives = []
238 244 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
239 245 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
240 246 untrusted=True):
241 247 archives.append({"type" : i[0], "extension": i[1],
242 248 "node": nodeid, "url": url})
243 249 return archives
244 250
245 251 def rawentries(subdir="", **map):
246 252
247 253 descend = self.ui.configbool('web', 'descend', True)
248 254 collapse = self.ui.configbool('web', 'collapse', False)
249 255 seenrepos = set()
250 256 seendirs = set()
251 257 for name, path in self.repos:
252 258
253 259 if not name.startswith(subdir):
254 260 continue
255 261 name = name[len(subdir):]
256 262 directory = False
257 263
258 264 if '/' in name:
259 265 if not descend:
260 266 continue
261 267
262 268 nameparts = name.split('/')
263 269 rootname = nameparts[0]
264 270
265 271 if not collapse:
266 272 pass
267 273 elif rootname in seendirs:
268 274 continue
269 275 elif rootname in seenrepos:
270 276 pass
271 277 else:
272 278 directory = True
273 279 name = rootname
274 280
275 281 # redefine the path to refer to the directory
276 282 discarded = '/'.join(nameparts[1:])
277 283
278 284 # remove name parts plus accompanying slash
279 285 path = path[:-len(discarded) - 1]
280 286
281 287 parts = [name]
282 288 if 'PATH_INFO' in req.env:
283 289 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
284 290 if req.env['SCRIPT_NAME']:
285 291 parts.insert(0, req.env['SCRIPT_NAME'])
286 292 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
287 293
288 294 # show either a directory entry or a repository
289 295 if directory:
290 296 # get the directory's time information
291 297 try:
292 298 d = (get_mtime(path), util.makedate()[1])
293 299 except OSError:
294 300 continue
295 301
296 302 # add '/' to the name to make it obvious that
297 303 # the entry is a directory, not a regular repository
298 304 row = dict(contact="",
299 305 contact_sort="",
300 306 name=name + '/',
301 307 name_sort=name,
302 308 url=url,
303 309 description="",
304 310 description_sort="",
305 311 lastchange=d,
306 312 lastchange_sort=d[1]-d[0],
307 313 archives=[])
308 314
309 315 seendirs.add(name)
310 316 yield row
311 317 continue
312 318
313 319 u = self.ui.copy()
314 320 try:
315 321 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
316 322 except Exception, e:
317 323 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
318 324 continue
319 325 def get(section, name, default=None):
320 326 return u.config(section, name, default, untrusted=True)
321 327
322 328 if u.configbool("web", "hidden", untrusted=True):
323 329 continue
324 330
325 331 if not self.read_allowed(u, req):
326 332 continue
327 333
328 334 # update time with local timezone
329 335 try:
330 336 r = hg.repository(self.ui, path)
331 337 except IOError:
332 338 u.warn(_('error accessing repository at %s\n') % path)
333 339 continue
334 340 except error.RepoError:
335 341 u.warn(_('error accessing repository at %s\n') % path)
336 342 continue
337 343 try:
338 344 d = (get_mtime(r.spath), util.makedate()[1])
339 345 except OSError:
340 346 continue
341 347
342 348 contact = get_contact(get)
343 349 description = get("web", "description", "")
344 350 name = get("web", "name", name)
345 351 row = dict(contact=contact or "unknown",
346 352 contact_sort=contact.upper() or "unknown",
347 353 name=name,
348 354 name_sort=name,
349 355 url=url,
350 356 description=description or "unknown",
351 357 description_sort=description.upper() or "unknown",
352 358 lastchange=d,
353 359 lastchange_sort=d[1]-d[0],
354 360 archives=archivelist(u, "tip", url))
355 361
356 362 seenrepos.add(name)
357 363 yield row
358 364
359 365 sortdefault = None, False
360 366 def entries(sortcolumn="", descending=False, subdir="", **map):
361 367 rows = rawentries(subdir=subdir, **map)
362 368
363 369 if sortcolumn and sortdefault != (sortcolumn, descending):
364 370 sortkey = '%s_sort' % sortcolumn
365 371 rows = sorted(rows, key=lambda x: x[sortkey],
366 372 reverse=descending)
367 373 for row, parity in zip(rows, paritygen(self.stripecount)):
368 374 row['parity'] = parity
369 375 yield row
370 376
371 377 self.refresh()
372 378 sortable = ["name", "description", "contact", "lastchange"]
373 379 sortcolumn, descending = sortdefault
374 380 if 'sort' in req.form:
375 381 sortcolumn = req.form['sort'][0]
376 382 descending = sortcolumn.startswith('-')
377 383 if descending:
378 384 sortcolumn = sortcolumn[1:]
379 385 if sortcolumn not in sortable:
380 386 sortcolumn = ""
381 387
382 388 sort = [("sort_%s" % column,
383 389 "%s%s" % ((not descending and column == sortcolumn)
384 390 and "-" or "", column))
385 391 for column in sortable]
386 392
387 393 self.refresh()
388 394 self.updatereqenv(req.env)
389 395
390 396 return tmpl("index", entries=entries, subdir=subdir,
391 397 sortcolumn=sortcolumn, descending=descending,
392 398 **dict(sort))
393 399
394 400 def templater(self, req):
395 401
396 402 def header(**map):
397 403 yield tmpl('header', encoding=encoding.encoding, **map)
398 404
399 405 def footer(**map):
400 406 yield tmpl("footer", **map)
401 407
402 408 def motd(**map):
403 409 if self.motd is not None:
404 410 yield self.motd
405 411 else:
406 412 yield config('web', 'motd', '')
407 413
408 414 def config(section, name, default=None, untrusted=True):
409 415 return self.ui.config(section, name, default, untrusted)
410 416
411 417 self.updatereqenv(req.env)
412 418
413 419 url = req.env.get('SCRIPT_NAME', '')
414 420 if not url.endswith('/'):
415 421 url += '/'
416 422
417 423 vars = {}
418 424 styles = (
419 425 req.form.get('style', [None])[0],
420 426 config('web', 'style'),
421 427 'paper'
422 428 )
423 429 style, mapfile = templater.stylemap(styles, self.templatepath)
424 430 if style == styles[0]:
425 431 vars['style'] = style
426 432
427 433 start = url[-1] == '?' and '&' or '?'
428 434 sessionvars = webutil.sessionvars(vars, start)
429 435 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
430 436 logoimg = config('web', 'logoimg', 'hglogo.png')
431 437 staticurl = config('web', 'staticurl') or url + 'static/'
432 438 if not staticurl.endswith('/'):
433 439 staticurl += '/'
434 440
435 441 tmpl = templater.templater(mapfile,
436 442 defaults={"header": header,
437 443 "footer": footer,
438 444 "motd": motd,
439 445 "url": url,
440 446 "logourl": logourl,
441 447 "logoimg": logoimg,
442 448 "staticurl": staticurl,
443 449 "sessionvars": sessionvars})
444 450 return tmpl
445 451
446 452 def updatereqenv(self, env):
447 453 if self._baseurl is not None:
448 454 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
449 455 env['SERVER_NAME'] = name
450 456 env['SERVER_PORT'] = port
451 457 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now