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

"""
CalDAV-aware resources.
"""

__all__ = [
    "CalDAVComplianceMixIn",
    "CalDAVResource",
    "CalendarPrincipalCollectionResource",
    "CalendarPrincipalResource",
    "isCalendarCollectionResource",
    "isPseudoCalendarCollectionResource",
    "isAddressBookCollectionResource",
]

from calendarserver.push.notifier import getPubSubAPSConfiguration

from twext.enterprise.ienterprise import AlreadyFinishedError
from twext.python.log import Logger

from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.defer import succeed, maybeDeferred, fail

from twistedcaldav import caldavxml, customxml
from twistedcaldav import carddavxml
from twistedcaldav import ical
from twistedcaldav.cache import PropfindCacheMixin
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.carddavxml import carddav_namespace
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
from twistedcaldav.datafilters.privateevents import PrivateEventFilter
from twistedcaldav.extensions import DAVResource, DAVPrincipalResource, \
    DAVResourceWithChildrenMixin
from twistedcaldav.ical import Component
from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
from twistedcaldav.linkresource import LinkResource
from twistedcaldav.sharing import SharedResourceMixin, SharedHomeMixin
from twistedcaldav.vcard import Component as vComponent

from txdav.caldav.datastore.util import normalizationLookup
from txdav.common.icommondatastore import InternalDataStoreError, \
    SyncTokenValidException, ExternalShareFailed
from txdav.common.datastore.podding.base import FailedCrossPodRequestError
from txdav.xml import element
from txdav.xml.element import dav_namespace

from txweb2 import responsecode, http, http_headers
from txweb2.auth.wrapper import UnauthorizedResponse
from txweb2.dav.auth import AuthenticationWrapper as SuperAuthenticationWrapper
from txweb2.dav.http import ErrorResponse
from txweb2.dav.idav import IDAVPrincipalCollectionResource
from txweb2.dav.resource import AccessDeniedError, DAVPrincipalCollectionResource, \
    davPrivilegeSet
from txweb2.dav.resource import TwistedACLInheritable
from txweb2.dav.util import joinURL, parentForURL, normalizeURL
from txweb2.http import HTTPError, RedirectResponse, StatusResponse, Response, JSONResponse
from txweb2.http_headers import MimeType, ETag
from txweb2.stream import MemoryStream

from urlparse import urlsplit
from uuid import UUID
from zope.interface import implements
import hashlib
import time
import urllib
import uuid

##
# Sharing Conts
##
SHARE_ACCEPT_STATE_NEEDS_ACTION = "0"
SHARE_ACCEPT_STATE_ACCEPTED = "1"
SHARE_ACCEPT_STATE_DECLINED = "2"
SHARE_ACCEPT_STATE_DELETED = "-1"

shareAccpetStates = {}
shareAccpetStates[SHARE_ACCEPT_STATE_NEEDS_ACTION] = "NEEDS-ACTION"
shareAccpetStates[SHARE_ACCEPT_STATE_ACCEPTED] = "ACCEPTED"
shareAccpetStates[SHARE_ACCEPT_STATE_DECLINED] = "DECLINED"
shareAccpetStates[SHARE_ACCEPT_STATE_DELETED] = "DELETED"

shareAcceptStatesByXML = {}
shareAcceptStatesByXML["NEEDS-ACTION"] = customxml.InviteStatusNoResponse()
shareAcceptStatesByXML["ACCEPTED"] = customxml.InviteStatusAccepted()
shareAcceptStatesByXML["DECLINED"] = customxml.InviteStatusDeclined()
shareAcceptStatesByXML["DELETED"] = customxml.InviteStatusDeleted()


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 CalDAVComplianceMixIn(object):

    def davComplianceClasses(self):
        return (
            tuple(super(CalDAVComplianceMixIn, self).davComplianceClasses()) +
            config.CalDAVComplianceClasses
        )


class ReadOnlyResourceMixIn (object):
    """
    Read only resource.
    """

    def writeProperty(self, property, request):
        raise HTTPError(self.readOnlyResponse)

    def http_ACL(self, request):
        return responsecode.FORBIDDEN

    def http_DELETE(self, request):
        return responsecode.FORBIDDEN

    def http_MKCOL(self, request):
        return responsecode.FORBIDDEN

    def http_MOVE(self, request):
        return responsecode.FORBIDDEN

    def http_PROPPATCH(self, request):
        return responsecode.FORBIDDEN

    def http_PUT(self, request):
        return responsecode.FORBIDDEN

    def http_MKCALENDAR(self, request):
        return ErrorResponse(
            responsecode.FORBIDDEN,
            (caldav_namespace, "calendar-collection-location-ok"),
            "Resource is read-only",
        )


class ReadOnlyNoCopyResourceMixIn (ReadOnlyResourceMixIn):
    """
    Read only resource that disallows COPY.
    """

    def http_COPY(self, request):
        return responsecode.FORBIDDEN


def _calendarPrivilegeSet():
    edited = False

    top_supported_privileges = []

    for supported_privilege in davPrivilegeSet.childrenOfType(element.SupportedPrivilege):
        all_privilege = supported_privilege.childOfType(element.Privilege)
        if isinstance(all_privilege.children[0], element.All):
            all_description = supported_privilege.childOfType(element.Description)
            all_supported_privileges = []
            for all_supported_privilege in supported_privilege.childrenOfType(element.SupportedPrivilege):
                read_privilege = all_supported_privilege.childOfType(element.Privilege)
                if isinstance(read_privilege.children[0], element.Read):
                    read_description = all_supported_privilege.childOfType(element.Description)
                    read_supported_privileges = list(all_supported_privilege.childrenOfType(element.SupportedPrivilege))
                    read_supported_privileges.append(
                        element.SupportedPrivilege(
                            element.Privilege(caldavxml.ReadFreeBusy()),
                            element.Description("allow free busy report query", **{"xml:lang": "en"}),
                        )
                    )
                    all_supported_privileges.append(
                        element.SupportedPrivilege(read_privilege, read_description, *read_supported_privileges)
                    )
                    edited = True
                else:
                    all_supported_privileges.append(all_supported_privilege)
            top_supported_privileges.append(
                element.SupportedPrivilege(all_privilege, all_description, *all_supported_privileges)
            )
        else:
            top_supported_privileges.append(supported_privilege)

    assert edited, "Structure of davPrivilegeSet changed in a way that I don't know how to extend for calendarPrivilegeSet"

    return element.SupportedPrivilegeSet(*top_supported_privileges)

calendarPrivilegeSet = _calendarPrivilegeSet()


class CalDAVResource (
        CalDAVComplianceMixIn, SharedResourceMixin,
        DAVResourceWithChildrenMixin, DAVResource
):
    """
    CalDAV resource.

    Extends L{DAVResource} to provide CalDAV functionality.
    """
    log = Logger()

    implements(ICalDAVResource)

    uuid_namespace = UUID("DD0E1AC0-56D6-40D4-8765-2F4D8A0F28A5")

    ##
    # HTTP
    ##

    def render(self, request):

        if not self.exists():
            return responsecode.NOT_FOUND

        if config.EnableMonolithicCalendars:
            #
            # Send listing instead of iCalendar data to HTML agents
            # This is mostly useful for debugging...
            #
            # FIXME: Add a self-link to the dirlist with a query string so
            #     users can still download the actual iCalendar data?
            #
            # FIXME: Are there better ways to detect this than hacking in
            #     user agents?
            #
            # FIXME: In the meantime, make this a configurable regex list?
            #
            agent = request.headers.getHeader("user-agent")
            if agent is not None and (
                agent.startswith("Mozilla/") and agent.find("Gecko") != -1
            ):
                renderAsHTML = True
            else:
                renderAsHTML = False
        else:
            renderAsHTML = True

        if not renderAsHTML and self.isPseudoCalendarCollection():
            # Render a monolithic iCalendar file
            if request.path[-1] != "/":
                # Redirect to include trailing '/' in URI
                return RedirectResponse(request.unparseURL(path=urllib.quote(urllib.unquote(request.path), safe=':/') + '/'))

            def _defer(result):
                data, accepted_type = result
                response = Response()
                response.stream = MemoryStream(data.getText(accepted_type))
                response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
                return response

            d = self.iCalendarRolledup(request)
            d.addCallback(_defer)
            return d

        return super(CalDAVResource, self).render(request)

    _associatedTransaction = None
    _transactionError = False

    def associateWithTransaction(self, transaction):
        """
        Associate this resource with a L{txdav.caldav.idav.ITransaction}; when this
        resource (or any of its children) are rendered successfully, commit the
        transaction.  Otherwise, abort the transaction.

        @param transaction: the transaction to associate this resource and its
            children with.

        @type transaction: L{txdav.caldav.idav.ITransaction}
        """
        # FIXME: needs to reject association with transaction if it's already
        # got one (resources associated with a transaction are not reusable)
        self._associatedTransaction = transaction

    def propagateTransaction(self, otherResource):
        """
        Propagate the transaction associated with this resource to another
        resource (which should ostensibly be a child resource).

        @param otherResource: Another L{CalDAVResource}, usually one being
            constructed as a child of this one.

        @type otherResource: L{CalDAVResource} (or a subclass thereof)
        """
        if not self._associatedTransaction:
            raise RuntimeError("No associated transaction to propagate")
        otherResource.associateWithTransaction(self._associatedTransaction)

    def methodRaisedException(self, failure):
        """
        An C{http_METHOD} method raised an exception.  Any type of exception,
        including those that result in perfectly valid HTTP responses, should
        abort the transaction.
        """
        self._transactionError = True
        return super(CalDAVResource, self).methodRaisedException(failure)

    @inlineCallbacks
    def renderHTTP(self, request, transaction=None):
        """
        Override C{renderHTTP} to commit the transaction when the resource is
        successfully rendered.

        @param request: the request to generate a response for.
        @type request: L{txweb2.iweb.IRequest}
        @param transaction: optional transaction to use instead of associated transaction
        @type transaction: L{txdav.caldav.idav.ITransaction}
        """
        try:
            response = yield super(CalDAVResource, self).renderHTTP(request)
        except AlreadyFinishedError:
            self._transactionError = True
        except ExternalShareFailed:
            # This happens when an external share is no longer valid and has been fixed
            # by removing it from this pod. We need to treat this as a 503 "error" but let
            # the transaction commit
            self._transactionError = False
            response = StatusResponse(responsecode.SERVICE_UNAVAILABLE, "Shared collection not valid - removing.")
        except FailedCrossPodRequestError:
            # This happens when a cross-pod connection attempt fails. Treat as a 503 so the client
            # can try again once the pod is back up.
            response = StatusResponse(responsecode.SERVICE_UNAVAILABLE, "Unable to do cross-pod request.")
            response.headers.setHeader("Retry-After", time.time() + config.TransactionHTTPRetrySeconds)

        if transaction is None:
            transaction = self._associatedTransaction
        if transaction is not None:
            if self._transactionError:
                try:
                    yield transaction.abort()
                except AlreadyFinishedError:
                    if transaction.timedout:
                        response = http.StatusResponse(responsecode.SERVICE_UNAVAILABLE, responsecode.RESPONSES[responsecode.SERVICE_UNAVAILABLE])
                        response.headers.setHeader("Retry-After", time.time() + config.TransactionHTTPRetrySeconds)
                        raise HTTPError(response)
                    else:
                        raise
            else:
                yield transaction.commit()

                # Log extended item
                if transaction.logItems:
                    if not hasattr(request, "extendedLogItems"):
                        request.extendedLogItems = {}
                    request.extendedLogItems.update(transaction.logItems)

                # May need to reset the last-modified header in the response as txn.commit() can change it due to pre-commit hooks
                if response.headers.hasHeader("last-modified"):
                    response.headers.setHeader("last-modified", self.lastModified())
        returnValue(response)

    @inlineCallbacks
    def handleMissingTrailingSlash(self, request):
        try:
            _ignore_authnUser, authzUser = yield self.authenticate(request)
        except Exception:
            authzUser = None

        # Turn 301 into 401
        if authzUser is None:
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            returnValue(response)
        else:
            response = RedirectResponse(
                request.unparseURL(
                    path=urllib.quote(
                        urllib.unquote(request.path),
                        safe=':/') + '/'
                )
            )
            returnValue(response)

    # Begin transitional new-store resource interface:

    def copyDeadPropertiesTo(self, other):
        """
        Copy this resource's dead properties to another resource.  This requires
        that the new resource have a back-end store.

        @param other: a resource to copy all properties to.
        @type other: subclass of L{CalDAVResource}
        """
        self.newStoreProperties().update(other.newStoreProperties())

    def newStoreProperties(self):
        """
        Return an L{IMapping} that represents properties.  Only available on
        new-storage objects.
        """
        raise NotImplementedError("%s does not implement newStoreProperties" %
                                  (self,))

    def storeRemove(self, *a, **kw):
        """
        Remove this resource from storage.
        """
        raise NotImplementedError("%s does not implement storeRemove" %
                                  (self,))

    def storeStream(self, stream, format):
        """
        Store the content of the stream in this resource, as it would via a PUT.

        @param stream: The stream containing the data to be stored.
        @type stream: L{IStream}

        @return: a L{Deferred} which fires with an HTTP response.
        @rtype: L{Deferred}
        """
        raise NotImplementedError("%s does not implement storeStream" % (self,))

    # End transitional new-store interface

    ##
    # WebDAV
    ##

    def liveProperties(self):
        baseProperties = (
            element.Owner.qname(),  # Private Events needs this but it is also OK to return empty
        )

        if self.isPseudoCalendarCollection():
            baseProperties += (
                caldavxml.SupportedCalendarComponentSet.qname(),
                caldavxml.SupportedCalendarData.qname(),
                customxml.GETCTag.qname(),
            )
            if config.MaxResourceSize:
                baseProperties += (
                    caldavxml.MaxResourceSize.qname(),
                )
            if config.MaxAllowedInstances:
                baseProperties += (
                    caldavxml.MaxInstances.qname(),
                )
            if config.MaxAttendeesPerInstance:
                baseProperties += (
                    caldavxml.MaxAttendeesPerInstance.qname(),
                )

        if self.isCalendarCollection():
            baseProperties += (
                element.ResourceID.qname(),

                # These are "live" properties in the sense of WebDAV, however "live" for twext actually means
                # ones that are also always present, but the default alarm properties are allowed to be absent
                # and are in fact stored in the property store.
                # caldavxml.DefaultAlarmVEventDateTime.qname(),
                # caldavxml.DefaultAlarmVEventDate.qname(),
                # caldavxml.DefaultAlarmVToDoDateTime.qname(),
                # caldavxml.DefaultAlarmVToDoDate.qname(),

                customxml.PubSubXMPPPushKeyProperty.qname(),

                caldavxml.ScheduleCalendarTransp.qname(),
            )

        if self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection():
            baseProperties += (
                element.ResourceID.qname(),
                carddavxml.SupportedAddressData.qname(),
                customxml.GETCTag.qname(),
                customxml.PubSubXMPPPushKeyProperty.qname(),
            )
            if config.MaxResourceSize:
                baseProperties += (
                    carddavxml.MaxResourceSize.qname(),
                )

        if self.isDirectoryBackedAddressBookCollection():
            baseProperties += (
                element.ResourceID.qname(),
                carddavxml.SupportedAddressData.qname(),
            )

        if self.isNotificationCollection():
            baseProperties += (
                customxml.GETCTag.qname(),
            )

        if hasattr(self, "scheduleTag") and self.scheduleTag:
            baseProperties += (
                caldavxml.ScheduleTag.qname(),
            )

        if config.EnableSyncReport and (element.Report(element.SyncCollection(),) in self.supportedReports()):
            baseProperties += (element.SyncToken.qname(),)

        if config.EnableAddMember and (self.isCalendarCollection() or self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection()):
            baseProperties += (element.AddMember.qname(),)

        if config.Sharing.Enabled:
            if config.Sharing.Calendars.Enabled and self.isCalendarCollection():
                baseProperties += (
                    customxml.AllowedSharingModes.qname(),
                    customxml.SharedURL.qname(),
                )
                if self.isSharedByOwner() or self.isShareeResource():
                    baseProperties += (
                        customxml.Invite.qname(),
                    )

            elif (
                config.Sharing.AddressBooks.Enabled and self.isAddressBookCollection() or
                config.Sharing.AddressBooks.Groups.Enabled and self.isGroup()
            ) and not self.isDirectoryBackedAddressBookCollection():
                baseProperties += (
                    customxml.Invite.qname(),
                    customxml.AllowedSharingModes.qname(),
                )

        return super(CalDAVResource, self).liveProperties() + baseProperties

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

        # Push notification DAV property "pushkey"
        if qname == customxml.PubSubXMPPPushKeyProperty.qname() and (
            self.isCalendarCollection() or (self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection())
        ):

            if hasattr(self, "_newStoreObject"):
                notifier = self._newStoreObject.getNotifier("push")
                if notifier is not None:
                    propVal = customxml.PubSubXMPPPushKeyProperty(notifier.nodeName())
                    returnValue(propVal)

            returnValue(customxml.PubSubXMPPPushKeyProperty())

        elif qname == element.Owner.qname():
            owner = (yield self.owner(request))
            returnValue(element.Owner(owner))

        elif qname == element.ResourceType.qname():
            returnValue(self.resourceType())

        elif qname == element.ResourceID.qname():
            returnValue(element.ResourceID(element.HRef.fromString(self.resourceID())))

        elif qname == customxml.GETCTag.qname() and (
            self.isPseudoCalendarCollection() or
            self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection() or
            self.isNotificationCollection()
        ):
            returnValue(customxml.GETCTag.fromString((yield self.getInternalSyncToken())))

        elif qname == element.SyncToken.qname() and config.EnableSyncReport and (
            element.Report(element.SyncCollection(),) in self.supportedReports()
        ):
            returnValue(element.SyncToken.fromString((yield self.getSyncToken())))

        elif qname == element.AddMember.qname() and config.EnableAddMember and (
            self.isCalendarCollection() or self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection()
        ):
            url = (yield self.canonicalURL(request))
            returnValue(element.AddMember(element.HRef.fromString(url + "/;add-member")))

        elif qname == caldavxml.SupportedCalendarComponentSet.qname() and self.isPseudoCalendarCollection():
            returnValue(self.getSupportedComponentSet())

        elif qname == caldavxml.SupportedCalendarData.qname() and self.isPseudoCalendarCollection():
            dataTypes = []
            dataTypes.append(
                caldavxml.CalendarData(**{
                    "content-type": "text/calendar",
                    "version": "2.0",
                }),
            )
            if config.EnableJSONData:
                dataTypes.append(
                    caldavxml.CalendarData(**{
                        "content-type": "application/calendar+json",
                        "version": "2.0",
                    }),
                )
            returnValue(caldavxml.SupportedCalendarData(*dataTypes))

        elif qname == caldavxml.MaxResourceSize.qname() and self.isPseudoCalendarCollection():
            if config.MaxResourceSize:
                returnValue(caldavxml.MaxResourceSize.fromString(
                    str(config.MaxResourceSize)
                ))

        elif qname == caldavxml.MaxInstances.qname() and self.isPseudoCalendarCollection():
            if config.MaxAllowedInstances:
                returnValue(caldavxml.MaxInstances.fromString(
                    str(config.MaxAllowedInstances)
                ))

        elif qname == caldavxml.MaxAttendeesPerInstance.qname() and self.isPseudoCalendarCollection():
            if config.MaxAttendeesPerInstance:
                returnValue(caldavxml.MaxAttendeesPerInstance.fromString(
                    str(config.MaxAttendeesPerInstance)
                ))

        elif qname == caldavxml.ScheduleTag.qname():
            # CalDAV-scheduling
            if hasattr(self, "scheduleTag") and self.scheduleTag:
                returnValue(caldavxml.ScheduleTag.fromString(
                    self.scheduleTag
                ))

        elif qname == caldavxml.ScheduleCalendarTransp.qname() and self.isCalendarCollection():
            returnValue(caldavxml.ScheduleCalendarTransp(caldavxml.Opaque() if self._newStoreObject.isUsedForFreeBusy() else caldavxml.Transparent()))

        elif qname == carddavxml.SupportedAddressData.qname() and self.isAddressBookCollection():
            # CardDAV, section 6.2.2
            dataTypes = []
            dataTypes.append(
                carddavxml.AddressDataType(**{
                    "content-type": "text/vcard",
                    "version": "3.0",
                }),
            )
            if config.EnableJSONData:
                dataTypes.append(
                    carddavxml.AddressDataType(**{
                        "content-type": "application/vcard+json",
                        "version": "3.0",
                    }),
                )
            returnValue(carddavxml.SupportedAddressData(*dataTypes))

        elif qname == carddavxml.MaxResourceSize.qname() and self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection():
            # CardDAV, section 6.2.3
            if config.MaxResourceSize:
                returnValue(carddavxml.MaxResourceSize.fromString(
                    str(config.MaxResourceSize)
                ))

        elif qname == customxml.Invite.qname():
            if config.Sharing.Enabled and (
                config.Sharing.Calendars.Enabled and self.isCalendarCollection() or (
                    config.Sharing.AddressBooks.Enabled and self.isAddressBookCollection() or
                    config.Sharing.AddressBooks.Groups.Enabled and self.isGroup()
                ) and not self.isDirectoryBackedAddressBookCollection()
            ):
                result = (yield self.inviteProperty(request))
                returnValue(result)

        elif qname == customxml.AllowedSharingModes.qname():
            if config.Sharing.Enabled:
                if config.Sharing.Calendars.Enabled and self.isCalendarCollection():
                    returnValue(customxml.AllowedSharingModes(customxml.CanBeShared()))
                elif (
                    config.Sharing.AddressBooks.Enabled and self.isAddressBookCollection() or
                    config.Sharing.AddressBooks.Groups.Enabled and self.isGroup()
                ) and not self.isDirectoryBackedAddressBookCollection():
                    returnValue(customxml.AllowedSharingModes(customxml.CanBeShared()))

        elif qname == customxml.SharedURL.qname():
            if self.isShareeResource():
                returnValue(customxml.SharedURL(element.HRef.fromString(self._share_url)))
            else:
                returnValue(None)

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

    @inlineCallbacks
    def writeProperty(self, property, request):
        assert isinstance(property, element.WebDAVElement), (
            "%r is not a WebDAVElement instance" % (property,)
        )

        if property.qname() == caldavxml.SupportedCalendarComponentSet.qname():
            if not self.isPseudoCalendarCollection():
                raise HTTPError(StatusResponse(
                    responsecode.FORBIDDEN,
                    "Property %s may only be set on calendar collection." % (property,)
                ))
            for component in property.children:
                if component not in self.supportedCalendarComponentSet:
                    raise HTTPError(StatusResponse(
                        responsecode.NOT_IMPLEMENTED,
                        "Component %s is not supported by this server" % (component.toxml(),)
                    ))

        # Strictly speaking CalDAV:calendar-timezone and CalDAV:calendar-timezone-id are live properties
        # in the sense that the server enforces what can be stored, however they need not actually
        # exist so we cannot list them in liveProperties on this resource, since their presence there
        # means that hasProperty will always return True for it.
        elif property.qname() in (caldavxml.CalendarTimeZone.qname(), caldavxml.CalendarTimeZoneID.qname(),):
            if not self.isCalendarCollection():
                raise HTTPError(StatusResponse(
                    responsecode.FORBIDDEN,
                    "Property %s may only be set on calendar collection." % (property,)
                ))

        # Validate default alarm properties (do this even if the default alarm feature is off)
        elif property.qname() in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            if not self.isCalendarCollection() and not isinstance(self, CalendarHomeResource):
                raise HTTPError(StatusResponse(
                    responsecode.FORBIDDEN,
                    "Property %s may only be set on calendar or home collection." % (property,)
                ))

        elif property.qname() == caldavxml.ScheduleCalendarTransp.qname():
            if not self.isCalendarCollection():
                raise HTTPError(StatusResponse(
                    responsecode.FORBIDDEN,
                    "Property %s may only be set on calendar collection." % (property,)
                ))

        result = (yield super(CalDAVResource, self).writeProperty(property, request))
        returnValue(result)

    ##
    # ACL
    ##

    def _get_accessMode(self):
        """
        Needed as a stub because only calendar object resources use this but we need to do ACL
        determination on the generic CalDAVResource for now.
        """
        return ""

    def _set_accessMode(self, value):
        raise NotImplementedError

    accessMode = property(_get_accessMode, _set_accessMode)

    # FIXME: Perhaps this is better done in authorize() instead.
    @inlineCallbacks
    def accessControlList(self, request, *args, **kwargs):

        acls = None
        if self.isShareeResource():
            acls = (yield self.shareeAccessControlList(request, *args, **kwargs))

        if acls is None:
            acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))

        # Look for private events access classification
        if self.accessMode:
            if self.accessMode in (Component.ACCESS_PRIVATE, Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED,):
                # Need to insert ACE to prevent non-owner principals from seeing this resource
                owner = (yield self.owner(request))
                newacls = []
                if self.accessMode == Component.ACCESS_PRIVATE:
                    newacls.extend(config.AdminACEs)
                    newacls.extend(config.ReadACEs)
                    newacls.append(element.ACE(
                        element.Invert(
                            element.Principal(owner),
                        ),
                        element.Deny(
                            element.Privilege(
                                element.Read(),
                            ),
                            element.Privilege(
                                element.Write(),
                            ),
                        ),
                        element.Protected(),
                    ))
                else:
                    newacls.extend(config.AdminACEs)
                    newacls.extend(config.ReadACEs)
                    newacls.append(element.ACE(
                        element.Invert(
                            element.Principal(owner),
                        ),
                        element.Deny(
                            element.Privilege(
                                element.Write(),
                            ),
                        ),
                        element.Protected(),
                    ))
                newacls.extend(acls.children)

                acls = element.ACL(*newacls)

        returnValue(acls)

    @inlineCallbacks
    def owner(self, request):
        """
        Return the DAV:owner property value (MUST be a DAV:href or None).
        """

        if getattr(self, "_newStoreObject", None) is not None:
            if not hasattr(self._newStoreObject, "ownerHome"):
                home = self._newStoreObject.parentCollection().ownerHome()
            else:
                home = self._newStoreObject.ownerHome()
            principal = (yield self.principalForUID(home.uid()))
            returnValue(element.HRef(principal.principalURL()) if principal else None)
        else:
            parent = (yield self.locateParent(request, request.urlForResource(self)))
        if parent and isinstance(parent, CalDAVResource):
            result = (yield parent.owner(request))
            returnValue(result)
        else:
            returnValue(None)

    @inlineCallbacks
    def ownerPrincipal(self, request):
        """
        Return the principal resource for the owner of this resource.
        """
        if hasattr(self, "_newStoreObject"):
            if not hasattr(self._newStoreObject, "ownerHome"):
                home = self._newStoreObject.parentCollection().ownerHome()
            else:
                home = self._newStoreObject.ownerHome()
            returnValue((yield self.principalForUID(home.uid())))
        else:
            parent = (yield self.locateParent(request, request.urlForResource(self)))
        if parent and isinstance(parent, CalDAVResource):
            result = (yield parent.ownerPrincipal(request))
            returnValue(result)
        else:
            returnValue(None)

    @inlineCallbacks
    def resourceOwnerPrincipal(self, request):
        """
        This is the principal resource of the owner of the resource based on the URI used to access it. For a shared
        collection it will be the sharee, otherwise it will be the regular the ownerPrincipal.
        """
        parent = (yield self.locateParent(
            request, request.urlForResource(self)
        ))
        if parent and isinstance(parent, CalDAVResource):
            result = (yield parent.resourceOwnerPrincipal(request))
            returnValue(result)
        else:
            returnValue(None)

    @inlineCallbacks
    def isOwner(self, request):
        """
        Determine whether the DAV:owner of this resource matches the currently
        authorized principal in the request, or if the user is a read-only or
        read-write administrator.
        """
        current = self.currentPrincipal(request)
        if current in config.AllAdminPrincipalObjects:
            returnValue(True)
        if element.Principal((yield self.owner(request))) == current:
            returnValue(True)
        returnValue(False)

    ##
    # DAVResource
    ##

    def displayName(self):
        if 'record' in dir(self):
            if self.record.fullNames:
                return self.record.fullNames[0]
            elif self.record.shortNames:
                return self.record.shortNames[0]

        return super(DAVResource, self).displayName()

    def name(self):
        return None

    def resourceID(self):
        return None

    ##
    # CalDAV
    ##

    def isCalendarCollection(self):
        """
        See L{ICalDAVResource.isCalendarCollection}.
        """
        return self.isSpecialCollection(caldavxml.Calendar)

    def isAddressBookCollection(self):
        """
        See L{ICalDAVResource.isAddressBookCollection}.
        """
        return self.isSpecialCollection(carddavxml.AddressBook)

    def isNotificationCollection(self):
        """
        See L{ICalDAVResource.isNotificationCollection}.
        """
        return self.isSpecialCollection(customxml.Notification)

    def isDirectoryBackedAddressBookCollection(self):       # ATM - temporary fix? (this one worked)
        return False

    def isSpecialCollection(self, collectiontype):
        """
        See L{ICalDAVResource.isSpecialCollection}.
        """
        if not self.isCollection():
            return False

        try:
            resourcetype = self.resourceType()
        except HTTPError, e:
            assert e.response.code == responsecode.NOT_FOUND, (
                "Unexpected response code: %s" % (e.response.code,)
            )
            return False
        return bool(resourcetype.childrenOfType(collectiontype))

    def isPseudoCalendarCollection(self):
        """
        See L{ICalDAVResource.isPseudoCalendarCollection}.
        """
        return self.isCalendarCollection()

    def findCalendarCollections(self, depth, request, callback, privileges=None):
        return self.findSpecialCollections(caldavxml.Calendar, depth, request, callback, privileges)

    def findAddressBookCollections(self, depth, request, callback, privileges=None):
        return self.findSpecialCollections(carddavxml.AddressBook, depth, request, callback, privileges)

    @inlineCallbacks
    def findSpecialCollectionsFaster(self, type, depth, request, callback, privileges=None):
        assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)

        if depth != "0" and self.isCollection():
            basepath = request.urlForResource(self)
            for childname in (yield self.listChildren()):
                childpath = joinURL(basepath, childname)
                child = (yield request.locateResource(childpath))
                if child:
                    if privileges:
                        try:
                            yield child.checkPrivileges(request, privileges)
                        except AccessDeniedError:
                            continue
                    if child.isSpecialCollection(type):
                        callback(child, childpath)

                    # No more regular collections. If we leave this in then dropbox is scanned at depth:infinity
                    # and that is very painful as it requires scanning all calendar resources too. Eventually we need
                    # to fix drop box and probably re-enable this for the generic case.
    #                elif child.isCollection():
    #                    if depth == "infinity":
    #                        yield child.findSpecialCollectionsFaster(type, depth, request, callback, privileges)

    findSpecialCollections = findSpecialCollectionsFaster

    def isDefaultCalendar(self, request):

        assert self.isCalendarCollection()

        return self._newStoreParentHome.isDefaultCalendar(self._newStoreObject)

    def iCalendarAddressDoNormalization(self, ical):
        """
        Normalize calendar user addresses in the supplied iCalendar object into their
        urn:uuid form where possible. Also reset CN= property and add EMAIL property.

        @param ical: calendar object to normalize.
        @type ical: L{Component}
        @return: L{Deferred}
        """
        return ical.normalizeCalendarUserAddresses(
            normalizationLookup,
            self.record.service.recordWithCalendarUserAddress
        )

    @inlineCallbacks
    def principalForCalendarUserAddress(self, address):
        for principalCollection in self.principalCollections():
            principal = (yield principalCollection.principalForCalendarUserAddress(address))
            if principal is not None:
                returnValue(principal)
        returnValue(None)

    @inlineCallbacks
    def principalForUID(self, principalUID):
        for principalCollection in self.principalCollections():
            principal = (yield principalCollection.principalForUID(principalUID))
            if principal is not None:
                returnValue(principal)
        returnValue(None)

    @inlineCallbacks
    def movedAddressBook(self, request, defaultAddressBook, destination, destination_uri):
        """
        AddressBook has been moved. Need to do some extra clean-up.
        """

        # Adjust the default addressbook setting if necessary
        if defaultAddressBook:
            principal = (yield self.resourceOwnerPrincipal(request))
            home = (yield principal.addressBookHome(request))
            (_ignore_scheme, _ignore_host, destination_path, _ignore_query, _ignore_fragment) = urlsplit(normalizeURL(destination_uri))
            yield home.writeProperty(carddavxml.DefaultAddressBookURL(element.HRef(destination_path)), request)

    @inlineCallbacks
    def isDefaultAddressBook(self, request):

        assert self.isAddressBookCollection()

        # Not allowed to delete the default address book
        principal = (yield self.resourceOwnerPrincipal(request))
        home = (yield principal.addressBookHome(request))
        default = (yield home.readProperty(carddavxml.DefaultAddressBookURL.qname(), request))
        if default and len(default.children) == 1:
            defaultURL = normalizeURL(str(default.children[0]))
            myURL = (yield self.canonicalURL(request))
            returnValue(defaultURL == myURL)

        returnValue(False)

    @inlineCallbacks
    def vCard(self):
        """
        See L{ICalDAVResource.vCard}.

        This implementation returns the an object created from the data returned
        by L{vCardText} when given the same arguments.

        Note that L{vCardText} by default calls this method, which creates
        an infinite loop.  A subclass must override one of both of these
        methods.
        """
        try:
            vcard_data = yield self.vCardText()
        except InternalDataStoreError:
            returnValue(None)

        if vcard_data is None:
            returnValue(None)

        try:
            returnValue(vComponent.fromString(vcard_data))
        except ValueError:
            returnValue(None)

    def supportedReports(self):
        result = super(CalDAVResource, self).supportedReports()
        if config.EnableCalDAV:
            result.append(element.Report(caldavxml.CalendarQuery(),))
            result.append(element.Report(caldavxml.CalendarMultiGet(),))
        if self.isCollection():
            # Only allowed on collections
            result.append(element.Report(caldavxml.FreeBusyQuery(),))
        if config.EnableCardDAV:
            result.append(element.Report(carddavxml.AddressBookQuery(),))
            result.append(element.Report(carddavxml.AddressBookMultiGet(),))
        if (
            self.isPseudoCalendarCollection() or
            self.isAddressBookCollection() or
            self.isNotificationCollection()
        ) and config.EnableSyncReport:
            # Only allowed on calendar/inbox/addressbook/notification collections
            result.append(element.Report(element.SyncCollection(),))
        return result

    def writeNewACEs(self, newaces):
        """
        Write a new ACL to the resource's property store. We override this for calendar collections
        and force all the ACEs to be inheritable so that all calendar object resources within the
        calendar collection have the same privileges unless explicitly overridden. The same applies
        to drop box collections as we want all resources (attachments) to have the same privileges as
        the drop box collection.

        @param newaces: C{list} of L{ACE} for ACL being set.
        """

        # Do this only for regular calendar collections and Inbox/Outbox
        if self.isPseudoCalendarCollection() or self.isAddressBookCollection():
            edited_aces = []
            for ace in newaces:
                if TwistedACLInheritable() not in ace.children:
                    children = list(ace.children)
                    children.append(TwistedACLInheritable())
                    edited_aces.append(element.ACE(*children))
                else:
                    edited_aces.append(ace)
        else:
            edited_aces = newaces

        # Do inherited with possibly modified set of aces
        super(CalDAVResource, self).writeNewACEs(edited_aces)

    ##
    # Utilities
    ##

    def locateParent(self, request, uri):
        """
        Locates the parent resource of the resource with the given URI.
        @param request: an L{IRequest} object for the request being processed.
        @param uri: the URI whose parent resource is desired.
        """
        return request.locateResource(parentForURL(uri))

    @inlineCallbacks
    def canonicalURL(self, request):

        if not hasattr(self, "_canonical_url"):

            myurl = request.urlForResource(self)
            _ignore_scheme, _ignore_host, path, _ignore_query, _ignore_fragment = urlsplit(normalizeURL(myurl))
            lastpath = path.split("/")[-1]

            parent = (yield request.locateResource(parentForURL(myurl)))
            if parent and isinstance(parent, CalDAVResource):
                canonical_parent = (yield parent.canonicalURL(request))
                self._canonical_url = joinURL(canonical_parent, lastpath)
            else:
                self._canonical_url = myurl

        returnValue(self._canonical_url)

    ##
    # Quota
    ##

    def hasQuotaRoot(self, request):
        """
        Quota root only ever set on calendar homes.
        """
        return False

    def quotaRoot(self, request):
        """
        Quota root only ever set on calendar homes.
        """
        return None

    @inlineCallbacks
    def quotaRootResource(self, request):
        """
        Return the quota root for this resource.

        @return: L{DAVResource} or C{None}
        """

        sharedParent = None
        if self.isShareeResource():
            # A sharee collection's quota root is the resource owner's root
            if self._share_url:
                sharedParent = (yield request.locateResource(parentForURL(self._share_url)))
        else:
            parent = (yield self.locateParent(request, request.urlForResource(self)))
            if isCalendarCollectionResource(parent) or isAddressBookCollectionResource(parent):
                if parent.isShareeResource():
                    # A sharee collection's quota root is the resource owner's root
                    sharedParent = (yield request.locateResource(parentForURL(parent._share_url)))

        if sharedParent:
            result = (yield sharedParent.quotaRootResource(request))
        else:
            result = (yield super(CalDAVResource, self).quotaRootResource(request))

        returnValue(result)

    # Collection sync stuff

    @inlineCallbacks
    def whatchanged(self, client_token, depth):

        client_data_token = None
        client_config_token = None

        if client_token:
            if "/" in client_token:
                client_data_token, client_config_token = client_token.split("/")
            else:
                client_data_token = client_token

        current_token = (yield self.getSyncToken())
        if "/" in current_token:
            current_token = current_token.split("/")[0]
        current_uuid, current_revision = current_token[6:].split("_", 1)
        current_revision = int(current_revision)

        if client_data_token:
            try:
                if not client_data_token.startswith("data:,"):
                    raise ValueError
                caluuid, revision = client_data_token[6:].split("_", 1)
                revision = int(revision)

                # Check client token validity
                if caluuid != current_uuid:
                    raise ValueError
                if revision > current_revision:
                    raise ValueError
            except ValueError:
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    (dav_namespace, "valid-sync-token"),
                    "Sync token is invalid",
                ))
        else:
            revision = 0

        try:
            changed, removed, notallowed = yield self._indexWhatChanged(revision, depth)
        except SyncTokenValidException:
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (dav_namespace, "valid-sync-token"),
                "Sync token not recognized",
            ))

        if config.EnableConfigSyncToken:
            # Append the app-level portion of sync token (e.g. derived from config)
            newConfigToken = config.syncToken()
            current_token = "{}/{}".format(current_token, newConfigToken)

            # If the config token changed, note that in the returned tuple
            resourceChanged = (newConfigToken != client_config_token)
        else:
            resourceChanged = False

        returnValue((changed, removed, notallowed, current_token, resourceChanged))

    def _indexWhatChanged(self, revision, depth):
        # Now handled directly by newstore
        raise NotImplementedError

    @inlineCallbacks
    def getSyncToken(self):
        """
        Return current sync-token value.
        """

        internal_token = (yield self.getInternalSyncToken())
        internal_token = "data:,%s" % (internal_token,)
        token = config.joinToken(internal_token)
        returnValue(token)

    def getInternalSyncToken(self):
        """
        Return current internal sync-token value.
        """
        raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Property not supported"))

    #
    # Stuff from CalDAVFile
    #

    @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() and hasattr(self, "scheduleEtags"):
                etags = self.scheduleEtags
                if len(etags) > 1:
                    # This is almost verbatim from txweb2.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(CalDAVResource, self).checkPreconditions(request))
        returnValue(result)

    @inlineCallbacks
    def createCalendar(self, request):
        """
        External API for creating a calendar.  Verify that the parent is a
        collection, exists, is I{not} a calendar collection; that this resource
        does not yet exist, then create it.

        @param request: the request used to look up parent resources to
            validate.

        @type request: L{txweb2.iweb.IRequest}

        @return: a deferred that fires when a calendar collection has been
            created in this resource.
        """
        if self.exists():
            self.log.error("Attempt to create collection where file exists: {s!r}", s=self)
            raise HTTPError(StatusResponse(responsecode.NOT_ALLOWED, "File exists"))

        # newStore guarantees that we always have a parent calendar home
        # if not self.fp.parent().isdir():
        #     log.error("Attempt to create collection with no parent: {p}", p=self.fp.path)
        #     raise HTTPError(StatusResponse(responsecode.CONFLICT, "No parent collection"))

        #
        # Verify that no parent collection is a calendar also
        #

        parent = (yield self._checkParents(request, isPseudoCalendarCollectionResource))

        if parent is not None:
            self.log.error("Cannot create a calendar collection within a calendar collection {p!r}", p=parent)
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (caldavxml.caldav_namespace, "calendar-collection-location-ok"),
                "Cannot create a calendar collection inside another calendar collection",
            ))

        # Check for any quota limits
        if config.MaxCollectionsPerHome:
            parent = (yield self.locateParent(request, request.urlForResource(self)))
            if (yield parent.countOwnedChildren()) >= config.MaxCollectionsPerHome:  # NB this ignores shares
                self.log.error("Cannot create a calendar collection because there are too many already present in {p!r}", p=parent)
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    customxml.MaxCollections(),
                    "Too many calendar collections",
                ))

        returnValue((yield self.createCalendarCollection()))

    def createCalendarCollection(self):
        """
        Internal API for creating a calendar collection.

        @return: a L{Deferred} which fires when the underlying collection has
            actually been created.
        """
        return fail(NotImplementedError())

    def iCalendarRolledup(self, request):
        """
        Only implemented by calendar collections; see storebridge.
        """
        raise HTTPError(responsecode.NOT_ALLOWED)

    @inlineCallbacks
    def iCalendarFiltered(self, isowner):

        # Now "filter" the resource calendar data
        caldata = (yield self.componentForUser())
        caldata = HiddenInstanceFilter().filter(caldata)
        caldata = PrivateEventFilter(self.accessMode, isowner).filter(caldata)
        returnValue(caldata)

    def component(self):
        # storebridge handles this method
        raise NotImplementedError()

    @inlineCallbacks
    def createAddressBook(self, request):
        """
        External API for creating an addressbook.  Verify that the parent is a
        collection, exists, is I{not} an addressbook collection; that this resource
        does not yet exist, then create it.

        @param request: the request used to look up parent resources to
            validate.

        @type request: L{txweb2.iweb.IRequest}

        @return: a deferred that fires when an addressbook collection has been
            created in this resource.
        """
        #
        # request object is required because we need to validate against parent
        # resources, and we need the request in order to locate the parents.
        #

        if self.exists():
            self.log.error("Attempt to create collection where file exists: {s!r}", s=self)
            raise HTTPError(StatusResponse(responsecode.NOT_ALLOWED, "File exists"))

        # newStore guarantees that we always have a parent calendar home
        # if not os.path.isdir(os.path.dirname(self.fp.path)):
        #     log.error("Attempt to create collection with no parent: {p}", p=self.fp.path)
        #     raise HTTPError(StatusResponse(responsecode.CONFLICT, "No parent collection"))

        #
        # Verify that no parent collection is a calendar also
        #

        parent = (yield self._checkParents(request, isAddressBookCollectionResource))
        if parent is not None:
            self.log.error("Cannot create an address book collection within an address book collection {p!r}", p=parent)
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (carddavxml.carddav_namespace, "addressbook-collection-location-ok"),
                "Cannot create an address book collection inside of an address book collection",
            ))

        # Check for any quota limits
        if config.MaxCollectionsPerHome:
            parent = (yield self.locateParent(request, request.urlForResource(self)))
            if (yield parent.countOwnedChildren()) >= config.MaxCollectionsPerHome:  # NB this ignores shares
                self.log.error("Cannot create a calendar collection because there are too many already present in {p!r}", p=parent)
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    customxml.MaxCollections(),
                    "Too many address book collections",
                ))

        returnValue((yield self.createAddressBookCollection()))

    def createAddressBookCollection(self):
        """
        Internal API for creating an addressbook collection.

        @return: a L{Deferred} which fires when the underlying collection has
            actually been created.
        """
        return fail(NotImplementedError())

    @inlineCallbacks
    def vCardRolledup(self, request):
        # TODO: just catenate all the vCards together
        yield fail(HTTPError((ErrorResponse(responsecode.BAD_REQUEST))))

    @inlineCallbacks
    def vCardText(self, name=None):
        if self.isAddressBookCollection():
            if name is None:
                returnValue(str((yield self.vCard())))
            vcard_resource = yield self.getChild(name)
            returnValue((yield vcard_resource.vCardText()))
        elif self.isCollection():
            returnValue(None)
        else:
            if name is not None:
                raise AssertionError("name must be None for non-collection vcard resource")
        # FIXME: StoreBridge handles this case
        raise NotImplementedError

    def supportedPrivileges(self, request):
        # read-free-busy support on calendar collection and calendar object resources
        if self.isCollection():
            return succeed(calendarPrivilegeSet)
        else:
            def gotParent(parent):
                if parent and isCalendarCollectionResource(parent):
                    return succeed(calendarPrivilegeSet)
                else:
                    return super(CalDAVResource, self).supportedPrivileges(request)

            d = self.locateParent(request, request.urlForResource(self))
            d.addCallback(gotParent)
            return d

        return super(CalDAVResource, self).supportedPrivileges(request)

    ##
    # Quota
    ##

    def quotaSize(self, request):
        """
        Get the size of this resource.
        TODO: Take into account size of dead-properties. Does stat include xattrs size?

        @return: an L{Deferred} with a C{int} result containing the size of the resource.
        """
#        if self.isCollection():
#            @inlineCallbacks
#            def walktree(top):
#                """
#                Recursively descend the directory tree rooted at top,
#                calling the callback function for each regular file
#
#                @param top: L{FilePath} for the directory to walk.
#                """
#
#                total = 0
#                for f in top.listdir():
#
#                    # Ignore the database
#                    if f.startswith("."):
#                        continue
#
#                    child = top.child(f)
#                    if child.isdir():
#                        # It's a directory, recurse into it
#                        total += yield walktree(child)
#                    elif child.isfile():
#                        # It's a file, call the callback function
#                        total += child.getsize()
#                    else:
#                        # Unknown file type, print a message
#                        pass
#
#                returnValue(total)
#
#            return walktree(self.fp)
#        else:
#            return succeed(self.fp.getsize())
        return succeed(0)

    ##
    # Utilities
    ##

    @staticmethod
    def _isChildURI(request, uri, immediateChild=True):
        """
        Verify that the supplied URI represents a resource that is a child
        of the request resource.
        @param request: the request currently in progress
        @param uri: the URI to test
        @return: True if the supplied URI is a child resource
                 False if not
        """
        if uri is None:
            return False

        #
        # Parse the URI
        #

        (scheme, host, path, query, fragment) = urlsplit(uri)  # @UnusedVariable

        # Request hostname and child uri hostname have to be the same.
        if host and host != request.headers.getHeader("host"):
            return False

        # Child URI must start with request uri text.
        parent = request.uri
        if not parent.endswith("/"):
            parent += "/"

        return path.startswith(parent) and (len(path) > len(parent)) and (not immediateChild or (path.find("/", len(parent)) == -1))

    @inlineCallbacks
    def _checkParents(self, request, test):
        """
        @param request: the request being processed.
        @param test: a callable
        @return: the closest parent for this resource using the request URI from
            the given request for which C{test(parent)} evaluates to a true
            value, or C{None} if no parent matches.
        """
        parent = self
        parent_uri = request.uri

        while True:
            parent_uri = parentForURL(parent_uri)
            if not parent_uri:
                break

            parent = yield request.locateResource(parent_uri)

            if test(parent):
                returnValue(parent)


class CalendarPrincipalCollectionResource (DAVPrincipalCollectionResource, CalDAVResource):
    """
    CalDAV principal collection.
    """
    implements(IDAVPrincipalCollectionResource)

    def isCollection(self):
        return True

    def isCalendarCollection(self):
        return False

    def isAddressBookCollection(self):
        return False

    def isDirectoryBackedAddressBookCollection(self):
        return False

    def principalForCalendarUserAddress(self, address):
        return None

    def supportedReports(self):
        """
        Principal collections are the only resources supporting the
        principal-search-property-set report.
        """
        result = super(CalendarPrincipalCollectionResource, self).supportedReports()
        result.append(element.Report(element.PrincipalSearchPropertySet(),))
        return result

    def principalSearchPropertySet(self):
        return element.PrincipalSearchPropertySet(
            element.PrincipalSearchProperty(
                element.PropertyContainer(
                    element.DisplayName()
                ),
                element.Description(
                    element.PCDATAElement("Display Name"),
                    **{"xml:lang": "en"}
                ),
            ),
            element.PrincipalSearchProperty(
                element.PropertyContainer(
                    caldavxml.CalendarUserAddressSet()
                ),
                element.Description(
                    element.PCDATAElement("Calendar User Addresses"),
                    **{"xml:lang": "en"}
                ),
            ),
        )


class CalendarPrincipalResource (CalDAVComplianceMixIn, DAVResourceWithChildrenMixin, DAVPrincipalResource):
    """
    CalDAV principal resource.

    Extends L{DAVPrincipalResource} to provide CalDAV functionality.
    """
    implements(ICalendarPrincipalResource)

    def liveProperties(self):

        baseProperties = ()

        if self.calendarsEnabled():
            baseProperties += (
                (caldav_namespace, "calendar-home-set"),
                (caldav_namespace, "calendar-user-address-set"),
                (caldav_namespace, "schedule-inbox-URL"),
                (caldav_namespace, "schedule-outbox-URL"),
                (caldav_namespace, "calendar-user-type"),
                (calendarserver_namespace, "calendar-proxy-read-for"),
                (calendarserver_namespace, "calendar-proxy-write-for"),
                (calendarserver_namespace, "auto-schedule-mode"),
            )

        if self.addressBooksEnabled():
            baseProperties += (carddavxml.AddressBookHomeSet.qname(),)
            if self.directoryAddressBookEnabled():
                baseProperties += (carddavxml.DirectoryGateway.qname(),)

        if config.EnableDropBox or config.EnableManagedAttachments:
            baseProperties += (customxml.DropBoxHomeURL.qname(),)

        if config.Sharing.Enabled:
            baseProperties += (customxml.NotificationURL.qname(),)

        return super(CalendarPrincipalResource, self).liveProperties() + baseProperties

    def isCollection(self):
        return True

    @inlineCallbacks
    def handleMissingTrailingSlash(self, request):
        try:
            _ignore_authnUser, authzUser = yield self.authenticate(request)
        except Exception:
            authzUser = None

        # Turn 301 into 401
        if authzUser is None:
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            returnValue(response)
        else:
            response = RedirectResponse(
                request.unparseURL(
                    path=urllib.quote(
                        urllib.unquote(request.path),
                        safe=':/') + '/'
                )
            )
            returnValue(response)

    def calendarsEnabled(self):
        return config.EnableCalDAV

    def addressBooksEnabled(self):
        return config.EnableCardDAV

    def directoryAddressBookEnabled(self):
        return config.DirectoryAddressBook.Enabled and config.EnableSearchAddressBook

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

        namespace, name = qname

        if namespace == caldav_namespace and self.calendarsEnabled():
            if name == "calendar-home-set":
                returnValue(caldavxml.CalendarHomeSet(
                    *[element.HRef(url) for url in self.calendarHomeURLs()]
                ))

            elif name == "calendar-user-address-set":
                returnValue(caldavxml.CalendarUserAddressSet(
                    *[element.HRef(uri) for uri in sorted(self.calendarUserAddresses())]
                ))

            elif name == "schedule-inbox-URL":
                url = self.scheduleInboxURL()
                if url is None:
                    returnValue(None)
                else:
                    returnValue(caldavxml.ScheduleInboxURL(element.HRef(url)))

            elif name == "schedule-outbox-URL":
                url = self.scheduleOutboxURL()
                if url is None:
                    returnValue(None)
                else:
                    returnValue(caldavxml.ScheduleOutboxURL(element.HRef(url)))

            elif name == "calendar-user-type":
                returnValue(caldavxml.CalendarUserType(self.record.getCUType()))

        elif namespace == calendarserver_namespace:
            if name == "dropbox-home-URL" and (config.EnableDropBox or config.EnableManagedAttachments):
                url = self.dropboxURL()
                if url is None:
                    returnValue(None)
                else:
                    returnValue(customxml.DropBoxHomeURL(element.HRef(url)))

            elif name == "notification-URL" and config.Sharing.Enabled:
                url = yield self.notificationURL()
                if url is None:
                    returnValue(None)
                else:
                    returnValue(customxml.NotificationURL(element.HRef(url)))

            elif name == "calendar-proxy-read-for" and self.calendarsEnabled():
                results = (yield self.proxyFor(False))
                returnValue(customxml.CalendarProxyReadFor(
                    *[element.HRef(principal.principalURL()) for principal in results]
                ))

            elif name == "calendar-proxy-write-for" and self.calendarsEnabled():
                results = (yield self.proxyFor(True))
                returnValue(customxml.CalendarProxyWriteFor(
                    *[element.HRef(principal.principalURL()) for principal in results]
                ))

            elif name == "auto-schedule-mode" and self.calendarsEnabled():
                autoScheduleMode = yield self.getAutoScheduleMode()
                returnValue(customxml.AutoScheduleMode(autoScheduleMode.description if autoScheduleMode else "default"))

        elif namespace == carddav_namespace and self.addressBooksEnabled():
            if name == "addressbook-home-set":
                returnValue(carddavxml.AddressBookHomeSet(
                    *[element.HRef(abhome_url) for abhome_url in self.addressBookHomeURLs()]
                ))
            elif name == "directory-gateway" and self.directoryAddressBookEnabled():
                returnValue(carddavxml.DirectoryGateway(
                    element.HRef.fromString(joinURL("/", config.DirectoryAddressBook.name, "/"))
                ))

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

    def calendarFreeBusyURIs(self, request):
        def gotInbox(inbox):
            if inbox is None:
                return ()

            def getFreeBusy(has):
                if not has:
                    return ()

                def parseFreeBusy(freeBusySet):
                    return tuple(str(href) for href in freeBusySet.children)

                d = inbox.readProperty((caldav_namespace, "calendar-free-busy-set"), request)
                d.addCallback(parseFreeBusy)
                return d

            d = inbox.hasProperty((caldav_namespace, "calendar-free-busy-set"), request)
            d.addCallback(getFreeBusy)
            return d

        d = self.scheduleInbox(request)
        d.addCallback(gotInbox)
        return d

    def scheduleInbox(self, request):
        """
        @return: the deferred schedule inbox for this principal.
        """
        return request.locateResource(self.scheduleInboxURL())

    ##
    # Quota
    ##

    def hasQuotaRoot(self, request):
        """
        Quota root only ever set on calendar homes.
        """
        return False

    def quotaRoot(self, request):
        """
        Quota root only ever set on calendar homes.
        """
        return None


class DefaultAlarmPropertyMixin(object):
    """
    A mixin for use with calendar home and calendars to allow direct access to
    the default alarm properties in a more useful way that using readProperty.
    In particular it will handle inheritance of the property from the home if a
    calendar does not explicitly have the property.

    Important: we need to distinguish between the property not being present, or
    present but empty, however the store by default is unable to distinguish between
    None and and empty C{str}. So what we do is use the value "empty" to represent
    a present but empty property.
    """

    ALARM_PROPERTIES = {
        caldavxml.DefaultAlarmVEventDateTime.qname(): (True, True,),
        caldavxml.DefaultAlarmVEventDate.qname(): (True, False,),
        caldavxml.DefaultAlarmVToDoDateTime.qname(): (False, True,),
        caldavxml.DefaultAlarmVToDoDate.qname(): (False, False,),
    }

    ALARM_PROPERTY_CLASSES = {
        caldavxml.DefaultAlarmVEventDateTime.qname(): caldavxml.DefaultAlarmVEventDateTime,
        caldavxml.DefaultAlarmVEventDate.qname(): caldavxml.DefaultAlarmVEventDate,
        caldavxml.DefaultAlarmVToDoDateTime.qname(): caldavxml.DefaultAlarmVToDoDateTime,
        caldavxml.DefaultAlarmVToDoDate.qname(): caldavxml.DefaultAlarmVToDoDate,
    }

    def getDefaultAlarmProperty(self, propname):

        vevent, timed = DefaultAlarmPropertyMixin.ALARM_PROPERTIES[propname]

        if self.isCalendarCollection():

            # Get from calendar or inherit from home
            alarm = self._newStoreObject.getDefaultAlarm(vevent, timed)
            if alarm is None:
                return self.parentResource().getDefaultAlarmProperty(propname)
            elif alarm == "empty":
                return DefaultAlarmPropertyMixin.ALARM_PROPERTY_CLASSES[propname]()
        else:
            # Just return whatever is on the home
            alarm = self._newStoreHome.getDefaultAlarm(vevent, timed)

        return DefaultAlarmPropertyMixin.ALARM_PROPERTY_CLASSES[propname](alarm) if alarm else None

    @inlineCallbacks
    def setDefaultAlarmProperty(self, prop):

        vevent, timed = DefaultAlarmPropertyMixin.ALARM_PROPERTIES[prop.qname()]
        alarm = str(prop)

        if self.isCalendarCollection():
            yield self._newStoreObject.setDefaultAlarm(alarm if alarm else "empty", vevent, timed)
        else:
            yield self._newStoreHome.setDefaultAlarm(alarm if alarm else "empty", vevent, timed)

    @inlineCallbacks
    def removeDefaultAlarmProperty(self, propname):

        vevent, timed = DefaultAlarmPropertyMixin.ALARM_PROPERTIES[propname]

        if self.isCalendarCollection():
            yield self._newStoreObject.setDefaultAlarm(None, vevent, timed)
        else:
            yield self._newStoreHome.setDefaultAlarm(None, vevent, timed)


class CommonHomeResource(PropfindCacheMixin, SharedHomeMixin, CalDAVResource):
    """
    Logic common to Calendar and Addressbook home resources.

    @ivar _provisionedChildren: A map of resource names to built-in children
        with protocol-level meanings, like C{"attachments"}, C{"inbox"},
        C{"outbox"}, and so on.
    @type _provisionedChildren: L{dict} mapping L{bytes} to L{Resource}

    @ivar _provisionedLinks: A map of resource names to built-in links that the
        server has inserted into this L{CommonHomeResource}.
    @type _provisionedLinks: L{dict} mapping L{bytes} to L{Resource}
    """

    def __init__(self, parent, name, transaction, home):
        self.parent = parent
        self.name = name
        self.associateWithTransaction(transaction)
        self._provisionedChildren = {}
        self._provisionedLinks = {}
        self._setupProvisions()
        self._newStoreHome = home
        CalDAVResource.__init__(self)

        from twistedcaldav.storebridge import _NewStorePropertiesWrapper
        self._dead_properties = _NewStorePropertiesWrapper(
            self._newStoreHome.properties()
        )

    @classmethod
    @inlineCallbacks
    def createHomeResource(cls, parent, name, transaction):
        home, _ignored_created = yield cls.homeFromTransaction(
            transaction, name)
        resource = cls(parent, name, transaction, home)
        returnValue(resource)

    @classmethod
    def homeFromTransaction(cls, transaction, uid):
        """
        Create or retrieve an appropriate back-end-home object from a
        transaction and a home UID.

        @return: a L{Deferred} which fires a 2-tuple of C{(created, home)}
            where C{created} is a boolean indicating whether this call created
            the home in the back-end, and C{home} is the home object itself.
        """
        raise NotImplementedError("Subclasses must implement.")

    def _setupProvisions(self):
        pass

    def liveProperties(self):

        props = super(CommonHomeResource, self).liveProperties() + (
            (customxml.calendarserver_namespace, "push-transports"),
            (customxml.calendarserver_namespace, "pushkey"),
        )

        if config.MaxCollectionsPerHome:
            props += (customxml.MaxCollections.qname(),)

        return props

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

    def canonicalURL(self, request):
        return succeed(self.url())

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

    def isCollection(self):
        return True

    def quotaSize(self, request):
        # FIXME: tests, workingness
        return succeed(0)

    def hasQuotaRoot(self, request):
        """
        Is this resource a quota root?  This returns True if the backend is
        enforcing quota.

        @return: a C{True} if this resource has quota root, C{False} otherwise.
        """
        return self._newStoreHome.quotaAllowedBytes() is not None

    def quotaRoot(self, request):
        """
        Retrieve the number of total allowed bytes from the backend.

        @return: a C{int} containing the maximum allowed bytes if this
            collection is quota-controlled, or C{None} if not quota controlled.
        """
        return self._newStoreHome.quotaAllowedBytes()

    def currentQuotaUse(self, request):
        """
        Get the quota use value
        """
        return maybeDeferred(self._newStoreHome.quotaUsedBytes)

    def supportedReports(self):
        result = super(CommonHomeResource, self).supportedReports()
        if config.EnableSyncReport and config.EnableSyncReportHome:
            # Allowed on any home
            result.append(element.Report(element.SyncCollection(),))
        return result

    def _mergeSyncTokens(self, hometoken, notificationtoken):
        """
        Merge two sync tokens, choosing the higher revision number of the two,
        but keeping the home resource-id intact.  If the config portion of
        the token is present, it is also kept intact.
        """
        if "/" in hometoken:
            hometoken, configtoken = hometoken.split("/")
        else:
            configtoken = None

        if "/" in notificationtoken:
            notificationtoken = notificationtoken.split("/")[0]

        homekey, homerev = hometoken.split("_", 1)
        notrev = notificationtoken.split("_", 1)[1]
        if int(notrev) > int(homerev):
            hometoken = "%s_%s" % (homekey, notrev,)

        if configtoken:
            hometoken = "{}/{}".format(hometoken, configtoken)

        return hometoken

    def canShare(self):
        raise NotImplementedError

    @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":
            yield self._newStoreHome.loadChildren()

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

        returnValue(result)

    @inlineCallbacks
    def makeChild(self, name):
        # Try built-in children first
        if name in self._provisionedChildren:
            cls = self._provisionedChildren[name]
            from twistedcaldav.notifications import NotificationCollectionResource
            if cls is NotificationCollectionResource:
                returnValue((yield self.createNotificationsCollection()))
            child = yield self._provisionedChildren[name](self)
            self.propagateTransaction(child)
            self.putChild(name, child)
            returnValue(child)

        # Try built-in links next
        if name in self._provisionedLinks:
            child = LinkResource(self, self._provisionedLinks[name])
            self.putChild(name, child)
            returnValue(child)

        # get regular or shared child
        child = yield self.makeRegularChild(name)

        # add _share attribute if child is shared; verify that child should
        # still be accessible and convert it to None if it's not.
        child = yield self.provisionShare(child)

        returnValue(child)

    @inlineCallbacks
    def createNotificationsCollection(self):
        txn = self._associatedTransaction
        notifications = yield txn.notificationsWithUID(self._newStoreHome.uid(), create=True)

        from twistedcaldav.storebridge import StoreNotificationCollectionResource
        similar = StoreNotificationCollectionResource(
            notifications,
            self,
            self._newStoreHome,
            principalCollections=self.principalCollections(),
        )
        self.propagateTransaction(similar)
        returnValue(similar)

    def makeRegularChild(self, name):
        raise NotImplementedError

    @inlineCallbacks
    def listChildren(self):
        """
        @return: a sequence of the names of all known children of this resource.
        """
        children = set(self._provisionedChildren.keys())
        children.update(self._provisionedLinks.keys())
        children.update((yield self._newStoreHome.listChildren()))
        returnValue(children)

    @inlineCallbacks
    def countOwnedChildren(self):
        """
        @return: the number of children (not shared ones).
        """
        returnValue(len(list((yield self._newStoreHome.listChildren()))))

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

        if qname == customxml.MaxCollections.qname() and config.MaxCollectionsPerHome:
            returnValue(customxml.MaxCollections.fromString(config.MaxCollectionsPerHome))

        elif qname == (customxml.calendarserver_namespace, "push-transports"):

            if config.Notifications.Services.APNS.Enabled:

                notifier = self._newStoreHome.getNotifier("push")
                nodeName = notifier.nodeName() if notifier is not None else None
                if nodeName:
                    notifierID = self._newStoreHome.notifierID()
                    if notifierID:
                        children = []

                        apsConfiguration = getPubSubAPSConfiguration(notifierID, config)
                        if apsConfiguration:
                            children.append(
                                customxml.PubSubTransportProperty(
                                    customxml.PubSubSubscriptionProperty(
                                        element.HRef(
                                            apsConfiguration["SubscriptionURL"]
                                        ),
                                    ),
                                    customxml.PubSubAPSBundleIDProperty(
                                        apsConfiguration["APSBundleID"]
                                    ),
                                    customxml.PubSubAPSEnvironmentProperty(
                                        apsConfiguration["APSEnvironment"]
                                    ),
                                    customxml.PubSubAPSRefreshIntervalProperty(
                                        str(apsConfiguration["SubscriptionRefreshIntervalSeconds"])
                                    ),
                                    type="APSD",
                                )
                            )

                        returnValue(customxml.PubSubPushTransportsProperty(*children))

            returnValue(None)

        elif qname == (customxml.calendarserver_namespace, "pushkey"):
            if (
                config.Notifications.Services.AMP.Enabled or
                config.Notifications.Services.APNS.Enabled
            ):
                notifier = self._newStoreHome.getNotifier("push")
                if notifier is not None:
                    returnValue(customxml.PubSubXMPPPushKeyProperty(notifier.nodeName()))
            returnValue(None)

        returnValue((yield super(CommonHomeResource, self).readProperty(property, request)))

    ##
    # ACL
    ##

    @inlineCallbacks
    def owner(self, request):
        principal = yield self.principalForRecord()
        returnValue(element.HRef(principal.principalURL()))

    def ownerPrincipal(self, request):
        return self.principalForRecord()

    def resourceOwnerPrincipal(self, request):
        return self.principalForRecord()

    @inlineCallbacks
    def defaultAccessControlList(self):
        myPrincipal = yield self.principalForRecord()

        # Server may be read only
        if config.EnableReadOnlyServer:
            owner_privs = (
                element.Privilege(element.Read()),
                element.Privilege(element.ReadCurrentUserPrivilegeSet()),
            )
        else:
            owner_privs = (element.Privilege(element.All()),)

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

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

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

        returnValue(element.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 self.defaultAccessControlList()

    def principalCollections(self):
        return self.parent.principalCollections()

    def principalForRecord(self):
        raise NotImplementedError("Subclass must implement principalForRecord()")

    @inlineCallbacks
    def etag(self):
        """
        Use the sync token as the etag
        """
        if self._newStoreHome:
            if config.EnableSyncReport and config.EnableSyncReportHome:
                token = (yield self.getInternalSyncToken())
            else:
                token = str(self._newStoreHome.modified())
            returnValue(ETag(hashlib.md5(token).hexdigest()))
        else:
            returnValue(None)

    def resourceID(self):
        return uuid.uuid5(self.uuid_namespace, str(self._newStoreHome.id())).urn

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

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

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

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

    def http_PUT(self, request):
        """
        Cannot PUT to existing collection.
        """
        return responsecode.FORBIDDEN

    # Methods not supported
    http_ACL = None
    http_COPY = None
    http_MOVE = None


class CalendarHomeResource(DefaultAlarmPropertyMixin, CommonHomeResource):
    """
    Calendar home collection classmethod.
    """

    @classmethod
    @inlineCallbacks
    def homeFromTransaction(cls, transaction, uid):
        storeHome = yield transaction.calendarHomeWithUID(uid)
        if storeHome is not None:
            created = False
        else:
            storeHome = yield transaction.calendarHomeWithUID(uid, create=True)
            created = True
        returnValue((storeHome, created))

    def liveProperties(self):

        existing = super(CalendarHomeResource, self).liveProperties()
        existing += (
            caldavxml.SupportedCalendarComponentSets.qname(),

            # These are "live" properties in the sense of WebDAV, however "live" for twext actually means
            # ones that are also always present, but the default alarm properties are allowed to be absent
            # and are in fact stored in the property store.
            # caldavxml.DefaultAlarmVEventDateTime.qname(),
            # caldavxml.DefaultAlarmVEventDate.qname(),
            # caldavxml.DefaultAlarmVToDoDateTime.qname(),
            # caldavxml.DefaultAlarmVToDoDate.qname(),

        )

        if config.EnableManagedAttachments:
            existing += (
                caldavxml.ManagedAttachmentsServerURL.qname(),
            )

        if config.EnableTimezonesByReference:
            existing += (
                caldavxml.TimezoneServiceSet.qname(),
            )

        return existing

    def dynamicProperties(self):
        return super(CalendarHomeResource, self).dynamicProperties() + tuple(
            DefaultAlarmPropertyMixin.ALARM_PROPERTIES.keys()
        )

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

        # Force calendar collections to always appear to have the property
        if qname in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            return succeed(self.getDefaultAlarmProperty(qname) is not None)

        else:
            return super(CalendarHomeResource, self).hasProperty(property, request)

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

        if qname == caldavxml.SupportedCalendarComponentSets.qname():
            if config.RestrictCalendarsToOneComponentType:
                prop = caldavxml.SupportedCalendarComponentSets(*[
                    caldavxml.SupportedCalendarComponentSet(
                        caldavxml.CalendarComponent(
                            name=name,
                        ),
                    ) for name in ical.allowedStoreComponents
                ])
            else:
                prop = caldavxml.SupportedCalendarComponentSets()
            returnValue(prop)

        elif qname == caldavxml.ManagedAttachmentsServerURL.qname():
            if config.EnableManagedAttachments:
                # The HRef is empty - this will force the client to treat all managed attachment URLs
                # as relative to this server scheme/host.
                returnValue(caldavxml.ManagedAttachmentsServerURL(element.HRef.fromString("")))
            else:
                returnValue(None)

        elif qname == caldavxml.TimezoneServiceSet.qname():
            if config.EnableTimezonesByReference and config.TimezoneService.Enabled:
                # Href points to standard timezone dist service.
                returnValue(caldavxml.TimezoneServiceSet(element.HRef.fromString(config.TimezoneService.URI)))
            else:
                returnValue(None)

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

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

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

        if property.qname() in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            if not property.valid():
                raise HTTPError(ErrorResponse(
                    responsecode.CONFLICT,
                    (caldav_namespace, "valid-calendar-data"),
                    description="Invalid property"
                ))
            yield self.setDefaultAlarmProperty(property)
            returnValue(None)

        result = (yield super(CalendarHomeResource, self).writeProperty(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)

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

    def _setupProvisions(self):

        # Cache children which must be of a specific type
        from twistedcaldav.storebridge import StoreScheduleInboxResource
        self._provisionedChildren["inbox"] = StoreScheduleInboxResource.maybeCreateInbox

        from twistedcaldav.scheduling_store.caldav.resource import ScheduleOutboxResource
        self._provisionedChildren["outbox"] = ScheduleOutboxResource

        if config.EnableDropBox and not config.EnableManagedAttachments:
            from twistedcaldav.storebridge import DropboxCollection
            self._provisionedChildren["dropbox"] = DropboxCollection

        if config.EnableManagedAttachments:
            from twistedcaldav.storebridge import AttachmentsCollection
            self._provisionedChildren["dropbox"] = AttachmentsCollection

        if config.FreeBusyURL.Enabled:
            from twistedcaldav.freebusyurl import FreeBusyURLResource
            self._provisionedChildren["freebusy"] = FreeBusyURLResource

        if config.Sharing.Enabled and config.Sharing.Calendars.Enabled:
            from twistedcaldav.notifications import NotificationCollectionResource
            self._provisionedChildren["notification"] = NotificationCollectionResource

    def canShare(self):
        return config.Sharing.Enabled and config.Sharing.Calendars.Enabled and self.exists()

    @inlineCallbacks
    def _otherPrincipalHomeURL(self, otherUID):
        ownerPrincipal = (yield self.principalForUID(otherUID))
        if ownerPrincipal and hasattr( ownerPrincipal, 'calendarHomeUrls' ) and len(ownerPrincipal.calendarHomeURLs()):
            returnValue(ownerPrincipal.calendarHomeURLs()[0])
        else:
            returnValue(None)

    @inlineCallbacks
    def makeRegularChild(self, name):
        newCalendar = yield self._newStoreHome.calendarWithName(name)
        if newCalendar and not newCalendar.owned() and not self.canShare():
            newCalendar = None

        from twistedcaldav.storebridge import CalendarCollectionResource
        similar = CalendarCollectionResource(
            newCalendar, self, name=name,
            principalCollections=self.principalCollections()
        )
        self.propagateTransaction(similar)
        returnValue(similar)

    def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
        """
        Test if there are other child object resources with the specified UID.

        Pass through direct to store.
        """
        return self._newStoreHome.hasCalendarResourceUIDSomewhereElse(uid, ok_object._newStoreObject, mode)

    @inlineCallbacks
    def defaultAccessControlList(self):
        myPrincipal = yield self.principalForRecord()

        # Server may be read only
        if config.EnableReadOnlyServer:
            owner_privs = (
                element.Privilege(element.Read()),
                element.Privilege(element.ReadCurrentUserPrivilegeSet()),
            )
        else:
            owner_privs = (element.Privilege(element.All()),)

        aces = (
            # Inheritable access for the resource's associated principal.
            element.ACE(
                element.Principal(element.HRef(myPrincipal.principalURL())),
                element.Grant(*owner_privs),
                element.Protected(),
                TwistedACLInheritable(),
            ),
            # Inheritable CALDAV:read-free-busy access for authenticated users.
            element.ACE(
                element.Principal(element.Authenticated()),
                element.Grant(element.Privilege(caldavxml.ReadFreeBusy())),
                TwistedACLInheritable(),
            ),
        )

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

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

        if config.EnableProxyPrincipals:
            # Server may be read only
            if config.EnableReadOnlyServer:
                ro_proxy_privs = rw_proxy_privs = (
                    element.Privilege(element.Read()),
                    element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                )
            else:
                ro_proxy_privs = (
                    element.Privilege(element.Read()),
                    element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                    element.Privilege(element.WriteProperties()),
                )
                rw_proxy_privs = (
                    element.Privilege(element.Read()),
                    element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                    element.Privilege(element.Write()),
                )

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

        returnValue(element.ACL(*aces))

    @inlineCallbacks
    def getInternalSyncToken(self):
        # The newstore implementation supports this directly
        caltoken = yield self._newStoreHome.syncToken()

        if config.Sharing.Enabled and config.Sharing.Calendars.Enabled:
            notificationtoken = yield (yield self.getChild("notification")).getInternalSyncToken()

            # Merge tokens
            caltoken = self._mergeSyncTokens(caltoken, notificationtoken)

        returnValue(caltoken)

    @inlineCallbacks
    def _indexWhatChanged(self, revision, depth):
        # The newstore implementation supports this directly
        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
            revision, depth
        )

        # Need to insert some addition items on first sync
        if revision == 0:
            changed.append("outbox/")

            if config.FreeBusyURL.Enabled:
                changed.append("freebusy")

            if config.Sharing.Enabled and config.Sharing.Calendars.Enabled:
                changed.append("notification/")

            # Dropbox is never synchronized
            if config.EnableDropBox or config.EnableManagedAttachments:
                notallowed.append("dropbox/")

        # Add in notification changes
        if config.Sharing.Enabled and config.Sharing.Calendars.Enabled:
            noti_changed, noti_deleted, noti_notallowed = yield (yield self.getChild("notification"))._indexWhatChanged(revision, depth)

            if noti_changed or noti_deleted:
                changed.append("notification")
            if depth == "infinity":
                changed.extend([joinURL("notification", name) for name in noti_changed])
                deleted.extend([joinURL("notification", name) for name in noti_deleted])
                notallowed.extend([joinURL("notification", name) for name in noti_notallowed])

        returnValue((changed, deleted, notallowed))

    @requiresPermissions(element.WriteContent())
    @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 == "emptytrash":
            days = int(request.args.get("days", ("0",))[0])
            yield self._newStoreHome.emptyTrash(days=days)
            returnValue(
                self._ok("ok", "Empty Trash")
            )

        elif action == "gettrashcontents":
            contents = yield self._newStoreHome.getTrashContents()
            returnValue(
                self._ok("ok", "Trash Contents", contents)
            )

        elif action == "recovertrash":
            mode = request.args.get("mode", ("event",))[0]
            recoveryID = request.args.get("id", (None,))[0]
            if recoveryID is None:
                returnValue(
                    self._ok("error", "Invalid id")
                )
            if recoveryID == "all":
                recoveryID = None
            else:
                try:
                    recoveryID = int(recoveryID)
                except ValueError:
                    returnValue(
                        self._ok("error", "Invalid id")
                    )

            yield self._newStoreHome.recoverTrash(mode, recoveryID)
            returnValue(
                self._ok("ok", "Recover Trash")
            )

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

    def _ok(self, status, description, result=None):
        if result is None:
            result = {}
        result["status"] = status
        result["description"] = description
        return JSONResponse(
            responsecode.OK,
            result,
        )


class AddressBookHomeResource (CommonHomeResource):
    """
    Address book home collection resource.
    """

    def __init__(self, *args, **kw):
        super(AddressBookHomeResource, self).__init__(*args, **kw)
        # get some Access header items
        self.http_MKCOL = None
        self.http_MKCALENDAR = None

    @classmethod
    @inlineCallbacks
    def homeFromTransaction(cls, transaction, uid):
        storeHome = yield transaction.addressbookHomeWithUID(uid)
        if storeHome is not None:
            created = False
        else:
            storeHome = yield transaction.addressbookHomeWithUID(uid, create=True)
            created = True
        returnValue((storeHome, created))

    def liveProperties(self):

        return super(AddressBookHomeResource, self).liveProperties() + (
            carddavxml.DefaultAddressBookURL.qname(),
        )

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

        if qname == carddavxml.DefaultAddressBookURL.qname():
            # Must have a valid default
            try:
                defaultAddressBookProperty = self.readDeadProperty(property)
            except HTTPError:
                defaultAddressBookProperty = None
            if defaultAddressBookProperty and len(defaultAddressBookProperty.children) == 1:
                defaultAddressBook = str(defaultAddressBookProperty.children[0])
                adbk = (yield request.locateResource(str(defaultAddressBook)))
                if adbk is not None and isAddressBookCollectionResource(adbk) and adbk.exists() and not adbk.isShareeResource():
                    returnValue(defaultAddressBookProperty)

            # Default is not valid - we have to try to pick one
            defaultAddressBookProperty = (yield self.pickNewDefaultAddressBook(request))
            returnValue(defaultAddressBookProperty)

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

    @inlineCallbacks
    def writeProperty(self, property, request):
        assert isinstance(property, element.WebDAVElement)

        if property.qname() == carddavxml.DefaultAddressBookURL.qname():
            # Verify that the address book added in the PROPPATCH is valid.
            property.children = [element.HRef(normalizeURL(str(href))) for href in property.children]
            new_adbk = [str(href) for href in property.children]
            adbk = None
            if len(new_adbk) == 1:
                adbkURI = str(new_adbk[0])
                adbk = (yield request.locateResource(str(new_adbk[0])))
            if adbk is None or not adbk.exists() or not isAddressBookCollectionResource(adbk) or adbk.isShareeResource():
                # Validate that href's point to a valid addressbook.
                raise HTTPError(ErrorResponse(
                    responsecode.CONFLICT,
                    (carddav_namespace, "valid-default-addressbook-URL"),
                    "Invalid URI",
                ))
            else:
                # Canonicalize the URL to __uids__ form and always ensure a trailing /
                adbkURI = (yield adbk.canonicalURL(request))
                if not adbkURI.endswith("/"):
                    adbkURI += "/"
                property = carddavxml.DefaultAddressBookURL(element.HRef(adbkURI))

        yield super(AddressBookHomeResource, self).writeProperty(property, request)

    def _setupProvisions(self):

        # Cache children which must be of a specific type
        if config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and not config.Sharing.Calendars.Enabled:
            from twistedcaldav.notifications import NotificationCollectionResource
            self._provisionedChildren["notification"] = NotificationCollectionResource

    def makeNewStore(self):
        return self._associatedTransaction.addressbookHomeWithUID(self.name, create=True), False     # Don't care about created

    def canShare(self):
        return config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and self.exists()

    @inlineCallbacks
    def _otherPrincipalHomeURL(self, otherUID):
        ownerPrincipal = (yield self.principalForUID(otherUID))
        returnValue(ownerPrincipal.addressBookHomeURLs()[0] if ownerPrincipal else None)

    @inlineCallbacks
    def makeRegularChild(self, name):

        # Check for public/global path
        from twistedcaldav.storebridge import (
            AddressBookCollectionResource,
        )
        mainCls = AddressBookCollectionResource
        newAddressBook = yield self._newStoreHome.addressbookWithName(name)
        if newAddressBook and not newAddressBook.owned() and not self.canShare():
            newAddressBook = None
        similar = mainCls(
            newAddressBook, self, name,
            principalCollections=self.principalCollections()
        )
        self.propagateTransaction(similar)
        returnValue(similar)

    @inlineCallbacks
    def pickNewDefaultAddressBook(self, request):
        """
        First see if "addressbook" exists in the addressbook home and pick that. Otherwise
        pick the first one we see.
        """
        defaultAddressBookURL = joinURL(self.url(), "addressbook")
        defaultAddressBook = (yield self.makeRegularChild("addressbook"))
        if defaultAddressBook is None or not defaultAddressBook.exists():
            addressbooks = yield self._newStoreHome.addressbooks()
            ownedAddressBooks = [addressbook for addressbook in addressbooks if addressbook.owned()]
            ownedAddressBooks.sort(key=lambda ab: ab.name())

            # These are only unshared children
            # FIXME: the back-end should re-provision a default addressbook here.
            # Really, the dead property shouldn't be necessary, and this should
            # be entirely computed by a back-end method like 'defaultAddressBook()'
            try:
                anAddressBook = ownedAddressBooks[0]
            except IndexError:
                raise RuntimeError("No address books at all.")

            defaultAddressBookURL = joinURL(self.url(), anAddressBook.name())

        # Always ensure a trailing /
        if not defaultAddressBookURL.endswith("/"):
            defaultAddressBookURL += "/"

        self.writeDeadProperty(
            carddavxml.DefaultAddressBookURL(
                element.HRef(defaultAddressBookURL)
            )
        )
        returnValue(carddavxml.DefaultAddressBookURL(
            element.HRef(defaultAddressBookURL))
        )

    @inlineCallbacks
    def getInternalSyncToken(self):
        # The newstore implementation supports this directly
        adbktoken = yield self._newStoreHome.syncToken()

        if config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and not config.Sharing.Calendars.Enabled:
            notifcationtoken = yield (yield self.getChild("notification")).getInternalSyncToken()

            # Merge tokens
            adbkkey, adbkrev = adbktoken.split("_", 1)
            notrev = notifcationtoken.split("_", 1)[1]
            if int(notrev) > int(adbkrev):
                adbktoken = "%s_%s" % (adbkkey, notrev,)

        returnValue(adbktoken)

    @inlineCallbacks
    def _indexWhatChanged(self, revision, depth):
        # The newstore implementation supports this directly
        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
            revision, depth
        )

        # Need to insert some addition items on first sync
        if revision == 0:
            if config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and not config.Sharing.Calendars.Enabled:
                changed.append("notification/")

        # Add in notification changes
        if config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and not config.Sharing.Calendars.Enabled:
            noti_changed, noti_deleted, noti_notallowed = yield (yield self.getChild("notification"))._indexWhatChanged(revision, depth)

            changed.extend([joinURL("notification", name) for name in noti_changed])
            deleted.extend([joinURL("notification", name) for name in noti_deleted])
            notallowed.extend([joinURL("notification", name) for name in noti_notallowed])

        returnValue((changed, deleted, notallowed))


class AuthenticationWrapper(SuperAuthenticationWrapper):

    """ AuthenticationWrapper implementation which allows overriding
        credentialFactories on a per-resource-path basis """

    def __init__(
        self, resource, portal,
        wireEncryptedCredentialFactories, wireUnencryptedCredentialFactories,
        loginInterfaces, overrides=None
    ):

        super(AuthenticationWrapper, self).__init__(
            resource, portal,
            wireEncryptedCredentialFactories, wireUnencryptedCredentialFactories,
            loginInterfaces
        )

        self.overrides = {}
        if overrides:
            for path, factories in overrides.iteritems():
                self.overrides[path] = dict(
                    [
                        (factory.scheme, factory)
                        for factory in factories
                    ]
                )

    def hook(self, req):
        """ Uses the default credentialFactories unless the request is for
            one of the overridden paths """

        super(AuthenticationWrapper, self).hook(req)

        factories = self.overrides.get(
            req.path.rstrip("/"),
            req.credentialFactories
        )
        req.credentialFactories = factories


##
# Utilities
##

def isCalendarCollectionResource(resource):
    try:
        resource = ICalDAVResource(resource)
    except TypeError:
        return False
    else:
        return resource.isCalendarCollection()


def isPseudoCalendarCollectionResource(resource):
    try:
        resource = ICalDAVResource(resource)
    except TypeError:
        return False
    else:
        return resource.isPseudoCalendarCollection()


def isAddressBookCollectionResource(resource):
    try:
        resource = ICalDAVResource(resource)
    except TypeError:
        return False
    else:
        return resource.isAddressBookCollection()
