##// END OF EJS Templates
hgweb: strip trailing '/' in apppath before appending '/static/' (issue5943)...
Cédric Krier -
r38768:258d90f6 @90 stable
parent child Browse files
Show More
@@ -1,472 +1,472
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 cspvalues,
18 18 permhooks,
19 19 statusmessage,
20 20 )
21 21
22 22 from .. import (
23 23 encoding,
24 24 error,
25 25 formatter,
26 26 hg,
27 27 hook,
28 28 profiling,
29 29 pycompat,
30 30 registrar,
31 31 repoview,
32 32 templatefilters,
33 33 templater,
34 34 templateutil,
35 35 ui as uimod,
36 36 util,
37 37 wireprotoserver,
38 38 )
39 39
40 40 from . import (
41 41 request as requestmod,
42 42 webcommands,
43 43 webutil,
44 44 wsgicgi,
45 45 )
46 46
47 47 def getstyle(req, configfn, templatepath):
48 48 styles = (
49 49 req.qsparams.get('style', None),
50 50 configfn('web', 'style'),
51 51 'paper',
52 52 )
53 53 return styles, templater.stylemap(styles, templatepath)
54 54
55 55 def makebreadcrumb(url, prefix=''):
56 56 '''Return a 'URL breadcrumb' list
57 57
58 58 A 'URL breadcrumb' is a list of URL-name pairs,
59 59 corresponding to each of the path items on a URL.
60 60 This can be used to create path navigation entries.
61 61 '''
62 62 if url.endswith('/'):
63 63 url = url[:-1]
64 64 if prefix:
65 65 url = '/' + prefix + url
66 66 relpath = url
67 67 if relpath.startswith('/'):
68 68 relpath = relpath[1:]
69 69
70 70 breadcrumb = []
71 71 urlel = url
72 72 pathitems = [''] + relpath.split('/')
73 73 for pathel in reversed(pathitems):
74 74 if not pathel or not urlel:
75 75 break
76 76 breadcrumb.append({'url': urlel, 'name': pathel})
77 77 urlel = os.path.dirname(urlel)
78 78 return templateutil.mappinglist(reversed(breadcrumb))
79 79
80 80 class requestcontext(object):
81 81 """Holds state/context for an individual request.
82 82
83 83 Servers can be multi-threaded. Holding state on the WSGI application
84 84 is prone to race conditions. Instances of this class exist to hold
85 85 mutable and race-free state for requests.
86 86 """
87 87 def __init__(self, app, repo, req, res):
88 88 self.repo = repo
89 89 self.reponame = app.reponame
90 90 self.req = req
91 91 self.res = res
92 92
93 93 self.maxchanges = self.configint('web', 'maxchanges')
94 94 self.stripecount = self.configint('web', 'stripes')
95 95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
96 96 self.maxfiles = self.configint('web', 'maxfiles')
97 97 self.allowpull = self.configbool('web', 'allow-pull')
98 98
99 99 # we use untrusted=False to prevent a repo owner from using
100 100 # web.templates in .hg/hgrc to get access to any file readable
101 101 # by the user running the CGI script
102 102 self.templatepath = self.config('web', 'templates', untrusted=False)
103 103
104 104 # This object is more expensive to build than simple config values.
105 105 # It is shared across requests. The app will replace the object
106 106 # if it is updated. Since this is a reference and nothing should
107 107 # modify the underlying object, it should be constant for the lifetime
108 108 # of the request.
109 109 self.websubtable = app.websubtable
110 110
111 111 self.csp, self.nonce = cspvalues(self.repo.ui)
112 112
113 113 # Trust the settings from the .hg/hgrc files by default.
114 114 def config(self, section, name, default=uimod._unset, untrusted=True):
115 115 return self.repo.ui.config(section, name, default,
116 116 untrusted=untrusted)
117 117
118 118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
119 119 return self.repo.ui.configbool(section, name, default,
120 120 untrusted=untrusted)
121 121
122 122 def configint(self, section, name, default=uimod._unset, untrusted=True):
123 123 return self.repo.ui.configint(section, name, default,
124 124 untrusted=untrusted)
125 125
126 126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
127 127 return self.repo.ui.configlist(section, name, default,
128 128 untrusted=untrusted)
129 129
130 130 def archivelist(self, nodeid):
131 131 return webutil.archivelist(self.repo.ui, nodeid)
132 132
133 133 def templater(self, req):
134 134 # determine scheme, port and server name
135 135 # this is needed to create absolute urls
136 136 logourl = self.config('web', 'logourl')
137 137 logoimg = self.config('web', 'logoimg')
138 138 staticurl = (self.config('web', 'staticurl')
139 or req.apppath + '/static/')
139 or req.apppath.rstrip('/') + '/static/')
140 140 if not staticurl.endswith('/'):
141 141 staticurl += '/'
142 142
143 143 # some functions for the templater
144 144
145 145 def motd(**map):
146 146 yield self.config('web', 'motd')
147 147
148 148 # figure out which style to use
149 149
150 150 vars = {}
151 151 styles, (style, mapfile) = getstyle(req, self.config,
152 152 self.templatepath)
153 153 if style == styles[0]:
154 154 vars['style'] = style
155 155
156 156 sessionvars = webutil.sessionvars(vars, '?')
157 157
158 158 if not self.reponame:
159 159 self.reponame = (self.config('web', 'name', '')
160 160 or req.reponame
161 161 or req.apppath
162 162 or self.repo.root)
163 163
164 164 filters = {}
165 165 templatefilter = registrar.templatefilter(filters)
166 166 @templatefilter('websub', intype=bytes)
167 167 def websubfilter(text):
168 168 return templatefilters.websub(text, self.websubtable)
169 169
170 170 # create the templater
171 171 # TODO: export all keywords: defaults = templatekw.keywords.copy()
172 172 defaults = {
173 173 'url': req.apppath + '/',
174 174 'logourl': logourl,
175 175 'logoimg': logoimg,
176 176 'staticurl': staticurl,
177 177 'urlbase': req.advertisedbaseurl,
178 178 'repo': self.reponame,
179 179 'encoding': encoding.encoding,
180 180 'motd': motd,
181 181 'sessionvars': sessionvars,
182 182 'pathdef': makebreadcrumb(req.apppath),
183 183 'style': style,
184 184 'nonce': self.nonce,
185 185 }
186 186 tres = formatter.templateresources(self.repo.ui, self.repo)
187 187 tmpl = templater.templater.frommapfile(mapfile,
188 188 filters=filters,
189 189 defaults=defaults,
190 190 resources=tres)
191 191 return tmpl
192 192
193 193 def sendtemplate(self, name, **kwargs):
194 194 """Helper function to send a response generated from a template."""
195 195 kwargs = pycompat.byteskwargs(kwargs)
196 196 self.res.setbodygen(self.tmpl.generate(name, kwargs))
197 197 return self.res.sendresponse()
198 198
199 199 class hgweb(object):
200 200 """HTTP server for individual repositories.
201 201
202 202 Instances of this class serve HTTP responses for a particular
203 203 repository.
204 204
205 205 Instances are typically used as WSGI applications.
206 206
207 207 Some servers are multi-threaded. On these servers, there may
208 208 be multiple active threads inside __call__.
209 209 """
210 210 def __init__(self, repo, name=None, baseui=None):
211 211 if isinstance(repo, bytes):
212 212 if baseui:
213 213 u = baseui.copy()
214 214 else:
215 215 u = uimod.ui.load()
216 216 r = hg.repository(u, repo)
217 217 else:
218 218 # we trust caller to give us a private copy
219 219 r = repo
220 220
221 221 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
222 222 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
223 223 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
224 224 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
225 225 # resolve file patterns relative to repo root
226 226 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
227 227 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
228 228 # it's unlikely that we can replace signal handlers in WSGI server,
229 229 # and mod_wsgi issues a big warning. a plain hgweb process (with no
230 230 # threading) could replace signal handlers, but we don't bother
231 231 # conditionally enabling it.
232 232 r.ui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
233 233 r.baseui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
234 234 # displaying bundling progress bar while serving feel wrong and may
235 235 # break some wsgi implementation.
236 236 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
237 237 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
238 238 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
239 239 self._lastrepo = self._repos[0]
240 240 hook.redirect(True)
241 241 self.reponame = name
242 242
243 243 def _webifyrepo(self, repo):
244 244 repo = getwebview(repo)
245 245 self.websubtable = webutil.getwebsubs(repo)
246 246 return repo
247 247
248 248 @contextlib.contextmanager
249 249 def _obtainrepo(self):
250 250 """Obtain a repo unique to the caller.
251 251
252 252 Internally we maintain a stack of cachedlocalrepo instances
253 253 to be handed out. If one is available, we pop it and return it,
254 254 ensuring it is up to date in the process. If one is not available,
255 255 we clone the most recently used repo instance and return it.
256 256
257 257 It is currently possible for the stack to grow without bounds
258 258 if the server allows infinite threads. However, servers should
259 259 have a thread limit, thus establishing our limit.
260 260 """
261 261 if self._repos:
262 262 cached = self._repos.pop()
263 263 r, created = cached.fetch()
264 264 else:
265 265 cached = self._lastrepo.copy()
266 266 r, created = cached.fetch()
267 267 if created:
268 268 r = self._webifyrepo(r)
269 269
270 270 self._lastrepo = cached
271 271 self.mtime = cached.mtime
272 272 try:
273 273 yield r
274 274 finally:
275 275 self._repos.append(cached)
276 276
277 277 def run(self):
278 278 """Start a server from CGI environment.
279 279
280 280 Modern servers should be using WSGI and should avoid this
281 281 method, if possible.
282 282 """
283 283 if not encoding.environ.get('GATEWAY_INTERFACE',
284 284 '').startswith("CGI/1."):
285 285 raise RuntimeError("This function is only intended to be "
286 286 "called while running as a CGI script.")
287 287 wsgicgi.launch(self)
288 288
289 289 def __call__(self, env, respond):
290 290 """Run the WSGI application.
291 291
292 292 This may be called by multiple threads.
293 293 """
294 294 req = requestmod.parserequestfromenv(env)
295 295 res = requestmod.wsgiresponse(req, respond)
296 296
297 297 return self.run_wsgi(req, res)
298 298
299 299 def run_wsgi(self, req, res):
300 300 """Internal method to run the WSGI application.
301 301
302 302 This is typically only called by Mercurial. External consumers
303 303 should be using instances of this class as the WSGI application.
304 304 """
305 305 with self._obtainrepo() as repo:
306 306 profile = repo.ui.configbool('profiling', 'enabled')
307 307 with profiling.profile(repo.ui, enabled=profile):
308 308 for r in self._runwsgi(req, res, repo):
309 309 yield r
310 310
311 311 def _runwsgi(self, req, res, repo):
312 312 rctx = requestcontext(self, repo, req, res)
313 313
314 314 # This state is global across all threads.
315 315 encoding.encoding = rctx.config('web', 'encoding')
316 316 rctx.repo.ui.environ = req.rawenv
317 317
318 318 if rctx.csp:
319 319 # hgwebdir may have added CSP header. Since we generate our own,
320 320 # replace it.
321 321 res.headers['Content-Security-Policy'] = rctx.csp
322 322
323 323 # /api/* is reserved for various API implementations. Dispatch
324 324 # accordingly. But URL paths can conflict with subrepos and virtual
325 325 # repos in hgwebdir. So until we have a workaround for this, only
326 326 # expose the URLs if the feature is enabled.
327 327 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
328 328 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
329 329 wireprotoserver.handlewsgiapirequest(rctx, req, res,
330 330 self.check_perm)
331 331 return res.sendresponse()
332 332
333 333 handled = wireprotoserver.handlewsgirequest(
334 334 rctx, req, res, self.check_perm)
335 335 if handled:
336 336 return res.sendresponse()
337 337
338 338 # Old implementations of hgweb supported dispatching the request via
339 339 # the initial query string parameter instead of using PATH_INFO.
340 340 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
341 341 # a value), we use it. Otherwise fall back to the query string.
342 342 if req.dispatchpath is not None:
343 343 query = req.dispatchpath
344 344 else:
345 345 query = req.querystring.partition('&')[0].partition(';')[0]
346 346
347 347 # translate user-visible url structure to internal structure
348 348
349 349 args = query.split('/', 2)
350 350 if 'cmd' not in req.qsparams and args and args[0]:
351 351 cmd = args.pop(0)
352 352 style = cmd.rfind('-')
353 353 if style != -1:
354 354 req.qsparams['style'] = cmd[:style]
355 355 cmd = cmd[style + 1:]
356 356
357 357 # avoid accepting e.g. style parameter as command
358 358 if util.safehasattr(webcommands, cmd):
359 359 req.qsparams['cmd'] = cmd
360 360
361 361 if cmd == 'static':
362 362 req.qsparams['file'] = '/'.join(args)
363 363 else:
364 364 if args and args[0]:
365 365 node = args.pop(0).replace('%2F', '/')
366 366 req.qsparams['node'] = node
367 367 if args:
368 368 if 'file' in req.qsparams:
369 369 del req.qsparams['file']
370 370 for a in args:
371 371 req.qsparams.add('file', a)
372 372
373 373 ua = req.headers.get('User-Agent', '')
374 374 if cmd == 'rev' and 'mercurial' in ua:
375 375 req.qsparams['style'] = 'raw'
376 376
377 377 if cmd == 'archive':
378 378 fn = req.qsparams['node']
379 379 for type_, spec in webutil.archivespecs.iteritems():
380 380 ext = spec[2]
381 381 if fn.endswith(ext):
382 382 req.qsparams['node'] = fn[:-len(ext)]
383 383 req.qsparams['type'] = type_
384 384 else:
385 385 cmd = req.qsparams.get('cmd', '')
386 386
387 387 # process the web interface request
388 388
389 389 try:
390 390 rctx.tmpl = rctx.templater(req)
391 391 ctype = rctx.tmpl.render('mimetype',
392 392 {'encoding': encoding.encoding})
393 393
394 394 # check read permissions non-static content
395 395 if cmd != 'static':
396 396 self.check_perm(rctx, req, None)
397 397
398 398 if cmd == '':
399 399 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
400 400 cmd = req.qsparams['cmd']
401 401
402 402 # Don't enable caching if using a CSP nonce because then it wouldn't
403 403 # be a nonce.
404 404 if rctx.configbool('web', 'cache') and not rctx.nonce:
405 405 tag = 'W/"%d"' % self.mtime
406 406 if req.headers.get('If-None-Match') == tag:
407 407 res.status = '304 Not Modified'
408 408 # Content-Type may be defined globally. It isn't valid on a
409 409 # 304, so discard it.
410 410 try:
411 411 del res.headers[b'Content-Type']
412 412 except KeyError:
413 413 pass
414 414 # Response body not allowed on 304.
415 415 res.setbodybytes('')
416 416 return res.sendresponse()
417 417
418 418 res.headers['ETag'] = tag
419 419
420 420 if cmd not in webcommands.__all__:
421 421 msg = 'no such method: %s' % cmd
422 422 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
423 423 else:
424 424 # Set some globals appropriate for web handlers. Commands can
425 425 # override easily enough.
426 426 res.status = '200 Script output follows'
427 427 res.headers['Content-Type'] = ctype
428 428 return getattr(webcommands, cmd)(rctx)
429 429
430 430 except (error.LookupError, error.RepoLookupError) as err:
431 431 msg = pycompat.bytestr(err)
432 432 if (util.safehasattr(err, 'name') and
433 433 not isinstance(err, error.ManifestLookupError)):
434 434 msg = 'revision not found: %s' % err.name
435 435
436 436 res.status = '404 Not Found'
437 437 res.headers['Content-Type'] = ctype
438 438 return rctx.sendtemplate('error', error=msg)
439 439 except (error.RepoError, error.RevlogError) as e:
440 440 res.status = '500 Internal Server Error'
441 441 res.headers['Content-Type'] = ctype
442 442 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
443 443 except ErrorResponse as e:
444 444 for k, v in e.headers:
445 445 res.headers[k] = v
446 446 res.status = statusmessage(e.code, pycompat.bytestr(e))
447 447 res.headers['Content-Type'] = ctype
448 448 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
449 449
450 450 def check_perm(self, rctx, req, op):
451 451 for permhook in permhooks:
452 452 permhook(rctx, req, op)
453 453
454 454 def getwebview(repo):
455 455 """The 'web.view' config controls changeset filter to hgweb. Possible
456 456 values are ``served``, ``visible`` and ``all``. Default is ``served``.
457 457 The ``served`` filter only shows changesets that can be pulled from the
458 458 hgweb instance. The``visible`` filter includes secret changesets but
459 459 still excludes "hidden" one.
460 460
461 461 See the repoview module for details.
462 462
463 463 The option has been around undocumented since Mercurial 2.5, but no
464 464 user ever asked about it. So we better keep it undocumented for now."""
465 465 # experimental config: web.view
466 466 viewconfig = repo.ui.config('web', 'view', untrusted=True)
467 467 if viewconfig == 'all':
468 468 return repo.unfiltered()
469 469 elif viewconfig in repoview.filtertable:
470 470 return repo.filtered(viewconfig)
471 471 else:
472 472 return repo.filtered('served')
@@ -1,533 +1,533
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 gc
12 12 import os
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_SERVER_ERROR,
20 20 cspvalues,
21 21 get_contact,
22 22 get_mtime,
23 23 ismember,
24 24 paritygen,
25 25 staticfile,
26 26 statusmessage,
27 27 )
28 28
29 29 from .. import (
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 hg,
34 34 profiling,
35 35 pycompat,
36 36 scmutil,
37 37 templater,
38 38 templateutil,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 request as requestmod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49 from ..utils import dateutil
50 50
51 51 def cleannames(items):
52 52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 53
54 54 def findrepos(paths):
55 55 repos = []
56 56 for prefix, root in cleannames(paths):
57 57 roothead, roottail = os.path.split(root)
58 58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 59 # /bar/ be served as as foo/N .
60 60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 62 try:
63 63 recurse = {'*': False, '**': True}[roottail]
64 64 except KeyError:
65 65 repos.append((prefix, root))
66 66 continue
67 67 roothead = os.path.normpath(os.path.abspath(roothead))
68 68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 69 repos.extend(urlrepos(prefix, roothead, paths))
70 70 return repos
71 71
72 72 def urlrepos(prefix, roothead, paths):
73 73 """yield url paths and filesystem paths from a list of repo paths
74 74
75 75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 80 """
81 81 for path in paths:
82 82 path = os.path.normpath(path)
83 83 yield (prefix + '/' +
84 84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 85
86 86 def readallowed(ui, req):
87 87 """Check allow_read and deny_read config options of a repo's ui object
88 88 to determine user permissions. By default, with neither option set (or
89 89 both empty), allow all users to read the repo. There are two ways a
90 90 user can be denied read access: (1) deny_read is not empty, and the
91 91 user is unauthenticated or deny_read contains user (or *), and (2)
92 92 allow_read is not empty and the user is not in allow_read. Return True
93 93 if user is allowed to read the repo, else return False."""
94 94
95 95 user = req.remoteuser
96 96
97 97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 98 if deny_read and (not user or ismember(ui, user, deny_read)):
99 99 return False
100 100
101 101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 102 # by default, allow reading if no allow_read option has been set
103 103 if not allow_read or ismember(ui, user, allow_read):
104 104 return True
105 105
106 106 return False
107 107
108 108 def rawindexentries(ui, repos, req, subdir=''):
109 109 descend = ui.configbool('web', 'descend')
110 110 collapse = ui.configbool('web', 'collapse')
111 111 seenrepos = set()
112 112 seendirs = set()
113 113 for name, path in repos:
114 114
115 115 if not name.startswith(subdir):
116 116 continue
117 117 name = name[len(subdir):]
118 118 directory = False
119 119
120 120 if '/' in name:
121 121 if not descend:
122 122 continue
123 123
124 124 nameparts = name.split('/')
125 125 rootname = nameparts[0]
126 126
127 127 if not collapse:
128 128 pass
129 129 elif rootname in seendirs:
130 130 continue
131 131 elif rootname in seenrepos:
132 132 pass
133 133 else:
134 134 directory = True
135 135 name = rootname
136 136
137 137 # redefine the path to refer to the directory
138 138 discarded = '/'.join(nameparts[1:])
139 139
140 140 # remove name parts plus accompanying slash
141 141 path = path[:-len(discarded) - 1]
142 142
143 143 try:
144 144 r = hg.repository(ui, path)
145 145 directory = False
146 146 except (IOError, error.RepoError):
147 147 pass
148 148
149 149 parts = [
150 150 req.apppath.strip('/'),
151 151 subdir.strip('/'),
152 152 name.strip('/'),
153 153 ]
154 154 url = '/' + '/'.join(p for p in parts if p) + '/'
155 155
156 156 # show either a directory entry or a repository
157 157 if directory:
158 158 # get the directory's time information
159 159 try:
160 160 d = (get_mtime(path), dateutil.makedate()[1])
161 161 except OSError:
162 162 continue
163 163
164 164 # add '/' to the name to make it obvious that
165 165 # the entry is a directory, not a regular repository
166 166 row = {'contact': "",
167 167 'contact_sort': "",
168 168 'name': name + '/',
169 169 'name_sort': name,
170 170 'url': url,
171 171 'description': "",
172 172 'description_sort': "",
173 173 'lastchange': d,
174 174 'lastchange_sort': d[1] - d[0],
175 175 'archives': templateutil.mappinglist([]),
176 176 'isdirectory': True,
177 177 'labels': templateutil.hybridlist([], name='label'),
178 178 }
179 179
180 180 seendirs.add(name)
181 181 yield row
182 182 continue
183 183
184 184 u = ui.copy()
185 185 try:
186 186 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
187 187 except Exception as e:
188 188 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
189 189 continue
190 190
191 191 def get(section, name, default=uimod._unset):
192 192 return u.config(section, name, default, untrusted=True)
193 193
194 194 if u.configbool("web", "hidden", untrusted=True):
195 195 continue
196 196
197 197 if not readallowed(u, req):
198 198 continue
199 199
200 200 # update time with local timezone
201 201 try:
202 202 r = hg.repository(ui, path)
203 203 except IOError:
204 204 u.warn(_('error accessing repository at %s\n') % path)
205 205 continue
206 206 except error.RepoError:
207 207 u.warn(_('error accessing repository at %s\n') % path)
208 208 continue
209 209 try:
210 210 d = (get_mtime(r.spath), dateutil.makedate()[1])
211 211 except OSError:
212 212 continue
213 213
214 214 contact = get_contact(get)
215 215 description = get("web", "description")
216 216 seenrepos.add(name)
217 217 name = get("web", "name", name)
218 218 labels = u.configlist('web', 'labels', untrusted=True)
219 219 row = {'contact': contact or "unknown",
220 220 'contact_sort': contact.upper() or "unknown",
221 221 'name': name,
222 222 'name_sort': name,
223 223 'url': url,
224 224 'description': description or "unknown",
225 225 'description_sort': description.upper() or "unknown",
226 226 'lastchange': d,
227 227 'lastchange_sort': d[1] - d[0],
228 228 'archives': webutil.archivelist(u, "tip", url),
229 229 'isdirectory': None,
230 230 'labels': templateutil.hybridlist(labels, name='label'),
231 231 }
232 232
233 233 yield row
234 234
235 235 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
236 236 descending, subdir):
237 237 rows = rawindexentries(ui, repos, req, subdir=subdir)
238 238
239 239 sortdefault = None, False
240 240
241 241 if sortcolumn and sortdefault != (sortcolumn, descending):
242 242 sortkey = '%s_sort' % sortcolumn
243 243 rows = sorted(rows, key=lambda x: x[sortkey],
244 244 reverse=descending)
245 245
246 246 for row, parity in zip(rows, paritygen(stripecount)):
247 247 row['parity'] = parity
248 248 yield row
249 249
250 250 def indexentries(ui, repos, req, stripecount, sortcolumn='',
251 251 descending=False, subdir=''):
252 252 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
253 253 return templateutil.mappinggenerator(_indexentriesgen, args=args)
254 254
255 255 class hgwebdir(object):
256 256 """HTTP server for multiple repositories.
257 257
258 258 Given a configuration, different repositories will be served depending
259 259 on the request path.
260 260
261 261 Instances are typically used as WSGI applications.
262 262 """
263 263 def __init__(self, conf, baseui=None):
264 264 self.conf = conf
265 265 self.baseui = baseui
266 266 self.ui = None
267 267 self.lastrefresh = 0
268 268 self.motd = None
269 269 self.refresh()
270 270
271 271 def refresh(self):
272 272 if self.ui:
273 273 refreshinterval = self.ui.configint('web', 'refreshinterval')
274 274 else:
275 275 item = configitems.coreitems['web']['refreshinterval']
276 276 refreshinterval = item.default
277 277
278 278 # refreshinterval <= 0 means to always refresh.
279 279 if (refreshinterval > 0 and
280 280 self.lastrefresh + refreshinterval > time.time()):
281 281 return
282 282
283 283 if self.baseui:
284 284 u = self.baseui.copy()
285 285 else:
286 286 u = uimod.ui.load()
287 287 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
288 288 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
289 289 # displaying bundling progress bar while serving feels wrong and may
290 290 # break some wsgi implementations.
291 291 u.setconfig('progress', 'disable', 'true', 'hgweb')
292 292
293 293 if not isinstance(self.conf, (dict, list, tuple)):
294 294 map = {'paths': 'hgweb-paths'}
295 295 if not os.path.exists(self.conf):
296 296 raise error.Abort(_('config file %s not found!') % self.conf)
297 297 u.readconfig(self.conf, remap=map, trust=True)
298 298 paths = []
299 299 for name, ignored in u.configitems('hgweb-paths'):
300 300 for path in u.configlist('hgweb-paths', name):
301 301 paths.append((name, path))
302 302 elif isinstance(self.conf, (list, tuple)):
303 303 paths = self.conf
304 304 elif isinstance(self.conf, dict):
305 305 paths = self.conf.items()
306 306
307 307 repos = findrepos(paths)
308 308 for prefix, root in u.configitems('collections'):
309 309 prefix = util.pconvert(prefix)
310 310 for path in scmutil.walkrepos(root, followsym=True):
311 311 repo = os.path.normpath(path)
312 312 name = util.pconvert(repo)
313 313 if name.startswith(prefix):
314 314 name = name[len(prefix):]
315 315 repos.append((name.lstrip('/'), repo))
316 316
317 317 self.repos = repos
318 318 self.ui = u
319 319 encoding.encoding = self.ui.config('web', 'encoding')
320 320 self.style = self.ui.config('web', 'style')
321 321 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
322 322 self.stripecount = self.ui.config('web', 'stripes')
323 323 if self.stripecount:
324 324 self.stripecount = int(self.stripecount)
325 325 prefix = self.ui.config('web', 'prefix')
326 326 if prefix.startswith('/'):
327 327 prefix = prefix[1:]
328 328 if prefix.endswith('/'):
329 329 prefix = prefix[:-1]
330 330 self.prefix = prefix
331 331 self.lastrefresh = time.time()
332 332
333 333 def run(self):
334 334 if not encoding.environ.get('GATEWAY_INTERFACE',
335 335 '').startswith("CGI/1."):
336 336 raise RuntimeError("This function is only intended to be "
337 337 "called while running as a CGI script.")
338 338 wsgicgi.launch(self)
339 339
340 340 def __call__(self, env, respond):
341 341 baseurl = self.ui.config('web', 'baseurl')
342 342 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
343 343 res = requestmod.wsgiresponse(req, respond)
344 344
345 345 return self.run_wsgi(req, res)
346 346
347 347 def run_wsgi(self, req, res):
348 348 profile = self.ui.configbool('profiling', 'enabled')
349 349 with profiling.profile(self.ui, enabled=profile):
350 350 try:
351 351 for r in self._runwsgi(req, res):
352 352 yield r
353 353 finally:
354 354 # There are known cycles in localrepository that prevent
355 355 # those objects (and tons of held references) from being
356 356 # collected through normal refcounting. We mitigate those
357 357 # leaks by performing an explicit GC on every request.
358 358 # TODO remove this once leaks are fixed.
359 359 # TODO only run this on requests that create localrepository
360 360 # instances instead of every request.
361 361 gc.collect()
362 362
363 363 def _runwsgi(self, req, res):
364 364 try:
365 365 self.refresh()
366 366
367 367 csp, nonce = cspvalues(self.ui)
368 368 if csp:
369 369 res.headers['Content-Security-Policy'] = csp
370 370
371 371 virtual = req.dispatchpath.strip('/')
372 372 tmpl = self.templater(req, nonce)
373 373 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
374 374
375 375 # Global defaults. These can be overridden by any handler.
376 376 res.status = '200 Script output follows'
377 377 res.headers['Content-Type'] = ctype
378 378
379 379 # a static file
380 380 if virtual.startswith('static/') or 'static' in req.qsparams:
381 381 if virtual.startswith('static/'):
382 382 fname = virtual[7:]
383 383 else:
384 384 fname = req.qsparams['static']
385 385 static = self.ui.config("web", "static", None,
386 386 untrusted=False)
387 387 if not static:
388 388 tp = self.templatepath or templater.templatepaths()
389 389 if isinstance(tp, str):
390 390 tp = [tp]
391 391 static = [os.path.join(p, 'static') for p in tp]
392 392
393 393 staticfile(static, fname, res)
394 394 return res.sendresponse()
395 395
396 396 # top-level index
397 397
398 398 repos = dict(self.repos)
399 399
400 400 if (not virtual or virtual == 'index') and virtual not in repos:
401 401 return self.makeindex(req, res, tmpl)
402 402
403 403 # nested indexes and hgwebs
404 404
405 405 if virtual.endswith('/index') and virtual not in repos:
406 406 subdir = virtual[:-len('index')]
407 407 if any(r.startswith(subdir) for r in repos):
408 408 return self.makeindex(req, res, tmpl, subdir)
409 409
410 410 def _virtualdirs():
411 411 # Check the full virtual path, each parent, and the root ('')
412 412 if virtual != '':
413 413 yield virtual
414 414
415 415 for p in util.finddirs(virtual):
416 416 yield p
417 417
418 418 yield ''
419 419
420 420 for virtualrepo in _virtualdirs():
421 421 real = repos.get(virtualrepo)
422 422 if real:
423 423 # Re-parse the WSGI environment to take into account our
424 424 # repository path component.
425 425 uenv = req.rawenv
426 426 if pycompat.ispy3:
427 427 uenv = {k.decode('latin1'): v for k, v in
428 428 uenv.iteritems()}
429 429 req = requestmod.parserequestfromenv(
430 430 uenv, reponame=virtualrepo,
431 431 altbaseurl=self.ui.config('web', 'baseurl'),
432 432 # Reuse wrapped body file object otherwise state
433 433 # tracking can get confused.
434 434 bodyfh=req.bodyfh)
435 435 try:
436 436 # ensure caller gets private copy of ui
437 437 repo = hg.repository(self.ui.copy(), real)
438 438 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
439 439 except IOError as inst:
440 440 msg = encoding.strtolocal(inst.strerror)
441 441 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
442 442 except error.RepoError as inst:
443 443 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
444 444
445 445 # browse subdirectories
446 446 subdir = virtual + '/'
447 447 if [r for r in repos if r.startswith(subdir)]:
448 448 return self.makeindex(req, res, tmpl, subdir)
449 449
450 450 # prefixes not found
451 451 res.status = '404 Not Found'
452 452 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
453 453 return res.sendresponse()
454 454
455 455 except ErrorResponse as e:
456 456 res.status = statusmessage(e.code, pycompat.bytestr(e))
457 457 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
458 458 return res.sendresponse()
459 459 finally:
460 460 tmpl = None
461 461
462 462 def makeindex(self, req, res, tmpl, subdir=""):
463 463 self.refresh()
464 464 sortable = ["name", "description", "contact", "lastchange"]
465 465 sortcolumn, descending = None, False
466 466 if 'sort' in req.qsparams:
467 467 sortcolumn = req.qsparams['sort']
468 468 descending = sortcolumn.startswith('-')
469 469 if descending:
470 470 sortcolumn = sortcolumn[1:]
471 471 if sortcolumn not in sortable:
472 472 sortcolumn = ""
473 473
474 474 sort = [("sort_%s" % column,
475 475 "%s%s" % ((not descending and column == sortcolumn)
476 476 and "-" or "", column))
477 477 for column in sortable]
478 478
479 479 self.refresh()
480 480
481 481 entries = indexentries(self.ui, self.repos, req,
482 482 self.stripecount, sortcolumn=sortcolumn,
483 483 descending=descending, subdir=subdir)
484 484
485 485 mapping = {
486 486 'entries': entries,
487 487 'subdir': subdir,
488 488 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
489 489 'sortcolumn': sortcolumn,
490 490 'descending': descending,
491 491 }
492 492 mapping.update(sort)
493 493 res.setbodygen(tmpl.generate('index', mapping))
494 494 return res.sendresponse()
495 495
496 496 def templater(self, req, nonce):
497 497
498 498 def motd(**map):
499 499 if self.motd is not None:
500 500 yield self.motd
501 501 else:
502 502 yield config('web', 'motd')
503 503
504 504 def config(section, name, default=uimod._unset, untrusted=True):
505 505 return self.ui.config(section, name, default, untrusted)
506 506
507 507 vars = {}
508 508 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
509 509 self.templatepath)
510 510 if style == styles[0]:
511 511 vars['style'] = style
512 512
513 513 sessionvars = webutil.sessionvars(vars, r'?')
514 514 logourl = config('web', 'logourl')
515 515 logoimg = config('web', 'logoimg')
516 516 staticurl = (config('web', 'staticurl')
517 or req.apppath + '/static/')
517 or req.apppath.rstrip('/') + '/static/')
518 518 if not staticurl.endswith('/'):
519 519 staticurl += '/'
520 520
521 521 defaults = {
522 522 "encoding": encoding.encoding,
523 523 "motd": motd,
524 524 "url": req.apppath + '/',
525 525 "logourl": logourl,
526 526 "logoimg": logoimg,
527 527 "staticurl": staticurl,
528 528 "sessionvars": sessionvars,
529 529 "style": style,
530 530 "nonce": nonce,
531 531 }
532 532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
533 533 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now