import urllib from zope.component import getMultiAdapter from zope.deferredimport import deprecated from zope.interface import classProvides from zope.interface import implements from repoze.bfg.location import LocationProxy from repoze.bfg.location import lineage from repoze.bfg.lru import lru_cache from repoze.bfg.url import _urlsegment from repoze.bfg.interfaces import IContextURL from repoze.bfg.interfaces import ILocation from repoze.bfg.interfaces import ITraverser from repoze.bfg.interfaces import ITraverserFactory from repoze.bfg.interfaces import VH_ROOT_KEY deprecated( "('from repoze.bfg.traversal import model_url' is now " "deprecated; instead use 'from repoze.bfg.url import model_url')", model_url = "repoze.bfg.url:model_url", ) deprecated( "('from repoze.bfg.traversal import RoutesModelTraverser' is now " "deprecated; instead use 'from repoze.bfg.urldispatch " "import RoutesModelTraverser')", RoutesModelTraverser = "repoze.bfg.urldispatch:RoutesModelTraverser", ) # ``split_path`` wasn't actually ever an API but people were using it # anyway. I turned it into the ``traversal_path`` API in 0.6.5, and # generate the below deprecation to give folks a heads up. deprecated( "('from repoze.bfg.traversal import split_path' is now deprecated; " "instead use 'from repoze.bfg.traversal import traversal_path')", split_path = "repoze.bfg.traversal:traversal_path", ) def find_root(model): """ Find the root node in the graph to which ``model`` belongs. Note that ``model`` should be :term:`location`-aware. Note that the root node is available in the request object by accessing the ``request.root`` attribute. """ for location in lineage(model): if location.__parent__ is None: model = location break return model def find_model(model, path): """ Given a model object and a tuple representing a path (such as the return value of ``model_path``), return an context in this application's model graph at the specified path. The model passed in *must* be :term:`location`-aware. If the first element in the path tuple is the empty string (for example ``('', 'a', 'b', 'c')``, the path is considered absolute and the graph traversal will start at the graph root object. If the first element in the path tuple is not the empty string (for example ``('a', 'b', 'c')``), the path is considered relative and graph traversal will begin at the model object supplied to the function. No URL-quoting or UTF-8-encoding of individual path segments within the tuple is required (each segment may be any string or unicode object representing a model name). If an empty sequence is passed as ``path``, the ``model`` passed in itself will be returned. If the path cannot be resolved, a KeyError will be raised. .. note:: It is also permissible to pass a string to this function as the ``path``, as long as each Unicode path segment is encoded as UTF-8 and as long as each path segment is escaped via Python's ``urllib.quote``. For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or ``to%20the/La%20Pe%C3%B1a`` (relative). ``find_model`` will consider a string path absolute if it starts with the ``/`` character; it will consider the path relative to the ``model`` passed in if it does not start with the ``/`` character. If an empty string is passed as ``path``, the ``model`` passed in will be returned. .. note:: This function is the logical inverse of ``model_path``; it can resolve any path tuple generated by ``model_path``. """ if hasattr(path, '__iter__'): # it's a tuple or some other iterable # the traverser factory expects PATH_INFO to be a string, not # unicode and it expects path segments to be utf-8 and # urlencoded (it's the same traverser which accepts PATH_INFO # from user agents; user agents always send strings). path = [_urlsegment(name) for name in path] if path: path = '/'.join(path) or '/' else: path = '' if path.startswith('/'): model = find_root(model) ob, name, _ = ITraverserFactory(model)({'PATH_INFO':path}) if name: raise KeyError('%r has no subelement %s' % (ob, name)) return ob def find_interface(model, interface): """ Return the first object found in the parent chain of ``model`` which provides the interface ``interface``. Return ``None`` if no object providing ``interface`` can be found in the parent chain. The ``model`` passed in *must* be :term:`location`-aware. """ for location in lineage(model): if interface.providedBy(location): return location def model_path(model, *elements): """ Return a tuple representing the absolute physical path of the model object based on its position in the model graph, e.g ``('', 'foo', 'bar')``. Any positional arguments passed in as ``elements`` will be appended as elements in the tuple representing the the model path. For instance, if the model's path is ``('', 'foo', 'bar')`` and elements equals ``('a', 'b')``, the returned tuple will be ``('foo', 'bar', 'a', b')``. The ``model`` passed in *must* be :term:`location`-aware. The first element of this tuple will always be the empty string (a leading empty string element in a path tuple represents that the path is absolute). This function is the logical inverse of ``find_model``: it can be used to generate path references that can later be resolved via ``find_model``. .. note:: Each segment in the path tuple returned will equal the ``__name__`` attribute of the model it represents within the graph. Each of these segments *should* be a unicode or string object (as per the contract of :term:`location-awareness`). However, no conversion or safety checking of model names is performed. For instance, if one of the models in your graph has a ``__name__`` which (by error) is a dictionary, that dictionary will be placed in the path tuple; no warning or error will be given. A single exception to this rule exists: the :term:`root` model may have a ``__name__`` attribute of any value; the value of this attribute will always be ignored (and effectively replaced with ``''``) when the path is generated. """ path = [] for location in lineage(model): path.append(location.__name__) # replace root __name__ with the empty string path.pop() path.append('') path.reverse() path.extend(elements) return tuple(path) def virtual_root(model, request): """ Provided any model and a request object, return the model object representing the 'virtual root' of the current request. Using a virtual root in a traversal-based :mod:`repoze.bfg` application permits rooting, for example, the object at the traversal path ``/cms`` at ``http://example.com/`` instead of rooting it at ``http://example.com/cms/``. If the ``model`` passed in is a context obtained via :term:`traversal`, and if the ``HTTP_X_VHM_ROOT`` key is in the WSGI environment, the value of this key will be treated as a 'virtual root path': the :func:`repoze.bfg.traversal.find_model` API will be used to find the virtual root object using this path; if the object is found, it will found will be returned. If the ``%s`` key is is not present in the WSGI environment, the physical :term:`root` of the graph will be returned instead. Virtual roots are not useful at all in applications that use :term:`URL dispatch`. Contexts obtained via URL dispatch don't really support being virtually rooted (each URL dispatch context is both its own physical and virtual root). However if this API is called with a ``model`` argument which is a context obtained via URL dispatch, the model passed in will be returned unconditonally.""" urlgenerator = getMultiAdapter((model, request), IContextURL) return urlgenerator.virtual_root() @lru_cache(500) def traversal_path(path): """ Given a PATH_INFO string (slash-separated path elements), return a tuple representing that path which can be used to traverse a graph. The PATH_INFO is split on slashes, creating a list of segments. Each segment is URL-unquoted, and decoded into Unicode. Each segment is assumed to be encoded using the UTF-8 encoding (or a subset, such as ASCII); a TypeError is raised if a segment cannot be decoded. If a segment name is empty or if it is ``.``, it is ignored. If a segment name is ``..``, the previous segment is deleted, and the ``..`` is ignored. Examples: ``/`` () ``/foo/bar/baz`` (u'foo', u'bar', u'baz') ``foo/bar/baz`` (u'foo', u'bar', u'baz') ``/foo/bar/baz/`` (u'foo', u'bar', u'baz') ``/foo//bar//baz/`` (u'foo', u'bar', u'baz') ``/foo/bar/baz/..`` (u'foo', u'bar') ``/my%20archives/hello`` (u'my archives', u'hello') ``/archives/La%20Pe%C3%B1a`` (u'archives', u'') """ path = path.rstrip('/') path = path.lstrip('/') clean = [] for segment in path.split('/'): segment = urllib.unquote(segment) # deal with spaces in path segment if not segment or segment=='.': continue elif segment == '..': del clean[-1] else: try: segment = segment.decode('utf-8') except UnicodeDecodeError: raise TypeError('Could not decode path segment %r using the ' 'UTF-8 decoding scheme' % segment) clean.append(segment) return tuple(clean) _marker = object() class ModelGraphTraverser(object): classProvides(ITraverserFactory) implements(ITraverser) def __init__(self, root): self.root = root def __call__(self, environ, _marker=_marker): try: path = environ['PATH_INFO'] except KeyError: path = '/' try: vroot = environ[VH_ROOT_KEY] path = vroot + path except KeyError: pass path = traversal_path(path) ob = self.root name = '' locatable = ILocation.providedBy(ob) i = 1 for segment in path: if segment[:2] =='@@': return ob, segment[2:], list(path[i:]) try: getitem = ob.__getitem__ except AttributeError: return ob, segment, list(path[i:]) try: next = getitem(segment) except KeyError: return ob, segment, list(path[i:]) if locatable and (not ILocation.providedBy(next)): next = LocationProxy(next, ob, segment) ob = next i += 1 return ob, '', [] class TraversalContextURL(object): """ The IContextURL adapter used to generate URLs for a context object obtained via graph traversal""" implements(IContextURL) vroot_varname = VH_ROOT_KEY def __init__(self, context, request): self.context = context self.request = request def virtual_root(self): try: vroot_path = self.request.environ[self.vroot_varname] except KeyError: # shortcut instead of using find_root; we probably already # have it on the request try: return self.request.root except AttributeError: return find_root(self.context) return find_model(self.context, vroot_path) def __call__(self): """ Generate a URL based on the lineage of a model obtained via traversal. If any model in the context lineage has a unicode name, it will be converted to a UTF-8 string before being attached to the URL. When composing the path based on the model lineage, empty names in the model graph are ignored. If a ``HTTP_X_VHM_ROOT`` key is present in the WSGI environment, its value will be treated as a 'virtual root path': the path of the URL generated by this will be left-stripped of this virtual root path value. """ rpath = [] for location in lineage(self.context): name = location.__name__ if name: rpath.append(_urlsegment(name)) if rpath: path = '/' + '/'.join(reversed(rpath)) + '/' else: path = '/' request = self.request # if the path starts with the virtual root path, trim it out vroot_path = request.environ.get(self.vroot_varname) if vroot_path is not None: if path.startswith(vroot_path): path = path[len(vroot_path):] app_url = request.application_url # never ends in a slash return app_url + path