| 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 |
|
|---|
| 19 | import cgi
|
|---|
| 20 | import dircache
|
|---|
| 21 | import gc
|
|---|
| 22 | import locale
|
|---|
| 23 | import os
|
|---|
| 24 | import pkg_resources
|
|---|
| 25 | import sys
|
|---|
| 26 | try:
|
|---|
| 27 | import threading
|
|---|
| 28 | except ImportError:
|
|---|
| 29 | import dummy_threading as threading
|
|---|
| 30 |
|
|---|
| 31 | from genshi.core import Markup
|
|---|
| 32 | from genshi.builder import Fragment
|
|---|
| 33 | from genshi.output import DocType
|
|---|
| 34 | from genshi.template import TemplateLoader
|
|---|
| 35 |
|
|---|
| 36 | from trac import __version__ as TRAC_VERSION
|
|---|
| 37 | from trac.config import ExtensionOption, Option, OrderedExtensionsOption
|
|---|
| 38 | from trac.core import *
|
|---|
| 39 | from trac.env import open_environment
|
|---|
| 40 | from trac.perm import PermissionCache, PermissionError, PermissionSystem
|
|---|
| 41 | from trac.resource import ResourceNotFound
|
|---|
| 42 | from trac.util import get_lines_from_file, get_last_traceback, hex_entropy, \
|
|---|
| 43 | arity
|
|---|
| 44 | from trac.util.compat import partial, reversed
|
|---|
| 45 | from trac.util.datefmt import format_datetime, http_date, localtz, timezone
|
|---|
| 46 | from trac.util.text import exception_to_unicode, shorten_line, to_unicode
|
|---|
| 47 | from trac.util.translation import _
|
|---|
| 48 | from trac.web.api import *
|
|---|
| 49 | from trac.web.chrome import Chrome
|
|---|
| 50 | from trac.web.clearsilver import HDFWrapper
|
|---|
| 51 | from trac.web.href import Href
|
|---|
| 52 | from trac.web.session import Session
|
|---|
| 53 |
|
|---|
| 54 | def 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 |
|
|---|
| 109 | class 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 |
|
|---|
| 315 | def 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 |
|
|---|
| 438 | def _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 |
|
|---|
| 536 | def 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 |
|
|---|
| 590 | def 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
|
|---|