##// END OF EJS Templates
hgweb: use archivespecs for links on repo index page too...
av6 -
r30749:e38e7ea2 default
parent child Browse files
Show More
@@ -1,468 +1,470
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 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 from __future__ import absolute_import
10 10
11 11 import contextlib
12 12 import os
13 13
14 14 from .common import (
15 15 ErrorResponse,
16 16 HTTP_BAD_REQUEST,
17 17 HTTP_NOT_FOUND,
18 18 HTTP_NOT_MODIFIED,
19 19 HTTP_OK,
20 20 HTTP_SERVER_ERROR,
21 21 caching,
22 22 permhooks,
23 23 )
24 24 from .request import wsgirequest
25 25
26 26 from .. import (
27 27 encoding,
28 28 error,
29 29 hg,
30 30 hook,
31 31 profiling,
32 32 repoview,
33 33 templatefilters,
34 34 templater,
35 35 ui as uimod,
36 36 util,
37 37 )
38 38
39 39 from . import (
40 40 protocol,
41 41 webcommands,
42 42 webutil,
43 43 wsgicgi,
44 44 )
45 45
46 46 perms = {
47 47 'changegroup': 'pull',
48 48 'changegroupsubset': 'pull',
49 49 'getbundle': 'pull',
50 50 'stream_out': 'pull',
51 51 'listkeys': 'pull',
52 52 'unbundle': 'push',
53 53 'pushkey': 'push',
54 54 }
55 55
56 archivespecs = util.sortdict((
57 ('zip', ('application/zip', 'zip', '.zip', None)),
58 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
59 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
60 ))
61
56 62 def makebreadcrumb(url, prefix=''):
57 63 '''Return a 'URL breadcrumb' list
58 64
59 65 A 'URL breadcrumb' is a list of URL-name pairs,
60 66 corresponding to each of the path items on a URL.
61 67 This can be used to create path navigation entries.
62 68 '''
63 69 if url.endswith('/'):
64 70 url = url[:-1]
65 71 if prefix:
66 72 url = '/' + prefix + url
67 73 relpath = url
68 74 if relpath.startswith('/'):
69 75 relpath = relpath[1:]
70 76
71 77 breadcrumb = []
72 78 urlel = url
73 79 pathitems = [''] + relpath.split('/')
74 80 for pathel in reversed(pathitems):
75 81 if not pathel or not urlel:
76 82 break
77 83 breadcrumb.append({'url': urlel, 'name': pathel})
78 84 urlel = os.path.dirname(urlel)
79 85 return reversed(breadcrumb)
80 86
81 87 class requestcontext(object):
82 88 """Holds state/context for an individual request.
83 89
84 90 Servers can be multi-threaded. Holding state on the WSGI application
85 91 is prone to race conditions. Instances of this class exist to hold
86 92 mutable and race-free state for requests.
87 93 """
88 94 def __init__(self, app, repo):
89 95 self.repo = repo
90 96 self.reponame = app.reponame
91 97
98 self.archivespecs = archivespecs
99
92 100 self.maxchanges = self.configint('web', 'maxchanges', 10)
93 101 self.stripecount = self.configint('web', 'stripes', 1)
94 102 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
95 103 self.maxfiles = self.configint('web', 'maxfiles', 10)
96 104 self.allowpull = self.configbool('web', 'allowpull', True)
97 105
98 106 # we use untrusted=False to prevent a repo owner from using
99 107 # web.templates in .hg/hgrc to get access to any file readable
100 108 # by the user running the CGI script
101 109 self.templatepath = self.config('web', 'templates', untrusted=False)
102 110
103 111 # This object is more expensive to build than simple config values.
104 112 # It is shared across requests. The app will replace the object
105 113 # if it is updated. Since this is a reference and nothing should
106 114 # modify the underlying object, it should be constant for the lifetime
107 115 # of the request.
108 116 self.websubtable = app.websubtable
109 117
110 118 # Trust the settings from the .hg/hgrc files by default.
111 119 def config(self, section, name, default=None, untrusted=True):
112 120 return self.repo.ui.config(section, name, default,
113 121 untrusted=untrusted)
114 122
115 123 def configbool(self, section, name, default=False, untrusted=True):
116 124 return self.repo.ui.configbool(section, name, default,
117 125 untrusted=untrusted)
118 126
119 127 def configint(self, section, name, default=None, untrusted=True):
120 128 return self.repo.ui.configint(section, name, default,
121 129 untrusted=untrusted)
122 130
123 131 def configlist(self, section, name, default=None, untrusted=True):
124 132 return self.repo.ui.configlist(section, name, default,
125 133 untrusted=untrusted)
126 134
127 archivespecs = util.sortdict((
128 ('zip', ('application/zip', 'zip', '.zip', None)),
129 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
130 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
131 ))
132
133 135 def archivelist(self, nodeid):
134 136 allowed = self.configlist('web', 'allow_archive')
135 137 for typ, spec in self.archivespecs.iteritems():
136 138 if typ in allowed or self.configbool('web', 'allow%s' % typ):
137 139 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
138 140
139 141 def templater(self, req):
140 142 # determine scheme, port and server name
141 143 # this is needed to create absolute urls
142 144
143 145 proto = req.env.get('wsgi.url_scheme')
144 146 if proto == 'https':
145 147 proto = 'https'
146 148 default_port = '443'
147 149 else:
148 150 proto = 'http'
149 151 default_port = '80'
150 152
151 153 port = req.env['SERVER_PORT']
152 154 port = port != default_port and (':' + port) or ''
153 155 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
154 156 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
155 157 logoimg = self.config('web', 'logoimg', 'hglogo.png')
156 158 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
157 159 if not staticurl.endswith('/'):
158 160 staticurl += '/'
159 161
160 162 # some functions for the templater
161 163
162 164 def motd(**map):
163 165 yield self.config('web', 'motd', '')
164 166
165 167 # figure out which style to use
166 168
167 169 vars = {}
168 170 styles = (
169 171 req.form.get('style', [None])[0],
170 172 self.config('web', 'style'),
171 173 'paper',
172 174 )
173 175 style, mapfile = templater.stylemap(styles, self.templatepath)
174 176 if style == styles[0]:
175 177 vars['style'] = style
176 178
177 179 start = req.url[-1] == '?' and '&' or '?'
178 180 sessionvars = webutil.sessionvars(vars, start)
179 181
180 182 if not self.reponame:
181 183 self.reponame = (self.config('web', 'name')
182 184 or req.env.get('REPO_NAME')
183 185 or req.url.strip('/') or self.repo.root)
184 186
185 187 def websubfilter(text):
186 188 return templatefilters.websub(text, self.websubtable)
187 189
188 190 # create the templater
189 191
190 192 defaults = {
191 193 'url': req.url,
192 194 'logourl': logourl,
193 195 'logoimg': logoimg,
194 196 'staticurl': staticurl,
195 197 'urlbase': urlbase,
196 198 'repo': self.reponame,
197 199 'encoding': encoding.encoding,
198 200 'motd': motd,
199 201 'sessionvars': sessionvars,
200 202 'pathdef': makebreadcrumb(req.url),
201 203 'style': style,
202 204 }
203 205 tmpl = templater.templater.frommapfile(mapfile,
204 206 filters={'websub': websubfilter},
205 207 defaults=defaults)
206 208 return tmpl
207 209
208 210
209 211 class hgweb(object):
210 212 """HTTP server for individual repositories.
211 213
212 214 Instances of this class serve HTTP responses for a particular
213 215 repository.
214 216
215 217 Instances are typically used as WSGI applications.
216 218
217 219 Some servers are multi-threaded. On these servers, there may
218 220 be multiple active threads inside __call__.
219 221 """
220 222 def __init__(self, repo, name=None, baseui=None):
221 223 if isinstance(repo, str):
222 224 if baseui:
223 225 u = baseui.copy()
224 226 else:
225 227 u = uimod.ui.load()
226 228 r = hg.repository(u, repo)
227 229 else:
228 230 # we trust caller to give us a private copy
229 231 r = repo
230 232
231 233 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
232 234 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
233 235 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
234 236 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
235 237 # resolve file patterns relative to repo root
236 238 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
237 239 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
238 240 # displaying bundling progress bar while serving feel wrong and may
239 241 # break some wsgi implementation.
240 242 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
241 243 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
242 244 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
243 245 self._lastrepo = self._repos[0]
244 246 hook.redirect(True)
245 247 self.reponame = name
246 248
247 249 def _webifyrepo(self, repo):
248 250 repo = getwebview(repo)
249 251 self.websubtable = webutil.getwebsubs(repo)
250 252 return repo
251 253
252 254 @contextlib.contextmanager
253 255 def _obtainrepo(self):
254 256 """Obtain a repo unique to the caller.
255 257
256 258 Internally we maintain a stack of cachedlocalrepo instances
257 259 to be handed out. If one is available, we pop it and return it,
258 260 ensuring it is up to date in the process. If one is not available,
259 261 we clone the most recently used repo instance and return it.
260 262
261 263 It is currently possible for the stack to grow without bounds
262 264 if the server allows infinite threads. However, servers should
263 265 have a thread limit, thus establishing our limit.
264 266 """
265 267 if self._repos:
266 268 cached = self._repos.pop()
267 269 r, created = cached.fetch()
268 270 else:
269 271 cached = self._lastrepo.copy()
270 272 r, created = cached.fetch()
271 273 if created:
272 274 r = self._webifyrepo(r)
273 275
274 276 self._lastrepo = cached
275 277 self.mtime = cached.mtime
276 278 try:
277 279 yield r
278 280 finally:
279 281 self._repos.append(cached)
280 282
281 283 def run(self):
282 284 """Start a server from CGI environment.
283 285
284 286 Modern servers should be using WSGI and should avoid this
285 287 method, if possible.
286 288 """
287 289 if not encoding.environ.get('GATEWAY_INTERFACE',
288 290 '').startswith("CGI/1."):
289 291 raise RuntimeError("This function is only intended to be "
290 292 "called while running as a CGI script.")
291 293 wsgicgi.launch(self)
292 294
293 295 def __call__(self, env, respond):
294 296 """Run the WSGI application.
295 297
296 298 This may be called by multiple threads.
297 299 """
298 300 req = wsgirequest(env, respond)
299 301 return self.run_wsgi(req)
300 302
301 303 def run_wsgi(self, req):
302 304 """Internal method to run the WSGI application.
303 305
304 306 This is typically only called by Mercurial. External consumers
305 307 should be using instances of this class as the WSGI application.
306 308 """
307 309 with self._obtainrepo() as repo:
308 310 with profiling.maybeprofile(repo.ui):
309 311 for r in self._runwsgi(req, repo):
310 312 yield r
311 313
312 314 def _runwsgi(self, req, repo):
313 315 rctx = requestcontext(self, repo)
314 316
315 317 # This state is global across all threads.
316 318 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
317 319 rctx.repo.ui.environ = req.env
318 320
319 321 # work with CGI variables to create coherent structure
320 322 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
321 323
322 324 req.url = req.env['SCRIPT_NAME']
323 325 if not req.url.endswith('/'):
324 326 req.url += '/'
325 327 if 'REPO_NAME' in req.env:
326 328 req.url += req.env['REPO_NAME'] + '/'
327 329
328 330 if 'PATH_INFO' in req.env:
329 331 parts = req.env['PATH_INFO'].strip('/').split('/')
330 332 repo_parts = req.env.get('REPO_NAME', '').split('/')
331 333 if parts[:len(repo_parts)] == repo_parts:
332 334 parts = parts[len(repo_parts):]
333 335 query = '/'.join(parts)
334 336 else:
335 337 query = req.env['QUERY_STRING'].partition('&')[0]
336 338 query = query.partition(';')[0]
337 339
338 340 # process this if it's a protocol request
339 341 # protocol bits don't need to create any URLs
340 342 # and the clients always use the old URL structure
341 343
342 344 cmd = req.form.get('cmd', [''])[0]
343 345 if protocol.iscmd(cmd):
344 346 try:
345 347 if query:
346 348 raise ErrorResponse(HTTP_NOT_FOUND)
347 349 if cmd in perms:
348 350 self.check_perm(rctx, req, perms[cmd])
349 351 return protocol.call(rctx.repo, req, cmd)
350 352 except ErrorResponse as inst:
351 353 # A client that sends unbundle without 100-continue will
352 354 # break if we respond early.
353 355 if (cmd == 'unbundle' and
354 356 (req.env.get('HTTP_EXPECT',
355 357 '').lower() != '100-continue') or
356 358 req.env.get('X-HgHttp2', '')):
357 359 req.drain()
358 360 else:
359 361 req.headers.append(('Connection', 'Close'))
360 362 req.respond(inst, protocol.HGTYPE,
361 363 body='0\n%s\n' % inst)
362 364 return ''
363 365
364 366 # translate user-visible url structure to internal structure
365 367
366 368 args = query.split('/', 2)
367 369 if 'cmd' not in req.form and args and args[0]:
368 370
369 371 cmd = args.pop(0)
370 372 style = cmd.rfind('-')
371 373 if style != -1:
372 374 req.form['style'] = [cmd[:style]]
373 375 cmd = cmd[style + 1:]
374 376
375 377 # avoid accepting e.g. style parameter as command
376 378 if util.safehasattr(webcommands, cmd):
377 379 req.form['cmd'] = [cmd]
378 380
379 381 if cmd == 'static':
380 382 req.form['file'] = ['/'.join(args)]
381 383 else:
382 384 if args and args[0]:
383 385 node = args.pop(0).replace('%2F', '/')
384 386 req.form['node'] = [node]
385 387 if args:
386 388 req.form['file'] = args
387 389
388 390 ua = req.env.get('HTTP_USER_AGENT', '')
389 391 if cmd == 'rev' and 'mercurial' in ua:
390 392 req.form['style'] = ['raw']
391 393
392 394 if cmd == 'archive':
393 395 fn = req.form['node'][0]
394 396 for type_, spec in rctx.archivespecs.iteritems():
395 397 ext = spec[2]
396 398 if fn.endswith(ext):
397 399 req.form['node'] = [fn[:-len(ext)]]
398 400 req.form['type'] = [type_]
399 401
400 402 # process the web interface request
401 403
402 404 try:
403 405 tmpl = rctx.templater(req)
404 406 ctype = tmpl('mimetype', encoding=encoding.encoding)
405 407 ctype = templater.stringify(ctype)
406 408
407 409 # check read permissions non-static content
408 410 if cmd != 'static':
409 411 self.check_perm(rctx, req, None)
410 412
411 413 if cmd == '':
412 414 req.form['cmd'] = [tmpl.cache['default']]
413 415 cmd = req.form['cmd'][0]
414 416
415 417 if rctx.configbool('web', 'cache', True):
416 418 caching(self, req) # sets ETag header or raises NOT_MODIFIED
417 419 if cmd not in webcommands.__all__:
418 420 msg = 'no such method: %s' % cmd
419 421 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
420 422 elif cmd == 'file' and 'raw' in req.form.get('style', []):
421 423 rctx.ctype = ctype
422 424 content = webcommands.rawfile(rctx, req, tmpl)
423 425 else:
424 426 content = getattr(webcommands, cmd)(rctx, req, tmpl)
425 427 req.respond(HTTP_OK, ctype)
426 428
427 429 return content
428 430
429 431 except (error.LookupError, error.RepoLookupError) as err:
430 432 req.respond(HTTP_NOT_FOUND, ctype)
431 433 msg = str(err)
432 434 if (util.safehasattr(err, 'name') and
433 435 not isinstance(err, error.ManifestLookupError)):
434 436 msg = 'revision not found: %s' % err.name
435 437 return tmpl('error', error=msg)
436 438 except (error.RepoError, error.RevlogError) as inst:
437 439 req.respond(HTTP_SERVER_ERROR, ctype)
438 440 return tmpl('error', error=str(inst))
439 441 except ErrorResponse as inst:
440 442 req.respond(inst, ctype)
441 443 if inst.code == HTTP_NOT_MODIFIED:
442 444 # Not allowed to return a body on a 304
443 445 return ['']
444 446 return tmpl('error', error=str(inst))
445 447
446 448 def check_perm(self, rctx, req, op):
447 449 for permhook in permhooks:
448 450 permhook(rctx, req, op)
449 451
450 452 def getwebview(repo):
451 453 """The 'web.view' config controls changeset filter to hgweb. Possible
452 454 values are ``served``, ``visible`` and ``all``. Default is ``served``.
453 455 The ``served`` filter only shows changesets that can be pulled from the
454 456 hgweb instance. The``visible`` filter includes secret changesets but
455 457 still excludes "hidden" one.
456 458
457 459 See the repoview module for details.
458 460
459 461 The option has been around undocumented since Mercurial 2.5, but no
460 462 user ever asked about it. So we better keep it undocumented for now."""
461 463 viewconfig = repo.ui.config('web', 'view', 'served',
462 464 untrusted=True)
463 465 if viewconfig == 'all':
464 466 return repo.unfiltered()
465 467 elif viewconfig in repoview.filtertable:
466 468 return repo.filtered(viewconfig)
467 469 else:
468 470 return repo.filtered('served')
@@ -1,522 +1,522
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 from __future__ import absolute_import
10 10
11 11 import os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 get_contact,
23 23 get_mtime,
24 24 ismember,
25 25 paritygen,
26 26 staticfile,
27 27 )
28 28 from .request import wsgirequest
29 29
30 30 from .. import (
31 31 encoding,
32 32 error,
33 33 hg,
34 34 profiling,
35 35 scmutil,
36 36 templater,
37 37 ui as uimod,
38 38 util,
39 39 )
40 40
41 41 from . import (
42 42 hgweb_mod,
43 43 webutil,
44 44 wsgicgi,
45 45 )
46 46
47 47 def cleannames(items):
48 48 return [(util.pconvert(name).strip('/'), path) for name, path in items]
49 49
50 50 def findrepos(paths):
51 51 repos = []
52 52 for prefix, root in cleannames(paths):
53 53 roothead, roottail = os.path.split(root)
54 54 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
55 55 # /bar/ be served as as foo/N .
56 56 # '*' will not search inside dirs with .hg (except .hg/patches),
57 57 # '**' will search inside dirs with .hg (and thus also find subrepos).
58 58 try:
59 59 recurse = {'*': False, '**': True}[roottail]
60 60 except KeyError:
61 61 repos.append((prefix, root))
62 62 continue
63 63 roothead = os.path.normpath(os.path.abspath(roothead))
64 64 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
65 65 repos.extend(urlrepos(prefix, roothead, paths))
66 66 return repos
67 67
68 68 def urlrepos(prefix, roothead, paths):
69 69 """yield url paths and filesystem paths from a list of repo paths
70 70
71 71 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
72 72 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
73 73 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
74 74 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
75 75 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
76 76 """
77 77 for path in paths:
78 78 path = os.path.normpath(path)
79 79 yield (prefix + '/' +
80 80 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
81 81
82 82 def geturlcgivars(baseurl, port):
83 83 """
84 84 Extract CGI variables from baseurl
85 85
86 86 >>> geturlcgivars("http://host.org/base", "80")
87 87 ('host.org', '80', '/base')
88 88 >>> geturlcgivars("http://host.org:8000/base", "80")
89 89 ('host.org', '8000', '/base')
90 90 >>> geturlcgivars('/base', 8000)
91 91 ('', '8000', '/base')
92 92 >>> geturlcgivars("base", '8000')
93 93 ('', '8000', '/base')
94 94 >>> geturlcgivars("http://host", '8000')
95 95 ('host', '8000', '/')
96 96 >>> geturlcgivars("http://host/", '8000')
97 97 ('host', '8000', '/')
98 98 """
99 99 u = util.url(baseurl)
100 100 name = u.host or ''
101 101 if u.port:
102 102 port = u.port
103 103 path = u.path or ""
104 104 if not path.startswith('/'):
105 105 path = '/' + path
106 106
107 107 return name, str(port), path
108 108
109 109 class hgwebdir(object):
110 110 """HTTP server for multiple repositories.
111 111
112 112 Given a configuration, different repositories will be served depending
113 113 on the request path.
114 114
115 115 Instances are typically used as WSGI applications.
116 116 """
117 117 def __init__(self, conf, baseui=None):
118 118 self.conf = conf
119 119 self.baseui = baseui
120 120 self.ui = None
121 121 self.lastrefresh = 0
122 122 self.motd = None
123 123 self.refresh()
124 124
125 125 def refresh(self):
126 126 refreshinterval = 20
127 127 if self.ui:
128 128 refreshinterval = self.ui.configint('web', 'refreshinterval',
129 129 refreshinterval)
130 130
131 131 # refreshinterval <= 0 means to always refresh.
132 132 if (refreshinterval > 0 and
133 133 self.lastrefresh + refreshinterval > time.time()):
134 134 return
135 135
136 136 if self.baseui:
137 137 u = self.baseui.copy()
138 138 else:
139 139 u = uimod.ui.load()
140 140 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
141 141 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
142 142 # displaying bundling progress bar while serving feels wrong and may
143 143 # break some wsgi implementations.
144 144 u.setconfig('progress', 'disable', 'true', 'hgweb')
145 145
146 146 if not isinstance(self.conf, (dict, list, tuple)):
147 147 map = {'paths': 'hgweb-paths'}
148 148 if not os.path.exists(self.conf):
149 149 raise error.Abort(_('config file %s not found!') % self.conf)
150 150 u.readconfig(self.conf, remap=map, trust=True)
151 151 paths = []
152 152 for name, ignored in u.configitems('hgweb-paths'):
153 153 for path in u.configlist('hgweb-paths', name):
154 154 paths.append((name, path))
155 155 elif isinstance(self.conf, (list, tuple)):
156 156 paths = self.conf
157 157 elif isinstance(self.conf, dict):
158 158 paths = self.conf.items()
159 159
160 160 repos = findrepos(paths)
161 161 for prefix, root in u.configitems('collections'):
162 162 prefix = util.pconvert(prefix)
163 163 for path in scmutil.walkrepos(root, followsym=True):
164 164 repo = os.path.normpath(path)
165 165 name = util.pconvert(repo)
166 166 if name.startswith(prefix):
167 167 name = name[len(prefix):]
168 168 repos.append((name.lstrip('/'), repo))
169 169
170 170 self.repos = repos
171 171 self.ui = u
172 172 encoding.encoding = self.ui.config('web', 'encoding',
173 173 encoding.encoding)
174 174 self.style = self.ui.config('web', 'style', 'paper')
175 175 self.templatepath = self.ui.config('web', 'templates', None)
176 176 self.stripecount = self.ui.config('web', 'stripes', 1)
177 177 if self.stripecount:
178 178 self.stripecount = int(self.stripecount)
179 179 self._baseurl = self.ui.config('web', 'baseurl')
180 180 prefix = self.ui.config('web', 'prefix', '')
181 181 if prefix.startswith('/'):
182 182 prefix = prefix[1:]
183 183 if prefix.endswith('/'):
184 184 prefix = prefix[:-1]
185 185 self.prefix = prefix
186 186 self.lastrefresh = time.time()
187 187
188 188 def run(self):
189 189 if not encoding.environ.get('GATEWAY_INTERFACE',
190 190 '').startswith("CGI/1."):
191 191 raise RuntimeError("This function is only intended to be "
192 192 "called while running as a CGI script.")
193 193 wsgicgi.launch(self)
194 194
195 195 def __call__(self, env, respond):
196 196 req = wsgirequest(env, respond)
197 197 return self.run_wsgi(req)
198 198
199 199 def read_allowed(self, ui, req):
200 200 """Check allow_read and deny_read config options of a repo's ui object
201 201 to determine user permissions. By default, with neither option set (or
202 202 both empty), allow all users to read the repo. There are two ways a
203 203 user can be denied read access: (1) deny_read is not empty, and the
204 204 user is unauthenticated or deny_read contains user (or *), and (2)
205 205 allow_read is not empty and the user is not in allow_read. Return True
206 206 if user is allowed to read the repo, else return False."""
207 207
208 208 user = req.env.get('REMOTE_USER')
209 209
210 210 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
211 211 if deny_read and (not user or ismember(ui, user, deny_read)):
212 212 return False
213 213
214 214 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
215 215 # by default, allow reading if no allow_read option has been set
216 216 if (not allow_read) or ismember(ui, user, allow_read):
217 217 return True
218 218
219 219 return False
220 220
221 221 def run_wsgi(self, req):
222 222 with profiling.maybeprofile(self.ui):
223 223 for r in self._runwsgi(req):
224 224 yield r
225 225
226 226 def _runwsgi(self, req):
227 227 try:
228 228 self.refresh()
229 229
230 230 virtual = req.env.get("PATH_INFO", "").strip('/')
231 231 tmpl = self.templater(req)
232 232 ctype = tmpl('mimetype', encoding=encoding.encoding)
233 233 ctype = templater.stringify(ctype)
234 234
235 235 # a static file
236 236 if virtual.startswith('static/') or 'static' in req.form:
237 237 if virtual.startswith('static/'):
238 238 fname = virtual[7:]
239 239 else:
240 240 fname = req.form['static'][0]
241 241 static = self.ui.config("web", "static", None,
242 242 untrusted=False)
243 243 if not static:
244 244 tp = self.templatepath or templater.templatepaths()
245 245 if isinstance(tp, str):
246 246 tp = [tp]
247 247 static = [os.path.join(p, 'static') for p in tp]
248 248 staticfile(static, fname, req)
249 249 return []
250 250
251 251 # top-level index
252 252 elif not virtual:
253 253 req.respond(HTTP_OK, ctype)
254 254 return self.makeindex(req, tmpl)
255 255
256 256 # nested indexes and hgwebs
257 257
258 258 repos = dict(self.repos)
259 259 virtualrepo = virtual
260 260 while virtualrepo:
261 261 real = repos.get(virtualrepo)
262 262 if real:
263 263 req.env['REPO_NAME'] = virtualrepo
264 264 try:
265 265 # ensure caller gets private copy of ui
266 266 repo = hg.repository(self.ui.copy(), real)
267 267 return hgweb_mod.hgweb(repo).run_wsgi(req)
268 268 except IOError as inst:
269 269 msg = inst.strerror
270 270 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
271 271 except error.RepoError as inst:
272 272 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
273 273
274 274 up = virtualrepo.rfind('/')
275 275 if up < 0:
276 276 break
277 277 virtualrepo = virtualrepo[:up]
278 278
279 279 # browse subdirectories
280 280 subdir = virtual + '/'
281 281 if [r for r in repos if r.startswith(subdir)]:
282 282 req.respond(HTTP_OK, ctype)
283 283 return self.makeindex(req, tmpl, subdir)
284 284
285 285 # prefixes not found
286 286 req.respond(HTTP_NOT_FOUND, ctype)
287 287 return tmpl("notfound", repo=virtual)
288 288
289 289 except ErrorResponse as err:
290 290 req.respond(err, ctype)
291 291 return tmpl('error', error=err.message or '')
292 292 finally:
293 293 tmpl = None
294 294
295 295 def makeindex(self, req, tmpl, subdir=""):
296 296
297 297 def archivelist(ui, nodeid, url):
298 298 allowed = ui.configlist("web", "allow_archive", untrusted=True)
299 299 archives = []
300 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
301 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
300 for typ, spec in hgweb_mod.archivespecs.iteritems():
301 if typ in allowed or ui.configbool("web", "allow" + typ,
302 302 untrusted=True):
303 archives.append({"type" : i[0], "extension": i[1],
303 archives.append({"type" : typ, "extension": spec[2],
304 304 "node": nodeid, "url": url})
305 305 return archives
306 306
307 307 def rawentries(subdir="", **map):
308 308
309 309 descend = self.ui.configbool('web', 'descend', True)
310 310 collapse = self.ui.configbool('web', 'collapse', False)
311 311 seenrepos = set()
312 312 seendirs = set()
313 313 for name, path in self.repos:
314 314
315 315 if not name.startswith(subdir):
316 316 continue
317 317 name = name[len(subdir):]
318 318 directory = False
319 319
320 320 if '/' in name:
321 321 if not descend:
322 322 continue
323 323
324 324 nameparts = name.split('/')
325 325 rootname = nameparts[0]
326 326
327 327 if not collapse:
328 328 pass
329 329 elif rootname in seendirs:
330 330 continue
331 331 elif rootname in seenrepos:
332 332 pass
333 333 else:
334 334 directory = True
335 335 name = rootname
336 336
337 337 # redefine the path to refer to the directory
338 338 discarded = '/'.join(nameparts[1:])
339 339
340 340 # remove name parts plus accompanying slash
341 341 path = path[:-len(discarded) - 1]
342 342
343 343 try:
344 344 r = hg.repository(self.ui, path)
345 345 directory = False
346 346 except (IOError, error.RepoError):
347 347 pass
348 348
349 349 parts = [name]
350 350 if 'PATH_INFO' in req.env:
351 351 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
352 352 if req.env['SCRIPT_NAME']:
353 353 parts.insert(0, req.env['SCRIPT_NAME'])
354 354 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
355 355
356 356 # show either a directory entry or a repository
357 357 if directory:
358 358 # get the directory's time information
359 359 try:
360 360 d = (get_mtime(path), util.makedate()[1])
361 361 except OSError:
362 362 continue
363 363
364 364 # add '/' to the name to make it obvious that
365 365 # the entry is a directory, not a regular repository
366 366 row = {'contact': "",
367 367 'contact_sort': "",
368 368 'name': name + '/',
369 369 'name_sort': name,
370 370 'url': url,
371 371 'description': "",
372 372 'description_sort': "",
373 373 'lastchange': d,
374 374 'lastchange_sort': d[1]-d[0],
375 375 'archives': [],
376 376 'isdirectory': True,
377 377 'labels': [],
378 378 }
379 379
380 380 seendirs.add(name)
381 381 yield row
382 382 continue
383 383
384 384 u = self.ui.copy()
385 385 try:
386 386 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
387 387 except Exception as e:
388 388 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
389 389 continue
390 390 def get(section, name, default=None):
391 391 return u.config(section, name, default, untrusted=True)
392 392
393 393 if u.configbool("web", "hidden", untrusted=True):
394 394 continue
395 395
396 396 if not self.read_allowed(u, req):
397 397 continue
398 398
399 399 # update time with local timezone
400 400 try:
401 401 r = hg.repository(self.ui, path)
402 402 except IOError:
403 403 u.warn(_('error accessing repository at %s\n') % path)
404 404 continue
405 405 except error.RepoError:
406 406 u.warn(_('error accessing repository at %s\n') % path)
407 407 continue
408 408 try:
409 409 d = (get_mtime(r.spath), util.makedate()[1])
410 410 except OSError:
411 411 continue
412 412
413 413 contact = get_contact(get)
414 414 description = get("web", "description", "")
415 415 seenrepos.add(name)
416 416 name = get("web", "name", name)
417 417 row = {'contact': contact or "unknown",
418 418 'contact_sort': contact.upper() or "unknown",
419 419 'name': name,
420 420 'name_sort': name,
421 421 'url': url,
422 422 'description': description or "unknown",
423 423 'description_sort': description.upper() or "unknown",
424 424 'lastchange': d,
425 425 'lastchange_sort': d[1]-d[0],
426 426 'archives': archivelist(u, "tip", url),
427 427 'isdirectory': None,
428 428 'labels': u.configlist('web', 'labels', untrusted=True),
429 429 }
430 430
431 431 yield row
432 432
433 433 sortdefault = None, False
434 434 def entries(sortcolumn="", descending=False, subdir="", **map):
435 435 rows = rawentries(subdir=subdir, **map)
436 436
437 437 if sortcolumn and sortdefault != (sortcolumn, descending):
438 438 sortkey = '%s_sort' % sortcolumn
439 439 rows = sorted(rows, key=lambda x: x[sortkey],
440 440 reverse=descending)
441 441 for row, parity in zip(rows, paritygen(self.stripecount)):
442 442 row['parity'] = parity
443 443 yield row
444 444
445 445 self.refresh()
446 446 sortable = ["name", "description", "contact", "lastchange"]
447 447 sortcolumn, descending = sortdefault
448 448 if 'sort' in req.form:
449 449 sortcolumn = req.form['sort'][0]
450 450 descending = sortcolumn.startswith('-')
451 451 if descending:
452 452 sortcolumn = sortcolumn[1:]
453 453 if sortcolumn not in sortable:
454 454 sortcolumn = ""
455 455
456 456 sort = [("sort_%s" % column,
457 457 "%s%s" % ((not descending and column == sortcolumn)
458 458 and "-" or "", column))
459 459 for column in sortable]
460 460
461 461 self.refresh()
462 462 self.updatereqenv(req.env)
463 463
464 464 return tmpl("index", entries=entries, subdir=subdir,
465 465 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
466 466 sortcolumn=sortcolumn, descending=descending,
467 467 **dict(sort))
468 468
469 469 def templater(self, req):
470 470
471 471 def motd(**map):
472 472 if self.motd is not None:
473 473 yield self.motd
474 474 else:
475 475 yield config('web', 'motd', '')
476 476
477 477 def config(section, name, default=None, untrusted=True):
478 478 return self.ui.config(section, name, default, untrusted)
479 479
480 480 self.updatereqenv(req.env)
481 481
482 482 url = req.env.get('SCRIPT_NAME', '')
483 483 if not url.endswith('/'):
484 484 url += '/'
485 485
486 486 vars = {}
487 487 styles = (
488 488 req.form.get('style', [None])[0],
489 489 config('web', 'style'),
490 490 'paper'
491 491 )
492 492 style, mapfile = templater.stylemap(styles, self.templatepath)
493 493 if style == styles[0]:
494 494 vars['style'] = style
495 495
496 496 start = url[-1] == '?' and '&' or '?'
497 497 sessionvars = webutil.sessionvars(vars, start)
498 498 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
499 499 logoimg = config('web', 'logoimg', 'hglogo.png')
500 500 staticurl = config('web', 'staticurl') or url + 'static/'
501 501 if not staticurl.endswith('/'):
502 502 staticurl += '/'
503 503
504 504 defaults = {
505 505 "encoding": encoding.encoding,
506 506 "motd": motd,
507 507 "url": url,
508 508 "logourl": logourl,
509 509 "logoimg": logoimg,
510 510 "staticurl": staticurl,
511 511 "sessionvars": sessionvars,
512 512 "style": style,
513 513 }
514 514 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
515 515 return tmpl
516 516
517 517 def updatereqenv(self, env):
518 518 if self._baseurl is not None:
519 519 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
520 520 env['SERVER_NAME'] = name
521 521 env['SERVER_PORT'] = port
522 522 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now