##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""This module implements a mix-in for traversable objects.

$Id$
"""

from urllib import quote

from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl import getSecurityManager
from AccessControl import Unauthorized
from AccessControl.ZopeGuards import guarded_getattr
from Acquisition import Acquired, aq_inner, aq_parent, aq_base
from zExceptions import NotFound
from ZODB.POSException import ConflictError
from OFS.interfaces import ITraversable
import webdav

from zope.interface import implements, Interface
from zope.component import queryMultiAdapter
from zope.traversing.interfaces import TraversalError
from zope.traversing.namespace import nsParse, namespaceLookup

_marker = object()


class Traversable:
    implements(ITraversable)

    security = ClassSecurityInfo()

    security.declarePublic('absolute_url')
    def absolute_url(self, relative=0):
        """Return the absolute URL of the object.

        This a canonical URL based on the object's physical
        containment path.  It is affected by the virtual host
        configuration, if any, and can be used by external
        agents, such as a browser, to address the object.

        If the relative argument is provided, with a true value, then
        the value of virtual_url_path() is returned.

        Some Products incorrectly use '/'+absolute_url(1) as an
        absolute-path reference.  This breaks in certain virtual
        hosting situations, and should be changed to use
        absolute_url_path() instead.
        """
        if relative:
            return self.virtual_url_path()

        spp = self.getPhysicalPath()
            
        try:
            toUrl = self.REQUEST.physicalPathToURL
        except AttributeError:
            return path2url(spp[1:])
        return toUrl(spp)

    security.declarePublic('absolute_url_path')
    def absolute_url_path(self):
        """Return the path portion of the absolute URL of the object.

        This includes the leading slash, and can be used as an
        'absolute-path reference' as defined in RFC 2396.
        """
        spp = self.getPhysicalPath()
        try:
            toUrl = self.REQUEST.physicalPathToURL
        except AttributeError:
            return path2url(spp) or '/'
        return toUrl(spp, relative=1) or '/'

    security.declarePublic('virtual_url_path')
    def virtual_url_path(self):
        """Return a URL for the object, relative to the site root.

        If a virtual host is configured, the URL is a path relative to
        the virtual host's root object.  Otherwise, it is the physical
        path.  In either case, the URL does not begin with a slash.
        """
        spp = self.getPhysicalPath()
        try:
            toVirt = self.REQUEST.physicalPathToVirtualPath
        except AttributeError:
            return path2url(spp[1:])
        return path2url(toVirt(spp))

    security.declarePrivate('getPhysicalRoot')
    getPhysicalRoot=Acquired

    security.declarePublic('getPhysicalPath')
    def getPhysicalPath(self):
        """Get the physical path of the object.

        Returns a path (an immutable sequence of strings) that can be used to
        access this object again later, for example in a copy/paste operation.
        getPhysicalRoot() and getPhysicalPath() are designed to operate
        together.
        """
        path = (self.getId(),)

        p = aq_parent(aq_inner(self))
        if p is not None:
            path = p.getPhysicalPath() + path

        return path

    security.declarePrivate('unrestrictedTraverse')
    def unrestrictedTraverse(self, path, default=_marker, restricted=False):
        """Lookup an object by path.

        path -- The path to the object. May be a sequence of strings or a slash
        separated string. If the path begins with an empty path element
        (i.e., an empty string or a slash) then the lookup is performed
        from the application root. Otherwise, the lookup is relative to
        self. Two dots (..) as a path element indicates an upward traversal
        to the acquisition parent.

        default -- If provided, this is the value returned if the path cannot
        be traversed for any reason (i.e., no object exists at that path or
        the object is inaccessible).

        restricted -- If false (default) then no security checking is performed.
        If true, then all of the objects along the path are validated with
        the security machinery. Usually invoked using restrictedTraverse().
        """
        if not path:
            return self

        if isinstance(path, str):
            # Unicode paths are not allowed
            path = path.split('/')
        else:
            path = list(path)

        REQUEST = {'TraversalRequestNameStack': path}
        path.reverse()
        path_pop = path.pop

        if len(path) > 1 and not path[0]:
            # Remove trailing slash
            path_pop(0)

        if restricted:
            validate = getSecurityManager().validate

        if not path[-1]:
            # If the path starts with an empty string, go to the root first.
            path_pop()
            obj = self.getPhysicalRoot()
            if restricted and not validate(None, None, None, obj):
                raise Unauthorized(name)
        else:
            obj = self

        resource = _marker
        try:
            while path:
                name = path_pop()
                __traceback_info__ = path, name

                if name[0] == '_':
                    # Never allowed in a URL.
                    raise NotFound, name

                if name == '..':
                    next = aq_parent(obj)
                    if next is not None:
                        if restricted and not validate(obj, obj, name, next):
                            raise Unauthorized(name)
                        obj = next
                        continue

                bobo_traverse = getattr(obj, '__bobo_traverse__', None)
                try:
                    if name and name[:1] in '@+':
                        # Process URI segment parameters.
                        ns, nm = nsParse(name)
                        if ns:
                            try:
                                next = namespaceLookup(
                                    ns, nm, obj, self.REQUEST).__of__(obj)
                                if restricted and not validate(
                                    obj, obj, name, next):
                                    raise Unauthorized(name)
                            except TraversalError:
                                raise AttributeError(name)
                    elif bobo_traverse is not None:
                        next = bobo_traverse(REQUEST, name)
                        if restricted:
                            if aq_base(next) is not next:
                                # The object is wrapped, so the acquisition
                                # context is the container.
                                container = aq_parent(aq_inner(next))
                            elif getattr(next, 'im_self', None) is not None:
                                # Bound method, the bound instance
                                # is the container
                                container = next.im_self
                            elif getattr(aq_base(obj), name, _marker) is next:
                                # Unwrapped direct attribute of the object so
                                # object is the container
                                container = obj
                            else:
                                # Can't determine container
                                container = None
                            # If next is a simple unwrapped property, its
                            # parentage is indeterminate, but it may have
                            # been acquired safely. In this case validate
                            # will raise an error, and we can explicitly
                            # check that our value was acquired safely.
                            try:
                                ok = validate(obj, container, name, next)
                            except Unauthorized:
                                ok = False
                            if not ok:
                                if (container is not None or
                                    guarded_getattr(obj, name, _marker)
                                        is not next):
                                    raise Unauthorized(name)
                    else:
                        if getattr(aq_base(obj), name, _marker) is not _marker:
                            if restricted:
                                next = guarded_getattr(obj, name)
                            else:
                                next = getattr(obj, name)
                        else:
                            try:
                                next = obj[name]
                                # The item lookup may return a NullResource,
                                # if this is the case we save it and return it
                                # if all other lookups fail.
                                if isinstance(next,
                                              webdav.NullResource.NullResource):
                                    resource = next
                                    raise KeyError(name)
                            except AttributeError:
                                # Raise NotFound for easier debugging
                                # instead of AttributeError: __getitem__
                                raise NotFound(name)
                            if restricted and not validate(
                                obj, obj, None, next):
                                raise Unauthorized(name)

                except (AttributeError, NotFound, KeyError), e:
                    # Try to look for a view
                    next = queryMultiAdapter((obj, self.REQUEST),
                                             Interface, name)

                    if next is not None:
                        next = next.__of__(obj)
                        if restricted and not validate(obj, obj, name, next):
                            raise Unauthorized(name)
                    elif bobo_traverse is not None:
                        # Attribute lookup should not be done after
                        # __bobo_traverse__:
                        raise e
                    else:
                        # No view, try acquired attributes
                        try:
                            if restricted:
                                next = guarded_getattr(obj, name, _marker)
                            else:
                                next = getattr(obj, name, _marker)
                        except AttributeError:
                            raise e
                        if next is _marker:
                            # If we have a NullResource from earlier use it.
                            next = resource
                            if next is _marker:
                                # Nothing found re-raise error
                                raise e

                obj = next

            return obj

        except ConflictError:
            raise
        except:
            if default is not _marker:
                return default
            else:
                raise

    security.declarePublic('restrictedTraverse')
    def restrictedTraverse(self, path, default=_marker):
        # Trusted code traversal code, always enforces securitys
        return self.unrestrictedTraverse(path, default, restricted=True)

InitializeClass(Traversable)


def path2url(path):
    return '/'.join(map(quote, path))
