[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