Log message for revision 67730: Merge of the traversal refactoring branch.
Changed: U Zope/trunk/doc/CHANGES.txt U Zope/trunk/lib/python/OFS/Traversable.py _U Zope/trunk/lib/python/Products/ U Zope/trunk/lib/python/Products/PageTemplates/Expressions.py U Zope/trunk/lib/python/ZPublisher/BaseRequest.py -=- Modified: Zope/trunk/doc/CHANGES.txt =================================================================== --- Zope/trunk/doc/CHANGES.txt 2006-04-28 18:07:25 UTC (rev 67729) +++ Zope/trunk/doc/CHANGES.txt 2006-04-28 18:07:58 UTC (rev 67730) @@ -49,6 +49,17 @@ Features added + - The traversal has been refactored to take heed of Zope3s + IPublishTraverse adapter interfaces. The ZCML directives + five:traversable and five:defaultViewable are therefore no + longer needed, as everything now is five:traversable and + five:defaultViewable. + + There was a bug in earlier versions of Five that allowed you + to do custom publishing traversal with ITraversable adapters. + This bug has been corrected. Anybody using ITraversable + adapters need to convert them to IPublishTraversal adapters. + - Testing.makerequest: Added an 'environ' argument so clients can use mappings other than os.environ. Modified: Zope/trunk/lib/python/OFS/Traversable.py =================================================================== --- Zope/trunk/lib/python/OFS/Traversable.py 2006-04-28 18:07:25 UTC (rev 67729) +++ Zope/trunk/lib/python/OFS/Traversable.py 2006-04-28 18:07:58 UTC (rev 67730) @@ -25,9 +25,14 @@ from Acquisition import Acquired, aq_inner, aq_parent, aq_base from zExceptions import NotFound from ZODB.POSException import ConflictError -from zope.interface import implements +from zope.interface import implements, Interface from interfaces import ITraversable +from zope.app.traversing.interfaces import ITraversable as IZope3Traversable +from zope.component import queryMultiAdapter +from zope.app.traversing.interfaces import TraversalError +from zope.app.traversing.namespace import nsParse +from zope.app.traversing.namespace import namespaceLookup _marker = object() @@ -59,6 +64,7 @@ return self.virtual_url_path() spp = self.getPhysicalPath() + try: toUrl = self.REQUEST.physicalPathToURL except AttributeError: @@ -133,7 +139,6 @@ 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 @@ -188,7 +193,19 @@ continue bobo_traverse = _getattr(obj, '__bobo_traverse__', _none) - if bobo_traverse is not _none: + 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 securityManager.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: @@ -228,11 +245,20 @@ next = _getattr(obj, name, marker) if next is marker: try: - next=obj[name] - except AttributeError: - # Raise NotFound for easier debugging - # instead of AttributeError: __getitem__ - raise NotFound, name + try: + next=obj[name] + except AttributeError: + # Raise NotFound for easier debugging + # instead of AttributeError: __getitem__ + raise NotFound, name + except (NotFound, KeyError): + # Try to look for a view + next = queryMultiAdapter((obj, self.REQUEST), + Interface, name) + if next is None: + # Didn't find one, reraise the error: + raise + next = next.__of__(obj) if restricted and not securityManager.validate( obj, obj, _none, next): raise Unauthorized, name Property changes on: Zope/trunk/lib/python/Products ___________________________________________________________________ Name: svn:externals - # updated to get a more recent working Five snapshot (what will be Five 1.5 # and actually included with Zope 2.10 Five -r67270 svn://svn.zope.org/repos/main/Products.Five/trunk + # updated to get a more recent working Five snapshot (what will be Five 1.5 # and actually included with Zope 2.10 Five -r67728 svn://svn.zope.org/repos/main/Products.Five/trunk Modified: Zope/trunk/lib/python/Products/PageTemplates/Expressions.py =================================================================== --- Zope/trunk/lib/python/Products/PageTemplates/Expressions.py 2006-04-28 18:07:25 UTC (rev 67729) +++ Zope/trunk/lib/python/Products/PageTemplates/Expressions.py 2006-04-28 18:07:58 UTC (rev 67730) @@ -248,12 +248,28 @@ def __repr__(self): return 'not:%s' % `self._s` +from zope.interface import Interface, implements +from zope.component import queryMultiAdapter +from zope.app.traversing.namespace import nsParse +from zope.app.traversing.namespace import namespaceLookup +from zope.app.traversing.interfaces import TraversalError +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.app.publication.browser import setDefaultSkin + +class FakeRequest(dict): + implements(IBrowserRequest) + + def getURL(self): + return "http://codespeak.net/z3/five" + def restrictedTraverse(object, path, securityManager, get=getattr, has=hasattr, N=None, M=[], TupleType=type(()) ): - REQUEST = {'path': path} + REQUEST = FakeRequest() + REQUEST['path'] = path REQUEST['TraversalRequestNameStack'] = path = path[:] # Copy! + setDefaultSkin(REQUEST) path.reverse() validate = securityManager.validate __traceback_info__ = REQUEST @@ -282,7 +298,20 @@ continue t=get(object, '__bobo_traverse__', N) - if t is not N: + if name and name[:1] in '@+': + import pdb + pdb.set_trace() + # Process URI segment parameters. + ns, nm = nsParse(name) + if ns: + try: + o = namespaceLookup(ns, nm, object, + REQUEST).__of__(object) + if not validate(object, object, name, o): + raise Unauthorized, name + except TraversalError: + raise AttributeError(name) + elif t is not N: o=t(REQUEST, name) container = None @@ -305,7 +334,16 @@ # XXX maybe in Python 2.2 we can just check whether # the object has the attribute "__getitem__" # instead of blindly catching exceptions. - o = object[name] + try: + o = object[name] + except (AttributeError, KeyError): + # Try to look for a view + o = queryMultiAdapter((object, REQUEST), + Interface, name) + if o is None: + # Didn't find one, reraise the error: + raise + o = o.__of__(object) except AttributeError, exc: if str(exc).find('__getitem__') >= 0: # The object does not support the item interface. Modified: Zope/trunk/lib/python/ZPublisher/BaseRequest.py =================================================================== --- Zope/trunk/lib/python/ZPublisher/BaseRequest.py 2006-04-28 18:07:25 UTC (rev 67729) +++ Zope/trunk/lib/python/ZPublisher/BaseRequest.py 2006-04-28 18:07:58 UTC (rev 67730) @@ -16,10 +16,22 @@ """ from urllib import quote import xmlrpc -from zExceptions import Forbidden +from zExceptions import Forbidden, Unauthorized, NotFound +from zope.interface import implements, providedBy, Interface +from zope.component import queryMultiAdapter +from zope.component import getSiteManager +from zope.component.interfaces import ComponentLookupError from zope.event import notify from zope.app.publication.interfaces import EndRequestEvent +from zope.app.publisher.browser import queryDefaultViewName +from zope.publisher.interfaces import IPublishTraverse +from zope.component.interfaces import IDefaultViewName +from zope.publisher.interfaces.browser import IBrowserPublisher +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.app.traversing.interfaces import TraversalError +from zope.app.traversing.namespace import nsParse +from zope.app.traversing.namespace import namespaceLookup UNSPECIFIED_ROLES='' @@ -45,7 +57,87 @@ def getRoles(container, name, value, default): return getattr(value, '__roles__', default) +class DefaultPublishTraverse(object): + implements(IBrowserPublisher) + + def __init__(self, context, request): + self.context = context + self.request = request + + def publishTraverse(self, request, name): + object = self.context + URL=request['URL'] + + if name[:1]=='_': + raise Forbidden("Object name begins with an underscore at: %s" % URL) + + try: + if hasattr(object,'__bobo_traverse__'): + subobject=object.__bobo_traverse__(request, name) + if type(subobject) is type(()) and len(subobject) > 1: + # Add additional parents into the path + # XXX This needs handling. Check the publish refactor branch... + parents[-1:] = list(subobject[:-1]) + object, subobject = subobject[-2:] + else: + try: + subobject=getattr(object, name) + except AttributeError: + subobject=object[name] + + except (AttributeError, KeyError, NotFound): + # Find a view even if it doesn't start with @@, but only + # If nothing else could be found + subobject = queryMultiAdapter((object, request), Interface, name) + if subobject is not None: + # OFS.Application.__bobo_traverse__ calls + # REQUEST.RESPONSE.notFoundError which sets the HTTP + # status code to 404 + request.RESPONSE.setStatus(200) + # We don't need to do the docstring security check + # for views, so lets skip it and return the object here. + return subobject.__of__(object) + raise + + # Ensure that the object has a docstring, or that the parent + # object has a pseudo-docstring for the object. Objects that + # have an empty or missing docstring are not published. + doc = getattr(subobject, '__doc__', None) + if doc is None: + doc = getattr(object, '%s__doc__' % name, None) + if not doc: + raise Forbidden( + "The object at %s has an empty or missing " \ + "docstring. Objects must have a docstring to be " \ + "published." % URL + ) + + # Hack for security: in Python 2.2.2, most built-in types + # gained docstrings that they didn't have before. That caused + # certain mutable types (dicts, lists) to become publishable + # when they shouldn't be. The following check makes sure that + # the right thing happens in both 2.2.2+ and earlier versions. + + if not typeCheck(subobject): + raise Forbidden( + "The object at %s is not publishable." % URL + ) + + return subobject + + def browserDefault(self, request): + if hasattr(self.context, '__browser_default__'): + return self.context.__browser_default__(request) + # Zope 3.2 still uses IDefaultView name when it + # registeres default views, even though it's + # deprecated. So we handle that here: + default_name = queryDefaultViewName(self.context, request) + if default_name is not None: + return self.context, (default_name,) + return self.context, () + + _marker=[] class BaseRequest: """Provide basic ZPublisher request management @@ -184,6 +276,35 @@ __repr__=__str__ + def traverseName(self, ob, name): + if name and name[:1] in '@+': + # Process URI segment parameters. + ns, nm = nsParse(name) + if ns: + try: + ob2 = namespaceLookup(ns, nm, ob, self) + except TraversalError: + raise KeyError(ob, name) + + return ob2.__of__(ob) + + if name == '.': + return ob + + if IPublishTraverse.providedBy(ob): + ob2 = ob.publishTraverse(self, name) + else: + adapter = queryMultiAdapter((ob, self), IPublishTraverse) + if adapter is None: + ## Zope2 doesn't set up its own adapters in a lot of cases + ## so we will just use a default adapter. + adapter = DefaultPublishTraverse(ob, self) + + ob2 = adapter.publishTraverse(self, name) + + return ob2 + + def traverse(self, path, response=None, validated_hook=None): """Traverse the object space @@ -193,7 +314,6 @@ request=self request_get=request.get if response is None: response=self.response - debug_mode=response.debug_mode # remember path for later use browser_path = path @@ -235,14 +355,14 @@ object=parents[-1] del parents[:] - roles = getRoles(None, None, object, UNSPECIFIED_ROLES) + self.roles = getRoles(None, None, object, UNSPECIFIED_ROLES) # if the top object has a __bobo_traverse__ method, then use it # to possibly traverse to an alternate top-level object. if hasattr(object,'__bobo_traverse__'): try: object=object.__bobo_traverse__(request) - roles = getRoles(None, None, object, UNSPECIFIED_ROLES) + self.roles = getRoles(None, None, object, UNSPECIFIED_ROLES) except: pass if not path and not method: @@ -277,125 +397,101 @@ path = request.path = request['TraversalRequestNameStack'] # Check for method: if path: - entry_name = path.pop() - elif hasattr(object, '__browser_default__'): - # If we have reached the end of the path. We look to see - # if the object implements __browser_default__. If so, we - # call it to let the object tell us how to publish it - # __browser_default__ returns the object to be published + entry_name = path.pop() + else: + # If we have reached the end of the path, we look to see + # if we can find IBrowserPublisher.browserDefault. If so, + # we call it to let the object tell us how to publish it + # BrowserDefault returns the object to be published # (usually self) and a sequence of names to traverse to - # find the method to be published. (Casey) - request._hacked_path=1 - object, default_path = object.__browser_default__(request) - if len(default_path) > 1: - path = list(default_path) - method = path.pop() - request['TraversalRequestNameStack'] = path - continue + # find the method to be published. + if (IBrowserPublisher.providedBy(object) or + IDefaultViewName.providedBy(object)): + adapter = object else: - entry_name = default_path[0] - elif (method and hasattr(object,method) - and entry_name != method - and getattr(object, method) is not None): - request._hacked_path=1 - entry_name = method - method = 'index_html' - else: - if (hasattr(object, '__call__')): - roles = getRoles(object, '__call__', object.__call__, - roles) - if request._hacked_path: - i=URL.rfind('/') - if i > 0: response.setBase(URL[:i]) - break + adapter = queryMultiAdapter((object, self), + IBrowserPublisher) + if adapter is None: + # Zope2 doesn't set up its own adapters in a lot + # of cases so we will just use a default adapter. + adapter = DefaultPublishTraverse(object, self) + + newobject, default_path = adapter.browserDefault(self) + if default_path or newobject is not object: + object = newobject + request._hacked_path=1 + if len(default_path) > 1: + path = list(default_path) + method = path.pop() + request['TraversalRequestNameStack'] = path + continue + else: + entry_name = default_path[0] + elif (method and hasattr(object,method) + and entry_name != method + and getattr(object, method) is not None): + request._hacked_path=1 + entry_name = method + method = 'index_html' + else: + if hasattr(object, '__call__'): + self.roles = getRoles(object, '__call__', object.__call__, + self.roles) + if request._hacked_path: + i=URL.rfind('/') + if i > 0: response.setBase(URL[:i]) + break step = quote(entry_name) _steps.append(step) request['URL'] = URL = '%s/%s' % (request['URL'], step) - got = 0 - if entry_name[:1]=='_': - if debug_mode: + + try: + subobject = self.traverseName(object, entry_name) + if (hasattr(object,'__bobo_traverse__') or + hasattr(object, entry_name)): + check_name = entry_name + else: + check_name = None + + self.roles = getRoles( + object, check_name, subobject, + self.roles) + object = subobject + except (KeyError, AttributeError): + if response.debug_mode: return response.debugError( - "Object name begins with an underscore at: %s" % URL) - else: return response.forbiddenError(entry_name) + "Cannot locate object at: %s" % URL) + else: + return response.notFoundError(URL) + except Forbidden, e: + if self.response.debug_mode: + return response.debugError(e.args) + else: + return response.forbiddenError(entry_name) + - if hasattr(object,'__bobo_traverse__'): - try: - subobject=object.__bobo_traverse__(request,entry_name) - if type(subobject) is type(()) and len(subobject) > 1: - # Add additional parents into the path - parents[-1:] = list(subobject[:-1]) - object, subobject = subobject[-2:] - except (AttributeError, KeyError): - if debug_mode: - return response.debugError( - "Cannot locate object at: %s" % URL) - else: - return response.notFoundError(URL) - else: - try: - # Note - no_acquire_flag is necessary to support - # things like DAV. We have to make sure - # that the target object is not acquired - # if the request_method is other than GET - # or POST. Otherwise, you could never use - # PUT to add a new object named 'test' if - # an object 'test' existed above it in the - # heirarchy -- you'd always get the - # existing object :( - - if (no_acquire_flag and len(path) == 0 and - hasattr(object, 'aq_base')): - if hasattr(object.aq_base, entry_name): - subobject=getattr(object, entry_name) - else: raise AttributeError, entry_name - else: subobject=getattr(object, entry_name) - except AttributeError: - got=1 - try: subobject=object[entry_name] - except (KeyError, IndexError, - TypeError, AttributeError): - if debug_mode: - return response.debugError( - "Cannot locate object at: %s" % URL) - else: - return response.notFoundError(URL) - - # Ensure that the object has a docstring, or that the parent - # object has a pseudo-docstring for the object. Objects that - # have an empty or missing docstring are not published. - doc = getattr(subobject, '__doc__', None) - if doc is None: - doc = getattr(object, '%s__doc__' % entry_name, None) - if not doc: - return response.debugError( - "The object at %s has an empty or missing " \ - "docstring. Objects must have a docstring to be " \ - "published." % URL - ) - - # Hack for security: in Python 2.2.2, most built-in types - # gained docstrings that they didn't have before. That caused - # certain mutable types (dicts, lists) to become publishable - # when they shouldn't be. The following check makes sure that - # the right thing happens in both 2.2.2+ and earlier versions. - - if not typeCheck(subobject): - return response.debugError( - "The object at %s is not publishable." % URL - ) - - roles = getRoles( - object, (not got) and entry_name or None, subobject, - roles) - - # Promote subobject to object - object=subobject parents.append(object) steps.append(entry_name) finally: parents.reverse() - + + # Note - no_acquire_flag is necessary to support + # things like DAV. We have to make sure + # that the target object is not acquired + # if the request_method is other than GET + # or POST. Otherwise, you could never use + # PUT to add a new object named 'test' if + # an object 'test' existed above it in the + # heirarchy -- you'd always get the + # existing object :( + if (no_acquire_flag and + hasattr(parents[1], 'aq_base') and + not hasattr(parents[1],'__bobo_traverse__')): + if not (hasattr(parents[1].aq_base, entry_name) or + parents[1].aq_base.has_key(entry_name)): + raise AttributeError, entry_name + # After traversal post traversal hooks aren't available anymore del self._post_traverse @@ -427,25 +523,25 @@ auth=request._auth - if v is old_validation and roles is UNSPECIFIED_ROLES: + if v is old_validation and self.roles is UNSPECIFIED_ROLES: # No roles, so if we have a named group, get roles from # group keys - if hasattr(groups,'keys'): roles=groups.keys() + if hasattr(groups,'keys'): self.roles=groups.keys() else: try: groups=groups() except: pass - try: roles=groups.keys() + try: self.roles=groups.keys() except: pass if groups is None: # Public group, hack structures to get it to validate - roles=None + self.roles=None auth='' if v is old_validation: - user=old_validation(groups, request, auth, roles) - elif roles is UNSPECIFIED_ROLES: user=v(request, auth) - else: user=v(request, auth, roles) + user=old_validation(groups, request, auth, self.roles) + elif self.roles is UNSPECIFIED_ROLES: user=v(request, auth) + else: user=v(request, auth, self.roles) while user is None and i < last_parent_index: parent=parents[i] @@ -456,11 +552,11 @@ if hasattr(groups,'validate'): v=groups.validate else: v=old_validation if v is old_validation: - user=old_validation(groups, request, auth, roles) - elif roles is UNSPECIFIED_ROLES: user=v(request, auth) - else: user=v(request, auth, roles) + user=old_validation(groups, request, auth, self.roles) + elif self.roles is UNSPECIFIED_ROLES: user=v(request, auth) + else: user=v(request, auth, self.roles) - if user is None and roles != UNSPECIFIED_ROLES: + if user is None and self.roles != UNSPECIFIED_ROLES: response.unauthorized() if user is not None: _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org http://mail.zope.org/mailman/listinfo/zope-checkins