import cgi
import mimetypes
import os
# See http://bugs.python.org/issue5853 which is a recursion bug
# that seems to effect Python 2.6, Python 2.6.1, and 2.6.2 (a fix
# has been applied on the Python 2 trunk). This workaround should
# really be in Paste if anywhere, but it's easiest to just do it
# here and get it over with to avoid needing to deal with any
# fallout.
if hasattr(mimetypes, 'init'):
mimetypes.init()
from webob import Response
from webob.exc import HTTPFound
import venusian
from paste.urlparser import StaticURLParser
from zope.deprecation import deprecated
from zope.interface import providedBy
from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.path import caller_package
from repoze.bfg.path import package_path
from repoze.bfg.resource import resolve_resource_spec
from repoze.bfg.resource import resource_spec_from_abspath
from repoze.bfg.static import PackageURLParser
from repoze.bfg.threadlocal import get_current_registry
# b/c imports
from repoze.bfg.security import view_execution_permitted
view_execution_permitted # prevent PyFlakes from complaining
deprecated('view_execution_permitted',
"('from repoze.bfg.view import view_execution_permitted' was "
"deprecated as of repoze.bfg 1.0; instead use 'from "
"repoze.bfg.security import view_execution_permitted')",
)
deprecated('NotFound',
"('from repoze.bfg.view import NotFound' was "
"deprecated as of repoze.bfg 1.1; instead use 'from "
"repoze.bfg.exceptions import NotFound')",
)
_marker = object()
def render_view_to_response(context, request, name='', secure=True):
""" Call the :term:`view callable` configured with a :term:`view
configuration` that matches the :term:`view name` ``name``
registered against the specified ``context`` and ``request`` and
return a :term:`response` object. This function will return
``None`` if a corresponding :term:`view callable` cannot be found
(when no :term:`view configuration` matches the combination of
``name`` / ``context`` / and ``request``).
If `secure`` is ``True``, and the :term:`view callable` found is
protected by a permission, the permission will be checked before
calling the view function. If the permission check disallows view
execution (based on the current :term:`authorization policy`), a
:exc:`repoze.bfg.exceptions.Forbidden` exception will be raised.
The exception's ``args`` attribute explains why the view access
was disallowed.
If ``secure`` is ``False``, no permission checking is done."""
provides = [IViewClassifier] + map(providedBy, (request, context))
try:
reg = request.registry
except AttributeError:
reg = get_current_registry()
view = reg.adapters.lookup(provides, IView, name=name)
if view is None:
return None
if not secure:
# the view will have a __call_permissive__ attribute if it's
# secured; otherwise it won't.
view = getattr(view, '__call_permissive__', view)
# if this view is secured, it will raise a Forbidden
# appropriately if the executing user does not have the proper
# permission
return view(context, request)
def render_view_to_iterable(context, request, name='', secure=True):
""" Call the :term:`view callable` configured with a :term:`view
configuration` that matches the :term:`view name` ``name``
registered against the specified ``context`` and ``request`` and
return an iterable object which represents the body of a response.
This function will return ``None`` if a corresponding :term:`view
callable` cannot be found (when no :term:`view configuration`
matches the combination of ``name`` / ``context`` / and
``request``). Additionally, this function will raise a
:exc:`ValueError` if a view function is found and called but the
view function's result does not have an ``app_iter`` attribute.
You can usually get the string representation of the return value
of this function by calling ``''.join(iterable)``, or just use
:func:`repoze.bfg.view.render_view` instead.
If ``secure`` is ``True``, and the view is protected by a
permission, the permission will be checked before the view
function is invoked. If the permission check disallows view
execution (based on the current :term:`authentication policy`), a
:exc:`repoze.bfg.exceptions.Forbidden` exception will be raised;
its ``args`` attribute explains why the view access was
disallowed.
If ``secure`` is ``False``, no permission checking is
done."""
response = render_view_to_response(context, request, name, secure)
if response is None:
return None
return response.app_iter
def render_view(context, request, name='', secure=True):
""" Call the :term:`view callable` configured with a :term:`view
configuration` that matches the :term:`view name` ``name``
registered against the specified ``context`` and ``request``
and unwind the view response's ``app_iter`` (see
:ref:`the_response`) into a single string. This function will
return ``None`` if a corresponding :term:`view callable` cannot be
found (when no :term:`view configuration` matches the combination
of ``name`` / ``context`` / and ``request``). Additionally, this
function will raise a :exc:`ValueError` if a view function is
found and called but the view function's result does not have an
``app_iter`` attribute. This function will return ``None`` if a
corresponding view cannot be found.
If ``secure`` is ``True``, and the view is protected by a
permission, the permission will be checked before the view is
invoked. If the permission check disallows view execution (based
on the current :term:`authorization policy`), a
:exc:`repoze.bfg.exceptions.Forbidden` exception will be raised;
its ``args`` attribute explains why the view access was
disallowed.
If ``secure`` is ``False``, no permission checking is done."""
iterable = render_view_to_iterable(context, request, name, secure)
if iterable is None:
return None
return ''.join(iterable)
def is_response(ob):
""" Return ``True`` if ``ob`` implements the interface implied by
:ref:`the_response`. ``False`` if not.
.. note:: this isn't a true interface check (in Zope terms), it's a
duck-typing check, as response objects are not obligated to
actually implement a Zope interface."""
# response objects aren't obligated to implement a Zope interface,
# so we do it the hard way
if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and
hasattr(ob, 'status') ):
if ( hasattr(ob.app_iter, '__iter__') and
hasattr(ob.headerlist, '__iter__') and
isinstance(ob.status, basestring) ) :
return True
return False
class static(object):
""" An instance of this class is a callable which can act as a
:mod:`repoze.bfg` :term:`view callable`; this view will serve
static files from a directory on disk based on the ``root_dir``
you provide to its constructor.
The directory may contain subdirectories (recursively); the static
view implementation will descend into these directories as
necessary based on the components of the URL in order to resolve a
path into a response.
You may pass an absolute or relative filesystem path or a
:term:`resource specification` representing the directory
containing static files as the ``root_dir`` argument to this
class' constructor.
If the ``root_dir`` path is relative, and the ``package_name``
argument is ``None``, ``root_dir`` will be considered relative to
the directory in which the Python file which *calls* ``static``
resides. If the ``package_name`` name argument is provided, and a
relative ``root_dir`` is provided, the ``root_dir`` will be
considered relative to the Python :term:`package` specified by
``package_name`` (a dotted path to a Python package).
``cache_max_age`` influences the ``Expires`` and ``Max-Age``
response headers returned by the view (default is 3600 seconds or
five minutes).
.. note:: If the ``root_dir`` is relative to a :term:`package`, or
is a :term:`resource specification` the :mod:`repoze.bfg`
``resource`` ZCML directive or
:class:`repoze.bfg.configuration.Configurator` method can be
used to override resources within the named ``root_dir``
package-relative directory. However, if the ``root_dir`` is
absolute, the ``resource`` directive will not be able to
override the resources it contains. """
def __init__(self, root_dir, cache_max_age=3600, package_name=None):
# package_name is for bw compat; it is preferred to pass in a
# package-relative path as root_dir
# (e.g. ``anotherpackage:foo/static``).
caller_package_name = caller_package().__name__
package_name = package_name or caller_package_name
package_name, root_dir = resolve_resource_spec(root_dir, package_name)
if package_name is None:
app = StaticURLParser(root_dir, cache_max_age=cache_max_age)
else:
app = PackageURLParser(
package_name, root_dir, cache_max_age=cache_max_age)
self.app = app
def __call__(self, context, request):
subpath = '/'.join(request.subpath)
request_copy = request.copy()
# Fix up PATH_INFO to get rid of everything but the "subpath"
# (the actual path to the file relative to the root dir).
request_copy.environ['PATH_INFO'] = '/' + subpath
# Zero out SCRIPT_NAME for good measure.
request_copy.environ['SCRIPT_NAME'] = ''
return request_copy.get_response(self.app)
class bfg_view(object):
""" A function, class or method :term:`decorator` which allows a
developer to create view registrations nearer to a :term:`view
callable` definition than use of :term:`ZCML` or :term:`imperative
configuration` to do the same.
For example, this code in a module ``views.py``::
from models import MyModel
@bfg_view(name='my_view', context=MyModel, permission='read',
route_name='site1')
def my_view(context, request):
return 'OK'
Might replace the following call to the
:meth:`repoze.bfg.configuration.Configurator.add_view` method::
import views
import models
config.add_view(views.my_view, context=models.MyModel, name='my_view',
permission='read', 'route_name='site1')
Or might replace the following ZCML ``view`` declaration::
%s
""" % (status, status, msg)
headers = [('Content-Length', str(len(html))),
('Content-Type', 'text/html')]
response_factory = Response
registry = getattr(request, 'registry', None)
if registry is not None:
# be kind to old tests
response_factory = registry.queryUtility(IResponseFactory,
default=Response)
return response_factory(status = status,
headerlist = headers,
app_iter = [html])
def default_forbidden_view(context, request):
return default_view(context, request, '401 Unauthorized')
def default_notfound_view(context, request):
return default_view(context, request, '404 Not Found')
def append_slash_notfound_view(context, request):
"""For behavior like Django's ``APPEND_SLASH=True``, use this view
as the :term:`Not Found view` in your application.
When this view is the Not Found view (indicating that no view was
found), and any routes have been defined in the configuration of
your application, if the value of the ``PATH_INFO`` WSGI
environment variable does not already end in a slash, and if the
value of ``PATH_INFO`` *plus* a slash matches any route's path, do
an HTTP redirect to the slash-appended PATH_INFO. Note that this
will *lose* ``POST`` data information (turning it into a GET), so
you shouldn't rely on this to redirect POST requests.
If you use :term:`ZCML`, add the following to your application's
``configure.zcml`` to use this view as the Not Found view::