[CalendarServer-changes] [6472] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Wed Oct 27 11:53:53 PDT 2010


Revision: 6472
          http://trac.macosforge.org/projects/calendarserver/changeset/6472
Author:   cdaboo at apple.com
Date:     2010-10-27 11:53:51 -0700 (Wed, 27 Oct 2010)
Log Message:
-----------
Handle link resources with missing target resources by returning a 404.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/linkresource.py
    CalendarServer/trunk/twistedcaldav/sharedcollection.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/test/test_link.py

Modified: CalendarServer/trunk/twistedcaldav/linkresource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/linkresource.py	2010-10-27 17:36:01 UTC (rev 6471)
+++ CalendarServer/trunk/twistedcaldav/linkresource.py	2010-10-27 18:53:51 UTC (rev 6472)
@@ -14,15 +14,16 @@
 # limitations under the License.
 ##
 
+
 from twext.python.log import LoggingMixIn
+from twext.web2 import responsecode, server, http
+from twext.web2.dav import davxml
+from twext.web2.http import HTTPError, StatusResponse
+from twext.web2.resource import WrapperResource
 
 from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
 
-from twext.web2.http import HTTPError
-from twext.web2 import responsecode
-from twext.web2.resource import WrapperResource
 from twistedcaldav.config import config
-from twext.web2.dav import davxml
 
 __all__ = [
     "LinkResource",
@@ -42,27 +43,34 @@
 
 class LinkResource(CalDAVComplianceMixIn, WrapperResource, LoggingMixIn):
     """
-    This is similar to a WrapperResource except that we locate our resource dynamically. 
+    This is similar to a WrapperResource except that we locate our resource dynamically. We need to deal with the
+    case of a missing underlying resource (broken link) as indicated by self._linkedResource being None.
     """
     
     def __init__(self, parent, link_url):
         self.parent = parent
         self.linkURL = link_url
+        self.loopDetect = set()
         super(LinkResource, self).__init__(self.parent.principalCollections())
 
     @inlineCallbacks
     def linkedResource(self, request):
         
         if not hasattr(self, "_linkedResource"):
+            if self.linkURL in self.loopDetect:
+                raise HTTPError(StatusResponse(responsecode.LOOP_DETECTED, "Recursive link target: %s" % (self.linkURL,)))
+            else:
+                self.loopDetect.add(self.linkURL)
             self._linkedResource = (yield request.locateResource(self.linkURL))
+            self.loopDetect.remove(self.linkURL)
 
         if self._linkedResource is None:
-            raise HTTPError(responsecode.NOT_FOUND)
+            raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Missing link target: %s" % (self.linkURL,)))
             
         returnValue(self._linkedResource)
 
     def isCollection(self):
-        return True
+        return True if hasattr(self, "_linkedResource") else False
 
     def resourceType(self):
         return self._linkedResource.resourceType() if hasattr(self, "_linkedResource") else davxml.ResourceType.link
@@ -70,33 +78,41 @@
     def locateChild(self, request, segments):
         
         def _defer(result):
-            return (result, segments)
+            if result is None:
+                return (self, server.StopTraversal)
+            else:
+                return (result, segments)
         d = self.linkedResource(request)
         d.addCallback(_defer)
         return d
 
+    @inlineCallbacks
     def renderHTTP(self, request):
-        return self.linkedResource(request)
+        linked_to = (yield self.linkedResource(request))
+        if linked_to:
+            returnValue(linked_to)
+        else:
+            returnValue(http.StatusResponse(responsecode.OK, "Link resource with missing target: %s" % (self.linkURL,)))
 
     def getChild(self, name):
-        return self._linkedResource.getChild(name)
+        return self._linkedResource.getChild(name) if hasattr(self, "_linkedResource") else None
 
     @inlineCallbacks
     def hasProperty(self, property, request):
         hosted = (yield self.linkedResource(request))
-        result = (yield hosted.hasProperty(property, request))
+        result = (yield hosted.hasProperty(property, request)) if hosted else False
         returnValue(result)
 
     @inlineCallbacks
     def readProperty(self, property, request):
         hosted = (yield self.linkedResource(request))
-        result = (yield hosted.readProperty(property, request))
+        result = (yield hosted.readProperty(property, request)) if hosted else None
         returnValue(result)
 
     @inlineCallbacks
     def writeProperty(self, property, request):
         hosted = (yield self.linkedResource(request))
-        result = (yield hosted.writeProperty(property, request))
+        result = (yield hosted.writeProperty(property, request)) if hosted else None
         returnValue(result)
 
 class LinkFollowerMixIn(object):
@@ -104,17 +120,13 @@
     @inlineCallbacks
     def locateChild(self, req, segments):
 
+        self._inside_locateChild = True
         resource, path = (yield maybeDeferred(super(LinkFollowerMixIn, self).locateChild, req, segments))
-        MAX_LINK_DEPTH = 10
-        ctr = 0
-        seenResource = set()
         while isinstance(resource, LinkResource):
-            seenResource.add(resource)
-            ctr += 1
-            resource = (yield resource.linkedResource(req))
-            
-            if ctr > MAX_LINK_DEPTH or resource in seenResource:
-                raise HTTPError(responsecode.LOOP_DETECTED)
+            linked_to = (yield resource.linkedResource(req))
+            if linked_to is None:
+                break
+            resource = linked_to
         
         returnValue((resource, path))
         

Modified: CalendarServer/trunk/twistedcaldav/sharedcollection.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharedcollection.py	2010-10-27 17:36:01 UTC (rev 6471)
+++ CalendarServer/trunk/twistedcaldav/sharedcollection.py	2010-10-27 18:53:51 UTC (rev 6472)
@@ -18,6 +18,9 @@
     "SharedCollectionResource",
 ]
 
+from twext.web2.http import HTTPError, StatusResponse
+from twext.web2 import responsecode
+
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav.linkresource import LinkResource
@@ -29,24 +32,31 @@
 
 class SharedCollectionResource(LinkResource):
     """
-    This is similar to a WrapperResource except that we locate our shared collection resource dynamically. 
+    This is similar to a LinkResource except that we locate our shared collection resource dynamically.
     """
     
     def __init__(self, parent, share):
         self.share = share
-        super(SharedCollectionResource, self).__init__(parent, None)
+        super(SharedCollectionResource, self).__init__(parent, self.share.hosturl)
 
     @inlineCallbacks
     def linkedResource(self, request):
+        """
+        Resolve the share host url to the underlying resource (set to be a virtual share).
+        """
         
         if not hasattr(self, "_linkedResource"):
             self._linkedResource = (yield request.locateResource(self.share.hosturl))
             
-            # FIXME: this is awkward - because we are "mutation" this object into a virtual share
-            # we must not cache the resource at this URL, otherwise an access of the owner's resource
-            # will return the same virtually shared one which would be wrong.
-            request._forgetResource(self._linkedResource, self.share.hosturl)
+            if self._linkedResource is not None:
+                # FIXME: this is awkward - because we are "mutating" this object into a virtual share
+                # we must not cache the resource at this URL, otherwise an access of the owner's resource
+                # will return the same virtually shared one which would be wrong.
+                request._forgetResource(self._linkedResource, self.share.hosturl)
+    
+                ownerPrincipal = (yield self.parent.ownerPrincipal(request))
+                self._linkedResource.setVirtualShare(ownerPrincipal, self.share)
+            else:
+                raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Missing link target: %s" % (self.linkURL,)))
 
-            ownerPrincipal = (yield self.parent.ownerPrincipal(request))
-            self._linkedResource.setVirtualShare(ownerPrincipal, self.share)
         returnValue(self._linkedResource)

Added: CalendarServer/trunk/twistedcaldav/test/test_link.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_link.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_link.py	2010-10-27 18:53:51 UTC (rev 6472)
@@ -0,0 +1,119 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2 import responsecode
+from twext.web2.http import HTTPError
+from twext.web2.test.test_server import SimpleRequest
+
+from twisted.internet.defer import inlineCallbacks, succeed
+
+from twistedcaldav.linkresource import LinkResource
+from twistedcaldav.resource import CalendarHomeResource
+from twistedcaldav.sharedcollection import SharedCollectionResource
+from twistedcaldav.test.util import TestCase
+
+
+class StubProperty(object):
+    def qname(self):
+        return "StubQnamespace", "StubQname"
+
+class StubHome(object):
+    def properties(self):
+        return []
+
+    def calendarWithName(self, name):
+        return succeed(None)
+
+class StubCalendarHomeResource(CalendarHomeResource):
+    def principalForRecord(self):
+        return None
+
+class StubShare(object):
+    def __init__(self, link):
+        self.hosturl = link
+
+class LinkResourceTests(TestCase):
+
+    @inlineCallbacks
+    def test_okLink(self):
+        resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome())
+        self.site.resource.putChild("home", resource)
+        link = LinkResource(resource, "/home/outbox/")
+        resource.putChild("link", link)
+
+        request = SimpleRequest(self.site, "GET", "/home/link/")
+        linked_to, _ignore = (yield resource.locateChild(request, ["link",]))
+        self.assertTrue(linked_to is resource.getChild("outbox"))
+
+    @inlineCallbacks
+    def test_badLink(self):
+        resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome())
+        self.site.resource.putChild("home", resource)
+        link = LinkResource(resource, "/home/outbox/abc")
+        resource.putChild("link", link)
+
+        request = SimpleRequest(self.site, "GET", "/home/link/")
+        try:
+            yield resource.locateChild(request, ["link",])
+        except HTTPError, e:
+            self.assertEqual(e.response.code, responsecode.NOT_FOUND)
+        else:
+            self.fail("HTTPError exception not raised")
+
+    @inlineCallbacks
+    def test_recursiveLink(self):
+        resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome())
+        self.site.resource.putChild("home", resource)
+        link1 = LinkResource(resource, "/home/link2/")
+        resource.putChild("link1", link1)
+        link2 = LinkResource(resource, "/home/link1/")
+        resource.putChild("link2", link2)
+
+        request = SimpleRequest(self.site, "GET", "/home/link1/")
+        try:
+            yield resource.locateChild(request, ["link1",])
+        except HTTPError, e:
+            self.assertEqual(e.response.code, responsecode.LOOP_DETECTED)
+        else:
+            self.fail("HTTPError exception not raised")
+
+class SharedCollectionResourceTests(TestCase):
+
+    @inlineCallbacks
+    def test_okLink(self):
+        resource = StubCalendarHomeResource(self.site.resource, "home", object(), StubHome())
+        self.site.resource.putChild("home", resource)
+        link = SharedCollectionResource(resource, StubShare("/home/outbox/"))
+        resource.putChild("link", link)
+
+        request = SimpleRequest(self.site, "GET", "/home/link/")
+        linked_to, _ignore = (yield resource.locateChild(request, ["link",]))
+        self.assertTrue(linked_to is resource.getChild("outbox"))
+
+    @inlineCallbacks
+    def test_badLink(self):
+        resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome())
+        self.site.resource.putChild("home", resource)
+        link = SharedCollectionResource(resource, StubShare("/home/outbox/abc"))
+        resource.putChild("link", link)
+
+        request = SimpleRequest(self.site, "GET", "/home/link/")
+        try:
+            yield resource.locateChild(request, ["link",])
+        except HTTPError, e:
+            self.assertEqual(e.response.code, responsecode.NOT_FOUND)
+        else:
+            self.fail("HTTPError exception not raised")
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20101027/9e98b60d/attachment-0001.html>


More information about the calendarserver-changes mailing list