Commits

Olemis Lang committed cc77f83

Trac #11585 : Backport Apache Bloodhound web bootstrap handler into Trac

Comments (0)

Files changed (2)

+t11585/t11585_web_bootstrap.diff
 # Placed by Bitbucket

t11585/t11585_web_bootstrap.diff

+# HG changeset patch
+# Parent df67d671a129206424eae17270d52ce0f618d222
+Trac #11585 : Web bootstrap handlers for advanced routing
+
+diff --git a/trac/web/hooks.py b/trac/web/hooks.py
+new file mode 100644
+--- /dev/null
++++ b/trac/web/hooks.py
+@@ -0,0 +1,233 @@
++# -*- coding: utf-8 -*-
++#
++# Copyright (C) 2014 Edgewall Software
++# All rights reserved.
++#
++# This software is licensed as described in the file COPYING, which
++# you should have received as part of this distribution. The terms
++# are also available at http://trac.edgewall.org/wiki/TracLicense.
++#
++# This software consists of voluntary contributions made by many
++# individuals. For the exact contribution history, see the revision
++# history and logs, available at http://trac.edgewall.org/log/.
++#
++# Author: Olemis Lang <olemis+trac@gmail.com>
++
++import os
++import pkg_resources
++
++from trac.env import open_environment
++from trac.util import exception_to_unicode
++from trac.web.href import Href
++
++__all__ = 'BootstrapHandlerBase', 'load_bootstrap_handler'
++
++class BootstrapHandlerBase(object):
++    """Objects responsible for loading the target environment and
++    request objects used in subsequent dispatching. 
++    """
++    def open_environment(self, environ, start_response):
++        """Load and initialize target Trac environment involved in request
++        dispatching.
++
++        The following WSGI entries will also be present in `environ` dict:
++
++        ||= WSGI variable =||= Environment variable =||= Comment =||
++        || trac.env_path || TRAC_ENV || See wiki:TracModWSGI ||
++        || trac.env_parent_dir || TRAC_ENV_PARENT_DIR || See wiki:TracModWSGI||
++        || trac.env_index_template || TRAC_ENV_INDEX_TEMPLATE || See wiki:TracInterfaceCustomization ||
++        || trac.template_vars || TRAC_TEMPLATE_VARS || See wiki:TracInterfaceCustomization ||
++        || trac.locale ||  || Target locale ||
++        || trac.base_url || TRAC_BASE_URL || Trac base URL hint ||
++
++        A new entry named 'trac.env_name' identifying environment SHOULD be
++        added (e.g. used by tracd to choose authentication realms). 
++        As a side-effect the WSGI environment dict (i.e. `environ`) may be
++        modified in many different ways to prepare it for subsequent
++        dispatching.
++
++        This method may handle the request (e.g. render environment index page)
++        in case environment lookup yields void results. In that case it MUST 
++        invoke WSGI `write` callable returned by `start_response` and raise 
++        `trac.web.api.RequestDone` exception.
++
++        :param environ: WSGI environment dict
++        :param start_response: WSGI callback for starting the response
++        :return: environment object
++        :throws RequestDone: if the request is fully processed while loading
++                             target environment e.g. environment index page
++        :throws EnvironmentError: if it is impossible to find a way to locate
++                                  target environment e.g. TRAC_ENV and 
++                                  TRAC_ENV_PARENT_DIR both missing
++        :throws Exception: any other exception will be processed by the caller 
++                           in order to send a generic error message back to
++                           the HTTP client
++        """
++        raise NotImplementedError("Must override method 'open_environment'")
++
++    def default_probe_environment(self, environ):
++        """By default it will invoke `open_environment` and discard the
++        resulting environment object. This approach is generic but not
++        efficient. Should be overridden whenever possible. 
++        """
++        # If the expected configuration keys aren't found in the WSGI
++        # environment, try looking them up in the process environment variables
++        environ.setdefault('trac.env_path', os.getenv('TRAC_ENV'))
++        environ.setdefault('trac.env_parent_dir',
++                           os.getenv('TRAC_ENV_PARENT_DIR'))
++        environ.setdefault('trac.env_index_template',
++                           os.getenv('TRAC_ENV_INDEX_TEMPLATE'))
++        environ.setdefault('trac.template_vars',
++                           os.getenv('TRAC_TEMPLATE_VARS'))
++        environ.setdefault('trac.locale', '')
++        environ.setdefault('trac.base_url',
++                           os.getenv('TRAC_BASE_URL'))
++
++        try:
++            self.open_environment(environ, 
++                                  lambda status, headers: (lambda data: None))
++        except Exception:
++            # Handle all exceptions; else potential HTTP protocol violation
++            pass
++
++    def probe_environment(self, environ):
++        """This method is aimed at providing a lightweight version of
++        `open_environment` by solely applying upon `environ` the side effects 
++        needed to dispatch the request in environment context.
++
++        By default it will invoke `open_environment` and discard the
++        resulting environment object. Specialized versions will have the chance
++        to implement more efficient strategies in case environment
++        instantiation may be avoided. 
++
++        :return: None
++        """
++        self.default_probe_environment(environ)
++
++    def create_request(self, env, environ, start_response):
++        """Instantiate request object used in subsequent request dispatching
++
++        :param env: target Trac environment returned by `open_environment`
++        :param environ: WSGI environment dict
++        :param start_response: WSGI callback for starting the response
++        """
++        raise NotImplementedError("Must override method 'create_request'")
++
++
++class DefaultBootstrapHandler(BootstrapHandlerBase):
++    """Default bootstrap handler
++
++    - Load environment based on URL path.
++    - Instantiate RequestWithSession
++    """
++
++    def open_environment(self, environ, start_response):
++        env_path = environ.get('trac.env_path')
++        if not env_path:
++            env_parent_dir = environ.get('trac.env_parent_dir')
++            env_paths = environ.get('trac.env_paths')
++            if env_parent_dir or env_paths:
++                # The first component of the path is the base name of the
++                # environment
++                path_info = environ.get('PATH_INFO', '').lstrip('/').split('/')
++                env_name = path_info.pop(0)
++
++                if not env_name:
++                    # No specific environment requested, so render an environment
++                    # index page
++                    send_project_index(environ, start_response, env_parent_dir,
++                                       env_paths)
++                    return []
++
++                errmsg = None
++
++                # To make the matching patterns of request handlers work, we append
++                # the environment name to the `SCRIPT_NAME` variable, and keep only
++                # the remaining path in the `PATH_INFO` variable.
++                script_name = environ.get('SCRIPT_NAME', '')
++                try:
++                    script_name = unicode(script_name, 'utf-8')
++                    # (as Href expects unicode parameters)
++                    environ['SCRIPT_NAME'] = Href(script_name)(env_name)
++                    environ['PATH_INFO'] = '/' + '/'.join(path_info)
++
++                    if env_parent_dir:
++                        env_path = os.path.join(env_parent_dir, env_name)
++                    else:
++                        env_path = get_environments(environ).get(env_name)
++
++                    if not env_path or not os.path.isdir(env_path):
++                        errmsg = 'Environment not found'
++                except UnicodeDecodeError:
++                    errmsg = 'Invalid URL encoding (was %r)' % script_name
++
++                if errmsg:
++                    start_response('404 Not Found',
++                                   [('Content-Type', 'text/plain'),
++                                    ('Content-Length', str(len(errmsg)))])
++                    return [errmsg]
++
++        if not env_path:
++            raise EnvironmentError('The environment options "TRAC_ENV" or '
++                                   '"TRAC_ENV_PARENT_DIR" or the mod_python '
++                                   'options "TracEnv" or "TracEnvParentDir" are '
++                                   'missing. Trac requires one of these options '
++                                   'to locate the Trac environment(s).')
++        run_once = environ['wsgi.run_once']
++
++        env = env_error = None
++        try:
++              env = open_environment(env_path, use_cache=not run_once)
++              if env.base_url_for_redirect:
++                  environ['trac.base_url'] = env.base_url
++  
++              # Web front-end type and version information
++              if not hasattr(env, 'webfrontend'):
++                  mod_wsgi_version = environ.get('mod_wsgi.version')
++                  if mod_wsgi_version:
++                      mod_wsgi_version = (
++                              "%s (WSGIProcessGroup %s WSGIApplicationGroup %s)" %
++                              ('.'.join([str(x) for x in mod_wsgi_version]),
++                               environ.get('mod_wsgi.process_group'),
++                               environ.get('mod_wsgi.application_group') or
++                               '%{GLOBAL}'))
++                      environ.update({
++                          'trac.web.frontend': 'mod_wsgi',
++                          'trac.web.version': mod_wsgi_version})
++                  env.webfrontend = environ.get('trac.web.frontend')
++                  if env.webfrontend:
++                      env.systeminfo.append((env.webfrontend,
++                                             environ['trac.web.version']))
++        except Exception, e:
++            env_error = e
++        else:
++            return env
++
++    def create_request(self, env, environ, start_response):
++        req = RequestWithSession(environ, start_response)
++        translation.make_activable(lambda: req.locale, env.path if env else None)
++
++default_bootstrap_handler = DefaultBootstrapHandler()
++
++
++def load_bootstrap_handler(bootstrap_ep, log=None):
++    """Load handler for environment lookup and instantiation of request objects
++
++    :param bootstrap_ep: entry point specification
++    :param log: file-like object used to report errors
++    """
++    bootstrap = None
++    if bootstrap_ep:
++        try:
++            ep = pkg_resources.EntryPoint.parse('x = ' + bootstrap_ep)
++            bootstrap = ep.load(require=False)
++        except Exception, e:
++            if log:
++                log.write("[FAIL] [Trac] entry point '%s'. Reason %s" %
++                          (bootstrap_ep, repr(exception_to_unicode(e))))
++    if bootstrap is None:
++        bootstrap = default_bootstrap_handler
++    return bootstrap
++
++# Recursive imports
++from trac.web.main import get_environments, RequestWithSession, send_project_index
+diff --git a/trac/web/main.py b/trac/web/main.py
+--- a/trac/web/main.py
++++ b/trac/web/main.py
+@@ -383,6 +383,7 @@
+     :param start_response: the WSGI callback for starting the response
+     """
+ 
++    print "Entered dispatch_request"
+     # SCRIPT_URL is an Apache var containing the URL before URL rewriting
+     # has been applied, so we can use it to reconstruct logical SCRIPT_NAME
+     script_url = environ.get('SCRIPT_URL')
+@@ -409,91 +410,72 @@
+     environ.setdefault('trac.locale', '')
+     environ.setdefault('trac.base_url',
+                        os.getenv('TRAC_BASE_URL'))
++    environ.setdefault('trac.bootstrap_handler',
++                       os.getenv('TRAC_BOOTSTRAP_HANDLER'))
+ 
+ 
+     locale.setlocale(locale.LC_ALL, environ['trac.locale'])
+ 
++    # Load handler for environment lookup and instantiation of request objects
++    bootstrap_ep = environ['trac.bootstrap_handler']
++    bootstrap = load_bootstrap_handler(bootstrap_ep,
++                                       environ.get('wsgi.errors'))
++    print "Loaded bootstrap handler", bootstrap
++ 
+     # Determine the environment
+-    env_path = environ.get('trac.env_path')
+-    if not env_path:
+-        env_parent_dir = environ.get('trac.env_parent_dir')
+-        env_paths = environ.get('trac.env_paths')
+-        if env_parent_dir or env_paths:
+-            # The first component of the path is the base name of the
+-            # environment
+-            path_info = environ.get('PATH_INFO', '').lstrip('/').split('/')
+-            env_name = path_info.pop(0)
++    env = env_error = None
++    try:
++        env = bootstrap.open_environment(environ, start_response)
++    except RequestDone:
++        return []
++    except EnvironmentError, e:
++        if e.__class__ is EnvironmentError:
++            raise
++        else:
++            env_error = e
+ 
+-            if not env_name:
+-                # No specific environment requested, so render an environment
+-                # index page
+-                send_project_index(environ, start_response, env_parent_dir,
+-                                   env_paths)
+-                return []
++    except Exception, e:
++        env_error = e
++    else:
++        try:
++            if env.base_url_for_redirect:
++                environ['trac.base_url'] = env.base_url
++    
++            # Web front-end type and version information
++            if not hasattr(env, 'webfrontend'):
++                mod_wsgi_version = environ.get('mod_wsgi.version')
++                if mod_wsgi_version:
++                    mod_wsgi_version = (
++                            "%s (WSGIProcessGroup %s WSGIApplicationGroup %s)" %
++                            ('.'.join([str(x) for x in mod_wsgi_version]),
++                             environ.get('mod_wsgi.process_group'),
++                             environ.get('mod_wsgi.application_group') or
++                             '%{GLOBAL}'))
++                    environ.update({
++                        'trac.web.frontend': 'mod_wsgi',
++                        'trac.web.version': mod_wsgi_version})
++                env.webfrontend = environ.get('trac.web.frontend')
++                if env.webfrontend:
++                    env.systeminfo.append((env.webfrontend,
++                                           environ['trac.web.version']))
++        except Exception, e:
++            env_error = e
+ 
+-            errmsg = None
+-
+-            # To make the matching patterns of request handlers work, we append
+-            # the environment name to the `SCRIPT_NAME` variable, and keep only
+-            # the remaining path in the `PATH_INFO` variable.
+-            script_name = environ.get('SCRIPT_NAME', '')
+-            try:
+-                script_name = unicode(script_name, 'utf-8')
+-                # (as Href expects unicode parameters)
+-                environ['SCRIPT_NAME'] = Href(script_name)(env_name)
+-                environ['PATH_INFO'] = '/' + '/'.join(path_info)
+-
+-                if env_parent_dir:
+-                    env_path = os.path.join(env_parent_dir, env_name)
+-                else:
+-                    env_path = get_environments(environ).get(env_name)
+-
+-                if not env_path or not os.path.isdir(env_path):
+-                    errmsg = 'Environment not found'
+-            except UnicodeDecodeError:
+-                errmsg = 'Invalid URL encoding (was %r)' % script_name
+-
+-            if errmsg:
+-                start_response('404 Not Found',
+-                               [('Content-Type', 'text/plain'),
+-                                ('Content-Length', str(len(errmsg)))])
+-                return [errmsg]
+-
+-    if not env_path:
+-        raise EnvironmentError('The environment options "TRAC_ENV" or '
+-                               '"TRAC_ENV_PARENT_DIR" or the mod_python '
+-                               'options "TracEnv" or "TracEnvParentDir" are '
+-                               'missing. Trac requires one of these options '
+-                               'to locate the Trac environment(s).')
+     run_once = environ['wsgi.run_once']
+ 
+-    env = env_error = None
+-    try:
+-        env = open_environment(env_path, use_cache=not run_once)
+-        if env.base_url_for_redirect:
+-            environ['trac.base_url'] = env.base_url
+-
+-        # Web front-end type and version information
+-        if not hasattr(env, 'webfrontend'):
+-            mod_wsgi_version = environ.get('mod_wsgi.version')
+-            if mod_wsgi_version:
+-                mod_wsgi_version = (
+-                        "%s (WSGIProcessGroup %s WSGIApplicationGroup %s)" %
+-                        ('.'.join([str(x) for x in mod_wsgi_version]),
+-                         environ.get('mod_wsgi.process_group'),
+-                         environ.get('mod_wsgi.application_group') or
+-                         '%{GLOBAL}'))
+-                environ.update({
+-                    'trac.web.frontend': 'mod_wsgi',
+-                    'trac.web.version': mod_wsgi_version})
+-            env.webfrontend = environ.get('trac.web.frontend')
+-            if env.webfrontend:
+-                env.systeminfo.append((env.webfrontend,
+-                                       environ['trac.web.version']))
+-    except Exception, e:
+-        env_error = e
+-
+-    req = RequestWithSession(environ, start_response)
++    req = None
++    if env_error is None:
++        try:
++            req = bootstrap.create_request(env, environ, start_response) \
++                if env is not None else Request(environ, start_response)
++        except Exception, e:
++            log = environ.get('wsgi.errors')
++            if log:
++                log.write("[FAIL] [Trac] Entry point '%s' "
++                          "Method 'create_request' Reason %s" %
++                          (bootstrap_ep, repr(exception_to_unicode(e))))
++    if req is None:
++        req = RequestWithSession(environ, start_response)
+     translation.make_activable(lambda: req.locale, env.path if env else None)
+     try:
+         return _dispatch_request(req, env, env_error)
+@@ -758,3 +740,6 @@
+         else:
+             envs[env_name] = env_path
+     return envs
++
++from trac.web.hooks import load_bootstrap_handler
++