blob: 0ce0c7a30dc879255c017923dc35e0f9402ba868 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import abc
import os
import pkg_resources
from trac.env import open_environment
from trac.util import exception_to_unicode
from trac.util.concurrency import threading
from trac.web.api import RequestDone
from trac.web.href import Href
from trac.web.main import RequestWithSession
__all__ = ['environment_factory', 'install_global_hooks']
class EnvironmentFactoryBase(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def open_environment(self, environ, env_path, global_env, use_cache=False):
raise NotImplementedError("Must override method 'open_environment'")
class RequestFactoryBase(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def create_request(self, env, environ, start_response):
pass
def load_class(fqn):
try:
pkg, resource = fqn.rsplit('.', 1)
except ValueError:
class_ = None
else:
try:
module = __import__(pkg, fromlist=[resource])
except ImportError:
class_ = None
else:
try:
class_ = getattr(module, resource)
except AttributeError:
class_ = None
return class_
_global_hooks_installed = False
_global_hooks_lock = threading.Lock()
def install_global_hooks():
global _global_hooks_installed, _global_hooks_lock
if _global_hooks_installed:
return
_global_hooks_lock.acquire()
try:
if not _global_hooks_installed:
try:
# TODO: this is currently hardcoded, maybe it could be made
# configurable in the future
import multiproduct.hooks
except:
pass
_global_hooks_installed = True
finally:
_global_hooks_lock.release()
return
def environment_factory(env):
print load_class(env.config.get('trac', 'environment_factory'))
return load_class(env.config.get('trac', 'environment_factory'))
def request_factory(env):
print load_class(env.config.get('trac', 'request_factory'))
return load_class(env.config.get('trac', 'request_factory'))
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
Notice: This class is a straightforward refactoring of factories
implementation.
"""
global_env = None
def open_environment(self, environ, start_response):
env_path = environ.get('trac.env_path')
if env_path:
environ['trac.env_name'] = os.path.basename(env_path)
else:
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)
raise RequestDone
environ['trac.env_name'] = env_name
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:
write = start_response('404 Not Found',
[('Content-Type', 'text/plain'),
('Content-Length',
str(len(errmsg)))])
write(errmsg)
raise RequestDone
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']
try:
global_env = open_environment(env_path, use_cache=not run_once)
factory = environment_factory(global_env)
factory_env = factory().open_environment(environ, env_path,
global_env,
use_cache=not run_once) \
if factory else None
except Exception:
raise
else:
self.global_env = global_env
env = factory_env if factory_env else global_env
return env
def create_request(self, env, environ, start_response):
factory = None
try:
factory = request_factory(self.global_env)
except Exception:
pass
return factory().create_request(env, environ, start_response) \
if factory else RequestWithSession(environ, start_response)
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 send_project_index, get_environments