Skip to content
Permalink
Browse files

CardDAV global addressbook support. Some addition CardDAV specific cl…

…ean-up.

git-svn-id: https://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk@5482 e27351fd-9f3e-4f54-a53b-843176b1656c
  • Loading branch information...
cyrusdaboo committed Apr 15, 2010
1 parent 17e87c4 commit 0d21ac3c7fcc253c5b256cfe16b54a6ed6b72874
@@ -344,7 +344,7 @@ def determineAppropriateGroupID():



class SocketGroupOwnership(BaseTestCase):
class SocketGroupOwnership(TestCase):
"""
Tests for L{GroupOwnedUNIXServer}.
"""
@@ -25,7 +25,7 @@
from time import sleep

from twisted.python.reflect import namedClass
from twisted.internet import reactor
from twisted.internet.reactor import addSystemEventTrigger
from twisted.cred.portal import Portal
from twext.web2.http_headers import Headers
from twext.web2.dav import auth
@@ -47,7 +47,8 @@
from twistedcaldav.notify import installNotificationClient
from twistedcaldav.resource import CalDAVResource, AuthenticationWrapper
from twistedcaldav.simpleresource import SimpleResource
from twistedcaldav.static import CalendarHomeProvisioningFile
from twistedcaldav.static import CalendarHomeProvisioningFile,\
GlobalAddressBookFile
from twistedcaldav.static import IScheduleInboxFile
from twistedcaldav.static import TimezoneServiceFile
from twistedcaldav.static import AddressBookHomeProvisioningFile, DirectoryBackedAddressBookFile
@@ -91,6 +92,7 @@ def getRootResource(config, resources=None):
webAdminResourceClass = WebAdminResource
addressBookResourceClass = AddressBookHomeProvisioningFile
directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookFile
globalAddressBookResourceClass = GlobalAddressBookFile

#
# Setup the Directory
@@ -290,10 +292,7 @@ def getRootResource(config, resources=None):
directoryPath,
principalCollections=(principalCollection,)
)
# do this after process is owned by carddav user, not root. XXX
# this should be fixed to execute at a different stage of service
# startup entirely.
reactor.callLater(1.0, directoryBackedAddressBookCollection.provisionDirectory)
addSystemEventTrigger("after", "startup", directoryBackedAddressBookCollection.provisionDirectory)
else:
# remove /directory from previous runs that may have created it
try:
@@ -303,6 +302,16 @@ def getRootResource(config, resources=None):
if e.errno != errno.ENOENT:
log.error("Could not delete: %s : %r" % (directoryPath, e,))

if config.GlobalAddressBook.Enabled:
log.info("Setting up global address book collection: %r" % (globalAddressBookResourceClass,))

globalAddressBookCollection = globalAddressBookResourceClass(
os.path.join(config.DocumentRoot, config.GlobalAddressBook.Name),
principalCollections=(principalCollection,)
)
if not globalAddressBookCollection.exists():
addSystemEventTrigger("after", "startup", globalAddressBookCollection.createAddressBookCollection)

log.info("Setting up root resource: %r" % (rootResourceClass,))

root = rootResourceClass(
@@ -322,6 +331,8 @@ def getRootResource(config, resources=None):
root.putChild('addressbooks', addressBookCollection)
if config.DirectoryAddressBook.Enabled:
root.putChild(config.DirectoryAddressBook.name, directoryBackedAddressBookCollection)
if config.GlobalAddressBook.Enabled:
root.putChild(config.GlobalAddressBook.Name, globalAddressBookCollection)

# /.well-known
if config.EnableWellKnown:
@@ -71,6 +71,7 @@
SERVICE_UNAVAILABLE = 503
GATEWAY_TIMEOUT = 504
HTTP_VERSION_NOT_SUPPORTED = 505
LOOP_DETECTED = 506
INSUFFICIENT_STORAGE_SPACE = 507
NOT_EXTENDED = 510

@@ -116,7 +117,7 @@
REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large",
REQUEST_URI_TOO_LONG: "Request-URI Too Long",
UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type",
REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable",
REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range Not Satisfiable",
EXPECTATION_FAILED: "Expectation Failed",
UNPROCESSABLE_ENTITY: "Unprocessable Entity",
LOCKED: "Locked",
@@ -128,7 +129,8 @@
BAD_GATEWAY: "Bad Gateway",
SERVICE_UNAVAILABLE: "Service Unavailable",
GATEWAY_TIMEOUT: "Gateway Time-out",
HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported",
HTTP_VERSION_NOT_SUPPORTED: "HTTP Version Not Supported",
LOOP_DETECTED: "Loop In Linked or Bound Resource",
INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space",
NOT_EXTENDED: "Not Extended"
}
@@ -151,7 +151,7 @@ class SupportedAddressData (CardDAVElement):
hidden = True
protected = True

allowed_children = { (carddav_namespace, "addressbook-data"): (0, None) }
allowed_children = { (carddav_namespace, "address-data-type"): (0, None) }

class MaxResourceSize (CardDAVTextElement):
"""
@@ -221,6 +221,19 @@ def __init__(self, *children, **attributes):
self.filter = filter
self.limit = limit

class AddressDataType (CardDAVEmptyElement):
"""
Defines which parts of a address component object should be returned by a
report.
(CardDAV, section 6.2.2)
"""
name = "address-data-type"

allowed_attributes = {
"content-type": False,
"version" : False,
}

class AddressData (CardDAVElement):
"""
Defines which parts of a address component object should be returned by a
@@ -28,7 +28,7 @@
from twext.web2.dav.element.base import twisted_private_namespace
from twext.web2.dav import davxml

from twistedcaldav import caldavxml
from twistedcaldav import caldavxml, carddavxml
from twistedcaldav.ical import Component as iComponent

from vobject.icalendar import utc
@@ -891,4 +891,6 @@ class NotificationType (davxml.WebDAVElement):
davxml.ResourceType.ischeduleinbox = davxml.ResourceType(IScheduleInbox())
davxml.ResourceType.freebusyurl = davxml.ResourceType(FreeBusyURL())
davxml.ResourceType.notification = davxml.ResourceType(davxml.Collection(), Notification())
davxml.ResourceType.sharedcalendar = davxml.ResourceType(davxml.Collection(), caldavxml.Calendar(), SharedOwner())
davxml.ResourceType.sharedownercalendar = davxml.ResourceType(davxml.Collection(), caldavxml.Calendar(), SharedOwner())
davxml.ResourceType.sharedcalendar = davxml.ResourceType(davxml.Collection(), caldavxml.Calendar(), Shared())
davxml.ResourceType.sharedaddressbook = davxml.ResourceType(davxml.Collection(), carddavxml.AddressBook(), Shared())
@@ -25,6 +25,7 @@
"DirectoryAddressBookHomeTypeProvisioningResource",
"DirectoryAddressBookHomeUIDProvisioningResource",
"DirectoryAddressBookHomeResource",
"GlobalAddressBookResource",
]

from twext.python.log import Logger
@@ -377,3 +378,49 @@ def quotaRoot(self, request):
is quota-controlled, or C{None} if not quota controlled.
"""
return config.UserQuota if config.UserQuota != 0 else None

class GlobalAddressBookResource (CalDAVResource):
"""
Global address book. All we care about is making sure permissions are setup.
"""

def resourceType(self, request):
return succeed(davxml.ResourceType.sharedaddressbook)

def url(self):
return joinURL("/", config.GlobalAddressBook.Name, "/")

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

def defaultAccessControlList(self):

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

if config.GlobalAddressBook.EnableAnonymousReadAccess:
aces += (
davxml.ACE(
davxml.Principal(davxml.Unauthenticated()),
davxml.Grant(
davxml.Privilege(davxml.Read()),
),
davxml.Protected(),
TwistedACLInheritable(),
),
)
return davxml.ACL(*aces)

def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
# Permissions here are fixed, and are not subject to inheritance rules, etc.
return succeed(self.defaultAccessControlList())
@@ -41,7 +41,7 @@
from twistedcaldav.dropbox import DropBoxHomeResource
from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource
from twistedcaldav.freebusyurl import FreeBusyURLResource
from twistedcaldav.resource import CalDAVResource
from twistedcaldav.resource import CalDAVResource, CalDAVComplianceMixIn
from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
from twistedcaldav.directory.idirectory import IDirectoryService
from twistedcaldav.directory.wiki import getWikiACL
@@ -57,6 +57,7 @@
class DirectoryCalendarProvisioningResource (
AutoProvisioningResourceMixIn,
ReadOnlyResourceMixIn,
CalDAVComplianceMixIn,
DAVResource,
):
def defaultAccessControlList(self):
@@ -0,0 +1,114 @@
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

from twext.python.log import LoggingMixIn

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

from twistedcaldav.resource import CalDAVComplianceMixIn
from twext.web2.http import HTTPError
from twext.web2 import responsecode
from twext.web2.resource import WrapperResource

__all__ = [
"LinkResource",
]

"""
A resource that is a soft-link to another.
"""

class LinkResource(CalDAVComplianceMixIn, WrapperResource, LoggingMixIn):
"""
This is similar to a WrapperResource except that we locate our resource dynamically.
"""

def __init__(self, parent, link_url):
self.parent = parent
self.linkURL = link_url
super(LinkResource, self).__init__(self.parent.principalCollections())

@inlineCallbacks
def linkedResource(self, request):

if not hasattr(self, "_linkedResource"):
self._linkedResource = (yield request.locateResource(self.linkURL))

if self._linkedResource is None:
raise HTTPError(responsecode.NOT_FOUND)

returnValue(self._linkedResource)

def isCollection(self):
return True

@inlineCallbacks
def resourceType(self, request):
hosted = (yield self.linkedResource(request))
result = (yield hosted.resourceType(request))
returnValue(result)

def locateChild(self, request, segments):

def _defer(result):
return (result, segments)
d = self.linkedResource(request)
d.addCallback(_defer)
return d

def renderHTTP(self, request):
return self.linkedResource(request)

def getChild(self, name):
return self._hostedResource.getChild(name)

@inlineCallbacks
def hasProperty(self, property, request):
hosted = (yield self.linkedResource(request))
result = (yield hosted.hasProperty(property, request))
returnValue(result)

@inlineCallbacks
def readProperty(self, property, request):
hosted = (yield self.linkedResource(request))
result = (yield hosted.readProperty(property, request))
returnValue(result)

@inlineCallbacks
def writeProperty(self, property, request):
hosted = (yield self.linkedResource(request))
result = (yield hosted.writeProperty(property, request))
returnValue(result)

class LinkFollowerMixIn(object):

@inlineCallbacks
def locateChild(self, req, segments):

resource, path = (yield maybeDeferred(super(LinkFollowerMixIn, self).locateChild, req, segments))
MAX_LINK_DEPTH = 10
ctr = 0
seenResource = set()
while isinstance(resource, LinkResource):
seenResource.add(resource)
ctr += 1
resource = (yield resource.linkedResource(req))

if ctr > MAX_LINK_DEPTH or resource in seenResource:
raise HTTPError(responsecode.LOOP_DETECTED)

returnValue((resource, path))

@@ -160,10 +160,22 @@ def _defer(data):
def liveProperties(self):
baseProperties = (
davxml.Owner.qname(), # Private Events needs this but it is also OK to return empty
caldavxml.SupportedCalendarComponentSet.qname(),
caldavxml.SupportedCalendarData.qname(),
)

if self.isPseudoCalendarCollection():
baseProperties += (
caldavxml.SupportedCalendarComponentSet.qname(),
caldavxml.SupportedCalendarData.qname(),
)

if self.isAddressBookCollection():
baseProperties += (
carddavxml.SupportedAddressData.qname(),
)

if config.EnableSyncReport and (self.isPseudoCalendarCollection() or self.isAddressBookCollection()):
baseProperties += (davxml.SyncToken.qname(),)

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

@@ -285,6 +297,11 @@ def _readGlobalProperty(self, qname, property, request):
owner = (yield self.owner(request))
returnValue(davxml.Owner(owner))

elif qname == davxml.SyncToken.qname() and config.EnableSyncReport and (
self.isPseudoCalendarCollection() or self.isAddressBookCollection()
):
returnValue(davxml.SyncToken.fromString(self.getSyncToken()))

elif qname == davxml.AddMember.qname() and config.EnableAddMember and (
self.isCalendarCollection() or self.isAddressBookCollection()
):
@@ -331,6 +348,15 @@ def _readGlobalProperty(self, qname, property, request):
opaque = url in fbset
self.writeDeadProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Opaque() if opaque else caldavxml.Transparent()))

elif qname == carddavxml.SupportedAddressData.qname():
# CardDAV, section 6.2.2
returnValue(carddavxml.SupportedAddressData(
carddavxml.AddressDataType(**{
"content-type": "text/vcard",
"version" : "3.0",
}),
))

elif qname == customxml.Invite.qname():
result = (yield self.inviteProperty(request))
returnValue(result)

0 comments on commit 0d21ac3

Please sign in to comment.
You can’t perform that action at this time.