Edgewall Software

source: branches/0.11-stable/trac/web/main.py

Last change on this file was 8796, checked in by Christian Boos, 8 years ago

mod_wsgi web front-end: display WSGIProcessGroup and WSGIApplicationGroup information in the about page, in addition to the mod_wsgi version.

  • Property svn:eol-style set to native
File size: 24.9 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2005-2009 Edgewall Software
4# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
5# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
6# All rights reserved.
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution. The terms
10# are also available at http://trac.edgewall.org/wiki/TracLicense.
11#
12# This software consists of voluntary contributions made by many
13# individuals. For the exact contribution history, see the revision
14# history and logs, available at http://trac.edgewall.org/log/.
15#
16# Author: Christopher Lenz <cmlenz@gmx.de>
17# Matthew Good <trac@matt-good.net>
18
19import cgi
20import dircache
21import gc
22import locale
23import os
24import pkg_resources
25import sys
26try:
27 import threading
28except ImportError:
29 import dummy_threading as threading
30
31from genshi.core import Markup
32from genshi.builder import Fragment
33from genshi.output import DocType
34from genshi.template import TemplateLoader
35
36from trac import __version__ as TRAC_VERSION
37from trac.config import ExtensionOption, Option, OrderedExtensionsOption
38from trac.core import *
39from trac.env import open_environment
40from trac.perm import PermissionCache, PermissionError, PermissionSystem
41from trac.resource import ResourceNotFound
42from trac.util import get_lines_from_file, get_last_traceback, hex_entropy, \
43 arity
44from trac.util.compat import partial, reversed
45from trac.util.datefmt import format_datetime, http_date, localtz, timezone
46from trac.util.text import exception_to_unicode, shorten_line, to_unicode
47from trac.util.translation import _
48from trac.web.api import *
49from trac.web.chrome import Chrome
50from trac.web.clearsilver import HDFWrapper
51from trac.web.href import Href
52from trac.web.session import Session
53
54def populate_hdf(hdf, env, req=None):
55 """Populate the HDF data set with various information, such as common URLs,
56 project information and request-related information.
57 """
58 # FIXME: do we really have req==None at times?
59 hdf['trac'] = {
60 'version': TRAC_VERSION,
61 'time': format_datetime(),
62 'time.gmt': http_date()
63 }
64 hdf['project'] = {
65 'shortname': os.path.basename(env.path),
66 'name': env.project_name,
67 'name_encoded': env.project_name,
68 'descr': env.project_description,
69 'footer': Markup(env.project_footer),
70 'url': env.project_url
71 }
72
73 if req:
74 hdf['trac.href'] = {
75 'wiki': req.href.wiki(),
76 'browser': req.href.browser('/'),
77 'timeline': req.href.timeline(),
78 'roadmap': req.href.roadmap(),
79 'milestone': req.href.milestone(None),
80 'report': req.href.report(),
81 'query': req.href.query(),
82 'newticket': req.href.newticket(),
83 'search': req.href.search(),
84 'about': req.href.about(),
85 'about_config': req.href.about('config'),
86 'login': req.href.login(),
87 'logout': req.href.logout(),
88 'settings': req.href.settings(),
89 'homepage': 'http://trac.edgewall.org/'
90 }
91
92 hdf['base_url'] = req.base_url
93 hdf['base_host'] = req.base_url[:req.base_url.rfind(req.base_path)]
94 hdf['cgi_location'] = req.base_path
95 hdf['trac.authname'] = req.authname
96
97 if req.perm:
98 for action in req.perm.permissions():
99 hdf['trac.acl.' + action] = True
100
101 for arg in [k for k in req.args.keys() if k]:
102 if isinstance(req.args[arg], (list, tuple)):
103 hdf['args.%s' % arg] = [v for v in req.args[arg]]
104 elif isinstance(req.args[arg], basestring):
105 hdf['args.%s' % arg] = req.args[arg]
106 # others are file uploads
107
108
109class RequestDispatcher(Component):
110 """Component responsible for dispatching requests to registered handlers."""
111
112 authenticators = ExtensionPoint(IAuthenticator)
113 handlers = ExtensionPoint(IRequestHandler)
114
115 filters = OrderedExtensionsOption('trac', 'request_filters', IRequestFilter,
116 doc="""Ordered list of filters to apply to all requests
117 (''since 0.10'').""")
118
119 default_handler = ExtensionOption('trac', 'default_handler',
120 IRequestHandler, 'WikiModule',
121 """Name of the component that handles requests to the base URL.
122
123 Options include `TimelineModule`, `RoadmapModule`, `BrowserModule`,
124 `QueryModule`, `ReportModule`, `TicketModule` and `WikiModule`. The
125 default is `WikiModule`. (''since 0.9'')""")
126
127 default_timezone = Option('trac', 'default_timezone', '',
128 """The default timezone to use""")
129
130 # Public API
131
132 def authenticate(self, req):
133 for authenticator in self.authenticators:
134 authname = authenticator.authenticate(req)
135 if authname:
136 return authname
137 else:
138 return 'anonymous'
139
140 def dispatch(self, req):
141 """Find a registered handler that matches the request and let it process
142 it.
143
144 In addition, this method initializes the HDF data set and adds the web
145 site chrome.
146 """
147 self.log.debug('Dispatching %r', req)
148 chrome = Chrome(self.env)
149
150 # Setup request callbacks for lazily-evaluated properties
151 req.callbacks.update({
152 'authname': self.authenticate,
153 'chrome': chrome.prepare_request,
154 'hdf': self._get_hdf,
155 'perm': self._get_perm,
156 'session': self._get_session,
157 'tz': self._get_timezone,
158 'form_token': self._get_form_token
159 })
160
161 try:
162 try:
163 # Select the component that should handle the request
164 chosen_handler = None
165 try:
166 for handler in self.handlers:
167 if handler.match_request(req):
168 chosen_handler = handler
169 break
170 if not chosen_handler:
171 if not req.path_info or req.path_info == '/':
172 chosen_handler = self.default_handler
173 # pre-process any incoming request, whether a handler
174 # was found or not
175 chosen_handler = self._pre_process_request(req,
176 chosen_handler)
177 except TracError, e:
178 raise HTTPInternalError(e)
179 if not chosen_handler:
180 if req.path_info.endswith('/'):
181 # Strip trailing / and redirect
182 target = req.path_info.rstrip('/').encode('utf-8')
183 if req.query_string:
184 target += '?' + req.query_string
185 req.redirect(req.href + target, permanent=True)
186 raise HTTPNotFound('No handler matched request to %s',
187 req.path_info)
188
189 req.callbacks['chrome'] = partial(chrome.prepare_request,
190 handler=chosen_handler)
191
192 # Protect against CSRF attacks: we validate the form token for
193 # all POST requests with a content-type corresponding to form
194 # submissions
195 if req.method == 'POST':
196 ctype = req.get_header('Content-Type')
197 if ctype:
198 ctype, options = cgi.parse_header(ctype)
199 if ctype in ('application/x-www-form-urlencoded',
200 'multipart/form-data') and \
201 req.args.get('__FORM_TOKEN') != req.form_token:
202 raise HTTPBadRequest('Missing or invalid form token. '
203 'Do you have cookies enabled?')
204
205 # Process the request and render the template
206 resp = chosen_handler.process_request(req)
207 if resp:
208 if len(resp) == 2: # Clearsilver
209 chrome.populate_hdf(req)
210 template, content_type = \
211 self._post_process_request(req, *resp)
212 # Give the session a chance to persist changes
213 req.session.save()
214 req.display(template, content_type or 'text/html')
215 else: # Genshi
216 template, data, content_type = \
217 self._post_process_request(req, *resp)
218 if 'hdfdump' in req.args:
219 req.perm.require('TRAC_ADMIN')
220 # debugging helper - no need to render first
221 from pprint import pprint
222 out = StringIO()
223 pprint(data, out)
224 req.send(out.getvalue(), 'text/plain')
225 else:
226 output = chrome.render_template(req, template,
227 data, content_type)
228 # Give the session a chance to persist changes
229 req.session.save()
230 req.send(output, content_type or 'text/html')
231 else:
232 self._post_process_request(req)
233 except RequestDone:
234 raise
235 except:
236 # post-process the request in case of errors
237 err = sys.exc_info()
238 try:
239 self._post_process_request(req)
240 except RequestDone:
241 raise
242 except Exception, e:
243 self.log.error("Exception caught while post-processing"
244 " request: %s",
245 exception_to_unicode(e, traceback=True))
246 raise err[0], err[1], err[2]
247 except PermissionError, e:
248 raise HTTPForbidden(to_unicode(e))
249 except ResourceNotFound, e:
250 raise HTTPNotFound(e)
251 except TracError, e:
252 raise HTTPInternalError(e)
253
254 # Internal methods
255
256 def _get_hdf(self, req):
257 hdf = HDFWrapper(loadpaths=Chrome(self.env).get_all_templates_dirs())
258 populate_hdf(hdf, self.env, req)
259 return hdf
260
261 def _get_perm(self, req):
262 return PermissionCache(self.env, self.authenticate(req))
263
264 def _get_session(self, req):
265 return Session(self.env, req)
266
267 def _get_timezone(self, req):
268 try:
269 return timezone(req.session.get('tz', self.default_timezone
270 or 'missing'))
271 except:
272 return localtz
273
274 def _get_form_token(self, req):
275 """Used to protect against CSRF.
276
277 The 'form_token' is strong shared secret stored in a user cookie.
278 By requiring that every POST form to contain this value we're able to
279 protect against CSRF attacks. Since this value is only known by the
280 user and not by an attacker.
281
282 If the the user does not have a `trac_form_token` cookie a new
283 one is generated.
284 """
285 if req.incookie.has_key('trac_form_token'):
286 return req.incookie['trac_form_token'].value
287 else:
288 req.outcookie['trac_form_token'] = hex_entropy(24)
289 req.outcookie['trac_form_token']['path'] = req.base_path or '/'
290 if self.env.secure_cookies:
291 req.outcookie['trac_form_token']['secure'] = True
292 return req.outcookie['trac_form_token'].value
293
294 def _pre_process_request(self, req, chosen_handler):
295 for filter_ in self.filters:
296 chosen_handler = filter_.pre_process_request(req, chosen_handler)
297 return chosen_handler
298
299 def _post_process_request(self, req, *args):
300 nbargs = len(args)
301 resp = args
302 for f in reversed(self.filters):
303 # As the arity of `post_process_request` has changed since
304 # Trac 0.10, only filters with same arity gets passed real values.
305 # Errors will call all filters with None arguments,
306 # and results will not be not saved.
307 extra_arg_count = arity(f.post_process_request) - 2
308 if extra_arg_count == nbargs:
309 resp = f.post_process_request(req, *resp)
310 elif nbargs == 0:
311 f.post_process_request(req, *(None,)*extra_arg_count)
312 return resp
313
314
315def dispatch_request(environ, start_response):
316 """Main entry point for the Trac web interface.
317
318 @param environ: the WSGI environment dict
319 @param start_response: the WSGI callback for starting the response
320 """
321
322 # SCRIPT_URL is an Apache var containing the URL before URL rewriting
323 # has been applied, so we can use it to reconstruct logical SCRIPT_NAME
324 script_url = environ.get('SCRIPT_URL')
325 if script_url is not None:
326 path_info = environ.get('PATH_INFO')
327 if not path_info:
328 environ['SCRIPT_NAME'] = script_url
329 elif script_url.endswith(path_info):
330 environ['SCRIPT_NAME'] = script_url[:-len(path_info)]
331
332 # If the expected configuration keys aren't found in the WSGI environment,
333 # try looking them up in the process environment variables
334 environ.setdefault('trac.env_path', os.getenv('TRAC_ENV'))
335 environ.setdefault('trac.env_parent_dir',
336 os.getenv('TRAC_ENV_PARENT_DIR'))
337 environ.setdefault('trac.env_index_template',
338 os.getenv('TRAC_ENV_INDEX_TEMPLATE'))
339 environ.setdefault('trac.template_vars',
340 os.getenv('TRAC_TEMPLATE_VARS'))
341 environ.setdefault('trac.locale', '')
342 environ.setdefault('trac.base_url',
343 os.getenv('TRAC_BASE_URL'))
344
345
346 locale.setlocale(locale.LC_ALL, environ['trac.locale'])
347
348 # Determine the environment
349 env_path = environ.get('trac.env_path')
350 if not env_path:
351 env_parent_dir = environ.get('trac.env_parent_dir')
352 env_paths = environ.get('trac.env_paths')
353 if env_parent_dir or env_paths:
354 # The first component of the path is the base name of the
355 # environment
356 path_info = environ.get('PATH_INFO', '').lstrip('/').split('/')
357 env_name = path_info.pop(0)
358
359 if not env_name:
360 # No specific environment requested, so render an environment
361 # index page
362 send_project_index(environ, start_response, env_parent_dir,
363 env_paths)
364 return []
365
366 # To make the matching patterns of request handlers work, we append
367 # the environment name to the `SCRIPT_NAME` variable, and keep only
368 # the remaining path in the `PATH_INFO` variable.
369 environ['SCRIPT_NAME'] = Href(environ['SCRIPT_NAME'])(env_name)
370 environ['PATH_INFO'] = '/' + '/'.join(path_info)
371
372 if env_parent_dir:
373 env_path = os.path.join(env_parent_dir, env_name)
374 else:
375 env_path = get_environments(environ).get(env_name)
376
377 if not env_path or not os.path.isdir(env_path):
378 errmsg = 'Environment not found'
379 start_response('404 Not Found',
380 [('Content-Type', 'text/plain'),
381 ('Content-Length', str(len(errmsg)))])
382 return [errmsg]
383
384 if not env_path:
385 raise EnvironmentError('The environment options "TRAC_ENV" or '
386 '"TRAC_ENV_PARENT_DIR" or the mod_python '
387 'options "TracEnv" or "TracEnvParentDir" are '
388 'missing. Trac requires one of these options '
389 'to locate the Trac environment(s).')
390 run_once = environ['wsgi.run_once']
391
392 env = env_error = None
393 try:
394 env = open_environment(env_path, use_cache=not run_once)
395 if env.base_url_for_redirect:
396 environ['trac.base_url'] = env.base_url
397
398 # Web front-end type and version information
399 if not hasattr(env, 'webfrontend'):
400 mod_wsgi_version = environ.get('mod_wsgi.version')
401 if mod_wsgi_version:
402 mod_wsgi_version = (
403 "%s (WSGIProcessGroup %s WSGIApplicationGroup %s)" %
404 ('.'.join([str(x) for x in mod_wsgi_version]),
405 environ.get('mod_wsgi.process_group'),
406 environ.get('mod_wsgi.application_group') or
407 '%{GLOBAL}'))
408 environ.update({
409 'trac.web.frontend': 'mod_wsgi',
410 'trac.web.version': mod_wsgi_version})
411 env.webfrontend = environ.get('trac.web.frontend')
412 if env.webfrontend:
413 env.systeminfo.append((env.webfrontend,
414 environ['trac.web.version']))
415 except Exception, e:
416 env_error = e
417
418 req = Request(environ, start_response)
419 try:
420 return _dispatch_request(req, env, env_error)
421 finally:
422 if env and not run_once:
423 env.shutdown(threading._get_ident())
424 # Now it's a good time to do some clean-ups
425 #
426 # Note: enable the '##' lines as soon as there's a suspicion
427 # of memory leak due to uncollectable objects (typically
428 # objects with a __del__ method caught in a cycle)
429 #
430 ##gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
431 unreachable = gc.collect()
432 ##env.log.debug("%d unreachable objects found.", unreachable)
433 ##uncollectable = len(gc.garbage)
434 ##if uncollectable:
435 ## del gc.garbage[:]
436 ## env.log.warn("%d uncollectable objects found.", uncollectable)
437
438def _dispatch_request(req, env, env_error):
439 resp = []
440
441 # fixup env.abs_href if `[trac] base_url` was not specified
442 if env and not env.abs_href.base:
443 env._abs_href = req.abs_href
444
445 try:
446 if not env and env_error:
447 raise HTTPInternalError(env_error)
448 try:
449 dispatcher = RequestDispatcher(env)
450 dispatcher.dispatch(req)
451 except RequestDone:
452 pass
453 resp = req._response or []
454
455 except HTTPException, e:
456 # This part is a bit more complex than it should be.
457 # See trac/web/api.py for the definition of HTTPException subclasses.
458 if env:
459 env.log.warn(exception_to_unicode(e))
460 title = 'Error'
461 if e.reason:
462 if 'error' in e.reason.lower():
463 title = e.reason
464 else:
465 title = 'Error: %s' % e.reason
466 # The message is based on the e.detail, which can be an Exception
467 # object, but not a TracError one: when creating HTTPException,
468 # a TracError.message is directly assigned to e.detail
469 if isinstance(e.detail, Exception): # not a TracError
470 message = exception_to_unicode(e.detail)
471 elif isinstance(e.detail, Fragment): # markup coming from a TracError
472 message = e.detail
473 else:
474 message = to_unicode(e.detail)
475 data = {'title': title, 'type': 'TracError', 'message': message,
476 'frames': [], 'traceback': None}
477 if e.code == 403 and req.authname == 'anonymous':
478 req.chrome['notices'].append(Markup(
479 _('You are currently not logged in. You may want to '
480 '<a href="%(href)s">do so</a> now.',
481 href=req.href.login())))
482 try:
483 req.send_error(sys.exc_info(), status=e.code, env=env, data=data)
484 except RequestDone:
485 pass
486
487 except Exception, e:
488 if env:
489 env.log.error("Internal Server Error: %s",
490 exception_to_unicode(e, traceback=True))
491
492 exc_info = sys.exc_info()
493 try:
494 message = "%s: %s" % (e.__class__.__name__, to_unicode(e))
495 traceback = get_last_traceback()
496
497 frames = []
498 has_admin = False
499 try:
500 has_admin = 'TRAC_ADMIN' in req.perm
501 except Exception, e:
502 pass
503 if has_admin and not isinstance(e, MemoryError):
504 tb = exc_info[2]
505 while tb:
506 tb_hide = tb.tb_frame.f_locals.get('__traceback_hide__')
507 if tb_hide in ('before', 'before_and_this'):
508 del frames[:]
509 tb_hide = tb_hide[6:]
510 if not tb_hide:
511 filename = tb.tb_frame.f_code.co_filename
512 lineno = tb.tb_lineno - 1
513 before, line, after = get_lines_from_file(filename,
514 lineno, 5)
515 frames += [{'traceback': tb, 'filename': filename,
516 'lineno': lineno, 'line': line,
517 'lines_before': before, 'lines_after': after,
518 'function': tb.tb_frame.f_code.co_name,
519 'vars': tb.tb_frame.f_locals}]
520 tb = tb.tb_next
521
522 data = {'title': 'Internal Error',
523 'type': 'internal', 'message': message,
524 'traceback': traceback, 'frames': frames,
525 'shorten_line': shorten_line}
526
527 try:
528 req.send_error(exc_info, status=500, env=env, data=data)
529 except RequestDone:
530 pass
531
532 finally:
533 del exc_info
534 return resp
535
536def send_project_index(environ, start_response, parent_dir=None,
537 env_paths=None):
538 req = Request(environ, start_response)
539
540 loadpaths = [pkg_resources.resource_filename('trac', 'templates')]
541 use_clearsilver = False
542 if req.environ.get('trac.env_index_template'):
543 tmpl_path, template = os.path.split(req.environ['trac.env_index_template'])
544 loadpaths.insert(0, tmpl_path)
545 use_clearsilver = template.endswith('.cs') # assume Clearsilver
546 if use_clearsilver:
547 req.hdf = HDFWrapper(loadpaths) # keep that for custom .cs templates
548 else:
549 template = 'index.html'
550
551 data = {'trac': {'version': TRAC_VERSION, 'time': format_datetime()}}
552 if req.environ.get('trac.template_vars'):
553 for pair in req.environ['trac.template_vars'].split(','):
554 key, val = pair.split('=')
555 data[key] = val
556 if use_clearsilver:
557 req.hdf[key] = val
558 try:
559 href = Href(req.base_path)
560 projects = []
561 for env_name, env_path in get_environments(environ).items():
562 try:
563 env = open_environment(env_path,
564 use_cache=not environ['wsgi.run_once'])
565 proj = {
566 'env': env,
567 'name': env.project_name,
568 'description': env.project_description,
569 'href': href(env_name)
570 }
571 except Exception, e:
572 proj = {'name': env_name, 'description': to_unicode(e)}
573 projects.append(proj)
574 projects.sort(lambda x, y: cmp(x['name'].lower(), y['name'].lower()))
575
576 data['projects'] = projects
577 if use_clearsilver:
578 req.hdf['projects'] = projects
579 req.display(template)
580
581 loader = TemplateLoader(loadpaths, variable_lookup='lenient')
582 tmpl = loader.load(template)
583 stream = tmpl.generate(**data)
584 output = stream.render('xhtml', doctype=DocType.XHTML_STRICT)
585 req.send(output, 'text/html')
586
587 except RequestDone:
588 pass
589
590def get_environments(environ, warn=False):
591 """Retrieve canonical environment name to path mapping.
592
593 The environments may not be all valid environments, but they are good
594 candidates.
595 """
596 env_paths = environ.get('trac.env_paths', [])
597 env_parent_dir = environ.get('trac.env_parent_dir')
598 if env_parent_dir:
599 env_parent_dir = os.path.normpath(env_parent_dir)
600 paths = dircache.listdir(env_parent_dir)[:]
601 dircache.annotate(env_parent_dir, paths)
602 env_paths += [os.path.join(env_parent_dir, project) \
603 for project in paths
604 if project[-1] == '/' and project != '.egg-cache/']
605 envs = {}
606 for env_path in env_paths:
607 env_path = os.path.normpath(env_path)
608 if not os.path.isdir(env_path):
609 continue
610 env_name = os.path.split(env_path)[1]
611 if env_name in envs:
612 if warn:
613 print >> sys.stderr, ('Warning: Ignoring project "%s" since '
614 'it conflicts with project "%s"'
615 % (env_path, envs[env_name]))
616 else:
617 envs[env_name] = env_path
618 return envs

Note: See TracBrowser for help on using the repository browser.