[CalendarServer-changes] [11100] CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav /method
source_changes at macosforge.org
source_changes at macosforge.org
Thu Apr 25 11:25:14 PDT 2013
Revision: 11100
http://trac.calendarserver.org//changeset/11100
Author: cdaboo at apple.com
Date: 2013-04-25 11:25:14 -0700 (Thu, 25 Apr 2013)
Log Message:
-----------
Checkpoint: get rid of redundant twistedcaldav.method modules.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/__init__.py
Removed Paths:
-------------
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove.py
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove_contact.py
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py
CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py
Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/__init__.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/__init__.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -23,14 +23,11 @@
__all__ = [
"acl",
- "copymove",
- "delete",
"get",
"mkcalendar",
"mkcol",
"post",
"propfind",
- "put",
"report",
"report_freebusy",
"report_calendar_multiget",
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,276 +0,0 @@
-##
-# Copyrightg (c) 2006-2012 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.
-##
-
-"""
-CalDAV COPY and MOVE methods.
-"""
-
-__all__ = ["http_COPY", "http_MOVE"]
-
-from urlparse import urlsplit
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.web2 import responsecode
-from twext.web2.filter.location import addLocation
-from txdav.xml import element as davxml
-from twext.web2.dav.http import ErrorResponse
-from twext.web2.dav.util import parentForURL
-from twext.web2.http import StatusResponse, HTTPError
-
-from twext.python.log import Logger
-
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import StoreCalendarObjectResource
-from twistedcaldav.method.copymove_contact import (
- maybeCOPYContact, maybeMOVEContact, KEEP_GOING
-)
-
-from twistedcaldav.resource import isCalendarCollectionResource,\
- isPseudoCalendarCollectionResource, CalDAVResource,\
- isAddressBookCollectionResource
-
-log = Logger()
-
- at inlineCallbacks
-def http_COPY(self, request):
- """
- Special handling of COPY request if parents are calendar collections.
- When copying we do not have to worry about the source resource as it
- is not being changed in any way. We do need to do an index update for
- the destination if its a calendar collection.
- """
-
- raise AssertionError("Never use this")
-
- # Copy of calendar collections isn't allowed.
- if isPseudoCalendarCollectionResource(self):
- returnValue(responsecode.FORBIDDEN)
-
- result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = (yield checkForCalendarAction(self, request))
- if not result or not destinationcal:
- # Check with CardDAV first (XXX might want to check EnableCardDAV switch?)
- result = yield maybeCOPYContact(self, request)
- if result is KEEP_GOING:
- result = yield super(CalDAVResource, self).http_COPY(request)
- returnValue(result)
-
- #
- # Check authentication and access controls
- #
- yield self.authorize(request, (davxml.Read(),), recurse=True)
-
- if destination.exists():
- yield destination.authorize(request, (davxml.WriteContent(), davxml.WriteProperties()), recurse=True)
- else:
- destparent = (yield request.locateResource(parentForURL(destination_uri)))
- yield destparent.authorize(request, (davxml.Bind(),))
-
- # Check for existing destination resource
- overwrite = request.headers.getHeader("overwrite", True)
- if destination.exists() and not overwrite:
- log.err("Attempt to copy onto existing resource without overwrite flag enabled: %s"
- % (destination,))
- raise HTTPError(StatusResponse(
- responsecode.PRECONDITION_FAILED,
- "Destination %s already exists." % (destination_uri,))
- )
-
- # Checks for copying a calendar collection
- if self.isCalendarCollection():
- log.err("Attempt to copy a calendar collection into another calendar collection %s" % destination)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "calendar-collection-location-ok"),
- "Cannot copy calendar collection inside another calendar collection",
- ))
-
- # We also do not allow regular collections in calendar collections
- if self.isCollection():
- log.err("Attempt to copy a collection into a calendar collection")
- raise HTTPError(StatusResponse(
- responsecode.FORBIDDEN,
- "Cannot create collection within special collection %s" % (destination,))
- )
-
- # May need to add a location header
- addLocation(request, destination_uri)
-
- storer = StoreCalendarObjectResource(
- request = request,
- source = self,
- source_uri = request.uri,
- sourceparent = sourceparent,
- sourcecal = sourcecal,
- destination = destination,
- destination_uri = destination_uri,
- destinationparent = destinationparent,
- destinationcal = destinationcal,
- )
- result = (yield storer.run())
- returnValue(result)
-
- at inlineCallbacks
-def http_MOVE(self, request):
- """
- Special handling of MOVE request if parent is a calendar collection.
- When moving we may need to remove the index entry for the source resource
- since its effectively being deleted. We do need to do an index update for
- the destination if its a calendar collection
- """
- raise AssertionError("Never use this")
-
- result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = (yield checkForCalendarAction(self, request))
- if not result:
- is_calendar_collection = isPseudoCalendarCollectionResource(self)
- defaultCalendarType = (yield self.isDefaultCalendar(request)) if is_calendar_collection else None
- is_addressbook_collection = isAddressBookCollectionResource(self)
- defaultAddressBook = (yield self.isDefaultAddressBook(request)) if is_addressbook_collection else False
-
- if not is_calendar_collection:
- result = yield maybeMOVEContact(self, request)
- if result is not KEEP_GOING:
- returnValue(result)
-
- # Do default WebDAV action
- result = (yield super(CalDAVResource, self).http_MOVE(request))
-
- if result == responsecode.NO_CONTENT:
- if is_calendar_collection:
- # Do some clean up
- yield self.movedCalendar(request, defaultCalendarType, destination, destination_uri)
- elif is_addressbook_collection:
- # Do some clean up
- yield self.movedAddressBook(request, defaultAddressBook, destination, destination_uri)
-
- returnValue(result)
-
- #
- # Check authentication and access controls
- #
- parent = (yield request.locateResource(parentForURL(request.uri)))
- yield parent.authorize(request, (davxml.Unbind(),))
-
- if destination.exists():
- yield destination.authorize(request, (davxml.Bind(), davxml.Unbind()), recurse=True)
- else:
- destparent = (yield request.locateResource(parentForURL(destination_uri)))
- yield destparent.authorize(request, (davxml.Bind(),))
-
- # Check for existing destination resource
- overwrite = request.headers.getHeader("overwrite", True)
- if destination.exists() and not overwrite:
- log.err("Attempt to copy onto existing resource without overwrite flag enabled: %s"
- % (destination,))
- raise HTTPError(StatusResponse(
- responsecode.PRECONDITION_FAILED,
- "Destination %s already exists." % (destination_uri,)
- ))
-
- if destinationcal:
- # Checks for copying a calendar collection
- if self.isCalendarCollection():
- log.err("Attempt to move a calendar collection into another calendar collection %s" % destination)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "calendar-collection-location-ok"),
- "Cannot move calendar collection inside another calendar collection",
- ))
-
- # We also do not allow regular collections in calendar collections
- if self.isCollection():
- log.err("Attempt to move a collection into a calendar collection")
- raise HTTPError(StatusResponse(
- responsecode.FORBIDDEN,
- "Cannot create collection within special collection %s" % (destination,)
- ))
-
- # May need to add a location header
- addLocation(request, destination_uri)
-
- storer = StoreCalendarObjectResource(
- request = request,
- source = self,
- source_uri = request.uri,
- sourceparent = sourceparent,
- sourcecal = sourcecal,
- deletesource = True,
- destination = destination,
- destination_uri = destination_uri,
- destinationparent = destinationparent,
- destinationcal = destinationcal,
- )
- result = (yield storer.run())
- returnValue(result)
-
- at inlineCallbacks
-def checkForCalendarAction(self, request):
- """
- Check to see whether the source or destination of the copy/move
- is a calendar collection, since we need to do special processing
- if that is the case.
- @return: tuple::
- result: True if special CalDAV processing required, False otherwise
- NB If there is any type of error with the request, return False
- and allow normal COPY/MOVE processing to return the error.
- sourcecal: True if source is in a calendar collection, False otherwise
- sourceparent: The parent resource for the source
- destination_uri: The URI of the destination resource
- destination: CalDAVResource of destination if special processing required,
- None otherwise
- destinationcal: True if the destination is in a calendar collection,
- False otherwise
- destinationparent:The parent resource for the destination
-
- """
-
- result = False
- sourcecal = False
- destinationcal = False
-
- # Check the source path first
- if not self.exists():
- log.err("Resource not found: %s" % (self,))
- raise HTTPError(StatusResponse(
- responsecode.NOT_FOUND,
- "Source resource %s not found." % (request.uri,)
- ))
-
- # Check for parent calendar collection
- sourceparent = (yield request.locateResource(parentForURL(request.uri)))
- if isCalendarCollectionResource(sourceparent):
- result = True
- sourcecal = True
-
- #
- # Find the destination resource
- #
- destination_uri = request.headers.getHeader("destination")
-
- if not destination_uri:
- msg = "No destination header in %s request." % (request.method,)
- log.err(msg)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-
- destination = (yield request.locateResource(destination_uri))
-
- # Check for parent calendar collection
- destination_uri = urlsplit(destination_uri)[2]
- destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
- if isCalendarCollectionResource(destinationparent):
- result = True
- destinationcal = True
-
- returnValue((result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent))
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove_contact.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove_contact.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/copymove_contact.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,249 +0,0 @@
-##
-# Copyright (c) 2006-2013 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.
-##
-
-"""
-CalDAV COPY and MOVE methods.
-"""
-
-__all__ = ["maybeCOPYContact", "maybeMOVEContact"]
-
-from urlparse import urlsplit
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.web2 import responsecode
-from twext.web2.filter.location import addLocation
-from txdav.xml import element as davxml
-from twext.web2.dav.http import ErrorResponse
-from twext.web2.dav.util import parentForURL
-from twext.web2.http import StatusResponse, HTTPError
-
-from twistedcaldav.carddavxml import carddav_namespace
-from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
-from twistedcaldav.resource import isAddressBookCollectionResource
-from twext.python.log import Logger
-
-log = Logger()
-
-KEEP_GOING = object()
-
- at inlineCallbacks
-def maybeCOPYContact(self, request):
- """
- Special handling of COPY request if parents are addressbook collections.
- When copying we do not have to worry about the source resource as it
- is not being changed in any way. We do need to do an index update for
- the destination if its an addressbook collection.
- """
- raise AssertionError("Never use this")
-
- # Copy of addressbook collections isn't allowed.
- if isAddressBookCollectionResource(self):
- returnValue(responsecode.FORBIDDEN)
-
- result, sourceadbk, sourceparent, destination_uri, destination, destinationadbk, destinationparent = (yield checkForAddressBookAction(self, request))
- if not result or not destinationadbk:
- # Give up, do default action.
-
- returnValue(KEEP_GOING)
-
- #
- # Check authentication and access controls
- #
- yield self.authorize(request, (davxml.Read(),), recurse=True)
-
- if destination.exists():
- yield destination.authorize(request, (davxml.WriteContent(), davxml.WriteProperties()), recurse=True)
- else:
- destparent = (yield request.locateResource(parentForURL(destination_uri)))
- yield destparent.authorize(request, (davxml.Bind(),))
-
- # Check for existing destination resource
- overwrite = request.headers.getHeader("overwrite", True)
- if destination.exists() and not overwrite:
- log.err("Attempt to copy onto existing resource without overwrite flag enabled: %s"
- % (destination,))
- raise HTTPError(StatusResponse(
- responsecode.PRECONDITION_FAILED,
- "Destination %s already exists." % (destination_uri,))
- )
-
- # Checks for copying an addressbook collection
- if self.isAddressBookCollection():
- log.err("Attempt to copy an addressbook collection into another addressbook collection %s" % destination)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "addressbook-collection-location-ok"),
- "Cannot copy address book collection inside another address book collection",
- ))
-
- # We also do not allow regular collections in addressbook collections
- if self.isCollection():
- log.err("Attempt to copy a collection into an addressbook collection")
- raise HTTPError(StatusResponse(
- responsecode.FORBIDDEN,
- "Cannot create collection within special collection %s" % (destination,))
- )
-
- # May need to add a location header
- addLocation(request, destination_uri)
-
- storer = StoreAddressObjectResource(
- request = request,
- source = self,
- source_uri = request.uri,
- sourceparent = sourceparent,
- sourceadbk = sourceadbk,
- destination = destination,
- destination_uri = destination_uri,
- destinationparent = destinationparent,
- destinationadbk = destinationadbk,
- )
- result = (yield storer.run())
- returnValue(result)
-
- at inlineCallbacks
-def maybeMOVEContact(self, request):
- """
- Special handling of MOVE request if parent is an addressbook collection.
- When moving we may need to remove the index entry for the source resource
- since its effectively being deleted. We do need to do an index update for
- the destination if its an addressbook collection
- """
- raise AssertionError("Never use this")
-
- result, sourceadbk, sourceparent, destination_uri, destination, destinationadbk, destinationparent = (yield checkForAddressBookAction(self, request))
- if not result or not destinationadbk:
-
- # Do default WebDAV action
- returnValue(KEEP_GOING)
-
- #
- # Check authentication and access controls
- #
- parent = (yield request.locateResource(parentForURL(request.uri)))
- yield parent.authorize(request, (davxml.Unbind(),))
-
- if destination.exists():
- yield destination.authorize(request, (davxml.Bind(), davxml.Unbind()), recurse=True)
- else:
- destparent = (yield request.locateResource(parentForURL(destination_uri)))
- yield destparent.authorize(request, (davxml.Bind(),))
-
- # Check for existing destination resource
- overwrite = request.headers.getHeader("overwrite", True)
- if destination.exists() and not overwrite:
- log.err("Attempt to copy onto existing resource without overwrite flag enabled: %s"
- % (destination,))
- raise HTTPError(StatusResponse(
- responsecode.PRECONDITION_FAILED,
- "Destination %s already exists." % (destination_uri,)
- ))
-
- if destinationadbk:
- # Checks for copying an addressbook collection
- if self.isAddressBookCollection():
- log.err("Attempt to move an addressbook collection into another addressbook collection %s" % destination)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "addressbook-collection-location-ok"),
- "Cannot move address book collection inside another address book collection",
- ))
-
- # We also do not allow regular collections in addressbook collections
- if self.isCollection():
- log.err("Attempt to move a collection into an addressbook collection")
- raise HTTPError(StatusResponse(
- responsecode.FORBIDDEN,
- "Cannot create collection within special collection %s" % (destination,)
- ))
-
- # May need to add a location header
- addLocation(request, destination_uri)
-
- storer = StoreAddressObjectResource(
- request = request,
- source = self,
- source_uri = request.uri,
- sourceparent = sourceparent,
- sourceadbk = sourceadbk,
- deletesource = True,
- destination = destination,
- destination_uri = destination_uri,
- destinationparent = destinationparent,
- destinationadbk = destinationadbk,
- )
- result = (yield storer.run())
- returnValue(result)
-
- at inlineCallbacks
-def checkForAddressBookAction(self, request):
- """
- Check to see whether the source or destination of the copy/move
- is an addressbook collection, since we need to do special processing
- if that is the case.
- @return: tuple::
- result: True if special CalDAV processing required, False otherwise
- NB If there is any type of error with the request, return False
- and allow normal COPY/MOVE processing to return the error.
- sourceadbk: True if source is in an addressbook collection, False otherwise
- sourceparent: The parent resource for the source
- destination_uri: The URI of the destination resource
- destination: CalDAVResource of destination if special processing required,
- None otherwise
- destinationadbk: True if the destination is in an addressbook collection,
- False otherwise
- destinationparent:The parent resource for the destination
-
- """
-
- result = False
- sourceadbk = False
- destinationadbk = False
-
- # Check the source path first
- if not self.exists():
- log.err("Resource not found: %s" % (self,))
- raise HTTPError(StatusResponse(
- responsecode.NOT_FOUND,
- "Source resource %s not found." % (request.uri,)
- ))
-
- # Check for parent addressbook collection
- sourceparent = (yield request.locateResource(parentForURL(request.uri)))
- if isAddressBookCollectionResource(sourceparent):
- result = True
- sourceadbk = True
-
- #
- # Find the destination resource
- #
- destination_uri = request.headers.getHeader("destination")
-
- if not destination_uri:
- msg = "No destination header in %s request." % (request.method,)
- log.err(msg)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-
- destination = (yield request.locateResource(destination_uri))
-
- # Check for parent addressbook collection
- destination_uri = urlsplit(destination_uri)[2]
- destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
- if isAddressBookCollectionResource(destinationparent):
- result = True
- destinationadbk = True
-
- returnValue((result, sourceadbk, sourceparent, destination_uri, destination, destinationadbk, destinationparent))
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,62 +0,0 @@
-##
-# Copyright (c) 2006-2013 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.
-##
-
-"""
-CalDAV DELETE method.
-"""
-
-__all__ = ["http_DELETE"]
-
-from twext.python.log import Logger
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from twext.web2.dav.util import parentForURL
-from twext.web2.http import HTTPError
-
-from twistedcaldav.method.delete_common import DeleteResource
-
-log = Logger()
-
- at inlineCallbacks
-def http_DELETE(self, request):
- #
- # Override base DELETE request handling to ensure that the calendar
- # index file has the entry for the deleted calendar component removed.
- #
-
- raise AssertionError("Never use this")
-
- if not self.exists():
- log.err("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
-
- depth = request.headers.getHeader("depth", "infinity")
-
- #
- # Check authentication and access controls
- #
- parentURL = parentForURL(request.uri)
- parent = (yield request.locateResource(parentURL))
-
- yield parent.authorize(request, (davxml.Unbind(),))
-
- # Do smart delete taking into account the need to do implicit CANCELs etc
- deleter = DeleteResource(request, self, request.uri, parent, depth)
- response = (yield deleter.run())
-
- returnValue(response)
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,58 +0,0 @@
-##
-# Copyright (c) 2006-2013 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.
-##
-
-
-"""
-CalDAV DELETE behaviors.
-"""
-
-__all__ = ["DeleteResource"]
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from twext.python.log import Logger
-
-log = Logger()
-
-class DeleteResource(object):
-
- def __init__(self, request, resource, resource_uri, parent, depth,
- internal_request=False, allowImplicitSchedule=True):
-
- raise AssertionError("Never use this")
-
- self.request = request
- self.resource = resource
- self.resource_uri = resource_uri
- self.parent = parent
- self.depth = depth
- self.internal_request = internal_request
- self.allowImplicitSchedule = allowImplicitSchedule
-
-
- @inlineCallbacks
- def run(self):
- # FIXME: this code-path shouldn't actually be used, as the things
- # with storeRemove on them also have their own http_DELETEs.
- response = (
- yield self.resource.storeRemove(
- self.request,
- not self.internal_request and self.allowImplicitSchedule,
- self.resource_uri
- )
- )
-
- returnValue(response)
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,164 +0,0 @@
-##
-# Copyright (c) 2005-2013 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.
-##
-
-"""
-CalDAV PUT method.
-"""
-
-__all__ = ["http_PUT"]
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.web2 import responsecode
-from twext.web2.dav.util import allDataFromStream, parentForURL
-from twext.web2.http import HTTPError, StatusResponse
-
-from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
-
-from twistedcaldav.caldavxml import caldav_namespace
-
-from twistedcaldav.method.put_common import StoreCalendarObjectResource
-from twistedcaldav.resource import isPseudoCalendarCollectionResource
-
-log = Logger()
-
-from twistedcaldav.carddavxml import carddav_namespace
-from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
-from twistedcaldav.resource import isAddressBookCollectionResource
-
- at inlineCallbacks
-def http_PUT(self, request):
-
- raise AssertionError("Never use this")
-
- parentURL = parentForURL(request.uri)
- parent = (yield request.locateResource(parentURL))
-
- if isPseudoCalendarCollectionResource(parent):
-
- # Content-type check
- content_type = request.headers.getHeader("content-type")
- if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
- log.err("MIME type %s not allowed in calendar collection" % (content_type,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "supported-calendar-data"),
- "Invalid MIME type for calendar collection",
- ))
-
- # Read the calendar component from the stream
- try:
- calendardata = (yield allDataFromStream(request.stream))
- if not hasattr(request, "extendedLogItems"):
- request.extendedLogItems = {}
- request.extendedLogItems["cl"] = str(len(calendardata)) if calendardata else "0"
-
- # We must have some data at this point
- if calendardata is None:
- # Use correct DAV:error response
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- description="No calendar data"
- ))
-
- storer = StoreCalendarObjectResource(
- request=request,
- destination=self,
- destination_uri=request.uri,
- destinationcal=True,
- destinationparent=parent,
- calendar=calendardata,
- )
- result = (yield storer.run())
-
- # 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 result.code / 100 == 2:
- oldcode = result.code
- result = (yield self.http_GET(request))
- if oldcode == responsecode.CREATED:
- result.code = responsecode.CREATED
- result.headers.setHeader("content-location", request.path)
-
- returnValue(result)
-
- except ValueError, e:
- log.err("Error while handling (calendar) PUT: %s" % (e,))
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
-
- elif isAddressBookCollectionResource(parent):
-
- # Content-type check
- content_type = request.headers.getHeader("content-type")
- if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "vcard"):
- log.err("MIME type %s not allowed in address book collection" % (content_type,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "supported-address-data"),
- "Invalid MIME type for address book collection",
- ))
-
- # Read the vcard component from the stream
- try:
- vcarddata = (yield allDataFromStream(request.stream))
- if not hasattr(request, "extendedLogItems"):
- request.extendedLogItems = {}
- request.extendedLogItems["cl"] = str(len(vcarddata)) if vcarddata else "0"
-
- # We must have some data at this point
- if vcarddata is None:
- # Use correct DAV:error response
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "valid-address-data"),
- description="No vcard data"
- ))
-
- storer = StoreAddressObjectResource(
- request=request,
- sourceadbk=False,
- vcard=vcarddata,
- destination=self,
- destination_uri=request.uri,
- destinationadbk=True,
- destinationparent=parent,
- )
- result = (yield storer.run())
-
- # 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 result.code / 100 == 2:
- oldcode = result.code
- result = (yield self.http_GET(request))
- if oldcode == responsecode.CREATED:
- result.code = responsecode.CREATED
- result.headers.setHeader("content-location", request.path)
-
- returnValue(result)
-
- except ValueError, e:
- log.err("Error while handling (address book) PUT: %s" % (e,))
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
-
- else:
- # No longer support arbitrary PUTs to resource. Instead we are going to require all the
- # resource classes we care about to explicitly define their own http_PUT.
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "PUTs not allowed here"))
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,599 +0,0 @@
-##
-# Copyright (c) 2005-2013 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.
-##
-
-"""
-PUT/COPY/MOVE common behavior.
-"""
-
-__all__ = ["StoreAddressObjectResource"]
-
-from twext.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.dav.http import ErrorResponse
-from twext.web2.dav.util import joinURL, parentForURL
-from twext.web2.http import HTTPError
-from twext.web2.http import StatusResponse
-from twext.web2.stream import MemoryStream
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.defer import returnValue
-from twisted.python.failure import Failure
-
-from twistedcaldav import customxml
-from twistedcaldav.carddavxml import NoUIDConflict, carddav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.vcard import Component
-
-from txdav.common.icommondatastore import ReservationError
-from txdav.xml import element as davxml
-
-import types
-
-log = Logger()
-
-class StoreAddressObjectResource(object):
-
- class UIDReservation(object):
-
- def __init__(self, index, uid, uri):
- self.reserved = False
- self.index = index
- self.uid = uid
- self.uri = uri
-
- @inlineCallbacks
- def reserve(self):
-
- # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
- # time to whoever has the reservation to finish and release it.
- failure_count = 0
- while(failure_count < 10):
- try:
- yield self.index.reserveUID(self.uid)
- self.reserved = True
- break
- except ReservationError:
- self.reserved = False
- failure_count += 1
-
- pause = Deferred()
- def _timedDeferred():
- pause.callback(True)
- reactor.callLater(0.5, _timedDeferred) #@UndefinedVariable
- yield pause
-
- if self.uri and not self.reserved:
- raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
-
- @inlineCallbacks
- def unreserve(self):
- if self.reserved:
- yield self.index.unreserveUID(self.uid)
- self.reserved = False
-
-
- def __init__(
- self,
- request,
- source=None, source_uri=None, sourceparent=None, sourceadbk=False, deletesource=False,
- destination=None, destination_uri=None, destinationparent=None, destinationadbk=True,
- vcard=None,
- indexdestination=True,
- returnData=False,
- ):
- """
- Function that does common PUT/COPY/MOVE behavior.
-
- @param request: the L{twext.web2.server.Request} for the current HTTP request.
- @param source: the L{CalDAVResource} for the source resource to copy from, or None if source data
- is to be read from the request.
- @param source_uri: the URI for the source resource.
- @param destination: the L{CalDAVResource} for the destination resource to copy into.
- @param destination_uri: the URI for the destination resource.
- @param vcard: the C{str} or L{Component} vcard data if there is no source, None otherwise.
- @param sourceadbk: True if the source resource is in a vcard collection, False otherwise.
- @param destinationadbk: True if the destination resource is in a vcard collection, False otherwise
- @param sourceparent: the L{CalDAVResource} for the source resource's parent collection, or None if source is None.
- @param destinationparent: the L{CalDAVResource} for the destination resource's parent collection.
- @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
- @param returnData: True if the caller wants the actual data written to the store returned
- """
-
- raise AssertionError("Never use this")
-
- # Check that all arguments are valid
- try:
- assert destination is not None and destinationparent is not None and destination_uri is not None
- assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
- assert (vcard is None and source is not None) or (vcard is not None and source is None)
- assert not deletesource or (deletesource and source is not None)
- except AssertionError:
- log.err("Invalid arguments to StoreAddressObjectResource.__init__():")
- log.err("request=%s\n" % (request,))
- log.err("sourceadbk=%s\n" % (sourceadbk,))
- log.err("destinationadbk=%s\n" % (destinationadbk,))
- log.err("source=%s\n" % (source,))
- log.err("source_uri=%s\n" % (source_uri,))
- log.err("sourceparent=%s\n" % (sourceparent,))
- log.err("destination=%s\n" % (destination,))
- log.err("destination_uri=%s\n" % (destination_uri,))
- log.err("destinationparent=%s\n" % (destinationparent,))
- log.err("vcard=%s\n" % (vcard,))
- log.err("deletesource=%s\n" % (deletesource,))
- raise
-
- self.request = request
- self.sourceadbk = sourceadbk
- self.destinationadbk = destinationadbk
- self.source = source
- self.source_uri = source_uri
- self.sourceparent = sourceparent
- self.destination = destination
- self.destination_uri = destination_uri
- self.destinationparent = destinationparent
- self.vcard = vcard
- self.vcarddata = None
- self.deletesource = deletesource
- self.indexdestination = indexdestination
- self.returnData = returnData
-
- self.access = None
-
-
- @inlineCallbacks
- def fullValidation(self):
- """
- Do full validation of source and destination vcard data.
- """
-
- if self.destinationadbk:
- # Valid resource name check
- result, message = self.validResourceName()
- if not result:
- log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
-
- # Valid collection size check on the destination parent resource
- result, message = (yield self.validCollectionSize())
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- customxml.MaxResources(),
- message,
- ))
-
- if not self.sourceadbk:
- # Valid content type check on the source resource if its not in a vcard collection
- if self.source is not None:
- result, message = self.validContentType()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "supported-address-data"),
- message,
- ))
-
- # At this point we need the calendar data to do more tests
- self.vcard = (yield self.source.vCard())
- else:
- try:
- if type(self.vcard) in (types.StringType, types.UnicodeType,):
- self.vcard = Component.fromString(self.vcard)
- except ValueError, e:
- log.err(str(e))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "valid-address-data"),
- "Could not parse vCard",
- ))
-
- # Valid vcard data check
- result, message = self.validAddressDataCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "valid-address-data"),
- message
- ))
-
- # Valid vcard data for CalDAV check
- result, message = self.validCardDAVDataCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "valid-addressbook-object-resource"),
- message,
- ))
-
- # Must have a valid UID at this point
- self.uid = self.vcard.resourceUID()
- else:
- # Get UID from original resource
- self.source_index = self.sourceparent.index()
- self.uid = yield self.source_index.resourceUIDForName(self.source.name())
- if self.uid is None:
- log.err("Source vcard does not have a UID: %s" % self.source.name())
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "valid-addressbook-object-resource"),
- "Missing UID in vCard",
- ))
-
- # FIXME: We need this here because we have to re-index the destination. Ideally it
- # would be better to copy the index entries from the source and add to the destination.
- self.vcard = (yield self.source.vCard())
-
- # Valid vcard data size check
- result, message = self.validSizeCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (carddav_namespace, "max-resource-size"),
- message,
- ))
-
- # Check access
- returnValue(None)
-
-
- def validResourceName(self):
- """
- Make sure that the resource name for the new resource is valid.
- """
- result = True
- message = ""
- filename = self.destination.name()
- if filename.startswith("."):
- result = False
- message = "Resource name %s not allowed in vcard collection" % (filename,)
-
- return result, message
-
-
- def validContentType(self):
- """
- Make sure that the content-type of the source resource is text/vcard.
- This test is only needed when the source is not in a vcard collection.
- """
- result = True
- message = ""
- content_type = self.source.contentType()
- if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "vcard")):
- result = False
- message = "MIME type %s not allowed in vcard collection" % (content_type,)
-
- return result, message
-
-
- @inlineCallbacks
- def validCollectionSize(self):
- """
- Make sure that any limits on the number of resources in a collection are enforced.
- """
- result = True
- message = ""
- if not self.destination.exists() and \
- config.MaxResourcesPerCollection and \
- (yield self.destinationparent.countChildren()) >= config.MaxResourcesPerCollection:
- result = False
- message = "Too many resources in collection %s" % (self.destinationparent,)
-
- returnValue((result, message,))
-
-
- def validAddressDataCheck(self):
- """
- Check that the calendar data is valid iCalendar.
- @return: tuple: (True/False if the calendar data is valid,
- log message string).
- """
- result = True
- message = ""
- if self.vcard is None:
- result = False
- message = "Empty resource not allowed in vcard collection"
- else:
- try:
- self.vcard.validVCardData()
- except ValueError, e:
- result = False
- message = "Invalid vcard data: %s" % (e,)
-
- return result, message
-
-
- def validCardDAVDataCheck(self):
- """
- Check that the vcard data is valid vCard.
- @return: tuple: (True/False if the vcard data is valid,
- log message string).
- """
- result = True
- message = ""
- try:
- self.vcard.validForCardDAV()
- except ValueError, e:
- result = False
- message = "vCard data does not conform to CardDAV requirements: %s" % (e,)
-
- return result, message
-
-
- def validSizeCheck(self):
- """
- Make sure that the content-type of the source resource is text/vcard.
- This test is only needed when the source is not in a vcard collection.
- """
- result = True
- message = ""
- if config.MaxResourceSize:
- vcardsize = len(str(self.vcard))
- if vcardsize > config.MaxResourceSize:
- result = False
- message = "Data size %d bytes is larger than allowed limit %d bytes" % (vcardsize, config.MaxResourceSize)
-
- return result, message
-
-
- @inlineCallbacks
- def noUIDConflict(self, uid):
- """
- Check that the UID of the new vcard object conforms to the requirements of
- CardDAV, i.e. it must be unique in the collection and we must not overwrite a
- different UID.
- @param uid: the UID for the resource being stored.
- @return: tuple: (True/False if the UID is valid, log message string,
- name of conflicted resource).
- """
-
- result = True
- message = ""
- rname = ""
-
- # Adjust for a move into same vcard collection
- oldname = None
- if self.sourceparent and (self.sourceparent == self.destinationparent) and self.deletesource:
- oldname = self.source.name()
-
- # UID must be unique
- index = self.destinationparent.index()
- if not (yield index.isAllowedUID(uid, oldname, self.destination.name())):
- rname = yield index.resourceNameForUID(uid)
- # This can happen if two simultaneous PUTs occur with the same UID.
- # i.e. one PUT has reserved the UID but has not yet written the resource,
- # the other PUT tries to reserve and fails but no index entry exists yet.
- if rname is None:
- rname = "<<Unknown Resource>>"
-
- result = False
- message = "Address book resource %s already exists with same UID %s" % (rname, uid)
- else:
- # Cannot overwrite a resource with different UID
- if self.destination.exists():
- olduid = yield index.resourceUIDForName(self.destination.name())
- if olduid != uid:
- rname = self.destination.name()
- result = False
- message = "Cannot overwrite vcard resource %s with different UID %s" % (rname, olduid)
-
- returnValue((result, message, rname))
-
-
- @inlineCallbacks
- def doStore(self):
- # Do put or copy based on whether source exists
- source = self.source
- if source is not None:
- # Retrieve information from the source, in case we have to delete
- # it.
- sourceProperties = dict(source.newStoreProperties().iteritems())
- sourceText = yield source.vCardText()
-
- # Delete the original source if needed (for example, if this is a
- # same-calendar MOVE of a calendar object, implemented as an
- # effective DELETE-then-PUT).
- if self.deletesource:
- yield self.doSourceDelete()
-
- response = (yield self.destination.storeStream(MemoryStream(sourceText)))
- self.destination.newStoreProperties().update(sourceProperties)
- else:
- response = (yield self.doStorePut())
-
- returnValue(response)
-
-
- @inlineCallbacks
- def doStorePut(self):
-
- stream = MemoryStream(str(self.vcard))
- response = (yield self.destination.storeStream(stream))
- returnValue(response)
-
-
- @inlineCallbacks
- def doSourceDelete(self):
- # Delete the source resource
- yield self.source.storeRemove(self.request, False, self.source_uri)
- log.debug("Source removed %s" % (self.source,))
- returnValue(None)
-
-
- @inlineCallbacks
- def run(self):
- """
- Function that does common PUT/COPY/MOVE behavior.
-
- @return: a Deferred with a status response result.
- """
-
- try:
- reservation = None
-
- # Handle all validation operations here.
- yield self.fullValidation()
-
- # Reservation and UID conflict checking is next.
- if self.destinationadbk:
- # Reserve UID
- self.destination_index = self.destinationparent.index()
- reservation = StoreAddressObjectResource.UIDReservation(
- self.destination_index, self.uid, self.destination_uri
- )
- if self.indexdestination:
- yield reservation.reserve()
-
- # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
- # try to write the same vcard data to two different resource URIs.
- result, message, rname = yield self.noUIDConflict(self.uid)
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- NoUIDConflict(
- davxml.HRef.fromString(
- joinURL(
- parentForURL(self.destination_uri),
- rname.encode("utf-8")
- )
- )
- ),
- "UID already used in another resource",
- ))
-
- # Do the actual put or copy
- response = (yield self.doStore())
-
- if reservation:
- yield reservation.unreserve()
-
- returnValue(response)
-
- except Exception:
-
- # Grab the current exception state here so we can use it in a re-raise - we need this because
- # an inlineCallback might be called and that raises an exception when it returns, wiping out the
- # original exception "context".
- ex = Failure()
-
- if reservation:
- yield reservation.unreserve()
-
- # Return the original failure (exception) state
- ex.raiseException()
-
-
- @inlineCallbacks
- def moveValidation(self):
- """
- Do full validation of source and destination calendar data.
- """
-
- # Valid resource name check
- result, message = self.validResourceName()
- if not result:
- log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
-
- # Valid collection size check on the destination parent resource
- result, message = (yield self.validCollectionSize())
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- customxml.MaxResources(),
- message,
- ))
-
- returnValue(None)
-
-
- @inlineCallbacks
- def doStoreMove(self):
-
- # Do move
- response = (yield self.source.storeMove(self.request, self.destinationparent, self.destination._name))
- returnValue(response)
-
-
- @inlineCallbacks
- def move(self):
- """
- Function that does common MOVE behavior.
-
- @return: a Deferred with a status response result.
- """
-
- try:
- reservation = None
-
- # Handle all validation operations here.
- yield self.moveValidation()
-
- # Reservation and UID conflict checking is next.
-
- # Reserve UID
- self.destination_index = self.destinationparent.index()
- reservation = StoreAddressObjectResource.UIDReservation(
- self.destination_index, self.source.uid(), self.destination_uri
- )
- if self.indexdestination:
- yield reservation.reserve()
-
- # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
- # try to write the same vcard data to two different resource URIs.
- result, message, rname = yield self.noUIDConflict(self.source.uid())
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- NoUIDConflict(
- davxml.HRef.fromString(
- joinURL(
- parentForURL(self.destination_uri),
- rname.encode("utf-8")
- )
- )
- ),
- "UID already used in another resource",
- ))
-
- # Do the actual put or copy
- response = (yield self.doStoreMove())
-
- if reservation:
- yield reservation.unreserve()
-
- returnValue(response)
-
- except Exception:
-
- # Grab the current exception state here so we can use it in a re-raise - we need this because
- # an inlineCallback might be called and that raises an exception when it returns, wiping out the
- # original exception "context".
- ex = Failure()
-
- if reservation:
- yield reservation.unreserve()
-
- # Return the original failure (exception) state
- ex.raiseException()
Deleted: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py 2013-04-25 17:56:21 UTC (rev 11099)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py 2013-04-25 18:25:14 UTC (rev 11100)
@@ -1,1493 +0,0 @@
-# -*- test-case-name: twistedcaldav.test.test_validation -*-
-##
-# Copyright (c) 2005-2013 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.
-##
-
-"""
-PUT/COPY/MOVE common behavior.
-"""
-
-__all__ = ["StoreCalendarObjectResource"]
-
-from twext.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.dav.http import ErrorResponse
-from twext.web2.dav.util import joinURL, parentForURL
-from twext.web2.http import HTTPError
-from twext.web2.http import StatusResponse
-from twext.web2.iweb import IResponse
-from twext.web2.stream import MemoryStream
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks, succeed
-from twisted.internet.defer import returnValue
-from twisted.python import hashlib
-from twisted.python.failure import Failure
-
-from twistedcaldav import customxml
-from twistedcaldav.caldavxml import caldav_namespace, NoUIDConflict, MaxInstances, MaxAttendeesPerInstance
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
-from twistedcaldav.ical import Component, Property
-from twistedcaldav.instance import TooManyInstancesError, \
- InvalidOverriddenInstanceError
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
-
-from txdav.caldav.icalendarstore import AttachmentStoreValidManagedID
-from txdav.common.icommondatastore import ReservationError
-from txdav.xml import element as davxml
-
-from urlparse import urlparse, urlunparse
-
-import types
-import uuid
-
-log = Logger()
-
-class StoreCalendarObjectResource(object):
-
- class UIDReservation(object):
-
- def __init__(self, index, uid, uri, internal_request, transaction):
- if internal_request:
- self.lock = None
- else:
- self.lock = MemcacheLock(
- "ImplicitUIDLock",
- uid,
- timeout=config.Scheduling.Options.UIDLockTimeoutSeconds,
- expire_time=config.Scheduling.Options.UIDLockExpirySeconds
- )
- self.reserved = False
- self.index = index
- self.uid = uid
- self.uri = uri
- self.transaction = transaction
-
- @inlineCallbacks
- def reserve(self):
-
- # Implicit lock
- if self.lock:
- try:
- yield self.lock.acquire()
- except MemcacheLockTimeoutError:
- raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use on the server." % (self.uri,)))
-
- # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
- # time to whoever has the reservation to finish and release it.
- failure_count = 0
- while(failure_count < 10):
- try:
- yield self.index.reserveUID(self.uid)
- self.reserved = True
- break
- except ReservationError:
- self.reserved = False
- failure_count += 1
-
- pause = Deferred()
- def _timedDeferred():
- pause.callback(True)
- reactor.callLater(0.5, _timedDeferred)
- yield pause
-
- if self.uri and not self.reserved:
- if self.lock:
- # Can release immediately as nothing happened
- yield self.lock.release()
- raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use in calendar." % (self.uri,)))
-
- @inlineCallbacks
- def unreserve(self):
- if self.reserved:
- yield self.index.unreserveUID(self.uid)
- self.reserved = False
- if self.lock:
- # Release lock after commit or abort
- self.transaction.postCommit(self.lock.clean)
- self.transaction.postAbort(self.lock.clean)
-
-
- def __init__(
- self,
- request,
- source=None, source_uri=None, sourceparent=None, sourcecal=False, deletesource=False,
- destination=None, destination_uri=None, destinationparent=None, destinationcal=True,
- calendar=None,
- isiTIP=False,
- allowImplicitSchedule=True,
- internal_request=False,
- processing_organizer=None,
- returnData=False,
- attachmentProcessingDone=False,
- ):
- """
- Function that does common PUT/COPY/MOVE behavior.
-
- @param request: the L{twext.web2.server.Request} for the current HTTP request.
- @param source: the L{CalDAVResource} for the source resource to copy from, or None if source data
- is to be read from the request.
- @param source_uri: the URI for the source resource.
- @param destination: the L{CalDAVResource} for the destination resource to copy into.
- @param destination_uri: the URI for the destination resource.
- @param calendar: the C{str} or L{Component} calendar data if there is no source, None otherwise.
- @param sourcecal: True if the source resource is in a calendar collection, False otherwise.
- @param destinationcal: True if the destination resource is in a calendar collection, False otherwise
- @param sourceparent: the L{CalDAVResource} for the source resource's parent collection, or None if source is None.
- @param destinationparent: the L{CalDAVResource} for the destination resource's parent collection.
- @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
- @param isiTIP: True if relaxed calendar data validation is to be done, False otherwise.
- @param allowImplicitSchedule: True if implicit scheduling should be attempted, False otherwise.
- @param internal_request: True if this request originates internally and needs to bypass scheduling authorization checks.
- @param processing_organizer: True if implicit processing for an organizer, False if for an attendee, None if not implicit processing.
- @param returnData: True if the caller wants the actual data written to the store returned
- @param attachmentProcessingDone True if the caller has already processed managed attachment changes
- """
-
- raise AssertionError("Never use this")
-
- # Check that all arguments are valid
- try:
- assert destination is not None and destinationparent is not None and destination_uri is not None
- assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
- assert (calendar is None and source is not None) or (calendar is not None and source is None)
- assert not deletesource or (deletesource and source is not None)
- except AssertionError:
- log.err("Invalid arguments to StoreCalendarObjectResource.__init__():")
- log.err("request=%s\n" % (request,))
- log.err("sourcecal=%s\n" % (sourcecal,))
- log.err("destinationcal=%s\n" % (destinationcal,))
- log.err("source=%s\n" % (source,))
- log.err("source_uri=%s\n" % (source_uri,))
- log.err("sourceparent=%s\n" % (sourceparent,))
- log.err("destination=%s\n" % (destination,))
- log.err("destination_uri=%s\n" % (destination_uri,))
- log.err("destinationparent=%s\n" % (destinationparent,))
- log.err("calendar=%s\n" % (calendar,))
- log.err("deletesource=%s\n" % (deletesource,))
- log.err("isiTIP=%s\n" % (isiTIP,))
- raise
-
- self.request = request
- self.sourcecal = sourcecal
- self.destinationcal = destinationcal
- self.source = source
- self.source_uri = source_uri
- self.sourceparent = sourceparent
- self.destination = destination
- self.destination_uri = destination_uri
- self.destinationparent = destinationparent
- self.calendar = calendar
- self.deletesource = deletesource
- self.isiTIP = isiTIP
- self.allowImplicitSchedule = allowImplicitSchedule
- self.internal_request = internal_request
- self.processing_organizer = processing_organizer
- self.returnData = returnData
- self.attachmentProcessingDone = attachmentProcessingDone
-
- self.access = None
- self.hasPrivateComments = False
- self.isScheduleResource = False
- self.dataChanged = False
-
-
- @inlineCallbacks
- def fullValidation(self):
- """
- Do full validation of source and destination calendar data.
- """
-
- # Basic validation
- self.validIfScheduleMatch()
-
- if self.destinationcal:
-
- # Skip validation on internal requests
- if not self.internal_request:
-
- # Valid resource name check
- result, message = self.validResourceName()
- if not result:
- log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
-
- # Valid collection size check on the destination parent resource
- result, message = (yield self.validCollectionSize())
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- customxml.MaxResources(),
- message,
- ))
-
- # Valid data sizes - do before parsing the data
- if self.source is not None:
- # Valid content length check on the source resource
- result, message = self.validContentLength()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "max-resource-size"),
- message,
- ))
- else:
- # Valid calendar data size check
- result, message = self.validSizeCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "max-resource-size"),
- message,
- ))
-
- if not self.sourcecal:
- # Valid content type check on the source resource if its not in a calendar collection
- if self.source is not None:
- result, message = self.validContentType()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "supported-calendar-data"),
- message,
- ))
-
- # At this point we need the calendar data to do more tests
- try:
- self.calendar = (yield self.source.iCalendarForUser(self.request))
- except ValueError, e:
- log.err(str(e))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- "Can't parse calendar data"
- ))
- else:
- try:
- if type(self.calendar) in (types.StringType, types.UnicodeType,):
- self.calendar = Component.fromString(self.calendar)
- except ValueError, e:
- log.err(str(e))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- "Can't parse calendar data"
- ))
-
- # Possible timezone stripping
- if config.EnableTimezonesByReference:
- self.calendar.stripKnownTimezones()
-
- # Skip validation on internal requests
- if not self.internal_request:
-
- # Valid calendar data check
- result, message = self.validCalendarDataCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- message
- ))
-
- # Valid calendar data for CalDAV check
- result, message = self.validCalDAVDataCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-object-resource"),
- message,
- ))
-
- # Valid calendar component for check
- result, message = self.validComponentType()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "supported-component"),
- message,
- ))
-
- # Valid attendee list size check
- result, message = (yield self.validAttendeeListSizeCheck())
- if not result:
- log.err(message)
- raise HTTPError(
- ErrorResponse(
- responsecode.FORBIDDEN,
- MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),
- message,
- )
- )
-
- # Normalize the calendar user addresses once we know we have valid
- # calendar data
- self.destination.iCalendarAddressDoNormalization(self.calendar)
-
- # Must have a valid UID at this point
- self.uid = self.calendar.resourceUID()
- else:
- # Get UID from original resource
- self.source_index = self.sourceparent.index()
- self.uid = yield self.source_index.resourceUIDForName(self.source.name())
- if self.uid is None:
- log.err("Source calendar does not have a UID: %s" % self.source)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-object-resource"),
- "Missing UID in calendar data",
- ))
-
- # FIXME: We need this here because we have to re-index the destination. Ideally it
- # would be better to copy the index entries from the source and add to the destination.
- self.calendar = (yield self.source.iCalendarForUser(self.request))
-
- # Check that moves to shared calendars are OK
- yield self.validCopyMoveOperation()
-
- # Check location/resource organizer requirement
- yield self.validLocationResourceOrganizer()
-
- # Check access
- if self.destinationcal and config.EnablePrivateEvents:
- result = (yield self.validAccess())
- returnValue(result)
- else:
- returnValue(None)
-
- elif self.sourcecal:
- self.source_index = self.sourceparent.index()
- self.calendar = (yield self.source.iCalendarForUser(self.request))
-
- # Check that moves to shared calendars are OK
- yield self.validCopyMoveOperation()
-
-
- @inlineCallbacks
- def validCopyMoveOperation(self):
- """
- Check that copy/move type behavior is valid.
- """
- if self.source:
- if not self.destinationcal:
- # Don't care about copies/moves to non-calendar destinations
- # In theory this state should not occur here as COPY/MOVE won't call into this as
- # they detect this state and do regular WebDAV copy/move.
- pass
- elif not self.sourcecal:
- # Moving into a calendar requires regular checks
- pass
- else:
- # Calendar to calendar moves are OK if the resource owner is the same.
- # Use resourceOwnerPrincipal for this as that takes into account sharing such that the
- # returned principal relates to the URI path used to access the resource rather than the
- # underlying resource owner (sharee).
- sourceowner = (yield self.sourceparent.resourceOwnerPrincipal(self.request))
- destowner = (yield self.destinationparent.resourceOwnerPrincipal(self.request))
-
- if sourceowner != destowner:
- msg = "Calendar-to-calendar %s with different homes are not supported" % ("moves" if self.deletesource else "copies",)
- log.debug(msg)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
-
- # Calendar to calendar moves where Organizer is present are not OK if the owners are different.
- sourceowner = (yield self.sourceparent.ownerPrincipal(self.request))
- destowner = (yield self.destinationparent.ownerPrincipal(self.request))
-
- if sourceowner != destowner:
- # Now check whether an ORGANIZER property is present in either sourcecal or destcal
- organizer = self.calendar.getOrganizer()
- if organizer is None and self.destination.exists() and self.destinationcal:
- oldCal = yield self.destination.iCalendar()
- organizer = oldCal.getOrganizer()
-
- if organizer is not None:
- msg = "Calendar-to-calendar %s with an organizer property present and different owners are not supported" % ("moves" if self.deletesource else "copies",)
- log.debug(msg)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
-
-
- def validIfScheduleMatch(self):
- """
- Check for If-ScheduleTag-Match header behavior.
- """
- # Only when a direct request
- self.schedule_tag_match = False
- if not self.isiTIP and not self.internal_request:
- header = self.request.headers.getHeader("If-Schedule-Tag-Match")
- if header:
- # If COPY/MOVE get Schedule-Tag on source, PUT use destination
- if self.source:
- matcher = self.source
- self.source.validIfScheduleMatch(self.request)
- else:
- matcher = self.destination
- matcher.validIfScheduleMatch(self.request)
- self.schedule_tag_match = True
- elif config.Scheduling.CalDAV.ScheduleTagCompatibility:
- # Compatibility with old clients. Policy:
- #
- # 1. If If-Match header is not present, never do smart merge.
- # 2. If If-Match is present and the specified ETag is
- # considered a "weak" match to the current Schedule-Tag,
- # then do smart merge, else reject with a 412.
- #
- # Actually by the time we get here the precondition will
- # already have been tested and found to be OK, so we can just
- # always do smart merge now if If-Match is present.
- self.schedule_tag_match = self.request.headers.getHeader("If-Match") is not None
-
-
- def validResourceName(self):
- """
- Make sure that the resource name for the new resource is valid.
- """
- result = True
- message = ""
- filename = self.destination.name()
- if filename.startswith("."):
- result = False
- message = "File name %s not allowed in calendar collection" % (filename,)
-
- return result, message
-
-
- def validContentType(self):
- """
- Make sure that the content-type of the source resource is text/calendar.
- This test is only needed when the source is not in a calendar collection.
- """
- result = True
- message = ""
- content_type = self.source.contentType()
- if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "calendar")):
- result = False
- message = "MIME type %s not allowed in calendar collection" % (content_type,)
-
- return result, message
-
-
- def validContentLength(self):
- """
- Make sure that the length of the source data is within bounds.
- """
- result = True
- message = ""
- if config.MaxResourceSize:
- calsize = self.source.contentLength()
- if calsize is not None and calsize > config.MaxResourceSize:
- result = False
- message = "File size %d bytes is larger than allowed limit %d bytes" % (calsize, config.MaxResourceSize)
-
- return result, message
-
-
- @inlineCallbacks
- def validCollectionSize(self):
- """
- Make sure that any limits on the number of resources in a collection are enforced.
- """
- result = True
- message = ""
- if not self.destination.exists() and \
- config.MaxResourcesPerCollection and \
- (yield self.destinationparent.countChildren()) >= config.MaxResourcesPerCollection:
- result = False
- message = "Too many resources in collection %s" % (self.destinationparent,)
-
- returnValue((result, message,))
-
-
- def validCalendarDataCheck(self):
- """
- Check that the calendar data is valid iCalendar.
- @return: tuple: (True/False if the calendar data is valid,
- log message string).
- """
- result = True
- message = ""
- if self.calendar is None:
- result = False
- message = "Empty resource not allowed in calendar collection"
- else:
- try:
- self.calendar.validCalendarData()
- except ValueError, e:
- result = False
- message = "Invalid calendar data: %s" % (e,)
-
- return result, message
-
-
- def validCalDAVDataCheck(self):
- """
- Check that the calendar data is valid as a CalDAV calendar object resource.
- @return: tuple: (True/False if the calendar data is valid,
- log message string).
- """
- result = True
- message = ""
- try:
- self.calendar.validCalendarForCalDAV(methodAllowed=self.isiTIP)
- except ValueError, e:
- result = False
- message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
-
- return result, message
-
-
- def validComponentType(self):
- """
- Make sure that any limits on the number of resources in a collection are enforced.
- """
- result = True
- message = ""
-
- if not self.destinationparent.isSupportedComponent(self.calendar.mainType()):
- result = False
- message = "Invalid component type %s for calendar: %s" % (self.calendar.mainType(), self.destinationparent,)
-
- return result, message
-
-
- def validSizeCheck(self):
- """
- Make sure that the content-type of the source resource is text/calendar.
- This test is only needed when the source is not in a calendar collection.
- """
- result = True
- message = ""
- if config.MaxResourceSize:
- # FIXME PERF could be done more efficiently?
- calsize = len(str(self.calendar))
- if calsize > config.MaxResourceSize:
- result = False
- message = "Data size %d bytes is larger than allowed limit %d bytes" % (calsize, config.MaxResourceSize)
-
- return result, message
-
-
- @inlineCallbacks
- def validAttendeeListSizeCheck(self):
- """
- Make sure that the Attendee list length is within bounds. We don't do this check for inbox because we
- will assume that the limit has been applied on the PUT causing the iTIP message to be created.
-
- FIXME: The inbox check might not take into account iSchedule stuff from outside. That needs to have
- the max attendees check applied at the time of delivery.
- """
- result = True
- message = ""
- if config.MaxAttendeesPerInstance and not self.isiTIP:
- uniqueAttendees = set()
- for attendee in self.calendar.getAllAttendeeProperties():
- uniqueAttendees.add(attendee.value())
- attendeeListLength = len(uniqueAttendees)
- if attendeeListLength > config.MaxAttendeesPerInstance:
-
- # Check to see whether we are increasing the count on an existing resource
- if self.destination.exists() and self.destinationcal:
- oldcalendar = (yield self.destination.iCalendarForUser(self.request))
- uniqueAttendees = set()
- for attendee in oldcalendar.getAllAttendeeProperties():
- uniqueAttendees.add(attendee.value())
- oldAttendeeListLength = len(uniqueAttendees)
- else:
- oldAttendeeListLength = 0
-
- if attendeeListLength > oldAttendeeListLength:
- result = False
- message = "Attendee list size %d is larger than allowed limit %d" % (attendeeListLength, config.MaxAttendeesPerInstance)
-
- returnValue((result, message,))
-
-
- def validAccess(self):
- """
- Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
- """
-
- if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
-
- # Must be a value we know about
- self.access = self.calendar.accessLevel(default=None)
- if self.access is None:
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (calendarserver_namespace, "valid-access-restriction"),
- "Private event access level not allowed",
- ))
-
- # Only DAV:owner is able to set the property to other than PUBLIC
- if not self.internal_request:
- def _callback(parent_owner):
-
- authz = self.destinationparent.currentPrincipal(self.request)
- if davxml.Principal(parent_owner) != authz and self.access != Component.ACCESS_PUBLIC:
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (calendarserver_namespace, "valid-access-restriction-change"),
- "Private event access level change not allowed",
- ))
-
- return None
-
- d = self.destinationparent.owner(self.request)
- d.addCallback(_callback)
- return d
- else:
- # Check whether an access property was present before and write that into the calendar data
- if not self.source and self.destination.exists() and self.destination.accessMode:
- old_access = self.destination.accessMode
- self.calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
-
- return succeed(None)
-
-
- @inlineCallbacks
- def validLocationResourceOrganizer(self):
- """
- If the calendar owner is a location or resource, check whether an ORGANIZER property is required.
- """
-
- if not self.internal_request:
- originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
- cutype = originatorPrincipal.getCUType() if originatorPrincipal is not None else "INDIVIDUAL"
- organizer = self.calendar.getOrganizer()
-
- # Check for an allowed change
- if organizer is None and (
- cutype == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
- cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (calendarserver_namespace, "valid-organizer"),
- "Organizer required in calendar data",
- ))
-
- # Check for tracking the modifier
- if organizer is None and (
- cutype == "ROOM" and config.Scheduling.Options.TrackUnscheduledLocationData or
- cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData):
-
- # Find current principal
- authz = None
- authz_principal = self.destinationparent.currentPrincipal(self.request).children[0]
- if isinstance(authz_principal, davxml.HRef):
- principalURL = str(authz_principal)
- if principalURL:
- authz = (yield self.request.locateResource(principalURL))
-
- if authz is not None:
- prop = Property("X-CALENDARSERVER-MODIFIED-BY", "urn:uuid:%s" % (authz.record.guid,))
- prop.setParameter("CN", authz.displayName())
- for candidate in authz.calendarUserAddresses():
- if candidate.startswith("mailto:"):
- prop.setParameter("EMAIL", candidate[7:])
- break
- self.calendar.replacePropertyInAllComponents(prop)
- else:
- self.calendar.removeAllPropertiesWithName("X-CALENDARSERVER-MODIFIED-BY")
- self.dataChanged = True
-
-
- @inlineCallbacks
- def preservePrivateComments(self):
- # Check for private comments on the old resource and the new resource and re-insert
- # ones that are lost.
- #
- # NB Do this before implicit scheduling as we don't want old clients to trigger scheduling when
- # the X- property is missing.
- self.hasPrivateComments = False
- if config.Scheduling.CalDAV.get("EnablePrivateComments", True) and self.calendar is not None:
- old_has_private_comments = self.destination.exists() and self.destinationcal and self.destination.hasPrivateComment
- self.hasPrivateComments = self.calendar.hasPropertyInAnyComponent((
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
-
- if old_has_private_comments and not self.hasPrivateComments:
- # Transfer old comments to new calendar
- log.debug("Private Comments properties were entirely removed by the client. Restoring existing properties.")
- old_calendar = (yield self.destination.iCalendarForUser(self.request))
- self.calendar.transferProperties(old_calendar, (
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
-
-
- @inlineCallbacks
- def replaceMissingToDoProperties(self):
- """
- Recover any lost ORGANIZER or ATTENDEE properties in non-recurring VTODOs.
- """
-
- if self.destination.exists() and self.calendar.resourceType() == "VTODO" and not self.calendar.isRecurring():
-
- old_calendar = (yield self.destination.iCalendarForUser(self.request))
-
- new_organizer = self.calendar.getOrganizer()
- old_organizer = old_calendar.getOrganizerProperty()
- new_attendees = self.calendar.getAttendees()
- old_attendees = tuple(old_calendar.getAllAttendeeProperties())
-
- new_completed = self.calendar.mainComponent().hasProperty("COMPLETED")
- old_completed = old_calendar.mainComponent().hasProperty("COMPLETED")
-
- if old_organizer and not new_organizer and len(old_attendees) > 0 and len(new_attendees) == 0:
- # Transfer old organizer and attendees to new calendar
- log.debug("Organizer and attendee properties were entirely removed by the client. Restoring existing properties.")
-
- # Get the originator who is the owner of the calendar resource being modified
- originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
- originatorAddresses = originatorPrincipal.calendarUserAddresses()
-
- for component in self.calendar.subcomponents():
- if component.name() != "VTODO":
- continue
-
- if not component.hasProperty("DTSTART"):
- # Need to put DTSTART back in or we get a date mismatch failure later
- for old_component in old_calendar.subcomponents():
- if old_component.name() != "VTODO":
- continue
- if old_component.hasProperty("DTSTART"):
- component.addProperty(old_component.getProperty("DTSTART").duplicate())
- break
-
- # Add organizer back in from previous resource
- component.addProperty(old_organizer.duplicate())
-
- # Add attendees back in from previous resource
- for anAttendee in old_attendees:
- anAttendee = anAttendee.duplicate()
- if component.hasProperty("COMPLETED") and anAttendee.value() in originatorAddresses:
- anAttendee.setParameter("PARTSTAT", "COMPLETED")
- component.addProperty(anAttendee)
-
- elif new_completed ^ old_completed and not self.internal_request:
- # COMPLETED changed - sync up attendee state
- # We need this because many VTODO clients are not aware of scheduling,
- # i.e. they do not adjust any ATTENDEE PARTSTATs. We are going to impose
- # our own requirement that PARTSTAT is set to COMPLETED when the COMPLETED
- # property is added.
-
- # Transfer old organizer and attendees to new calendar
- log.debug("Sync COMPLETED property change.")
-
- # Get the originator who is the owner of the calendar resource being modified
- originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
- originatorAddresses = originatorPrincipal.calendarUserAddresses()
-
- for component in self.calendar.subcomponents():
- if component.name() != "VTODO":
- continue
-
- # Change owner partstat
- for anAttendee in component.properties("ATTENDEE"):
- if anAttendee.value() in originatorAddresses:
- oldpartstat = anAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
- newpartstat = "COMPLETED" if component.hasProperty("COMPLETED") else "IN-PROCESS"
- if newpartstat != oldpartstat:
- anAttendee.setParameter("PARTSTAT", newpartstat)
-
-
- @inlineCallbacks
- def dropboxPathNormalization(self):
- """
- Make sure sharees only use dropbox paths of the sharer.
- """
-
- # Only relevant if calendar is sharee collection
- if self.destinationparent.isShareeCollection():
-
- # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
- xdropboxes = self.calendar.getAllPropertiesInAnyComponent(
- "X-APPLE-DROPBOX",
- depth=1,
- )
- attachments = self.calendar.getAllPropertiesInAnyComponent(
- "ATTACH",
- depth=1,
- )
- attachments = [
- attachment for attachment in attachments
- if attachment.parameterValue("VALUE", "TEXT") == "URI" and attachment.value().startswith("http")
- ]
-
- if len(xdropboxes) or len(attachments):
-
- # Determine owner GUID
- ownerPrincipal = (yield self.destinationparent.ownerPrincipal(self.request))
- owner = ownerPrincipal.principalURL().split("/")[-2]
-
- def uriNormalize(uri):
- urichanged = False
- scheme, netloc, path, params, query, fragment = urlparse(uri)
- pathbits = path.split("/")
- if pathbits[1] != "calendars":
- pathbits[1] = "calendars"
- urichanged = True
- if pathbits[2] != "__uids__":
- pathbits[2] = "__uids__"
- urichanged = True
- if pathbits[3] != owner:
- pathbits[3] = owner
- urichanged = True
- if urichanged:
- return urlunparse((scheme, netloc, "/".join(pathbits), params, query, fragment,))
- return None
-
- for xdropbox in xdropboxes:
- uri = uriNormalize(xdropbox.value())
- if uri:
- xdropbox.setValue(uri)
- self.dataChanged = True
- for attachment in attachments:
- uri = uriNormalize(attachment.value())
- if uri:
- attachment.setValue(uri)
- self.dataChanged = True
-
-
- def processAlarms(self):
- """
- Remove duplicate alarms. Add a default alarm if required.
- """
-
- # Remove duplicate alarms
- if config.RemoveDuplicateAlarms and self.calendar.hasDuplicateAlarms(doFix=True):
- self.dataChanged = True
-
- # Only if feature enabled
- if not config.EnableDefaultAlarms:
- return
-
- # Check that we are creating and this is not the inbox
- if not self.destinationcal or self.destination.exists() or self.isiTIP:
- return
-
- # Never add default alarms to calendar data in shared calendars
- if self.destinationparent.isShareeCollection():
- return
-
- # Add default alarm for VEVENT and VTODO only
- mtype = self.calendar.mainType().upper()
- if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
- return
- vevent = mtype == "VEVENT"
-
- # Check timed or all-day
- start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
- if start is None:
- # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
- return
- timed = not start.isDateOnly()
-
- # See if default exists and add using appropriate logic
- alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
- if alarm and self.calendar.addAlarms(alarm):
- self.dataChanged = True
-
-
- @inlineCallbacks
- def noUIDConflict(self, uid):
- """
- Check that the UID of the new calendar object conforms to the requirements of
- CalDAV, i.e. it must be unique in the collection and we must not overwrite a
- different UID.
- @param uid: the UID for the resource being stored.
- @return: tuple: (True/False if the UID is valid, log message string,
- name of conflicted resource).
- """
-
- result = True
- message = ""
- rname = ""
-
- # Adjust for a move into same calendar collection
- oldname = None
- if self.sourceparent and (self.sourceparent == self.destinationparent) and self.deletesource:
- oldname = self.source.name()
-
- # UID must be unique
- index = self.destinationparent.index()
- if not (yield index.isAllowedUID(uid, oldname, self.destination.name())):
- rname = yield index.resourceNameForUID(uid)
- # This can happen if two simultaneous PUTs occur with the same UID.
- # i.e. one PUT has reserved the UID but has not yet written the resource,
- # the other PUT tries to reserve and fails but no index entry exists yet.
- if rname is None:
- rname = "<<Unknown Resource>>"
- result = False
- message = "Calendar resource %s already exists with same UID %s" % (rname, uid)
- else:
- # Cannot overwrite a resource with different UID
- if self.destination.exists():
- olduid = yield index.resourceUIDForName(self.destination.name())
- if olduid != uid:
- rname = self.destination.name()
- result = False
- message = "Cannot overwrite calendar resource %s with different UID %s" % (rname, olduid)
-
- returnValue((result, message, rname))
-
-
- @inlineCallbacks
- def hasCalendarResourceUIDSomewhereElse(self, uid):
- """
- See if a calendar component with a matching UID exists anywhere in the calendar home of the
- current recipient owner and is not the resource being targeted.
- """
-
- # Ignore for an overwrite or a MOVE
- if self.destination.exists() or self.sourceparent and self.deletesource:
- returnValue(None)
-
- failed = False
-
- # Always fail a copy
- if self.sourceparent and self.sourcecal and not self.deletesource and self.destinationcal:
- failed = True
- else:
- # Get owner's calendar-home
- calendar_owner_principal = (yield self.destination.resourceOwnerPrincipal(self.request))
- calendar_home = yield calendar_owner_principal.calendarHome(self.request)
-
- # Check for matching resource somewhere else in the home use the "schedule" mode to prevent any kind of match
- failed = (yield calendar_home.hasCalendarResourceUIDSomewhereElse(uid, self.destination, "schedule"))
-
- if failed:
- log.debug("Implicit - found component with same UID in a different collection: %s" % (self.destination_uri,))
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "unique-scheduling-object-resource"),
- "Cannot duplicate scheduling object resource",
- ))
-
-
- @inlineCallbacks
- def doImplicitScheduling(self):
-
- data_changed = False
- did_implicit_action = False
-
- # Do scheduling
- if not self.isiTIP:
- scheduler = ImplicitScheduler()
-
- # Determine type of operation PUT, COPY or DELETE
- if not self.source:
- # PUT
- do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingPUT(
- self.request,
- self.destination,
- self.destination_uri,
- self.calendar,
- internal_request=self.internal_request,
- ))
- elif self.deletesource:
- # MOVE
- do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingMOVE(
- self.request,
- self.source,
- self.sourcecal,
- self.source_uri,
- self.destination,
- self.destinationcal,
- self.destination_uri,
- self.calendar,
- internal_request=self.internal_request,
- ))
- else:
- # COPY
- do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingCOPY(
- self.request,
- self.source,
- self.sourcecal,
- self.source_uri,
- self.destination,
- self.destinationcal,
- self.destination_uri,
- self.calendar,
- internal_request=self.internal_request,
- ))
-
- if do_implicit_action and self.allowImplicitSchedule:
-
- # Cannot do implicit in sharee's shared calendar
- isShareeCollection = self.destinationparent.isShareeCollection()
- if isShareeCollection:
- scheduler.setSchedulingNotAllowed(
- HTTPError,
- ErrorResponse(
- responsecode.FORBIDDEN,
- (calendarserver_namespace, "sharee-privilege-needed",),
- description="Sharee's cannot schedule",
- ),
- )
-
- new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match))
- if new_calendar:
- if isinstance(new_calendar, int):
- returnValue(new_calendar)
- else:
- self.calendar = new_calendar
- data_changed = True
- did_implicit_action = True
- else:
- is_scheduling_resource = False
-
- returnValue((is_scheduling_resource, data_changed, did_implicit_action,))
-
-
- @inlineCallbacks
- def mergePerUserData(self):
- if self.calendar:
- accessUID = (yield self.destination.resourceOwnerPrincipal(self.request))
- accessUID = accessUID.principalUID() if accessUID else ""
- if self.destination.exists() and self.destinationcal:
- oldCal = yield self.destination.iCalendar()
- else:
- oldCal = None
-
- # Duplicate before we do the merge because someone else may "own" the calendar object
- # and we should not change it. This is not ideal as we may duplicate it unnecessarily
- # but we currently have no api to let the caller tell us whether it cares about the
- # whether the calendar data is changed or not.
- try:
- self.calendar = PerUserDataFilter(accessUID).merge(self.calendar.duplicate(), oldCal)
- except ValueError:
- log.err("Invalid per-user data merge")
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- "Cannot merge per-user data",
- ))
-
-
- @inlineCallbacks
- def doStore(self, implicit):
-
- # Stash the current calendar data as we may need to return it
- if self.returnData:
- self.storeddata = str(self.calendar)
-
- # Always do the per-user data merge right before we store
- yield self.mergePerUserData()
-
- # Do put or copy based on whether source exists
- source = self.source
- if source is not None:
- # Retrieve information from the source, in case we have to delete
- # it.
- sourceProperties = dict(source.newStoreProperties().iteritems())
- if not implicit:
- # Only needed in implicit case; see below.
- sourceText = yield source.iCalendarText()
-
- # Delete the original source if needed (for example, if this is a
- # same-calendar MOVE of a calendar object, implemented as an
- # effective DELETE-then-PUT).
- if self.deletesource:
- yield self.doSourceDelete()
-
- if implicit:
- response = (yield self.doStorePut())
- else:
- response = (yield self.doStorePut(sourceText))
- self.destination.newStoreProperties().update(sourceProperties)
- else:
- response = (yield self.doStorePut())
-
- returnValue(response)
-
-
- @inlineCallbacks
- def doStorePut(self, data=None):
-
- if data is None:
- # We'll be passing this component directly to storeComponent( )
- componentToStore = self.calendar
- data = str(self.calendar)
- else:
- # We'll be passing data as a stream to storeStream( )
- componentToStore = None
-
- # Update calendar-access property value on the resource. We need to do this before the
- # store as the store will "commit" the new value.
- if self.access:
- self.destination.accessMode = self.access
-
- # Do not remove the property if access was not specified and we are storing in a calendar.
- # This ensure that clients that do not preserve the iCalendar property do not cause access
- # restrictions to be lost.
- elif not self.destinationcal:
- self.destination.accessMode = ""
-
- # Check for existence of private comments and write property
- if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
- self.destination.hasPrivateComment = self.hasPrivateComments
-
- # Check for scheduling object resource and write property
- self.destination.isScheduleObject = self.isScheduleResource
- if self.isScheduleResource:
- # Need to figure out when to change the schedule tag:
- #
- # 1. If this is not an internal request then the resource is being explicitly changed
- # 2. If it is an internal request for the Organizer, schedule tag never changes
- # 3. If it is an internal request for an Attendee and the message being processed came
- # from the Organizer then the schedule tag changes.
-
- change_scheduletag = True
- if self.internal_request:
- # Check what kind of processing is going on
- if self.processing_organizer == True:
- # All auto-processed updates for an Organizer leave the tag unchanged
- change_scheduletag = False
- elif self.processing_organizer == False:
- # Auto-processed updates that are the result of an organizer "refresh" due
- # to another Attendee's REPLY should leave the tag unchanged
- change_scheduletag = not hasattr(self.request, "doing_attendee_refresh")
-
- if change_scheduletag or not self.destination.scheduleTag:
- self.destination.scheduleTag = str(uuid.uuid4())
-
- # Handle weak etag compatibility
- if config.Scheduling.CalDAV.ScheduleTagCompatibility:
- if change_scheduletag:
- # Schedule-Tag change => weak ETag behavior must not happen
- etags = ()
- else:
- # Schedule-Tag did not change => add current ETag to list of those that can
- # be used in a weak precondition test
- etags = self.destination.scheduleEtags
- if etags is None:
- etags = ()
- etags += (hashlib.md5(data + (self.destination.scheduleTag if self.destination.scheduleTag else "")).hexdigest(),)
- self.destination.scheduleEtags = etags
- else:
- self.destination.scheduleEtags = ()
- else:
- self.destination.scheduleTag = ""
- self.destination.scheduleEtags = ()
-
- if componentToStore is None:
- stream = MemoryStream(data)
- response = yield self.destination.storeStream(stream)
- else:
- # Since we already have a component, we can pass it directly
- response = yield self.destination.storeComponent(componentToStore)
- response = IResponse(response)
-
- if self.isScheduleResource:
- # Add a response header
- response.headers.setHeader("Schedule-Tag", self.destination.scheduleTag)
-
- returnValue(response)
-
-
- @inlineCallbacks
- def doSourceDelete(self):
- # Delete the source resource
- yield self.source.storeRemove(self.request, False, self.source_uri)
- log.debug("Source removed %s" % (self.source,))
- returnValue(None)
-
-
- @inlineCallbacks
- def run(self):
- """
- Function that does common PUT/COPY/MOVE behavior.
-
- @return: a Deferred with a status response result.
- """
-
- try:
- reservation = None
-
- # Handle all validation operations here.
- yield self.fullValidation()
-
- # Reservation and UID conflict checking is next.
- if self.destinationcal:
- # Reserve UID
- self.destination_index = self.destinationparent.index()
- reservation = StoreCalendarObjectResource.UIDReservation(
- self.destination_index, self.uid, self.destination_uri,
- self.internal_request or self.isiTIP,
- self.destination._associatedTransaction,
- )
- yield reservation.reserve()
- # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
- # try to write the same calendar data to two different resource URIs.
- if not self.isiTIP:
- result, message, rname = yield self.noUIDConflict(self.uid)
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- NoUIDConflict(
- davxml.HRef.fromString(
- joinURL(
- parentForURL(self.destination_uri),
- rname.encode("utf-8")
- )
- )
- ),
- "UID already exists",
- ))
- yield self.hasCalendarResourceUIDSomewhereElse(self.uid)
-
- # Preserve private comments
- yield self.preservePrivateComments()
-
- # Fix broken VTODOs
- yield self.replaceMissingToDoProperties()
-
- # Handle sharing dropbox normalization
- yield self.dropboxPathNormalization()
-
- # Pre-process managed attachments
- if not self.internal_request and not self.attachmentProcessingDone:
- managed_copied, managed_removed = (yield self.destination.preProcessManagedAttachments(self.calendar))
-
- # Default/duplicate alarms
- self.processAlarms()
-
- # Do scheduling
- implicit_result = (yield self.doImplicitScheduling())
- if isinstance(implicit_result, int):
- if implicit_result == ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT:
- if reservation:
- yield reservation.unreserve()
-
- returnValue(StatusResponse(responsecode.CREATED, "Resource created but immediately deleted by the server."))
-
- elif implicit_result == ImplicitScheduler.STATUS_ORPHANED_EVENT:
- if reservation:
- yield reservation.unreserve()
-
- # Now forcibly delete the event
- if self.destination.exists():
- yield self.destination.storeRemove(self.request, False, self.destination_uri)
- else:
- msg = "Attendee cannot create event for Organizer: %s" % (implicit_result,)
- log.err(msg)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "attendee-allowed"),
- description=msg
- ))
-
- returnValue(StatusResponse(responsecode.OK, "Resource modified but immediately deleted by the server."))
-
- else:
- msg = "Invalid return status code from ImplicitScheduler: %s" % (implicit_result,)
- log.err(msg)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- description=msg
- ))
- else:
- self.isScheduleResource, data_changed, did_implicit_action = implicit_result
-
- # Do the actual put or copy
- response = (yield self.doStore(data_changed))
-
- # Post process managed attachments
- if not self.internal_request and not self.attachmentProcessingDone:
- yield self.destination.postProcessManagedAttachments(managed_copied, managed_removed)
-
- # Must not set ETag in response if data changed
- if did_implicit_action or self.dataChanged:
- def _removeEtag(request, response):
- response.headers.removeHeader('etag')
- return response
- _removeEtag.handleErrors = True
-
- self.request.addResponseFilter(_removeEtag, atEnd=True)
-
- if reservation:
- yield reservation.unreserve()
-
- returnValue(response)
-
- except Exception, err:
-
- # Grab the current exception state here so we can use it in a re-raise - we need this because
- # an inlineCallback might be called and that raises an exception when it returns, wiping out the
- # original exception "context".
- ex = Failure()
-
- if reservation:
- yield reservation.unreserve()
-
- if isinstance(err, InvalidOverriddenInstanceError):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- description="Invalid overridden instance"
- ))
- elif isinstance(err, TooManyInstancesError):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- MaxInstances.fromString(str(err.max_allowed)),
- "Too many recurrence instances",
- ))
- elif isinstance(err, AttachmentStoreValidManagedID):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-managed-id"),
- "Invalid Managed-ID parameter in calendar data",
- ))
- else:
- # Return the original failure (exception) state
- ex.raiseException()
-
-
- @inlineCallbacks
- def moveValidation(self):
- """
- Do full validation of source and destination calendar data.
- """
-
- # Basic validation
- self.validIfScheduleMatch()
-
- # Valid resource name check
- result, message = self.validResourceName()
- if not result:
- log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
-
- # Valid collection size check on the destination parent resource
- result, message = (yield self.validCollectionSize())
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- customxml.MaxResources(),
- message,
- ))
-
- # Check that moves to shared calendars are OK
- yield self.validCopyMoveOperation()
-
- returnValue(None)
-
-
- @inlineCallbacks
- def doStoreMove(self):
-
- # Do move
- response = (yield self.source.storeMove(self.request, self.destinationparent, self.destination._name))
- returnValue(response)
-
-
- @inlineCallbacks
- def move(self):
- """
- Function that does common MOVE behavior.
-
- @return: a Deferred with a status response result.
- """
-
- try:
- reservation = None
-
- # Handle all validation operations here.
- self.calendar = (yield self.source.iCalendarForUser(self.request))
- yield self.moveValidation()
-
- # Reservation and UID conflict checking is next.
-
- # Reserve UID
- self.destination_index = self.destinationparent.index()
- reservation = StoreCalendarObjectResource.UIDReservation(
- self.destination_index, self.source.uid(), self.destination_uri,
- self.internal_request or self.isiTIP,
- self.destination._associatedTransaction,
- )
- yield reservation.reserve()
- # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
- # try to write the same calendar data to two different resource URIs.
- if not self.isiTIP:
- result, message, rname = yield self.noUIDConflict(self.source.uid())
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- NoUIDConflict(
- davxml.HRef.fromString(
- joinURL(
- parentForURL(self.destination_uri),
- rname.encode("utf-8")
- )
- )
- ),
- "UID already exists",
- ))
-
- # Do the actual put or copy
- response = (yield self.doStoreMove())
-
- if reservation:
- yield reservation.unreserve()
-
- returnValue(response)
-
- except Exception, err:
-
- # Grab the current exception state here so we can use it in a re-raise - we need this because
- # an inlineCallback might be called and that raises an exception when it returns, wiping out the
- # original exception "context".
- ex = Failure()
-
- if reservation:
- yield reservation.unreserve()
-
- if isinstance(err, InvalidOverriddenInstanceError):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- description="Invalid overridden instance"
- ))
- elif isinstance(err, TooManyInstancesError):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- MaxInstances.fromString(str(err.max_allowed)),
- "Too many recurrence instances",
- ))
- elif isinstance(err, AttachmentStoreValidManagedID):
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-managed-id"),
- "Invalid Managed-ID parameter in calendar data",
- ))
- else:
- # Return the original failure (exception) state
- ex.raiseException()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130425/0ba99e6e/attachment-0001.html>
More information about the calendarserver-changes
mailing list