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