[CalendarServer-changes] [2990] CalendarServer/trunk/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Fri Sep 12 09:30:41 PDT 2008
Revision: 2990
http://trac.macosforge.org/projects/calendarserver/changeset/2990
Author: cdaboo at apple.com
Date: 2008-09-12 09:30:40 -0700 (Fri, 12 Sep 2008)
Log Message:
-----------
Merged implicitauto brnach to trunk. This fixes auto-accept and proxy implicit scheduling behavior.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/itip.py
CalendarServer/trunk/twistedcaldav/memcacher.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
Added Paths:
-----------
CalendarServer/trunk/twistedcaldav/memcachelock.py
CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -1291,7 +1291,18 @@
if component.name() == "VTIMEZONE":
continue
component.addProperty(property)
+
+ def replacePropertyInAllComponents(self, property):
+ """
+ Replace a property in all components.
+ @param property: the L{Property} to replace in this component.
+ """
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ component.replaceProperty(property)
+
def attendeesView(self, attendees):
"""
Filter out any components that all attendees are not present in. Use EXDATEs
@@ -1338,11 +1349,10 @@
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
- if self.name() == "VCALENDAR":
- for component in self.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value() != attendee]
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value() != attendee]
def removeAlarms(self):
"""
@@ -1366,14 +1376,13 @@
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
- if self.name() == "VCALENDAR":
- for component in self.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- if keep:
- [component.removeProperty(p) for p in tuple(component.properties()) if p.name() not in keep]
- if remove:
- [component.removeProperty(p) for p in tuple(component.properties()) if p.name() in remove]
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ if keep:
+ [component.removeProperty(p) for p in tuple(component.properties()) if p.name() not in keep]
+ if remove:
+ [component.removeProperty(p) for p in tuple(component.properties()) if p.name() in remove]
def removeXProperties(self, keep_properties):
"""
@@ -1382,15 +1391,14 @@
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
- if self.name() == "VCALENDAR":
- for component in self.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- [
- component.removeProperty(p)
- for p in tuple(component.properties())
- if p.name().startswith("X-") and p.name() not in keep_properties
- ]
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ [
+ component.removeProperty(p)
+ for p in tuple(component.properties())
+ if p.name().startswith("X-") and p.name() not in keep_properties
+ ]
def removePropertyParameters(self, property, params):
"""
@@ -1399,18 +1407,34 @@
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
- if self.name() == "VCALENDAR":
- for component in self.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- props = component.properties(property)
- for prop in props:
- for param in params:
- try:
- del prop.params()[param]
- except KeyError:
- pass
-
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ props = component.properties(property)
+ for prop in props:
+ for param in params:
+ try:
+ del prop.params()[param]
+ except KeyError:
+ pass
+
+ def normalizePropertyValueLists(self, propname):
+ """
+ Convert properties that have a list of values into single properties, to make it easier
+ to do comparisons between two ical objects.
+ """
+
+ assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ for prop in tuple(component.properties(propname)):
+ if type(prop.value()) is list and len(prop.value()) > 1:
+ component.removeProperty(prop)
+ for value in prop.value():
+ component.addProperty(Property(propname, [value,]))
+
##
# Dates and date-times
##
Modified: CalendarServer/trunk/twistedcaldav/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/itip.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/itip.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -35,7 +35,8 @@
import time
from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred,\
+ succeed
from twisted.web2.dav import davxml
from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
from twisted.web2.dav.util import joinURL
@@ -45,7 +46,8 @@
from twistedcaldav import caldavxml
from twistedcaldav.accounting import accountingEnabled, emitAccounting
from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
+from twistedcaldav.ical import Property
+from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.method import report_common
from twistedcaldav.resource import isCalendarCollectionResource
@@ -63,6 +65,7 @@
class iTipProcessor(object):
+ @inlineCallbacks
def handleRequest(self, request, principal, inbox, calendar, child):
"""
Handle an iTIP response automatically.
@@ -93,7 +96,19 @@
else:
self.childname = ""
- return f()
+ # Get a lock on the inbox first
+ _lock = MemcacheLock("iTIPAutoProcess", inbox.fp.path, timeout=60.0, retry_interval=1.0, expire_time=300)
+
+ try:
+ yield _lock.acquire()
+ yield f()
+ yield _lock.release()
+ except MemcacheLockTimeoutError:
+ raise
+ except Exception, e:
+ log.error(e)
+ yield _lock.clean()
+ raise
@inlineCallbacks
def processRequest(self):
@@ -108,10 +123,8 @@
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
+ 1. If not, add to f-b-s calendar DECLINED
+ 2. If so, add to f-b-s calendar ACCEPTED
2. If not,
1. remove the one we got - its 'stale'
3. Delete the request from the Inbox.
@@ -153,30 +166,24 @@
if check_reply:
# Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield self.checkForReply()
+ valid, accepted = yield self.checkForReply()
- try:
- if accepted:
+ if valid:
+ try:
if calmatch:
+ log.info("Replaced calendar component %s with new iTIP message in %s (%s)." % (calmatch, calURL, accepted,))
yield self.writeResource(calURL, updatecal, calmatch, self.calendar)
- log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
else:
+ log.info("Added new calendar component in %s (%s)." % (calURL, accepted,))
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()
+
+ processed = "processed"
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+ else:
+ processed = "ignored"
else:
# So we have a partial update. That means we have to do partial updates to instances in
@@ -215,29 +222,26 @@
if check_reply:
# Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield self.checkForReply()
+ valid, accepted = yield self.checkForReply()
- 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:
+ if valid:
+ try:
+ if calmatch:
+ # Merge the new instances with the old ones
+ self.mergeComponents(self.calendar, cal)
+ log.info("Merged calendar component %s with new iTIP message in %s (%s)." % (calmatch, calURL, accepted,))
+ yield self.writeResource(calURL, updatecal, calmatch, cal)
+ else:
+ log.info("Added new calendar component in %s (%s)." % (calURL, 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"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
+ processed = "processed"
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+ else:
+ processed = "ignored"
# Remove the now processed incoming request.
if self.inbox:
@@ -315,8 +319,8 @@
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))
+ yield self.deleteResource(updatecal, calmatch,)
except:
# FIXME: bare except
log.err("Error while auto-processing iTIP: %s" % (Failure(),))
@@ -371,12 +375,12 @@
# 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))
+ yield self.deleteResource(updatecal, calmatch)
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))
+ yield self.writeResource(calURL, updatecal, calmatch, existing_calendar)
processed = "processed"
else:
processed = "older"
@@ -405,7 +409,7 @@
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.
- @return: C{True} if a reply is needed, C{False} otherwise.
+ @return: a C{tuple} of C{bool} indicating whether a valid iTIP was received, and C{str} new partstat.
"""
# We need to figure out whether the specified component will clash with any others in the f-b-set calendars
@@ -454,58 +458,21 @@
cuas = self.principal.calendarUserAddresses()
attendeeProps = self.calendar.getAttendeeProperties(cuas)
if not attendeeProps:
- returnValue((False, None, accepted))
+ returnValue((False, "",))
- # 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"]
-
if accepted:
partstat = "ACCEPTED"
else:
partstat = "DECLINED"
+
+ # Make sure declined events are TRANSPARENT on the calendar
+ self.calendar.replacePropertyInAllComponents(Property("TRANSP", "TRANSPARENT"))
+
for attendeeProp in attendeeProps:
- if "PARTSTAT" in attendeeProp.params():
- attendeeProp.params()["PARTSTAT"][0] = partstat
- else:
- attendeeProp.params()["PARTSTAT"] = [partstat]
+ 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."))
+ returnValue((True, partstat,))
- # 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))
-
@inlineCallbacks
def writeReply(self, replycal):
"""
@@ -583,7 +550,8 @@
destinationparent = collection,
destinationcal = True,
calendar = calendar,
- isiTIP = itipper
+ isiTIP = itipper,
+ internal_request=True,
).run()
returnValue(newchild)
@@ -614,9 +582,9 @@
def deleteInboxResource(self, processed_state):
# Remove the now processed incoming request.
try:
- yield self.deleteResource(self.inbox, self.childname)
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." %
+ log.info("Deleting new iTIP message %s in Inbox because it has been %s." %
(self.childname, processed_state,))
+ yield self.deleteResource(self.inbox, self.childname)
except:
# FIXME: bare except
log.err("Error while auto-processing iTIP: %s" % (Failure(),))
@@ -632,6 +600,12 @@
"""
delchild = collection.getChild(name)
+
+ # Sometimes the resource might already be gone...
+ if delchild is None:
+ log.warn("Nothing to delete: %s in %s is missing." % (name, collection))
+ return succeed(None)
+
index = collection.index()
index.deleteResource(delchild.fp.basename())
Copied: CalendarServer/trunk/twistedcaldav/memcachelock.py (from rev 2989, CalendarServer/branches/users/cdaboo/implicitauto-2947/twistedcaldav/memcachelock.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcachelock.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/memcachelock.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -0,0 +1,117 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.memcacher import Memcacher
+from twisted.internet.defer import inlineCallbacks, Deferred, returnValue,\
+ succeed
+from twisted.internet import reactor
+import time
+
+class MemcacheLock(Memcacher):
+
+ def __init__(self, namespace, locktoken, timeout=5.0, retry_interval=0.1, expire_time=0):
+ """
+
+ @param namespace: a unique namespace for this lock's tokens
+ @type namespace: C{str}
+ @param locktoken: the name of the locktoken
+ @type locktoken: C{str}
+ @param timeout: the maximum time in seconds that the lock should block
+ @type timeout: C{float}
+ @param retry_interval: the interval to retry acquiring the lock
+ @type retry_interval: C{float}
+ @param expiryTime: the time in seconds for the lock to expire. Zero: no expiration.
+ @type expiryTime: C{float}
+ """
+
+ super(MemcacheLock, self).__init__(namespace)
+ self._locktoken = locktoken
+ self._timeout = timeout
+ self._retry_interval = retry_interval
+ self._expire_time = expire_time
+ self._hasLock = False
+
+ def _getMemcacheProtocol(self):
+
+ result = super(MemcacheLock, self)._getMemcacheProtocol()
+
+ if isinstance(result, Memcacher.nullCacher):
+ raise AssertionError("No implementation of shared locking without memcached")
+
+ return result
+
+ @inlineCallbacks
+ def acquire(self):
+
+ assert not self._hasLock, "Lock already acquired."
+
+ start_time = time.time()
+ waiting = False
+ while time.time() < start_time + self._timeout:
+
+ result = (yield self.add(self._locktoken, "1", self._expire_time))
+ if result:
+ self._hasLock = True
+ if waiting:
+ self.log_debug("Got lock after waiting on %s" % (self._locktoken,))
+ break
+
+ waiting = True
+ self.log_debug("Waiting for lock on %s" % (self._locktoken,))
+ pause = Deferred()
+ def _timedDeferred():
+ pause.callback(True)
+ reactor.callLater(self._retry_interval, _timedDeferred)
+ yield pause
+ else:
+ self.log_debug("Timed out lock after waiting on %s" % (self._locktoken,))
+ raise MemcacheLockTimeoutError()
+
+ returnValue(True)
+
+ def release(self):
+
+ assert self._hasLock, "Lock not acquired."
+
+ def _done(result):
+ self._hasLock = False
+ return result
+
+ d = self.delete(self._locktoken)
+ d.addCallback(_done)
+ return d
+
+ def clean(self):
+
+ if self._hasLock:
+ return self.delete(self._locktoken)
+ else:
+ return succeed(True)
+
+ def locked(self):
+ """
+ Test if the lock is currently being held.
+ """
+
+ def _gotit(value):
+ return value is not None
+
+ d = self.get(self._locktoken)
+ d.addCallback(_gotit)
+ return d
+
+class MemcacheLockTimeoutError(Exception):
+ pass
Modified: CalendarServer/trunk/twistedcaldav/memcacher.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcacher.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/memcacher.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -34,7 +34,15 @@
def __init__(self):
self._cache = {}
- def set(self, key, value):
+
+ def add(self, key, value, expireTime=0):
+ if key not in self._cache:
+ self._cache[key] = value
+ return succeed(True)
+ else:
+ return succeed(False)
+
+ def set(self, key, value, expireTime=0):
self._cache[key] = value
return succeed(True)
@@ -59,9 +67,12 @@
does not actually cache anything.
"""
- def set(self, key, value):
+ def add(self, key, value, expireTime=0):
return succeed(True)
+ def set(self, key, value, expireTime=0):
+ return succeed(True)
+
def get(self, key):
return succeed((0, None,))
@@ -109,12 +120,21 @@
return self._memcacheProtocol
- def set(self, key, value):
+ def add(self, key, value, expire_time=0):
my_value = value
if self._pickle:
my_value = cPickle.dumps(value)
+ self.log_debug("Adding Cache Token for %r" % (key,))
+ return self._getMemcacheProtocol().add(
+ '%s:%s' % (self._namespace, key), my_value, expireTime=expire_time)
+
+ def set(self, key, value, expire_time=0):
+ my_value = value
+ if self._pickle:
+ my_value = cPickle.dumps(value)
+ self.log_debug("Setting Cache Token for %r" % (key,))
return self._getMemcacheProtocol().set(
- '%s:%s' % (self._namespace, key), my_value)
+ '%s:%s' % (self._namespace, key), my_value, expireTime=expire_time)
def get(self, key):
def _gotit(result):
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -183,6 +183,7 @@
calendar=None,
isiTIP=False,
allowImplicitSchedule=True,
+ internal_request=False,
):
"""
Function that does common PUT/COPY/MOVE behavior.
@@ -201,6 +202,7 @@
@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.
@param allowImplicitSchedule: True if implicit scheduling should be attempted, False otherwise.
+ @param internal_request: True if this request originates internally and needs to bypass scheduling authorization checks.
"""
# Check that all arguments are valid
@@ -239,6 +241,7 @@
self.deletesource = deletesource
self.isiTIP = isiTIP
self.allowImplicitSchedule = allowImplicitSchedule
+ self.internal_request = internal_request
self.rollback = None
self.access = None
@@ -690,7 +693,7 @@
# Do scheduling
if not self.isiTIP and self.allowImplicitSchedule:
scheduler = ImplicitScheduler()
- new_calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False))
+ new_calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False, internal_request=self.internal_request))
if new_calendar:
self.calendar = new_calendar
self.calendardata = str(self.calendar)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -135,7 +135,7 @@
for principal, inbox, child in autoresponses:
# Add delayed reactor task to handle iTIP responses
itip = iTipProcessor()
- reactor.callLater(0.0, itip.handleRequest, *(self.scheduler.request, principal, inbox, self.scheduler.calendar.duplicate(), child))
+ reactor.callLater(2.0, itip.handleRequest, *(self.scheduler.request, principal, inbox, self.scheduler.calendar.duplicate(), child))
@inlineCallbacks
def generateResponse(self, recipient, responses, autoresponses):
@@ -147,20 +147,24 @@
childURL = joinURL(recipient.inboxURL, name)
child = (yield self.scheduler.request.locateResource(childURL))
- # Do implicit scheduling message processing
- try:
- processor = ImplicitProcessor()
- processed, autoprocessed = (yield processor.doImplicitProcessing(
- self.scheduler.request,
- self.scheduler.calendar,
- self.scheduler.originator,
- recipient
- ))
- except ImplicitProcessorException, e:
- 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=e.msg)
- returnValue(False)
+ # Do implicit scheduling message processing - but not for auto-accept principals.
+ # Auto-accepts will be processed internally by the server a little later on.
+ if not recipient.principal.autoSchedule():
+ try:
+ processor = ImplicitProcessor()
+ processed, autoprocessed = (yield processor.doImplicitProcessing(
+ self.scheduler.request,
+ self.scheduler.calendar,
+ self.scheduler.originator,
+ recipient
+ ))
+ except ImplicitProcessorException, e:
+ 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=e.msg)
+ returnValue(False)
+ else:
+ processed = autoprocessed = False
if autoprocessed:
# No need to write the inbox item as it has already been auto-processed
@@ -198,7 +202,7 @@
child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.ScheduleProcessed() if processed else caldavxml.ScheduleUnprocessed()))
# Look for auto-schedule option
- if not processed and recipient.principal.autoSchedule():
+ if recipient.principal.autoSchedule():
autoresponses.append((recipient.principal, recipient.inbox, child))
returnValue(True)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -85,11 +85,13 @@
# Do straight comparison without alarms
self.calendar1 = self.calendar1.duplicate()
+ self.calendar1.attendeesView((attendee,))
+ self.calendar1.normalizePropertyValueLists("EXDATE")
self.calendar1.removeXProperties(("X-CALENDARSERVER-PRIVATE-COMMENT",))
- self.calendar1.attendeesView((attendee,))
iTipGenerator.prepareSchedulingMessage(self.calendar1)
self.calendar2 = self.calendar2.duplicate()
+ self.calendar2.normalizePropertyValueLists("EXDATE")
self.calendar2.removeXProperties(("X-CALENDARSERVER-PRIVATE-COMMENT",))
iTipGenerator.prepareSchedulingMessage(self.calendar2)
@@ -100,10 +102,12 @@
# Make sure the same VCALENDAR properties match
if not self._checkVCALENDARProperties():
+ log.debug("attendeeMerge: VCALENDAR properties do not match")
return False, False
# Make sure the same VTIMEZONE components appear
if not self._compareVTIMEZONEs():
+ log.debug("attendeeMerge: VTIMEZONEs do not match")
return False, False
# Compare each component instance from the new calendar with each derived
@@ -186,6 +190,7 @@
for key in set2 - set1:
component1 = self.calendar1.deriveInstance(key[2])
if component1 is None:
+ log.debug("_compareComponents: Could not derive instance: %s" % (key[2],))
return False, False
component2 = map2[key]
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -25,6 +25,7 @@
from twistedcaldav.method import report_common
from twistedcaldav.scheduling.icaldiff import iCalDiff
from twistedcaldav import caldavxml
+from twisted.web2.dav import davxml
__all__ = [
"ImplicitScheduler",
@@ -47,7 +48,7 @@
pass
@inlineCallbacks
- def doImplicitScheduling(self, request, resource, calendar, deleting):
+ def doImplicitScheduling(self, request, resource, calendar, deleting, internal_request=False):
"""
Do implicit scheduling operation based on the calendar data that is being PUT
@@ -69,7 +70,7 @@
self.calendar = calendar
self.calendar_owner = (yield self.resource.owner(self.request))
self.deleting = deleting
- self.internal_request = False
+ self.internal_request = internal_request
self.except_attendees = ()
# When deleting we MUST have the calendar as the actual resource
@@ -77,7 +78,7 @@
assert deleting and calendar or not deleting
# Get some useful information from the calendar
- self.extractCalendarData()
+ yield self.extractCalendarData()
# Determine what type of scheduling this is: Organizer triggered or Attendee triggered
if self.isOrganizerScheduling():
@@ -89,6 +90,7 @@
returnValue(self.calendar)
+ @inlineCallbacks
def refreshAllAttendeesExceptSome(self, request, resource, calendar, attendees):
"""
@@ -109,13 +111,33 @@
self.except_attendees = attendees
# Get some useful information from the calendar
- self.extractCalendarData()
+ yield self.extractCalendarData()
self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
- return self.processRequests()
+ result = (yield self.processRequests())
+ returnValue(result)
+
+ @inlineCallbacks
def extractCalendarData(self):
+ # Get the originator who is the authenticated user
+ self.originatorPrincipal = None
+ self.originator = ""
+ authz_principal = self.resource.currentPrincipal(self.request).children[0]
+ if isinstance(authz_principal, davxml.HRef):
+ originatorPrincipalURL = str(authz_principal)
+ if originatorPrincipalURL:
+ self.originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
+ if self.originatorPrincipal:
+ # Pick the first mailto cu address or the first other type
+ for item in self.originatorPrincipal.calendarUserAddresses():
+ if not self.originator:
+ self.originator = item
+ if item.startswith("mailto:"):
+ self.originator = item
+ break
+
# Get the ORGANIZER and verify it is the same for all components
organizers = self.calendar.getOrganizersByInstance()
self.organizer = None
@@ -173,8 +195,30 @@
return False
+ @inlineCallbacks
+ def doAccessControl(self, principal, is_organizer):
+ """
+ Check that the currently authorized user has the appropriate scheduling privilege
+ on the principal's Outbox.
+
+ @param principal:
+ @type principal:
+ @param is_organizer:
+ @type is_organizer:
+ """
+
+ # Find outbox
+ outboxURL = principal.scheduleOutboxURL()
+ outbox = (yield self.request.locateResource(outboxURL))
+ yield outbox.authorize(self.request, (caldavxml.Schedule(),))
+
+ @inlineCallbacks
def doImplicitOrganizer(self):
+ # Do access control
+ if not self.internal_request:
+ yield self.doAccessControl(self.organizerPrincipal, True)
+
# Check for a delete
if self.deleting:
@@ -194,7 +238,7 @@
if self.isChangeInsignificant():
# Nothing to do
log.debug("Implicit - organizer '%s' is updating UID: '%s' but change is not significant" % (self.organizer, self.uid))
- return
+ returnValue(None)
log.debug("Implicit - organizer '%s' is updating UID: '%s'" % (self.organizer, self.uid))
@@ -205,7 +249,7 @@
self.oldcalendar = None
self.cancelledAttendees = ()
- return self.scheduleWithAttendees()
+ yield self.scheduleWithAttendees()
def isChangeInsignificant(self):
@@ -324,7 +368,7 @@
# Do the PUT processing
log.info("Implicit CANCEL - organizer: '%s' to attendee: '%s', UID: '%s', RIDs: '%s'" % (self.organizer, attendee, self.uid, rids))
- response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg, self.internal_request))
+ response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, self.internal_request))
self.handleSchedulingResponse(response, True)
@inlineCallbacks
@@ -334,7 +378,7 @@
# being requested, but for now we will do one scheduling message per attendee.
# Do one per attendee
- for attendee, _ignore in self.attendeesByInstance:
+ for attendee in self.attendees:
# Don't send message back to the ORGANIZER
if attendee in self.organizerPrincipal.calendarUserAddresses():
@@ -353,7 +397,7 @@
# Do the PUT processing
log.info("Implicit REQUEST - organizer: '%s' to attendee: '%s', UID: '%s'" % (self.organizer, attendee, self.uid,))
- response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg, self.internal_request))
+ response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, self.internal_request))
self.handleSchedulingResponse(response, True)
def handleSchedulingResponse(self, response, is_organizer):
@@ -376,6 +420,10 @@
@inlineCallbacks
def doImplicitAttendee(self):
+ # Do access control
+ if not self.internal_request:
+ yield self.doAccessControl(self.attendeePrincipal, False)
+
if self.deleting:
#log.error("Attendee '%s' is not allowed to delete an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
#raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
@@ -390,7 +438,7 @@
# Determine whether the current change is allowed
if self.isAttendeeChangeInsignificant():
log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
- return
+ returnValue(None)
log.debug("Implicit - attendee '%s' is updating UID: '%s'" % (self.attendee, self.uid))
yield self.scheduleWithOrganizer()
@@ -469,6 +517,6 @@
self.handleSchedulingResponse(response, False)
log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
- d = scheduler.doSchedulingViaPUT(self.attendee, (self.organizer,), itipmsg, self.internal_request)
+ d = scheduler.doSchedulingViaPUT(self.originator, (self.organizer,), itipmsg, self.internal_request)
d.addCallback(_gotResponse)
return d
Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -376,7 +376,7 @@
instance = original.overriddenComponent(instance_rid)
if instance is None:
instance = original.masterComponent()
- assert instance is not None
+ assert instance is not None, "Need a master component"
# Add some required properties extracted from the original
comp.addProperty(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
@@ -404,7 +404,7 @@
# Extract the matching attendee property
for attendee in attendees:
attendeeProp = instance.getAttendeeProperty((attendee,))
- assert attendeeProp is not None
+ assert attendeeProp is not None, "Must have matching ATTENDEE property"
comp.addProperty(attendeeProp)
tzids.update(comp.timezoneIDs())
@@ -429,6 +429,9 @@
itip.replaceProperty(Property("PRODID", iCalendarProductID))
itip.addProperty(Property("METHOD", "REQUEST"))
+ # Force update to DTSTAMP everywhere
+ itip.replacePropertyInAllComponents(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
+
# Now filter out components that do not contain every attendee
itip.attendeesView(attendees)
@@ -445,6 +448,9 @@
itip.replaceProperty(Property("PRODID", iCalendarProductID))
itip.addProperty(Property("METHOD", "REPLY"))
+ # Force update to DTSTAMP everywhere
+ itip.replacePropertyInAllComponents(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
+
# Remove all attendees except the one we want
itip.removeAllButOneAttendee(attendee)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -18,6 +18,7 @@
import twistedcaldav.test.util
from twistedcaldav.scheduling.implicit import ImplicitScheduler
from dateutil.tz import tzutc
+from twisted.web2.dav import davxml
import datetime
class Implicit (twistedcaldav.test.util.TestCase):
@@ -746,8 +747,16 @@
),
)
+ class TestResource(object):
+ def currentPrincipal(self, request):
+ return davxml.Principal(davxml.Unauthenticated)
+
+ resource = TestResource()
+
for description, calendar1, calendar2, result in data:
scheduler = ImplicitScheduler()
+ scheduler.resource = resource
+ scheduler.request = None
scheduler.oldcalendar = Component.fromString(calendar1)
scheduler.calendar = Component.fromString(calendar2)
scheduler.extractCalendarData()
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py 2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -893,7 +893,9 @@
for original, filtered, attendees in data:
component = Component.fromString(original)
itipped = iTipGenerator.generateAttendeeRequest(component, attendees)
- self.assertEqual(filtered, str(itipped).replace("\r", ""))
+ itipped = str(itipped).replace("\r", "")
+ itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
+ self.assertEqual(filtered, itipped)
def test_cancel(self):
Copied: CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py (from rev 2989, CalendarServer/branches/users/cdaboo/implicitauto-2947/twistedcaldav/test/test_memcachelock.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py 2008-09-12 16:30:40 UTC (rev 2990)
@@ -0,0 +1,170 @@
+# Copyright (c) 2007 Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test the memcache client protocol.
+"""
+
+from twistedcaldav.memcache import MemCacheProtocol
+
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransportWithDisconnection
+from twisted.internet.task import Clock
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.memcachelock import MemcacheLock
+
+
+class MemCacheTestCase(TestCase):
+ """
+ Test client protocol class L{MemCacheProtocol}.
+ """
+
+ class FakedMemcacheLock(MemcacheLock):
+
+ def __init__(self, faked, namespace, locktoken, timeout=5.0, retry_interval=0.1, expire_time=0):
+ """
+
+ @param namespace: a unique namespace for this lock's tokens
+ @type namespace: C{str}
+ @param locktoken: the name of the locktoken
+ @type locktoken: C{str}
+ @param timeout: the maximum time in seconds that the lock should block
+ @type timeout: C{float}
+ @param retry_interval: the interval to retry acquiring the lock
+ @type retry_interval: C{float}
+ @param expiryTime: the time in seconds for the lock to expire. Zero: no expiration.
+ @type expiryTime: C{float}
+ """
+
+ super(MemCacheTestCase.FakedMemcacheLock, self).__init__(namespace, locktoken, timeout, retry_interval, expire_time)
+ self.faked = faked
+
+ def _getMemcacheProtocol(self):
+
+ return self.faked
+
+ def setUp(self):
+ """
+ Create a memcache client, connect it to a string protocol, and make it
+ use a deterministic clock.
+ """
+ self.proto = MemCacheProtocol()
+ self.clock = Clock()
+ self.proto.callLater = self.clock.callLater
+ self.transport = StringTransportWithDisconnection()
+ self.transport.protocol = self.proto
+ self.proto.makeConnection(self.transport)
+
+
+ def _test(self, d, send, recv, result):
+ """
+ Shortcut method for classic tests.
+
+ @param d: the resulting deferred from the memcache command.
+ @type d: C{Deferred}
+
+ @param send: the expected data to be sent.
+ @type send: C{str}
+
+ @param recv: the data to simulate as reception.
+ @type recv: C{str}
+
+ @param result: the expected result.
+ @type result: C{any}
+ """
+ def cb(res):
+ self.assertEquals(res, result)
+ self.assertEquals(self.transport.value(), send)
+ self.transport.clear()
+ d.addCallback(cb)
+ self.proto.dataReceived(recv)
+ return d
+
+ def test_get(self):
+ """
+ L{MemCacheProtocol.get} should return a L{Deferred} which is
+ called back with the value and the flag associated with the given key
+ if the server returns a successful result.
+ """
+ lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+ return self._test(
+ lock.get("foo"),
+ "get lock:foo\r\n",
+ "VALUE lock:foo 0 3\r\nbar\r\nEND\r\n",
+ "bar"
+ )
+
+ def test_set(self):
+ """
+ L{MemCacheProtocol.get} should return a L{Deferred} which is
+ called back with the value and the flag associated with the given key
+ if the server returns a successful result.
+ """
+ lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+ return self._test(
+ lock.set("foo", "bar"),
+ "set lock:foo 0 0 3\r\nbar\r\n",
+ "STORED\r\n",
+ True
+ )
+
+ @inlineCallbacks
+ def test_acquire(self):
+ """
+ L{MemCacheProtocol.get} should return a L{Deferred} which is
+ called back with the value and the flag associated with the given key
+ if the server returns a successful result.
+ """
+ lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+ yield self._test(
+ lock.acquire(),
+ "add lock:locking 0 0 1\r\n1\r\n",
+ "STORED\r\n",
+ True
+ )
+ self.assertTrue(lock._hasLock)
+
+ @inlineCallbacks
+ def test_acquire_release(self):
+ """
+ L{MemCacheProtocol.get} should return a L{Deferred} which is
+ called back with the value and the flag associated with the given key
+ if the server returns a successful result.
+ """
+ lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+ yield self._test(
+ lock.acquire(),
+ "add lock:locking 0 0 1\r\n1\r\n",
+ "STORED\r\n",
+ True
+ )
+ self.assertTrue(lock._hasLock)
+ yield self._test(
+ lock.release(),
+ "delete lock:locking\r\n",
+ "DELETED\r\n",
+ True
+ )
+ self.assertFalse(lock._hasLock)
+
+ @inlineCallbacks
+ def test_acquire_clean(self):
+ """
+ L{MemCacheProtocol.get} should return a L{Deferred} which is
+ called back with the value and the flag associated with the given key
+ if the server returns a successful result.
+ """
+ lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+ yield self._test(
+ lock.acquire(),
+ "add lock:locking 0 0 1\r\n1\r\n",
+ "STORED\r\n",
+ True
+ )
+ yield self._test(
+ lock.clean(),
+ "delete lock:locking\r\n",
+ "DELETED\r\n",
+ True
+ )
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080912/b6d2cca6/attachment-0001.html
More information about the calendarserver-changes
mailing list