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

"""
CalDAV-aware resources.
"""

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

import hashlib
from urlparse import urlsplit
import urllib
from uuid import UUID
import uuid


from zope.interface import implements

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

from twext.python.log import Logger

from txdav.xml import element
from txdav.xml.element import dav_namespace

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

from twistedcaldav import caldavxml, customxml
from twistedcaldav import carddavxml
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.peruserdata import PerUserDataFilter
from twistedcaldav.datafilters.privateevents import PrivateEventFilter
from twistedcaldav.directory.internal import InternalDirectoryRecord
from twistedcaldav.extensions import DAVResource, DAVPrincipalResource, \
    DAVResourceWithChildrenMixin
from twistedcaldav.ical import Component

from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
from twistedcaldav.linkresource import LinkResource
from calendarserver.push.notifier import getPubSubAPSConfiguration
from twistedcaldav.sharing import SharedResourceMixin, SharedHomeMixin
from twistedcaldav.util import normalizationLookup
from twistedcaldav.vcard import Component as vComponent

from txdav.common.icommondatastore import InternalDataStoreError, \
    SyncTokenValidException

##
# 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()

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(data):
                response = Response()
                response.stream = MemoryStream(str(data))
                response.headers.setHeader("content-type", MimeType.fromString("text/calendar"))
                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{twext.web2.iweb.IRequest}
        @param transaction: optional transaction to use instead of associated transaction
        @type transaction: L{txdav.caldav.idav.ITransaction}
        """
        response = yield super(CalDAVResource, self).renderHTTP(request)
        if transaction is None:
            transaction = self._associatedTransaction
        if transaction is not None:
            if self._transactionError:
                yield transaction.abort()
            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)


    # 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):
        """
        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(),
            )

        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.Invite.qname(),
                    customxml.AllowedSharingModes.qname(),
                    customxml.SharedURL.qname(),
                )

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

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


    def isShadowableProperty(self, qname):
        """
        Shadowable properties are ones on shared resources where a "default" exists until
        a user overrides with their own value.
        """
        return qname in (
            caldavxml.CalendarDescription.qname(),
            caldavxml.CalendarTimeZone.qname(),
            carddavxml.AddressBookDescription.qname(),
        )


    def isGlobalProperty(self, qname):
        """
        A global property is one that is the same for all users.
        """
        if qname in self.liveProperties():
            if qname in (
                element.DisplayName.qname(),
            ):
                return False
            else:
                return True
        else:
            return False


    @inlineCallbacks
    def hasProperty(self, property, request):
        """
        Need to special case schedule-calendar-transp for backwards compatability.
        """
        res = (yield self._hasGlobalProperty(property, request))
        returnValue(res)


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

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

        # Force calendar collections to always appear to have the property
        if qname == caldavxml.ScheduleCalendarTransp.qname() and self.isCalendarCollection():
            return succeed(True)

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


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

        if self.isCalendarCollection() or (self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection()):

            # Push notification DAV property "pushkey"
            if qname == customxml.PubSubXMPPPushKeyProperty.qname():

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

                returnValue(customxml.PubSubXMPPPushKeyProperty())

        res = (yield self._readGlobalProperty(qname, property, request))
        returnValue(res)


    def _readSharedProperty(self, qname, request):

        # Default behavior - read per-user dead property
        p = self.deadProperties().get(qname)
        return p


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

        if 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():
            returnValue(caldavxml.SupportedCalendarData(
                caldavxml.CalendarData(**{
                    "content-type": "text/calendar",
                    "version"     : "2.0",
                }),
            ))

        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
            returnValue(carddavxml.SupportedAddressData(
                carddavxml.AddressDataType(**{
                    "content-type": "text/vcard",
                    "version"     : "3.0",
                }),
            ))

        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 self.isGroup()) and not self.isDirectoryBackedAddressBookCollection()
            ):
                result = (yield self.inviteProperty(request))
                returnValue(result)

        elif qname == customxml.AllowedSharingModes.qname():
            if config.Sharing.Enabled and config.Sharing.Calendars.Enabled and self.isCalendarCollection():
                returnValue(customxml.AllowedSharingModes(customxml.CanBeShared()))
            elif config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and (self.isAddressBookCollection() or 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,)
        )

        self._preProcessWriteProperty(property, request)

        res = (yield self._writeGlobalProperty(property, request))
        returnValue(res)


    def _preProcessWriteProperty(self, property, request, isShare=False):
        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:timezone is a live property in the sense that the
        # server enforces what can be stored, however it need not actually
        # exist so we cannot list it in liveProperties on this resource, since its
        # its presence there means that hasProperty will always return True for it.
        elif property.qname() == caldavxml.CalendarTimeZone.qname():
            if not self.isCalendarCollection():
                raise HTTPError(StatusResponse(
                    responsecode.FORBIDDEN,
                    "Property %s may only be set on calendar collection." % (property,)
                ))
            if not property.valid():
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    (caldav_namespace, "valid-calendar-data"),
                    description="Invalid property"
                ))

        # Validate default alarm properties (do this even if the default alarm feature is off)
        elif property.qname() in (
            caldavxml.DefaultAlarmVEventDateTime.qname(),
            caldavxml.DefaultAlarmVEventDate.qname(),
            caldavxml.DefaultAlarmVToDoDateTime.qname(),
            caldavxml.DefaultAlarmVToDoDate.qname(),
        ):
            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,)
                ))

            if not property.valid():
                raise HTTPError(ErrorResponse(
                    responsecode.CONFLICT,
                    (caldav_namespace, "valid-calendar-data"),
                    description="Invalid 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,)
                ))


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

        if property.qname() == caldavxml.ScheduleCalendarTransp.qname():
            yield self._newStoreObject.setUsedForFreeBusy(property == caldavxml.ScheduleCalendarTransp(caldavxml.Opaque()))
            returnValue(None)

        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 self.isShareeResource():
            parent = (yield self.locateParent(request, self._share.url()))
        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 DAV:owner property value (MUST be a DAV:href or None).
        """
        if self.isShareeResource():
            parent = (yield self.locateParent(request, self._share.url()))
        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 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 self.isAddressBookCollection() and not self.hasDeadProperty((dav_namespace, "displayname")):
            return None

        if 'record' in dir(self):
            if self.record.fullName:
                return self.record.fullName
            elif self.record.shortNames:
                return self.record.shortNames[0]
            else:
                return super(DAVResource, self).displayName()
        else:
            result = super(DAVResource, self).displayName()
            if not result:
                result = self.name()
            return result


    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)


    @inlineCallbacks
    def iCalendarForUser(self, request):

        caldata = yield self.iCalendar()

        accessUID = (yield self.resourceOwnerPrincipal(request))
        if accessUID is None:
            accessUID = ""
        else:
            accessUID = accessUID.principalUID()

        returnValue(PerUserDataFilter(accessUID).filter(caldata))


    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}
        """
        ical.normalizeCalendarUserAddresses(normalizationLookup,
            self.principalForCalendarUserAddress)


    def principalForCalendarUserAddress(self, address):
        for principalCollection in self.principalCollections():
            principal = principalCollection.principalForCalendarUserAddress(address)
            if principal is not None:
                return principal
        return None


    def principalForUID(self, principalUID):
        for principalCollection in self.principalCollections():
            principal = principalCollection.principalForUID(principalUID)
            if principal is not None:
                return principal
        return 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
            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):
        current_token = (yield self.getSyncToken())
        current_uuid, current_revision = current_token[6:].split("_", 1)
        current_revision = int(current_revision)

        if client_token:
            try:
                if not client_token.startswith("data:,"):
                    raise ValueError
                caluuid, revision = client_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",
            ))

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


    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())
        returnValue("data:,%s" % (internal_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 twext.web2.static.checkPreconditions
                    if request.method not in ("GET", "HEAD"):

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

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

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

        result = (yield super(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{twext.web2.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" % (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: %s" % (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 %s" % (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 %s" % (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, accessUID=None):

        # Now "filter" the resource calendar data
        caldata = (yield self.iCalendar())
        if accessUID:
            caldata = PerUserDataFilter(accessUID).filter(caldata)
        caldata = HiddenInstanceFilter().filter(caldata)
        caldata = PrivateEventFilter(self.accessMode, isowner).filter(caldata)
        returnValue(caldata)


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


    def iCalendar(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{twext.web2.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" % (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: %s" % (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 %s" % (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 %s" % (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"),
                (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


    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" and self.calendarsEnabled():
                autoSchedule = self.getAutoSchedule()
                returnValue(customxml.AutoSchedule("true" if autoSchedule else "false"))

            elif name == "auto-schedule-mode" and self.calendarsEnabled():
                autoScheduleMode = self.getAutoScheduleMode()
                returnValue(customxml.AutoScheduleMode(autoScheduleMode 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.
        """
        homekey, homerev = hometoken.split("_", 1)
        notrev = notificationtoken.split("_", 1)[1]
        if int(notrev) > int(homerev):
            hometoken = "%s_%s" % (homekey, notrev,)
        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())

        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
    ##

    def owner(self, request):
        return succeed(element.HRef(self.principalForRecord().principalURL()))


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


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


    def defaultAccessControlList(self):
        myPrincipal = 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

        return 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 succeed(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()

    # 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(),
            )

        return existing


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

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

        # 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)._hasGlobalProperty(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="VEVENT",
                        ),
                    ),
                    caldavxml.SupportedCalendarComponentSet(
                        caldavxml.CalendarComponent(
                            name="VTODO",
                        ),
                    ),
                )
            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 in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
            returnValue(self.getDefaultAlarmProperty(qname))

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


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

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

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


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

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

        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 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)


    def defaultAccessControlList(self):
        myPrincipal = 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:
                rw_proxy_privs = (
                    element.Privilege(element.Read()),
                    element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                )
            else:
                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(
                        element.Privilege(element.Read()),
                        element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                    ),
                    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(),
                ),
            )

        return 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 = yield self._newStoreHome.resourceNamesSinceToken(
            revision, depth
        )
        notallowed = []

        # 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))



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

        if config.GlobalAddressBook.Enabled:
            self._provisionedLinks[config.GlobalAddressBook.Name] = "/addressbooks/public/global/addressbook/"


    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 makeRegularChild(self, name):

        # Check for public/global path
        from twistedcaldav.storebridge import (
            AddressBookCollectionResource,
            GlobalAddressBookCollectionResource,
        )
        mainCls = AddressBookCollectionResource
        if isinstance(self.record, InternalDirectoryRecord):
            if "global" in self.record.shortNames:
                mainCls = GlobalAddressBookCollectionResource

        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 = yield self._newStoreHome.resourceNamesSinceToken(
            revision, depth
        )
        notallowed = []

        # 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 GlobalAddressBookResource (ReadOnlyResourceMixIn, CalDAVResource):
    """
    Global address book. All we care about is making sure permissions are setup.
    """

    def resourceType(self):
        return element.ResourceType.sharedaddressbook #@UndefinedVariable


    def defaultAccessControlList(self):

        aces = (
            element.ACE(
                element.Principal(element.Authenticated()),
                element.Grant(
                    element.Privilege(element.Read()),
                    element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                    element.Privilege(element.Write()),
                ),
                element.Protected(),
                TwistedACLInheritable(),
           ),
        )

        if config.GlobalAddressBook.EnableAnonymousReadAccess:
            aces += (
                element.ACE(
                    element.Principal(element.Unauthenticated()),
                    element.Grant(
                        element.Privilege(element.Read()),
                    ),
                    element.Protected(),
                    TwistedACLInheritable(),
               ),
            )
        return 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 succeed(self.defaultAccessControlList())



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()
