summaryrefslogtreecommitdiff
path: root/cherrypy/_cpdispatch.py
diff options
context:
space:
mode:
Diffstat (limited to 'cherrypy/_cpdispatch.py')
-rw-r--r--cherrypy/_cpdispatch.py685
1 files changed, 0 insertions, 685 deletions
diff --git a/cherrypy/_cpdispatch.py b/cherrypy/_cpdispatch.py
deleted file mode 100644
index 2cb03c7e..00000000
--- a/cherrypy/_cpdispatch.py
+++ /dev/null
@@ -1,685 +0,0 @@
-"""CherryPy dispatchers.
-
-A 'dispatcher' is the object which looks up the 'page handler' callable
-and collects config for the current request based on the path_info, other
-request attributes, and the application architecture. The core calls the
-dispatcher as early as possible, passing it a 'path_info' argument.
-
-The default dispatcher discovers the page handler by matching path_info
-to a hierarchical arrangement of objects, starting at request.app.root.
-"""
-
-import string
-import sys
-import types
-try:
- classtype = (type, types.ClassType)
-except AttributeError:
- classtype = type
-
-import cherrypy
-
-
-class PageHandler(object):
-
- """Callable which sets response.body."""
-
- def __init__(self, callable, *args, **kwargs):
- self.callable = callable
- self.args = args
- self.kwargs = kwargs
-
- def get_args(self):
- return cherrypy.serving.request.args
-
- def set_args(self, args):
- cherrypy.serving.request.args = args
- return cherrypy.serving.request.args
-
- args = property(
- get_args,
- set_args,
- doc="The ordered args should be accessible from post dispatch hooks"
- )
-
- def get_kwargs(self):
- return cherrypy.serving.request.kwargs
-
- def set_kwargs(self, kwargs):
- cherrypy.serving.request.kwargs = kwargs
- return cherrypy.serving.request.kwargs
-
- kwargs = property(
- get_kwargs,
- set_kwargs,
- doc="The named kwargs should be accessible from post dispatch hooks"
- )
-
- def __call__(self):
- try:
- return self.callable(*self.args, **self.kwargs)
- except TypeError:
- x = sys.exc_info()[1]
- try:
- test_callable_spec(self.callable, self.args, self.kwargs)
- except cherrypy.HTTPError:
- raise sys.exc_info()[1]
- except:
- raise x
- raise
-
-
-def test_callable_spec(callable, callable_args, callable_kwargs):
- """
- Inspect callable and test to see if the given args are suitable for it.
-
- When an error occurs during the handler's invoking stage there are 2
- erroneous cases:
- 1. Too many parameters passed to a function which doesn't define
- one of *args or **kwargs.
- 2. Too little parameters are passed to the function.
-
- There are 3 sources of parameters to a cherrypy handler.
- 1. query string parameters are passed as keyword parameters to the
- handler.
- 2. body parameters are also passed as keyword parameters.
- 3. when partial matching occurs, the final path atoms are passed as
- positional args.
- Both the query string and path atoms are part of the URI. If they are
- incorrect, then a 404 Not Found should be raised. Conversely the body
- parameters are part of the request; if they are invalid a 400 Bad Request.
- """
- show_mismatched_params = getattr(
- cherrypy.serving.request, 'show_mismatched_params', False)
- try:
- (args, varargs, varkw, defaults) = getargspec(callable)
- except TypeError:
- if isinstance(callable, object) and hasattr(callable, '__call__'):
- (args, varargs, varkw,
- defaults) = getargspec(callable.__call__)
- else:
- # If it wasn't one of our own types, re-raise
- # the original error
- raise
-
- if args and args[0] == 'self':
- args = args[1:]
-
- arg_usage = dict([(arg, 0,) for arg in args])
- vararg_usage = 0
- varkw_usage = 0
- extra_kwargs = set()
-
- for i, value in enumerate(callable_args):
- try:
- arg_usage[args[i]] += 1
- except IndexError:
- vararg_usage += 1
-
- for key in callable_kwargs.keys():
- try:
- arg_usage[key] += 1
- except KeyError:
- varkw_usage += 1
- extra_kwargs.add(key)
-
- # figure out which args have defaults.
- args_with_defaults = args[-len(defaults or []):]
- for i, val in enumerate(defaults or []):
- # Defaults take effect only when the arg hasn't been used yet.
- if arg_usage[args_with_defaults[i]] == 0:
- arg_usage[args_with_defaults[i]] += 1
-
- missing_args = []
- multiple_args = []
- for key, usage in arg_usage.items():
- if usage == 0:
- missing_args.append(key)
- elif usage > 1:
- multiple_args.append(key)
-
- if missing_args:
- # In the case where the method allows body arguments
- # there are 3 potential errors:
- # 1. not enough query string parameters -> 404
- # 2. not enough body parameters -> 400
- # 3. not enough path parts (partial matches) -> 404
- #
- # We can't actually tell which case it is,
- # so I'm raising a 404 because that covers 2/3 of the
- # possibilities
- #
- # In the case where the method does not allow body
- # arguments it's definitely a 404.
- message = None
- if show_mismatched_params:
- message = "Missing parameters: %s" % ",".join(missing_args)
- raise cherrypy.HTTPError(404, message=message)
-
- # the extra positional arguments come from the path - 404 Not Found
- if not varargs and vararg_usage > 0:
- raise cherrypy.HTTPError(404)
-
- body_params = cherrypy.serving.request.body.params or {}
- body_params = set(body_params.keys())
- qs_params = set(callable_kwargs.keys()) - body_params
-
- if multiple_args:
- if qs_params.intersection(set(multiple_args)):
- # If any of the multiple parameters came from the query string then
- # it's a 404 Not Found
- error = 404
- else:
- # Otherwise it's a 400 Bad Request
- error = 400
-
- message = None
- if show_mismatched_params:
- message = "Multiple values for parameters: "\
- "%s" % ",".join(multiple_args)
- raise cherrypy.HTTPError(error, message=message)
-
- if not varkw and varkw_usage > 0:
-
- # If there were extra query string parameters, it's a 404 Not Found
- extra_qs_params = set(qs_params).intersection(extra_kwargs)
- if extra_qs_params:
- message = None
- if show_mismatched_params:
- message = "Unexpected query string "\
- "parameters: %s" % ", ".join(extra_qs_params)
- raise cherrypy.HTTPError(404, message=message)
-
- # If there were any extra body parameters, it's a 400 Not Found
- extra_body_params = set(body_params).intersection(extra_kwargs)
- if extra_body_params:
- message = None
- if show_mismatched_params:
- message = "Unexpected body parameters: "\
- "%s" % ", ".join(extra_body_params)
- raise cherrypy.HTTPError(400, message=message)
-
-
-try:
- import inspect
-except ImportError:
- test_callable_spec = lambda callable, args, kwargs: None
-else:
- getargspec = inspect.getargspec
- # Python 3 requires using getfullargspec if keyword-only arguments are present
- if hasattr(inspect, 'getfullargspec'):
- def getargspec(callable):
- return inspect.getfullargspec(callable)[:4]
-
-
-class LateParamPageHandler(PageHandler):
-
- """When passing cherrypy.request.params to the page handler, we do not
- want to capture that dict too early; we want to give tools like the
- decoding tool a chance to modify the params dict in-between the lookup
- of the handler and the actual calling of the handler. This subclass
- takes that into account, and allows request.params to be 'bound late'
- (it's more complicated than that, but that's the effect).
- """
-
- def _get_kwargs(self):
- kwargs = cherrypy.serving.request.params.copy()
- if self._kwargs:
- kwargs.update(self._kwargs)
- return kwargs
-
- def _set_kwargs(self, kwargs):
- cherrypy.serving.request.kwargs = kwargs
- self._kwargs = kwargs
-
- kwargs = property(_get_kwargs, _set_kwargs,
- doc='page handler kwargs (with '
- 'cherrypy.request.params copied in)')
-
-
-if sys.version_info < (3, 0):
- punctuation_to_underscores = string.maketrans(
- string.punctuation, '_' * len(string.punctuation))
-
- def validate_translator(t):
- if not isinstance(t, str) or len(t) != 256:
- raise ValueError(
- "The translate argument must be a str of len 256.")
-else:
- punctuation_to_underscores = str.maketrans(
- string.punctuation, '_' * len(string.punctuation))
-
- def validate_translator(t):
- if not isinstance(t, dict):
- raise ValueError("The translate argument must be a dict.")
-
-
-class Dispatcher(object):
-
- """CherryPy Dispatcher which walks a tree of objects to find a handler.
-
- The tree is rooted at cherrypy.request.app.root, and each hierarchical
- component in the path_info argument is matched to a corresponding nested
- attribute of the root object. Matching handlers must have an 'exposed'
- attribute which evaluates to True. The special method name "index"
- matches a URI which ends in a slash ("/"). The special method name
- "default" may match a portion of the path_info (but only when no longer
- substring of the path_info matches some other object).
-
- This is the default, built-in dispatcher for CherryPy.
- """
-
- dispatch_method_name = '_cp_dispatch'
- """
- The name of the dispatch method that nodes may optionally implement
- to provide their own dynamic dispatch algorithm.
- """
-
- def __init__(self, dispatch_method_name=None,
- translate=punctuation_to_underscores):
- validate_translator(translate)
- self.translate = translate
- if dispatch_method_name:
- self.dispatch_method_name = dispatch_method_name
-
- def __call__(self, path_info):
- """Set handler and config for the current request."""
- request = cherrypy.serving.request
- func, vpath = self.find_handler(path_info)
-
- if func:
- # Decode any leftover %2F in the virtual_path atoms.
- vpath = [x.replace("%2F", "/") for x in vpath]
- request.handler = LateParamPageHandler(func, *vpath)
- else:
- request.handler = cherrypy.NotFound()
-
- def find_handler(self, path):
- """Return the appropriate page handler, plus any virtual path.
-
- This will return two objects. The first will be a callable,
- which can be used to generate page output. Any parameters from
- the query string or request body will be sent to that callable
- as keyword arguments.
-
- The callable is found by traversing the application's tree,
- starting from cherrypy.request.app.root, and matching path
- components to successive objects in the tree. For example, the
- URL "/path/to/handler" might return root.path.to.handler.
-
- The second object returned will be a list of names which are
- 'virtual path' components: parts of the URL which are dynamic,
- and were not used when looking up the handler.
- These virtual path components are passed to the handler as
- positional arguments.
- """
- request = cherrypy.serving.request
- app = request.app
- root = app.root
- dispatch_name = self.dispatch_method_name
-
- # Get config for the root object/path.
- fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
- fullpath_len = len(fullpath)
- segleft = fullpath_len
- nodeconf = {}
- if hasattr(root, "_cp_config"):
- nodeconf.update(root._cp_config)
- if "/" in app.config:
- nodeconf.update(app.config["/"])
- object_trail = [['root', root, nodeconf, segleft]]
-
- node = root
- iternames = fullpath[:]
- while iternames:
- name = iternames[0]
- # map to legal Python identifiers (e.g. replace '.' with '_')
- objname = name.translate(self.translate)
-
- nodeconf = {}
- subnode = getattr(node, objname, None)
- pre_len = len(iternames)
- if subnode is None:
- dispatch = getattr(node, dispatch_name, None)
- if dispatch and hasattr(dispatch, '__call__') and not \
- getattr(dispatch, 'exposed', False) and \
- pre_len > 1:
- # Don't expose the hidden 'index' token to _cp_dispatch
- # We skip this if pre_len == 1 since it makes no sense
- # to call a dispatcher when we have no tokens left.
- index_name = iternames.pop()
- subnode = dispatch(vpath=iternames)
- iternames.append(index_name)
- else:
- # We didn't find a path, but keep processing in case there
- # is a default() handler.
- iternames.pop(0)
- else:
- # We found the path, remove the vpath entry
- iternames.pop(0)
- segleft = len(iternames)
- if segleft > pre_len:
- # No path segment was removed. Raise an error.
- raise cherrypy.CherryPyException(
- "A vpath segment was added. Custom dispatchers may only "
- + "remove elements. While trying to process "
- + "{0} in {1}".format(name, fullpath)
- )
- elif segleft == pre_len:
- # Assume that the handler used the current path segment, but
- # did not pop it. This allows things like
- # return getattr(self, vpath[0], None)
- iternames.pop(0)
- segleft -= 1
- node = subnode
-
- if node is not None:
- # Get _cp_config attached to this node.
- if hasattr(node, "_cp_config"):
- nodeconf.update(node._cp_config)
-
- # Mix in values from app.config for this path.
- existing_len = fullpath_len - pre_len
- if existing_len != 0:
- curpath = '/' + '/'.join(fullpath[0:existing_len])
- else:
- curpath = ''
- new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
- for seg in new_segs:
- curpath += '/' + seg
- if curpath in app.config:
- nodeconf.update(app.config[curpath])
-
- object_trail.append([name, node, nodeconf, segleft])
-
- def set_conf():
- """Collapse all object_trail config into cherrypy.request.config.
- """
- base = cherrypy.config.copy()
- # Note that we merge the config from each node
- # even if that node was None.
- for name, obj, conf, segleft in object_trail:
- base.update(conf)
- if 'tools.staticdir.dir' in conf:
- base['tools.staticdir.section'] = '/' + \
- '/'.join(fullpath[0:fullpath_len - segleft])
- return base
-
- # Try successive objects (reverse order)
- num_candidates = len(object_trail) - 1
- for i in range(num_candidates, -1, -1):
-
- name, candidate, nodeconf, segleft = object_trail[i]
- if candidate is None:
- continue
-
- # Try a "default" method on the current leaf.
- if hasattr(candidate, "default"):
- defhandler = candidate.default
- if getattr(defhandler, 'exposed', False):
- # Insert any extra _cp_config from the default handler.
- conf = getattr(defhandler, "_cp_config", {})
- object_trail.insert(
- i + 1, ["default", defhandler, conf, segleft])
- request.config = set_conf()
- # See https://github.com/cherrypy/cherrypy/issues/613
- request.is_index = path.endswith("/")
- return defhandler, fullpath[fullpath_len - segleft:-1]
-
- # Uncomment the next line to restrict positional params to
- # "default".
- # if i < num_candidates - 2: continue
-
- # Try the current leaf.
- if getattr(candidate, 'exposed', False):
- request.config = set_conf()
- if i == num_candidates:
- # We found the extra ".index". Mark request so tools
- # can redirect if path_info has no trailing slash.
- request.is_index = True
- else:
- # We're not at an 'index' handler. Mark request so tools
- # can redirect if path_info has NO trailing slash.
- # Note that this also includes handlers which take
- # positional parameters (virtual paths).
- request.is_index = False
- return candidate, fullpath[fullpath_len - segleft:-1]
-
- # We didn't find anything
- request.config = set_conf()
- return None, []
-
-
-class MethodDispatcher(Dispatcher):
-
- """Additional dispatch based on cherrypy.request.method.upper().
-
- Methods named GET, POST, etc will be called on an exposed class.
- The method names must be all caps; the appropriate Allow header
- will be output showing all capitalized method names as allowable
- HTTP verbs.
-
- Note that the containing class must be exposed, not the methods.
- """
-
- def __call__(self, path_info):
- """Set handler and config for the current request."""
- request = cherrypy.serving.request
- resource, vpath = self.find_handler(path_info)
-
- if resource:
- # Set Allow header
- avail = [m for m in dir(resource) if m.isupper()]
- if "GET" in avail and "HEAD" not in avail:
- avail.append("HEAD")
- avail.sort()
- cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
-
- # Find the subhandler
- meth = request.method.upper()
- func = getattr(resource, meth, None)
- if func is None and meth == "HEAD":
- func = getattr(resource, "GET", None)
- if func:
- # Grab any _cp_config on the subhandler.
- if hasattr(func, "_cp_config"):
- request.config.update(func._cp_config)
-
- # Decode any leftover %2F in the virtual_path atoms.
- vpath = [x.replace("%2F", "/") for x in vpath]
- request.handler = LateParamPageHandler(func, *vpath)
- else:
- request.handler = cherrypy.HTTPError(405)
- else:
- request.handler = cherrypy.NotFound()
-
-
-class RoutesDispatcher(object):
-
- """A Routes based dispatcher for CherryPy."""
-
- def __init__(self, full_result=False, **mapper_options):
- """
- Routes dispatcher
-
- Set full_result to True if you wish the controller
- and the action to be passed on to the page handler
- parameters. By default they won't be.
- """
- import routes
- self.full_result = full_result
- self.controllers = {}
- self.mapper = routes.Mapper(**mapper_options)
- self.mapper.controller_scan = self.controllers.keys
-
- def connect(self, name, route, controller, **kwargs):
- self.controllers[name] = controller
- self.mapper.connect(name, route, controller=name, **kwargs)
-
- def redirect(self, url):
- raise cherrypy.HTTPRedirect(url)
-
- def __call__(self, path_info):
- """Set handler and config for the current request."""
- func = self.find_handler(path_info)
- if func:
- cherrypy.serving.request.handler = LateParamPageHandler(func)
- else:
- cherrypy.serving.request.handler = cherrypy.NotFound()
-
- def find_handler(self, path_info):
- """Find the right page handler, and set request.config."""
- import routes
-
- request = cherrypy.serving.request
-
- config = routes.request_config()
- config.mapper = self.mapper
- if hasattr(request, 'wsgi_environ'):
- config.environ = request.wsgi_environ
- config.host = request.headers.get('Host', None)
- config.protocol = request.scheme
- config.redirect = self.redirect
-
- result = self.mapper.match(path_info)
-
- config.mapper_dict = result
- params = {}
- if result:
- params = result.copy()
- if not self.full_result:
- params.pop('controller', None)
- params.pop('action', None)
- request.params.update(params)
-
- # Get config for the root object/path.
- request.config = base = cherrypy.config.copy()
- curpath = ""
-
- def merge(nodeconf):
- if 'tools.staticdir.dir' in nodeconf:
- nodeconf['tools.staticdir.section'] = curpath or "/"
- base.update(nodeconf)
-
- app = request.app
- root = app.root
- if hasattr(root, "_cp_config"):
- merge(root._cp_config)
- if "/" in app.config:
- merge(app.config["/"])
-
- # Mix in values from app.config.
- atoms = [x for x in path_info.split("/") if x]
- if atoms:
- last = atoms.pop()
- else:
- last = None
- for atom in atoms:
- curpath = "/".join((curpath, atom))
- if curpath in app.config:
- merge(app.config[curpath])
-
- handler = None
- if result:
- controller = result.get('controller')
- controller = self.controllers.get(controller, controller)
- if controller:
- if isinstance(controller, classtype):
- controller = controller()
- # Get config from the controller.
- if hasattr(controller, "_cp_config"):
- merge(controller._cp_config)
-
- action = result.get('action')
- if action is not None:
- handler = getattr(controller, action, None)
- # Get config from the handler
- if hasattr(handler, "_cp_config"):
- merge(handler._cp_config)
- else:
- handler = controller
-
- # Do the last path atom here so it can
- # override the controller's _cp_config.
- if last:
- curpath = "/".join((curpath, last))
- if curpath in app.config:
- merge(app.config[curpath])
-
- return handler
-
-
-def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
- from cherrypy.lib import xmlrpcutil
-
- def xmlrpc_dispatch(path_info):
- path_info = xmlrpcutil.patched_path(path_info)
- return next_dispatcher(path_info)
- return xmlrpc_dispatch
-
-
-def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
- **domains):
- """
- Select a different handler based on the Host header.
-
- This can be useful when running multiple sites within one CP server.
- It allows several domains to point to different parts of a single
- website structure. For example::
-
- http://www.domain.example -> root
- http://www.domain2.example -> root/domain2/
- http://www.domain2.example:443 -> root/secure
-
- can be accomplished via the following config::
-
- [/]
- request.dispatch = cherrypy.dispatch.VirtualHost(
- **{'www.domain2.example': '/domain2',
- 'www.domain2.example:443': '/secure',
- })
-
- next_dispatcher
- The next dispatcher object in the dispatch chain.
- The VirtualHost dispatcher adds a prefix to the URL and calls
- another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
-
- use_x_forwarded_host
- If True (the default), any "X-Forwarded-Host"
- request header will be used instead of the "Host" header. This
- is commonly added by HTTP servers (such as Apache) when proxying.
-
- ``**domains``
- A dict of {host header value: virtual prefix} pairs.
- The incoming "Host" request header is looked up in this dict,
- and, if a match is found, the corresponding "virtual prefix"
- value will be prepended to the URL path before calling the
- next dispatcher. Note that you often need separate entries
- for "example.com" and "www.example.com". In addition, "Host"
- headers may contain the port number.
- """
- from cherrypy.lib import httputil
-
- def vhost_dispatch(path_info):
- request = cherrypy.serving.request
- header = request.headers.get
-
- domain = header('Host', '')
- if use_x_forwarded_host:
- domain = header("X-Forwarded-Host", domain)
-
- prefix = domains.get(domain, "")
- if prefix:
- path_info = httputil.urljoin(prefix, path_info)
-
- result = next_dispatcher(path_info)
-
- # Touch up staticdir config. See
- # https://github.com/cherrypy/cherrypy/issues/614.
- section = request.config.get('tools.staticdir.section')
- if section:
- section = section[len(prefix):]
- request.config['tools.staticdir.section'] = section
-
- return result
- return vhost_dispatch