# -*- test-case-name: twistedcaldav.test.test_wrapping -*-
##
# Copyright (c) 2005-2014 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

from pycalendar.datetime import PyCalendarDateTime

from twext.enterprise.locking import LockTimeout
from twext.python.log import Logger
from twext.web2 import responsecode, http_headers, http
from twext.web2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
from twext.web2.dav.noneprops import NonePropertyStore
from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError, \
    davPrivilegeSet
from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
from twext.web2.filter.location import addLocation
from twext.web2.http import HTTPError, StatusResponse, Response
from twext.web2.http_headers import ETag, MimeType, MimeDisposition
from twext.web2.iweb import IResponse
from twext.web2.responsecode import \
    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, \
    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
from twext.web2.stream import ProducerStream, readStream, MemoryStream

from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
from twisted.internet.protocol import Protocol
from twisted.python.hashlib import md5
from twisted.python.util import FancyEqMixin

from twistedcaldav import customxml, carddavxml, caldavxml
from twistedcaldav.caldavxml import caldav_namespace, MaxAttendeesPerInstance, \
    MaxInstances, NoUIDConflict
from twistedcaldav.carddavxml import carddav_namespace, NoUIDConflict as NovCardUIDConflict
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.directory.wiki import WikiDirectoryService, getWikiAccess
from twistedcaldav.ical import Component as VCalendar, Property as VProperty, \
    InvalidICalendarDataError, iCalendarProductID, allowedComponents, Component
from twistedcaldav.instance import InvalidOverriddenInstanceError, \
    TooManyInstancesError
from twistedcaldav.memcachelock import MemcacheLockTimeoutError
from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource, \
    DefaultAlarmPropertyMixin
from twistedcaldav.scheduling_store.caldav.resource import ScheduleInboxResource
from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError

from txdav.base.propertystore.base import PropertyName
from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed, \
    AttachmentStoreValidManagedID, AttachmentRemoveFailed, \
    AttachmentDropboxNotAllowed, InvalidComponentTypeError, \
    TooManyAttendeesError, InvalidCalendarAccessError, ValidOrganizerError, \
    InvalidPerUserDataMerge, \
    AttendeeAllowedError, ResourceDeletedError, InvalidAttachmentOperation, \
    ShareeAllowedError, DuplicatePrivateCommentsError, InvalidSplit
from txdav.carddav.iaddressbookstore import KindChangeNotAllowedError, \
    GroupWithUnsharedAddressNotAllowedError
from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE, \
    _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
from txdav.common.icommondatastore import NoSuchObjectResourceError, \
    TooManyObjectResourcesError, ObjectResourceTooBigError, \
    InvalidObjectResourceError, ObjectResourceNameNotAllowedError, \
    ObjectResourceNameAlreadyExistsError, UIDExistsError, \
    UIDExistsElsewhereError, InvalidUIDError, InvalidResourceMove, \
    InvalidComponentForStoreError
from txdav.idav import PropertyChangeNotAllowedError
from txdav.xml import element as davxml
from txdav.xml.base import dav_namespace, WebDAVUnknownElement, encodeXMLName

from urlparse import urlsplit
import collections
import hashlib
import time
import uuid
"""
Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
"""

log = Logger()

class _NewStorePropertiesWrapper(object):
    """
    Wrap a new-style property store (a L{txdav.idav.IPropertyStore}) in the old-
    style interface for compatibility with existing code.
    """

    # FIXME: UID arguments on everything need to be tested against something.
    def __init__(self, newPropertyStore):
        """
        Initialize an old-style property store from a new one.

        @param newPropertyStore: the new-style property store.
        @type newPropertyStore: L{txdav.idav.IPropertyStore}
        """
        self._newPropertyStore = newPropertyStore


    @classmethod
    def _convertKey(cls, qname):
        namespace, name = qname
        return PropertyName(namespace, name)


    def get(self, qname):
        try:
            return self._newPropertyStore[self._convertKey(qname)]
        except KeyError:
            raise HTTPError(StatusResponse(
                NOT_FOUND,
                "No such property: %s" % (encodeXMLName(*qname),)
            ))


    def set(self, prop):
        try:
            self._newPropertyStore[self._convertKey(prop.qname())] = prop
        except PropertyChangeNotAllowedError:
            raise HTTPError(StatusResponse(
                FORBIDDEN,
                "Property cannot be changed: %s" % (prop.sname(),)
            ))


    def delete(self, qname):
        try:
            del self._newPropertyStore[self._convertKey(qname)]
        except KeyError:
            # RFC 2518 Section 12.13.1 says that removal of
            # non-existing property is not an error.
            pass


    def contains(self, qname):
        return (self._convertKey(qname) in self._newPropertyStore)


    def list(self):
        return [(pname.namespace, pname.name) for pname in
                self._newPropertyStore.keys()]



def requiresPermissions(*permissions, **kw):
    """
    A decorator to wrap http_ methods in, to indicate that they should not be
    run until the current user principal has been authorized for the given
    permission set.
    """
    fromParent = kw.get('fromParent')
    # FIXME: direct unit tests
    def wrap(thunk):
        def authAndContinue(self, request, *args, **kwargs):
            if permissions:
                d = self.authorize(request, permissions)
            else:
                d = succeed(None)
            if fromParent:
                d.addCallback(
                    lambda whatever:
                        request.locateResource(parentForURL(request.uri))
                ).addCallback(
                    lambda parent:
                        parent.authorize(request, fromParent)
                )
            d.addCallback(lambda whatever: thunk(self, request, *args, **kwargs))
            return d
        return authAndContinue
    return wrap



class _NewStoreFileMetaDataHelper(object):

    def exists(self):
        return self._newStoreObject is not None


    def name(self):
        return self._newStoreObject.name() if self._newStoreObject is not None else self._name


    def etag(self):
        return succeed(ETag(self._newStoreObject.md5()) if self._newStoreObject is not None else None)


    def contentType(self):
        return self._newStoreObject.contentType() if self._newStoreObject is not None else None


    def contentLength(self):
        return self._newStoreObject.size() if self._newStoreObject is not None else None


    def lastModified(self):
        return self._newStoreObject.modified() if self._newStoreObject is not None else None


    def creationDate(self):
        return self._newStoreObject.created() if self._newStoreObject is not None else None


    def newStoreProperties(self):
        return self._newStoreObject.properties() if self._newStoreObject is not None else None



class _CommonHomeChildCollectionMixin(object):
    """
    Methods for things which are like calendars.
    """

    _childClass = None

    def _initializeWithHomeChild(self, child, home):
        """
        Initialize with a home child object.

        @param child: the new store home child object.
        @type calendar: L{txdav.common._.CommonHomeChild}

        @param home: the home through which the given home child was accessed.
        @type home: L{txdav.common._.CommonHome}
        """
        self._newStoreObject = child
        self._newStoreParentHome = home._newStoreHome
        self._parentResource = home
        self._dead_properties = _NewStorePropertiesWrapper(
            self._newStoreObject.properties()
        ) if self._newStoreObject else NonePropertyStore(self)


    def liveProperties(self):

        props = super(_CommonHomeChildCollectionMixin, self).liveProperties()

        if config.MaxResourcesPerCollection:
            props += (customxml.MaxResources.qname(),)

        if config.EnableBatchUpload:
            props += (customxml.BulkRequests.qname(),)

        return props


    @inlineCallbacks
    def readProperty(self, prop, request):
        if type(prop) is tuple:
            qname = prop
        else:
            qname = prop.qname()

        if qname == customxml.MaxResources.qname() and config.MaxResourcesPerCollection:
            returnValue(customxml.MaxResources.fromString(config.MaxResourcesPerCollection))

        returnValue((yield super(_CommonHomeChildCollectionMixin, self).readProperty(prop, request)))


    def url(self):
        return joinURL(self._parentResource.url(), self._name, "/")


    def owner_url(self):
        if self.isShareeResource():
            return joinURL(self._share.url(), "/")
        else:
            return self.url()


    def parentResource(self):
        return self._parentResource


    def index(self):
        """
        Retrieve the new-style index wrapper.
        """
        return self._newStoreObject.retrieveOldIndex()


    def exists(self):
        # FIXME: tests
        return self._newStoreObject is not None


    @inlineCallbacks
    def _indexWhatChanged(self, revision, depth):
        # The newstore implementation supports this directly
        returnValue(
            (yield self._newStoreObject.resourceNamesSinceToken(revision))
            + ([],)
        )


    @inlineCallbacks
    def makeChild(self, name):
        """
        Create a L{CalendarObjectResource} based on a calendar object name.
        """

        if self._newStoreObject:
            newStoreObject = yield self._newStoreObject.objectResourceWithName(name)

            similar = self._childClass(
                newStoreObject,
                self._newStoreObject,
                self,
                name,
                principalCollections=self._principalCollections
            )

            self.propagateTransaction(similar)
            returnValue(similar)
        else:
            returnValue(NoParent())


    @inlineCallbacks
    def listChildren(self):
        """
        @return: a sequence of the names of all known children of this resource.
        """
        children = set(self.putChildren.keys())
        children.update((yield self._newStoreObject.listObjectResources()))
        returnValue(sorted(children))


    def countChildren(self):
        """
        @return: L{Deferred} with the count of all known children of this resource.
        """
        return self._newStoreObject.countObjectResources()


    def name(self):
        return self._name


    @inlineCallbacks
    def etag(self):
        """
        Use the sync token as the etag
        """
        if self._newStoreObject:
            token = (yield self.getInternalSyncToken())
            returnValue(ETag(hashlib.md5(token).hexdigest()))
        else:
            returnValue(None)


    def lastModified(self):
        return self._newStoreObject.modified() if self._newStoreObject else None


    def creationDate(self):
        return self._newStoreObject.created() if self._newStoreObject else None


    def getInternalSyncToken(self):
        return self._newStoreObject.syncToken() if self._newStoreObject else None


    def resourceID(self):
        rid = "%s/%s" % (self._newStoreParentHome.id(), self._newStoreObject.id(),)
        return uuid.uuid5(self.uuid_namespace, rid).urn


    @inlineCallbacks
    def findChildrenFaster(
        self, depth, request, okcallback, badcallback, missingcallback, unavailablecallback,
        names, privileges, inherited_aces
    ):
        """
        Override to pre-load children in certain collection types for better performance.
        """

        if depth == "1":
            if names:
                yield self._newStoreObject.objectResourcesWithNames(names)
            else:
                yield self._newStoreObject.objectResources()

        result = (yield super(_CommonHomeChildCollectionMixin, self).findChildrenFaster(
            depth, request, okcallback, badcallback, missingcallback, unavailablecallback, names, privileges, inherited_aces
        ))

        returnValue(result)


    @inlineCallbacks
    def createCollection(self):
        """
        Override C{createCollection} to actually do the work.
        """
        self._newStoreObject = (yield self._newStoreParentHome.createChildWithName(self._name))

        # Re-initialize to get stuff setup again now we have a "real" object
        self._initializeWithHomeChild(self._newStoreObject, self._parentResource)

        returnValue(CREATED)


    @requiresPermissions(fromParent=[davxml.Unbind()])
    @inlineCallbacks
    def http_DELETE(self, request):
        """
        Override http_DELETE to validate 'depth' header.
        """

        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        depth = request.headers.getHeader("depth", "infinity")
        if depth != "infinity":
            msg = "illegal depth header for DELETE on collection: %s" % (
                depth,
            )
            log.error(msg)
            raise HTTPError(StatusResponse(BAD_REQUEST, msg))
        response = (yield self.storeRemove(request))
        returnValue(response)


    @inlineCallbacks
    def storeRemove(self, request):
        """
        Delete this collection resource, first deleting each contained
        object resource.

        This has to emulate the behavior in fileop.delete in that any errors
        need to be reported back in a multistatus response.

        @param request: The request used to locate child resources.  Note that
            this is the request which I{triggered} the C{DELETE}, but which may
            not actually be a C{DELETE} request itself.

        @type request: L{twext.web2.iweb.IRequest}

        @return: an HTTP response suitable for sending to a client (or
            including in a multi-status).

        @rtype: something adaptable to L{twext.web2.iweb.IResponse}
        """

        # Check sharee collection first
        if self.isShareeResource():
            log.debug("Removing shared collection %s" % (self,))
            yield self.removeShareeResource(request)
            returnValue(NO_CONTENT)

        log.debug("Deleting collection %s" % (self,))

        # 'deluri' is this resource's URI; I should be able to synthesize it
        # from 'self'.

        errors = ResponseQueue(request.uri, "DELETE", NO_CONTENT)

        for childname in (yield self.listChildren()):

            childurl = joinURL(request.uri, childname)

            # FIXME: use a more specific API; we should know what this child
            # resource is, and not have to look it up.  (Sharing information
            # needs to move into the back-end first, though.)
            child = (yield request.locateChildResource(self, childname))

            try:
                yield child.storeRemove(request)
            except:
                log.failure("storeRemove({request})", request=request)
                errors.add(childurl, BAD_REQUEST)

        # Now do normal delete

        # Handle sharing
        wasShared = self.isShared()
        if wasShared:
            yield self.downgradeFromShare(request)

        # Actually delete it.
        yield self._newStoreObject.remove()

        # Re-initialize to get stuff setup again now we have no object
        self._initializeWithHomeChild(None, self._parentResource)

        # FIXME: handle exceptions, possibly like this:

        #        if isinstance(more_responses, MultiStatusResponse):
        #            # Merge errors
        #            errors.responses.update(more_responses.children)

        response = errors.response()

        returnValue(response)


    def http_COPY(self, request):
        """
        Copying of calendar collections isn't allowed.
        """
        # FIXME: no direct tests
        return FORBIDDEN


    # FIXME: access control
    @inlineCallbacks
    def http_MOVE(self, request):
        """
        Moving a collection is allowed for the purposes of changing
        that collections's name.
        """
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        # Can not move outside of home or to existing collection
        sourceURI = request.uri
        destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
        if parentForURL(sourceURI) != parentForURL(destinationURI):
            returnValue(FORBIDDEN)

        destination = yield request.locateResource(destinationURI)
        if destination.exists():
            returnValue(FORBIDDEN)

        # Forget the destination now as after the move we will need to re-init it with its
        # new store object
        request._forgetResource(destination, destinationURI)

        # Move is valid so do it
        basename = destinationURI.rstrip("/").split("/")[-1]
        yield self._newStoreObject.rename(basename)
        returnValue(NO_CONTENT)


    @inlineCallbacks
    def POST_handler_add_member(self, request):
        """
        Handle a POST ;add-member request on this collection

        @param request: the request object
        @type request: L{Request}
        """

        # Create a name for the new child
        name = str(uuid.uuid4()) + self.resourceSuffix()

        # Get a resource for the new child
        parentURL = request.path
        newchildURL = joinURL(parentURL, name)
        newchild = (yield request.locateResource(newchildURL))

        # Treat as if it were a regular PUT to a new resource
        response = (yield newchild.http_PUT(request))

        # May need to add a location header
        addLocation(request, request.unparseURL(path=newchildURL, params=""))

        returnValue(response)


    @inlineCallbacks
    def _readGlobalProperty(self, qname, prop, request):

        if config.EnableBatchUpload and qname == customxml.BulkRequests.qname():
            returnValue(customxml.BulkRequests(
                customxml.Simple(
                    customxml.MaxBulkResources.fromString(str(config.MaxResourcesBatchUpload)),
                    customxml.MaxBulkBytes.fromString(str(config.MaxBytesBatchUpload)),
                ),
                customxml.CRUD(
                    customxml.MaxBulkResources.fromString(str(config.MaxResourcesBatchUpload)),
                    customxml.MaxBulkBytes.fromString(str(config.MaxBytesBatchUpload)),
                ),
            ))
        else:
            result = (yield super(_CommonHomeChildCollectionMixin, self)._readGlobalProperty(qname, prop, request))
            returnValue(result)


    @inlineCallbacks
    def checkCTagPrecondition(self, request):
        if request.headers.hasHeader("If"):
            iffy = request.headers.getRawHeaders("If")[0]
            prefix = "<%sctag/" % (customxml.mm_namespace,)
            if prefix in iffy:
                testctag = iffy[iffy.find(prefix):]
                testctag = testctag[len(prefix):]
                testctag = testctag.split(">", 1)[0]
                ctag = (yield self.getInternalSyncToken())
                if testctag != ctag:
                    raise HTTPError(StatusResponse(PRECONDITION_FAILED, "CTag pre-condition failure"))


    def checkReturnChanged(self, request):
        if request.headers.hasHeader("X-MobileMe-DAV-Options"):
            return_changed = request.headers.getRawHeaders("X-MobileMe-DAV-Options")[0]
            return ("return-changed-data" in return_changed)
        else:
            return False


    @requiresPermissions(davxml.Bind())
    @inlineCallbacks
    def simpleBatchPOST(self, request):

        # If CTag precondition
        yield self.checkCTagPrecondition(request)

        # Look for return changed data option
        return_changed = self.checkReturnChanged(request)

        # Read in all data
        data = (yield allDataFromStream(request.stream))

        components = self.componentsFromData(data)
        if components is None:
            raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body"))

        # Build response
        xmlresponses = [None] * len(components)
        indexedComponents = [idxComponent for idxComponent in enumerate(components)]
        yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses)

        result = MultiStatusResponse(xmlresponses)

        newctag = (yield self.getInternalSyncToken())
        result.headers.setRawHeaders("CTag", (newctag,))

        # Setup some useful logging
        request.submethod = "Simple batch"
        if not hasattr(request, "extendedLogItems"):
            request.extendedLogItems = {}
        request.extendedLogItems["rcount"] = len(xmlresponses)

        returnValue(result)


    @inlineCallbacks
    def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses):
        """
        Do create from simpleBatchPOST or crudCreate()
        Subclasses may override
        """
        for index, component in indexedComponents:

            try:
                # Create a new name if one was not provided
                name = md5(str(index) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()

                # Get a resource for the new item
                newchildURL = joinURL(request.path, name)
                newchild = (yield request.locateResource(newchildURL))
                changedData = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))

            except HTTPError, e:
                # Extract the pre-condition
                code = e.response.code
                if isinstance(e.response, ErrorResponse):
                    error = e.response.error
                    error = (error.namespace, error.name,)

                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error)
                )

            except Exception:
                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code=BAD_REQUEST, error=None)
                )

            else:
                if not return_changed:
                    changedData = None
                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
                )


    @inlineCallbacks
    def bulkCreateResponse(self, component, newchildURL, newchild, changedData, code, error):
        """
        generate one xmlresponse for bulk create
        """
        if code is None:
            etag = (yield newchild.etag())
            if changedData is None:
                returnValue(
                    davxml.PropertyStatusResponse(
                        davxml.HRef.fromString(newchildURL),
                        davxml.PropertyStatus(
                            davxml.PropertyContainer(
                                davxml.GETETag.fromString(etag.generate()),
                                customxml.UID.fromString(component.resourceUID()),
                            ),
                            davxml.Status.fromResponseCode(OK),
                        )
                    )
                )
            else:
                returnValue(
                    davxml.PropertyStatusResponse(
                        davxml.HRef.fromString(newchildURL),
                        davxml.PropertyStatus(
                            davxml.PropertyContainer(
                                davxml.GETETag.fromString(etag.generate()),
                                self.xmlDataElementType().fromTextData(changedData),
                            ),
                            davxml.Status.fromResponseCode(OK),
                        )
                    )
                )
        else:
            returnValue(
                davxml.StatusResponse(
                    davxml.HRef.fromString(""),
                    davxml.Status.fromResponseCode(code),
                    davxml.Error(
                        WebDAVUnknownElement.withName(*error),
                        customxml.UID.fromString(component.resourceUID()),
                    ) if error else None,
                )
            )


    @inlineCallbacks
    def crudBatchPOST(self, request, xmlroot):

        # Need to force some kind of overall authentication on the request
        yield self.authorize(request, (davxml.Read(), davxml.Write(),))

        # If CTag precondition
        yield self.checkCTagPrecondition(request)

        # Look for return changed data option
        return_changed = self.checkReturnChanged(request)

        # setup for create, update, and delete
        crudDeleteInfo = []
        crudUpdateInfo = []
        crudCreateInfo = []
        for index, xmlchild in enumerate(xmlroot.children):

            # Determine the multiput operation: create, update, delete
            href = xmlchild.childOfType(davxml.HRef.qname())
            set_items = xmlchild.childOfType(davxml.Set.qname())
            prop = set_items.childOfType(davxml.PropertyContainer.qname()) if set_items is not None else None
            xmldata_root = prop if prop else set_items
            xmldata = xmldata_root.childOfType(self.xmlDataElementType().qname()) if xmldata_root is not None else None
            if href is None:

                if xmldata is None:
                    raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))

                crudCreateInfo.append((index, xmldata))
            else:
                delete = xmlchild.childOfType(customxml.Delete.qname())
                ifmatch = xmlchild.childOfType(customxml.IfMatch.qname())
                if ifmatch:
                    ifmatch = str(ifmatch.children[0]) if len(ifmatch.children) == 1 else None
                if delete is None:
                    if set_items is None:
                        raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body - no set_items of delete operation"))
                    if xmldata is None:
                        raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body for set_items operation"))
                    crudUpdateInfo.append((index, str(href), xmldata, ifmatch))
                else:
                    crudDeleteInfo.append((index, str(href), ifmatch))

        # now do the work
        xmlresponses = [None] * len(xmlroot.children)
        yield self.crudDelete(crudDeleteInfo, request, xmlresponses)
        yield self.crudCreate(crudCreateInfo, request, xmlresponses, return_changed)
        yield self.crudUpdate(crudUpdateInfo, request, xmlresponses, return_changed)

        result = MultiStatusResponse(xmlresponses) #@UndefinedVariable

        newctag = (yield self.getInternalSyncToken())
        result.headers.setRawHeaders("CTag", (newctag,))

        # Setup some useful logging
        request.submethod = "CRUD batch"
        if not hasattr(request, "extendedLogItems"):
            request.extendedLogItems = {}
        request.extendedLogItems["rcount"] = len(xmlresponses)
        if crudCreateInfo:
            request.extendedLogItems["create"] = len(crudCreateInfo)
        if crudUpdateInfo:
            request.extendedLogItems["update"] = len(crudUpdateInfo)
        if crudDeleteInfo:
            request.extendedLogItems["delete"] = len(crudDeleteInfo)

        returnValue(result)


    @inlineCallbacks
    def crudCreate(self, crudCreateInfo, request, xmlresponses, return_changed):

        if crudCreateInfo:
            # Do privilege check on collection once
            try:
                yield self.authorize(request, (davxml.Bind(),))
                hasPrivilege = True
            except HTTPError, e:
                hasPrivilege = e

            #get components
            indexedComponents = []
            for index, xmldata in crudCreateInfo:

                component = xmldata.generateComponent()

                if hasPrivilege is not True:
                    e = hasPrivilege # use same code pattern as exception
                    code = e.response.code
                    if isinstance(e.response, ErrorResponse):
                        error = e.response.error
                        error = (error.namespace, error.name,)

                    xmlresponse = yield self.bulkCreateResponse(component, None, None, None, code, error)
                    xmlresponses[index] = xmlresponse

                else:
                    indexedComponents.append((index, component,))

            yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses)


    @inlineCallbacks
    def crudUpdate(self, crudUpdateInfo, request, xmlresponses, return_changed):

        for index, href, xmldata, ifmatch in crudUpdateInfo:

            code = None
            error = None
            try:
                componentdata = xmldata.textData()
                component = xmldata.generateComponent()

                updateResource = (yield request.locateResource(href))
                if not updateResource.exists():
                    raise HTTPError(NOT_FOUND)

                # Check privilege
                yield updateResource.authorize(request, (davxml.Write(),))

                # Check if match
                etag = (yield updateResource.etag())
                if ifmatch and ifmatch != etag.generate():
                    raise HTTPError(PRECONDITION_FAILED)

                changedData = yield self.storeResourceData(updateResource, component, componentdata)

            except HTTPError, e:
                # Extract the pre-condition
                code = e.response.code
                if isinstance(e.response, ErrorResponse):
                    error = e.response.error
                    error = (error.namespace, error.name,)

            except Exception:
                code = BAD_REQUEST

            if code is None:
                if not return_changed or changedData is None:
                    xmlresponses[index] = davxml.PropertyStatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.PropertyStatus(
                            davxml.PropertyContainer(
                                davxml.GETETag.fromString(etag.generate()),
                            ),
                            davxml.Status.fromResponseCode(OK),
                        )
                    )
                else:
                    xmlresponses[index] = davxml.PropertyStatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.PropertyStatus(
                            davxml.PropertyContainer(
                                davxml.GETETag.fromString(etag.generate()),
                                self.xmlDataElementType().fromTextData(changedData),
                            ),
                            davxml.Status.fromResponseCode(OK),
                        )
                    )
            else:
                xmlresponses[index] = davxml.StatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.Status.fromResponseCode(code),
                        davxml.Error(
                            WebDAVUnknownElement.withName(*error),
                        ) if error else None,
                    )


    @inlineCallbacks
    def crudDelete(self, crudDeleteInfo, request, xmlresponses):

        if crudDeleteInfo:

            # Do privilege check on collection once
            try:
                yield self.authorize(request, (davxml.Unbind(),))
                hasPrivilege = True
            except HTTPError, e:
                hasPrivilege = e

            for index, href, ifmatch in crudDeleteInfo:
                code = None
                error = None
                try:
                    if hasPrivilege is not True:
                        raise hasPrivilege

                    deleteResource = (yield request.locateResource(href))
                    if not deleteResource.exists():
                        raise HTTPError(NOT_FOUND)

                    # Check if match
                    etag = (yield deleteResource.etag())
                    if ifmatch and ifmatch != etag.generate():
                        raise HTTPError(PRECONDITION_FAILED)

                    yield deleteResource.storeRemove(request)

                except HTTPError, e:
                    # Extract the pre-condition
                    code = e.response.code
                    if isinstance(e.response, ErrorResponse):
                        error = e.response.error
                        error = (error.namespace, error.name,)

                except Exception:
                    code = BAD_REQUEST

                if code is None:
                    xmlresponses[index] = davxml.StatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.Status.fromResponseCode(OK),
                    )
                else:
                    xmlresponses[index] = davxml.StatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.Status.fromResponseCode(code),
                        davxml.Error(
                            WebDAVUnknownElement.withName(*error),
                        ) if error else None,
                    )


    def notifierID(self):
        return "%s/%s" % self._newStoreObject.notifierID()


    def notifyChanged(self):
        return self._newStoreObject.notifyChanged()



class _CalendarCollectionBehaviorMixin():
    """
    Functions common to calendar and inbox collections
    """

    # Support component set behaviors
    def setSupportedComponentSet(self, support_components_property):
        """
        Parse out XML property into list of components and give to store.
        """
        support_components = tuple([comp.attributes["name"].upper() for comp in support_components_property.children])
        return self.setSupportedComponents(support_components)


    def getSupportedComponentSet(self):
        comps = self._newStoreObject.getSupportedComponents()
        if comps:
            comps = comps.split(",")
        else:
            comps = allowedComponents
        return caldavxml.SupportedCalendarComponentSet(
            *[caldavxml.CalendarComponent(name=item) for item in comps]
        )


    def setSupportedComponents(self, components):
        """
        Set the allowed component set for this calendar.

        @param components: list of names of components to support
        @type components: C{list}
        """

        # Validate them first - raise on failure
        if not self.validSupportedComponents(components):
            raise HTTPError(StatusResponse(FORBIDDEN, "Invalid CALDAV:supported-calendar-component-set"))

        support_components = ",".join(sorted([comp.upper() for comp in components]))
        return maybeDeferred(self._newStoreObject.setSupportedComponents, support_components)


    def getSupportedComponents(self):
        comps = self._newStoreObject.getSupportedComponents()
        if comps:
            comps = comps.split(",")
        else:
            comps = allowedComponents
        return comps


    def isSupportedComponent(self, componentType):
        return self._newStoreObject.isSupportedComponent(componentType)


    def validSupportedComponents(self, components):
        """
        Test whether the supplied set of components is valid for the current server's component set
        restrictions.
        """
        if config.RestrictCalendarsToOneComponentType:
            return components in (("VEVENT",), ("VTODO",),)
        return True



class CalendarCollectionResource(DefaultAlarmPropertyMixin, _CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, CalDAVResource):
    """
    Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
    """

    def __init__(self, calendar, home, name=None, *args, **kw):
        """
        Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
        and the arguments required for L{CalDAVResource}.
        """

        self._childClass = CalendarObjectResource
        super(CalendarCollectionResource, self).__init__(*args, **kw)
        self._initializeWithHomeChild(calendar, home)
        self._name = calendar.name() if calendar else name

        if config.EnableBatchUpload:
            self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
            self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST


    def __repr__(self):
        return "<Calendar Collection Resource %r:%r %s>" % (
            self._newStoreParentHome.uid(),
            self._name,
            "" if self._newStoreObject else "Non-existent"
        )


    def isCollection(self):
        return True


    def isCalendarCollection(self):
        """
        Yes, it is a calendar collection.
        """
        return True


    def resourceType(self):
        if self.isShared():
            return customxml.ResourceType.sharedownercalendar
        elif self.isShareeResource():
            return customxml.ResourceType.sharedcalendar
        else:
            return caldavxml.ResourceType.calendar


    @inlineCallbacks
    def iCalendarRolledup(self, request):
        # FIXME: uncached: implement cache in the storage layer

        # Generate a monolithic calendar
        calendar = VCalendar("VCALENDAR")
        calendar.addProperty(VProperty("VERSION", "2.0"))
        calendar.addProperty(VProperty("PRODID", iCalendarProductID))

        # Add a display name if available
        displayName = self.displayName()
        if displayName is not None:
            calendar.addProperty(VProperty("X-WR-CALNAME", displayName))

        # Do some optimisation of access control calculation by determining any
        # inherited ACLs outside of the child resource loop and supply those to
        # the checkPrivileges on each child.
        filteredaces = (yield self.inheritedACEsforChildren(request))

        tzids = set()
        isowner = (yield self.isOwner(request))
        accessPrincipal = (yield self.resourceOwnerPrincipal(request))

        for name, _ignore_uid, _ignore_type in (yield maybeDeferred(self.index().bruteForceSearch)):
            try:
                child = yield request.locateChildResource(self, name)
            except TypeError:
                child = None

            if child is not None:
                # Check privileges of child - skip if access denied
                try:
                    yield child.checkPrivileges(request, (davxml.Read(),), inherited_aces=filteredaces)
                except AccessDeniedError:
                    continue

                # Get the access filtered view of the data
                try:
                    subcalendar = yield child.iCalendarFiltered(isowner, accessPrincipal.principalUID() if accessPrincipal else "")
                except ValueError:
                    continue
                assert subcalendar.name() == "VCALENDAR"

                for component in subcalendar.subcomponents():

                    # Only insert VTIMEZONEs once
                    if component.name() == "VTIMEZONE":
                        tzid = component.propertyValue("TZID")
                        if tzid in tzids:
                            continue
                        tzids.add(tzid)

                    calendar.addComponent(component)

        # Cache the data
        data = str(calendar)
        data = (yield self.getInternalSyncToken()) + "\r\n" + data

        returnValue(calendar)

    createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection


    @classmethod
    def componentsFromData(cls, data):
        """
        Need to split a single VCALENDAR into separate ones based on UID with the
        appropriate VTIEMZONES included.
        """

        results = []

        # Split into components by UID and TZID
        try:
            vcal = VCalendar.fromString(data)
        except InvalidICalendarDataError:
            return None

        by_uid = collections.OrderedDict()
        by_tzid = {}
        for subcomponent in vcal.subcomponents():
            if subcomponent.name() == "VTIMEZONE":
                by_tzid[subcomponent.propertyValue("TZID")] = subcomponent
            else:
                by_uid.setdefault(subcomponent.propertyValue("UID"), []).append(subcomponent)

        # Re-constitute as separate VCALENDAR objects
        for components in by_uid.values():

            newvcal = VCalendar("VCALENDAR")
            newvcal.addProperty(VProperty("VERSION", "2.0"))
            newvcal.addProperty(VProperty("PRODID", vcal.propertyValue("PRODID")))

            # Get the set of TZIDs and include them
            tzids = set()
            for component in components:
                tzids.update(component.timezoneIDs())
            for tzid in tzids:
                try:
                    tz = by_tzid[tzid]
                    newvcal.addComponent(tz.duplicate())
                except KeyError:
                    # We ignore the error and generate invalid ics which someone will
                    # complain about at some point
                    pass

            # Now add each component
            for component in components:
                newvcal.addComponent(component.duplicate())

            results.append(newvcal)

        return results


    @classmethod
    def resourceSuffix(cls):
        return ".ics"


    @classmethod
    def xmlDataElementType(cls):
        return caldavxml.CalendarData


    def _hasGlobalProperty(self, property, request):
        """
        Need to special case schedule-calendar-transp for backwards compatability.
        """

        if type(property) is tuple:
            qname = property
        else:
            qname = property.qname()

        # Handle certain built-in values
        if qname in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            return succeed(self.getDefaultAlarmProperty(qname) is not None)

        elif qname == caldavxml.CalendarTimeZone.qname():
            return succeed(self._newStoreObject.getTimezone() is not None)

        else:
            return super(CalendarCollectionResource, self)._hasGlobalProperty(property, request)


    @inlineCallbacks
    def readProperty(self, property, request):
        if type(property) is tuple:
            qname = property
        else:
            qname = property.qname()

        if qname in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            returnValue(self.getDefaultAlarmProperty(qname))

        elif qname == caldavxml.CalendarTimeZone.qname():
            timezone = self._newStoreObject.getTimezone()
            returnValue(caldavxml.CalendarTimeZone.fromString(str(timezone)) if timezone else None)

        result = (yield super(CalendarCollectionResource, self).readProperty(property, request))
        returnValue(result)


    @inlineCallbacks
    def _writeGlobalProperty(self, property, request):

        if property.qname() in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            yield self.setDefaultAlarmProperty(property)
            returnValue(None)

        elif property.qname() == caldavxml.CalendarTimeZone.qname():
            yield self._newStoreObject.setTimezone(property.calendar())
            returnValue(None)

        result = (yield super(CalendarCollectionResource, self)._writeGlobalProperty(property, request))
        returnValue(result)


    @inlineCallbacks
    def removeProperty(self, property, request):
        if type(property) is tuple:
            qname = property
        else:
            qname = property.qname()

        if qname in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            result = (yield self.removeDefaultAlarmProperty(qname))
            returnValue(result)

        elif qname == caldavxml.CalendarTimeZone.qname():
            yield self._newStoreObject.setTimezone(None)
            returnValue(None)

        result = (yield super(CalendarCollectionResource, self).removeProperty(property, request))
        returnValue(result)


    @inlineCallbacks
    def storeResourceData(self, newchild, component, returnChangedData=False):

        yield newchild.storeComponent(component)
        if returnChangedData and newchild._newStoreObject._componentChanged:
            result = (yield newchild.componentForUser())
            returnValue(str(result))
        else:
            returnValue(None)


    # FIXME: access control
    @inlineCallbacks
    def http_MOVE(self, request):
        """
        Moving a calendar collection is allowed for the purposes of changing
        that calendar's name.
        """
        result = (yield super(CalendarCollectionResource, self).http_MOVE(request))
        returnValue(result)



class StoreScheduleInboxResource(_CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, ScheduleInboxResource):

    def __init__(self, *a, **kw):

        self._childClass = CalendarObjectResource
        super(StoreScheduleInboxResource, self).__init__(*a, **kw)
        self.parent.propagateTransaction(self)


    @classmethod
    @inlineCallbacks
    def maybeCreateInbox(cls, *a, **kw):
        self = cls(*a, **kw)
        home = self.parent._newStoreHome
        storage = yield home.calendarWithName("inbox")
        if storage is None:
            # raise RuntimeError("backend should be handling this for us")
            # FIXME: spurious error, sanity check, should not be needed;
            # unfortunately, user09's calendar home does not have an inbox, so
            # this is a temporary workaround.
            yield home.createCalendarWithName("inbox")
            storage = yield home.calendarWithName("inbox")
        self._initializeWithHomeChild(
            storage,
            self.parent
        )
        self._name = storage.name()
        returnValue(self)


    def provisionFile(self):
        pass


    def provision(self):
        pass


    def http_DELETE(self, request):
        return FORBIDDEN


    def http_COPY(self, request):
        return FORBIDDEN


    def http_MOVE(self, request):
        return FORBIDDEN



class _GetChildHelper(CalDAVResource):

    def locateChild(self, request, segments):
        if segments[0] == '':
            return self, segments[1:]
        return self.getChild(segments[0]), segments[1:]


    def getChild(self, name):
        return None


    def readProperty(self, prop, request):
        if type(prop) is tuple:
            qname = prop
        else:
            qname = prop.qname()

        if qname == (dav_namespace, "resourcetype"):
            return succeed(self.resourceType())
        return super(_GetChildHelper, self).readProperty(prop, request)


    def davComplianceClasses(self):
        return ("1", "access-control")


    @requiresPermissions(davxml.Read())
    def http_GET(self, request):
        return super(_GetChildHelper, self).http_GET(request)



class DropboxCollection(_GetChildHelper):
    """
    A collection of all dropboxes (containers for attachments), presented as a
    resource under the user's calendar home, where a dropbox is a
    L{CalendarObjectDropbox}.
    """
    # FIXME: no direct tests for this class at all.

    def __init__(self, parent, *a, **kw):
        kw.update(principalCollections=parent.principalCollections())
        super(DropboxCollection, self).__init__(*a, **kw)
        self._newStoreHome = parent._newStoreHome
        parent.propagateTransaction(self)


    def isCollection(self):
        """
        It is a collection.
        """
        return True


    @inlineCallbacks
    def getChild(self, name):
        calendarObject = yield self._newStoreHome.calendarObjectWithDropboxID(name)
        if calendarObject is None:
            returnValue(NoDropboxHere())
        objectDropbox = CalendarObjectDropbox(
            calendarObject, principalCollections=self.principalCollections()
        )
        self.propagateTransaction(objectDropbox)
        returnValue(objectDropbox)


    def resourceType(self,):
        return davxml.ResourceType.dropboxhome #@UndefinedVariable


    def listChildren(self):
        return self._newStoreHome.getAllDropboxIDs()



class NoDropboxHere(_GetChildHelper):

    def getChild(self, name):
        raise HTTPError(FORBIDDEN)


    def isCollection(self):
        return False


    def exists(self):
        return False


    def http_GET(self, request):
        return FORBIDDEN


    def http_MKCALENDAR(self, request):
        return FORBIDDEN


    @requiresPermissions(fromParent=[davxml.Bind()])
    def http_MKCOL(self, request):
        return CREATED



class CalendarObjectDropbox(_GetChildHelper):
    """
    A wrapper around a calendar object which serves that calendar object's
    attachments as a DAV collection.
    """

    def __init__(self, calendarObject, *a, **kw):
        super(CalendarObjectDropbox, self).__init__(*a, **kw)
        self._newStoreCalendarObject = calendarObject


    def isCollection(self):
        return True


    def resourceType(self):
        return davxml.ResourceType.dropbox #@UndefinedVariable


    @inlineCallbacks
    def getChild(self, name):
        attachment = yield self._newStoreCalendarObject.attachmentWithName(name)
        result = CalendarAttachment(
            self._newStoreCalendarObject,
            attachment,
            name,
            False,
            principalCollections=self.principalCollections()
        )
        self.propagateTransaction(result)
        returnValue(result)


    @requiresPermissions(davxml.WriteACL())
    @inlineCallbacks
    def http_ACL(self, request):
        """
        Don't ever actually make changes, but attempt to deny any ACL requests
        that refer to permissions not referenced by attendees in the iCalendar
        data.
        """

        attendees = (yield self._newStoreCalendarObject.component()).getAttendees()
        attendees = [attendee.split("urn:uuid:")[-1] for attendee in attendees]
        document = yield davXMLFromStream(request.stream)
        for ace in document.root_element.children:
            for child in ace.children:
                if isinstance(child, davxml.Principal):
                    for href in child.children:
                        principalURI = href.children[0].data
                        uidsPrefix = '/principals/__uids__/'
                        if not principalURI.startswith(uidsPrefix):
                            # Unknown principal.
                            returnValue(FORBIDDEN)
                        principalElements = principalURI[
                            len(uidsPrefix):].split("/")
                        if principalElements[-1] == '':
                            principalElements.pop()
                        if principalElements[-1] in ('calendar-proxy-read',
                                                     'calendar-proxy-write'):
                            principalElements.pop()
                        if len(principalElements) != 1:
                            returnValue(FORBIDDEN)
                        principalUID = principalElements[0]
                        if principalUID not in attendees:
                            returnValue(FORBIDDEN)
        returnValue(OK)


    @requiresPermissions(fromParent=[davxml.Bind()])
    def http_MKCOL(self, request):
        return CREATED


    @requiresPermissions(fromParent=[davxml.Unbind()])
    def http_DELETE(self, request):
        return NO_CONTENT


    @inlineCallbacks
    def listChildren(self):
        l = []
        for attachment in (yield self._newStoreCalendarObject.attachments()):
            l.append(attachment.name())
        returnValue(l)


    @inlineCallbacks
    def accessControlList(self, request, *a, **kw):
        """
        All principals identified as ATTENDEEs on the event for this dropbox
        may read all its children. Also include proxies of ATTENDEEs. Ignore
        unknown attendees.
        """
        originalACL = yield super(
            CalendarObjectDropbox, self).accessControlList(request, *a, **kw)
        originalACEs = list(originalACL.children)

        if config.EnableProxyPrincipals:
            owner = (yield self.ownerPrincipal(request))

            originalACEs += (
                # DAV:write-acl access for this principal's calendar-proxy-write users.
                davxml.ACE(
                    davxml.Principal(davxml.HRef(joinURL(owner.principalURL(), "calendar-proxy-write/"))),
                    davxml.Grant(
                        davxml.Privilege(davxml.WriteACL()),
                    ),
                    davxml.Protected(),
                    TwistedACLInheritable(),
                ),
            )

        othersCanWrite = self._newStoreCalendarObject.attendeesCanManageAttachments()
        cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
        newACEs = []
        for calendarUserAddress in cuas:
            principal = self.principalForCalendarUserAddress(
                calendarUserAddress
            )
            if principal is None:
                continue

            principalURL = principal.principalURL()
            writePrivileges = [
                davxml.Privilege(davxml.Read()),
                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
                davxml.Privilege(davxml.Write()),
            ]
            readPrivileges = [
                davxml.Privilege(davxml.Read()),
                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
            ]
            if othersCanWrite:
                privileges = writePrivileges
            else:
                privileges = readPrivileges
            newACEs.append(davxml.ACE(
                davxml.Principal(davxml.HRef(principalURL)),
                davxml.Grant(*privileges),
                davxml.Protected(),
                TwistedACLInheritable(),
            ))
            newACEs.append(davxml.ACE(
                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-write/"))),
                davxml.Grant(*privileges),
                davxml.Protected(),
                TwistedACLInheritable(),
            ))
            newACEs.append(davxml.ACE(
                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-read/"))),
                davxml.Grant(*readPrivileges),
                davxml.Protected(),
                TwistedACLInheritable(),
            ))

        # Now also need invitees
        newACEs.extend((yield self.sharedDropboxACEs()))

        returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))


    @inlineCallbacks
    def sharedDropboxACEs(self):

        aces = ()

        invites = yield self._newStoreCalendarObject._parentCollection.sharingInvites()
        for invite in invites:

            # Only want accepted invites
            if invite.status() != _BIND_STATUS_ACCEPTED:
                continue

            userprivs = [
            ]
            if invite.mode() in (_BIND_MODE_READ, _BIND_MODE_WRITE,):
                userprivs.append(davxml.Privilege(davxml.Read()))
                userprivs.append(davxml.Privilege(davxml.ReadACL()))
                userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))
            if invite.mode() in (_BIND_MODE_READ,):
                userprivs.append(davxml.Privilege(davxml.WriteProperties()))
            if invite.mode() in (_BIND_MODE_WRITE,):
                userprivs.append(davxml.Privilege(davxml.Write()))
            proxyprivs = list(userprivs)
            proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))

            principal = self.principalForUID(invite.shareeUID())
            if principal is not None:
                aces += (
                    # Inheritable specific access for the resource's associated principal.
                    davxml.ACE(
                        davxml.Principal(davxml.HRef(principal.principalURL())),
                        davxml.Grant(*userprivs),
                        davxml.Protected(),
                        TwistedACLInheritable(),
                    ),
                )

                if config.EnableProxyPrincipals:
                    aces += (
                        # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
                        davxml.ACE(
                            davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-read/"))),
                            davxml.Grant(
                                davxml.Privilege(davxml.Read()),
                                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
                            ),
                            davxml.Protected(),
                            TwistedACLInheritable(),
                        ),
                        # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
                        davxml.ACE(
                            davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-write/"))),
                            davxml.Grant(*proxyprivs),
                            davxml.Protected(),
                            TwistedACLInheritable(),
                        ),
                    )

        returnValue(aces)



class AttachmentsCollection(_GetChildHelper):
    """
    A collection of all managed attachments, presented as a
    resource under the user's calendar home. Attachments are stored
    in L{AttachmentsChildCollection} child collections of this one.
    """
    # FIXME: no direct tests for this class at all.

    def __init__(self, parent, *a, **kw):
        kw.update(principalCollections=parent.principalCollections())
        super(AttachmentsCollection, self).__init__(*a, **kw)
        self.parent = parent
        self._newStoreHome = self.parent._newStoreHome
        self.parent.propagateTransaction(self)


    def isCollection(self):
        """
        It is a collection.
        """
        return True


    @inlineCallbacks
    def getChild(self, name):
        calendarObject = yield self._newStoreHome.calendarObjectWithDropboxID(name)

        # Hide the dropbox if it has no children
        if calendarObject:
            l = (yield calendarObject.managedAttachmentList())
            if len(l) == 0:
                l = (yield calendarObject.attachments())
                if len(l) == 0:
                    calendarObject = None

        if calendarObject is None:
            returnValue(NoDropboxHere())
        objectDropbox = AttachmentsChildCollection(
            calendarObject, self, principalCollections=self.principalCollections()
        )
        self.propagateTransaction(objectDropbox)
        returnValue(objectDropbox)


    def resourceType(self,):
        return davxml.ResourceType.dropboxhome #@UndefinedVariable


    def listChildren(self):
        return self._newStoreHome.getAllDropboxIDs()


    def supportedPrivileges(self, request):
        # Just DAV standard privileges - no CalDAV ones
        return succeed(davPrivilegeSet)


    def defaultAccessControlList(self):
        """
        Only read privileges allowed for managed attachments.
        """
        myPrincipal = self.parent.principalForRecord()

        read_privs = (
            davxml.Privilege(davxml.Read()),
            davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
        )

        aces = (
            # Inheritable access for the resource's associated principal.
            davxml.ACE(
                davxml.Principal(davxml.HRef(myPrincipal.principalURL())),
                davxml.Grant(*read_privs),
                davxml.Protected(),
                TwistedACLInheritable(),
            ),
        )

        # Give read access to config.ReadPrincipals
        aces += config.ReadACEs

        # Give all access to config.AdminPrincipals
        aces += config.AdminACEs

        if config.EnableProxyPrincipals:
            aces += (
                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
                davxml.ACE(
                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-read/"))),
                    davxml.Grant(*read_privs),
                    davxml.Protected(),
                    TwistedACLInheritable(),
                ),
                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-write users.
                davxml.ACE(
                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write/"))),
                    davxml.Grant(*read_privs),
                    davxml.Protected(),
                    TwistedACLInheritable(),
                ),
            )

        return davxml.ACL(*aces)


    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
        # Permissions here are fixed, and are not subject to inheritance rules, etc.
        return succeed(self.defaultAccessControlList())



class AttachmentsChildCollection(_GetChildHelper):
    """
    A collection of all containers for attachments, presented as a
    resource under the user's calendar home, where a dropbox is a
    L{CalendarObjectDropbox}.
    """
    # FIXME: no direct tests for this class at all.

    def __init__(self, calendarObject, parent, *a, **kw):
        kw.update(principalCollections=parent.principalCollections())
        super(AttachmentsChildCollection, self).__init__(*a, **kw)
        self._newStoreCalendarObject = calendarObject
        parent.propagateTransaction(self)


    def isCollection(self):
        """
        It is a collection.
        """
        return True


    @inlineCallbacks
    def getChild(self, name):
        attachmentObject = yield self._newStoreCalendarObject.managedAttachmentRetrieval(name)
        if attachmentObject is not None:
            result = CalendarAttachment(
                None,
                attachmentObject,
                name,
                True,
                principalCollections=self.principalCollections()
            )
        else:
            attachment = yield self._newStoreCalendarObject.attachmentWithName(name)
            result = CalendarAttachment(
                self._newStoreCalendarObject,
                attachment,
                name,
                False,
                principalCollections=self.principalCollections()
            )

        self.propagateTransaction(result)
        returnValue(result)


    def resourceType(self,):
        return davxml.ResourceType.dropbox #@UndefinedVariable


    @inlineCallbacks
    def listChildren(self):
        l = (yield self._newStoreCalendarObject.managedAttachmentList())
        for attachment in (yield self._newStoreCalendarObject.attachments()):
            l.append(attachment.name())
        returnValue(l)


    @inlineCallbacks
    def http_ACL(self, request):
        # For managed attachment compatibility this is always forbidden as dropbox clients must never be
        # allowed to store attachments or make any changes.
        return FORBIDDEN


    def http_MKCOL(self, request):
        # For managed attachment compatibility this is always forbidden as dropbox clients must never be
        # allowed to store attachments or make any changes.
        return FORBIDDEN


    @requiresPermissions(fromParent=[davxml.Unbind()])
    def http_DELETE(self, request):
        # For managed attachment compatibility this always succeeds as dropbox clients will do
        # this but we don't want them to see an error. Managed attachments will always be cleaned
        # up on removal of the actual calendar object resource.
        return NO_CONTENT


    @inlineCallbacks
    def accessControlList(self, request, *a, **kw):
        """
        All principals identified as ATTENDEEs on the event for this dropbox
        may read all its children. Also include proxies of ATTENDEEs. Ignore
        unknown attendees. Do not allow attendees to write as we don't support
        that with managed attachments. Also include sharees of the event.
        """
        originalACL = yield super(
            AttachmentsChildCollection, self).accessControlList(request, *a, **kw)
        originalACEs = list(originalACL.children)

        if config.EnableProxyPrincipals:
            owner = (yield self.ownerPrincipal(request))

            originalACEs += (
                # DAV:write-acl access for this principal's calendar-proxy-write users.
                davxml.ACE(
                    davxml.Principal(davxml.HRef(joinURL(owner.principalURL(), "calendar-proxy-write/"))),
                    davxml.Grant(
                        davxml.Privilege(davxml.WriteACL()),
                    ),
                    davxml.Protected(),
                    TwistedACLInheritable(),
                ),
            )

        cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
        newACEs = []
        for calendarUserAddress in cuas:
            principal = self.principalForCalendarUserAddress(
                calendarUserAddress
            )
            if principal is None:
                continue

            principalURL = principal.principalURL()
            privileges = [
                davxml.Privilege(davxml.Read()),
                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
            ]
            newACEs.append(davxml.ACE(
                davxml.Principal(davxml.HRef(principalURL)),
                davxml.Grant(*privileges),
                davxml.Protected(),
                TwistedACLInheritable(),
            ))
            newACEs.append(davxml.ACE(
                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-write/"))),
                davxml.Grant(*privileges),
                davxml.Protected(),
                TwistedACLInheritable(),
            ))
            newACEs.append(davxml.ACE(
                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-read/"))),
                davxml.Grant(*privileges),
                davxml.Protected(),
                TwistedACLInheritable(),
            ))

        # Now also need invitees
        newACEs.extend((yield self.sharedDropboxACEs()))

        returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))


    @inlineCallbacks
    def _sharedAccessControl(self, invite):
        """
        Check the shared access mode of this resource, potentially consulting
        an external access method if necessary.

        @return: a L{Deferred} firing a L{bytes} or L{None}, with one of the
            potential values: C{"own"}, which means that the home is the owner
            of the collection and it is not shared; C{"read-only"}, meaning
            that the home that this collection is bound into has only read
            access to this collection; C{"read-write"}, which means that the
            home has both read and write access; C{"original"}, which means
            that it should inherit the ACLs of the owner's collection, whatever
            those happen to be, or C{None}, which means that the external
            access control mechanism has dictate the home should no longer have
            any access at all.
        """
        if invite.mode() in (_BIND_MODE_DIRECT,):
            ownerUID = invite.ownerUID()
            owner = self.principalForUID(ownerUID)
            shareeUID = invite.shareeUID()
            if owner.record.recordType == WikiDirectoryService.recordType_wikis:
                # Access level comes from what the wiki has granted to the
                # sharee
                sharee = self.principalForUID(shareeUID)
                userID = sharee.record.guid
                wikiID = owner.record.shortNames[0]
                access = (yield getWikiAccess(userID, wikiID))
                if access == "read":
                    returnValue("read-only")
                elif access in ("write", "admin"):
                    returnValue("read-write")
                else:
                    returnValue(None)
            else:
                returnValue("original")
        elif invite.mode() in (_BIND_MODE_READ,):
            returnValue("read-only")
        elif invite.mode() in (_BIND_MODE_WRITE,):
            returnValue("read-write")
        returnValue("original")


    @inlineCallbacks
    def sharedDropboxACEs(self):

        aces = ()
        invites = yield self._newStoreCalendarObject._parentCollection.sharingInvites()
        for invite in invites:

            # Only want accepted invites
            if invite.status() != _BIND_STATUS_ACCEPTED:
                continue

            privileges = [
                davxml.Privilege(davxml.Read()),
                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
            ]
            userprivs = []
            access = (yield self._sharedAccessControl(invite))
            if access in ("read-only", "read-write",):
                userprivs.extend(privileges)

            principal = self.principalForUID(invite.shareeUID())
            aces += (
                # Inheritable specific access for the resource's associated principal.
                davxml.ACE(
                    davxml.Principal(davxml.HRef(principal.principalURL())),
                    davxml.Grant(*userprivs),
                    davxml.Protected(),
                    TwistedACLInheritable(),
                ),
            )

            if config.EnableProxyPrincipals:
                aces += (
                    # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
                    davxml.ACE(
                        davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-read/"))),
                        davxml.Grant(*userprivs),
                        davxml.Protected(),
                        TwistedACLInheritable(),
                    ),
                    # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
                    davxml.ACE(
                        davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-write/"))),
                        davxml.Grant(*userprivs),
                        davxml.Protected(),
                        TwistedACLInheritable(),
                    ),
                )

        returnValue(aces)



class CalendarAttachment(_NewStoreFileMetaDataHelper, _GetChildHelper):

    def __init__(self, calendarObject, attachment, attachmentName, managed, **kw):
        super(CalendarAttachment, self).__init__(**kw)
        self._newStoreCalendarObject = calendarObject  # This can be None for a managed attachment
        self._newStoreAttachment = self._newStoreObject = attachment
        self._managed = managed
        self._dead_properties = NonePropertyStore(self)
        self.attachmentName = attachmentName


    def getChild(self, name):
        return None


    def displayName(self):
        return self.name()


    @requiresPermissions(davxml.WriteContent())
    @inlineCallbacks
    def http_PUT(self, request):
        # FIXME: direct test
        # FIXME: CDT test to make sure that permissions are enforced.

        # Cannot PUT to a managed attachment
        if self._managed:
            raise HTTPError(FORBIDDEN)

        content_type = request.headers.getHeader("content-type")
        if content_type is None:
            content_type = MimeType("application", "octet-stream")

        try:
            creating = (self._newStoreAttachment is None)
            if creating:
                self._newStoreAttachment = self._newStoreObject = (
                    yield self._newStoreCalendarObject.createAttachmentWithName(
                        self.attachmentName))
            t = self._newStoreAttachment.store(content_type)
            yield readStream(request.stream, t.write)

        except AttachmentDropboxNotAllowed:
            log.error("Dropbox cannot be used after migration to managed attachments")
            raise HTTPError(FORBIDDEN)

        except Exception, e:
            log.error("Unable to store attachment: %s" % (e,))
            raise HTTPError(SERVICE_UNAVAILABLE)

        try:
            yield t.loseConnection()
        except QuotaExceeded:
            raise HTTPError(
                ErrorResponse(INSUFFICIENT_STORAGE_SPACE,
                              (dav_namespace, "quota-not-exceeded"))
            )
        returnValue(CREATED if creating else NO_CONTENT)


    @requiresPermissions(davxml.Read())
    def http_GET(self, request):

        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        stream = ProducerStream()
        class StreamProtocol(Protocol):
            def connectionMade(self):
                stream.registerProducer(self.transport, False)
            def dataReceived(self, data):
                stream.write(data)
            def connectionLost(self, reason):
                stream.finish()
        try:
            self._newStoreAttachment.retrieve(StreamProtocol())
        except IOError, e:
            log.error("Unable to read attachment: %s, due to: %s" % (self, e,))
            raise HTTPError(NOT_FOUND)

        headers = {"content-type": self.contentType()}
        headers["content-disposition"] = MimeDisposition("attachment", params={"filename": self.displayName()})
        return Response(OK, headers, stream)


    @requiresPermissions(fromParent=[davxml.Unbind()])
    @inlineCallbacks
    def http_DELETE(self, request):
        # Cannot DELETE a managed attachment
        if self._managed:
            raise HTTPError(FORBIDDEN)

        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        yield self._newStoreCalendarObject.removeAttachmentWithName(
            self._newStoreAttachment.name()
        )
        self._newStoreAttachment = self._newStoreCalendarObject = None
        returnValue(NO_CONTENT)

    http_MKCOL = None
    http_MKCALENDAR = None


    def http_PROPPATCH(self, request):
        """
        No dead properties allowed on attachments.
        """
        return FORBIDDEN


    def isCollection(self):
        return False


    def supportedPrivileges(self, request):
        # Just DAV standard privileges - no CalDAV ones
        return succeed(davPrivilegeSet)



class NoParent(CalDAVResource):

    def http_MKCALENDAR(self, request):
        return CONFLICT


    def http_PUT(self, request):
        return CONFLICT


    def isCollection(self):
        return False


    def exists(self):
        return False



class _CommonObjectResource(_NewStoreFileMetaDataHelper, CalDAVResource, FancyEqMixin):

    _componentFromStream = None

    def __init__(self, storeObject, parentObject, parentResource, name, *args, **kw):
        """
        Construct a L{_CommonObjectResource} from an L{CommonObjectResource}.

        @param storeObject: The storage for the object.
        @type storeObject: L{txdav.common.CommonObjectResource}
        """
        super(_CommonObjectResource, self).__init__(*args, **kw)
        self._initializeWithObject(storeObject, parentObject)
        self._parentResource = parentResource
        self._name = name
        self._metadata = {}


    def _initializeWithObject(self, storeObject, parentObject):
        self._newStoreParent = parentObject
        self._newStoreObject = storeObject
        self._dead_properties = _NewStorePropertiesWrapper(
            self._newStoreObject.properties()
        ) if self._newStoreObject and self._newStoreParent.objectResourcesHaveProperties() else NonePropertyStore(self)


    def url(self):
        return joinURL(self._parentResource.url(), self.name())


    def isCollection(self):
        return False


    def quotaSize(self, request):
        return succeed(self._newStoreObject.size())


    def uid(self):
        return self._newStoreObject.uid()


    def component(self):
        return self._newStoreObject.component()


    @inlineCallbacks
    def render(self, request):
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        output = yield self.component()

        response = Response(OK, {}, str(output))
        response.headers.setHeader("content-type", self.contentType())
        returnValue(response)


    @inlineCallbacks
    def checkPreconditions(self, request):
        """
        We override the base class to trap the failure case and process any Prefer header.
        """

        try:
            response = yield super(_CommonObjectResource, self).checkPreconditions(request)
        except HTTPError as e:
            if e.response.code == responsecode.PRECONDITION_FAILED:
                response = yield self._processPrefer(request, e.response)
                raise HTTPError(response)
            else:
                raise

        returnValue(response)


    @inlineCallbacks
    def _processPrefer(self, request, response):
        # Look for Prefer header
        prefer = request.headers.getHeader("prefer", {})
        returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])

        if returnRepresentation and (response.code / 100 == 2 or response.code == responsecode.PRECONDITION_FAILED):
            oldcode = response.code
            response = (yield self.http_GET(request))
            if oldcode in (responsecode.CREATED, responsecode.PRECONDITION_FAILED):
                response.code = oldcode
            response.headers.removeHeader("content-location")
            response.headers.setHeader("content-location", self.url())

        returnValue(response)

    # The following are used to map store exceptions into HTTP error responses
    StoreExceptionsErrors = {}
    StoreMoveExceptionsErrors = {}
    StoreRemoveExceptionsErrors = {}

    @classmethod
    def _storeExceptionStatus(cls, err, arg):
        """
        Raise a status error.

        @param err: the actual exception that caused the error
        @type err: L{Exception}
        @param arg: description of error or C{None}
        @type arg: C{str} or C{None}
        """
        raise HTTPError(StatusResponse(responsecode.FORBIDDEN, arg if arg is not None else str(err)))


    @classmethod
    def _storeExceptionError(cls, err, arg):
        """
        Raise a DAV:error error with the supplied error element.

        @param err: the actual exception that caused the error
        @type err: L{Exception}
        @param arg: the error element
        @type arg: C{tuple}
        """
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            arg,
            str(err),
        ))


    @classmethod
    def _storeExceptionUnavailable(cls, err, arg):
        """
        Raise a service unavailable error.

        @param err: the actual exception that caused the error
        @type err: L{Exception}
        @param arg: description of error or C{None}
        @type arg: C{str} or C{None}
        """
        raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, arg if arg is not None else str(err)))


    @requiresPermissions(fromParent=[davxml.Unbind()])
    def http_DELETE(self, request):
        """
        Override http_DELETE to validate 'depth' header.
        """
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        return self.storeRemove(request)


    def http_COPY(self, request):
        """
        Copying of calendar data isn't allowed.
        """
        # FIXME: no direct tests
        return FORBIDDEN


    @inlineCallbacks
    def http_MOVE(self, request):
        """
        MOVE for object resources.
        """

        # Do some pre-flight checks - must exist, must be move to another
        # CommonHomeChild in the same Home, destination resource must not exist
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        parent = (yield request.locateResource(parentForURL(request.uri)))

        #
        # Find the destination resource
        #
        destination_uri = request.headers.getHeader("destination")
        overwrite = request.headers.getHeader("overwrite", True)

        if not destination_uri:
            msg = "No destination header in MOVE request."
            log.error(msg)
            raise HTTPError(StatusResponse(BAD_REQUEST, msg))

        destination = (yield request.locateResource(destination_uri))
        if destination is None:
            msg = "Destination of MOVE does not exist: %s" % (destination_uri,)
            log.debug(msg)
            raise HTTPError(StatusResponse(BAD_REQUEST, msg))
        if destination.exists():
            if overwrite:
                msg = "Cannot overwrite existing resource with a MOVE"
                log.debug(msg)
                raise HTTPError(StatusResponse(FORBIDDEN, msg))
            else:
                msg = "Cannot MOVE to existing resource without overwrite flag enabled"
                log.debug(msg)
                raise HTTPError(StatusResponse(PRECONDITION_FAILED, msg))

        # Check for parent calendar collection
        destination_uri = urlsplit(destination_uri)[2]
        destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
        if not isinstance(destinationparent, _CommonHomeChildCollectionMixin):
            msg = "Destination of MOVE is not valid: %s" % (destination_uri,)
            log.debug(msg)
            raise HTTPError(StatusResponse(FORBIDDEN, msg))
        if parentForURL(parentForURL(destination_uri)) != parentForURL(parentForURL(request.uri)):
            msg = "Can only MOVE within the same home collection: %s" % (destination_uri,)
            log.debug(msg)
            raise HTTPError(StatusResponse(FORBIDDEN, msg))

        #
        # Check authentication and access controls
        #
        yield parent.authorize(request, (davxml.Unbind(),))
        yield destinationparent.authorize(request, (davxml.Bind(),))

        # May need to add a location header
        addLocation(request, destination_uri)

        try:
            response = (yield self.storeMove(request, destinationparent, destination.name()))
            returnValue(response)

        # Handle the various store errors
        except Exception as 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".
            if type(err) in self.StoreMoveExceptionsErrors:
                error, arg = self.StoreMoveExceptionsErrors[type(err)]
                error(err, arg)
            else:
                # Return the original failure (exception) state
                raise


    def http_PROPPATCH(self, request):
        """
        No dead properties allowed on object resources.
        """
        if self._newStoreParent.objectResourcesHaveProperties():
            return super(_CommonObjectResource, self).http_PROPPATCH(request)
        else:
            return FORBIDDEN


    @inlineCallbacks
    def storeStream(self, stream):

        # FIXME: direct tests
        component = self._componentFromStream((yield allDataFromStream(stream)))
        result = (yield self.storeComponent(component))
        returnValue(result)


    @inlineCallbacks
    def storeComponent(self, component, **kwargs):

        try:
            if self._newStoreObject:
                yield self._newStoreObject.setComponent(component, **kwargs)
                returnValue(NO_CONTENT)
            else:
                self._newStoreObject = (yield self._newStoreParent.createObjectResourceWithName(
                    self.name(), component, self._metadata
                ))

                # Re-initialize to get stuff setup again now we have no object
                self._initializeWithObject(self._newStoreObject, self._newStoreParent)
                returnValue(CREATED)

        # Map store exception to HTTP errors
        except Exception as err:
            if type(err) in self.StoreExceptionsErrors:
                error, arg = self.StoreExceptionsErrors[type(err)]
                error(err, arg)
            else:
                raise


    @inlineCallbacks
    def storeMove(self, request, destinationparent, destination_name):
        """
        Move this object to a different parent.

        @param request:
        @type request: L{twext.web2.iweb.IRequest}
        @param destinationparent: Parent to move to
        @type destinationparent: L{CommonHomeChild}
        @param destination_name: name of new resource
        @type destination_name: C{str}
        """

        yield self._newStoreObject.moveTo(destinationparent._newStoreObject, destination_name)
        returnValue(CREATED)


    @inlineCallbacks
    def storeRemove(self, request):
        """
        Delete this object.

        @param request: Unused by this implementation; present for signature
            compatibility with L{CalendarCollectionResource.storeRemove}.

        @type request: L{twext.web2.iweb.IRequest}

        @return: an HTTP response suitable for sending to a client (or
            including in a multi-status).

         @rtype: something adaptable to L{twext.web2.iweb.IResponse}
        """

        # Do delete

        try:
            yield self._newStoreObject.remove()
        except NoSuchObjectResourceError:
            raise HTTPError(NOT_FOUND)

        # Map store exception to HTTP errors
        except Exception as err:
            if type(err) in self.StoreExceptionsErrors:
                error, arg = self.StoreExceptionsErrors[type(err)]
                error(err, arg)
            else:
                raise

        # Re-initialize to get stuff setup again now we have no object
        self._initializeWithObject(None, self._newStoreParent)

        returnValue(NO_CONTENT)



class _MetadataProperty(object):
    """
    A python property which can be set either on a _newStoreObject or on some
    metadata if no new store object exists yet.
    """

    def __init__(self, name):
        self.name = name


    def __get__(self, oself, ptype=None):
        if oself._newStoreObject:
            return getattr(oself._newStoreObject, self.name)
        else:
            return oself._metadata.get(self.name, None)


    def __set__(self, oself, value):
        if oself._newStoreObject:
            setattr(oself._newStoreObject, self.name, value)
        else:
            oself._metadata[self.name] = value



class _CalendarObjectMetaDataMixin(object):
    """
    Dynamically create the required meta-data for an object resource
    """

    accessMode = _MetadataProperty("accessMode")
    isScheduleObject = _MetadataProperty("isScheduleObject")
    scheduleTag = _MetadataProperty("scheduleTag")
    scheduleEtags = _MetadataProperty("scheduleEtags")
    hasPrivateComment = _MetadataProperty("hasPrivateComment")



class CalendarObjectResource(_CalendarObjectMetaDataMixin, _CommonObjectResource):
    """
    A resource wrapping a calendar object.
    """

    compareAttributes = (
        "_newStoreObject",
    )

    _componentFromStream = VCalendar.fromString

    @inlineCallbacks
    def inNewTransaction(self, request, label=""):
        """
        Implicit auto-replies need to span multiple transactions.  Clean out
        the given request's resource-lookup mapping, transaction, and re-look-
        up this L{CalendarObjectResource}'s calendar object in a new
        transaction.

        @return: a Deferred which fires with the new transaction, so it can be
            committed.
        """
        objectName = self._newStoreObject.name()
        calendar = self._newStoreObject.calendar()
        calendarName = calendar.name()
        ownerHome = calendar.ownerCalendarHome()
        homeUID = ownerHome.uid()
        txn = ownerHome.transaction().store().newTransaction(
            "new transaction for %s, doing: %s" % (self._newStoreObject.name(), label,))
        newParent = (yield (yield txn.calendarHomeWithUID(homeUID))
                             .calendarWithName(calendarName))
        newObject = (yield newParent.calendarObjectWithName(objectName))
        request._newStoreTransaction = txn
        request._resourcesByURL.clear()
        request._urlsByResource.clear()
        self._initializeWithObject(newObject, newParent)
        returnValue(txn)


    @inlineCallbacks
    def iCalendarText(self):
        data = yield self.iCalendar()
        returnValue(str(data))

    iCalendar = _CommonObjectResource.component


    def componentForUser(self):
        return self._newStoreObject.componentForUser()


    def validIfScheduleMatch(self, request):
        """
        Check to see if the given request's C{If-Schedule-Tag-Match} header
        matches this resource's schedule tag.

        @raise HTTPError: if the tag does not match.

        @return: None
        """
        # Note, internal requests shouldn't issue this.
        header = request.headers.getHeader("If-Schedule-Tag-Match")
        if header:
            # Do "precondition" test
            if (self.scheduleTag != header):
                log.debug(
                    "If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" %
                    (header, self.scheduleTag,))
                raise HTTPError(PRECONDITION_FAILED)
            return 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.
            return request.headers.getHeader("If-Match") is not None

        else:
            return False

    StoreExceptionsErrors = {
        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
        ObjectResourceTooBigError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "max-resource-size"),),
        InvalidObjectResourceError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
        InvalidComponentForStoreError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-object-resource"),),
        InvalidComponentTypeError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "supported-component"),),
        TooManyAttendeesError: (_CommonObjectResource._storeExceptionError, MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),),
        InvalidCalendarAccessError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-access-restriction"),),
        ValidOrganizerError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-organizer"),),
        UIDExistsError: (_CommonObjectResource._storeExceptionError, NoUIDConflict(),),
        UIDExistsElsewhereError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "unique-scheduling-object-resource"),),
        InvalidUIDError: (_CommonObjectResource._storeExceptionError, NoUIDConflict(),),
        InvalidPerUserDataMerge: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
        AttendeeAllowedError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "attendee-allowed"),),
        InvalidOverriddenInstanceError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
        TooManyInstancesError: (_CommonObjectResource._storeExceptionError, MaxInstances.fromString(str(config.MaxAllowedInstances)),),
        AttachmentStoreValidManagedID: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-managed-id"),),
        ShareeAllowedError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "sharee-privilege-needed",),),
        DuplicatePrivateCommentsError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "no-duplicate-private-comments",),),
        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
    }

    StoreMoveExceptionsErrors = {
        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
        InvalidResourceMove: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-move"),),
        InvalidComponentTypeError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "supported-component"),),
        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
    }

    StoreRemoveExceptionsErrors = {
        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
    }

    StoreAttachmentValidErrors = set((
        AttachmentStoreFailed,
        InvalidAttachmentOperation,
    ))

    StoreAttachmentExceptionsErrors = {
        AttachmentStoreValidManagedID: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-managed-id-parameter",),),
        AttachmentRemoveFailed: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-attachment-remove",),),
    }


    @inlineCallbacks
    def _checkPreconditions(self, request):
        """
        We override the base class to handle the special implicit scheduling weak ETag behavior
        for compatibility with old clients using If-Match.
        """

        if config.Scheduling.CalDAV.ScheduleTagCompatibility:

            if self.exists():
                etags = self.scheduleEtags
                if len(etags) > 1:
                    # This is almost verbatim from twext.web2.static.checkPreconditions
                    if request.method not in ("GET", "HEAD"):

                        # Always test against the current etag first just in case schedule-etags is out of sync
                        etag = (yield self.etag())
                        etags = (etag,) + tuple([http_headers.ETag(schedule_etag) for schedule_etag in etags])

                        # Loop over each tag and succeed if any one matches, else re-raise last exception
                        exists = self.exists()
                        last_modified = self.lastModified()
                        last_exception = None
                        for etag in etags:
                            try:
                                http.checkPreconditions(
                                    request,
                                    entityExists=exists,
                                    etag=etag,
                                    lastModified=last_modified,
                                )
                            except HTTPError, e:
                                last_exception = e
                            else:
                                break
                        else:
                            if last_exception:
                                raise last_exception

                    # Check per-method preconditions
                    method = getattr(self, "preconditions_" + request.method, None)
                    if method:
                        returnValue((yield method(request)))
                    else:
                        returnValue(None)

        result = (yield super(CalendarObjectResource, self).checkPreconditions(request))
        returnValue(result)


    @inlineCallbacks
    def checkPreconditions(self, request):
        """
        We override the base class to do special schedule tag processing.
        """

        try:
            response = yield self._checkPreconditions(request)
        except HTTPError as e:
            if e.response.code == responsecode.PRECONDITION_FAILED:
                response = yield self._processPrefer(request, e.response)
                raise HTTPError(response)
            else:
                raise

        returnValue(response)


    @inlineCallbacks
    def http_PUT(self, request):

        # Content-type check
        content_type = request.headers.getHeader("content-type")
        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
            log.error("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",
            ))

        # Do schedule tag check
        try:
            schedule_tag_match = self.validIfScheduleMatch(request)
        except HTTPError as e:
            if e.response.code == responsecode.PRECONDITION_FAILED:
                response = yield self._processPrefer(request, e.response)
                raise HTTPError(response)
            else:
                raise

        # Read the calendar component from the stream
        try:
            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"
                ))

            try:
                component = Component.fromString(calendardata)
            except ValueError, e:
                log.error(str(e))
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    (caldav_namespace, "valid-calendar-data"),
                    "Can't parse calendar data"
                ))

            # storeComponent needs to know who the auth'd user is for access control
            # TODO: this needs to be done in a better way - ideally when the txn is created for the request,
            # we should set a txn.authzid attribute.
            authz = None
            authz_principal = self._parentResource.currentPrincipal(request).children[0]
            if isinstance(authz_principal, davxml.HRef):
                principalURL = str(authz_principal)
                if principalURL:
                    authz = (yield request.locateResource(principalURL))
                    self._parentResource._newStoreObject._txn._authz_uid = authz.record.guid

            try:
                response = (yield self.storeComponent(component, smart_merge=schedule_tag_match))
            except ResourceDeletedError:
                # This is OK - it just means the server deleted the resource during the PUT. We make it look
                # like the PUT succeeded.
                response = responsecode.CREATED if self.exists() else responsecode.NO_CONTENT

                # Re-initialize to get stuff setup again now we have no object
                self._initializeWithObject(None, self._newStoreParent)

                returnValue(response)

            response = IResponse(response)

            if self._newStoreObject.isScheduleObject:
                # Add a response header
                response.headers.setHeader("Schedule-Tag", self._newStoreObject.scheduleTag)

            # Must not set ETag in response if data changed
            if self._newStoreObject._componentChanged:
                def _removeEtag(request, response):
                    response.headers.removeHeader('etag')
                    return response
                _removeEtag.handleErrors = True

                request.addResponseFilter(_removeEtag, atEnd=True)

            # Handle Prefer header
            response = yield self._processPrefer(request, response)

            returnValue(response)

        # Handle the various store errors
        except Exception as err:

            if isinstance(err, ValueError):
                log.error("Error while handling (calendar) PUT: %s" % (err,))
                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(err)))
            else:
                raise


    @requiresPermissions(fromParent=[davxml.Unbind()])
    def http_DELETE(self, request):
        """
        Override http_DELETE to do schedule tag behavior.
        """
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        # Do schedule tag check
        self.validIfScheduleMatch(request)

        return self.storeRemove(request)


    @inlineCallbacks
    def http_MOVE(self, request):
        """
        Need If-Schedule-Tag-Match behavior
        """

        # Do some pre-flight checks - must exist, must be move to another
        # CommonHomeChild in the same Home, destination resource must not exist
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        # Do schedule tag check
        self.validIfScheduleMatch(request)

        result = (yield super(CalendarObjectResource, self).http_MOVE(request))
        returnValue(result)


    @inlineCallbacks
    def POST_handler_action(self, request, action):
        """
        Handle a POST request with an action= query parameter

        @param request: the request to process
        @type request: L{Request}
        @param action: the action to execute
        @type action: C{str}
        """
        if action.startswith("attachment-"):
            result = (yield self.POST_handler_attachment(request, action))
            returnValue(result)
        else:
            actioner = {
                "split": self.POST_handler_split,
            }
            if action in actioner:
                result = (yield actioner[action](request, action))
                returnValue(result)
            else:
                raise HTTPError(ErrorResponse(
                    FORBIDDEN,
                    (caldav_namespace, "valid-action-parameter",),
                    "The action parameter in the request-URI is not valid",
                ))


    @requiresPermissions(davxml.WriteContent())
    @inlineCallbacks
    def POST_handler_split(self, request, action):
        """
        Handle a split of a calendar object resource.

        @param request: HTTP request object
        @type request: L{Request}
        @param action: The request-URI 'action' argument
        @type action: C{str}

        @return: an HTTP response
        """

        # Resource must exist
        if not self.exists():
            raise HTTPError(NOT_FOUND)

        # Split point is in the rid query parameter
        rid = request.args.get("rid")
        if rid is None:
            raise HTTPError(ErrorResponse(
                FORBIDDEN,
                (caldav_namespace, "valid-rid-parameter",),
                "The rid parameter in the request-URI contains an invalid value",
            ))

        try:
            rid = PyCalendarDateTime.parseText(rid[0])
        except ValueError:
            raise HTTPError(ErrorResponse(
                FORBIDDEN,
                (caldav_namespace, "valid-rid-parameter",),
                "The rid parameter in the request-URI contains an invalid value",
            ))
        try:
            otherStoreObject = yield self._newStoreObject.splitAt(rid)
        except InvalidSplit:
            raise HTTPError(ErrorResponse(
                FORBIDDEN,
                (calendarserver_namespace, "invalid-split",),
                "The rid parameter in the request-URI contains an invalid value",
            ))

        other = yield request.locateChildResource(self._parentResource, otherStoreObject.name())
        if other is None:
            raise responsecode.INTERNAL_SERVER_ERROR

        # 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:
            etag1 = yield self.etag()
            etag2 = yield other.etag()
            cal1 = yield self.component()
            cal2 = yield other.component()

            xml_responses = [
                davxml.PropertyStatusResponse(
                    davxml.HRef.fromString(self.url()),
                    davxml.PropertyStatus(
                        davxml.PropertyContainer(
                            davxml.GETETag.fromString(etag1.generate()),
                            caldavxml.CalendarData.fromCalendar(cal1),
                        ),
                        davxml.Status.fromResponseCode(OK),
                    )
                ),
                davxml.PropertyStatusResponse(
                    davxml.HRef.fromString(other.url()),
                    davxml.PropertyStatus(
                        davxml.PropertyContainer(
                            davxml.GETETag.fromString(etag2.generate()),
                            caldavxml.CalendarData.fromCalendar(cal2),
                        ),
                        davxml.Status.fromResponseCode(OK),
                    )
                ),
            ]

            # Return multistatus with calendar data for this resource and the new one
            result = MultiStatusResponse(xml_responses)
        else:
            result = Response(responsecode.NO_CONTENT)
            result.headers.addRawHeader("Split-Component-URL", other.url())

        returnValue(result)


    @requiresPermissions(davxml.WriteContent())
    @inlineCallbacks
    def POST_handler_attachment(self, request, action):
        """
        Handle a managed attachments request on the calendar object resource.

        @param request: HTTP request object
        @type request: L{Request}
        @param action: The request-URI 'action' argument
        @type action: C{str}

        @return: an HTTP response
        """

        if not config.EnableManagedAttachments:
            returnValue(StatusResponse(responsecode.FORBIDDEN, "Managed Attachments not supported."))

        # Resource must exist to allow attachment operations
        if not self.exists():
            raise HTTPError(NOT_FOUND)

        def _getRIDs():
            rids = request.args.get("rid")
            if rids is not None:
                rids = rids[0].split(",")
                try:
                    rids = [PyCalendarDateTime.parseText(rid) if rid != "M" else None for rid in rids]
                except ValueError:
                    raise HTTPError(ErrorResponse(
                        FORBIDDEN,
                        (caldav_namespace, "valid-rid-parameter",),
                        "The rid parameter in the request-URI contains an invalid value",
                    ))

                if rids:
                    raise HTTPError(ErrorResponse(
                        FORBIDDEN,
                        (caldav_namespace, "valid-rid-parameter",),
                        "Server does not support per-instance attachments",
                    ))

            return rids

        def _getMID():
            mid = request.args.get("managed-id")
            if mid is None:
                raise HTTPError(ErrorResponse(
                    FORBIDDEN,
                    (caldav_namespace, "valid-managed-id-parameter",),
                    "The managed-id parameter is missing from the request-URI",
                ))
            return mid[0]

        def _getContentInfo():
            content_type = request.headers.getHeader("content-type")
            if content_type is None:
                content_type = MimeType("application", "octet-stream")
            content_disposition = request.headers.getHeader("content-disposition")
            if content_disposition is None or "filename" not in content_disposition.params:
                filename = str(uuid.uuid4())
            else:
                filename = content_disposition.params["filename"]
            return content_type, filename

        valid_preconditions = {
            "attachment-add": "valid-attachment-add",
            "attachment-update": "valid-attachment-update",
            "attachment-remove": "valid-attachment-remove",
        }

        # Dispatch to store object
        try:
            if action == "attachment-add":
                rids = _getRIDs()
                content_type, filename = _getContentInfo()
                attachment, location = (yield self._newStoreObject.addAttachment(rids, content_type, filename, request.stream))
                post_result = Response(CREATED)

            elif action == "attachment-update":
                mid = _getMID()
                content_type, filename = _getContentInfo()
                attachment, location = (yield self._newStoreObject.updateAttachment(mid, content_type, filename, request.stream))
                post_result = Response(NO_CONTENT)

            elif action == "attachment-remove":
                rids = _getRIDs()
                mid = _getMID()
                yield self._newStoreObject.removeAttachment(rids, mid)
                post_result = Response(NO_CONTENT)

            else:
                raise HTTPError(ErrorResponse(
                    FORBIDDEN,
                    (caldav_namespace, "valid-action-parameter",),
                    "The action parameter in the request-URI is not valid",
                ))

        except QuotaExceeded:
            raise HTTPError(ErrorResponse(
                INSUFFICIENT_STORAGE_SPACE,
                (dav_namespace, "quota-not-exceeded"),
                "Could not store the supplied attachment because user quota would be exceeded",
            ))

        # Map store exception to HTTP errors
        except Exception as err:

            if type(err) in self.StoreAttachmentValidErrors:
                self._storeExceptionError(err, (caldav_namespace, valid_preconditions[action],))

            elif type(err) in self.StoreAttachmentExceptionsErrors:
                error, arg = self.StoreAttachmentExceptionsErrors[type(err)]
                error(err, arg)

            elif type(err) in self.StoreExceptionsErrors:
                error, arg = self.StoreExceptionsErrors[type(err)]
                error(err, arg)

            else:
                raise

        # Look for Prefer header
        result = yield self._processPrefer(request, post_result)

        if action in ("attachment-add", "attachment-update",):
            result.headers.setHeader("location", location)
            result.headers.addRawHeader("Cal-Managed-ID", attachment.managedID())

        returnValue(result)



class AddressBookCollectionResource(_CommonHomeChildCollectionMixin, CalDAVResource):
    """
    Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
    """

    def __init__(self, addressbook, home, name=None, *args, **kw):
        """
        Create a AddressBookCollectionResource from a L{txdav.carddav.iaddressbook.IAddressBook}
        and the arguments required for L{CalDAVResource}.
        """

        self._childClass = AddressBookObjectResource
        super(AddressBookCollectionResource, self).__init__(*args, **kw)
        self._initializeWithHomeChild(addressbook, home)
        self._name = addressbook.name() if addressbook else name

        if config.EnableBatchUpload:
            self._postHandlers[("text", "vcard")] = AddressBookCollectionResource.simpleBatchPOST
            self.xmlDocHandlers[customxml.Multiput] = AddressBookCollectionResource.crudBatchPOST


    def __repr__(self):
        return "<AddressBook Collection Resource %r:%r %s>" % (
            self._newStoreParentHome.uid(),
            self._name,
            "" if self._newStoreObject else "Non-existent"
        )


    def isCollection(self):
        return True


    def isAddressBookCollection(self):
        return True


    def resourceType(self):
        if self.isShared():
            return customxml.ResourceType.sharedowneraddressbook
        elif self.isShareeResource():
            return customxml.ResourceType.sharedaddressbook
        else:
            return carddavxml.ResourceType.addressbook

    createAddressBookCollection = _CommonHomeChildCollectionMixin.createCollection


    @classmethod
    def componentsFromData(cls, data):
        try:
            return VCard.allFromString(data)
        except InvalidVCardDataError:
            return None


    @classmethod
    def resourceSuffix(cls):
        return ".vcf"


    @classmethod
    def xmlDataElementType(cls):
        return carddavxml.AddressData


    @inlineCallbacks
    def storeResourceData(self, newchild, component, returnChangedData=False):

        yield newchild.storeComponent(component)
        if returnChangedData and newchild._newStoreObject._componentChanged:
            result = (yield newchild.component())
            returnValue(str(result))
        else:
            returnValue(None)


    def http_MOVE(self, request):
        """
        Addressbooks may not be renamed.
        """
        return FORBIDDEN


    @inlineCallbacks
    def makeChild(self, name):
        """
        call super and provision group share
        """
        abObjectResource = yield super(AddressBookCollectionResource, self).makeChild(name)
        if abObjectResource.exists() and abObjectResource._newStoreObject.shareUID() is not None:
            abObjectResource = yield self.parentResource().provisionShare(abObjectResource)
        returnValue(abObjectResource)


    @inlineCallbacks
    def storeRemove(self, request):
        """
        handle remove of partially shared addressbook, else call super
        """
        if self.isShareeResource() and self._newStoreObject.shareUID() is None:
            log.debug("Removing shared collection %s" % (self,))
            for childname in (yield self.listChildren()):
                child = (yield request.locateChildResource(self, childname))
                if child.isShareeResource():
                    yield child.storeRemove(request)

            returnValue(NO_CONTENT)

        returnValue((yield super(AddressBookCollectionResource, self).storeRemove(request)))


    @inlineCallbacks
    def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses):
        """
        bulk create allowing groups to contain member UIDs added during the same bulk create
        """
        groupRetries = []
        coaddedUIDs = set()
        for index, component in indexedComponents:

            try:
                # Create a new name if one was not provided
                name = md5(str(index) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()

                # Get a resource for the new item
                newchildURL = joinURL(request.path, name)
                newchild = (yield request.locateResource(newchildURL))
                changedData = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))

            except GroupWithUnsharedAddressNotAllowedError, e:
                # save off info and try again below
                missingUIDs = set(e.message)
                groupRetries.append((index, component, newchildURL, newchild, missingUIDs,))

            except HTTPError, e:
                # Extract the pre-condition
                code = e.response.code
                if isinstance(e.response, ErrorResponse):
                    error = e.response.error
                    error = (error.namespace, error.name,)

                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error)
                )

            except Exception:
                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code=BAD_REQUEST, error=None)
                )

            else:
                if not return_changed:
                    changedData = None
                coaddedUIDs |= set([component.resourceUID()])
                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
                )

        if groupRetries:
            # get set of UIDs added
            coaddedUIDs |= set([groupRetry[1].resourceUID() for groupRetry in groupRetries])

            # check each group add to see if it will succeed if coaddedUIDs are allowed
            while(True):
                for groupRetry in groupRetries:
                    if bool(groupRetry[4] - coaddedUIDs):
                        break
                else:
                    break

                # give FORBIDDEN response
                index, component, newchildURL, newchild, missingUIDs = groupRetry
                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData=None, code=FORBIDDEN, error=None)
                )
                coaddedUIDs -= set([component.resourceUID()]) # group uid not added
                groupRetries.remove(groupRetry) # remove this retry

            for index, component, newchildURL, newchild, missingUIDs in groupRetries:
                # newchild._metadata -> newchild._options during store
                newchild._metadata["coaddedUIDs"] = coaddedUIDs

                # don't catch errors, abort the whole transaction
                changedData = yield self.storeResourceData(newchild, component, returnChangedData=return_changed)
                if not return_changed:
                    changedData = None
                xmlresponses[index] = (
                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
                )


    @inlineCallbacks
    def crudDelete(self, crudDeleteInfo, request, xmlresponses):
        """
        Change handling of privileges
        """
        if crudDeleteInfo:
            # Do privilege check on collection once
            try:
                yield self.authorize(request, (davxml.Unbind(),))
                hasPrivilege = True
            except HTTPError, e:
                hasPrivilege = e

            for index, href, ifmatch in crudDeleteInfo:
                code = None
                error = None
                try:
                    deleteResource = (yield request.locateResource(href))
                    if not deleteResource.exists():
                        raise HTTPError(NOT_FOUND)

                    # Check if match
                    etag = (yield deleteResource.etag())
                    if ifmatch and ifmatch != etag.generate():
                        raise HTTPError(PRECONDITION_FAILED)

                    #===========================================================
                    # # If unshared is allowed deletes fails but crud adds works work!
                    # if (hasPrivilege is not True and not (
                    #             deleteResource.isShareeResource() or
                    #             deleteResource._newStoreObject.isGroupForSharedAddressBook()
                    #         )
                    #     ):
                    #     raise hasPrivilege
                    #===========================================================

                    # don't allow shared group deletion -> unshare
                    if (deleteResource.isShareeResource() or
                        deleteResource._newStoreObject.isGroupForSharedAddressBook()):
                        raise HTTPError(FORBIDDEN)

                    if hasPrivilege is not True:
                        raise hasPrivilege

                    yield deleteResource.storeRemove(request)

                except HTTPError, e:
                    # Extract the pre-condition
                    code = e.response.code
                    if isinstance(e.response, ErrorResponse):
                        error = e.response.error
                        error = (error.namespace, error.name,)

                except Exception:
                    code = BAD_REQUEST

                if code is None:
                    xmlresponses[index] = davxml.StatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.Status.fromResponseCode(OK),
                    )
                else:
                    xmlresponses[index] = davxml.StatusResponse(
                        davxml.HRef.fromString(href),
                        davxml.Status.fromResponseCode(code),
                        davxml.Error(
                            WebDAVUnknownElement.withName(*error),
                        ) if error else None,
                    )



class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
    """
    Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
    """
    pass



class AddressBookObjectResource(_CommonObjectResource):
    """
    A resource wrapping a addressbook object.
    """

    compareAttributes = (
        "_newStoreObject",
    )

    _componentFromStream = VCard.fromString

    @inlineCallbacks
    def vCardText(self):
        data = yield self.vCard()
        returnValue(str(data))

    vCard = _CommonObjectResource.component

    StoreExceptionsErrors = {
        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
        ObjectResourceTooBigError: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "max-resource-size"),),
        InvalidObjectResourceError: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "valid-address-data"),),
        InvalidComponentForStoreError: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "valid-addressbook-object-resource"),),
        UIDExistsError: (_CommonObjectResource._storeExceptionError, NovCardUIDConflict(),),
        InvalidUIDError: (_CommonObjectResource._storeExceptionError, NovCardUIDConflict(),),
        InvalidPerUserDataMerge: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "valid-address-data"),),
        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
    }

    StoreMoveExceptionsErrors = {
        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
        InvalidResourceMove: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-move"),),
        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
    }

    StoreRemoveExceptionsErrors = {
        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
    }


    def resourceType(self):
        if self.isShared():
            return customxml.ResourceType.sharedownergroup
        elif self.isShareeResource():
            return customxml.ResourceType.sharedgroup
        else:
            return super(AddressBookObjectResource, self).resourceType()


    @inlineCallbacks
    def storeRemove(self, request):
        """
        Remove this address book object
        """

        # Handle sharing
        if self.isShareeResource():
            log.debug("Removing shared resource %s" % (self,))
            yield self.removeShareeResource(request)
            returnValue(NO_CONTENT)
        elif self._newStoreObject.isGroupForSharedAddressBook():
            abCollectionResource = (yield request.locateResource(parentForURL(request.uri)))
            returnValue((yield abCollectionResource.storeRemove(request)))

        elif self.isShared():
            yield self.downgradeFromShare(request)

        response = (
            yield super(AddressBookObjectResource, self).storeRemove(
                request
            )
        )

        returnValue(response)


    @inlineCallbacks
    def http_PUT(self, request):

        # 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.error("MIME type %s not allowed in vcard collection" % (content_type,))
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (carddav_namespace, "supported-address-data"),
                "Invalid MIME type for vcard collection",
            ))

        # Read the vcard 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"
                ))

            try:
                component = VCard.fromString(vcarddata)
            except ValueError, e:
                log.error(str(e))
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    (carddav_namespace, "valid-address-data"),
                    "Could not parse vCard",
                ))

            # storeComponent needs to know who the auth'd user is for access control
            # TODO: this needs to be done in a better way - ideally when the txn is created for the request,
            # we should set a txn.authzid attribute.
            authz = None
            authz_principal = self._parentResource.currentPrincipal(request).children[0]
            if isinstance(authz_principal, davxml.HRef):
                principalURL = str(authz_principal)
                if principalURL:
                    authz = (yield request.locateResource(principalURL))
                    self._parentResource._newStoreObject._txn._authz_uid = authz.record.guid

            try:
                response = (yield self.storeComponent(component))
            except ResourceDeletedError:
                # This is OK - it just means the server deleted the resource during the PUT. We make it look
                # like the PUT succeeded.
                response = responsecode.CREATED if self.exists() else responsecode.NO_CONTENT

                # Re-initialize to get stuff setup again now we have no object
                self._initializeWithObject(None, self._newStoreParent)

                returnValue(response)

            response = IResponse(response)

            # Must not set ETag in response if data changed
            if self._newStoreObject._componentChanged:
                def _removeEtag(request, response):
                    response.headers.removeHeader('etag')
                    return response
                _removeEtag.handleErrors = True

                request.addResponseFilter(_removeEtag, atEnd=True)

            # Look for Prefer header
            response = yield self._processPrefer(request, response)

            returnValue(response)

        # Handle the various store errors
        except KindChangeNotAllowedError:
            raise HTTPError(StatusResponse(
                FORBIDDEN,
                "vCard kind may not be changed",)
            )

        # Handle the various store errors
        except GroupWithUnsharedAddressNotAllowedError:
            raise HTTPError(StatusResponse(
                FORBIDDEN,
                "Sharee cannot add unshared group members",)
            )

        except Exception as err:

            if isinstance(err, ValueError):
                log.error("Error while handling (vCard) PUT: %s" % (err,))
                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(err)))
            else:
                raise


    @inlineCallbacks
    def http_DELETE(self, request):
        """
        Override http_DELETE handle shared group deletion without fromParent=[davxml.Unbind()]
        """
        if (self.isShareeResource() or
            self.exists() and self._newStoreObject.isGroupForSharedAddressBook()):
            returnValue((yield self.storeRemove(request)))

        returnValue((yield super(AddressBookObjectResource, self).http_DELETE(request)))


    @inlineCallbacks
    def accessControlList(self, request, *a, **kw):
        """
        Return WebDAV ACLs appropriate for the current user accessing the
        a vcard in a shared addressbook or shared group.

        Items in an "invite" share get read-only privileges.
        (It's not clear if that case ever occurs)

        "direct" shares are not supported.

        @param request: the request used to locate the owner resource.
        @type request: L{twext.web2.iweb.IRequest}

        @param args: The arguments for
            L{twext.web2.dav.idav.IDAVResource.accessControlList}

        @param kwargs: The keyword arguments for
            L{twext.web2.dav.idav.IDAVResource.accessControlList}, plus
            keyword-only arguments.

        @return: the appropriate WebDAV ACL for the sharee
        @rtype: L{davxml.ACL}
        """
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        if not self._parentResource.isShareeResource():
            returnValue((yield super(AddressBookObjectResource, self).accessControlList(request, *a, **kw)))

        # Direct shares use underlying privileges of shared collection
        userprivs = []
        userprivs.append(davxml.Privilege(davxml.Read()))
        userprivs.append(davxml.Privilege(davxml.ReadACL()))
        userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))

        if (yield self._newStoreObject.readWriteAccess()):
            userprivs.append(davxml.Privilege(davxml.Write()))
        else:
            userprivs.append(davxml.Privilege(davxml.WriteProperties()))

        sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
        aces = (
            # Inheritable specific access for the resource's associated principal.
            davxml.ACE(
                davxml.Principal(davxml.HRef(sharee.principalURL())),
                davxml.Grant(*userprivs),
                davxml.Protected(),
                TwistedACLInheritable(),
            ),
        )

        # Give read access to config.ReadPrincipals
        aces += config.ReadACEs

        # Give all access to config.AdminPrincipals
        aces += config.AdminACEs

        returnValue(davxml.ACL(*aces))



class _NotificationChildHelper(object):
    """
    Methods for things which are like notification objects.
    """

    def _initializeWithNotifications(self, notifications, home):
        """
        Initialize with a notification collection.

        @param notifications: the wrapped notification collection backend
            object.
        @type notifications: L{txdav.common.inotification.INotificationCollection}

        @param home: the home through which the given notification collection
            was accessed.
        @type home: L{txdav.icommonstore.ICommonHome}
        """
        self._newStoreNotifications = notifications
        self._newStoreParentHome = home
        self._dead_properties = _NewStorePropertiesWrapper(
            self._newStoreNotifications.properties()
        )


    def locateChild(self, request, segments):
        if segments[0] == '':
            return self, segments[1:]
        return self.getChild(segments[0]), segments[1:]


    def exists(self):
        # FIXME: tests
        return True


    @inlineCallbacks
    def makeChild(self, name):
        """
        Create a L{NotificationObjectFile} or L{ProtoNotificationObjectFile}
        based on the name of a notification.
        """
        newStoreObject = (
            yield self._newStoreNotifications.notificationObjectWithName(name)
        )

        similar = StoreNotificationObjectFile(newStoreObject, self)

        # FIXME: tests should be failing without this line.
        # Specifically, http_PUT won't be committing its transaction properly.
        self.propagateTransaction(similar)
        returnValue(similar)


    @inlineCallbacks
    def listChildren(self):
        """
        @return: a sequence of the names of all known children of this resource.
        """
        children = set(self.putChildren.keys())
        children.update(self._newStoreNotifications.listNotificationObjects())
        returnValue(children)



class StoreNotificationCollectionResource(_NotificationChildHelper, NotificationCollectionResource):
    """
    Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
    """

    def __init__(self, notifications, homeResource, home, *args, **kw):
        """
        Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
        and the arguments required for L{CalDAVResource}.
        """
        super(StoreNotificationCollectionResource, self).__init__(*args, **kw)
        self._initializeWithNotifications(notifications, home)
        self._parentResource = homeResource


    def name(self):
        return "notification"


    def url(self):
        return joinURL(self._parentResource.url(), self.name(), "/")


    @inlineCallbacks
    def listChildren(self):
        l = []
        for notification in (yield self._newStoreNotifications.notificationObjects()):
            l.append(notification.name())
        returnValue(l)


    def isCollection(self):
        return True


    def getInternalSyncToken(self):
        return self._newStoreNotifications.syncToken()


    @inlineCallbacks
    def _indexWhatChanged(self, revision, depth):
        # The newstore implementation supports this directly
        returnValue(
            (yield self._newStoreNotifications.resourceNamesSinceToken(revision))
            + ([],)
        )


    def addNotification(self, request, uid, xmltype, xmldata):
        return maybeDeferred(
            self._newStoreNotifications.writeNotificationObject,
            uid, xmltype, xmldata
        )


    def deleteNotification(self, request, record):
        return maybeDeferred(
            self._newStoreNotifications.removeNotificationObjectWithName,
            record.name
        )



class StoreNotificationObjectFile(_NewStoreFileMetaDataHelper, NotificationResource):
    """
    A resource wrapping a calendar object.
    """

    def __init__(self, notificationObject, *args, **kw):
        """
        Construct a L{CalendarObjectResource} from an L{ICalendarObject}.

        @param calendarObject: The storage for the calendar object.
        @type calendarObject: L{txdav.caldav.icalendarstore.ICalendarObject}
        """
        super(StoreNotificationObjectFile, self).__init__(*args, **kw)
        self._initializeWithObject(notificationObject)


    def _initializeWithObject(self, notificationObject):
        self._newStoreObject = notificationObject
        self._dead_properties = NonePropertyStore(self)


    def liveProperties(self):

        props = super(StoreNotificationObjectFile, self).liveProperties()
        props += (customxml.NotificationType.qname(),)
        return props


    @inlineCallbacks
    def readProperty(self, prop, request):
        if type(prop) is tuple:
            qname = prop
        else:
            qname = prop.qname()

        if qname == customxml.NotificationType.qname():
            returnValue(self._newStoreObject.xmlType())

        returnValue((yield super(StoreNotificationObjectFile, self).readProperty(prop, request)))


    def isCollection(self):
        return False


    def quotaSize(self, request):
        return succeed(self._newStoreObject.size())


    def text(self, ignored=None):
        assert ignored is None, "This is a notification object, not a notification"
        return self._newStoreObject.xmldata()


    @requiresPermissions(davxml.Read())
    @inlineCallbacks
    def http_GET(self, request):
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        returnValue(
            Response(OK, {"content-type": self.contentType()},
                     MemoryStream((yield self.text())))
        )


    @requiresPermissions(fromParent=[davxml.Unbind()])
    def http_DELETE(self, request):
        """
        Override http_DELETE to validate 'depth' header.
        """
        if not self.exists():
            log.debug("Resource not found: %s" % (self,))
            raise HTTPError(NOT_FOUND)

        return self.storeRemove(request)


    def http_PROPPATCH(self, request):
        """
        No dead properties allowed on notification objects.
        """
        return FORBIDDEN


    @inlineCallbacks
    def storeRemove(self, request):
        """
        Remove this notification object.
        """
        try:

            storeNotifications = self._newStoreObject.notificationCollection()

            # Do delete

            # FIXME: public attribute please
            yield storeNotifications.removeNotificationObjectWithName(
                self._newStoreObject.name()
            )

            self._initializeWithObject(None)

        except MemcacheLockTimeoutError:
            raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (request.uri,)))
        except NoSuchObjectResourceError:
            raise HTTPError(NOT_FOUND)

        returnValue(NO_CONTENT)
