268 lines
10 KiB
Python
268 lines
10 KiB
Python
"""Provides a publisher object that behaves like the Quixote 1 Publisher.
|
|
Specifically, arbitrary namespaces may be exported and the HTTPRequest
|
|
object is passed as the first argument to exported functions. Also,
|
|
the _q_lookup(), _q_resolve(), and _q_access() methods work as they did
|
|
in Quixote 1.
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
import types
|
|
import warnings
|
|
from quixote import errors, get_request, redirect
|
|
from quixote.publish import Publisher as _Publisher
|
|
from quixote.directory import Directory
|
|
from quixote.html import htmltext
|
|
|
|
|
|
class Publisher(_Publisher):
|
|
"""
|
|
Instance attributes:
|
|
namespace_stack : [ module | instance | class ]
|
|
"""
|
|
|
|
def __init__(self, root_namespace, config=None):
|
|
from quixote.config import Config
|
|
if isinstance(root_namespace, str):
|
|
root_namespace = _get_module(root_namespace)
|
|
self.namespace_stack = [root_namespace]
|
|
if config is None:
|
|
config = Config()
|
|
directory = RootDirectory(root_namespace, self.namespace_stack)
|
|
_Publisher.__init__(self, directory, config=config)
|
|
|
|
def debug(self, msg):
|
|
self.log(msg)
|
|
|
|
def get_namespace_stack(self):
|
|
"""get_namespace_stack() -> [ module | instance | class ]
|
|
"""
|
|
return self.namespace_stack
|
|
|
|
|
|
class RootDirectory(Directory):
|
|
def __init__(self, root_namespace, namespace_stack):
|
|
self.root_namespace = root_namespace
|
|
self.namespace_stack = namespace_stack
|
|
|
|
def _q_traverse(self, path):
|
|
# Initialize the publisher's namespace_stack
|
|
del self.namespace_stack[:]
|
|
|
|
request = get_request()
|
|
|
|
# Traverse package to a (hopefully-) callable object
|
|
object = _traverse_url(self.root_namespace, path, request,
|
|
self.namespace_stack)
|
|
|
|
# None means no output -- traverse_url() just issued a redirect.
|
|
if object is None:
|
|
return None
|
|
|
|
# Anything else must be either a string...
|
|
if isstring(object):
|
|
output = object
|
|
|
|
# ...or a callable.
|
|
elif hasattr(object, '__call__'):
|
|
output = object(request)
|
|
if output is None:
|
|
raise RuntimeError('callable %r returned None' % object)
|
|
|
|
# Uh-oh: 'object' is neither a string nor a callable.
|
|
else:
|
|
raise RuntimeError(
|
|
"object is neither callable nor a string: %s" % repr(object))
|
|
|
|
return output
|
|
|
|
|
|
def _get_module(name):
|
|
"""Get a module object by name."""
|
|
__import__(name)
|
|
module = sys.modules[name]
|
|
return module
|
|
|
|
|
|
_slash_pat = re.compile("//*")
|
|
|
|
def _traverse_url(root_namespace, path_components, request, namespace_stack):
|
|
"""(root_namespace : any, path_components : [string],
|
|
request : HTTPRequest, namespace_stack : list) -> (object : any)
|
|
|
|
Perform traversal based on the provided path, starting at the root
|
|
object. It returns the script name and path info values for
|
|
the arrived-at object, along with the object itself and
|
|
a list of the namespaces traversed to get there.
|
|
|
|
It's expected that the final object is something callable like a
|
|
function or a method; intermediate objects along the way will
|
|
usually be packages or modules.
|
|
|
|
To prevent crackers from writing URLs that traverse private
|
|
objects, every package, module, or object along the way must have
|
|
a _q_exports attribute containing a list of publicly visible
|
|
names. Not having a _q_exports attribute is an error, though
|
|
having _q_exports be an empty list is OK. If a component of the path
|
|
isn't in _q_exports, that also produces an error.
|
|
|
|
Modifies the namespace_stack as it traverses the url, so that
|
|
any exceptions encountered along the way can be handled by the
|
|
nearest handler.
|
|
"""
|
|
|
|
path = '/' + '/'.join(path_components)
|
|
|
|
# If someone accesses a Quixote driver script without a trailing
|
|
# slash, we'll wind up here with an empty path. This won't
|
|
# work; relative references in the page generated by the root
|
|
# namespace's _q_index() will be off. Fix it by redirecting the
|
|
# user to the right URL; when the client follows the redirect,
|
|
# we'll wind up here again with path == '/'.
|
|
if not path:
|
|
return redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=1)
|
|
|
|
# Traverse starting at the root
|
|
object = root_namespace
|
|
namespace_stack.append(object)
|
|
|
|
# Loop over the components of the path
|
|
for component in path_components:
|
|
if component == "":
|
|
# "/q/foo/" == "/q/foo/_q_index"
|
|
component = "_q_index"
|
|
object = _get_component(object, component, request, namespace_stack)
|
|
|
|
if not (isstring(object) or hasattr(object, '__call__')):
|
|
# We went through all the components of the path and ended up at
|
|
# something which isn't callable, like a module or an instance
|
|
# without a __call__ method.
|
|
if path[-1] != '/':
|
|
if not request.form:
|
|
# This is for the convenience of users who type in paths.
|
|
# Repair the path and redirect. This should not happen for
|
|
# URLs within the site.
|
|
return redirect(request.get_path() + "/", permanent=1)
|
|
|
|
else:
|
|
# Automatic redirects disabled or there is form data. If
|
|
# there is form data then the programmer is using the
|
|
# wrong path. A redirect won't work if the form data came
|
|
# from a POST anyhow.
|
|
raise errors.TraversalError(
|
|
"object is neither callable nor string "
|
|
"(missing trailing slash?)",
|
|
private_msg=repr(object),
|
|
path=path)
|
|
else:
|
|
raise errors.TraversalError(
|
|
"object is neither callable nor string",
|
|
private_msg=repr(object),
|
|
path=path)
|
|
|
|
return object
|
|
|
|
|
|
def _get_component(container, component, request, namespace_stack):
|
|
"""Get one component of a path from a namespace.
|
|
"""
|
|
# First security check: if the container doesn't even have an
|
|
# _q_exports list, fail now: all Quixote-traversable namespaces
|
|
# (modules, packages, instances) must have an export list!
|
|
if not hasattr(container, '_q_exports'):
|
|
raise errors.TraversalError(
|
|
private_msg="%r has no _q_exports list" % container)
|
|
|
|
# Second security check: call _q_access function if it's present.
|
|
if hasattr(container, '_q_access'):
|
|
# will raise AccessError if access failed
|
|
container._q_access(request)
|
|
|
|
# Third security check: make sure the current name component
|
|
# is in the export list or is '_q_index'. If neither
|
|
# condition is true, check for a _q_lookup() and call it.
|
|
# '_q_lookup()' translates an arbitrary string into an object
|
|
# that we continue traversing. (This is very handy; it lets
|
|
# you put user-space objects into your URL-space, eliminating
|
|
# the need for digging ID strings out of a query, or checking
|
|
# PATHINFO after Quixote's done with it. But it is a
|
|
# compromise to security: it opens up the traversal algorithm
|
|
# to arbitrary names not listed in _q_exports!) If
|
|
# _q_lookup() doesn't exist or is None, a TraversalError is
|
|
# raised.
|
|
|
|
# Check if component is in _q_exports. The elements in
|
|
# _q_exports can be strings or 2-tuples mapping external names
|
|
# to internal names.
|
|
if component in container._q_exports or component == '_q_index':
|
|
internal_name = component
|
|
else:
|
|
# check for an explicit external to internal mapping
|
|
for value in container._q_exports:
|
|
if type(value) is tuple:
|
|
if value[0] == component:
|
|
internal_name = value[1]
|
|
break
|
|
else:
|
|
internal_name = None
|
|
|
|
if internal_name is None:
|
|
# Component is not in exports list.
|
|
object = None
|
|
if hasattr(container, "_q_lookup"):
|
|
object = container._q_lookup(request, component)
|
|
elif hasattr(container, "_q_getname"):
|
|
warnings.warn("_q_getname() on %s used; should "
|
|
"be replaced by _q_lookup()" % type(container))
|
|
object = container._q_getname(request, component)
|
|
if object is None:
|
|
raise errors.TraversalError(
|
|
private_msg="object %r has no attribute %r" % (
|
|
container,
|
|
component))
|
|
|
|
# From here on, you can assume that the internal_name is not None
|
|
elif hasattr(container, internal_name):
|
|
# attribute is in _q_exports and exists
|
|
object = getattr(container, internal_name)
|
|
|
|
elif internal_name == '_q_index':
|
|
if hasattr(container, "_q_lookup"):
|
|
object = container._q_lookup(request, "")
|
|
else:
|
|
raise errors.AccessError(
|
|
private_msg=("_q_index not found in %r" % container))
|
|
|
|
elif hasattr(container, "_q_resolve"):
|
|
object = container._q_resolve(internal_name)
|
|
if object is None:
|
|
raise RuntimeError("component listed in _q_exports, "
|
|
"but not returned by _q_resolve(%r)"
|
|
% internal_name)
|
|
else:
|
|
# Set the object, so _q_resolve won't need to be called again.
|
|
setattr(container, internal_name, object)
|
|
|
|
elif type(container) is types.ModuleType:
|
|
# try importing it as a sub-module. If we get an ImportError
|
|
# here we don't catch it. It means that something that
|
|
# doesn't exist was exported or an exception was raised from
|
|
# deeper in the code.
|
|
mod_name = container.__name__ + '.' + internal_name
|
|
object = _get_module(mod_name)
|
|
|
|
else:
|
|
# a non-existent attribute is in _q_exports,
|
|
# and the container is not a module. Give up.
|
|
raise errors.TraversalError(
|
|
private_msg=("%r in _q_exports list, "
|
|
"but not found in %r" % (component,
|
|
container)))
|
|
|
|
namespace_stack.append(object)
|
|
return object
|
|
|
|
|
|
def isstring(x):
|
|
return isinstance(x, (str, htmltext))
|