"Fossies" - the Fresh Open Source Software Archive

Member "PURELIB/trac/web/main.py" (27 Aug 2019, 38704 Bytes) of package /windows/misc/Trac-1.4.win-amd64.exe:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "main.py": 1.3.5_vs_1.3.6.

    1 # -*- coding: utf-8 -*-
    2 #
    3 # Copyright (C) 2005-2019 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 https://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 https://trac.edgewall.org/log/.
   15 #
   16 # Author: Christopher Lenz <cmlenz@gmx.de>
   17 #         Matthew Good <trac@matt-good.net>
   18 
   19 from __future__ import print_function
   20 
   21 import cgi
   22 import fnmatch
   23 from functools import partial
   24 import gc
   25 import io
   26 import locale
   27 import os
   28 import pkg_resources
   29 from pprint import pformat, pprint
   30 import re
   31 import sys
   32 import traceback
   33 from urlparse import urlparse
   34 
   35 from jinja2 import FileSystemLoader
   36 
   37 from trac import __version__ as TRAC_VERSION
   38 from trac.config import BoolOption, ChoiceOption, ConfigSection, \
   39                         ConfigurationError, ExtensionOption, Option, \
   40                         OrderedExtensionsOption
   41 from trac.core import *
   42 from trac.env import open_environment
   43 from trac.loader import get_plugin_info, match_plugins_to_frames
   44 from trac.perm import PermissionCache, PermissionError
   45 from trac.resource import ResourceNotFound
   46 from trac.util import arity, get_frame_info, get_last_traceback, hex_entropy, \
   47                       lazy, read_file, safe_repr, translation, \
   48                       warn_setuptools_issue
   49 from trac.util.concurrency import get_thread_id
   50 from trac.util.datefmt import format_datetime, localtz, timezone, user_time
   51 from trac.util.html import tag, valid_html_bytes
   52 from trac.util.text import (exception_to_unicode, jinja2env, shorten_line,
   53                             to_unicode, to_utf8, unicode_quote)
   54 from trac.util.translation import _, get_negotiated_locale, has_babel, \
   55                                   safefmt, tag_
   56 from trac.web.api import HTTPBadRequest, HTTPException, HTTPForbidden, \
   57                          HTTPInternalServerError, HTTPNotFound, IAuthenticator, \
   58                          IRequestFilter, IRequestHandler, Request, \
   59                          RequestDone, TracNotImplementedError, \
   60                          is_valid_default_handler
   61 from trac.web.chrome import Chrome, ITemplateProvider, add_notice, \
   62                             add_stylesheet, add_warning
   63 from trac.web.href import Href
   64 from trac.web.session import SessionDict, Session
   65 
   66 #: This URL is used for semi-automatic bug reports (see
   67 #: `send_internal_error`). Please modify it to point to your own
   68 #: Trac instance if you distribute a patched version of Trac.
   69 default_tracker = 'https://trac.edgewall.org'
   70 
   71 
   72 class FakeSession(SessionDict):
   73 
   74     def get_session(self, sid, authenticated=False):
   75         pass
   76 
   77     def save(self):
   78         pass
   79 
   80 
   81 class FakePerm(object):
   82 
   83     username = 'anonymous'
   84 
   85     def __call__(self, realm_or_resource, id=False, version=False):
   86         return self
   87 
   88     def has_permission(self, action, realm_or_resource=None, id=False,
   89                        version=False):
   90         return False
   91 
   92     __contains__ = has_permission
   93 
   94     def require(self, action, realm_or_resource=None, id=False, version=False,
   95                 message=None):
   96         if message is None:
   97             raise PermissionError(action)
   98         else:
   99             raise PermissionError(msg=message)
  100 
  101     assert_permission = require
  102 
  103 
  104 class RequestWithSession(Request):
  105     """A request that saves its associated session when sending the reply."""
  106 
  107     def send_response(self, code=200):
  108         if code < 400:
  109             self.session.save()
  110         super(RequestWithSession, self).send_response(code)
  111 
  112 
  113 class RequestDispatcher(Component):
  114     """Web request dispatcher.
  115 
  116     This component dispatches incoming requests to registered handlers.
  117     It also takes care of user authentication and request pre- and
  118     post-processing.
  119     """
  120     required = True
  121 
  122     implements(ITemplateProvider)
  123 
  124     authenticators = ExtensionPoint(IAuthenticator)
  125     handlers = ExtensionPoint(IRequestHandler)
  126 
  127     filters = OrderedExtensionsOption('trac', 'request_filters',
  128                                       IRequestFilter,
  129         doc="""Ordered list of filters to apply to all requests.""")
  130 
  131     default_handler = ExtensionOption('trac', 'default_handler',
  132                                       IRequestHandler, 'WikiModule',
  133         """Name of the component that handles requests to the base
  134         URL.
  135 
  136         Options include `TimelineModule`, `RoadmapModule`,
  137         `BrowserModule`, `QueryModule`, `ReportModule`, `TicketModule`
  138         and `WikiModule`.
  139 
  140         The [/prefs/userinterface session preference] for default handler
  141         take precedence, when set.
  142         """)
  143 
  144     default_timezone = Option('trac', 'default_timezone', '',
  145         """The default timezone to use""")
  146 
  147     default_language = Option('trac', 'default_language', '',
  148         """The preferred language to use if no user preference has been set.
  149         """)
  150 
  151     default_date_format = ChoiceOption('trac', 'default_date_format',
  152                                        ('', 'iso8601'),
  153         """The date format. Valid options are 'iso8601' for selecting
  154         ISO 8601 format, or leave it empty which means the default
  155         date format will be inferred from the browser's default
  156         language. (''since 1.0'')
  157         """)
  158 
  159     use_xsendfile = BoolOption('trac', 'use_xsendfile', 'false',
  160         """When true, send a `X-Sendfile` header and no content when sending
  161         files from the filesystem, so that the web server handles the content.
  162         This requires a web server that knows how to handle such a header,
  163         like Apache with `mod_xsendfile` or lighttpd. (''since 1.0'')
  164         """)
  165 
  166     xsendfile_header = Option('trac', 'xsendfile_header', 'X-Sendfile',
  167         """The header to use if `use_xsendfile` is enabled. If Nginx is used,
  168         set `X-Accel-Redirect`. (''since 1.0.6'')""")
  169 
  170     configurable_headers = ConfigSection('http-headers', """
  171         Headers to be added to the HTTP request. (''since 1.2.3'')
  172 
  173         The header name must conform to RFC7230 and the following
  174         reserved names are not allowed: content-type, content-length,
  175         location, etag, pragma, cache-control, expires.
  176         """)
  177 
  178     # Public API
  179 
  180     def authenticate(self, req):
  181         for authenticator in self.authenticators:
  182             try:
  183                 authname = authenticator.authenticate(req)
  184             except TracError as e:
  185                 self.log.error("Can't authenticate using %s: %s",
  186                                authenticator.__class__.__name__,
  187                                exception_to_unicode(e, traceback=True))
  188                 add_warning(req, _("Authentication error. "
  189                                    "Please contact your administrator."))
  190                 break  # don't fallback to other authenticators
  191             if authname:
  192                 return authname
  193         return 'anonymous'
  194 
  195     def dispatch(self, req):
  196         """Find a registered handler that matches the request and let
  197         it process it.
  198 
  199         In addition, this method initializes the data dictionary
  200         passed to the the template and adds the web site chrome.
  201         """
  202         self.log.debug('Dispatching %r', req)
  203         chrome = Chrome(self.env)
  204 
  205         try:
  206             # Select the component that should handle the request
  207             chosen_handler = None
  208             for handler in self._request_handlers.values():
  209                 if handler.match_request(req):
  210                     chosen_handler = handler
  211                     break
  212             if not chosen_handler and req.path_info in ('', '/'):
  213                 chosen_handler = self._get_valid_default_handler(req)
  214             # pre-process any incoming request, whether a handler
  215             # was found or not
  216             self.log.debug("Chosen handler is %s", chosen_handler)
  217             chosen_handler = self._pre_process_request(req, chosen_handler)
  218             if not chosen_handler:
  219                 if req.path_info.endswith('/'):
  220                     # Strip trailing / and redirect
  221                     target = unicode_quote(req.path_info.rstrip('/'))
  222                     if req.query_string:
  223                         target += '?' + req.query_string
  224                     req.redirect(req.href + target, permanent=True)
  225                 raise HTTPNotFound('No handler matched request to %s',
  226                                    req.path_info)
  227 
  228             req.callbacks['chrome'] = partial(chrome.prepare_request,
  229                                               handler=chosen_handler)
  230 
  231             # Protect against CSRF attacks: we validate the form token
  232             # for all POST requests with a content-type corresponding
  233             # to form submissions
  234             if req.method == 'POST':
  235                 ctype = req.get_header('Content-Type')
  236                 if ctype:
  237                     ctype, options = cgi.parse_header(ctype)
  238                 if ctype in ('application/x-www-form-urlencoded',
  239                              'multipart/form-data') and \
  240                         req.args.get('__FORM_TOKEN') != req.form_token:
  241                     if self.env.secure_cookies and req.scheme == 'http':
  242                         msg = _('Secure cookies are enabled, you must '
  243                                 'use https to submit forms.')
  244                     else:
  245                         msg = _('Do you have cookies enabled?')
  246                     raise HTTPBadRequest(_('Missing or invalid form token.'
  247                                            ' %(msg)s', msg=msg))
  248 
  249             # Process the request and render the template
  250             resp = chosen_handler.process_request(req)
  251             if resp:
  252                 resp = self._post_process_request(req, *resp)
  253                 template, data, metadata, method = resp
  254                 if 'hdfdump' in req.args:
  255                     req.perm.require('TRAC_ADMIN')
  256                     # debugging helper - no need to render first
  257                     out = io.BytesIO()
  258                     pprint({'template': template,
  259                             'metadata': metadata,
  260                             'data': data}, out)
  261                     req.send(out.getvalue(), 'text/plain')
  262                 self.log.debug("Rendering response with template %s", template)
  263                 iterable = chrome.use_chunked_encoding
  264                 if isinstance(metadata, dict):
  265                     iterable = metadata.setdefault('iterable', iterable)
  266                     content_type = metadata.get('content_type')
  267                 else:
  268                     content_type = metadata
  269                 output = chrome.render_template(req, template, data, metadata,
  270                                                 iterable=iterable,
  271                                                 method=method)
  272                 # TODO (1.5.1) remove iterable and method parameters
  273                 req.send(output, content_type or 'text/html')
  274             else:
  275                 self.log.debug("Empty or no response from handler. "
  276                                "Entering post_process_request.")
  277                 self._post_process_request(req)
  278         except RequestDone:
  279             raise
  280         except Exception as e:
  281             # post-process the request in case of errors
  282             err = sys.exc_info()
  283             try:
  284                 self._post_process_request(req)
  285             except RequestDone:
  286                 raise
  287             except TracError as e2:
  288                 self.log.warning("Exception caught while post-processing"
  289                                  " request: %s", exception_to_unicode(e2))
  290             except Exception as e2:
  291                 if not (type(e) is type(e2) and e.args == e2.args):
  292                     self.log.error("Exception caught while post-processing"
  293                                    " request: %s",
  294                                    exception_to_unicode(e2, traceback=True))
  295             if isinstance(e, PermissionError):
  296                 raise HTTPForbidden(e)
  297             if isinstance(e, ResourceNotFound):
  298                 raise HTTPNotFound(e)
  299             if isinstance(e, NotImplementedError):
  300                 tb = traceback.extract_tb(err[2])[-1]
  301                 self.log.warning("%s caught from %s:%d in %s: %s",
  302                                  e.__class__.__name__, tb[0], tb[1], tb[2],
  303                                  to_unicode(e) or "(no message)")
  304                 raise HTTPInternalServerError(TracNotImplementedError(e))
  305             if isinstance(e, TracError):
  306                 raise HTTPInternalServerError(e)
  307             raise err[0], err[1], err[2]
  308 
  309     # ITemplateProvider methods
  310 
  311     def get_htdocs_dirs(self):
  312         return []
  313 
  314     def get_templates_dirs(self):
  315         return [pkg_resources.resource_filename('trac.web', 'templates')]
  316 
  317     # Internal methods
  318 
  319     def set_default_callbacks(self, req):
  320         """Setup request callbacks for lazily-evaluated properties.
  321         """
  322         req.callbacks.update({
  323             'authname': self.authenticate,
  324             'chrome': Chrome(self.env).prepare_request,
  325             'form_token': self._get_form_token,
  326             'lc_time': self._get_lc_time,
  327             'locale': self._get_locale,
  328             'perm': self._get_perm,
  329             'session': self._get_session,
  330             'tz': self._get_timezone,
  331             'use_xsendfile': self._get_use_xsendfile,
  332             'xsendfile_header': self._get_xsendfile_header,
  333             'configurable_headers': self._get_configurable_headers,
  334         })
  335 
  336     @lazy
  337     def _request_handlers(self):
  338         return {handler.__class__.__name__: handler
  339                 for handler in self.handlers}
  340 
  341     def _get_valid_default_handler(self, req):
  342         # Use default_handler from the Session if it is a valid value.
  343         name = req.session.get('default_handler')
  344         handler = self._request_handlers.get(name)
  345         if handler and not is_valid_default_handler(handler):
  346             handler = None
  347 
  348         if not handler:
  349             # Use default_handler from project configuration.
  350             handler = self.default_handler
  351             if not is_valid_default_handler(handler):
  352                 raise ConfigurationError(
  353                     tag_("%(handler)s is not a valid default handler. Please "
  354                          "update %(option)s through the %(page)s page or by "
  355                          "directly editing trac.ini.",
  356                          handler=tag.code(handler.__class__.__name__),
  357                          option=tag.code("[trac] default_handler"),
  358                          page=tag.a(_("Basic Settings"),
  359                                     href=req.href.admin('general/basics'))))
  360         return handler
  361 
  362     def _get_perm(self, req):
  363         if isinstance(req.session, FakeSession):
  364             return FakePerm()
  365         else:
  366             return PermissionCache(self.env, req.authname)
  367 
  368     def _get_session(self, req):
  369         try:
  370             return Session(self.env, req)
  371         except TracError as e:
  372             msg = "can't retrieve session: %s"
  373             if isinstance(e, TracValueError):
  374                 self.log.warning(msg, e)
  375             else:
  376                 self.log.error(msg, exception_to_unicode(e))
  377             return FakeSession()
  378 
  379     def _get_locale(self, req):
  380         if has_babel:
  381             preferred = req.session.get('language')
  382             default = self.default_language
  383             negotiated = get_negotiated_locale([preferred, default] +
  384                                                req.languages)
  385             self.log.debug("Negotiated locale: %s -> %s", preferred,
  386                            negotiated)
  387             return negotiated
  388 
  389     def _get_lc_time(self, req):
  390         lc_time = req.session.get('lc_time')
  391         if not lc_time or lc_time == 'locale' and not has_babel:
  392             lc_time = self.default_date_format
  393         if lc_time == 'iso8601':
  394             return 'iso8601'
  395         return req.locale
  396 
  397     def _get_timezone(self, req):
  398         try:
  399             return timezone(req.session.get('tz', self.default_timezone
  400                                             or 'missing'))
  401         except Exception:
  402             return localtz
  403 
  404     def _get_form_token(self, req):
  405         """Used to protect against CSRF.
  406 
  407         The 'form_token' is strong shared secret stored in a user
  408         cookie. By requiring that every POST form to contain this
  409         value we're able to protect against CSRF attacks. Since this
  410         value is only known by the user and not by an attacker.
  411 
  412         If the the user does not have a `trac_form_token` cookie a new
  413         one is generated.
  414         """
  415         if 'trac_form_token' in req.incookie:
  416             return req.incookie['trac_form_token'].value
  417         else:
  418             req.outcookie['trac_form_token'] = form_token = hex_entropy(24)
  419             req.outcookie['trac_form_token']['path'] = req.base_path or '/'
  420             if self.env.secure_cookies:
  421                 req.outcookie['trac_form_token']['secure'] = True
  422             req.outcookie['trac_form_token']['httponly'] = True
  423             return form_token
  424 
  425     def _get_use_xsendfile(self, req):
  426         return self.use_xsendfile
  427 
  428     @lazy
  429     def _xsendfile_header(self):
  430         header = self.xsendfile_header.strip()
  431         if Request.is_valid_header(header):
  432             return to_utf8(header)
  433         else:
  434             if not self._warn_xsendfile_header:
  435                 self._warn_xsendfile_header = True
  436                 self.log.warning("[trac] xsendfile_header is invalid: '%s'",
  437                                  header)
  438             return None
  439 
  440     def _get_xsendfile_header(self, req):
  441         return self._xsendfile_header
  442 
  443     @lazy
  444     def _configurable_headers(self):
  445         headers = []
  446         invalids = []
  447         for name, val in self.configurable_headers.options():
  448             if Request.is_valid_header(name, val):
  449                 headers.append((name, val))
  450             else:
  451                 invalids.append((name, val))
  452         if invalids:
  453             self.log.warning('[http-headers] invalid headers are ignored: %s',
  454                              ', '.join('%r: %r' % i for i in invalids))
  455         return tuple(headers)
  456 
  457     def _get_configurable_headers(self, req):
  458         return iter(self._configurable_headers)
  459 
  460     def _pre_process_request(self, req, chosen_handler):
  461         for filter_ in self.filters:
  462             chosen_handler = filter_.pre_process_request(req, chosen_handler)
  463         return chosen_handler
  464 
  465     def _post_process_request(self, req, *args):
  466         resp = args
  467         # `metadata` and the backward compatibility `method` are
  468         # optional in IRequestHandler's response. If not specified,
  469         # the default value is appended to response.
  470         metadata = {}
  471         method = None
  472         if len(resp) == 2:
  473             resp += (metadata, None)
  474         elif len(resp) == 3:
  475             metadata = resp[2]
  476             resp += (None,)
  477         elif len(resp) == 4:
  478             metadata = resp[2]
  479             method = resp[3]
  480         if method and isinstance(metadata, dict):
  481             metadata['method'] = method
  482         nbargs = len(resp)
  483         for f in reversed(self.filters):
  484             # As the arity of `post_process_request` has changed since
  485             # Trac 0.10, only filters with same arity gets passed real values.
  486             # Errors will call all filters with None arguments,
  487             # and results will not be not saved.
  488             extra_arg_count = arity(f.post_process_request) - 1
  489             if extra_arg_count == nbargs:
  490                 resp = f.post_process_request(req, *resp)
  491             elif extra_arg_count == nbargs - 1:
  492                 # IRequestFilters may modify the `method`, but the `method`
  493                 # is forwarded when not accepted by the IRequestFilter.
  494                 method = resp[-1]
  495                 resp = f.post_process_request(req, *resp[:-1])
  496                 resp += (method,)
  497             elif nbargs == 0:
  498                 f.post_process_request(req, *(None,) * extra_arg_count)
  499         return resp
  500 
  501 
  502 _warn_setuptools = False
  503 _slashes_re = re.compile(r'/+')
  504 
  505 def dispatch_request(environ, start_response):
  506     """Main entry point for the Trac web interface.
  507 
  508     :param environ: the WSGI environment dict
  509     :param start_response: the WSGI callback for starting the response
  510     """
  511 
  512     global _warn_setuptools
  513     if _warn_setuptools is False:
  514         _warn_setuptools = True
  515         warn_setuptools_issue(out=environ.get('wsgi.errors'))
  516 
  517     # SCRIPT_URL is an Apache var containing the URL before URL rewriting
  518     # has been applied, so we can use it to reconstruct logical SCRIPT_NAME
  519     script_url = environ.get('SCRIPT_URL')
  520     if script_url is not None:
  521         path_info = environ.get('PATH_INFO')
  522         if not path_info:
  523             environ['SCRIPT_NAME'] = script_url
  524         else:
  525             # mod_wsgi squashes slashes in PATH_INFO (!)
  526             script_url = _slashes_re.sub('/', script_url)
  527             path_info = _slashes_re.sub('/', path_info)
  528             if script_url.endswith(path_info):
  529                 environ['SCRIPT_NAME'] = script_url[:-len(path_info)]
  530 
  531     # If the expected configuration keys aren't found in the WSGI environment,
  532     # try looking them up in the process environment variables
  533     environ.setdefault('trac.env_path', os.getenv('TRAC_ENV'))
  534     environ.setdefault('trac.env_parent_dir',
  535                        os.getenv('TRAC_ENV_PARENT_DIR'))
  536     environ.setdefault('trac.env_index_template',
  537                        os.getenv('TRAC_ENV_INDEX_TEMPLATE'))
  538     environ.setdefault('trac.template_vars',
  539                        os.getenv('TRAC_TEMPLATE_VARS'))
  540     environ.setdefault('trac.locale', '')
  541     environ.setdefault('trac.base_url',
  542                        os.getenv('TRAC_BASE_URL'))
  543 
  544     locale.setlocale(locale.LC_ALL, environ['trac.locale'])
  545 
  546     # Determine the environment
  547     env_path = environ.get('trac.env_path')
  548     if not env_path:
  549         env_parent_dir = environ.get('trac.env_parent_dir')
  550         env_paths = environ.get('trac.env_paths')
  551         if env_parent_dir or env_paths:
  552             # The first component of the path is the base name of the
  553             # environment
  554             path_info = environ.get('PATH_INFO', '').lstrip('/').split('/')
  555             env_name = path_info.pop(0)
  556 
  557             if not env_name:
  558                 # No specific environment requested, so render an environment
  559                 # index page
  560                 send_project_index(environ, start_response, env_parent_dir,
  561                                    env_paths)
  562                 return []
  563 
  564             errmsg = None
  565 
  566             # To make the matching patterns of request handlers work, we append
  567             # the environment name to the `SCRIPT_NAME` variable, and keep only
  568             # the remaining path in the `PATH_INFO` variable.
  569             script_name = environ.get('SCRIPT_NAME', '')
  570             try:
  571                 script_name = unicode(script_name, 'utf-8')
  572             except UnicodeDecodeError:
  573                 errmsg = 'Invalid URL encoding (was %r)' % script_name
  574             else:
  575                 # (as Href expects unicode parameters)
  576                 environ['SCRIPT_NAME'] = Href(script_name)(env_name)
  577                 environ['PATH_INFO'] = '/' + '/'.join(path_info)
  578 
  579                 if env_parent_dir:
  580                     env_path = os.path.join(env_parent_dir, env_name)
  581                 else:
  582                     env_path = get_environments(environ).get(env_name)
  583 
  584                 if not env_path or not os.path.isdir(env_path):
  585                     errmsg = 'Environment not found'
  586 
  587             if errmsg:
  588                 start_response('404 Not Found',
  589                                [('Content-Type', 'text/plain'),
  590                                 ('Content-Length', str(len(errmsg)))])
  591                 return [errmsg]
  592 
  593     if not env_path:
  594         raise EnvironmentError('The environment options "TRAC_ENV" or '
  595                                '"TRAC_ENV_PARENT_DIR" or the mod_python '
  596                                'options "TracEnv" or "TracEnvParentDir" are '
  597                                'missing. Trac requires one of these options '
  598                                'to locate the Trac environment(s).')
  599     run_once = environ['wsgi.run_once']
  600 
  601     env = env_error = None
  602     try:
  603         env = open_environment(env_path, use_cache=not run_once)
  604     except Exception as e:
  605         env_error = e
  606     else:
  607         if env.base_url_for_redirect:
  608             environ['trac.base_url'] = env.base_url
  609 
  610         # Web front-end type and version information
  611         if not hasattr(env, 'webfrontend'):
  612             mod_wsgi_version = environ.get('mod_wsgi.version')
  613             if mod_wsgi_version:
  614                 mod_wsgi_version = (
  615                         "%s (WSGIProcessGroup %s WSGIApplicationGroup %s)" %
  616                         ('.'.join(str(x) for x in mod_wsgi_version),
  617                          environ.get('mod_wsgi.process_group'),
  618                          environ.get('mod_wsgi.application_group') or
  619                          '%{GLOBAL}'))
  620                 environ.update({
  621                     'trac.web.frontend': 'mod_wsgi',
  622                     'trac.web.version': mod_wsgi_version})
  623             env.webfrontend = environ.get('trac.web.frontend')
  624             if env.webfrontend:
  625                 env.webfrontend_version = environ['trac.web.version']
  626 
  627     req = RequestWithSession(environ, start_response)
  628     # fixup env.abs_href if `[trac] base_url` was not specified
  629     if env and not env.abs_href.base:
  630         env.abs_href = req.abs_href
  631     translation.make_activable(lambda: req.locale, env.path if env else None)
  632     resp = []
  633     try:
  634         if env_error:
  635             raise HTTPInternalServerError(env_error)
  636         dispatcher = RequestDispatcher(env)
  637         dispatcher.set_default_callbacks(req)
  638         try:
  639             dispatcher.dispatch(req)
  640         except RequestDone as req_done:
  641             resp = req_done.iterable
  642     except HTTPException as e:
  643         if not req.response_started:
  644             _send_user_error(req, env, e)
  645     except Exception:
  646         if not req.response_started:
  647             send_internal_error(env, req, sys.exc_info())
  648     else:
  649         resp = resp or req._response or []
  650     finally:
  651         translation.deactivate()
  652         if env and not run_once:
  653             env.shutdown(get_thread_id())
  654             # Now it's a good time to do some clean-ups
  655             #
  656             # Note: enable the '##' lines as soon as there's a suspicion
  657             #       of memory leak due to uncollectable objects (typically
  658             #       objects with a __del__ method caught in a cycle)
  659             #
  660             ##gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
  661             unreachable = gc.collect()
  662             ##env.log.debug("%d unreachable objects found.", unreachable)
  663             ##uncollectable = len(gc.garbage)
  664             ##if uncollectable:
  665             ##    del gc.garbage[:]
  666             ##    env.log.warning("%d uncollectable objects found.",
  667             ##                    uncollectable)
  668         return resp
  669 
  670 
  671 def _send_error(req, exc_info, template='error.html', content_type='text/html',
  672                 status=500, env=None, data={}):
  673     if env:
  674         add_stylesheet(req, 'common/css/code.css')
  675         metadata = {'content_type': 'text/html', 'iterable': False}
  676         try:
  677             content = Chrome(env).render_template(req, template,
  678                                                   data, metadata)
  679         except Exception:
  680             # second chance rendering, in "safe" mode
  681             data['trac_error_rendering'] = True
  682             try:
  683                 content = Chrome(env).render_template(req, template,
  684                                                       data, metadata)
  685             except Exception:
  686                 content = get_last_traceback()
  687                 content_type = 'text/plain'
  688     else:
  689         content_type = 'text/plain'
  690         content = '%s\n\n%s: %s' % (data.get('title'),
  691                                     data.get('type'),
  692                                     data.get('message'))
  693 
  694     if isinstance(content, unicode):
  695         content = content.encode('utf-8')
  696 
  697     try:
  698         req.send_error(exc_info, content, content_type, status)
  699     except RequestDone:
  700         pass
  701 
  702 
  703 def _send_user_error(req, env, e):
  704     # See trac/web/api.py for the definition of HTTPException subclasses.
  705     if env:
  706         env.log.warning('[%s] %s, %r, referrer %r',
  707                         req.remote_addr, exception_to_unicode(e),
  708                         req, req.environ.get('HTTP_REFERER'))
  709     data = {'title': e.title, 'type': 'TracError', 'message': e.message,
  710             'frames': [], 'traceback': None}
  711     if e.code == 403 and not req.is_authenticated:
  712         # TRANSLATOR: ... not logged in, you may want to 'do so' now (link)
  713         do_so = tag.a(_("do so"), href=req.href.login())
  714         add_notice(req, tag_("You are currently not logged in. You may want "
  715                              "to %(do_so)s now.", do_so=do_so))
  716     _send_error(req, sys.exc_info(), status=e.code, env=env, data=data)
  717 
  718 
  719 def send_internal_error(env, req, exc_info):
  720     if env:
  721         env.log.error("[%s] Internal Server Error: %r, referrer %r%s",
  722                       req.remote_addr, req, req.environ.get('HTTP_REFERER'),
  723                       exception_to_unicode(exc_info[1], traceback=True))
  724     message = exception_to_unicode(exc_info[1])
  725     traceback = get_last_traceback()
  726 
  727     frames, plugins, faulty_plugins, interface_custom = [], [], [], []
  728     th = 'http://trac-hacks.org'
  729     try:
  730         has_admin = 'TRAC_ADMIN' in req.perm
  731     except Exception:
  732         has_admin = False
  733 
  734     tracker = default_tracker
  735     tracker_args = {}
  736     if has_admin and not isinstance(exc_info[1], MemoryError):
  737         # Collect frame and plugin information
  738         frames = get_frame_info(exc_info[2])
  739         if env:
  740             plugins = [p for p in get_plugin_info(env)
  741                        if any(c['enabled']
  742                               for m in p['modules'].itervalues()
  743                               for c in m['components'].itervalues())]
  744             match_plugins_to_frames(plugins, frames)
  745 
  746             # Identify the tracker where the bug should be reported
  747             faulty_plugins = [p for p in plugins if 'frame_idx' in p]
  748             faulty_plugins.sort(key=lambda p: p['frame_idx'])
  749             if faulty_plugins:
  750                 info = faulty_plugins[0]['info']
  751                 home_page = info.get('home_page', '')
  752                 if 'trac' in info:
  753                     tracker = info['trac']
  754                 elif urlparse(home_page).netloc == urlparse(th).netloc:
  755                     tracker = th
  756                     plugin_name = info.get('home_page', '').rstrip('/') \
  757                                                            .split('/')[-1]
  758                     tracker_args = {'component': plugin_name}
  759             interface_custom = Chrome(env).get_interface_customization_files()
  760 
  761     def get_description(_):
  762         if env and has_admin:
  763             sys_info = "".join("|| '''`%s`''' || `%s` ||\n"
  764                                % (k, (v.replace('\n', '` [[br]] `') if v
  765                                       else _('N/A')))
  766                                for k, v in env.system_info)
  767             sys_info += "|| '''`jQuery`''' || `#JQUERY#` ||\n" \
  768                         "|| '''`jQuery UI`''' || `#JQUERYUI#` ||\n" \
  769                         "|| '''`jQuery Timepicker`''' || `#JQUERYTP#` ||\n"
  770             enabled_plugins = "".join("|| '''`%s`''' || `%s` ||\n"
  771                                       % (p['name'], p['version'] or _('N/A'))
  772                                       for p in plugins)
  773             files = Chrome(env).get_interface_customization_files().items()
  774             interface_files = "".join("|| **%s** || %s ||\n"
  775                                       % (k, ", ".join("`%s`" % f for f in v))
  776                                       for k, v in sorted(files))
  777         else:
  778             sys_info = _("''System information not available''\n")
  779             enabled_plugins = _("''Plugin information not available''\n")
  780             interface_files = _("''Interface customization information not "
  781                                  "available''\n")
  782         return _("""\
  783 ==== How to Reproduce
  784 
  785 While doing a %(method)s operation on `%(path_info)s`, \
  786 Trac issued an internal error.
  787 
  788 ''(please provide additional details here)''
  789 
  790 Request parameters:
  791 {{{
  792 %(req_args)s
  793 }}}
  794 
  795 User agent: `#USER_AGENT#`
  796 
  797 ==== System Information
  798 %(sys_info)s
  799 ==== Enabled Plugins
  800 %(enabled_plugins)s
  801 ==== Interface Customization
  802 %(interface_customization)s
  803 ==== Python Traceback
  804 {{{
  805 %(traceback)s}}}""",
  806             method=req.method, path_info=req.path_info,
  807             req_args=pformat(req.args), sys_info=sys_info,
  808             enabled_plugins=enabled_plugins,
  809             interface_customization=interface_files,
  810             traceback=to_unicode(traceback))
  811 
  812     # Generate the description once in English, once in the current locale
  813     description_en = get_description(lambda s, **kw: safefmt(s, kw))
  814     try:
  815         description = get_description(_)
  816     except Exception:
  817         description = description_en
  818 
  819     data = {'title': 'Internal Error',
  820             'type': 'internal', 'message': message,
  821             'traceback': traceback, 'frames': frames,
  822             'shorten_line': shorten_line, 'repr': safe_repr,
  823             'plugins': plugins, 'faulty_plugins': faulty_plugins,
  824             'interface': interface_custom,
  825             'tracker': tracker, 'tracker_args': tracker_args,
  826             'description': description, 'description_en': description_en}
  827 
  828     if env:
  829         Chrome(env).add_jquery_ui(req)
  830     _send_error(req, sys.exc_info(), status=500, env=env, data=data)
  831 
  832 
  833 def send_project_index(environ, start_response, parent_dir=None,
  834                        env_paths=None):
  835     req = Request(environ, start_response)
  836 
  837     loadpaths = [pkg_resources.resource_filename('trac', 'templates')]
  838     if req.environ.get('trac.env_index_template'):
  839         env_index_template = req.environ['trac.env_index_template']
  840         tmpl_path, template = os.path.split(env_index_template)
  841         loadpaths.insert(0, tmpl_path)
  842     else:
  843         template = 'index.html'
  844 
  845     data = {'trac': {'version': TRAC_VERSION,
  846                      'time': user_time(req, format_datetime)},
  847             'req': req}
  848     if req.environ.get('trac.template_vars'):
  849         for pair in req.environ['trac.template_vars'].split(','):
  850             key, val = pair.split('=')
  851             data[key] = val
  852 
  853     href = Href(req.base_path)
  854     projects = []
  855     for env_name, env_path in get_environments(environ).items():
  856         try:
  857             env = open_environment(env_path,
  858                                    use_cache=not environ['wsgi.run_once'])
  859         except Exception as e:
  860             proj = {'name': env_name, 'description': to_unicode(e)}
  861         else:
  862             proj = {
  863                 'env': env,
  864                 'name': env.project_name,
  865                 'description': env.project_description,
  866                 'href': href(env_name)
  867             }
  868         projects.append(proj)
  869     projects.sort(key=lambda proj: proj['name'].lower())
  870 
  871     data['projects'] = projects
  872 
  873     jenv = jinja2env(loader=FileSystemLoader(loadpaths))
  874     jenv.globals.update(translation.functions)
  875     tmpl = jenv.get_template(template)
  876     output = valid_html_bytes(tmpl.render(**data).encode('utf-8'))
  877     content_type = 'text/xml' if template.endswith('.xml') else 'text/html'
  878     try:
  879         req.send(output, content_type)
  880     except RequestDone:
  881         pass
  882 
  883 
  884 def get_tracignore_patterns(env_parent_dir):
  885     """Return the list of patterns from env_parent_dir/.tracignore or
  886     a default pattern of `".*"` if the file doesn't exist.
  887     """
  888     path = os.path.join(env_parent_dir, '.tracignore')
  889     try:
  890         lines = [line.strip() for line in read_file(path).splitlines()]
  891     except IOError:
  892         return ['.*']
  893     return [line for line in lines if line and not line.startswith('#')]
  894 
  895 
  896 def get_environments(environ, warn=False):
  897     """Retrieve canonical environment name to path mapping.
  898 
  899     The environments may not be all valid environments, but they are
  900     good candidates.
  901     """
  902     env_paths = environ.get('trac.env_paths', [])
  903     env_parent_dir = environ.get('trac.env_parent_dir')
  904     if env_parent_dir:
  905         env_parent_dir = os.path.normpath(env_parent_dir)
  906         # Filter paths that match the .tracignore patterns
  907         ignore_patterns = get_tracignore_patterns(env_parent_dir)
  908         paths = [name for name in os.listdir(env_parent_dir)
  909                       if os.path.isdir(os.path.join(env_parent_dir, name)) and
  910                       not any(fnmatch.fnmatch(name, pattern)
  911                               for pattern in ignore_patterns)]
  912         env_paths.extend(os.path.join(env_parent_dir, project)
  913                          for project in paths)
  914     envs = {}
  915     for env_path in env_paths:
  916         env_path = os.path.normpath(env_path)
  917         if not os.path.isdir(env_path):
  918             continue
  919         env_name = os.path.split(env_path)[1]
  920         if env_name in envs:
  921             if warn:
  922                 print('Warning: Ignoring project "%s" since it conflicts with'
  923                       ' project "%s"' % (env_path, envs[env_name]),
  924                       file=sys.stderr)
  925         else:
  926             envs[env_name] = env_path
  927     return envs