[CalendarServer-changes] [2528] CalendarServer/branches/users/cdaboo/server2server-2524
source_changes at macosforge.org
source_changes at macosforge.org
Tue Jun 3 13:48:16 PDT 2008
Revision: 2528
http://trac.macosforge.org/projects/calendarserver/changeset/2528
Author: cdaboo at apple.com
Date: 2008-06-03 13:48:15 -0700 (Tue, 03 Jun 2008)
Log Message:
-----------
Merged from trunk.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd-test.plist
CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd.plist
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/__init__.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/config.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/customxml.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/directory/calendar.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/ical.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/itip.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/log.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/copymove.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put_common.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/report_common.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/static.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/tap.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver-test.xml
CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver.dtd
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/freebusyurl.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule_common.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserver.py
CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserverparser.py
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd-test.plist 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd-test.plist 2008-06-03 20:48:15 UTC (rev 2528)
@@ -348,7 +348,36 @@
<key>EnableTimezoneService</key>
<true/>
+ <!-- Server to server protocol -->
+ <key>ServerToServer</key>
+ <dict>
+ <key>Enabled</key>
+ <true/>
+ <key>Email Domain</key>
+ <string>example.com</string>
+ <key>HTTP Domain</key>
+ <string>example.com</string>
+ <key>Local Addresses</key>
+ <array>
+ </array>
+ <key>Remote Addresses</key>
+ <array>
+ </array>
+ <key>Servers</key>
+ <string>conf/servertoserver-test.xml</string>
+ </dict>
+ <!-- Free-busy URL protocol -->
+ <key>FreeBusyURL</key>
+ <dict>
+ <key>Enabled</key>
+ <true/>
+ <key>Time Period</key>
+ <integer>14</integer>
+ <key>Anonymous Access</key>
+ <false/>
+ </dict>
+
<!--
Twisted
-->
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd.plist 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/conf/caldavd.plist 2008-06-03 20:48:15 UTC (rev 2528)
@@ -269,6 +269,35 @@
<key>EnableTimezoneService</key>
<true/>
+ <!-- Server to server protocol -->
+ <key>ServerToServer</key>
+ <dict>
+ <key>Enabled</key>
+ <true/>
+ <key>Email Domain</key>
+ <string></string>
+ <key>HTTP Domain</key>
+ <string></string>
+ <key>Local Addresses</key>
+ <array>
+ </array>
+ <key>Remote Addresses</key>
+ <array>
+ </array>
+ <key>Servers</key>
+ <string>/etc/caldavd/servertoserver.xml</string>
+ </dict>
+ <!-- Free-busy URL protocol -->
+ <key>FreeBusyURL</key>
+ <dict>
+ <key>Enabled</key>
+ <true/>
+ <key>Time Period</key>
+ <integer>14</integer>
+ <key>Anonymous Access</key>
+ <false/>
+ </dict>
+
</dict>
</plist>
Copied: CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver-test.xml (from rev 2525, CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver-test.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver-test.xml 2008-06-03 20:48:15 UTC (rev 2528)
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2007 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.
+ -->
+
+<!DOCTYPE servers SYSTEM "servertoserver.dtd">
+
+<servers>
+ <server>
+ <uri>https://localhost:8543/inbox</uri>
+ <allow-requests-from/>
+ <allow-requests-to/>
+ <domains>
+ <domain>example.org</domain>
+ </domains>
+ <hosts>
+ <host>127.0.0.1</host>
+ </hosts>
+ </server>
+</servers>
Copied: CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver.dtd (from rev 2525, CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver.dtd)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver.dtd (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/conf/servertoserver.dtd 2008-06-03 20:48:15 UTC (rev 2528)
@@ -0,0 +1,35 @@
+<!--
+Copyright (c) 2006-2007 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.
+
+DRI: Cyrus Daboo, cdaboo at apple.com
+ -->
+
+<!ELEMENT servers (server*) >
+
+ <!ELEMENT server (uri, authentication?, allow-requests-from, allow-requests-to, domains?, hosts?) >
+
+ <!ELEMENT uri (#PCDATA) >
+ <!ELEMENT authentication (user, password) >
+ <!ATTLIST authentication type (basic) "">
+ <!ELEMENT user (#PCDATA) >
+ <!ELEMENT password (#PCDATA) >
+
+ <!ELEMENT allow-requests-from EMPTY >
+ <!ELEMENT allow-requests-to EMPTY >
+ <!ELEMENT domains (domain*) >
+ <!ELEMENT domain (#PCDATA) >
+ <!ELEMENT hosts (host*) >
+ <!ELEMENT host (#PCDATA) >
+
\ No newline at end of file
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/__init__.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/__init__.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -34,6 +34,7 @@
"dropbox",
"extensions",
"fileops",
+ "freebusyurl",
"ical",
"icaldav",
"index",
@@ -46,8 +47,12 @@
"resource",
"root",
"schedule",
+ "schedule_common",
+ "servertoserver",
+ "servertoserverparser",
"sql",
"static",
+ "timezones",
]
try:
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/config.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/config.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -157,7 +157,21 @@
"EnableDropBox" : False, # Calendar Drop Box
"EnablePrivateEvents" : False, # Private Events
"EnableTimezoneService" : False, # Timezone service
+
+ "ServerToServer": {
+ "Enabled" : False, # Server-to-server protocol
+ "Email Domain" : "", # Domain for mailto calendar user addresses on this server
+ "HTTP Domain" : "", # Domain for http calendar user addresses on this server
+ "Local Addresses" : [], # Reg-ex patterns to match local calendar user addresses
+ "Remote Addresses" : [], # Reg-ex patterns to match remote calendar user addresses
+ },
+ "FreeBusyURL": {
+ "Enabled" : False, # Per-user free-busy-url protocol
+ "Time Period" : 14, # Number of days into the future to generate f-b data if no explicit time-range is specified
+ "Anonymous Access" : False, # Allow anonymous read access to free-busy URL
+ },
+
#
# Implementation details
#
@@ -217,6 +231,9 @@
}
class Config (object):
+ """
+ @DynamicAttrs
+ """
def __init__(self, defaults):
self.setDefaults(defaults)
self._data = copy.deepcopy(self._defaults)
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/customxml.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/customxml.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -244,6 +244,22 @@
namespace = calendarserver_namespace
name = "utc-offset"
+class ServerToServerInbox (davxml.WebDAVEmptyElement):
+ """
+ Denotes the resourcetype of a server-to-server Inbox.
+ (CalDAV-s2s-xx, section x.x.x)
+ """
+ namespace = calendarserver_namespace
+ name = "server-to-server-inbox"
+
+class FreeBusyURL (davxml.WebDAVEmptyElement):
+ """
+ Denotes the resourcetype of a free-busy URL resource.
+ (CalDAV-s2s-xx, section x.x.x)
+ """
+ namespace = calendarserver_namespace
+ name = "free-busy-url"
+
##
# Extensions to davxml.ResourceType
##
@@ -253,3 +269,5 @@
davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
davxml.ResourceType.timezones = davxml.ResourceType(Timezones())
+davxml.ResourceType.servertoserverinbox = davxml.ResourceType(ServerToServerInbox())
+davxml.ResourceType.freebusyurl = davxml.ResourceType(FreeBusyURL())
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/directory/calendar.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/directory/calendar.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -37,6 +37,7 @@
from twistedcaldav.config import config
from twistedcaldav.dropbox import DropBoxHomeResource
from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource
+from twistedcaldav.freebusyurl import FreeBusyURLResource
from twistedcaldav.resource import CalDAVResource
from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
from twistedcaldav.directory.idirectory import IDirectoryService
@@ -261,6 +262,10 @@
childlist += (
("dropbox", DropBoxHomeResource),
)
+ if config.FreeBusyURL["Enabled"]:
+ childlist += (
+ ("freebusy", FreeBusyURLResource),
+ )
for name, cls in childlist:
child = self.provisionChild(name)
assert isinstance(child, cls), "Child %r is not a %s: %r" % (name, cls.__name__, child)
Copied: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/freebusyurl.py (from rev 2525, CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/freebusyurl.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/freebusyurl.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/freebusyurl.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -0,0 +1,247 @@
+##
+# Copyright (c) 2005-2007 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.
+##
+
+"""
+Free-busy-URL resources.
+"""
+
+__all__ = [
+ "FreeBusyURLResource",
+]
+
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http import StatusResponse
+from twisted.web2.http_headers import MimeType
+from twisted.web2.stream import MemoryStream
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import Property
+from twistedcaldav.ical import parse_datetime
+from twistedcaldav.ical import parse_duration
+from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.schedule_common import Scheduler
+
+from vobject.icalendar import utc
+
+import datetime
+
+class FreeBusyURLResource (CalDAVResource):
+ """
+ Free-busy URL resource.
+
+ Extends L{DAVResource} to provide free-busy URL functionality.
+ """
+
+ def __init__(self, parent):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
+
+ CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+ self.parent = parent
+
+ def defaultAccessControlList(self):
+ aces = (
+ # DAV:Read, CalDAV:schedule for all principals (does not include anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.Authenticated()),
+ davxml.Grant(
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(caldavxml.Schedule()),
+ ),
+ davxml.Protected(),
+ ),
+ )
+ if config.FreeBusyURL["Anonymous Access"]:
+ aces += (
+ # DAV:Read, for unauthenticated principals
+ davxml.ACE(
+ davxml.Principal(davxml.Unauthenticated()),
+ davxml.Grant(
+ davxml.Privilege(davxml.Read()),
+ ),
+ davxml.Protected(),
+ ),
+ )
+ return davxml.ACL(*aces)
+
+ def resourceType(self):
+ return davxml.ResourceType.freebusyurl
+
+ def isCollection(self):
+ return False
+
+ def isCalendarCollection(self):
+ return False
+
+ def isPseudoCalendarCollection(self):
+ return False
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Free-Busy URL Resource</title>
+</head>
+<body>
+<h1>Free-busy URL Resource.</h1>
+</body
+</html>"""
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
+
+ def http_GET(self, request):
+ """
+ The free-busy URL POST method.
+ """
+ return self._processFBURL(request)
+
+ def http_POST(self, request):
+ """
+ The free-busy URL POST method.
+ """
+ return self._processFBURL(request)
+
+ @deferredGenerator
+ def _processFBURL(self, request):
+
+ #
+ # Check authentication and access controls
+ #
+ x = waitForDeferred(self.authorize(request, (davxml.Read(),)))
+ yield x
+ x.getResult()
+
+ # Extract query parameters from the URL
+ args = ('start', 'end', 'duration', 'token', 'format', 'user',)
+ for arg in args:
+ setattr(self, arg, request.args.get(arg, [None])[0])
+
+ # Some things we do not handle
+ if self.token or self.user:
+ raise HTTPError(ErrorResponse(responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-query-parameter")))
+
+ # Check format
+ if self.format:
+ self.format = self.format.split(";")[0]
+ if self.format not in ("text/calendar", "text/plain"):
+ raise HTTPError(ErrorResponse(responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-format")))
+ else:
+ self.format = "text/calendar"
+
+ # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values
+ try:
+ if self.start:
+ self.start = parse_datetime(self.start)
+ if self.start.tzinfo != utc:
+ raise ValueError()
+ if self.end:
+ self.end = parse_datetime(self.end)
+ if self.end.tzinfo != utc:
+ raise ValueError()
+ if self.duration:
+ self.duration = parse_duration(self.duration)
+ except ValueError:
+ raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters")))
+
+ # Sanity check start/end/duration
+
+ # End and duration cannot both be present
+ if self.end and self.duration:
+ raise HTTPError(ErrorResponse(responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "valid-query-parameters")))
+
+ # Duration must be positive
+ if self.duration and self.duration.days < 0:
+ raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters")))
+
+ # Now fill in the missing pieces
+ if self.start is None:
+ now = datetime.datetime.now()
+ self.start = now.replace(hour=0, minute=0, second=0, tzinfo=utc)
+ if self.duration:
+ self.end = self.start + self.duration
+ if self.end is None:
+ self.end = self.start + datetime.timedelta(days=config.FreeBusyURL["Time Period"])
+
+ # End > start
+ if self.end <= self.start:
+ raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters")))
+
+ # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)
+
+ # Now lookup the principal details for the targeted user
+ principal = self.parent.principalForRecord()
+
+ # Pick the first mailto cu address or the first other type
+ cuaddr = None
+ for item in principal.calendarUserAddresses():
+ if cuaddr is None:
+ cuaddr = item
+ if item.startswith("mailto"):
+ cuaddr = item
+ break
+
+ # Get inbox details
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL is None:
+ raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,)))
+ try:
+ inbox = waitForDeferred(request.locateResource(inboxURL))
+ yield inbox
+ inbox = inbox.getResult()
+ except:
+ log.err("No schedule inbox for principal: %s" % (principal,))
+ inbox = None
+ if inbox is None:
+ raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))
+
+ scheduler = Scheduler(request, self)
+ scheduler.timerange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
+ scheduler.timerange.start = self.start
+ scheduler.timerange.end = self.end
+
+ scheduler.organizer = Scheduler.LocalCalendarUser(cuaddr, principal, inbox, inboxURL)
+
+ attendeeProp = Property("ATTENDEE", scheduler.organizer.cuaddr)
+
+ d = waitForDeferred(scheduler.generateAttendeeFreeBusyResponse(
+ scheduler.organizer,
+ None,
+ None,
+ attendeeProp,
+ True,
+ ))
+ yield d
+ fbresult = d.getResult()
+
+ response = Response()
+ response.stream = MemoryStream(str(fbresult))
+ response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))
+
+ yield response
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/ical.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/ical.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -346,7 +346,8 @@
@return: the L{Component} of the primary type.
@raise: L{ValueError} if there is more than one primary type.
"""
- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
+ if self.name() != "VCALENDAR":
+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
result = None
for component in self.subcomponents():
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/itip.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/itip.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -46,7 +46,7 @@
from twistedcaldav.log import Logger
from twistedcaldav.ical import Property, iCalendarProductID
from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
from twistedcaldav.resource import isCalendarCollectionResource
log = Logger()
@@ -54,885 +54,840 @@
__version__ = "0.0"
__all__ = [
- "handleRequest",
- "canAutoRespond",
+ "iTipProcessor",
]
class iTipException(Exception):
pass
-def handleRequest(request, principal, inbox, calendar, child):
- """
- Handle an iTIP response automatically.
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- @return: L{Deferred} that is a L{deferredGenerator}
- """
+class iTipProcessor(object):
- method = calendar.propertyValue("METHOD")
- if method == "REQUEST":
- f = processRequest
- elif method == "ADD":
- f = processAdd
- elif method == "CANCEL":
- f = processCancel
+ def handleRequest(self, request, principal, inbox, calendar, child):
+ """
+ Handle an iTIP response automatically.
+
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+ @param calendar: the L{Component} for the iTIP message we are processing.
+ @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+ @return: L{Deferred} that is a L{deferredGenerator}
+ """
+
+ method = calendar.propertyValue("METHOD")
+ if method == "REQUEST":
+ f = self.processRequest
+ elif method == "ADD":
+ f = self.processAdd
+ elif method == "CANCEL":
+ f = self.processCancel
- return f(request, principal, inbox, calendar, child)
+ self.request = request
+ self.principal = principal
+ self.inbox = inbox
+ self.calendar = calendar
+ self.child = child
+ if self.child:
+ self.childname = self.child.fp.basename()
+ else:
+ self.childname = ""
+
+ return f()
- at inlineCallbacks
-def processRequest(request, principal, inbox, calendar, child):
- """
- Process a METHOD=REQUEST.
-
- Steps:
+ @inlineCallbacks
+ def processRequest(self):
+ """
+ Process a METHOD=REQUEST.
+ This is a deferredGenerator function so use yield whenever we have a deferred.
- 1. See if this updates existing ones in Inbox.
- 1. If so,
- 1. Remove existing ones in Inbox.
- 2. See if this updates existing ones in free-busy-set calendars.
- 3. Remove existing ones in those calendars.
- 4. See if this fits into a free slot:
- 1. If not, send REPLY with failure status
- 2. If so
- 1. send REPLY with success
- 2. add to f-b-s calendar
- 2. If not,
- 1. remove the one we got - its 'stale'
- 3. Delete the request from the Inbox.
+ Steps:
+
+ 1. See if this updates existing ones in Inbox.
+ 1. If so,
+ 1. Remove existing ones in Inbox.
+ 2. See if this updates existing ones in free-busy-set calendars.
+ 3. Remove existing ones in those calendars.
+ 4. See if this fits into a free slot:
+ 1. If not, send REPLY with failure status
+ 2. If so
+ 1. send REPLY with success
+ 2. add to f-b-s calendar
+ 2. If not,
+ 1. remove the one we got - its 'stale'
+ 3. Delete the request from the Inbox.
+
+ """
+
+ log.info("Auto-processing iTIP REQUEST for: %s" % (str(self.principal),))
+ processed = "ignored"
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
+ # First determine whether this is a full or partial update. A full update is one containing the master
+ # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
+ # being changed.
+
+ new_master = self.calendar.masterComponent()
- log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
- processed = "ignored"
-
- # First determine whether this is a full or partial update. A full update is one containing the master
- # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
- # being changed.
-
- new_master = calendar.masterComponent()
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- if new_master:
- # So we have a full update. That means we need to delete any existing events completely and
- # replace with the ones provided so long as the new one is newer.
+ # Next we want to try and find a match to any components on existing calendars listed as contributing
+ # to free-busy as we will need to update those with the new one.
+ calmatch, updatecal, calURL = yield self.findCalendarMatch()
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # See whether the new component is older than any existing ones and throw it away if so
- newinfo = (None,) + getComponentSyncInfo(new_master)
- cal = updatecal.iCalendar(calmatch)
- info = getSyncInfo(calmatch, cal)
- if compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
+ if new_master:
+ # So we have a full update. That means we need to delete any existing events completely and
+ # replace with the ones provided so long as the new one is newer.
+
+ # If we have a match then we need to check whether we are updating etc
+ check_reply = False
+ if calmatch:
+ # See whether the new component is older than any existing ones and throw it away if so
+ newinfo = (None,) + self.getComponentSyncInfo(new_master)
+ cal = updatecal.iCalendar(calmatch)
+ info = self.getSyncInfo(calmatch, cal)
+ if self.compareSyncInfo(info, newinfo) < 0:
+ # Existing resource is older and will be replaced
+ check_reply = True
+ else:
+ processed = "older"
else:
- processed = "older"
+ # We have a new request which we can reply to
+ check_reply = True
+
+ if check_reply:
+ # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+ doreply, replycal, accepted = yield self.checkForReply()
+
+ try:
+ if accepted:
+ if calmatch:
+ yield self.writeResource(calURL, updatecal, calmatch, self.calendar)
+ log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+ else:
+ yield self.writeResource(calURL, updatecal, None, self.calendar)
+ log.info("Added new calendar component in %s." % (calURL,))
+ else:
+ if calmatch:
+ yield self.deleteResource(updatecal, calmatch)
+ log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+
+ # Send a reply if needed.
+ if doreply:
+ log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+ yield self.writeReply(replycal)
+ processed = "processed"
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if accepted:
- if calmatch:
- newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
- log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+ # So we have a partial update. That means we have to do partial updates to instances in
+ # the existing calendar component.
+
+ # If we have a match then we need to check whether we are updating etc
+ check_reply = False
+ if calmatch:
+ # Check each component to see whether its new
+ cal = updatecal.iCalendar(calmatch)
+ old_master = cal.masterComponent()
+ processed = "older"
+ new_components = [component for component in self.calendar.subcomponents()]
+ for component in new_components:
+ if component.name() == "VTIMEZONE":
+ continue
+
+ newinfo = (None,) + self.getComponentSyncInfo(component)
+ old_component = self.findMatchingComponent(component, cal)
+ if old_component:
+ info = (None,) + self.getComponentSyncInfo(old_component)
+ elif old_master:
+ info = (None,) + self.getComponentSyncInfo(old_master)
else:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
- else:
- if calmatch:
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+ info = None
- # Send a reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- else:
- # So we have a partial update. That means we have to do partial updates to instances in
- # the existing calendar component.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # Check each component to see whether its new
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- processed = "older"
- new_components = [component for component in calendar.subcomponents()]
- for component in new_components:
- if component.name() == "VTIMEZONE":
- continue
+ if info is None or self.compareSyncInfo(info, newinfo) < 0:
+ # Existing resource is older and will be replaced
+ check_reply = True
+ processed = "processed"
+ else:
+ self.calendar.removeComponent(component)
+ else:
+ # We have a new request which we can reply to
+ check_reply = True
+
+ if check_reply:
+ # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+ doreply, replycal, accepted = yield self.checkForReply()
- newinfo = (None,) + getComponentSyncInfo(component)
- old_component = findMatchingComponent(component, cal)
- if old_component:
- info = (None,) + getComponentSyncInfo(old_component)
- elif old_master:
- info = (None,) + getComponentSyncInfo(old_master)
- else:
- info = None
-
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
+ try:
+ if calmatch:
+ # Merge the new instances with the old ones
+ self.mergeComponents(self.calendar, cal)
+ yield self.writeResource(calURL, updatecal, calmatch, cal)
+ log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+ else:
+ if accepted:
+ yield self.writeResource(calURL, updatecal, None, self.calendar)
+ log.info("Added new calendar component in %s." % (calURL,))
+
+ # Do reply if needed.
+ if doreply:
+ log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+ yield self.writeReply(replycal)
+
processed = "processed"
- else:
- calendar.removeComponent(component)
- else:
- # We have a new request which we can reply to
- check_reply = True
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
+ # Remove the now processed incoming request.
+ if self.inbox:
+ yield self.deleteInboxResource({
+ "processed":"processed",
+ "older": "ignored: older",
+ "ignored": "ignored: no match"
+ }[processed])
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if calmatch:
- # Merge the new instances with the old ones
- mergeComponents(calendar, cal)
- newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
- log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- if accepted:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
-
- # Do reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
-
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
+ returnValue(None)
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
-def processAdd(request, principal, inbox, calendar, child):
- """
- Process a METHOD=ADD.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
- log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
- raise NotImplementedError()
-
- at inlineCallbacks
-def processCancel(request, principal, inbox, calendar, child):
- """
- Process a METHOD=CANCEL.
-
- Policy find all components that match UID, SEQ and R-ID and remove them.
-
- Steps:
+ def processAdd(self):
+ """
+ Process a METHOD=ADD.
+ This is a deferredGenerator function so use yield whenever we have a deferred.
+ """
+ log.info("Auto-processing iTIP ADD for: %s" % (str(self.principal),))
- 1. See if this updates existing ones in Inbox.
- 2. Remove existing ones in Inbox.
- 3. See if this updates existing ones in free-busy-set calendars.
- 4. Remove existing ones in those calendars.
- 5. Remove the incoming request.
-
- NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
- instance may be cancelled. What we need to do for this is:
+ raise NotImplementedError()
- 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
- delete the old one.
- 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
- leave the new and existing ones in the Inbox.
- 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
- remove the overridden component from the f-b-s resource.
- 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
+ @inlineCallbacks
+ def processCancel(self):
+ """
+ Process a METHOD=CANCEL.
+ This is a deferredGenerator function so use yield whenever we have a deferred.
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+ Policy find all components that match UID, SEQ and R-ID and remove them.
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
+ Steps:
+
+ 1. See if this updates existing ones in Inbox.
+ 2. Remove existing ones in Inbox.
+ 3. See if this updates existing ones in free-busy-set calendars.
+ 4. Remove existing ones in those calendars.
+ 5. Remove the incoming request.
- log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
- processed = "ignored"
-
- # Get all component info for this iTIP message
- newinfo = getSyncInfo(child.fp.basename(), calendar)
- info = getAllInfo(inbox, calendar, child)
-
- # First see if we have a recurrence id which will force extra work
- has_rid = False
- if newinfo[4] is not None:
- has_rid = True
- else:
- for i in info:
- if i[4] is not None:
- has_rid = True
- break
-
- if not has_rid:
- # Compare the new one with each existing one.
- delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
- if delete_child:
- return
-
+ NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
+ instance may be cancelled. What we need to do for this is:
+
+ 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
+ delete the old one.
+ 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
+ leave the new and existing ones in the Inbox.
+ 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
+ remove the overridden component from the f-b-s resource.
+ 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
+
+ TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+ """
+
+ log.info("Auto-processing iTIP CANCEL for: %s" % (str(self.principal),))
+ processed = "ignored"
+
+ # Get all component info for this iTIP message
+ newinfo = self.getSyncInfo(self.childname, self.calendar)
+
+ # First see if we have a recurrence id which will force extra work
+ has_rid = False
+ if newinfo[4] is not None:
+ has_rid = True
+ else:
+ for i in self.getAllInfo(self.inbox, self.calendar, self.child):
+ if i[4] is not None:
+ has_rid = True
+ break
+
# Next we want to try and find a match to any components on existing calendars listed as contributing
# to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+ calmatch, updatecal, calURL = yield self.findCalendarMatch()
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # See whether the current component is older than any existing ones and throw it away if so
- cal = updatecal.iCalendar(calmatch)
- info = getSyncInfo(calmatch, cal)
- if compareSyncInfo(info, newinfo) < 0:
- # Delete existing resource which has been cancelled
- try:
- yield deleteResource(updatecal, calmatch)
- log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- processed = "processed"
+ if not has_rid:
+ # If we have a match then we need to check whether we are updating etc
+ if calmatch:
+ # See whether the current component is older than any existing ones and throw it away if so
+ cal = updatecal.iCalendar(calmatch)
+ info = self.getSyncInfo(calmatch, cal)
+ if self.compareSyncInfo(info, newinfo) < 0:
+ # Delete existing resource which has been cancelled
+ try:
+ yield self.deleteResource(updatecal, calmatch,)
+ log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+ processed = "processed"
+ else:
+ processed = "older"
else:
- processed = "older"
+ # Nothing to do except delete the inbox item as we have nothing to cancel.
+ processed = "ignored"
else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
- else:
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
- # So we need to iterate over each iTIP component.
-
- # Get the existing calendar object
- existing_calendar = updatecal.iCalendar(calmatch)
- existing_master = existing_calendar.masterComponent()
- exdates = []
-
- for component in calendar.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- # Find matching component in existing calendar
- old_component = findMatchingComponent(component, existing_calendar)
+ # If we have a match then we need to check whether we are updating etc
+ if calmatch:
+ # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
+ # So we need to iterate over each iTIP component.
+
+ # Get the existing calendar object
+ existing_calendar = updatecal.iCalendar(calmatch)
+ existing_master = existing_calendar.masterComponent()
+ exdates = []
+
+ for component in self.calendar.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
- if old_component:
- # We are cancelling an overridden component, so we need to check the
- # SEQUENCE/DTSAMP with the master.
- if compareComponents(old_component, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # Remove the existing component.
- existing_calendar.removeComponent(old_component)
- elif existing_master:
- # We are trying to CANCEL a non-overridden instance, so we need to
- # check SEQUENCE/DTSTAMP with the master.
- if compareComponents(existing_master, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # If we have any EXDATEs lets add them to the existing calendar object and write
- # it back.
- if exdates:
- if existing_master:
- existing_master.addProperty(Property("EXDATE", exdates))
-
- # See if there are still components in the calendar - we might have deleted the last overridden instance
- # in which case the calendar object is empty (except for VTIMEZONEs).
- if existing_calendar.mainType() is None:
- # Delete the now empty calendar object
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+ # Find matching component in existing calendar
+ old_component = self.findMatchingComponent(component, existing_calendar)
+
+ if old_component:
+ # We are cancelling an overridden component, so we need to check the
+ # SEQUENCE/DTSAMP with the master.
+ if self.compareComponents(old_component, component) < 0:
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+
+ # Remove the existing component.
+ existing_calendar.removeComponent(old_component)
+ elif existing_master:
+ # We are trying to CANCEL a non-overridden instance, so we need to
+ # check SEQUENCE/DTSTAMP with the master.
+ if self.compareComponents(existing_master, component) < 0:
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+
+ # If we have any EXDATEs lets add them to the existing calendar object and write
+ # it back.
+ if exdates:
+ if existing_master:
+ existing_master.addProperty(Property("EXDATE", exdates))
+
+ # See if there are still components in the calendar - we might have deleted the last overridden instance
+ # in which case the calendar object is empty (except for VTIMEZONEs).
+ if existing_calendar.mainType() is None:
+ # Delete the now empty calendar object
+ yield self.deleteResource(updatecal, calmatch)
+ log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+ else:
+ # Update the existing calendar object
+ yield self.writeResource(calURL, updatecal, calmatch, existing_calendar)
+ log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
+ processed = "processed"
else:
- # Update the existing calendar object
- newchild = yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
- log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
- processed = "processed"
+ processed = "older"
else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- at inlineCallbacks
-def checkForReply(request, principal, calendar):
- """
- Check whether a reply to the given iTIP message is needed. We will not process a reply
- if RSVP=FALSE. A reply will either be positive (accepted
- invitation) or negative (denied invitation). In addition we will modify calendar to reflect
- any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
+ # Nothing to do except delete the inbox item as we have nothing to cancel.
+ processed = "ignored"
- BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
- At the moment we will treat a failure on one instances as a DECLINE of the entire set.
+ # Remove the now processed incoming request.
+ if self.inbox:
+ yield self.deleteInboxResource({
+ "processed":"processed",
+ "older": "ignored: older",
+ "ignored": "ignored: no match"
+ }[processed])
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @return: C{True} if a reply is needed, C{False} otherwise.
- """
+ returnValue(None)
- # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
- accepted = True
+ @inlineCallbacks
+ def checkForReply(self):
+ """
+ Check whether a reply to the given iTIP message is needed. We will not process a reply
+ if RSVP=FALSE. A reply will either be positive (accepted
+ invitation) or negative (denied invitation). In addition we will modify calendar to reflect
+ any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
- # First expand current one to get instances (only go 1 year into the future)
- default_future_expansion_duration = datetime.timedelta(days=356*1)
- expand_max = datetime.date.today() + default_future_expansion_duration
- instances = calendar.expandTimeRanges(expand_max)
-
- # Extract UID from primary component as we want to ignore this one if we match it
- # in any calendars.
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
+ BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
+ At the moment we will treat a failure on one instance as a DECLINE of the entire set.
- # Now compare each instance time-range with the index and see if there is an overlap
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- for calURL in fbset:
- testcal = yield request.locateResource(calURL)
+ @return: C{True} if a reply is needed, C{False} otherwise.
+ """
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
+ # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
+ accepted = True
+
+ # First expand current one to get instances (only go 1 year into the future)
+ default_future_expansion_duration = datetime.timedelta(days=356*1)
+ expand_max = datetime.date.today() + default_future_expansion_duration
+ instances = self.calendar.expandTimeRanges(expand_max)
- # Now do search for overlapping time-range
- for instance in instances.instances.itervalues():
- try:
- tr = caldavxml.TimeRange(start="20000101", end="20000101")
- tr.start = instance.start
- tr.end = instance.end
- yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
-
- # If any fbinfo entries exist we have an overlap
- if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+ # Extract UID from primary component as we want to ignore this one if we match it
+ # in any calendars.
+ comp = self.calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now compare each instance time-range with the index and see if there is an overlap
+ calendars = yield self.getCalendarsToMatch()
+
+ for calURL in calendars:
+ testcal = yield self.request.locateResource(calURL)
+
+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = ([], [], [])
+
+ # Now do search for overlapping time-range
+ for instance in instances.instances.itervalues():
+ try:
+ tr = caldavxml.TimeRange(start="20000101", end="20000101")
+ tr.start = instance.start
+ tr.end = instance.end
+ yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid)
+
+ # If any fbinfo entries exist we have an overlap
+ if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+ accepted = False
+ break
+ except NumberOfMatchesWithinLimits:
accepted = False
+ log.info("Exceeded number of matches whilst trying to find free-time.")
break
- except NumberOfMatchesWithinLimits:
- accepted = False
- log.info("Exceeded number of matches whilst trying to find free-time.")
+
+ if not accepted:
break
-
- if not accepted:
- break
-
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProps = calendar.getAttendeeProperties(cuas)
- if not attendeeProps:
- returnValue((False, None, accepted))
-
- # Look for specific parameters
- rsvp = True
- for attendeeProp in attendeeProps:
- if "RSVP" in attendeeProp.params():
- if attendeeProp.params()["RSVP"][0] == "FALSE":
- rsvp = False
+
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = self.principal.calendarUserAddresses()
+ attendeeProps = self.calendar.getAttendeeProperties(cuas)
+ if not attendeeProps:
+ returnValue((False, None, accepted))
- # Now modify the original component
- del attendeeProp.params()["RSVP"]
-
- if accepted:
- partstat = "ACCEPTED"
- else:
- partstat = "DECLINED"
- for attendeeProp in attendeeProps:
- if "PARTSTAT" in attendeeProp.params():
- attendeeProp.params()["PARTSTAT"][0] = partstat
- else:
- attendeeProp.params()["PARTSTAT"] = [partstat]
+ # Look for specific parameters
+ rsvp = True
+ for attendeeProp in attendeeProps:
+ if "RSVP" in attendeeProp.params():
+ if attendeeProp.params()["RSVP"][0] == "FALSE":
+ rsvp = False
+
+ # Now modify the original component
+ del attendeeProp.params()["RSVP"]
- # Now create a new calendar object for the reply
-
- # First get useful props from the original
- replycal = calendar.duplicate()
-
- # Change METHOD
- replycal.getProperty("METHOD").setValue("REPLY")
-
- # Change PRODID to this server
- replycal.getProperty("PRODID").setValue(iCalendarProductID)
-
- # Add REQUEST-STATUS
- for component in replycal.subcomponents():
if accepted:
- component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+ partstat = "ACCEPTED"
else:
- component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
- # Remove all attendees other than ourselves
- for component in replycal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- attendeeProp = component.getAttendeeProperty(cuas)
- attendees = tuple(component.properties("ATTENDEE"))
- for attendee in attendees:
- if attendeeProp is None or (attendee.value() != attendeeProp.value()):
- component.removeProperty(attendee)
-
- returnValue((rsvp, replycal, accepted))
-
- at inlineCallbacks
-def writeReply(request, principal, replycal, ainbox):
- """
- Write an iTIP message reply into the specified Inbox.
+ partstat = "DECLINED"
+ for attendeeProp in attendeeProps:
+ if "PARTSTAT" in attendeeProp.params():
+ attendeeProp.params()["PARTSTAT"][0] = partstat
+ else:
+ attendeeProp.params()["PARTSTAT"] = [partstat]
+
+ # Now create a new calendar object for the reply
+
+ # First get useful props from the original
+ replycal = self.calendar.duplicate()
+
+ # Change METHOD
+ replycal.getProperty("METHOD").setValue("REPLY")
+
+ # Change PRODID to this server
+ replycal.getProperty("PRODID").setValue(iCalendarProductID)
+
+ # Add REQUEST-STATUS
+ for component in replycal.subcomponents():
+ if accepted:
+ component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+ else:
+ component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param replycal: the L{Component} for the iTIP message reply.
- @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
- """
+ # Remove all attendees other than ourselves
+ for component in replycal.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ attendeeProp = component.getAttendeeProperty(cuas)
+ attendees = tuple(component.properties("ATTENDEE"))
+ for attendee in attendees:
+ if attendeeProp is None or (attendee.value() != attendeeProp.value()):
+ component.removeProperty(attendee)
- # Get the Inbox of the ORGANIZER
- organizer = replycal.getOrganizer()
- assert organizer is not None
- inboxURL = ainbox.principalForCalendarUserAddress(organizer).scheduleInboxURL()
- assert inboxURL
+ returnValue((rsvp, replycal, accepted))
- # Determine whether current principal has CALDAV:schedule right on that Inbox
- inbox = yield request.locateResource(inboxURL)
+ @inlineCallbacks
+ def writeReply(self, replycal):
+ """
+ Write an iTIP message reply into the specified Inbox.
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
- except AccessDeniedError:
- log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
- returnValue(None)
+ @param replycal: the L{Component} for the iTIP message reply.
+ """
+
+ # Get the Inbox of the ORGANIZER
+ organizer = replycal.getOrganizer()
+ assert organizer is not None
+ inboxURL = self.inbox.principalForCalendarUserAddress(organizer).scheduleInboxURL()
+ assert inboxURL
+
+ # Determine whether current principal has CALDAV:schedule right on that Inbox
+ writeinbox = yield self.request.locateResource(inboxURL)
- # Now deposit the new calendar into the inbox
- result = yield writeResource(request, inboxURL, inbox, None, replycal)
- returnValue(result)
+ try:
+ yield writeinbox.checkPrivileges(self.request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(self.principal.principalURL())))
+ except AccessDeniedError:
+ log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (self.principal.principalURL(), organizer))
+ returnValue(None)
+
+ # Now deposit the new calendar into the inbox
+ newchild = yield self.writeResource(inboxURL, writeinbox, None, replycal)
- at inlineCallbacks
-def writeResource(request, collURL, collection, name, calendar):
- """
- Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
- resource or by creating a new one.
+ self.newInboxResource(self.child, newchild)
+
+ returnValue(newchild)
- @param request: the L{IRequest} for the current request.
- @param collURL: the C{str} containing the URL of the calendar collection.
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @param calendar: the L{Component} calendar to write.
- @return: C{tuple} of L{Deferred}, L{CalDAVFile}
- """
+ @inlineCallbacks
+ def writeResource(self, collURL, collection, name, calendar):
+ """
+ Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
+ resource or by creating a new one.
+
+ @param collURL: the C{str} containing the URL of the calendar collection.
+ @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+ @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+ @param calendar: the L{Component} calendar to write.
+ @return: C{tuple} of L{Deferred}, L{CalDAVFile}
+ """
+
+ # Create a new name if one was not provided
+ if name is None:
+ name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
- # Create a new name if one was not provided
- if name is None:
- name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- newchildURL = joinURL(collURL, name)
- newchild = yield request.locateResource(newchildURL)
+ # Get a resource for the new item
+ newchildURL = joinURL(collURL, name)
+ newchild = yield self.request.locateResource(newchildURL)
+
+ # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
+ # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
+ # NB Only do this when writing to something other than an Inbox or Outbox
+ itipper = True
+ if collection.isCalendarCollection():
+ method = calendar.getProperty("METHOD")
+ if method:
+ calendar.removeProperty(method)
+ itipper = False
+
+ # Now write it to the resource
+ yield StoreCalendarObjectResource(
+ request=self.request,
+ destination = newchild,
+ destination_uri = newchildURL,
+ destinationparent = collection,
+ destinationcal = True,
+ calendar = calendar,
+ isiTIP = itipper
+ ).run()
+
+ returnValue(newchild)
- # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
- # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
- # NB Only do this when writing to something other than an Inbox or Outbox
- itipper = True
- if collection.isCalendarCollection():
- method = calendar.getProperty("METHOD")
- if method:
- calendar.removeProperty(method)
- itipper = False
+ def newInboxResource(self, child, newchild):
+ """
+ Copy recipient and organizer properties from one iTIP resource, to another,
+ switching them as appropriate for a reply, and also set the state.
+
+ @param child: the L{CalDAVFile} for the original iTIP message.
+ @param newchild: the L{CalDAVFile} for the iTIP message reply.
+ """
+ # Make previous Recipient the new Originator
+ if child.hasDeadProperty(caldavxml.Recipient):
+ recip = child.readDeadProperty(caldavxml.Recipient)
+ if recip.children:
+ # Store CALDAV:originator property
+ newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
+
+ # Make previous Originator the new Recipient
+ if child.hasDeadProperty(caldavxml.Originator):
+ orig = child.readDeadProperty(caldavxml.Originator)
+ if orig.children:
+ # Store CALDAV:originator property
+ newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
- # Now write it to the resource
- try:
- yield storeCalendarObjectResource(
- request=request,
- sourcecal = False,
- destination = newchild,
- destination_uri = newchildURL,
- calendardata = str(calendar),
- destinationparent = collection,
- destinationcal = True,
- isiTIP = itipper
- )
- except:
- # FIXME: bare except
- return
-
- returnValue(newchild)
-
-def newInboxResource(child, newchild):
- """
- Copy recipient and organizer properties from one iTIP resource, to another,
- switching them as appropriate for a reply, and also set the state.
-
- @param child: the L{CalDAVFile} for the original iTIP message.
- @param newchild: the L{CalDAVFile} for the iTIP message reply.
- """
- # Make previous Recipient the new Originator
- if child.hasDeadProperty(caldavxml.Recipient):
- recip = child.readDeadProperty(caldavxml.Recipient)
- if recip.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-
- # Make previous Originator the new Recipient
- if child.hasDeadProperty(caldavxml.Originator):
- orig = child.readDeadProperty(caldavxml.Originator)
- if orig.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-
-def deleteResource(collection, name):
- """
- Delete the calendar resource in the specified calendar.
-
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @return: L{Deferred}
- """
-
- delchild = collection.getChild(name)
- index = collection.index()
- index.deleteResource(delchild.fp.basename())
-
- def _deletedResourced(result):
- # Change CTag on the parent calendar collection
- return collection.updateCTag().addCallback(lambda _: result)
-
- d = maybeDeferred(delete, "", delchild.fp, "0")
- d.addCallback(_deletedResourced)
- return d
-
-def canAutoRespond(calendar):
- """
- Check whether the METHOD of this iTIP calendar object is one we can process. Also,
- we will only handle VEVENTs right now.
-
- @param calendar: L{Component} for calendar to examine.
- @return: C{True} if we can auto-respond, C{False} if not.
- """
-
- try:
- method = calendar.propertyValue("METHOD")
- if method not in ("REQUEST", "ADD", "CANCEL"):
- return False
- if calendar.mainType() not in ("VEVENT"):
- return False
- except ValueError:
- return False
-
- return True
-
- at inlineCallbacks
-def processOthersInInbox(info, newinfo, inbox, child):
- # Compare the new one with each existing one.
- delete_child = False
- for i in info:
- # For any that are older, delete them.
- if compareSyncInfo(i, newinfo) < 0:
- try:
- yield deleteResource(inbox, i[0])
- log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- else:
- # For any that are newer or the same, mark the new one to be deleted.
- delete_child = True
-
- # Delete the new one if so marked.
- if delete_child:
+ @inlineCallbacks
+ def deleteInboxResource(self, processed_state):
+ # Remove the now processed incoming request.
try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
+ yield self.deleteResource(self.inbox, self.childname)
+ log.info("Deleted new iTIP message %s in Inbox because it has been %s." %
+ (self.childname, processed_state,))
except:
# FIXME: bare except
log.err("Error while auto-processing iTIP: %s" % (Failure(),))
raise iTipException()
-
- returnValue(delete_child)
- at inlineCallbacks
-def findCalendarMatch(request, principal, calendar):
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
+ def deleteResource(self, collection, name):
+ """
+ Delete the calendar resource in the specified calendar.
+
+ @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+ @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+ @return: L{Deferred}
+ """
+
+ delchild = collection.getChild(name)
+ index = collection.index()
+ index.deleteResource(delchild.fp.basename())
+
+ def _deletedResourced(result):
+ # Change CTag on the parent calendar collection
+ return collection.updateCTag().addCallback(lambda _: result)
- # Find the current recipients calendar-free-busy-set
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- # Find the first calendar in the list with a component matching the one we are processing
- calmatch = None
- updatecal = None
- calURL = None
- for calURL in fbset:
- updatecal = yield request.locateResource(calURL)
- if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
- calmatch = matchComponentInCalendar(updatecal, calendar)
- if calmatch:
- log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
- break
+ d = maybeDeferred(delete, "", delchild.fp, "0")
+ d.addCallback(_deletedResourced)
+ return d
- if calmatch is None and len(fbset):
- calURL = fbset[0]
- updatecal = yield request.locateResource(calURL)
-
- returnValue((calmatch, updatecal, calURL))
-
-def matchComponentInCalendar(collection, calendar):
- """
- See if the component in the provided iTIP calendar object matches any in the specified calendar
- collection.
+ @staticmethod
+ def canAutoRespond(calendar):
+ """
+ Check whether the METHOD of this iTIP calendar object is one we can process. Also,
+ we will only handle VEVENTs right now.
- @param collection: L{CalDAVFile} for the calendar collection to examine.
- @param calendar: L{Component} for calendar to examine.
- @return: C{list} of resource names found.
- """
-
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
+ @param calendar: L{Component} for calendar to examine.
+ @return: C{True} if we can auto-respond, C{False} if not.
+ """
+
+ try:
+ method = calendar.propertyValue("METHOD")
+ if method not in ("REQUEST", "ADD", "CANCEL"):
+ return False
+ if calendar.mainType() not in ("VEVENT"):
+ return False
+ except ValueError:
+ return False
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- result = index.resourceNamesForUID(uid)
+ return True
+
+ @inlineCallbacks
+ def findCalendarMatch(self):
+ # Try and find a match to any components on existing calendars listed as contributing
+ # to free-busy as we will need to update those with the new one.
- # There can be only one
- if len(result) > 0:
- return result[0]
- else:
- return None
- except ValueError:
- return None
-
-def findMatchingComponent(component, calendar):
- """
- See if any overridden component in the provided iTIP calendar object matches the specified component.
+ # Find the current recipients calendar-free-busy-set
+ calendars = yield self.getCalendarsToMatch()
- @param component: the component to try and match.
- @type component: L{Component}
- @param calendar: the calendar to find a match in.
- @type calendar: L{Component}
- @return: L{Component} for matching component,
- or C{None} if not found.
- """
-
- # Extract RECURRENCE-ID value from component
- rid = component.getRecurrenceIDUTC()
+ # Find the first calendar in the list with a component matching the one we are processing
+ calmatch = None
+ updatecal = None
+ calURL = None
+ for calURL in calendars:
+ updatecal = yield self.request.locateResource(calURL)
+ if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
+ # We will ignore missing calendars. If the recipient has failed to
+ # properly manage the free busy set that should not prevent us from working.
+ continue
+ calmatch = self.matchComponentInCalendar(updatecal, self.calendar)
+ if calmatch:
+ log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
+ break
+
+ if calmatch is None and len(calendars):
+ calURL = calendars[0]
+ updatecal = yield self.request.locateResource(calURL)
- # Return the one that matches in the calendar
- return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
- """
- Merge the overridden instance components in newcal into oldcal replacing any
- matching components there.
-
- @param newcal: the new overridden instances to use.
- @type newcal: L{Component}
- @param oldcal: the component to merge into.
- @type oldcal: L{Component}
- """
+ returnValue((calmatch, updatecal, calURL))
- # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
- # use a TZID that is already specified in the old component set.
-
- # We will update the SEQUENCE on the master to the highest value of the current one on the master
- # or the ones in the components we are changing.
-
- for component in newcal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
+ def getCalendarsToMatch(self):
+ # Determine the set of calendar URIs for a principal need to be searched.
- rid = component.getRecurrenceIDUTC()
- old_component = oldcal.overriddenComponent(rid)
- if old_component:
- oldcal.removeComponent(old_component)
- oldcal.addComponent(component)
+ # Find the current recipients calendar-free-busy-set
+ return self.principal.calendarFreeBusyURIs(self.request)
-def getAllInfo(collection, calendar, ignore):
- """
- Find each component in the calendar collection that has a matching UID with
- the supplied component, and get useful synchronization details from it, ignoring
- the one with the supplied resource name.
-
- @param collection: the L{CalDAVFile} for the calendar collection.
- @param calendar: the L{Component} for the component being compared with.
- @param ignore: the C{str} containing the name of a resource to ignore,
- or C{None} if none to ignore.
- @return: C{list} of synchronization information for each resource found.
- """
- names = []
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
+ def matchComponentInCalendar(self, collection, calendar):
+ """
+ See if the component in the provided iTIP calendar object matches any in the specified calendar
+ collection.
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- names = index.resourceNamesForUID(uid)
+ @param collection: L{CalDAVFile} for the calendar collection to examine.
+ @param calendar: L{Component} for calendar to examine.
+ @return: C{list} of resource names found.
+ """
+
+ try:
+ # Extract UID from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now use calendar collection index to find all other resources with the same UID
+ index = collection.index()
+ result = index.resourceNamesForUID(uid)
+
+ # There can be only one
+ if len(result) > 0:
+ return result[0]
+ else:
+ return None
+ except ValueError:
+ return None
+
+ def findMatchingComponent(self, component, calendar):
+ """
+ See if any overridden component in the provided iTIP calendar object matches the specified component.
- # Remove the one we want to ignore
- if ignore is not None:
- names = [name for name in names if name != ignore.fp.basename()]
- except ValueError:
- return []
+ @param component: the component to try and match.
+ @type component: L{Component}
+ @param calendar: the calendar to find a match in.
+ @type calendar: L{Component}
+ @return: L{Component} for matching component,
+ or C{None} if not found.
+ """
- # Now get info for each name
- result = []
- for name in names:
- cal = collection.iCalendar(name)
- result.append(getSyncInfo(name, cal))
-
- return result
+ # Extract RECURRENCE-ID value from component
+ rid = component.getRecurrenceIDUTC()
+
+ # Return the one that matches in the calendar
+ return calendar.overriddenComponent(rid)
-def getSyncInfo(name, calendar):
- """
- Get property value details needed to synchronize iTIP components.
+ def mergeComponents(self, newcal, oldcal):
+ """
+ Merge the overridden instance components in newcal into oldcal replacing any
+ matching components there.
- @param calendar: L{Component} for calendar to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract components from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
+ @param newcal: the new overridden instances to use.
+ @type newcal: L{Component}
+ @param oldcal: the component to merge into.
+ @type oldcal: L{Component}
+ """
- except ValueError:
- return (name, None, None, None, None)
+ # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
+ # use a TZID that is already specified in the old component set.
- return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
- """
- Get property value details needed to synchronize iTIP components.
+ # We will update the SEQUENCE on the master to the highest value of the current one on the master
+ # or the ones in the components we are changing.
- @param component: L{Component} to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract items from component
- uid = component.propertyValue("UID")
- seq = component.propertyValue("SEQUENCE")
- if seq:
- seq = int(seq)
- dtstamp = component.propertyValue("DTSTAMP")
- rid = component.propertyValue("RECURRENCE-ID")
+ for component in newcal.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+
+ rid = component.getRecurrenceIDUTC()
+ old_component = oldcal.overriddenComponent(rid)
+ if old_component:
+ oldcal.removeComponent(old_component)
+ oldcal.addComponent(component)
+
+ def getAllInfo(self, collection, calendar, ignore):
+ """
+ Find each component in the calendar collection that has a matching UID with
+ the supplied component, and get useful synchronization details from it, ignoring
+ the one with the supplied resource name.
+
+ @param collection: the L{CalDAVFile} for the calendar collection.
+ @param calendar: the L{Component} for the component being compared with.
+ @param ignore: the C{str} containing the name of a resource to ignore,
+ or C{None} if none to ignore.
+ @return: C{list} of synchronization information for each resource found.
+ """
+ names = []
+ try:
+ # Extract UID from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now use calendar collection index to find all other resources with the same UID
+ index = collection.index()
+ names = index.resourceNamesForUID(uid)
+
+ # Remove the one we want to ignore
+ if ignore is not None:
+ names = [name for name in names if name != ignore.fp.basename()]
+ except ValueError:
+ return []
- except ValueError:
- return (None, None, None, None)
+ # Now get info for each name
+ result = []
+ for name in names:
+ cal = collection.iCalendar(name)
+ result.append(self.getSyncInfo(name, cal))
- return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
- """
- Compare synchronization information for two components to see if they match according to iTIP.
-
- @param component1: first component to check.
- @type component1: L{Component}
- @param component2: second component to check.
- @type component2: L{Component}
+ return result
+
+ def getSyncInfo(self, name, calendar):
+ """
+ Get property value details needed to synchronize iTIP components.
+
+ @param calendar: L{Component} for calendar to check.
+ @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+ """
+ try:
+ # Extract components from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid, seq, dtstamp, rid = self.getComponentSyncInfo(comp)
+
+ except ValueError:
+ return (name, None, None, None, None)
+
+ return (name, uid, seq, dtstamp, rid)
- @return: 0, 1, -1 as per compareSyncInfo.
- """
- info1 = (None,) + getComponentSyncInfo(component1)
- info2 = (None,) + getComponentSyncInfo(component2)
- return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
- """
- Compare two synchronization information records.
+ def getComponentSyncInfo(self, component):
+ """
+ Get property value details needed to synchronize iTIP components.
+
+ @param component: L{Component} to check.
+ @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+ """
+ try:
+ # Extract items from component
+ uid = component.propertyValue("UID")
+ seq = component.propertyValue("SEQUENCE")
+ if seq:
+ seq = int(seq)
+ dtstamp = component.propertyValue("DTSTAMP")
+ rid = component.propertyValue("RECURRENCE-ID")
+
+ except ValueError:
+ return (None, None, None, None)
+
+ return (uid, seq, dtstamp, rid)
- @param info1: a C{tuple} as returned by L{getSyncInfo}.
- @param info2: a C{tuple} as returned by L{getSyncInfo}.
- @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
- """
- # UIDs MUST match
- assert info1[1] == info2[1]
+ def compareComponents(self, component1, component2):
+ """
+ Compare synchronization information for two components to see if they match according to iTIP.
- # Look for sequence
- if (info1[2] is not None) and (info2[2] is not None):
- if info1[2] > info2[2]:
+ @param component1: first component to check.
+ @type component1: L{Component}
+ @param component2: second component to check.
+ @type component2: L{Component}
+
+ @return: 0, 1, -1 as per compareSyncInfo.
+ """
+ info1 = (None,) + self.getComponentSyncInfo(component1)
+ info2 = (None,) + self.getComponentSyncInfo(component2)
+ return self.compareSyncInfo(info1, info2)
+
+ def compareSyncInfo(self, info1, info2):
+ """
+ Compare two synchronization information records.
+
+ @param info1: a C{tuple} as returned by L{getSyncInfo}.
+ @param info2: a C{tuple} as returned by L{getSyncInfo}.
+ @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
+ """
+ # UIDs MUST match
+ assert info1[1] == info2[1]
+
+ # Look for sequence
+ if (info1[2] is not None) and (info2[2] is not None):
+ if info1[2] > info2[2]:
+ return 1
+ if info1[2] < info2[2]:
+ return -1
+ elif (info1[2] is not None) and (info2[2] is None):
return 1
- if info1[2] < info2[2]:
+ elif (info1[2] is None) and (info2[2] is not None):
return -1
- elif (info1[2] is not None) and (info2[2] is None):
- return 1
- elif (info1[2] is None) and (info2[2] is not None):
- return -1
-
- # Look for DTSTAMP
- if (info1[3] is not None) and (info2[3] is not None):
- if info1[3] > info2[3]:
+
+ # Look for DTSTAMP
+ if (info1[3] is not None) and (info2[3] is not None):
+ if info1[3] > info2[3]:
+ return 1
+ if info1[3] < info2[3]:
+ return -1
+ elif (info1[3] is not None) and (info2[3] is None):
return 1
- if info1[3] < info2[3]:
+ elif (info1[3] is None) and (info2[3] is not None):
return -1
- elif (info1[3] is not None) and (info2[3] is None):
- return 1
- elif (info1[3] is None) and (info2[3] is not None):
- return -1
-
- return 0
+
+ return 0
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/log.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/log.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -59,6 +59,14 @@
from twisted.python import log
+from StringIO import StringIO
+
+from twisted.internet.defer import inlineCallbacks
+
+from twisted.web2 import responsecode
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
+
logLevels = (
"debug",
"info",
@@ -78,7 +86,7 @@
return sorted(levels, cmpLogLevels, reverse=True)[0]
##
-# Tools for manageing log levels
+# Tools for managing log levels
##
defaultLogLevel = "info"
@@ -194,6 +202,75 @@
"""
return cmpLogLevels(self.level(), level) <= 0
+ @inlineCallbacks
+ def logRequest(self, level, message, request, **kwargs):
+ """
+ Log an HTTP request.
+ """
+
+ assert level in logLevels
+
+ if self.willLogAtLevel(level):
+ iostr = StringIO()
+ iostr.write("%s\n" % (message,))
+ if hasattr(request, "clientproto"):
+ protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+ else:
+ protocol = "HTTP/1.1"
+ iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+ for name, valuelist in request.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("Authorization",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again.
+ data = yield allDataFromStream(request.stream)
+ iostr.write(data)
+
+ request.stream = MemoryStream(data)
+ request.stream.doStartReading = None
+
+ self.emit(level, iostr.getvalue(), **kwargs)
+
+ @inlineCallbacks
+ def logResponse(self, level, message, response, **kwargs):
+ """
+ Log an HTTP request.
+ """
+
+ assert level in logLevels
+
+ if self.willLogAtLevel(level):
+ iostr = StringIO()
+ iostr.write("%s\n" % (message,))
+ code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+ iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+ for name, valuelist in response.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("WWW-Authenticate",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the response stream to ensure we don't mess it up. So we
+ # read it, store the value in a MemoryStream, and replace the response's stream with that,
+ # so the data can be read again.
+ data = yield allDataFromStream(response.stream)
+ iostr.write(data)
+
+ response.stream = MemoryStream(data)
+ response.stream.doStartReading = None
+
+ self.emit(level, iostr.getvalue(), **kwargs)
+
class LoggingMixIn (object):
"""
Mix-in class for logging methods.
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/copymove.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/copymove.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -31,7 +31,7 @@
from twisted.web2.http import StatusResponse, HTTPError
from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
from twistedcaldav.resource import isCalendarCollectionResource
from twistedcaldav.log import Logger
@@ -101,7 +101,7 @@
# May need to add a location header
addLocation(request, destination_uri)
- x = waitForDeferred(storeCalendarObjectResource(
+ storer = StoreCalendarObjectResource(
request = request,
source = self,
source_uri = request.uri,
@@ -111,7 +111,8 @@
destination_uri = destination_uri,
destinationparent = destinationparent,
destinationcal = destinationcal,
- ))
+ )
+ x = waitForDeferred(storer.run())
yield x
yield x.getResult()
@@ -185,18 +186,19 @@
# May need to add a location header
addLocation(request, destination_uri)
- x = waitForDeferred(storeCalendarObjectResource(
+ storer = StoreCalendarObjectResource(
request = request,
source = self,
source_uri = request.uri,
sourceparent = sourceparent,
sourcecal = sourcecal,
+ deletesource = True,
destination = destination,
destination_uri = destination_uri,
destinationparent = destinationparent,
destinationcal = destinationcal,
- deletesource = True,
- ))
+ )
+ x = waitForDeferred(storer.run())
yield x
yield x.getResult()
@@ -209,15 +211,15 @@
if that is the case.
@return: tuple::
result: True if special CalDAV processing required, False otherwise
- NB If there is any type of error with the request, return False
- and allow normal COPY/MOVE processing to return the error.
+ NB If there is any type of error with the request, return False
+ and allow normal COPY/MOVE processing to return the error.
sourcecal: True if source is in a calendar collection, False otherwise
sourceparent: The parent resource for the source
destination_uri: The URI of the destination resource
destination: CalDAVFile of destination if special proccesing required,
None otherwise
destinationcal: True if the destination is in a calendar collection,
- False otherwise
+ False otherwise
destinationparent:The parent resource for the destination
"""
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -27,7 +27,7 @@
from twisted.web2.http import HTTPError, StatusResponse
from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
from twistedcaldav.resource import isPseudoCalendarCollectionResource
from twistedcaldav.log import Logger
@@ -60,15 +60,15 @@
# Use correct DAV:error response
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- d = waitForDeferred(storeCalendarObjectResource(
+ storer = StoreCalendarObjectResource(
request = request,
- sourcecal = False,
- calendardata = calendardata,
destination = self,
destination_uri = request.uri,
destinationcal = True,
- destinationparent = parent,)
+ destinationparent = parent,
+ calendar = calendardata,
)
+ d = waitForDeferred(storer.run())
yield d
yield d.getResult()
return
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put_common.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/put_common.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -18,8 +18,10 @@
PUT/COPY/MOVE common behavior.
"""
-__all__ = ["storeCalendarObjectResource"]
+__all__ = ["StoreCalendarObjectResource"]
+import types
+
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.internet.defer import deferredGenerator
@@ -57,56 +59,8 @@
log = Logger()
- at deferredGenerator
-def storeCalendarObjectResource(
- request,
- sourcecal, destinationcal,
- source=None, source_uri=None, sourceparent=None,
- destination=None, destination_uri=None, destinationparent=None,
- calendardata=None,
- deletesource=False,
- isiTIP=False
-):
- """
- Function that does common PUT/COPY/MOVE behaviour.
+class StoreCalendarObjectResource(object):
- @param request: the L{twisted.web2.server.Request} for the current HTTP request.
- @param source: the L{CalDAVFile} for the source resource to copy from, or None if source data
- is to be read from the request.
- @param source_uri: the URI for the source resource.
- @param destination: the L{CalDAVFile} for the destination resource to copy into.
- @param destination_uri: the URI for the destination resource.
- @param calendardata: the string data read directly from the request body if there is no source, None otherwise.
- @param sourcecal: True if the source resource is in a calendar collection, False otherwise.
- @param destinationcal: True if the destination resource is in a calendar collection, False otherwise
- @param sourceparent: the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
- @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
- @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
- @param isiTIP: True if relaxed calendar data validation is to be done, False otherwise.
- @return: a Deferred with a status response result.
- """
-
- try:
- assert destination is not None and destinationparent is not None and destination_uri is not None
- assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
- assert (calendardata is None and source is not None) or (calendardata is not None and source is None)
- assert not deletesource or (deletesource and source is not None)
- except AssertionError:
- log.err("Invalid arguments to storeCalendarObjectResource():")
- log.err("request=%s\n" % (request,))
- log.err("sourcecal=%s\n" % (sourcecal,))
- log.err("destinationcal=%s\n" % (destinationcal,))
- log.err("source=%s\n" % (source,))
- log.err("source_uri=%s\n" % (source_uri,))
- log.err("sourceparent=%s\n" % (sourceparent,))
- log.err("destination=%s\n" % (destination,))
- log.err("destination_uri=%s\n" % (destination_uri,))
- log.err("destinationparent=%s\n" % (destinationparent,))
- log.err("calendardata=%s\n" % (calendardata,))
- log.err("deletesource=%s\n" % (deletesource,))
- log.err("isiTIP=%s\n" % (isiTIP,))
- raise
-
class RollbackState(object):
"""
This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE
@@ -114,7 +68,8 @@
processed. The DoRollback method will actually execute the rollback operations.
"""
- def __init__(self):
+ def __init__(self, storer):
+ self.storer = storer
self.active = True
self.source_copy = None
self.destination_copy = None
@@ -134,32 +89,32 @@
log.debug("Rollback: rollback")
try:
if self.source_copy and self.source_deleted:
- self.source_copy.moveTo(source.fp)
- log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path))
+ self.source_copy.moveTo(self.storer.source.fp)
+ log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, self.storer.source.fp.path))
self.source_copy = None
self.source_deleted = False
if self.destination_copy:
- destination.fp.remove()
- log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path))
- self.destination_copy.moveTo(destination.fp)
+ self.storer.destination.fp.remove()
+ log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, self.storer.destination.fp.path))
+ self.destination_copy.moveTo(self.storer.destination.fp)
self.destination_copy = None
elif self.destination_created:
- if destinationcal:
- doRemoveDestinationIndex()
- log.debug("Rollback: destination index removed %s" % (destination.fp.path,))
+ if self.storer.destinationcal:
+ self.storer.doRemoveDestinationIndex()
+ log.debug("Rollback: destination index removed %s" % (self.storer.destination.fp.path,))
self.destination_index_deleted = False
- destination.fp.remove()
- log.debug("Rollback: destination removed %s" % (destination.fp.path,))
+ self.storer.destination.fp.remove()
+ log.debug("Rollback: destination removed %s" % (self.storer.destination.fp.path,))
self.destination_created = False
if self.destination_index_deleted:
# Must read in calendar for destination being re-indexed
- doDestinationIndex(destination.iCalendar())
+ self.storer.doDestinationIndex(self.storer.destination.iCalendar())
self.destination_index_deleted = False
- log.debug("Rollback: destination re-indexed %s" % (destination.fp.path,))
+ log.debug("Rollback: destination re-indexed %s" % (self.storer.destination.fp.path,))
if self.source_index_deleted:
- doSourceIndexRecover()
+ self.storer.doSourceIndexRecover()
self.destination_index_deleted = False
- log.debug("Rollback: source re-indexed %s" % (source.fp.path,))
+ log.debug("Rollback: source re-indexed %s" % (self.storer.source.fp.path,))
except:
log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
@@ -182,57 +137,229 @@
self.source_deleted = False
self.source_index_deleted = False
self.destination_index_deleted = False
+
+ class UIDReservation(object):
+
+ def __init__(self, index, uid, uri):
+ self.reserved = False
+ self.index = index
+ self.uid = uid
+ self.uri = uri
+
+ @deferredGenerator
+ def reserve(self):
+
+ # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
+ # time to whoever has the reservation to finish and release it.
+ failure_count = 0
+ while(failure_count < 10):
+ try:
+ self.index.reserveUID(self.uid)
+ self.reserved = True
+ break
+ except ReservationError:
+ self.reserved = False
+ failure_count += 1
+
+ d = Deferred()
+ def _timedDeferred():
+ d.callback(True)
+ reactor.callLater(0.5, _timedDeferred)
+ pause = waitForDeferred(d)
+ yield pause
+ pause.getResult()
+
+ if self.uri and not self.reserved:
+ raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
+
+ def unreserve(self):
+ if self.reserved:
+ self.index.unreserveUID(self.uid)
+ self.reserved = False
+
+ def __init__(
+ self,
+ request,
+ source=None, source_uri=None, sourceparent=None, sourcecal=False, deletesource=False,
+ destination=None, destination_uri=None, destinationparent=None, destinationcal=True,
+ calendar=None,
+ isiTIP=False
+ ):
+ """
+ Function that does common PUT/COPY/MOVE behavior.
+
+ @param request: the L{twisted.web2.server.Request} for the current HTTP request.
+ @param source: the L{CalDAVFile} for the source resource to copy from, or None if source data
+ is to be read from the request.
+ @param source_uri: the URI for the source resource.
+ @param destination: the L{CalDAVFile} for the destination resource to copy into.
+ @param destination_uri: the URI for the destination resource.
+ @param calendar: the C{str} or L{Component} calendar data if there is no source, None otherwise.
+ @param sourcecal: True if the source resource is in a calendar collection, False otherwise.
+ @param destinationcal: True if the destination resource is in a calendar collection, False otherwise
+ @param sourceparent: the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
+ @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
+ @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
+ @param isiTIP: True if relaxed calendar data validation is to be done, False otherwise.
+ """
+
+ # Check that all arguments are valid
+ try:
+ assert destination is not None and destinationparent is not None and destination_uri is not None
+ assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
+ assert (calendar is None and source is not None) or (calendar is not None and source is None)
+ assert not deletesource or (deletesource and source is not None)
+ except AssertionError:
+ log.err("Invalid arguments to StoreCalendarObjectResource.__init__():")
+ log.err("request=%s\n" % (request,))
+ log.err("sourcecal=%s\n" % (sourcecal,))
+ log.err("destinationcal=%s\n" % (destinationcal,))
+ log.err("source=%s\n" % (source,))
+ log.err("source_uri=%s\n" % (source_uri,))
+ log.err("sourceparent=%s\n" % (sourceparent,))
+ log.err("destination=%s\n" % (destination,))
+ log.err("destination_uri=%s\n" % (destination_uri,))
+ log.err("destinationparent=%s\n" % (destinationparent,))
+ log.err("calendar=%s\n" % (calendar,))
+ log.err("deletesource=%s\n" % (deletesource,))
+ log.err("isiTIP=%s\n" % (isiTIP,))
+ raise
- rollback = RollbackState()
+ self.request = request
+ self.sourcecal = sourcecal
+ self.destinationcal = destinationcal
+ self.source = source
+ self.source_uri = source_uri
+ self.sourceparent = sourceparent
+ self.destination = destination
+ self.destination_uri = destination_uri
+ self.destinationparent = destinationparent
+ self.calendar = calendar
+ self.calendardata = None
+ self.deletesource = deletesource
+ self.isiTIP = isiTIP
+
+ self.rollback = None
+ self.access = None
- def validResourceName():
+ @deferredGenerator
+ def fullValidation(self):
"""
+ Do full validation of source and destination calendar data.
+ """
+
+ if self.destinationcal:
+ # Valid resource name check
+ result, message = self.validResourceName()
+ if not result:
+ log.err(message)
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+
+ if not self.sourcecal:
+ # Valid content type check on the source resource if its not in a calendar collection
+ if self.source is not None:
+ result, message = self.validContentType()
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+
+ # At this point we need the calendar data to do more tests
+ self.calendar = self.source.iCalendar()
+ else:
+ try:
+ if type(self.calendar) in (types.StringType, types.UnicodeType,):
+ self.calendardata = self.calendar
+ self.calendar = Component.fromString(self.calendar)
+ except ValueError, e:
+ log.err(str(e))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Valid calendar data check
+ result, message = self.validCalendarDataCheck()
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Valid calendar data for CalDAV check
+ result, message = self.validCalDAVDataCheck()
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
+
+ # Must have a valid UID at this point
+ self.uid = self.calendar.resourceUID()
+ else:
+ # Get UID from original resource
+ self.source_index = self.sourceparent.index()
+ self.uid = self.source_index.resourceUIDForName(self.source.fp.basename())
+ if self.uid is None:
+ log.err("Source calendar does not have a UID: %s" % self.source.fp.basename())
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
+
+ # FIXME: We need this here because we have to re-index the destination. Ideally it
+ # would be better to copy the index entries from the source and add to the destination.
+ self.calendar = self.source.iCalendar()
+
+ # Valid calendar data size check
+ result, message = self.validSizeCheck()
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
+
+ # Check access
+ if self.destinationcal and config.EnablePrivateEvents:
+ d = waitForDeferred(self.validAccess())
+ yield d
+ d.getResult()
+
+ def validResourceName(self):
+ """
Make sure that the resource name for the new resource is valid.
"""
result = True
message = ""
- filename = destination.fp.basename()
+ filename = self.destination.fp.basename()
if filename.startswith("."):
result = False
message = "File name %s not allowed in calendar collection" % (filename,)
return result, message
- def validContentType():
+ def validContentType(self):
"""
Make sure that the content-type of the source resource is text/calendar.
This test is only needed when the source is not in a calendar collection.
"""
result = True
message = ""
- content_type = source.contentType()
+ content_type = self.source.contentType()
if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "calendar")):
result = False
message = "MIME type %s not allowed in calendar collection" % (content_type,)
return result, message
- def validCalendarDataCheck():
+ def validCalendarDataCheck(self):
"""
Check that the calendar data is valid iCalendar.
- @return: tuple: (True/False if the calendra data is valid,
+ @return: tuple: (True/False if the calendar data is valid,
log message string).
"""
result = True
message = ""
- if calendar is None:
+ if self.calendar is None:
result = False
message = "Empty resource not allowed in calendar collection"
else:
try:
- calendar.validCalendarForCalDAV()
+ self.calendar.validCalendarForCalDAV()
except ValueError, e:
result = False
message = "Invalid calendar data: %s" % (e,)
return result, message
- def validCalDAVDataCheck():
+ def validCalDAVDataCheck(self):
"""
Check that the calendar data is valid as a CalDAV calendar object resource.
@return: tuple: (True/False if the calendar data is valid,
@@ -241,17 +368,17 @@
result = True
message = ""
try:
- if isiTIP:
- calendar.validateComponentsForCalDAV(True)
+ if self.isiTIP:
+ self.calendar.validateComponentsForCalDAV(True)
else:
- calendar.validateForCalDAV()
+ self.calendar.validateForCalDAV()
except ValueError, e:
result = False
message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
return result, message
- def validSizeCheck():
+ def validSizeCheck(self):
"""
Make sure that the content-type of the source resource is text/calendar.
This test is only needed when the source is not in a calendar collection.
@@ -259,7 +386,7 @@
result = True
message = ""
if config.MaximumAttachmentSize:
- calsize = len(str(calendar))
+ calsize = len(str(self.calendar))
if calsize > config.MaximumAttachmentSize:
result = False
message = "Data size %d bytes is larger than allowed limit %d bytes" % (calsize, config.MaximumAttachmentSize)
@@ -267,45 +394,40 @@
return result, message
@deferredGenerator
- def validAccess():
+ def validAccess(self):
"""
Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
"""
- if calendar.hasProperty(Component.ACCESS_PROPERTY):
+ if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
# Must be a value we know about
- access = calendar.accessLevel(default=None)
- if access is None:
+ self.access = self.calendar.accessLevel(default=None)
+ if self.access is None:
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction")))
# Only DAV:owner is able to set the property to other than PUBLIC
- d = waitForDeferred(destinationparent.owner(request))
+ d = waitForDeferred(self.destinationparent.owner(self.request))
yield d
parent_owner = d.getResult()
- authz = destinationparent.currentPrincipal(request)
- if davxml.Principal(parent_owner) != authz and access != Component.ACCESS_PUBLIC:
+ authz = self.destinationparent.currentPrincipal(self.request)
+ if davxml.Principal(parent_owner) != authz and self.access != Component.ACCESS_PUBLIC:
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction-change")))
-
- yield access, calendardata
else:
# Check whether an access property was present before and write that into the calendar data
- newcalendardata = calendardata
- if not source and destination.exists() and destination.hasDeadProperty(TwistedCalendarAccessProperty):
- old_access = str(destination.readDeadProperty(TwistedCalendarAccessProperty))
- calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
- newcalendardata = str(calendar)
+ if not self.source and self.destination.exists() and self.destination.hasDeadProperty(TwistedCalendarAccessProperty):
+ old_access = str(self.destination.readDeadProperty(TwistedCalendarAccessProperty))
+ self.calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
+ self.calendardata = str(self.calendar)
- yield None, newcalendardata
-
- def noUIDConflict(uid):
+ def noUIDConflict(self, uid):
"""
Check that the UID of the new calendar object conforms to the requirements of
CalDAV, i.e. it must be unique in the collection and we must not overwrite a
different UID.
@param uid: the UID for the resource being stored.
- @return: tuple: (True/False if the uid is valid, log message string,
+ @return: tuple: (True/False if the UID is valid, log message string,
name of conflicted resource).
"""
@@ -315,14 +437,14 @@
# Adjust for a move into same calendar collection
oldname = None
- if sourceparent and (sourceparent.fp.path == destinationparent.fp.path) and deletesource:
- oldname = source.fp.basename()
+ if self.sourceparent and (self.sourceparent.fp.path == self.destinationparent.fp.path) and self.deletesource:
+ oldname = self.source.fp.basename()
- # UID must be unqiue
- index = destinationparent.index()
- if not index.isAllowedUID(uid, oldname, destination.fp.basename()):
+ # UID must be unique
+ index = self.destinationparent.index()
+ if not index.isAllowedUID(uid, oldname, self.destination.fp.basename()):
rname = index.resourceNameForUID(uid)
- # This can happen if two simulataneous PUTs occur with the same UID.
+ # This can happen if two simultaneous PUTs occur with the same UID.
# i.e. one PUT has reserved the UID but has not yet written the resource,
# the other PUT tries to reserve and fails but no index entry exists yet.
if rname is None:
@@ -332,349 +454,322 @@
message = "Calendar resource %s already exists with same UID %s" % (rname, uid)
else:
# Cannot overwrite a resource with different UID
- if destination.fp.exists():
- olduid = index.resourceUIDForName(destination.fp.basename())
+ if self.destination.fp.exists():
+ olduid = index.resourceUIDForName(self.destination.fp.basename())
if olduid != uid:
- rname = destination.fp.basename()
+ rname = self.destination.fp.basename()
result = False
message = "Cannot overwrite calendar resource %s with different UID %s" % (rname, olduid)
return result, message, rname
- try:
+ @deferredGenerator
+ def checkQuota(self):
"""
- Handle validation operations here.
+ Get quota details for destination and source before we start messing with adding other files.
"""
- reserved = False
- access = None
- if destinationcal:
- # Valid resource name check
- result, message = validResourceName()
- if not result:
- log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
- if not sourcecal:
- # Valid content type check on the source resource if its not in a calendar collection
- if source is not None:
- result, message = validContentType()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-
- # At this point we need the calendar data to do more tests
- calendar = source.iCalendar()
- else:
- try:
- calendar = Component.fromString(calendardata)
- except ValueError, e:
- log.err(str(e))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Valid calendar data check
- result, message = validCalendarDataCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Valid calendar data for CalDAV check
- result, message = validCalDAVDataCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
-
- # Must have a valid UID at this point
- uid = calendar.resourceUID()
- else:
- # Get uid from original resource
- source_index = sourceparent.index()
- uid = source_index.resourceUIDForName(source.fp.basename())
- if uid is None:
- log.err("Source calendar does not have a UID: %s" % source.fp.basename())
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
-
- # FIXME: We need this here because we have to re-index the destination. Ideally it
- # would be better to copy the index entries from the source and add to the destination.
- calendar = source.iCalendar()
-
- # Valid calendar data size check
- result, message = validSizeCheck()
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
-
- # Check access
- if destinationcal and config.EnablePrivateEvents:
- d = waitForDeferred(validAccess())
- yield d
- access, calendardata = d.getResult()
-
- # Reserve UID
- destination_index = destinationparent.index()
-
- # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
- # time to whoever has the reservation to finish and release it.
- failure_count = 0
- while(failure_count < 10):
- try:
- destination_index.reserveUID(uid)
- reserved = True
- break
- except ReservationError:
- reserved = False
- failure_count += 1
-
- d = Deferred()
- def _timedDeferred():
- d.callback(True)
- reactor.callLater(0.5, _timedDeferred)
- pause = waitForDeferred(d)
- yield pause
- pause.getResult()
-
- if destination_uri and not reserved:
- raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (destination_uri,)))
-
- # uid conflict check - note we do this after reserving the UID to avoid a race condition where two requests
- # try to write the same calendar data to two different resource URIs.
- if not isiTIP:
- result, message, rname = noUIDConflict(uid)
- if not result:
- log.err(message)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
- NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(destination_uri), rname.encode("utf-8"))))
- ))
-
- """
- Handle rollback setup here.
- """
-
- # Do quota checks on destination and source before we start messing with adding other files
- if request is None:
- destquota = None
+ if self.request is None:
+ self.destquota = None
else:
- destquota = waitForDeferred(destination.quota(request))
- yield destquota
- destquota = destquota.getResult()
- if destquota is not None and destination.exists():
- old_dest_size = waitForDeferred(destination.quotaSize(request))
- yield old_dest_size
- old_dest_size = old_dest_size.getResult()
+ self.destquota = waitForDeferred(self.destination.quota(self.request))
+ yield self.destquota
+ self.destquota = self.destquota.getResult()
+ if self.destquota is not None and self.destination.exists():
+ self.old_dest_size = waitForDeferred(self.destination.quotaSize(self.request))
+ yield self.old_dest_size
+ self.old_dest_size = self.old_dest_size.getResult()
else:
- old_dest_size = 0
+ self.old_dest_size = 0
- if request is None:
- sourcequota = None
- elif source is not None:
- sourcequota = waitForDeferred(source.quota(request))
- yield sourcequota
- sourcequota = sourcequota.getResult()
- if sourcequota is not None and source.exists():
- old_source_size = waitForDeferred(source.quotaSize(request))
- yield old_source_size
- old_source_size = old_source_size.getResult()
+ if self.request is None:
+ self.sourcequota = None
+ elif self.source is not None:
+ self.sourcequota = waitForDeferred(self.source.quota(self.request))
+ yield self.sourcequota
+ self.sourcequota = self.sourcequota.getResult()
+ if self.sourcequota is not None and self.source.exists():
+ self.old_source_size = waitForDeferred(self.source.quotaSize(self.request))
+ yield self.old_source_size
+ self.old_source_size = self.old_source_size.getResult()
else:
- old_source_size = 0
+ self.old_source_size = 0
else:
- sourcequota = None
- old_source_size = 0
+ self.sourcequota = None
+ self.old_source_size = 0
- # We may need to restore the original resource data if the PUT/COPY/MOVE fails,
- # so rename the original file in case we need to rollback.
- overwrite = destination.exists()
- if overwrite:
- rollback.destination_copy = FilePath(destination.fp.path)
- rollback.destination_copy.path += ".rollback"
- copyToWithXAttrs(destination.fp, rollback.destination_copy)
- log.debug("Rollback: backing up destination %s to %s" % (destination.fp.path, rollback.destination_copy.path))
- else:
- rollback.destination_created = True
- log.debug("Rollback: will create new destination %s" % (destination.fp.path,))
+ yield None
- if deletesource:
- rollback.source_copy = FilePath(source.fp.path)
- rollback.source_copy.path += ".rollback"
- copyToWithXAttrs(source.fp, rollback.source_copy)
- log.debug("Rollback: backing up source %s to %s" % (source.fp.path, rollback.source_copy.path))
-
+ def setupRollback(self):
"""
- Handle actual store operations here.
-
- The order in which this is done is import:
-
- 1. Do store operation for new data
- 2. Delete source and source index if needed
- 3. Do new indexing if needed
-
- Note that we need to remove the source index BEFORE doing the destination index to cover the
- case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
- column must be unique in SQL, we cannot add the new index before remove the old one.
+ We may need to restore the original resource data if the PUT/COPY/MOVE fails,
+ so rename the original file in case we need to rollback.
"""
+ self.rollback = StoreCalendarObjectResource.RollbackState(self)
+ self.overwrite = self.destination.exists()
+ if self.overwrite:
+ self.rollback.destination_copy = FilePath(self.destination.fp.path)
+ self.rollback.destination_copy.path += ".rollback"
+ copyToWithXAttrs(self.destination.fp, self.rollback.destination_copy)
+ log.debug("Rollback: backing up destination %s to %s" % (self.destination.fp.path, self.rollback.destination_copy.path))
+ else:
+ self.rollback.destination_created = True
+ log.debug("Rollback: will create new destination %s" % (self.destination.fp.path,))
+
+ if self.deletesource:
+ self.rollback.source_copy = FilePath(self.source.fp.path)
+ self.rollback.source_copy.path += ".rollback"
+ copyToWithXAttrs(self.source.fp, self.rollback.source_copy)
+ log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, self.rollback.source_copy.path))
+
+ @deferredGenerator
+ def doStore(self):
# Do put or copy based on whether source exists
- if source is not None:
- response = maybeDeferred(copyWithXAttrs, source.fp, destination.fp, destination_uri)
+ if self.source is not None:
+ response = maybeDeferred(copyWithXAttrs, self.source.fp, self.destination.fp, self.destination_uri)
else:
- md5 = MD5StreamWrapper(MemoryStream(calendardata))
- response = maybeDeferred(putWithXAttrs, md5, destination.fp)
+ if self.calendardata is None:
+ self.calendardata = str(self.calendar)
+ md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+ response = maybeDeferred(putWithXAttrs, md5, self.destination.fp)
response = waitForDeferred(response)
yield response
response = response.getResult()
# Update the MD5 value on the resource
- if source is not None:
+ if self.source is not None:
# Copy MD5 value from source to destination
- if source.hasDeadProperty(TwistedGETContentMD5):
- md5 = source.readDeadProperty(TwistedGETContentMD5)
- destination.writeDeadProperty(md5)
+ if self.source.hasDeadProperty(TwistedGETContentMD5):
+ md5 = self.source.readDeadProperty(TwistedGETContentMD5)
+ self.destination.writeDeadProperty(md5)
else:
- # Finish MD5 calc and write dead property
+ # Finish MD5 calculation and write dead property
md5.close()
md5 = md5.getMD5()
- destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
-
+ self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+
# Update calendar-access property value on the resource
- if access:
- destination.writeDeadProperty(TwistedCalendarAccessProperty(access))
+ if self.access:
+ self.destination.writeDeadProperty(TwistedCalendarAccessProperty(self.access))
# Do not remove the property if access was not specified and we are storing in a calendar.
# This ensure that clients that do not preserve the iCalendar property do not cause access
# restrictions to be lost.
- elif not destinationcal:
- destination.removeDeadProperty(TwistedCalendarAccessProperty)
+ elif not self.destinationcal:
+ self.destination.removeDeadProperty(TwistedCalendarAccessProperty)
- response = IResponse(response)
+ yield IResponse(response)
- def doDestinationIndex(caltoindex):
- """
- Do destination resource indexing, replacing any index previous stored.
-
- @return: None if successful, ErrorResponse on failure
- """
-
- # Delete index for original item
- if overwrite:
- doRemoveDestinationIndex()
-
- # Add or update the index for this resource.
- try:
- destination_index.addResource(destination.fp.basename(), caltoindex)
- log.debug("Destination indexed %s" % (destination.fp.path,))
- except TooManyInstancesError, ex:
- log.err("Cannot index calendar resource as there are too many recurrence instances %s" % destination)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
- ))
- except (ValueError, TypeError), ex:
- log.err("Cannot index calendar resource: %s" % (ex,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ @deferredGenerator
+ def doSourceDelete(self):
+ # Delete index for original item
+ if self.sourcecal:
+ self.source_index.deleteResource(self.source.fp.basename())
+ self.rollback.source_index_deleted = True
+ log.debug("Source index removed %s" % (self.source.fp.path,))
- destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
- return None
+ # Delete the source resource
+ delete(self.source_uri, self.source.fp, "0")
+ self.rollback.source_deleted = True
+ log.debug("Source removed %s" % (self.source.fp.path,))
- def doRemoveDestinationIndex():
- """
- Remove any existing destination index.
- """
-
- # Delete index for original item
- if destinationcal:
- destination_index.deleteResource(destination.fp.basename())
- rollback.destination_index_deleted = True
- log.debug("Destination index removed %s" % (destination.fp.path,))
+ # Update quota
+ if self.sourcequota is not None:
+ delete_size = 0 - self.old_source_size
+ d = waitForDeferred(self.source.quotaSizeAdjust(self.request, delete_size))
+ yield d
+ d.getResult()
- def doSourceDelete():
- # Delete index for original item
- if sourcecal:
- source_index.deleteResource(source.fp.basename())
- rollback.source_index_deleted = True
- log.debug("Source index removed %s" % (source.fp.path,))
+ # Change CTag on the parent calendar collection
+ if self.sourcecal:
+ d = waitForDeferred(self.sourceparent.updateCTag())
+ yield d
+ d.getResult()
+
+ yield None
- # Delete the source resource
- delete(source_uri, source.fp, "0")
- rollback.source_deleted = True
- log.debug("Source removed %s" % (source.fp.path,))
+ @deferredGenerator
+ def doDestinationQuotaCheck(self):
+ # Get size of new/old resources
+ new_dest_size = waitForDeferred(self.destination.quotaSize(self.request))
+ yield new_dest_size
+ new_dest_size = new_dest_size.getResult()
- def doSourceIndexRecover():
- """
- Do source resource indexing. This only gets called when restoring
- the source after its index has been deleted.
-
- @return: None if successful, ErrorResponse on failure
- """
-
- # Add or update the index for this resource.
- try:
- source_index.addResource(source.fp.basename(), calendar)
- except TooManyInstancesError, ex:
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
+ diff_size = new_dest_size - self.old_dest_size
+
+ if diff_size >= self.destquota[0]:
+ log.err("Over quota: available %d, need %d" % (self.destquota[0], diff_size))
+ raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
+ d = waitForDeferred(self.destination.quotaSizeAdjust(self.request, diff_size))
+ yield d
+ d.getResult()
+
+ yield None
+
+ def doSourceIndexRecover(self):
+ """
+ Do source resource indexing. This only gets called when restoring
+ the source after its index has been deleted.
+
+ @return: None if successful, ErrorResponse on failure
+ """
+
+ # Add or update the index for this resource.
+ try:
+ self.source_index.addResource(self.source.fp.basename(), self.calendar)
+ except TooManyInstancesError, ex:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
))
- source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+ self.source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
return None
- if deletesource:
- doSourceDelete()
- # Update quota
- if sourcequota is not None:
- delete_size = 0 - old_source_size
- d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
- yield d
- d.getResult()
+ def doDestinationIndex(self, caltoindex):
+ """
+ Do destination resource indexing, replacing any index previous stored.
+
+ @return: None if successful, ErrorResponse on failure
+ """
+
+ # Delete index for original item
+ if self.overwrite:
+ self.doRemoveDestinationIndex()
+
+ # Add or update the index for this resource.
+ try:
+ self.destination_index.addResource(self.destination.fp.basename(), caltoindex)
+ log.debug("Destination indexed %s" % (self.destination.fp.path,))
+ except TooManyInstancesError, ex:
+ log.err("Cannot index calendar resource as there are too many recurrence instances %s" % self.destination)
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
+ ))
+ except (ValueError, TypeError), ex:
+ log.err("Cannot index calendar resource: %s" % (ex,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- if sourcecal:
- # Change CTag on the parent calendar collection
- d = waitForDeferred(sourceparent.updateCTag())
- yield d
- d.getResult()
+ self.destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+ return None
- if destinationcal:
- result = doDestinationIndex(calendar)
- if result is not None:
- rollback.Rollback()
- yield result
- return
+ def doRemoveDestinationIndex(self):
+ """
+ Remove any existing destination index.
+ """
+
+ # Delete index for original item
+ if self.destinationcal:
+ self.destination_index.deleteResource(self.destination.fp.basename())
+ self.rollback.destination_index_deleted = True
+ log.debug("Destination index removed %s" % (self.destination.fp.path,))
- # Do quota check on destination
- if destquota is not None:
- # Get size of new/old resources
- new_dest_size = waitForDeferred(destination.quotaSize(request))
- yield new_dest_size
- new_dest_size = new_dest_size.getResult()
- diff_size = new_dest_size - old_dest_size
- if diff_size >= destquota[0]:
- log.err("Over quota: available %d, need %d" % (destquota[0], diff_size))
- raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
- d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size))
+ @deferredGenerator
+ def run(self):
+ """
+ Function that does common PUT/COPY/MOVE behavior.
+
+ @return: a Deferred with a status response result.
+ """
+
+ try:
+ reservation = None
+
+ # Handle all validation operations here.
+ d = waitForDeferred(self.fullValidation())
yield d
d.getResult()
-
- if destinationcal:
- # Change CTag on the parent calendar collection
- d = waitForDeferred(destinationparent.updateCTag())
+ # Reservation and UID conflict checking is next.
+ if self.destinationcal:
+ # Reserve UID
+ self.destination_index = self.destinationparent.index()
+ reservation = StoreCalendarObjectResource.UIDReservation(self.destination_index, self.uid, self.destination_uri)
+ d = waitForDeferred(reservation.reserve())
+ yield d
+ d.getResult()
+
+ # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+ # try to write the same calendar data to two different resource URIs.
+ if not self.isiTIP:
+ result, message, rname = self.noUIDConflict(self.uid)
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
+ NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(self.destination_uri), rname.encode("utf-8"))))
+ ))
+
+ # Get current quota state.
+ d = waitForDeferred(self.checkQuota())
yield d
d.getResult()
+
+ # Initialize the rollback system
+ self.setupRollback()
+
+ """
+ Handle actual store operations here.
+
+ The order in which this is done is import:
+
+ 1. Do store operation for new data
+ 2. Delete source and source index if needed
+ 3. Do new indexing if needed
+
+ Note that we need to remove the source index BEFORE doing the destination index to cover the
+ case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
+ column must be unique in SQL, we cannot add the new index before remove the old one.
+ """
+
+ # Do the actual put or copy
+ response = waitForDeferred(self.doStore())
+ yield response
+ response = response.getResult()
+
+ # Delete the original source if needed.
+ if self.deletesource:
+ d = waitForDeferred(self.doSourceDelete())
+ yield d
+ d.getResult()
+
+ # Index the new resource if storing to a calendar.
+ if self.destinationcal:
+ result = self.doDestinationIndex(self.calendar)
+ if result is not None:
+ self.rollback.Rollback()
+ yield result
+ return
+
+ # Do quota check on destination
+ if self.destquota is not None:
+ d = waitForDeferred(self.doDestinationQuotaCheck())
+ yield d
+ d.getResult()
+
+ if self.destinationcal:
+ # Change CTag on the parent calendar collection
+ d = waitForDeferred(self.destinationparent.updateCTag())
+ yield d
+ d.getResult()
+
+ # Can now commit changes and forget the rollback details
+ self.rollback.Commit()
+
+ if reservation:
+ reservation.unreserve()
+
+ yield response
+ return
+
+ except:
+ if reservation:
+ reservation.unreserve()
+
+ # Roll back changes to original server state. Note this may do nothing
+ # if the rollback has already occurred or changes already committed.
+ if self.rollback:
+ self.rollback.Rollback()
- # Can now commit changes and forget the rollback details
- rollback.Commit()
-
- if reserved:
- destination_index.unreserveUID(uid)
- reserved = False
-
- yield response
- return
-
- except:
- if reserved:
- destination_index.unreserveUID(uid)
- reserved = False
-
- # Roll back changes to original server state. Note this may do nothing
- # if the rollback has already ocurred or changes already committed.
- rollback.Rollback()
- raise
+ raise
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/report_common.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/method/report_common.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -284,7 +284,8 @@
_namedPropertiesForResource = deferredGenerator(_namedPropertiesForResource)
def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
- excludeuid=None, organizer=None, same_calendar_user=False):
+ excludeuid=None, organizer=None, same_calendar_user=False,
+ servertoserver=False):
"""
Run a free busy report on the specified calendar collection
accumulating the free busy info for later processing.
@@ -299,16 +300,19 @@
This is used in conjunction with the UID value to process exclusions.
@param same_calendar_user: a C{bool} indicating whether the calendar user requesting tyhe free-busy information
is the same as the calendar user being targeted.
+ @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request.
"""
# First check the privilege on this collection
- try:
- d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
- yield d
- d.getResult()
- except AccessDeniedError:
- yield matchtotal
- return
+ # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+ if not servertoserver:
+ try:
+ d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
+ yield d
+ d.getResult()
+ except AccessDeniedError:
+ yield matchtotal
+ return
#
# What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
@@ -353,12 +357,14 @@
yield child
child = child.getResult()
- try:
- d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
- yield d
- d.getResult()
- except AccessDeniedError:
- continue
+ # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+ if not servertoserver:
+ try:
+ d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
+ yield d
+ d.getResult()
+ except AccessDeniedError:
+ continue
calendar = calresource.iCalendar(name)
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2007 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.
@@ -21,34 +21,26 @@
__all__ = [
"ScheduleInboxResource",
"ScheduleOutboxResource",
+ "ScheduleServerToServerResource",
]
-import md5
-import time
-
-from twisted.internet import reactor
-from twisted.internet.defer import maybeDeferred, succeed, inlineCallbacks, returnValue
-from twisted.python.failure import Failure
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
from twisted.web2.dav import davxml
-from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
-from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.http import ErrorResponse
from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
from twistedcaldav import caldavxml
-from twistedcaldav import itip
-from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import Component
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import CalDAVResource
from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.schedule_common import CalDAVScheduler
+from twistedcaldav.schedule_common import ServerToServerScheduler
class CalendarSchedulingCollectionResource (CalDAVResource):
"""
@@ -194,402 +186,81 @@
# Check authentication and access controls
yield self.authorize(request, (caldavxml.Schedule(),))
- # Must be content-type text/calendar
- contentType = request.headers.getHeader("content-type")
- if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
- self.log_error("MIME type %s not allowed in calendar collection" % (contentType,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-
- # Must have Originator header
- originator = request.headers.getRawHeaders("originator")
- if originator is None or (len(originator) != 1):
- self.log_error("POST request must have Originator header")
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
- else:
- originator = originator[0]
-
- # Verify that Originator is a valid calendar user (has an INBOX)
- originatorPrincipal = self.principalForCalendarUserAddress(originator)
- if originatorPrincipal is None:
- self.log_error("Could not find principal for originator: %s" % (originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ # This is a local CALDAV scheduling operation.
+ scheduler = CalDAVScheduler(request, self)
- inboxURL = originatorPrincipal.scheduleInboxURL()
- if inboxURL is None:
- self.log_error("Could not find inbox for originator: %s" % (originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- # Verify that Originator matches the authenticated user
- if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != self.currentPrincipal(request):
- self.log_error("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ # Do the POST processing treating
+ response = yield scheduler.doSchedulingViaPOST()
+ returnValue(response)
- # Get list of Recipient headers
- rawRecipients = request.headers.getRawHeaders("recipient")
- if rawRecipients is None or (len(rawRecipients) == 0):
- self.log_error("POST request must have at least one Recipient header")
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+class ScheduleServerToServerResource (CalDAVResource):
+ """
+ Server-to-server schedule Inbox resource.
- # Recipient header may be comma separated list
- recipients = []
- for rawRecipient in rawRecipients:
- for r in rawRecipient.split(","):
- r = r.strip()
- if len(r):
- recipients.append(r)
+ Extends L{DAVResource} to provide Server-to-server functionality.
+ """
- timeRange = TimeRange(start="20000101", end="20000102")
- recipientsState = {"OK":0, "BAD":0}
+ def __init__(self, parent):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
- # Parse the calendar object from the HTTP request stream
- try:
- calendar = yield Component.fromIStream(request.stream)
- except:
- self.log_error("Error while handling POST: %s" % (Failure(),))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Must be a valid calendar
- try:
- calendar.validCalendarForCalDAV()
- except ValueError:
- self.log_error("POST request calendar component is not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
- # Must have a METHOD
- if not calendar.isValidMethod():
- self.log_error("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Verify iTIP behaviour
- if not calendar.isValidITIP():
- self.log_error("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ self.parent = parent
- # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
- if calendar.hasProperty(Component.ACCESS_PROPERTY):
- self.log_error("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-
- # Verify that the ORGANIZER's cu address maps to the request.uri
- organizer = calendar.getOrganizer()
- if organizer is None:
- organizerPrincipal = None
- else:
- organizerPrincipal = self.principalForCalendarUserAddress(organizer)
+ def defaultAccessControlList(self):
+ return davxml.ACL(
+ # DAV:Read, CalDAV:schedule for all principals (includes anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
+ davxml.Grant(
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(caldavxml.Schedule()),
+ ),
+ davxml.Protected(),
+ ),
+ )
- if organizerPrincipal is None:
- self.log_error("ORGANIZER in calendar data is not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ def resourceType(self):
+ return davxml.ResourceType.servertoserverinbox
- # Prevent spoofing of ORGANIZER with specific METHODs
- if (
- calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER") and
- organizerPrincipal.record != self.parent.record
- ):
- self.log_error("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ def isCollection(self):
+ return False
- # Prevent spoofing when doing reply-like METHODs
- if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
- # Verify that there is a single ATTENDEE property and that the Originator has permission
- # to send on behalf of that ATTENDEE
- attendees = calendar.getAttendees()
-
- # Must have only one
- if len(attendees) != 1:
- self.log_error("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
- # Attendee's Outbox MUST be the request URI
- attendeePrincipal = self.principalForCalendarUserAddress(attendees[0])
- if attendeePrincipal is None or attendeePrincipal.record != self.parent.record:
- self.log_error("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ def isCalendarCollection(self):
+ return False
- # For free-busy do immediate determination of iTIP result rather than fan-out
- self.log_debug("METHOD: %s, Component: %s" % (calendar.propertyValue("METHOD"), calendar.mainType(),))
- if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
- # Extract time range from VFREEBUSY object
- vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
- if len(vfreebusies) != 1:
- self.log_error("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- dtstart = vfreebusies[0].getStartDateUTC()
- dtend = vfreebusies[0].getEndDateUTC()
- if dtstart is None or dtend is None:
- self.log_error("VFREEBUSY start/end not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- timeRange.start = dtstart
- timeRange.end = dtend
+ def isPseudoCalendarCollection(self):
+ return False
- # Look for maksed UID
- excludeUID = calendar.getMaskUID()
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
- # Do free busy operation
- freebusy = True
- else:
- # Do regular invite (fan-out)
- freebusy = False
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
- #
- # Accounting
- #
- # Note that we associate logging with the organizer, not the
- # originator, which is good for looking for why something
- # shows up in a given principal's calendars, rather than
- # tracking the activities of a specific user.
- #
- if accountingEnabled("iTIP", organizerPrincipal):
- emitAccounting(
- "iTIP", organizerPrincipal,
- "Originator: %s\nRecipients: %s\n\n%s"
- % (originator, ", ".join(recipients), str(calendar))
- )
-
- # Prepare for multiple responses
- responses = ScheduleResponseQueue("POST", responsecode.OK)
-
- # Loop over each recipient and do appropriate action.
- autoresponses = []
- for recipient in recipients:
- # Get the principal resource for this recipient
- principal = self.principalForCalendarUserAddress(recipient)
-
- # Map recipient to their inbox
- inbox = None
- if principal is None:
- self.log_error("No schedulable principal for calendar user address: %r" % (recipient,))
- else:
- inboxURL = principal.scheduleInboxURL()
- if inboxURL:
- inbox = yield request.locateResource(inboxURL)
- else:
- self.log_error("No schedule inbox for principal: %s" % (principal,))
-
- if inbox is None:
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
- recipientsState["BAD"] += 1
-
- # Process next recipient
- continue
- else:
- #
- # Check access controls
- #
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(organizerPrincipal.principalURL())))
- except AccessDeniedError:
- self.log_error("Could not access Inbox for recipient: %s" % (recipient,))
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipientsState["BAD"] += 1
-
- # Process next recipient
- continue
-
- # Different behaviour for free-busy vs regular invite
- if freebusy:
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProp = calendar.getAttendeeProperty(cuas)
-
- # Find the current recipients calendar-free-busy-set
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- try:
- # Process the availability property from the Inbox.
- has_prop = yield inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request)
- if has_prop:
- availability = yield inbox.readProperty((calendarserver_namespace, "calendar-availability"), request)
- availability = availability.calendar()
- report_common.processAvailabilityFreeBusy(availability, fbinfo, timeRange)
-
- # Check to see if the recipient is the same calendar user as the organizer.
- # Needed for masked UID stuff.
- same_calendar_user = organizerPrincipal.principalURL() == principal.principalURL()
-
- # Now process free-busy set calendars
- matchtotal = 0
- for calendarResourceURL in fbset:
- calendarResource = yield request.locateResource(calendarResourceURL)
- if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
-
- matchtotal = yield report_common.generateFreeBusyInfo(
- request,
- calendarResource,
- fbinfo,
- timeRange,
- matchtotal,
- excludeuid = excludeUID,
- organizer = organizer,
- same_calendar_user = same_calendar_user
- )
-
- # Build VFREEBUSY iTIP reply for this recipient
- fbresult = report_common.buildFreeBusyResult(
- fbinfo,
- timeRange,
- organizer = calendar.getOrganizerProperty(),
- attendee = attendeeProp,
- uid = calendar.resourceUID(),
- method="REPLY"
- )
-
- responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
- recipientsState["OK"] += 1
-
- except:
- self.log_error("Could not determine free busy information: %s" % (recipient,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipientsState["BAD"] += 1
-
- else:
- # Hash the iCalendar data for use as the last path element of the URI path
- name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- childURL = joinURL(inboxURL, name)
- child = yield request.locateResource(childURL)
-
- try:
- # Copy calendar to inbox (doing fan-out)
- yield maybeDeferred(
- storeCalendarObjectResource,
- request=request,
- sourcecal = False,
- destination = child,
- destination_uri = childURL,
- calendardata = str(calendar),
- destinationparent = inbox,
- destinationcal = True,
- isiTIP = True
- )
- except: # FIXME: bare except
- self.log_error("Could not store data in Inbox : %s" % (inbox,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipientsState["BAD"] += 1
- else:
- responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
- recipientsState["OK"] += 1
-
- # Store CALDAV:originator property
- child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
-
- # Store CALDAV:recipient property
- child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
-
- # Look for auto-schedule option
- if principal.autoSchedule():
- autoresponses.append((principal, inbox, child))
-
- # Now we have to do auto-respond
- if len(autoresponses) != 0:
- # First check that we have a method that we can auto-respond to
- if not itip.canAutoRespond(calendar):
- autoresponses = []
-
- # Now do the actual auto response
- for principal, inbox, child in autoresponses:
- # Add delayed reactor task to handle iTIP responses
- reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
- #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-
- # Return with final response if we are done
- returnValue(responses.response())
-
-class ScheduleResponseResponse (Response):
- """
- ScheduleResponse L{Response} object.
- Renders itself as a CalDAV:schedule-response XML document.
- """
- def __init__(self, xml_responses, location=None):
+ @inlineCallbacks
+ def http_POST(self, request):
"""
- @param xml_responses: an interable of davxml.Response objects.
- @param location: the value of the location header to return in the response,
- or None.
+ The server-to-server POST method.
"""
- Response.__init__(self, code=responsecode.OK,
- stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+ # Check authentication and access controls
+ yield self.authorize(request, (caldavxml.Schedule(),))
- self.headers.setHeader("content-type", MimeType("text", "xml"))
-
- if location is not None:
- self.headers.setHeader("location", location)
+ # This is a server-to-server scheduling operation.
+ scheduler = ServerToServerScheduler(request, self)
-class ScheduleResponseQueue (LoggingMixIn):
- """
- Stores a list of (typically error) responses for use in a
- L{ScheduleResponse}.
- """
- def __init__(self, method, success_response):
- """
- @param method: the name of the method generating the queue.
- @param success_response: the response to return in lieu of a
- L{ScheduleResponse} if no responses are added to this queue.
- """
- self.responses = []
- self.method = method
- self.success_response = success_response
- self.location = None
-
- def setLocation(self, location):
- """
- @param location: the value of the location header to return in the response,
- or None.
- """
- self.location = location
-
- def add(self, recipient, what, reqstatus=None, calendar=None):
- """
- Add a response.
- @param recipient: the recipient for this response.
- @param what: a status code or a L{Failure} for the given recipient.
- @param status: the iTIP request-status for the given recipient.
- @param calendar: the calendar data for the given recipient response.
- """
- if type(what) is int:
- code = what
- error = None
- message = responsecode.RESPONSES[code]
- elif isinstance(what, Failure):
- code = statusForFailure(what)
- error = errorForFailure(what)
- message = messageForFailure(what)
- else:
- raise AssertionError("Unknown data type: %r" % (what,))
-
- if code > 400: # Error codes only
- self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
-
- children = []
- children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
- children.append(caldavxml.RequestStatus(reqstatus))
- if calendar is not None:
- children.append(caldavxml.CalendarData.fromCalendar(calendar))
- if error is not None:
- children.append(error)
- if message is not None:
- children.append(davxml.ResponseDescription(message))
- self.responses.append(caldavxml.Response(*children))
-
- def response(self):
- """
- Generate a L{ScheduleResponseResponse} with the responses contained in the
- queue or, if no such responses, return the C{success_response} provided
- to L{__init__}.
- @return: the response.
- """
- if self.responses:
- return ScheduleResponseResponse(self.responses, self.location)
- else:
- return self.success_response
+ # Do the POST processing treating this as a non-local schedule
+ response = yield scheduler.doSchedulingViaPOST()
+ returnValue(response)
Copied: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule_common.py (from rev 2525, CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/schedule_common.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule_common.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/schedule_common.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -0,0 +1,1010 @@
+##
+# Copyright (c) 2005-2007 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/Server-to-Server scheduling behavior.
+"""
+
+__all__ = [
+ "Scheduler",
+ "CalDAVScheduler",
+ "ServerToServerScheduler",
+]
+
+from twisted.internet import reactor
+from twisted.internet.defer import DeferredList
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.log import Logger, LoggingMixIn
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import Component
+from twistedcaldav.itip import iTipProcessor
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.servertoserver import ServerToServer
+from twistedcaldav.servertoserver import ServerToServerRequest
+import itertools
+import md5
+import re
+import socket
+import time
+
+log = Logger()
+
+class Scheduler(object):
+
+ class CalendarUser(object):
+ def __init__(self, cuaddr):
+ self.cuaddr = cuaddr
+
+ class LocalCalendarUser(CalendarUser):
+ def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
+ self.cuaddr = cuaddr
+ self.principal = principal
+ self.inbox = inbox
+ self.inboxURL = inboxURL
+
+ def __str__(self):
+ return "Local calendar user: %s" % (self.cuaddr,)
+
+ class RemoteCalendarUser(CalendarUser):
+ def __init__(self, cuaddr):
+ self.cuaddr = cuaddr
+ self.extractDomain()
+
+ def __str__(self):
+ return "Remote calendar user: %s" % (self.cuaddr,)
+
+ def extractDomain(self):
+ if self.cuaddr.startswith("mailto:"):
+ splits = self.cuaddr[7:].split("?")
+ self.domain = splits[0].split("@")[1]
+ elif self.cuaddr.startswith("http://") or self.cuaddr.startswith("https://"):
+ splits = self.cuaddr.split(":")[1][2:].split("/")
+ self.domain = splits[0]
+ else:
+ self.domain = ""
+
+ class InvalidCalendarUser(CalendarUser):
+
+ def __str__(self):
+ return "Invalid calendar user: %s" % (self.cuaddr,)
+
+
+ def __init__(self, request, resource):
+ self.request = request
+ self.resource = resource
+ self.originator = None
+ self.recipients = None
+ self.calendar = None
+ self.organizer = None
+ self.timeRange = None
+ self.excludeUID = None
+
+ @inlineCallbacks
+ def doSchedulingViaPOST(self):
+ """
+ The Scheduling POST operation.
+ """
+
+ # Do some extra authorization checks
+ self.checkAuthorization()
+
+ #d = waitForDeferred(log.logRequest("debug", "Received POST request:", self.request))
+ #yield d
+ #d.getResult()
+
+ # Load various useful bits doing some basic checks on those
+ self.loadOriginator()
+ self.loadRecipients()
+ yield self.loadCalendar()
+
+ # Check validity of Originator header.
+ self.checkOriginator()
+
+ # Get recipient details.
+ yield self.checkRecipients()
+
+ # Check calendar data.
+ self.checkCalendarData()
+
+ # Check validity of ORGANIZER
+ self.checkOrganizer()
+
+ # Do security checks (e.g. spoofing)
+ self.securityChecks()
+
+ # Generate accounting information
+ self.doAccounting()
+
+ # Do scheduling tasks
+ response = yield self.generateSchedulingResponse()
+
+ #yield log.logResponse("debug", "Sending POST response:", response)
+
+ returnValue(response)
+
+ def loadOriginator(self):
+ # Must have Originator header
+ originator = self.request.headers.getRawHeaders("originator")
+ if originator is None or (len(originator) != 1):
+ log.err("POST request must have Originator header")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+ else:
+ self.originator = originator[0]
+
+ def loadRecipients(self):
+ # Get list of Recipient headers
+ rawRecipients = self.request.headers.getRawHeaders("recipient")
+ if rawRecipients is None or (len(rawRecipients) == 0):
+ log.err("POST request must have at least one Recipient header")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+
+ # Recipient header may be comma separated list
+ self.recipients = []
+ for rawRecipient in rawRecipients:
+ for r in rawRecipient.split(","):
+ r = r.strip()
+ if len(r):
+ self.recipients.append(r)
+
+ @inlineCallbacks
+ def loadCalendar(self):
+ # Must be content-type text/calendar
+ contentType = self.request.headers.getHeader("content-type")
+ if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
+ log.err("MIME type %s not allowed in calendar collection" % (contentType,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+
+ # Parse the calendar object from the HTTP request stream
+ try:
+ self.calendar = yield Component.fromIStream(self.request.stream)
+ except:
+ # FIXME: Bare except
+ log.err("Error while handling POST: %s" % (Failure(),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ def checkAuthorization(self):
+ raise NotImplementedError
+
+ def checkOriginator(self):
+ raise NotImplementedError
+
+ def checkRecipient(self):
+ raise NotImplementedError
+
+ def checkOrganizer(self):
+ raise NotImplementedError
+
+ def checkOrganizerAsOriginator(self):
+ raise NotImplementedError
+
+ def checkAttendeeAsOriginator(self):
+ raise NotImplementedError
+
+ def checkCalendarData(self):
+ # Must be a valid calendar
+ try:
+ self.calendar.validCalendarForCalDAV()
+ except ValueError:
+ log.err("POST request calendar component is not valid: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Must have a METHOD
+ if not self.calendar.isValidMethod():
+ log.err("POST request must have valid METHOD property in calendar component: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Verify iTIP behavior
+ if not self.calendar.isValidITIP():
+ log.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
+ if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
+ log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
+
+ def checkForFreeBusy(self):
+ if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
+ # Extract time range from VFREEBUSY object
+ vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
+ if len(vfreebusies) != 1:
+ log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ dtstart = vfreebusies[0].getStartDateUTC()
+ dtend = vfreebusies[0].getEndDateUTC()
+ if dtstart is None or dtend is None:
+ log.err("VFREEBUSY start/end not valid: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ self.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
+ self.timeRange.start = dtstart
+ self.timeRange.end = dtend
+
+ # Look for masked UID
+ self.excludeUID = self.calendar.getMaskUID()
+
+ # Do free busy operation
+ return True
+ else:
+ # Do regular invite (fan-out)
+ return False
+
+ def securityChecks(self):
+ raise NotImplementedError
+
+ def doAccounting(self):
+ #
+ # Accounting
+ #
+ # Note that we associate logging with the organizer, not the
+ # originator, which is good for looking for why something
+ # shows up in a given principal's calendars, rather than
+ # tracking the activities of a specific user.
+ #
+ if isinstance(self.organizer, Scheduler.LocalCalendarUser):
+ if accountingEnabled("iTIP", self.organizer.principal):
+ emitAccounting(
+ "iTIP", self.organizer.principal,
+ "Originator: %s\nRecipients:\n%s\n%s"
+ % (
+ str(self.originator),
+ "".join([" %s\n" % (recipient,) for recipient in self.recipients]),
+ str(self.calendar)
+ )
+ )
+
+ @staticmethod
+ def isCalendarUserAddressInMyDomain(cuaddr):
+ """
+ Check whether the supplied calendar user address corresponds to one that ought to be within
+ this server's domain.
+
+ For now we will try to match email and http domains against ones in our config.
+
+ @param cuaddr: the calendar user address to check.
+ @type cuaddr: C{str}
+
+ @return: C{True} if the address is within the server's domain,
+ C{False} otherwise.
+ """
+
+ if config.ServerToServer["Email Domain"] and cuaddr.startswith("mailto:"):
+ splits = cuaddr[7:].split("?")
+ domain = config.ServerToServer["Email Domain"]
+ return splits[0].endswith(domain)
+ elif config.ServerToServer["HTTP Domain"] and (cuaddr.startswith("http://") or cuaddr.startswith("https://")):
+ splits = cuaddr.split(":")[0][2:].split("?")
+ domain = config.ServerToServer["HTTP Domain"]
+ return splits[0].endswith(domain)
+ elif cuaddr.startswith("/"):
+ # Assume relative HTTP URL - i.e. on this server
+ return True
+
+ result = False
+
+ for pattern in config.ServerToServer["Local Addresses"]:
+ try:
+ if re.match(pattern, cuaddr) is not None:
+ result = True
+ except re.error:
+ log.debug("Invalid regular expression for ServerToServer configuration 'Local Addresses': %s" % (pattern,))
+
+ for pattern in config.ServerToServer["Remote Addresses"]:
+ try:
+ if re.match(pattern, cuaddr) is not None:
+ result = False
+ except re.error:
+ log.debug("Invalid regular expression for ServerToServer configuration 'Remote Addresses': %s" % (pattern,))
+
+ return result
+
+ @inlineCallbacks
+ def generateSchedulingResponse(self):
+
+ log.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),))
+
+ # For free-busy do immediate determination of iTIP result rather than fan-out
+ freebusy = self.checkForFreeBusy()
+
+ # Prepare for multiple responses
+ responses = ScheduleResponseQueue("POST", responsecode.OK)
+
+ # Extract the ORGANIZER property and UID value from the calendar data for use later
+ organizerProp = self.calendar.getOrganizerProperty()
+ uid = self.calendar.resourceUID()
+
+ # Loop over each recipient and do appropriate action.
+ remote_recipients = []
+ autoresponses = []
+ for recipient in self.recipients:
+
+ if isinstance(recipient, Scheduler.InvalidCalendarUser):
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+
+ # Process next recipient
+ continue
+ elif isinstance(recipient, Scheduler.RemoteCalendarUser):
+ # Pool remote recipients into a separate list for processing after the local ones.
+ remote_recipients.append(recipient)
+
+ # Process next recipient
+ continue
+ elif isinstance(recipient, Scheduler.LocalCalendarUser):
+ #
+ # Check access controls
+ #
+ if isinstance(self.organizer, Scheduler.LocalCalendarUser):
+ try:
+ yield recipient.inbox.checkPrivileges(self.request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(self.organizer.principal.principalURL())))
+ except AccessDeniedError:
+ log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+
+ # Process next recipient
+ continue
+ else:
+ # TODO: need to figure out how best to do server-to-server authorization.
+ # First thing would be to check for DAV:unauthenticated privilege.
+ # Next would be to allow the calendar user address of the organizer/originator to be used
+ # as a principal.
+ pass
+
+ # Different behavior for free-busy vs regular invite
+ if freebusy:
+ yield self.generateLocalFreeBusyResponse(recipient, responses, organizerProp, uid)
+ else:
+ yield self.generateLocalResponse(recipient, responses, autoresponses)
+
+ # Now process remote recipients
+ if remote_recipients:
+ yield self.generateRemoteSchedulingResponses(remote_recipients, responses)
+
+ # Now we have to do auto-respond
+ if len(autoresponses) != 0:
+ # First check that we have a method that we can auto-respond to
+ if not iTipProcessor.canAutoRespond(self.calendar):
+ autoresponses = []
+
+ # Now do the actual auto response
+ for principal, inbox, child in autoresponses:
+ # Add delayed reactor task to handle iTIP responses
+ itip = iTipProcessor()
+ reactor.callLater(0.0, itip.handleRequest, *(self.request, principal, inbox, self.calendar.duplicate(), child)) #@UndefinedVariable
+
+ # Return with final response if we are done
+ returnValue(responses.response())
+
+ @inlineCallbacks
+ def generateRemoteSchedulingResponses(self, recipients, responses):
+ """
+ Generate scheduling responses for remote recipients.
+ """
+
+ # Group recipients by server so that we can do a single request with multiple recipients
+ # to each different server.
+ groups = {}
+ servermgr = ServerToServer()
+ for recipient in recipients:
+ # Map the recipient's domain to a server
+ server = servermgr.mapDomain(recipient.domain)
+ if not server:
+ # Cannot do server-to-server for this recipient.
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.3;No scheduling support for user")
+
+ # Process next recipient
+ continue
+
+ if not server.allow_to:
+ # Cannot do server-to-server outgoing requests for this server.
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
+
+ # Process next recipient
+ continue
+
+ groups.setdefault(server, []).append(recipient)
+
+ if len(groups) == 0:
+ returnValue(None)
+
+ # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+ # we will generate for each request. That way we can have parallel requests in progress
+ # rather than serialize them.
+ deferreds = []
+ for server, recipients in groups.iteritems():
+ requestor = ServerToServerRequest(self, server, recipients, responses)
+ deferreds.append(requestor.doRequest())
+
+ yield DeferredList(deferreds)
+
+ @inlineCallbacks
+ def generateLocalResponse(self, recipient, responses, autoresponses):
+ # Hash the iCalendar data for use as the last path element of the URI path
+ calendar_str = str(self.calendar)
+ name = md5.new(calendar_str + str(time.time()) + recipient.inbox.fp.path).hexdigest() + ".ics"
+
+ # Get a resource for the new item
+ childURL = joinURL(recipient.inboxURL, name)
+ child = yield self.request.locateResource(childURL)
+
+ # Copy calendar to inbox (doing fan-out)
+ try:
+ yield StoreCalendarObjectResource(
+ request=self.request,
+ destination = child,
+ destination_uri = childURL,
+ destinationparent = recipient.inbox,
+ destinationcal = True,
+ calendar = self.calendar,
+ isiTIP = True
+ ).run()
+ except:
+ # FIXME: Bare except
+ log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+ returnValue(False)
+ else:
+ responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+
+ # Store CALDAV:originator property
+ child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.originator.cuaddr)))
+
+ # Store CALDAV:recipient property
+ child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
+
+ # Look for auto-schedule option
+ if recipient.principal.autoSchedule():
+ autoresponses.append((recipient.principal, recipient.inbox, child))
+
+ returnValue(True)
+
+ @inlineCallbacks
+ def generateLocalFreeBusyResponse(self, recipient, responses, organizerProp, uid):
+
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = recipient.principal.calendarUserAddresses()
+ attendeeProp = self.calendar.getAttendeeProperty(cuas)
+
+ remote = isinstance(self.organizer, Scheduler.RemoteCalendarUser)
+
+ try:
+ fbresult = yield self.generateAttendeeFreeBusyResponse(
+ recipient,
+ organizerProp,
+ uid,
+ attendeeProp,
+ remote,
+ )
+ except:
+ log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+ returnValue(False)
+ else:
+ responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
+ returnValue(True)
+
+ @inlineCallbacks
+ def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, uid, attendeeProp, remote):
+
+ # Find the current recipients calendar-free-busy-set
+ fbset = yield recipient.principal.calendarFreeBusyURIs(self.request)
+
+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = ([], [], [])
+
+ # Process the availability property from the Inbox.
+ has_prop = yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.request)
+ if has_prop:
+ availability = yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.request)
+ availability = availability.calendar()
+ report_common.processAvailabilityFreeBusy(availability, fbinfo, self.timeRange)
+
+ # Check to see if the recipient is the same calendar user as the organizer.
+ # Needed for masked UID stuff.
+ if isinstance(self.organizer, Scheduler.LocalCalendarUser):
+ same_calendar_user = self.organizer.principal.principalURL() == recipient.principal.principalURL()
+ else:
+ same_calendar_user = False
+
+ # Now process free-busy set calendars
+ matchtotal = 0
+ for calendarResourceURL in fbset:
+ calendarResource = yield self.request.locateResource(calendarResourceURL)
+ if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
+ # We will ignore missing calendars. If the recipient has failed to
+ # properly manage the free busy set that should not prevent us from working.
+ continue
+
+ matchtotal = yield report_common.generateFreeBusyInfo(
+ self.request,
+ calendarResource,
+ fbinfo,
+ self.timeRange,
+ matchtotal,
+ excludeuid = self.excludeUID,
+ organizer = self.organizer.cuaddr,
+ same_calendar_user = same_calendar_user,
+ servertoserver=remote)
+
+ # Build VFREEBUSY iTIP reply for this recipient
+ fbresult = report_common.buildFreeBusyResult(
+ fbinfo,
+ self.timeRange,
+ organizer = organizerProp,
+ attendee = attendeeProp,
+ uid = uid,
+ method = "REPLY"
+ )
+
+ returnValue(fbresult)
+
+ def generateRemoteResponse(self):
+ raise NotImplementedError
+
+ def generateRemoteFreeBusyResponse(self):
+ raise NotImplementedError
+
+class CalDAVScheduler(Scheduler):
+
+ def checkAuthorization(self):
+ # Must have an authenticated user
+ if self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
+ log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ def checkOriginator(self):
+ """
+ Check the validity of the Originator header. Extract the corresponding principal.
+ """
+
+ # Verify that Originator is a valid calendar user
+ originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+ if originatorPrincipal is None:
+ # Local requests MUST have a principal.
+ log.err("Could not find principal for originator: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ else:
+ # Must have a valid Inbox.
+ inboxURL = originatorPrincipal.scheduleInboxURL()
+ if inboxURL is None:
+ log.err("Could not find inbox for originator: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ # Verify that Originator matches the authenticated user.
+ authn_principal = self.resource.currentPrincipal(self.request)
+ if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != authn_principal:
+ log.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ self.originator = Scheduler.LocalCalendarUser(self.originator, originatorPrincipal)
+
+ @inlineCallbacks
+ def checkRecipients(self):
+ """
+ Check the validity of the Recipient header values. Map these into local or
+ remote CalendarUsers.
+ """
+
+ results = []
+ for recipient in self.recipients:
+ # Get the principal resource for this recipient
+ principal = self.resource.principalForCalendarUserAddress(recipient)
+
+ # If no principal we may have a remote recipient but we should check whether
+ # the address is one that ought to be on our server and treat that as a missing
+ # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+ if principal is None:
+ if self.isCalendarUserAddressInMyDomain(recipient):
+ log.err("No schedulable principal for calendar user address: %s" % (recipient,))
+ results.append(Scheduler.InvalidCalendarUser(recipient))
+ elif not config.ServerToServer["Enabled"]:
+ log.err("Unknown calendar user address: %s" % (recipient,))
+ results.append(Scheduler.InvalidCalendarUser(recipient))
+ else:
+ results.append(Scheduler.RemoteCalendarUser(recipient))
+ else:
+ # Map recipient to their inbox
+ inbox = None
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL:
+ inbox = yield self.request.locateResource(inboxURL)
+
+ if inbox:
+ results.append(Scheduler.LocalCalendarUser(recipient, principal, inbox, inboxURL))
+ else:
+ log.err("No schedule inbox for principal: %s" % (principal,))
+ results.append(Scheduler.InvalidCalendarUser(recipient))
+
+ self.recipients = results
+
+ def checkOrganizer(self):
+ """
+ Check the validity of the ORGANIZER value. ORGANIZER must be local.
+ """
+
+ # Verify that the ORGANIZER's cu address maps to a valid user
+ organizer = self.calendar.getOrganizer()
+ if organizer:
+ organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+ if organizerPrincipal:
+ outboxURL = organizerPrincipal.scheduleOutboxURL()
+ if outboxURL:
+ self.organizer = Scheduler.LocalCalendarUser(organizer, organizerPrincipal)
+ else:
+ log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ elif self.isCalendarUserAddressInMyDomain(organizer):
+ log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ else:
+ self.organizer = Scheduler.RemoteCalendarUser(organizer)
+ else:
+ log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ def checkOrganizerAsOriginator(self):
+
+ # Make sure that the ORGANIZER is local
+ if not isinstance(self.organizer, Scheduler.LocalCalendarUser):
+ log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ # Make sure that the ORGANIZER's Outbox is the request URI
+ if self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+ log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ def checkAttendeeAsOriginator(self):
+ """
+ Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+ Only local attendees are allowed for message originating from this server.
+ """
+
+ # Verify that there is a single ATTENDEE property
+ attendees = self.calendar.getAttendees()
+
+ # Must have only one
+ if len(attendees) != 1:
+ log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ attendee = attendees[0]
+
+ # Attendee's Outbox MUST be the request URI
+ attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+ if attendeePrincipal:
+ aoutboxURL = attendeePrincipal.scheduleOutboxURL()
+ if aoutboxURL is None or aoutboxURL != self.request.uri:
+ log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ else:
+ log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ def securityChecks(self):
+ """
+ Check that the originator has the appropriate rights to send this type of iTIP message.
+ """
+
+ # Prevent spoofing of ORGANIZER with specific METHODs when local
+ if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+ self.checkOrganizerAsOriginator()
+
+ # Prevent spoofing when doing reply-like METHODs
+ elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+ self.checkAttendeeAsOriginator()
+
+ else:
+ log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+class ServerToServerScheduler(Scheduler):
+
+ def checkAuthorization(self):
+ # Must have an unauthenticated user
+ if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+ log.err("Authenticated originators not allowed: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ def checkOriginator(self):
+ """
+ Check the validity of the Originator header.
+ """
+
+ # For remote requests we do not allow the originator to be a local user or one within our domain.
+ originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+ if originatorPrincipal or self.isCalendarUserAddressInMyDomain(self.originator):
+ log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ else:
+ self.originator = Scheduler.RemoteCalendarUser(self.originator)
+
+ # We will only accept originator in known domains.
+ servermgr = ServerToServer()
+ server = servermgr.mapDomain(self.originator.domain)
+ if not server or not server.allow_from:
+ log.err("Originator not on recognized server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ else:
+ # Get the request IP and map to hostname.
+ clientip = self.request.remoteAddr.host
+
+ # First compare as dotted IP
+ matched = False
+ compare_with = (server.host,) + tuple(server.client_hosts)
+ if clientip in compare_with:
+ matched = True
+ else:
+ # Now do hostname lookup
+ host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+ for host in itertools.chain((host,), aliases):
+ # Try simple match first
+ if host in compare_with:
+ matched = True
+ break
+
+ # Try pattern match next
+ for pattern in compare_with:
+ try:
+ if re.match(pattern, host) is not None:
+ matched = True
+ break
+ except re.error:
+ log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
+ else:
+ continue
+ break
+
+ if not matched:
+ log.err("Originator not on allowed server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ @inlineCallbacks
+ def checkRecipients(self):
+ """
+ Check the validity of the Recipient header values. These must all be local as there
+ is no concept of server-to-server relaying.
+ """
+
+ results = []
+ for recipient in self.recipients:
+ # Get the principal resource for this recipient
+ principal = self.resource.principalForCalendarUserAddress(recipient)
+
+ # If no principal we may have a remote recipient but we should check whether
+ # the address is one that ought to be on our server and treat that as a missing
+ # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+ if principal is None:
+ if self.isCalendarUserAddressInMyDomain(recipient):
+ log.err("No principal for calendar user address: %s" % (recipient,))
+ results.append(Scheduler.InvalidCalendarUser(recipient))
+ else:
+ log.err("Unknown calendar user address: %s" % (recipient,))
+ results.append(Scheduler.InvalidCalendarUser(recipient))
+ else:
+ # Map recipient to their inbox
+ inbox = None
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL:
+ inbox = yield self.request.locateResource(inboxURL)
+
+ if inbox:
+ results.append(Scheduler.LocalCalendarUser(recipient, principal, inbox, inboxURL))
+ else:
+ log.err("No schedule inbox for principal: %s" % (principal,))
+ results.append(Scheduler.InvalidCalendarUser(recipient))
+
+ self.recipients = results
+
+ def checkOrganizer(self):
+ """
+ Delay ORGANIZER check until we know what their role is.
+ """
+ pass
+
+ def checkOrganizerAsOriginator(self):
+ """
+ Check the validity of the ORGANIZER value. ORGANIZER must not be local.
+ """
+
+ # Verify that the ORGANIZER's cu address does not map to a valid user
+ organizer = self.calendar.getOrganizer()
+ if organizer:
+ organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+ if organizerPrincipal:
+ log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ elif self.isCalendarUserAddressInMyDomain(organizer):
+ log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ else:
+ self.organizer = Scheduler.RemoteCalendarUser(organizer)
+ else:
+ log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ def checkAttendeeAsOriginator(self):
+ """
+ Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+ Only local attendees are allowed for message originating from this server.
+ """
+
+ # Verify that there is a single ATTENDEE property
+ attendees = self.calendar.getAttendees()
+
+ # Must have only one
+ if len(attendees) != 1:
+ log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ attendee = attendees[0]
+
+ # Attendee cannot be local.
+ attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+ if attendeePrincipal:
+ log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ elif self.isCalendarUserAddressInMyDomain(attendee):
+ log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ # TODO: in this case we should check that the ORGANIZER is the sole recipient.
+
+ def securityChecks(self):
+ """
+ Check that the originator has the appropriate rights to send this type of iTIP message.
+ """
+
+ # Prevent spoofing of ORGANIZER with specific METHODs when local
+ if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+ self.checkOrganizerAsOriginator()
+
+ # Prevent spoofing when doing reply-like METHODs
+ elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+ self.checkAttendeeAsOriginator()
+
+ else:
+ log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+class ScheduleResponseResponse (Response):
+ """
+ ScheduleResponse L{Response} object.
+ Renders itself as a CalDAV:schedule-response XML document.
+ """
+ def __init__(self, xml_responses, location=None):
+ """
+ @param xml_responses: an iterable of davxml.Response objects.
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+
+ Response.__init__(self, code=responsecode.OK,
+ stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ if location is not None:
+ self.headers.setHeader("location", location)
+
+class ScheduleResponseQueue (LoggingMixIn):
+ """
+ Stores a list of (typically error) responses for use in a
+ L{ScheduleResponse}.
+ """
+ def __init__(self, method, success_response):
+ """
+ @param method: the name of the method generating the queue.
+ @param success_response: the response to return in lieu of a
+ L{ScheduleResponse} if no responses are added to this queue.
+ """
+ self.responses = []
+ self.method = method
+ self.success_response = success_response
+ self.location = None
+
+ def setLocation(self, location):
+ """
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+ self.location = location
+
+ def add(self, recipient, what, reqstatus=None, calendar=None):
+ """
+ Add a response.
+ @param recipient: the recipient for this response.
+ @param what: a status code or a L{Failure} for the given recipient.
+ @param status: the iTIP request-status for the given recipient.
+ @param calendar: the calendar data for the given recipient response.
+ """
+ if type(what) is int:
+ code = what
+ error = None
+ message = responsecode.RESPONSES[code]
+ elif isinstance(what, Failure):
+ code = statusForFailure(what)
+ error = errorForFailure(what)
+ message = messageForFailure(what)
+ else:
+ raise AssertionError("Unknown data type: %r" % (what,))
+
+ if code > 400: # Error codes only
+ self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
+
+ children = []
+ children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
+ children.append(caldavxml.RequestStatus(reqstatus))
+ if calendar is not None:
+ children.append(caldavxml.CalendarData.fromCalendar(calendar))
+ if error is not None:
+ children.append(error)
+ if message is not None:
+ children.append(davxml.ResponseDescription(message))
+ self.responses.append(caldavxml.Response(*children))
+
+ def clone(self, clone):
+ """
+ Add a response cloned from an existing caldavxml.Response element.
+ @param clone: the response to clone.
+ """
+ if not isinstance(clone, caldavxml.Response):
+ raise AssertionError("Incorrect element type: %r" % (clone,))
+
+ recipient = clone.childOfType(caldavxml.Recipient)
+ request_status = clone.childOfType(caldavxml.RequestStatus)
+ calendar_data = clone.childOfType(caldavxml.CalendarData)
+ error = clone.childOfType(davxml.Error)
+ desc = clone.childOfType(davxml.ResponseDescription)
+
+ children = []
+ children.append(recipient)
+ children.append(request_status)
+ if calendar_data is not None:
+ children.append(calendar_data)
+ if error is not None:
+ children.append(error)
+ if desc is not None:
+ children.append(desc)
+ self.responses.append(caldavxml.Response(*children))
+
+ def response(self):
+ """
+ Generate a L{ScheduleResponseResponse} with the responses contained in the
+ queue or, if no such responses, return the C{success_response} provided
+ to L{__init__}.
+ @return: the response.
+ """
+ if self.responses:
+ return ScheduleResponseResponse(self.responses, self.location)
+ else:
+ return self.success_response
Copied: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserver.py (from rev 2525, CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/servertoserver.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserver.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserver.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -0,0 +1,160 @@
+##
+# Copyright (c) 2005-2007 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.
+##
+
+"""
+Server to server utility functions and client requests.
+"""
+
+__all__ = [
+ "ServerToServer",
+ "ServerToServerRequest",
+]
+
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.protocol import ClientCreator
+from twisted.python.failure import Failure
+from twisted.python.filepath import FilePath
+from twisted.web2 import responsecode
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.util import davXMLFromStream
+from twisted.web2.http import HTTPError
+from twisted.web2.http_headers import Headers
+from twisted.web2.http_headers import MimeType
+
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.servertoserverparser import ServerToServerParser
+from twistedcaldav import caldavxml
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+class ServerToServer(object):
+
+ _fileInfo = None
+ _xmlFile = None
+ _servers = None
+ _domainMap = None
+
+ def __init__(self):
+
+ self._loadConfig()
+
+ def _loadConfig(self):
+ if ServerToServer._servers is None:
+ ServerToServer._xmlFile = FilePath(config.ServerToServer["Servers"])
+ ServerToServer._xmlFile.restat()
+ fileInfo = (ServerToServer._xmlFile.getmtime(), ServerToServer._xmlFile.getsize())
+ if fileInfo != ServerToServer._fileInfo:
+ parser = ServerToServerParser(ServerToServer._xmlFile)
+ ServerToServer._servers = parser.servers
+ self._mapDomains()
+ ServerToServer._fileInfo = fileInfo
+
+ def _mapDomains(self):
+ ServerToServer._domainMap = {}
+ for server in ServerToServer._servers:
+ for domain in server.domains:
+ ServerToServer._domainMap[domain] = server
+
+ def mapDomain(self, domain):
+ """
+ Map a calendar user address domain to a suitable server that can
+ handle server-to-server requests for that user.
+ """
+ return ServerToServer._domainMap.get(domain)
+
+class ServerToServerRequest(object):
+
+ def __init__(self, scheduler, server, recipients, responses):
+
+ self.scheduler = scheduler
+ self.server = server
+ self.recipients = recipients
+ self.responses = responses
+
+ self._generateHeaders()
+ self._prepareData()
+
+ @inlineCallbacks
+ def doRequest(self):
+
+ # Generate an HTTP client request
+ try:
+ from twisted.internet import reactor
+ if self.server.ssl:
+ from tap import ChainingOpenSSLContextFactory
+ context = ChainingOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, certificateChainFile=config.SSLAuthorityChain)
+ proto = yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context)
+ else:
+ proto = yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port)
+
+ request = ClientRequest("POST", self.server.path, self.headers, self.data)
+ yield log.logRequest("debug", "Sending server-to-server POST request:", request)
+ response = yield proto.submitRequest(request)
+
+ yield log.logResponse("debug", "Received server-to-server POST response:", response)
+ xml = yield davXMLFromStream(response.stream)
+
+ self._parseResponse(xml)
+ except Exception, e:
+ # Generated failed responses for each recipient
+ log.err("Could not do server-to-server request : %s %s" % (self, e))
+ for recipient in self.recipients:
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
+
+ def _generateHeaders(self):
+ self.headers = Headers()
+ self.headers.setHeader('Host', self.server.host + ":%s" % (self.server.port,))
+ self.headers.addRawHeader('Originator', self.scheduler.originator.cuaddr)
+ self._doAuthentication()
+ for recipient in self.recipients:
+ self.headers.addRawHeader('Recipient', recipient.cuaddr)
+ self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
+
+ def _doAuthentication(self):
+ if self.server.authentication and self.server.authentication[0] == "basic":
+ self.headers.setHeader(
+ 'Authorization',
+ ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+ )
+
+ def _prepareData(self):
+ self.data = str(self.scheduler.calendar)
+
+ def _parseResponse(self, xml):
+
+ # Check for correct root element
+ schedule_response = xml.root_element
+ if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
+ raise HTTPError(responsecode.BAD_REQUEST)
+
+ # Parse each response - do this twice: once looking for errors that will
+ # result in all recipients shown as failures; the second loop adds all the
+ # valid responses to the actual result.
+ for response in schedule_response.children:
+ if not isinstance(response, caldavxml.Response) or not response.children:
+ raise HTTPError(responsecode.BAD_REQUEST)
+ recipient = response.childOfType(caldavxml.Recipient)
+ request_status = response.childOfType(caldavxml.RequestStatus)
+ if not recipient or not request_status:
+ raise HTTPError(responsecode.BAD_REQUEST)
+ for response in schedule_response.children:
+ self.responses.clone(response)
Copied: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserverparser.py (from rev 2525, CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/servertoserverparser.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserverparser.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/servertoserverparser.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -0,0 +1,165 @@
+##
+# Copyright (c) 2006-2007 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.
+##
+
+
+"""
+XML based server-to-server configuration file handling.
+"""
+
+__all__ = [
+ "ServerToServerParser",
+ "ServerToServerRecord",
+]
+
+import xml.dom.minidom
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_URI = "uri"
+ELEMENT_AUTHENTICATION = "authentication"
+ATTRIBUTE_TYPE = "type"
+ATTRIBUTE_BASICAUTH = "basic"
+ELEMENT_USER = "user"
+ELEMENT_PASSWORD = "password"
+ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
+ELEMENT_DOMAINS = "domains"
+ELEMENT_DOMAIN = "domain"
+ELEMENT_CLIENT_HOSTS = "hosts"
+ELEMENT_HOST = "host"
+
+class ServerToServerParser(object):
+ """
+ Server-to-server configuration file parser.
+ """
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+ def __init__(self, xmlFile):
+
+ self.servers = []
+
+ # Read in XML
+ fd = open(xmlFile.path, "r")
+ doc = xml.dom.minidom.parse(fd)
+ fd.close()
+
+ # Verify that top-level element is correct
+ servers_node = doc._get_documentElement()
+ if servers_node._get_localName() != ELEMENT_SERVERS:
+ log.error("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
+ return
+ self._parseXML(servers_node)
+
+ def _parseXML(self, node):
+ """
+ Parse the XML root node from the server-to-server configuration document.
+ @param node: the L{Node} to parse.
+ """
+
+ for child in node._get_childNodes():
+ child_name = child._get_localName()
+ if child_name is None:
+ continue
+ elif child_name == ELEMENT_SERVER:
+ self.servers.append(ServerToServerRecord())
+ self.servers[-1].parseXML(child)
+
+class ServerToServerRecord (object):
+ """
+ Contains server-to-server details.
+ """
+ def __init__(self):
+ """
+ @param recordType: record type for directory entry.
+ """
+ self.uri = ""
+ self.authentication = None
+ self.allow_from = False
+ self.allow_to = True
+ self.domains = []
+ self.client_hosts = []
+
+ def parseXML(self, node):
+ for child in node._get_childNodes():
+ child_name = child._get_localName()
+ if child_name is None:
+ continue
+ elif child_name == ELEMENT_URI:
+ if child.firstChild is not None:
+ self.uri = child.firstChild.data.encode("utf-8")
+ elif child_name == ELEMENT_AUTHENTICATION:
+ self._parseAuthentication(child)
+ elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
+ self.allow_from = True
+ elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
+ self.allow_to = True
+ elif child_name == ELEMENT_DOMAINS:
+ self._parseList(child, ELEMENT_DOMAIN, self.domains)
+ elif child_name == ELEMENT_CLIENT_HOSTS:
+ self._parseList(child, ELEMENT_HOST, self.client_hosts)
+ else:
+ raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
+
+ self._parseDetails()
+
+ def _parseList(self, node, element_name, appendto):
+ for child in node._get_childNodes():
+ if child._get_localName() == element_name:
+ if child.firstChild is not None:
+ appendto.append(child.firstChild.data.encode("utf-8"))
+
+ def _parseAuthentication(self, node):
+ if node.hasAttribute(ATTRIBUTE_TYPE):
+ type = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
+ if type != ATTRIBUTE_BASICAUTH:
+ return
+ else:
+ return
+
+ for child in node._get_childNodes():
+ if child._get_localName() == ELEMENT_USER:
+ if child.firstChild is not None:
+ user = child.firstChild.data.encode("utf-8")
+ elif child._get_localName() == ELEMENT_PASSWORD:
+ if child.firstChild is not None:
+ password = child.firstChild.data.encode("utf-8")
+
+ self.authentication = ("basic", user, password,)
+
+ def _parseDetails(self):
+ # Extract scheme, host, port and path
+ if self.uri.startswith("http://"):
+ self.ssl = False
+ rest = self.uri[7:]
+ elif self.uri.startswith("https://"):
+ self.ssl = True
+ rest = self.uri[8:]
+
+ splits = rest.split("/", 1)
+ hostport = splits[0].split(":")
+ self.host = hostport[0]
+ if len(hostport) > 1:
+ self.port = int(hostport[1])
+ else:
+ self.port = {False:80, True:443}[self.ssl]
+ self.path = "/"
+ if len(splits) > 1:
+ self.path += splits[1]
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/static.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/static.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -56,11 +56,12 @@
from twistedcaldav.config import config
from twistedcaldav.extensions import DAVFile
from twistedcaldav.extensions import CachingXattrPropertyStore
+from twistedcaldav.freebusyurl import FreeBusyURLResource
from twistedcaldav.ical import Component as iComponent
from twistedcaldav.ical import Property as iProperty
from twistedcaldav.index import Index, IndexSchedule
from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, ScheduleServerToServerResource
from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
from twistedcaldav.directory.calendar import uidsResourceName
from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -573,10 +574,16 @@
else:
DropBoxHomeFileClass = None
+ if config.FreeBusyURL["Enabled"]:
+ FreeBusyURLFileClass = FreeBusyURLFile
+ else:
+ FreeBusyURLFileClass = None
+
cls = {
"inbox" : ScheduleInboxFile,
"outbox" : ScheduleOutboxFile,
"dropbox" : DropBoxHomeFileClass,
+ "freebusy" : FreeBusyURLFileClass,
}.get(name, None)
if cls is not None:
@@ -680,6 +687,80 @@
def __repr__(self):
return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
+class ServerToServerInboxFile (ScheduleServerToServerResource, CalDAVFile):
+ """
+ Server-to-server scheduling inbox resource.
+ """
+ def __init__(self, path, parent):
+ CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+ ScheduleServerToServerResource.__init__(self, parent)
+
+ self.fp.open("w").close()
+ self.fp.restat(False)
+
+ def __repr__(self):
+ return "<%s (server-to-server inbox resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+ def isCollection(self):
+ return False
+
+ def createSimilarFile(self, path):
+ if path == self.fp.path:
+ return self
+ else:
+ return responsecode.NOT_FOUND
+
+ def http_PUT (self, request): return responsecode.FORBIDDEN
+ def http_COPY (self, request): return responsecode.FORBIDDEN
+ def http_MOVE (self, request): return responsecode.FORBIDDEN
+ def http_DELETE (self, request): return responsecode.FORBIDDEN
+ def http_MKCOL (self, request): return responsecode.FORBIDDEN
+
+ def http_MKCALENDAR(self, request):
+ return ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "calendar-collection-location-ok")
+ )
+
+class FreeBusyURLFile (AutoProvisioningFileMixIn, FreeBusyURLResource, CalDAVFile):
+ """
+ Free-busy URL resource.
+ """
+ def __init__(self, path, parent):
+ CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+ FreeBusyURLResource.__init__(self, parent)
+
+ def __repr__(self):
+ return "<%s (free-busy URL resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+ def isCollection(self):
+ return False
+
+ def createSimilarFile(self, path):
+ if path == self.fp.path:
+ return self
+ else:
+ return responsecode.NOT_FOUND
+
+ def http_PUT (self, request): return responsecode.FORBIDDEN
+ def http_COPY (self, request): return responsecode.FORBIDDEN
+ def http_MOVE (self, request): return responsecode.FORBIDDEN
+ def http_DELETE (self, request): return responsecode.FORBIDDEN
+ def http_MKCOL (self, request): return responsecode.FORBIDDEN
+
+ def http_MKCALENDAR(self, request):
+ return ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "calendar-collection-location-ok")
+ )
+
+ ##
+ # ACL
+ ##
+
+ def supportedPrivileges(self, request):
+ return succeed(schedulePrivilegeSet)
+
class DropBoxHomeFile (AutoProvisioningFileMixIn, DropBoxHomeResource, CalDAVFile):
def __init__(self, path, parent):
DropBoxHomeResource.__init__(self)
Modified: CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/tap.py 2008-06-03 20:45:25 UTC (rev 2527)
+++ CalendarServer/branches/users/cdaboo/server2server-2524/twistedcaldav/tap.py 2008-06-03 20:48:15 UTC (rev 2528)
@@ -53,6 +53,7 @@
from twistedcaldav.directory.aggregate import AggregateDirectoryService
from twistedcaldav.directory.sudo import SudoDirectoryService
from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import ServerToServerInboxFile
from twistedcaldav.static import TimezoneServiceFile
from twistedcaldav.timezones import TimezoneCache
from twistedcaldav import pdmonster
@@ -454,6 +455,7 @@
rootResourceClass = RootResource
principalResourceClass = DirectoryPrincipalProvisioningResource
calendarResourceClass = CalendarHomeProvisioningFile
+ servertoserverResourceClass = ServerToServerInboxFile
timezoneServiceResourceClass = TimezoneServiceFile
def makeService_Slave(self, options):
@@ -528,7 +530,7 @@
root.putChild('principals', principalCollection)
root.putChild('calendars', calendarCollection)
- # Timezone service is optional
+ # Timezone service is optional
if config.EnableTimezoneService:
timezoneService = self.timezoneServiceResourceClass(
os.path.join(config.DocumentRoot, "timezones"),
@@ -536,6 +538,16 @@
)
root.putChild('timezones', timezoneService)
+ # Server-to-server service is optional
+ if config.ServerToServer["Enabled"]:
+ log.msg("Setting up server-to-server resource: %r" % (self.servertoserverResourceClass,))
+
+ servertoserver = self.servertoserverResourceClass(
+ os.path.join(config.DocumentRoot, 'inbox'),
+ root,
+ )
+ root.putChild('inbox', servertoserver)
+
# Configure default ACLs on the root resource
log.info("Setting up default ACEs on root resource")
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080603/794e0f7d/attachment-0001.htm
More information about the calendarserver-changes
mailing list