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

"""
Sharing behavior
"""


__all__ = [
    "SharedResourceMixin",
    "SharedHomeMixin",
]

from twext.who.idirectory import RecordType

from twisted.internet.defer import succeed, inlineCallbacks, DeferredList, \
    returnValue

from twistedcaldav import customxml, caldavxml
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.linkresource import LinkFollowerMixIn

from txdav.common.datastore.sql_tables import _ABO_KIND_GROUP, \
    _BIND_MODE_DIRECT, _BIND_MODE_INDIRECT, _BIND_MODE_OWN, _BIND_MODE_READ, \
    _BIND_MODE_WRITE, _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, \
    _BIND_STATUS_DELETED, _BIND_STATUS_INVALID, _BIND_STATUS_INVITED
from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
from txdav.xml import element
from txdav.who.wiki import RecordType as WikiRecordType, WikiAccessLevel

from txweb2 import responsecode
from txweb2.dav.http import ErrorResponse, MultiStatusResponse
from txweb2.dav.resource import TwistedACLInheritable
from txweb2.dav.util import allDataFromStream, joinURL
from txweb2.http import HTTPError, Response, XMLResponse


class SharedResourceMixin(object):
    """
    A mix-in for calendar/addressbook resources that implements sharing-related
    functionality.
    """

    @inlineCallbacks
    def inviteProperty(self, request):
        """
        Calculate the customxml.Invite property (for readProperty) from the
        invites database.
        """
        if config.Sharing.Enabled:

            @inlineCallbacks
            def invitePropertyElement(invitation, includeUID=True):

                userid = "urn:x-uid:" + invitation.shareeUID
                principal = yield self.principalForUID(invitation.shareeUID)
                cn = principal.displayName() if principal else invitation.shareeUID
                returnValue(customxml.InviteUser(
                    customxml.UID.fromString(invitation.uid) if includeUID else None,
                    element.HRef.fromString(userid),
                    customxml.CommonName.fromString(cn),
                    customxml.InviteAccess(invitationBindModeToXMLMap[invitation.mode]()),
                    invitationBindStatusToXMLMap[invitation.status](),
                ))

            # See if this property is on the shared calendar
            if self.isSharedByOwner():
                invitations = yield self.validateInvites(request)
                returnValue(customxml.Invite(
                    *[(yield invitePropertyElement(invitation)) for invitation in invitations]
                ))

            # See if it is on the sharee calendar
            if self.isShareeResource():
                original = yield self._newStoreObject.ownerView()
                if original is not None:
                    invitations = yield original.allInvitations()
                    invitations = yield self.validateInvites(request, invitations)

                    ownerPrincipal = yield self.principalForUID(self._newStoreObject.ownerHome().uid())
                    if ownerPrincipal is None:
                        owner = "invalid"
                        ownerCN = "Invalid"
                    else:
                        # FIXME:  use urn:x-uid in all cases
                        if self.isCalendarCollection():
                            owner = ownerPrincipal.principalURL()
                        else:
                            owner = "urn:x-uid:" + ownerPrincipal.principalUID()
                        ownerCN = ownerPrincipal.displayName()

                    returnValue(customxml.Invite(
                        customxml.Organizer(
                            element.HRef.fromString(owner),
                            customxml.CommonName.fromString(ownerCN),
                        ),
                        *[(yield invitePropertyElement(invitation, includeUID=False)) for invitation in invitations]
                    ))

        returnValue(None)

    @inlineCallbacks
    def upgradeToShare(self):
        """
        Set the resource-type property on this resource to indicate that this
        is the owner's version of a resource which has been shared.
        """
        # Change status on store object
        yield self._newStoreObject.setShared(True)

    @inlineCallbacks
    def downgradeFromShare(self, request):

        # Change status on store object
        yield self._newStoreObject.setShared(False)

        # Remove all invitees
        for invitation in (yield self._newStoreObject.allInvitations()):
            yield self._newStoreObject.uninviteUIDFromShare(invitation.shareeUID)

        returnValue(True)

    @inlineCallbacks
    def directShare(self, request):
        """
        Directly bind an accessible calendar/address book collection into the
        current principal's calendar/addressbook home.

        @param request: the request triggering this action
        @type request: L{IRequest}

        @return: the (asynchronous) HTTP result to respond to the direct-share
            request.
        @rtype: L{Deferred} firing L{txweb2.http.Response}, failing with
            L{HTTPError}
        """

        # Need to have at least DAV:read to do this
        yield self.authorize(request, (element.Read(),))

        # Find current principal
        authz_principal = self.currentPrincipal(request).children[0]
        if not isinstance(authz_principal, element.HRef):
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "valid-principal"),
                "Current user principal not a DAV:href",
            ))
        principalURL = str(authz_principal)
        if not principalURL:
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "valid-principal"),
                "Current user principal not specified",
            ))
        sharee = (yield request.locateResource(principalURL))

        # Check enabled for service
        from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
        if not isinstance(sharee, DirectoryCalendarPrincipalResource):
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "invalid-principal"),
                "Current user principal is not a calendar/addressbook enabled principal",
            ))

        # Get the home collection
        if self.isCalendarCollection():
            shareeHomeResource = yield sharee.calendarHome(request)
        elif self.isAddressBookCollection() or self.isGroup():
            shareeHomeResource = yield sharee.addressBookHome(request)
        else:
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "invalid-principal"),
                "No calendar/addressbook home for principal",
            ))

        # TODO: Make sure principal is not sharing back to themselves
        hostURL = (yield self.canonicalURL(request))
        shareeHomeURL = shareeHomeResource.url()
        if hostURL.startswith(shareeHomeURL):
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "invalid-share"),
                "Can't share your own calendar or addressbook",
            ))

        # Accept it
        shareeView = yield self._newStoreObject.directShareWithUser(
            sharee.principalUID(),
            displayName=self.displayName()
        )

        # Return the URL of the shared calendar
        sharedAsURL = joinURL(shareeHomeResource.url(), shareeView.name())
        returnValue(XMLResponse(
            code=responsecode.OK,
            element=customxml.SharedAs(
                element.HRef.fromString(sharedAsURL)
            )
        ))

    def isSharedByOwner(self):
        """
        Return True if this is an owner shared calendar collection.
        """
        try:
            return self._newStoreObject.isSharedByOwner() if self._newStoreObject else False
        except AttributeError:
            return False

    def setShare(self, share_url):
        """
        Set the URL associated with this L{SharedResourceMixin}.  (This
        is only invoked on the sharee's resource, not the owner's.)
        """
        self._isShareeResource = True
        self._share_url = share_url

    def isShareeResource(self):
        """
        Return True if this is a sharee view of a shared collection.
        """
        return (
            hasattr(self, "_newStoreObject") and
            hasattr(self._newStoreObject, "owned") and
            not self._newStoreObject.owned() and
            getattr(self._newStoreObject, "_bindMode", None) is not None
        )

    def removeShareeResource(self, request):
        """
        Called when the sharee DELETEs a shared collection.
        """
        return self._newStoreObject.deleteShare()

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

        @return: a L{Deferred} firing a L{bytes} or L{None}, with one of the
            potential values: C{"own"}, which means that the home is the owner
            of the collection and it is not shared; C{"read-only"}, meaning
            that the home that this collection is bound into has only read
            access to this collection; C{"read-write"}, which means that the
            home has both read and write access; C{"original"}, which means
            that it should inherit the ACLs of the owner's collection, whatever
            those happen to be, or C{None}, which means that the external
            access control mechanism has dictate the home should no longer have
            any access at all.
        """
        if self._newStoreObject.direct():
            owner = yield self.principalForUID(self._newStoreObject.ownerHome().uid())
            sharee = yield self.principalForUID(self._newStoreObject.viewerHome().uid())
            if owner.record.recordType == WikiRecordType.macOSXServerWiki:
                # Access level comes from what the wiki has granted to the
                # sharee
                access = (yield owner.record.accessForRecord(sharee.record))
                if access == WikiAccessLevel.read:
                    returnValue("read-only")
                elif access == WikiAccessLevel.write:
                    returnValue("read-write")
                else:
                    returnValue(None)
            else:
                # Check proxy access
                proxy_mode = yield sharee.proxyMode(owner)
                if proxy_mode == "none":
                    returnValue("original")
                else:
                    returnValue("read-write" if proxy_mode == "write" else "read-only")
        else:
            # Invited shares use access mode from the invite
            # Get the access for self
            bindMode = yield self._newStoreObject.effectiveShareMode()
            returnValue(invitationAccessFromBindModeMap.get(bindMode))

    @inlineCallbacks
    def shareeAccessControlList(self, request, *args, **kwargs):
        """
        Return WebDAV ACLs appropriate for the current user accessing the
        shared collection.  For an "invite" share we take the privilege granted
        to the sharee in the invite and map that to WebDAV ACLs.  For a
        "direct" share, if it is a wiki collection we map the wiki privileges
        into WebDAV ACLs, otherwise we use whatever privileges exist on the
        underlying shared collection.

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

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

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

        @return: the appropriate WebDAV ACL for the sharee
        @rtype: L{davxml.ACL}
        """

        assert self._isShareeResource, "Only call this for a sharee resource"
        assert self.isCalendarCollection() or self.isAddressBookCollection(), "Only call this for a address book or calendar resource"

        sharee = yield self.principalForUID(self._newStoreObject.viewerHome().uid())
        access = yield self._checkAccessControl()

        if access == "original" and not self._newStoreObject.ownerHome().external():
            original = (yield request.locateResource(self._share_url))
            result = (yield original.accessControlList(request, *args, **kwargs))
            returnValue(result)

        # Direct shares use underlying privileges of shared collection
        userprivs = [
        ]
        if access in ("read-only", "read-write",):
            userprivs.append(element.Privilege(element.Read()))
            userprivs.append(element.Privilege(element.ReadACL()))
            userprivs.append(element.Privilege(element.ReadCurrentUserPrivilegeSet()))
        if access in ("read-only",):
            userprivs.append(element.Privilege(element.WriteProperties()))
        if access in ("read-write",):
            userprivs.append(element.Privilege(element.Write()))
        proxyprivs = list(userprivs)
        try:
            proxyprivs.remove(element.Privilege(element.ReadACL()))
        except ValueError:
            # If wiki says no-access then ReadACL won't be in the list
            pass

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

        if self.isCalendarCollection():
            aces += (
                # 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 self.isCalendarCollection() and config.EnableProxyPrincipals:
            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(sharee.principalURL(), "calendar-proxy-read/"))),
                    element.Grant(
                        element.Privilege(element.Read()),
                        element.Privilege(element.ReadCurrentUserPrivilegeSet()),
                        element.Privilege(element.WriteProperties()),
                    ),
                    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(sharee.principalURL(), "calendar-proxy-write/"))),
                    element.Grant(*proxyprivs),
                    element.Protected(),
                    TwistedACLInheritable(),
                ),
            )

        returnValue(element.ACL(*aces))

    @inlineCallbacks
    def validUserIDForShare(self, userid, request=None):
        """
        Test the user id to see if it is a valid identifier for sharing and
        return a "normalized" form for our own use (e.g. convert mailto: to
        urn:uuid).

        @param userid: the userid to test
        @type userid: C{str}

        @return: C{str} of normalized userid or C{None} if
            userid is not allowed.
        """

        # First try to resolve as a calendar principal
        principal = yield self.principalForCalendarUserAddress(userid)
        if principal is None:
            principal = yield self.principalForCalendarGroupAddress(userid)
        if principal:
            if request:
                ownerPrincipal = (yield self.ownerPrincipal(request))
                if ownerPrincipal is None or ownerPrincipal.principalURL() == principal.principalURL():
                    returnValue(None)
            returnValue(principal.principalURL())

        # TODO: we do not support external users right now so this is being hard-coded
        # off in spite of the config option.
        # elif config.Sharing.AllowExternalUsers:
        #    return userid
        else:
            returnValue(None)

    @inlineCallbacks
    def principalForCalendarGroupAddress(self, groupid):
        """
        Get principal for group address if extant
        """

        if (
            config.Sharing.Enabled and
            config.Sharing.Calendars.Enabled and
            config.Sharing.Calendars.Groups.Enabled
        ):
            # see if group
            for principalCollection in self.principalCollections():
                record = yield principalCollection.directory.recordWithCalendarUserAddress(groupid)
                if record is not None and record.recordType == RecordType.group:
                    groupPrincipal = yield principalCollection.principalForRecord(record)
                    if groupPrincipal is not None:
                        returnValue(groupPrincipal)

        returnValue(None)

    @inlineCallbacks
    def validateInvites(self, request, invitations=None):
        """
        Make sure each userid in an invite is valid - if not re-write status.
        """
        # assert request
        if invitations is None:
            invitations = yield self._newStoreObject.allInvitations()
        adjusted_invitations = []
        for invitation in invitations:
            if invitation.status != _BIND_STATUS_INVALID:
                if not (yield self.validUserIDForShare("urn:x-uid:" + invitation.shareeUID, request)):
                    self.log.error("Invalid sharee detected: {uid}", uid=invitation.shareeUID)
                    invitation = invitation._replace(status=_BIND_STATUS_INVALID)

            invitation = invitation._replace(
                mode=(
                    yield self._newStoreObject._effectiveShareMode(
                        invitation.mode, invitation.shareeUID, self._newStoreObject._txn
                    )
                )
            )
            adjusted_invitations.append(invitation)

        returnValue(adjusted_invitations)

    def inviteUIDToShare(self, userid, cn, ace, summary, request):
        """ Send out in invite first, and then add this user to the share list
            @param userid:
            @param ace: Must be one of customxml.ReadWriteAccess or customxml.ReadAccess
        """

        # TODO: Check if this collection is shared, and error out if it isn't
        resultIsList = True
        if type(userid) is not list:
            userid = [userid]
            resultIsList = False
        if type(cn) is not list:
            cn = [cn]

        dl = [self.inviteSingleUserToShare(_user, _cn, ace, summary, request) for _user, _cn in zip(userid, cn)]
        return self._processShareActionList(dl, resultIsList)

    def uninviteUIDFromShare(self, userid, ace, request):
        """
        Send out in uninvite first, and then remove this user from the share list.
        """
        # Do not validate the userid - we want to allow invalid users to be removed because they
        # may have been valid when added, but no longer valid now. Clients should be able to clear out
        # anything known to be invalid.

        # TODO: Check if this collection is shared, and error out if it isn't
        resultIsList = True
        if type(userid) is not list:
            userid = [userid]
            resultIsList = False

        dl = [self.uninviteSingleUserFromShare(user, ace, request) for user in userid]
        return self._processShareActionList(dl, resultIsList)

    def inviteUserUpdateToShare(self, userid, cn, aceOLD, aceNEW, summary, request):

        resultIsList = True
        if type(userid) is not list:
            userid = [userid]
            resultIsList = False
        if type(cn) is not list:
            cn = [cn]

        dl = [self.inviteSingleUserUpdateToShare(_user, _cn, aceOLD, aceNEW, summary, request) for _user, _cn in zip(userid, cn)]
        return self._processShareActionList(dl, resultIsList)

    def _processShareActionList(self, dl, resultIsList):
        def _defer(resultset):
            results = [result if success else False for success, result in resultset]
            return results if resultIsList else results[0]
        return DeferredList(dl).addCallback(_defer)

    @inlineCallbacks
    def inviteSingleUserToShare(self, userid, cn, ace, summary, request):  # @UnusedVariable

        # We currently only handle local users
        sharee = yield self.principalForCalendarUserAddress(userid)
        if sharee is None:
            sharee = yield self.principalForCalendarGroupAddress(userid)
            if sharee is None:
                returnValue(False)

        try:
            result = (yield self._newStoreObject.inviteUIDToShare(
                sharee.principalUID(),
                invitationBindModeFromXMLMap[type(ace)],
                summary,
            ))
        except Exception as e:
            self.log.error("Could not send sharing invite '{userid}': {ex}", userid=userid, ex=e)
            result = None

        returnValue(result)

    @inlineCallbacks
    def uninviteSingleUserFromShare(self, userid, aces, request):  # @UnusedVariable

        # Cancel invites - we'll just use whatever userid we are given. However, if we
        # cannot find a matching principal, try to extract the uid from the userid
        # and use that (to allow invalid principals to be removed).
        sharee = yield self.principalForCalendarUserAddress(userid)
        if sharee is not None:
            uid = sharee.principalUID()
        elif userid.startswith("urn:x-uid:"):
            uid = userid[10:]
        else:
            returnValue(False)

        try:
            result = (yield self._newStoreObject.uninviteUIDFromShare(uid))
        except Exception as e:
            self.log.error("Could not send sharing uninvite '{userid}': {ex}", userid=userid, ex=e)
            result = None

        returnValue(result)

    @inlineCallbacks
    def uninviteFromShare(self, invitation, request):

        yield self._newStoreObject.uninviteFromShare(invitation)
        returnValue(True)

    def inviteSingleUserUpdateToShare(self, userid, commonName, acesOLD, aceNEW, summary, request):  # @UnusedVariable

        # Just update existing
        return self.inviteSingleUserToShare(userid, commonName, aceNEW, summary, request)

    @inlineCallbacks
    def _xmlHandleInvite(self, request, docroot):
        # Sharing must be enabled for this collection
        if not self.canBeShared():
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (customxml.calendarserver_namespace, "valid-request"),
                "Sharing not supported on this resource",
            ))
        yield self.authorize(request, (element.Read(), element.Write()))
        result = (yield self._handleInvite(request, docroot))
        returnValue(result)

    @inlineCallbacks
    def _handleInvite(self, request, invitedoc):
        def _handleInviteSet(inviteset):
            userid = None
            cn = None
            access = None
            summary = None
            for item in inviteset.children:
                if isinstance(item, element.HRef):
                    userid = str(item)
                    continue
                if isinstance(item, customxml.CommonName):
                    cn = str(item)
                    continue
                if isinstance(item, customxml.InviteSummary):
                    summary = str(item)
                    continue
                if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
                    access = item
                    continue
            if userid and access and summary:
                return (userid, cn, access, summary)
            else:
                error_text = []
                if userid is None:
                    error_text.append("missing href")
                if access is None:
                    error_text.append("missing access")
                if summary is None:
                    error_text.append("missing summary")
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    (customxml.calendarserver_namespace, "valid-request"),
                    "%s: %s" % (", ".join(error_text), inviteset,),
                ))

        def _handleInviteRemove(inviteremove):
            userid = None
            access = []
            for item in inviteremove.children:
                if isinstance(item, element.HRef):
                    userid = str(item)
                    continue
                if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
                    access.append(item)
                    continue
            if userid is None:
                raise HTTPError(ErrorResponse(
                    responsecode.FORBIDDEN,
                    (customxml.calendarserver_namespace, "valid-request"),
                    "Missing href: %s" % (inviteremove,),
                ))
            if len(access) == 0:
                access = None
            else:
                access = set(access)
            return (userid, access)

        setDict, removeDict, updateinviteDict = {}, {}, {}
        okusers = set()
        badusers = set()
        for item in invitedoc.children:
            if isinstance(item, customxml.InviteSet):
                userid, cn, access, summary = _handleInviteSet(item)
                setDict[userid] = (cn, access, summary)

                # Validate each userid on add only
                uid = (yield self.validUserIDForShare(userid, request))
                if uid is None:
                    uid = yield self.principalForCalendarGroupAddress(userid)
                (badusers if uid is None else okusers).add(userid)
            elif isinstance(item, customxml.InviteRemove):
                userid, access = _handleInviteRemove(item)
                removeDict[userid] = access

                # Treat removed userids as valid as we will fail invalid ones silently
                okusers.add(userid)

        # Only make changes if all OK
        if len(badusers) == 0:
            okusers = set()
            badusers = set()
            # Special case removing and adding the same user and treat that as an add
            sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
            for u in sameUseridInRemoveAndSet:
                removeACL = removeDict[u]
                cn, newACL, summary = setDict[u]
                updateinviteDict[u] = (cn, removeACL, newACL, summary)
                del removeDict[u]
                del setDict[u]
            for userid, access in removeDict.iteritems():
                result = (yield self.uninviteUIDFromShare(userid, access, request))
                # If result is False that means the user being removed was not
                # actually invited, but let's not return an error in this case.
                okusers.add(userid)
            for userid, (cn, access, summary) in setDict.iteritems():
                result = (yield self.inviteUIDToShare(userid, cn, access, summary, request))
                (okusers if result else badusers).add(userid)
            for userid, (cn, removeACL, newACL, summary) in updateinviteDict.iteritems():
                result = (yield self.inviteUserUpdateToShare(userid, cn, removeACL, newACL, summary, request))
                (okusers if result else badusers).add(userid)

            # In this case bad items do not prevent ok items from being processed
            ok_code = responsecode.OK
        else:
            # In this case a bad item causes all ok items not to be processed so failed dependency is returned
            ok_code = responsecode.FAILED_DEPENDENCY

        # Do a final validation of the entire set of invites
        invites = (yield self.validateInvites(request))
        numRecords = len(invites)

        # Set the sharing state on the collection
        shared = self.isSharedByOwner()
        if shared and numRecords == 0:
            yield self.downgradeFromShare(request)
        elif not shared and numRecords != 0:
            yield self.upgradeToShare()

        # Create the multistatus response - only needed if some are bad
        if badusers:
            xml_responses = []
            xml_responses.extend([
                element.StatusResponse(element.HRef(userid), element.Status.fromResponseCode(ok_code))
                for userid in sorted(okusers)
            ])
            xml_responses.extend([
                element.StatusResponse(element.HRef(userid), element.Status.fromResponseCode(responsecode.FORBIDDEN))
                for userid in sorted(badusers)
            ])

            #
            # Return response
            #
            returnValue(MultiStatusResponse(xml_responses))
        else:
            returnValue(responsecode.OK)

    @inlineCallbacks
    def _xmlHandleInviteReply(self, request, docroot):
        # Sharing must be enabled for this collection
        if not self.canShare():
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (customxml.calendarserver_namespace, "valid-request"),
                "Sharing not supported on this resource",
            ))
        yield self.authorize(request, (element.Read(), element.Write()))
        result = (yield self._handleInviteReply(request, docroot))
        returnValue(result)

    def _handleInviteReply(self, request, docroot):
        raise NotImplementedError

    @inlineCallbacks
    def xmlRequestHandler(self, request):

        # Need to read the data and get the root element first
        xmldata = (yield allDataFromStream(request.stream))
        try:
            doc = element.WebDAVDocument.fromString(xmldata)
        except ValueError, e:
            self.log.error("Error parsing doc ({ex}) Doc:\n {x}", ex=str(e), x=xmldata)
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (customxml.calendarserver_namespace, "valid-request"),
                "Invalid XML",
            ))

        root = doc.root_element
        if type(root) in self.xmlDocHandlers:
            result = (yield self.xmlDocHandlers[type(root)](self, request, root))
            returnValue(result)
        else:
            self.log.error("Unsupported XML ({r})", r=root)
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (customxml.calendarserver_namespace, "valid-request"),
                "Unsupported XML",
            ))

    xmlDocHandlers = {
        customxml.InviteShare: _xmlHandleInvite,
        customxml.InviteReply: _xmlHandleInviteReply,
    }

    def isGroup(self):
        try:
            return self._newStoreObject._kind == _ABO_KIND_GROUP
        except AttributeError:
            return False

    def POST_handler_content_type(self, request, contentType):
        if self.isCollection() or self.isGroup():
            if contentType:
                if contentType in self._postHandlers:
                    return self._postHandlers[contentType](self, request)
                else:
                    self.log.info("Got a POST on collection or group with an unsupported content type: {t}", t=contentType)
            else:
                self.log.info("Got a POST on collection or group with no content type")
        return succeed(responsecode.FORBIDDEN)

    _postHandlers = {
        ("application", "xml"): xmlRequestHandler,
        ("text", "xml"): xmlRequestHandler,
    }


invitationBindStatusToXMLMap = {
    _BIND_STATUS_INVITED: customxml.InviteStatusNoResponse,
    _BIND_STATUS_ACCEPTED: customxml.InviteStatusAccepted,
    _BIND_STATUS_DECLINED: customxml.InviteStatusDeclined,
    _BIND_STATUS_INVALID: customxml.InviteStatusInvalid,
    _BIND_STATUS_DELETED: customxml.InviteStatusDeleted,
}
invitationBindStatusFromXMLMap = dict((v, k) for k, v in invitationBindStatusToXMLMap.iteritems())

invitationBindModeToXMLMap = {
    _BIND_MODE_READ: customxml.ReadAccess,
    _BIND_MODE_WRITE: customxml.ReadWriteAccess,
}
invitationBindModeFromXMLMap = dict((v, k) for k, v in invitationBindModeToXMLMap.iteritems())

invitationAccessFromBindModeMap = {
    _BIND_MODE_OWN: "own",
    _BIND_MODE_READ: "read-only",
    _BIND_MODE_WRITE: "read-write",
    _BIND_MODE_DIRECT: "read-write",
    _BIND_MODE_INDIRECT: "read-write",
}


class SharedHomeMixin(LinkFollowerMixIn):
    """
    A mix-in for calendar/addressbook homes that defines the operations for
    manipulating a sharee's set of shared calendars.
    """

    @inlineCallbacks
    def provisionShare(self, child, request=None):
        """
        Set shared state and check access control.
        """
        if child._newStoreObject is not None and not child._newStoreObject.owned():
            ownerHomeURL = (yield self._otherPrincipalHomeURL(child._newStoreObject.ownerHome().uid()))
            ownerView = yield child._newStoreObject.ownerView()
            child.setShare(joinURL(ownerHomeURL, ownerView.name()) if ownerHomeURL else None)
            access = yield child._checkAccessControl()
            if access is None:
                returnValue(None)
        returnValue(child)

    def _otherPrincipalHomeURL(self, otherUID):
        # Is this only meant to be overridden?
        pass

    @inlineCallbacks
    def acceptShare(self, request, inviteUID, summary):

        # Accept the share
        try:
            shareeView = yield self._newStoreHome.acceptShare(inviteUID, summary)
        except DirectoryRecordNotFoundError:
            # Missing sharer record => fail request
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "invalid-share"),
                "Invite UID not valid",
            ))
        if shareeView is None:
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "invalid-share"),
                "Invite UID not valid",
            ))

        # Return the URL of the shared collection
        sharedAsURL = joinURL(self.url(), shareeView.shareName())
        returnValue(XMLResponse(
            code=responsecode.OK,
            element=customxml.SharedAs(
                element.HRef.fromString(sharedAsURL)
            )
        ))

    @inlineCallbacks
    def declineShare(self, request, inviteUID):

        # Remove it if it is in the DB
        try:
            result = yield self._newStoreHome.declineShare(inviteUID)
        except DirectoryRecordNotFoundError:
            # Missing sharer record => just treat decline as success
            result = True
        if not result:
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (calendarserver_namespace, "invalid-share"),
                "Invite UID not valid",
            ))
        returnValue(Response(code=responsecode.NO_CONTENT))

    def _handleInviteReply(self, request, invitereplydoc):
        """
        Handle a user accepting or declining a sharing invite
        """
        hostUrl = None
        accepted = None
        summary = None
        replytoUID = None
        for item in invitereplydoc.children:
            if isinstance(item, customxml.InviteStatusAccepted):
                accepted = True
            elif isinstance(item, customxml.InviteStatusDeclined):
                accepted = False
            elif isinstance(item, customxml.InviteSummary):
                summary = str(item)
            elif isinstance(item, customxml.HostURL):
                for hosturlItem in item.children:
                    if isinstance(hosturlItem, element.HRef):
                        hostUrl = str(hosturlItem)
            elif isinstance(item, customxml.InReplyTo):
                replytoUID = str(item)

        if accepted is None or hostUrl is None or replytoUID is None:
            raise HTTPError(ErrorResponse(
                responsecode.FORBIDDEN,
                (customxml.calendarserver_namespace, "valid-request"),
                "Missing required XML elements",
            ))
        if accepted:
            return self.acceptShare(request, replytoUID, summary=summary)
        else:
            return self.declineShare(request, replytoUID)
