2
# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
4
# Licensed under the Apache License, Version 2.0 (the "License");
5
# you may not use this file except in compliance with the License.
6
# You may obtain a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
16
# DRI: Wilfredo Sanchez, wsanchez@apple.com
20
CalDAV-aware resources.
24
"CalDAVComplianceMixIn",
26
"CalendarPrincipalCollectionResource",
27
"CalendarPrincipalResource",
28
"isCalendarCollectionResource",
29
"isPseudoCalendarCollectionResource",
32
from zope.interface import implements
34
from twisted.internet import reactor
35
from twisted.internet.defer import Deferred, maybeDeferred, succeed
36
from twisted.internet.defer import waitForDeferred
37
from twisted.internet.defer import deferredGenerator
38
from twisted.web2 import responsecode
39
from twisted.web2.dav import davxml
40
from twisted.web2.dav.idav import IDAVPrincipalCollectionResource
41
from twisted.web2.dav.resource import AccessDeniedError, DAVPrincipalCollectionResource
42
from twisted.web2.dav.davxml import dav_namespace
43
from twisted.web2.dav.http import ErrorResponse
44
from twisted.web2.dav.resource import TwistedACLInheritable
45
from twisted.web2.dav.util import joinURL, parentForURL, unimplemented
46
from twisted.web2.http import HTTPError, RedirectResponse, StatusResponse, Response
47
from twisted.web2.http_headers import MimeType
48
from twisted.web2.iweb import IResponse
49
from twisted.web2.stream import MemoryStream
50
import twisted.web2.server
53
from twistedcaldav import caldavxml, customxml
54
from twistedcaldav.config import config
55
from twistedcaldav.customxml import TwistedCalendarAccessProperty
56
from twistedcaldav.extensions import DAVResource, DAVPrincipalResource
57
from twistedcaldav.ical import Component
58
from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
59
from twistedcaldav.caldavxml import caldav_namespace
60
from twistedcaldav.customxml import calendarserver_namespace
61
from twistedcaldav.ical import allowedComponents
62
from twistedcaldav.ical import Component as iComponent
64
if twistedcaldav.__version__:
65
serverVersion = twisted.web2.server.VERSION + " TwistedCalDAV/" + twistedcaldav.__version__
67
serverVersion = twisted.web2.server.VERSION + " TwistedCalDAV/?"
69
class CalDAVComplianceMixIn(object):
71
def davComplianceClasses(self):
72
extra_compliance = caldavxml.caldav_compliance
73
if config.EnableProxyPrincipals:
74
extra_compliance += customxml.calendarserver_proxy_compliance
75
if config.EnablePrivateEvents:
76
extra_compliance += customxml.calendarserver_private_events_compliance
77
return tuple(super(CalDAVComplianceMixIn, self).davComplianceClasses()) + extra_compliance
80
class CalDAVResource (CalDAVComplianceMixIn, DAVResource):
84
Extends L{DAVResource} to provide CalDAV functionality.
86
implements(ICalDAVResource)
92
def render(self, request):
93
# Send listing instead of iCalendar data to HTML agents
94
# This is mostly useful for debugging...
95
# FIXME: Add a self-link to the dirlist with a query string so
96
# users can still download the actual iCalendar data?
97
agent = request.headers.getHeader("user-agent")
98
if agent is not None and agent.startswith("Mozilla/") and agent.find("Gecko") != -1:
103
if not html_agent and self.isPseudoCalendarCollection():
104
# Render a monolithic iCalendar file
105
if request.uri[-1] != "/":
106
# Redirect to include trailing '/' in URI
107
return RedirectResponse(request.unparseURL(path=request.path+"/"))
110
response = Response()
111
response.stream = MemoryStream(str(data))
112
response.headers.setHeader("content-type", MimeType.fromString("text/calendar"))
115
d = self.iCalendarRolledup(request)
116
d.addCallback(_defer)
119
return super(CalDAVResource, self).render(request)
121
def renderHTTP(self, request):
122
response = maybeDeferred(super(CalDAVResource, self).renderHTTP, request)
124
def setHeaders(response):
125
response = IResponse(response)
126
response.headers.setHeader("server", serverVersion)
130
response.addCallback(setHeaders)
138
liveProperties = DAVResource.liveProperties + (
139
(dav_namespace, "owner"), # Private Events needs this but it is also OK to return empty
140
(caldav_namespace, "supported-calendar-component-set"),
141
(caldav_namespace, "supported-calendar-data" ),
144
supportedCalendarComponentSet = caldavxml.SupportedCalendarComponentSet(
145
*[caldavxml.CalendarComponent(name=item) for item in allowedComponents]
148
def readProperty(self, property, request):
149
if type(property) is tuple:
152
qname = property.qname()
154
namespace, name = qname
156
if namespace == dav_namespace:
158
d = self.owner(request)
159
d.addCallback(lambda x: davxml.Owner(x))
162
elif namespace == caldav_namespace:
163
if name == "supported-calendar-component-set":
164
# CalDAV-access-09, section 5.2.3
165
if self.hasDeadProperty(qname):
166
return succeed(self.readDeadProperty(qname))
167
return succeed(self.supportedCalendarComponentSet)
168
elif name == "supported-calendar-data":
169
# CalDAV-access-09, section 5.2.4
170
return succeed(caldavxml.SupportedCalendarData(
171
caldavxml.CalendarData(**{
172
"content-type": "text/calendar",
176
elif name == "max-resource-size":
177
# CalDAV-access-15, section 5.2.5
178
if config.MaximumAttachmentSize:
179
return succeed(caldavxml.MaxResourceSize.fromString(
180
str(config.MaximumAttachmentSize)
183
return super(CalDAVResource, self).readProperty(property, request)
185
def writeProperty(self, property, request):
186
assert isinstance(property, davxml.WebDAVElement)
188
if property.qname() == (caldav_namespace, "supported-calendar-component-set"):
189
if not self.isPseudoCalendarCollection():
190
raise HTTPError(StatusResponse(
191
responsecode.FORBIDDEN,
192
"Property %s may only be set on calendar collection." % (property,)
194
for component in property.children:
195
if component not in self.supportedCalendarComponentSet:
196
raise HTTPError(StatusResponse(
197
responsecode.NOT_IMPLEMENTED,
198
"Component %s is not supported by this server" % (component.toxml(),)
201
# Strictly speaking CalDAV:timezone is a live property in the sense that the
202
# server enforces what can be stored, however it need not actually
203
# exist so we cannot list it in liveProperties on this resource, since its
204
# its presence there means that hasProperty will always return True for it.
205
elif property.qname() == (caldav_namespace, "calendar-timezone"):
206
if not self.isCalendarCollection():
207
raise HTTPError(StatusResponse(
208
responsecode.FORBIDDEN,
209
"Property %s may only be set on calendar collection." % (property,)
211
if not property.valid():
212
raise HTTPError(ErrorResponse(
213
responsecode.CONFLICT,
214
(caldav_namespace, "valid-calendar-data")
217
return super(CalDAVResource, self).writeProperty(property, request)
223
def disable(self, disabled=True):
225
Completely disables all access to this resource, regardless of ACL
227
@param disabled: If true, disabled all access. If false, enables access.
230
self.writeDeadProperty(AccessDisabled())
232
self.removeDeadProperty(AccessDisabled())
234
def isDisabled(self):
236
@return: C{True} if access to this resource is disabled, C{False}
239
return self.hasDeadProperty(AccessDisabled)
241
# FIXME: Perhaps this is better done in authorize() instead.
243
def accessControlList(self, request, *args, **kwargs):
244
if self.isDisabled():
248
d = waitForDeferred(super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
252
# Look for private events access classification
253
if self.hasDeadProperty(TwistedCalendarAccessProperty):
254
access = self.readDeadProperty(TwistedCalendarAccessProperty)
255
if access.getValue() in (Component.ACCESS_PRIVATE, Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED,):
256
# Need to insert ACE to prevent non-owner principals from seeing this resource
257
d = waitForDeferred(self.owner(request))
259
owner = d.getResult()
260
if access.getValue() == Component.ACCESS_PRIVATE:
263
davxml.Principal(owner),
278
davxml.Principal(owner),
288
acls = davxml.ACL(ace, *acls.children)
292
def owner(self, request):
294
Return the DAV:owner property value (MUST be a DAV:href or None).
296
d = waitForDeferred(self.locateParent(request, request.urlForResource(self)))
298
parent = d.getResult()
299
if parent and isinstance(parent, CalDAVResource):
300
d = waitForDeferred(parent.owner(request))
307
def isOwner(self, request):
309
Determine whether the DAV:owner of this resource matches the currently authorized principal
313
d = waitForDeferred(self.owner(request))
315
owner = d.getResult()
316
result = (davxml.Principal(owner) == self.currentPrincipal(request))
323
def isCalendarCollection(self):
325
See L{ICalDAVResource.isCalendarCollection}.
327
return self.isSpecialCollection(caldavxml.Calendar)
329
def isSpecialCollection(self, collectiontype):
331
See L{ICalDAVResource.isSpecialCollection}.
333
if not self.isCollection(): return False
336
resourcetype = self.readDeadProperty((dav_namespace, "resourcetype"))
337
return bool(resourcetype.childrenOfType(collectiontype))
339
assert e.response.code == responsecode.NOT_FOUND
342
def isPseudoCalendarCollection(self):
344
See L{ICalDAVResource.isPseudoCalendarCollection}.
346
return self.isCalendarCollection()
348
def findCalendarCollections(self, depth, request, callback, privileges=None):
350
See L{ICalDAVResource.findCalendarCollections}.
352
assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
354
def checkPrivilegesError(failure):
355
failure.trap(AccessDeniedError)
357
reactor.callLater(0, getChild)
359
def checkPrivileges(child):
360
if privileges is None:
363
ca = child.checkPrivileges(request, privileges)
364
ca.addCallback(lambda ign: child)
367
def gotChild(child, childpath):
368
if child.isCalendarCollection():
369
callback(child, childpath)
370
elif child.isCollection():
371
if depth == "infinity":
372
fc = child.findCalendarCollections(depth, request, callback, privileges)
373
fc.addCallback(lambda x: reactor.callLater(0, getChild))
376
reactor.callLater(0, getChild)
380
childname = children.pop()
382
completionDeferred.callback(None)
384
childpath = joinURL(basepath, childname)
385
child = request.locateResource(childpath)
386
child.addCallback(checkPrivileges)
387
child.addCallbacks(gotChild, checkPrivilegesError, (childpath,))
388
child.addErrback(completionDeferred.errback)
390
completionDeferred = Deferred()
392
if depth != "0" and self.isCollection():
393
basepath = request.urlForResource(self)
394
children = self.listChildren()
397
completionDeferred.callback(None)
399
return completionDeferred
401
def createCalendar(self, request):
403
See L{ICalDAVResource.createCalendar}.
404
This implementation raises L{NotImplementedError}; a subclass must
409
def iCalendar(self, name=None):
411
See L{ICalDAVResource.iCalendar}.
413
This implementation returns the an object created from the data returned
414
by L{iCalendarText} when given the same arguments.
416
Note that L{iCalendarText} by default calls this method, which creates
417
an infinite loop. A subclass must override one of both of these
420
calendar_data = self.iCalendarText(name)
422
if calendar_data is None: return None
425
return iComponent.fromString(calendar_data)
429
def iCalendarRolledup(self, request):
431
See L{ICalDAVResource.iCalendarRolledup}.
433
This implementation raises L{NotImplementedError}; a subclass must
438
def iCalendarText(self, name=None):
440
See L{ICalDAVResource.iCalendarText}.
442
This implementation returns the string representation (according to
443
L{str}) of the object returned by L{iCalendar} when given the same
446
Note that L{iCalendar} by default calls this method, which creates
447
an infinite loop. A subclass must override one of both of these
450
return str(self.iCalendar(name))
452
def iCalendarXML(self, name=None):
454
See L{ICalDAVResource.iCalendarXML}.
455
This implementation returns an XML element constructed from the object
456
returned by L{iCalendar} when given the same arguments.
458
return caldavxml.CalendarData.fromCalendar(self.iCalendar(name))
460
def principalForCalendarUserAddress(self, address):
461
for principalCollection in self.principalCollections():
462
principal = principalCollection.principalForCalendarUserAddress(address)
463
if principal is not None:
467
def supportedReports(self):
468
result = super(CalDAVResource, self).supportedReports()
469
result.append(davxml.Report(caldavxml.CalendarQuery(),))
470
result.append(davxml.Report(caldavxml.CalendarMultiGet(),))
471
if (self.isCollection()):
472
# Only allowed on collections
473
result.append(davxml.Report(caldavxml.FreeBusyQuery(),))
476
def writeNewACEs(self, newaces):
478
Write a new ACL to the resource's property store. We override this for calendar collections
479
and force all the ACEs to be inheritable so that all calendar object resources within the
480
calendar collection have the same privileges unless explicitly overridden. The same applies
481
to drop box collections as we want all resources (attachments) to have the same privileges as
482
the drop box collection.
484
@param newaces: C{list} of L{ACE} for ACL being set.
487
# Do this only for regular calendar collections and Inbox/Outbox
488
if self.isPseudoCalendarCollection():
491
if TwistedACLInheritable() not in ace.children:
492
children = list(ace.children)
493
children.append(TwistedACLInheritable())
494
edited_aces.append(davxml.ACE(*children))
496
edited_aces.append(ace)
498
edited_aces = newaces
500
# Do inherited with possibly modified set of aces
501
super(CalDAVResource, self).writeNewACEs(edited_aces)
507
def locateParent(self, request, uri):
509
Locates the parent resource of the resource with the given URI.
510
@param request: an L{IRequest} object for the request being processed.
511
@param uri: the URI whose parent resource is desired.
513
return request.locateResource(parentForURL(uri))
515
class CalendarPrincipalCollectionResource (DAVPrincipalCollectionResource, CalDAVResource):
517
CalDAV principal collection.
519
implements(IDAVPrincipalCollectionResource)
521
def isCollection(self):
524
def isCalendarCollection(self):
527
def isPseudoCalendarCollection(self):
530
def principalForCalendarUserAddress(self, address):
533
def supportedReports(self):
535
Principal collections are the only resources supporting the
536
principal-search-property-set report.
538
result = super(CalendarPrincipalCollectionResource, self).supportedReports()
539
result.append(davxml.Report(davxml.PrincipalSearchPropertySet(),))
542
def principalSearchPropertySet(self):
543
return davxml.PrincipalSearchPropertySet(
544
davxml.PrincipalSearchProperty(
545
davxml.PropertyContainer(
549
davxml.PCDATAElement("Display Name"),
553
davxml.PrincipalSearchProperty(
554
davxml.PropertyContainer(
555
caldavxml.CalendarUserAddressSet()
558
davxml.PCDATAElement("Calendar User Addresses"),
564
class CalendarPrincipalResource (CalDAVComplianceMixIn, DAVPrincipalResource):
566
CalDAV principal resource.
568
Extends L{DAVPrincipalResource} to provide CalDAV functionality.
570
implements(ICalendarPrincipalResource)
572
liveProperties = tuple(DAVPrincipalResource.liveProperties) + (
573
(caldav_namespace, "calendar-home-set" ),
574
(caldav_namespace, "calendar-user-address-set"),
575
(caldav_namespace, "schedule-inbox-URL" ),
576
(caldav_namespace, "schedule-outbox-URL" ),
580
def enableDropBox(clz, enable):
581
qname = (calendarserver_namespace, "dropbox-home-URL" )
582
if enable and qname not in clz.liveProperties:
583
clz.liveProperties += (qname,)
584
elif not enable and qname in clz.liveProperties:
585
clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
588
def enableNotifications(clz, enable):
589
qname = (calendarserver_namespace, "notifications-URL" )
590
if enable and qname not in clz.liveProperties:
591
clz.liveProperties += (qname,)
592
elif not enable and qname in clz.liveProperties:
593
clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
595
def isCollection(self):
598
def readProperty(self, property, request):
600
if type(property) is tuple:
603
qname = property.qname()
605
namespace, name = qname
607
if namespace == caldav_namespace:
608
if name == "calendar-home-set":
609
return caldavxml.CalendarHomeSet(
610
*[davxml.HRef(url) for url in self.calendarHomeURLs()]
613
if name == "calendar-user-address-set":
614
return succeed(caldavxml.CalendarUserAddressSet(
615
*[davxml.HRef(uri) for uri in self.calendarUserAddresses()]
618
if name == "schedule-inbox-URL":
619
url = self.scheduleInboxURL()
623
return caldavxml.ScheduleInboxURL(davxml.HRef(url))
625
if name == "schedule-outbox-URL":
626
url = self.scheduleOutboxURL()
630
return caldavxml.ScheduleOutboxURL(davxml.HRef(url))
632
elif namespace == calendarserver_namespace:
633
if name == "dropbox-home-URL" and config.EnableDropBox:
634
url = self.dropboxURL()
638
return customxml.DropBoxHomeURL(davxml.HRef(url))
640
if name == "notifications-URL" and config.EnableNotifications:
641
url = self.notificationsURL()
645
return customxml.NotificationsURL(davxml.HRef(url))
647
return super(CalendarPrincipalResource, self).readProperty(property, request)
649
return maybeDeferred(defer)
651
def groupMembers(self):
654
def groupMemberships(self):
657
def calendarHomeURLs(self):
658
if self.hasDeadProperty((caldav_namespace, "calendar-home-set")):
659
home_set = self.readDeadProperty((caldav_namespace, "calendar-home-set"))
660
return [str(h) for h in home_set.children]
664
def calendarUserAddresses(self):
665
if self.hasDeadProperty((caldav_namespace, "calendar-user-address-set")):
666
addresses = self.readDeadProperty((caldav_namespace, "calendar-user-address-set"))
667
return [str(h) for h in addresses.children]
669
# Must have a valid address of some kind so use the principal uri
670
return (self.principalURL(),)
672
def calendarFreeBusyURIs(self, request):
677
def getFreeBusy(has):
681
def parseFreeBusy(freeBusySet):
682
return tuple(str(href) for href in freeBusySet.children)
684
d = inbox.readProperty((caldav_namespace, "calendar-free-busy-set"), request)
685
d.addCallback(parseFreeBusy)
688
d = inbox.hasProperty((caldav_namespace, "calendar-free-busy-set"), request)
689
d.addCallback(getFreeBusy)
692
d = self.scheduleInbox(request)
693
d.addCallback(gotInbox)
696
def scheduleInbox(self, request):
698
@return: the deferred schedule inbox for this principal.
700
return request.locateResource(self.scheduleInboxURL())
702
def scheduleInboxURL(self):
703
if self.hasDeadProperty((caldav_namespace, "schedule-inbox-URL")):
704
inbox = self.readDeadProperty((caldav_namespace, "schedule-inbox-URL"))
705
return str(inbox.children[0])
709
def scheduleOutboxURL(self):
711
@return: the schedule outbox URL for this principal.
713
if self.hasDeadProperty((caldav_namespace, "schedule-outbox-URL")):
714
outbox = self.readDeadProperty((caldav_namespace, "schedule-outbox-URL"))
715
return str(outbox.children[0])
719
def dropboxURL(self):
721
@return: the drop box home collection URL for this principal.
723
if self.hasDeadProperty((calendarserver_namespace, "dropbox-home-URL")):
724
inbox = self.readDeadProperty((caldav_namespace, "dropbox-home-URL"))
725
return str(inbox.children[0])
729
def notificationsURL(self):
731
@return: the notifications collection URL for this principal.
733
if self.hasDeadProperty((calendarserver_namespace, "notifications-URL")):
734
inbox = self.readDeadProperty((caldav_namespace, "notifications-URL"))
735
return str(inbox.children[0])
743
class AccessDisabled (davxml.WebDAVEmptyElement):
744
namespace = davxml.twisted_private_namespace
745
name = "caldav-access-disabled"
747
davxml.registerElement(AccessDisabled)
750
def isCalendarCollectionResource(resource):
752
resource = ICalDAVResource(resource)
756
return resource.isCalendarCollection()
758
def isPseudoCalendarCollectionResource(resource):
760
resource = ICalDAVResource(resource)
764
return resource.isPseudoCalendarCollection()