[CalendarServer-changes] [11772] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Oct 1 09:17:59 PDT 2013


Revision: 11772
          http://trac.calendarserver.org//changeset/11772
Author:   cdaboo at apple.com
Date:     2013-10-01 09:17:59 -0700 (Tue, 01 Oct 2013)
Log Message:
-----------
Make sure return=representation works on 412 responses.

Modified Paths:
--------------
    CalendarServer/trunk/twext/web2/dav/test/test_util.py
    CalendarServer/trunk/twext/web2/dav/util.py
    CalendarServer/trunk/twistedcaldav/storebridge.py

Modified: CalendarServer/trunk/twext/web2/dav/test/test_util.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/test_util.py	2013-10-01 16:05:19 UTC (rev 11771)
+++ CalendarServer/trunk/twext/web2/dav/test/test_util.py	2013-10-01 16:17:59 UTC (rev 11772)
@@ -7,10 +7,10 @@
 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 # copies of the Software, and to permit persons to whom the Software is
 # furnished to do so, subject to the following conditions:
-# 
+#
 # The above copyright notice and this permission notice shall be included in all
 # copies or substantial portions of the Software.
-# 
+#
 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -42,6 +42,7 @@
         self.assertEquals(util.normalizeURL("///../"), "/")
         self.assertEquals(util.normalizeURL("/.."), "/")
 
+
     def test_joinURL(self):
         """
         joinURL()
@@ -67,6 +68,7 @@
         self.assertEquals(util.joinURL("/foo", "/../"), "/")
         self.assertEquals(util.joinURL("/foo", "/./"), "/foo/")
 
+
     def test_parentForURL(self):
         """
         parentForURL()
@@ -83,6 +85,8 @@
         self.assertEquals(util.parentForURL("http://server/foo/bar/."), "http://server/foo/")
         self.assertEquals(util.parentForURL("http://server/foo/bar"), "http://server/foo/")
         self.assertEquals(util.parentForURL("http://server/foo/bar/"), "http://server/foo/")
+        self.assertEquals(util.parentForURL("http://server/foo/bar?x=1&y=2"), "http://server/foo/")
+        self.assertEquals(util.parentForURL("http://server/foo/bar/?x=1&y=2"), "http://server/foo/")
         self.assertEquals(util.parentForURL("/"), None)
         self.assertEquals(util.parentForURL("/foo/.."), None)
         self.assertEquals(util.parentForURL("/foo/../"), None)
@@ -94,3 +98,5 @@
         self.assertEquals(util.parentForURL("/foo/bar/."), "/foo/")
         self.assertEquals(util.parentForURL("/foo/bar"), "/foo/")
         self.assertEquals(util.parentForURL("/foo/bar/"), "/foo/")
+        self.assertEquals(util.parentForURL("/foo/bar?x=1&y=2"), "/foo/")
+        self.assertEquals(util.parentForURL("/foo/bar/?x=1&y=2"), "/foo/")

Modified: CalendarServer/trunk/twext/web2/dav/util.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/util.py	2013-10-01 16:05:19 UTC (rev 11771)
+++ CalendarServer/trunk/twext/web2/dav/util.py	2013-10-01 16:17:59 UTC (rev 11772)
@@ -8,10 +8,10 @@
 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 # copies of the Software, and to permit persons to whom the Software is
 # furnished to do so, subject to the following conditions:
-# 
+#
 # The above copyright notice and this permission notice shall be included in all
 # copies or substantial portions of the Software.
-# 
+#
 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -61,7 +61,8 @@
 def allDataFromStream(stream, filter=None):
     data = []
     def gotAllData(_):
-        if not data: return None
+        if not data:
+            return None
         result = "".join([str(x) for x in data])
         if filter is None:
             return result
@@ -69,6 +70,8 @@
             return filter(result)
     return readStream(stream, data.append).addCallback(gotAllData)
 
+
+
 def davXMLFromStream(stream):
     # FIXME:
     #   This reads the request body into a string and then parses it.
@@ -77,6 +80,7 @@
     if stream is None:
         return succeed(None)
 
+
     def parse(xml):
         try:
             doc = WebDAVDocument.fromString(xml)
@@ -87,11 +91,16 @@
             raise
     return allDataFromStream(stream, parse)
 
+
+
 def noDataFromStream(stream):
     def gotData(data):
-        if data: raise ValueError("Stream contains unexpected data.")
+        if data:
+            raise ValueError("Stream contains unexpected data.")
     return readStream(stream, gotData)
 
+
+
 ##
 # URLs
 ##
@@ -111,9 +120,10 @@
         if path[0] == "/":
             count = 0
             for char in path:
-                if char != "/": break
+                if char != "/":
+                    break
                 count += 1
-            path = path[count-1:]
+            path = path[count - 1:]
 
         return path
 
@@ -123,6 +133,8 @@
 
     return urlunsplit((scheme, host, urllib.quote(path), query, fragment))
 
+
+
 def joinURL(*urls):
     """
     Appends URLs in series.
@@ -142,16 +154,19 @@
     else:
         return url + trailing
 
+
+
 def parentForURL(url):
     """
     Extracts the URL of the containing collection resource for the resource
-    corresponding to a given URL.
+    corresponding to a given URL. This removes any query or fragment pieces.
+
     @param url: an absolute (server-relative is OK) URL.
     @return: the normalized URL of the collection resource containing the
         resource corresponding to C{url}.  The returned URL will always contain
         a trailing C{"/"}.
     """
-    (scheme, host, path, query, fragment) = urlsplit(normalizeURL(url))
+    (scheme, host, path, _ignore_query, _ignore_fragment) = urlsplit(normalizeURL(url))
 
     index = path.rfind("/")
     if index is 0:
@@ -165,8 +180,10 @@
         else:
             path = path[:index] + "/"
 
-    return urlunsplit((scheme, host, path, query, fragment))
+    return urlunsplit((scheme, host, path, None, None))
 
+
+
 ##
 # Python magic
 ##
@@ -180,6 +197,8 @@
     caller = inspect.getouterframes(inspect.currentframe())[1][3]
     raise NotImplementedError("Method %s is unimplemented in subclass %s" % (caller, obj.__class__))
 
+
+
 def bindMethods(module, clazz, prefixes=("preconditions_", "http_", "report_")):
     """
     Binds all functions in the given module (as defined by that module's

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2013-10-01 16:05:19 UTC (rev 11771)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2013-10-01 16:17:59 UTC (rev 11772)
@@ -77,7 +77,7 @@
 import hashlib
 import time
 import uuid
-from twext.web2 import responsecode
+from twext.web2 import responsecode, http_headers, http
 from twext.web2.iweb import IResponse
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.instance import InvalidOverriddenInstanceError, \
@@ -2222,6 +2222,41 @@
         response.headers.setHeader("content-type", self.contentType())
         returnValue(response)
 
+
+    @inlineCallbacks
+    def checkPreconditions(self, request):
+        """
+        We override the base class to trap the failure case and process any Prefer header.
+        """
+
+        try:
+            response = yield super(_CommonObjectResource, self).checkPreconditions(request)
+        except HTTPError as e:
+            if e.response.code == responsecode.PRECONDITION_FAILED:
+                response = yield self._processPrefer(request, e.response)
+                raise HTTPError(response)
+            else:
+                raise
+
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def _processPrefer(self, request, response):
+        # Look for Prefer header
+        prefer = request.headers.getHeader("prefer", {})
+        returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
+
+        if returnRepresentation and (response.code / 100 == 2 or response.code == responsecode.PRECONDITION_FAILED):
+            oldcode = response.code
+            response = (yield self.http_GET(request))
+            if oldcode in (responsecode.CREATED, responsecode.PRECONDITION_FAILED):
+                response.code = oldcode
+            response.headers.removeHeader("content-location")
+            response.headers.setHeader("content-location", self.url())
+
+        returnValue(response)
+
     # The following are used to map store exceptions into HTTP error responses
     StoreExceptionsStatusErrors = set()
     StoreExceptionsErrors = {}
@@ -2601,7 +2636,76 @@
         AttachmentRemoveFailed: (caldav_namespace, "valid-attachment-remove",),
     }
 
+
     @inlineCallbacks
+    def _checkPreconditions(self, request):
+        """
+        We override the base class to handle the special implicit scheduling weak ETag behavior
+        for compatibility with old clients using If-Match.
+        """
+
+        if config.Scheduling.CalDAV.ScheduleTagCompatibility:
+
+            if self.exists():
+                etags = self.scheduleEtags
+                if len(etags) > 1:
+                    # This is almost verbatim from twext.web2.static.checkPreconditions
+                    if request.method not in ("GET", "HEAD"):
+
+                        # Always test against the current etag first just in case schedule-etags is out of sync
+                        etag = (yield self.etag())
+                        etags = (etag,) + tuple([http_headers.ETag(schedule_etag) for schedule_etag in etags])
+
+                        # Loop over each tag and succeed if any one matches, else re-raise last exception
+                        exists = self.exists()
+                        last_modified = self.lastModified()
+                        last_exception = None
+                        for etag in etags:
+                            try:
+                                http.checkPreconditions(
+                                    request,
+                                    entityExists=exists,
+                                    etag=etag,
+                                    lastModified=last_modified,
+                                )
+                            except HTTPError, e:
+                                last_exception = e
+                            else:
+                                break
+                        else:
+                            if last_exception:
+                                raise last_exception
+
+                    # Check per-method preconditions
+                    method = getattr(self, "preconditions_" + request.method, None)
+                    if method:
+                        returnValue((yield method(request)))
+                    else:
+                        returnValue(None)
+
+        result = (yield super(CalendarObjectResource, self).checkPreconditions(request))
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def checkPreconditions(self, request):
+        """
+        We override the base class to do special schedule tag processing.
+        """
+
+        try:
+            response = yield self._checkPreconditions(request)
+        except HTTPError as e:
+            if e.response.code == responsecode.PRECONDITION_FAILED:
+                response = yield self._processPrefer(request, e.response)
+                raise HTTPError(response)
+            else:
+                raise
+
+        returnValue(response)
+
+
+    @inlineCallbacks
     def http_PUT(self, request):
 
         # Content-type check
@@ -2615,7 +2719,14 @@
             ))
 
         # Do schedule tag check
-        schedule_tag_match = self.validIfScheduleMatch(request)
+        try:
+            schedule_tag_match = self.validIfScheduleMatch(request)
+        except HTTPError as e:
+            if e.response.code == responsecode.PRECONDITION_FAILED:
+                response = yield self._processPrefer(request, e.response)
+                raise HTTPError(response)
+            else:
+                raise
 
         # Read the calendar component from the stream
         try:
@@ -2681,18 +2792,9 @@
 
                 request.addResponseFilter(_removeEtag, atEnd=True)
 
-            # Look for Prefer header
-            prefer = request.headers.getHeader("prefer", {})
-            returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
+            # Handle Prefer header
+            response = yield self._processPrefer(request, response)
 
-            if returnRepresentation and response.code / 100 == 2:
-                oldcode = response.code
-                response = (yield self.http_GET(request))
-                if oldcode == responsecode.CREATED:
-                    response.code = responsecode.CREATED
-                response.headers.removeHeader("content-location")
-                response.headers.setHeader("content-location", self.url())
-
             returnValue(response)
 
         # Handle the various store errors
@@ -2871,18 +2973,12 @@
                 raise
 
         # Look for Prefer header
-        prefer = request.headers.getHeader("prefer", {})
-        returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
-        if returnRepresentation:
-            result = (yield self.render(request))
-            result.code = OK
-            result.headers.removeHeader("content-location")
-            result.headers.setHeader("content-location", request.path)
-        else:
-            result = post_result
+        result = yield self._processPrefer(request, post_result)
+
         if action in ("attachment-add", "attachment-update",):
             result.headers.setHeader("location", location)
             result.headers.addRawHeader("Cal-Managed-ID", attachment.managedID())
+
         returnValue(result)
 
 
@@ -3313,17 +3409,8 @@
                 request.addResponseFilter(_removeEtag, atEnd=True)
 
             # Look for Prefer header
-            prefer = request.headers.getHeader("prefer", {})
-            returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
+            response = yield self._processPrefer(request, response)
 
-            if returnRepresentation and response.code / 100 == 2:
-                oldcode = response.code
-                response = (yield self.http_GET(request))
-                if oldcode == responsecode.CREATED:
-                    response.code = responsecode.CREATED
-                response.headers.removeHeader("content-location")
-                response.headers.setHeader("content-location", self.url())
-
             returnValue(response)
 
         # Handle the various store errors
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20131001/9cf9c7ce/attachment-0001.html>


More information about the calendarserver-changes mailing list