[CalendarServer-changes] [11639] CalendarServer/branches/users/cdaboo/json
source_changes at macosforge.org
source_changes at macosforge.org
Mon Aug 26 09:43:05 PDT 2013
Revision: 11639
http://trac.calendarserver.org//changeset/11639
Author: cdaboo at apple.com
Date: 2013-08-26 09:43:05 -0700 (Mon, 26 Aug 2013)
Log Message:
-----------
Support content-negotiated variants of calendar and contact data in requests and responses.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py
CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py
CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -135,18 +135,97 @@
-class CalDAVTimeZoneElement (CalDAVTextElement):
+class CalDAVDataMixin(object):
"""
- CalDAV element containing iCalendar data with a single VTIMEZONE component.
+ A mixin to support accept/returning data in various formats.
"""
+
+ def __init__(self, *children, **attributes):
+
+ if "content-type" in attributes:
+ self.content_type = attributes["content-type"]
+ else:
+ self.content_type = "text/calendar"
+
+ if "version" in attributes:
+ self.version = attributes["version"]
+ else:
+ self.version = "2.0"
+
+ super(CalDAVDataMixin, self).__init__(*children, **attributes)
+
+
+ def verifyTypeVersion(self):
+ """
+ Make sure any content-type and version matches at least one supported set.
+
+ @return: C{True} if there is at least one match, C{False} otherwise.
+ """
+ allowedTypes = set()
+ allowedTypes.add(("text/calendar", "2.0",))
+ if config.EnableJSONData:
+ allowedTypes.add(("application/calendar+json", "2.0",))
+ for format, version in allowedTypes:
+ if (format == self.content_type) and (version == self.version):
+ return True
+
+ return False
+
+
+ @classmethod
+ def fromCalendar(clazz, calendar, format=None):
+ attrs = {}
+ if format is not None and format != "text/calendar":
+ attrs["content-type"] = format
+
+ if isinstance(calendar, str):
+ if not calendar:
+ raise ValueError("Missing calendar data")
+ return clazz(PCDATAElement(calendar), **attrs)
+ elif isinstance(calendar, iComponent):
+ assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
+ return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=format)), **attrs)
+ else:
+ raise ValueError("Not a calendar: %s" % (calendar,))
+
+ fromTextData = fromCalendar
+ fromComponent = fromCalendar
+
def calendar(self):
"""
- Returns a calendar component derived from this element, which contains
- exactly one VTIMEZONE component.
+ Returns a calendar component derived from this element.
"""
- return iComponent.fromString(str(self))
+ data = self.calendarData()
+ if data:
+ return iComponent.fromString(data, format=self.content_type)
+ else:
+ return None
+ generateComponent = calendar
+
+ def calendarData(self):
+ """
+ Returns the calendar data derived from this element.
+ """
+ for data in self.children:
+ if not isinstance(data, PCDATAElement):
+ return None
+ else:
+ # We guaranteed in __init__() that there is only one child...
+ break
+
+ return str(data)
+
+ textData = calendarData
+
+
+
+class CalDAVTimeZoneElement (CalDAVDataMixin, CalDAVTextElement):
+ """
+ CalDAV element containing iCalendar data with a single VTIMEZONE component.
+ """
+
def gettimezone(self):
"""
Get the timezone to use. If none, return UTC timezone.
@@ -227,8 +306,13 @@
name = "calendar-timezone"
hidden = True
+ allowed_attributes = {
+ "content-type": False,
+ "version" : False,
+ }
+
@registerElement
class SupportedCalendarComponentSets (CalDAVElement):
"""
@@ -424,7 +508,7 @@
@registerElement
-class CalendarData (CalDAVElement):
+class CalendarData (CalDAVDataMixin, CalDAVElement):
"""
Defines which parts of a calendar component object should be returned by a
report.
@@ -444,21 +528,6 @@
"version" : False,
}
- @classmethod
- def fromCalendar(clazz, calendar):
- if isinstance(calendar, str):
- if not calendar:
- raise ValueError("Missing calendar data")
- return clazz(PCDATAElement(calendar))
- elif isinstance(calendar, iComponent):
- assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
- return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
- else:
- raise ValueError("Not a calendar: %s" % (calendar,))
-
- fromTextData = fromCalendar
-
-
def __init__(self, *children, **attributes):
super(CalendarData, self).__init__(*children, **attributes)
@@ -511,61 +580,8 @@
# optimize them originals away
self.children = (data,)
- if "content-type" in attributes:
- self.content_type = attributes["content-type"]
- else:
- self.content_type = "text/calendar"
- if "version" in attributes:
- self.version = attributes["version"]
- else:
- self.version = "2.0"
-
- def verifyTypeVersion(self, types_and_versions):
- """
- Make sure any content-type and version matches at least one of the supplied set.
-
- @param types_and_versions: a list of (content-type, version) tuples to test against.
- @return: True if there is at least one match, False otherwise.
- """
- for item in types_and_versions:
- if (item[0] == self.content_type) and (item[1] == self.version):
- return True
-
- return False
-
-
- def calendar(self):
- """
- Returns a calendar component derived from this element.
- """
- data = self.calendarData()
- if data:
- return iComponent.fromString(data)
- else:
- return None
-
- generateComponent = calendar
-
-
- def calendarData(self):
- """
- Returns the calendar data derived from this element.
- """
- for data in self.children:
- if not isinstance(data, PCDATAElement):
- return None
- else:
- # We guaranteed in __init__() that there is only one child...
- break
-
- return str(data)
-
- textData = calendarData
-
-
-
@registerElement
class CalendarComponent (CalDAVElement):
"""
@@ -878,8 +894,13 @@
"""
name = "timezone"
+ allowed_attributes = {
+ "content-type": False,
+ "version" : False,
+ }
+
@registerElement
class TimeRange (CalDAVTimeRangeElement):
"""
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -30,6 +30,7 @@
from txdav.xml.element import WebDAVEmptyElement, WebDAVTextElement
from txdav.xml.element import ResourceType, Collection
+from twistedcaldav.config import config
from twistedcaldav.vcard import Component
##
@@ -67,6 +68,92 @@
+class CardDAVDataMixin(object):
+ """
+ A mixin to support accept/returning data in various formats.
+ """
+
+ def __init__(self, *children, **attributes):
+
+ if "content-type" in attributes:
+ self.content_type = attributes["content-type"]
+ else:
+ self.content_type = "text/vcard"
+
+ if "version" in attributes:
+ self.version = attributes["version"]
+ else:
+ self.version = "3.0"
+
+ super(CardDAVDataMixin, self).__init__(*children, **attributes)
+
+
+ def verifyTypeVersion(self):
+ """
+ Make sure any content-type and version matches at least one supported set.
+
+ @return: C{True} if there is at least one match, C{False} otherwise.
+ """
+ allowedTypes = set()
+ allowedTypes.add(("text/vcard", "3.0",))
+ if config.EnableJSONData:
+ allowedTypes.add(("application/vcard+json", "3.0",))
+ for format, version in allowedTypes:
+ if (format == self.content_type) and (version == self.version):
+ return True
+
+ return False
+
+
+ @classmethod
+ def fromAddress(clazz, address, format=None):
+ attrs = {}
+ if format is not None and format != "text/vcard":
+ attrs["content-type"] = format
+
+ if isinstance(address, str):
+ if not address:
+ raise ValueError("Missing address data")
+ return clazz(PCDATAElement(address), **attrs)
+ elif isinstance(address, Component):
+ assert address.name() == "VCARD", "Not a vCard: %r" % (address,)
+ return clazz(PCDATAElement(address.getText(format)), **attrs)
+ else:
+ raise ValueError("Not an address: %s" % (address,))
+
+ fromTextData = fromAddress
+ fromComponent = fromAddress
+
+ def address(self):
+ """
+ Returns an address component derived from this element.
+ """
+ data = self.addressData()
+ if data:
+ return Component.fromString(data, format=self.content_type)
+ else:
+ return None
+
+ generateComponent = address
+
+
+ def addressData(self):
+ """
+ Returns the address data derived from this element.
+ """
+ for data in self.children:
+ if not isinstance(data, PCDATAElement):
+ return None
+ else:
+ # We guaranteed in __init__() that there is only one child...
+ break
+
+ return str(data)
+
+ textData = addressData
+
+
+
@registerElement
class AddressBookHomeSet (CardDAVElement):
"""
@@ -201,7 +288,7 @@
@registerElement
-class AddressData (CardDAVElement):
+class AddressData (CardDAVDataMixin, CardDAVElement):
"""
Defines which parts of a address component object should be returned by a
report.
@@ -219,23 +306,6 @@
"version" : False,
}
- @classmethod
- def fromAddress(clazz, address):
- assert address.name() == "VCARD", "Not a vCard: %r" % (address,)
- return clazz(PCDATAElement(str(address)))
-
-
- @classmethod
- def fromAddressData(clazz, addressdata):
- """
- Return a AddressData element comprised of the supplied address data.
- @param addressdata: a string of valid address data.
- @return: a L{Addressata} element.
- """
- return clazz(PCDATAElement(addressdata))
-
- fromTextData = fromAddressData
-
def __init__(self, *children, **attributes):
super(AddressData, self).__init__(*children, **attributes)
@@ -284,60 +354,8 @@
# optimize them originals away
self.children = (data,)
- if "content-type" in attributes:
- self.content_type = attributes["content-type"]
- else:
- self.content_type = "text/vcard"
- if "version" in attributes:
- self.version = attributes["version"]
- else:
- self.version = "3.0"
-
- def verifyTypeVersion(self, types_and_versions):
- """
- Make sure any content-type and version matches at least one of the supplied set.
-
- @param types_and_versions: a list of (content-type, version) tuples to test against.
- @return: True if there is at least one match, False otherwise.
- """
- for item in types_and_versions:
- if (item[0] == self.content_type) and (item[1] == self.version):
- return True
-
- return False
-
-
- def address(self):
- """
- Returns an address component derived from this element.
- """
- data = self.addressData()
- if data:
- return Component.fromString(data)
- else:
- return None
-
- generateComponent = address
-
- def addressData(self):
- """
- Returns an address component derived from this element.
- """
- for data in self.children:
- if not isinstance(data, PCDATAElement):
- return None
- else:
- # We guaranteed in __init__() that there is only one child...
- break
-
- return str(data)
-
- textData = addressData
-
-
-
@registerElement
class AllProperties (CardDAVEmptyElement):
"""
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -28,7 +28,6 @@
"tzexpand",
]
-import cStringIO as StringIO
import codecs
from difflib import unified_diff
import heapq
@@ -376,66 +375,95 @@
# Hidden instance.
HIDDEN_INSTANCE_PROPERTY = "X-CALENDARSERVER-HIDDEN-INSTANCE"
+ allowedTypesList = None
+
+
@classmethod
- def allFromString(clazz, string):
+ def allowedTypes(cls):
+ if cls.allowedTypesList is None:
+ cls.allowedTypesList = ["text/calendar"]
+ if config.EnableJSONData:
+ cls.allowedTypesList.append("application/calendar+json")
+ return cls.allowedTypesList
+
+
+ @classmethod
+ def allFromString(clazz, string, format=None):
"""
Just default to reading a single VCALENDAR
"""
- return clazz.fromString(string)
+ return clazz.fromString(string, format)
@classmethod
- def allFromStream(clazz, stream):
+ def allFromStream(clazz, stream, format=None):
"""
Just default to reading a single VCALENDAR
"""
- return clazz.fromStream(stream)
+ return clazz.fromStream(stream, format)
@classmethod
- def fromString(clazz, string):
+ def fromString(clazz, string, format=None):
"""
Construct a L{Component} from a string.
@param string: a string containing iCalendar data.
@return: a L{Component} representing the first component described by
C{string}.
"""
- if type(string) is unicode:
- string = string.encode("utf-8")
- else:
- # Valid utf-8 please
- string.decode("utf-8")
+ return clazz._fromData(string, False, format)
- # No BOMs please
- if string[:3] == codecs.BOM_UTF8:
- string = string[3:]
- return clazz.fromStream(StringIO.StringIO(string))
+ @classmethod
+ def fromStream(clazz, stream, format=None):
+ """
+ Construct a L{Component} from a stream.
+ @param stream: a C{read()}able stream containing iCalendar data.
+ @return: a L{Component} representing the first component described by
+ C{stream}.
+ """
+ return clazz._fromData(stream, True, format)
@classmethod
- def fromStream(clazz, stream):
+ def _fromData(clazz, data, isstream, format=None):
"""
Construct a L{Component} from a stream.
@param stream: a C{read()}able stream containing iCalendar data.
+ @param format: a C{str} indicating whether the data is iCalendar or jCal
@return: a L{Component} representing the first component described by
C{stream}.
"""
- cal = Calendar()
+
+ if isstream:
+ pass
+ else:
+ if type(data) is unicode:
+ data = data.encode("utf-8")
+ else:
+ # Valid utf-8 please
+ data.decode("utf-8")
+
+ # No BOMs please
+ if data[:3] == codecs.BOM_UTF8:
+ data = data[3:]
+
errmsg = "Unknown"
try:
- result = cal.parse(stream)
+ result = Calendar.parseData(data, format)
except ErrorBase, e:
errmsg = "%s: %s" % (e.mReason, e.mData,)
result = None
if not result:
- stream.seek(0)
- raise InvalidICalendarDataError("%s\n%s" % (errmsg, stream.read(),))
- return clazz(None, pycalendar=cal)
+ if isstream:
+ data.seek(0)
+ data = data.read()
+ raise InvalidICalendarDataError("%s\n%s" % (errmsg, data,))
+ return clazz(None, pycalendar=result)
@classmethod
- def fromIStream(clazz, stream):
+ def fromIStream(clazz, stream, format=None):
"""
Construct a L{Component} from a stream.
@param stream: an L{IStream} containing iCalendar data.
@@ -449,7 +477,7 @@
# request stream.
#
def parse(data):
- return clazz.fromString(data)
+ return clazz.fromString(data, format)
return allDataFromStream(IStream(stream), parse)
@@ -543,13 +571,20 @@
return self._pycalendar == other._pycalendar
- def getTextWithTimezones(self, includeTimezones):
+ def getText(self, format=None):
+ return self.getTextWithTimezones(False, format)
+
+
+ def getTextWithTimezones(self, includeTimezones, format=None):
"""
- Return text representation and include timezones if the option is on
+ Return text representation and include timezones if the option is on.
"""
assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
- return self._pycalendar.getText(includeTimezones=includeTimezones)
+ result = self._pycalendar.getText(includeTimezones=includeTimezones, format=format)
+ if result is None:
+ raise ValueError("Unknown format requested for calendar data.")
+ return result
# FIXME: Should this not be in __eq__?
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -25,7 +25,7 @@
from txdav.xml import element as davxml
from twext.web2.dav.http import ErrorResponse
from twext.web2.dav.util import parentForURL
-from twext.web2.http import HTTPError
+from twext.web2.http import HTTPError, StatusResponse
from twext.web2.http import Response
from twext.web2.http_headers import MimeType
from twext.web2.stream import MemoryStream
@@ -34,8 +34,10 @@
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
from twistedcaldav.datafilters.privateevents import PrivateEventFilter
+from twistedcaldav.ical import Component
from twistedcaldav.resource import isPseudoCalendarCollectionResource, \
CalDAVResource
+from twistedcaldav.util import bestAcceptType
@inlineCallbacks
def http_GET(self, request):
@@ -70,6 +72,8 @@
returnValue(response)
else:
+ # FIXME: this should be implemented in storebridge.CalendarObject.render
+
# Look for calendar access restriction on existing resource.
parentURL = parentForURL(request.uri)
parent = (yield request.locateResource(parentURL))
@@ -78,6 +82,11 @@
# Check authorization first
yield self.authorize(request, (davxml.Read(),))
+ # Accept header handling
+ accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
+ if accepted_type is None:
+ raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
caldata = (yield self.iCalendarForUser(request))
# Filter any attendee hidden instances
@@ -92,8 +101,8 @@
caldata = PrivateEventFilter(self.accessMode, isowner).filter(caldata)
response = Response()
- response.stream = MemoryStream(caldata.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference))
- response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+ response.stream = MemoryStream(caldata.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=accepted_type))
+ response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
# Add Schedule-Tag header if property is present
if self.scheduleTag:
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -98,7 +98,7 @@
search_properties = "names"
elif isinstance(container, davxml.PropertyContainer):
properties = container.children
- search_properties = [(p.namespace, p.name) for p in properties]
+ search_properties = properties
else:
raise AssertionError("Unexpected element type in %s: %s"
% (davxml.PropertyFind.sname(), container))
@@ -245,7 +245,11 @@
# Utilities
##
-def propertyName(name):
+def propertyName(prop):
+ if type(prop) is tuple:
+ name = prop
+ else:
+ name = prop.qname()
property_namespace, property_name = name
pname = davxml.WebDAVUnknownElement()
pname.namespace = property_namespace
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -32,7 +32,8 @@
from twext.web2.http import HTTPError, StatusResponse
from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace, MaxInstances
+from twistedcaldav.caldavxml import caldav_namespace, MaxInstances, \
+ CalendarTimeZone
from twistedcaldav.config import config
from txdav.common.icommondatastore import IndexedSearchException, \
ConcurrentModification
@@ -171,10 +172,10 @@
if calresource.isPseudoCalendarCollection():
# Get the timezone property from the collection if one was not set in the query,
# and store in the query filter for later use
- has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
+ has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
timezone = query_timezone
if query_tz is None and has_prop:
- tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
+ tz = (yield calresource.readProperty(CalendarTimeZone(), request))
filter.settimezone(tz)
timezone = tuple(tz.calendar().subcomponents())[0]
@@ -233,9 +234,9 @@
parent = (yield calresource.locateParent(request, uri))
assert parent is not None and parent.isPseudoCalendarCollection()
- has_prop = (yield parent.hasProperty((caldav_namespace, "calendar-timezone"), request))
+ has_prop = (yield parent.hasProperty(CalendarTimeZone(), request))
if has_prop:
- tz = (yield parent.readProperty((caldav_namespace, "calendar-timezone"), request))
+ tz = (yield parent.readProperty(CalendarTimeZone(), request))
filter.settimezone(tz)
timezone = tuple(tz.calendar().subcomponents())[0]
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -52,7 +52,7 @@
from twistedcaldav import caldavxml
from twistedcaldav import carddavxml
-from twistedcaldav.caldavxml import caldav_namespace, CalendarData, TimeRange
+from twistedcaldav.caldavxml import CalendarData, CalendarTimeZone, TimeRange
from twistedcaldav.carddavxml import AddressData
from twistedcaldav.config import config
from twistedcaldav.datafilters.calendardata import CalendarDataFilter
@@ -299,7 +299,7 @@
generate_calendar_data = False
for property in prop.children:
if isinstance(property, caldavxml.CalendarData):
- if not property.verifyTypeVersion([("text/calendar", "2.0")]):
+ if not property.verifyTypeVersion():
result = False
message = "Calendar-data element type/version not supported: content-type: %s, version: %s" % (property.content_type, property.version)
generate_calendar_data = True
@@ -324,7 +324,7 @@
generate_address_data = False
for property in prop.children:
if isinstance(property, carddavxml.AddressData):
- if not property.verifyTypeVersion([("text/vcard", "3.0")]):
+ if not property.verifyTypeVersion():
result = False
message = "Address-data element type/version not supported: content-type: %s, version: %s" % (property.content_type, property.version)
generate_address_data = True
@@ -371,7 +371,7 @@
filtered = HiddenInstanceFilter().filter(calendar)
filtered = PrivateEventFilter(resource.accessMode, isowner).filter(filtered)
filtered = CalendarDataFilter(property, timezone).filter(filtered)
- propvalue = CalendarData().fromCalendar(filtered)
+ propvalue = CalendarData.fromCalendar(filtered, format=property.content_type)
properties_by_status[responsecode.OK].append(propvalue)
continue
@@ -379,7 +379,7 @@
if vcard is None:
vcard = (yield resource.vCard())
filtered = AddressDataFilter(property).filter(vcard)
- propvalue = AddressData().fromAddress(filtered)
+ propvalue = AddressData.fromAddress(filtered, format=property.content_type)
properties_by_status[responsecode.OK].append(propvalue)
continue
@@ -392,7 +392,7 @@
if has:
try:
- prop = (yield resource.readProperty(qname, request))
+ prop = (yield resource.readProperty(property, request))
if prop is not None:
properties_by_status[responsecode.OK].append(prop)
elif not returnMinimal:
@@ -515,9 +515,9 @@
useruid = ""
# Get the timezone property from the collection.
- has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
+ has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
if has_prop:
- tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
+ tz = (yield calresource.readProperty(CalendarTimeZone(), request))
else:
tz = None
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -31,7 +31,9 @@
from twext.web2.stream import MemoryStream
from twistedcaldav import caldavxml
+from twistedcaldav.ical import Component
from twistedcaldav.method import report_common
+from twistedcaldav.util import bestAcceptType
from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
from txdav.xml import element as davxml
@@ -60,6 +62,11 @@
matchcount = [0]
+ accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
+ if accepted_type is None:
+ raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
+
def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
"""
Run a free busy report on the specified calendar collection
@@ -104,7 +111,7 @@
fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange)
response = Response()
- response.stream = MemoryStream(str(fbcalendar))
- response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+ response.stream = MemoryStream(fbcalendar.getText(accepted_type))
+ response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
returnValue(response)
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -257,10 +257,11 @@
# Redirect to include trailing '/' in URI
return RedirectResponse(request.unparseURL(path=urllib.quote(urllib.unquote(request.path), safe=':/') + '/'))
- def _defer(data):
+ def _defer(result):
+ data, accepted_type = result
response = Response()
- response.stream = MemoryStream(str(data))
- response.headers.setHeader("content-type", MimeType.fromString("text/calendar"))
+ response.stream = MemoryStream(data.getText(accepted_type))
+ response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
return response
d = self.iCalendarRolledup(request)
@@ -365,7 +366,7 @@
(self,))
- def storeStream(self, stream):
+ def storeStream(self, stream, format):
"""
Store the content of the stream in this resource, as it would via a PUT.
@@ -375,8 +376,7 @@
@return: a L{Deferred} which fires with an HTTP response.
@rtype: L{Deferred}
"""
- raise NotImplementedError("%s does not implement storeStream" %
- (self,))
+ raise NotImplementedError("%s does not implement storeStream" % (self,))
# End transitional new-store interface
@@ -595,12 +595,21 @@
returnValue(self.getSupportedComponentSet())
elif qname == caldavxml.SupportedCalendarData.qname() and self.isPseudoCalendarCollection():
- returnValue(caldavxml.SupportedCalendarData(
+ dataTypes = []
+ dataTypes.append(
caldavxml.CalendarData(**{
"content-type": "text/calendar",
"version" : "2.0",
}),
- ))
+ )
+ if config.EnableJSONData:
+ dataTypes.append(
+ caldavxml.CalendarData(**{
+ "content-type": "application/calendar+json",
+ "version" : "2.0",
+ }),
+ )
+ returnValue(caldavxml.SupportedCalendarData(*dataTypes))
elif qname == caldavxml.MaxResourceSize.qname() and self.isPseudoCalendarCollection():
if config.MaxResourceSize:
@@ -632,12 +641,21 @@
elif qname == carddavxml.SupportedAddressData.qname() and self.isAddressBookCollection():
# CardDAV, section 6.2.2
- returnValue(carddavxml.SupportedAddressData(
+ dataTypes = []
+ dataTypes.append(
carddavxml.AddressDataType(**{
"content-type": "text/vcard",
"version" : "3.0",
}),
- ))
+ )
+ if config.EnableJSONData:
+ dataTypes.append(
+ carddavxml.AddressDataType(**{
+ "content-type": "application/vcard+json",
+ "version" : "3.0",
+ }),
+ )
+ returnValue(carddavxml.SupportedAddressData(*dataTypes))
elif qname == carddavxml.MaxResourceSize.qname() and self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection():
# CardDAV, section 6.2.3
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -407,7 +407,7 @@
# Check authentication and access controls
yield self.authorize(request, (caldavxml.ScheduleSend(),))
- calendar = (yield self.loadCalendarFromRequest(request))
+ calendar, format = (yield self.loadCalendarFromRequest(request))
originator = (yield self.loadOriginatorFromRequestDetails(request))
recipients = self.loadRecipientsFromCalendarData(calendar)
@@ -427,14 +427,25 @@
# Do the POST processing treating
result = (yield scheduler.doSchedulingViaPOST(originator, recipients, calendar))
- returnValue(result.response())
+ returnValue(result.response(format=format))
+ def determineType(self, content_type):
+ """
+ Determine if the supplied content-type is valid for storing and return the matching PyCalendar type.
+ """
+ format = None
+ if content_type is not None:
+ format = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+ return format if format in Component.allowedTypes() else None
+
+
@inlineCallbacks
def loadCalendarFromRequest(self, request):
# Must be content-type text/calendar
contentType = request.headers.getHeader("content-type")
- if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
+ format = self.determineType(contentType)
+ if format is None:
self.log.error("MIME type %s not allowed in calendar collection" % (contentType,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
@@ -444,7 +455,7 @@
# Parse the calendar object from the HTTP request stream
try:
- calendar = (yield Component.fromIStream(request.stream))
+ calendar = (yield Component.fromIStream(request.stream, format=format))
except:
# FIXME: Bare except
self.log.error("Error while handling POST: %s" % (Failure(),))
@@ -454,7 +465,7 @@
description="Can't parse calendar data"
))
- returnValue(calendar)
+ returnValue((calendar, format,))
@inlineCallbacks
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -539,6 +539,11 @@
"EnableManagedAttachments" : False, # Support Managed Attachments
#
+ # Generic CalDAV/CardDAV extensions
+ #
+ "EnableJSONData" : True, # Allow clients to send/receive JSON jCal and jCard format data
+
+ #
# Non-standard CalDAV extensions
#
"EnableDropBox" : False, # Calendar Drop Box
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -82,6 +82,7 @@
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.instance import InvalidOverriddenInstanceError, \
TooManyInstancesError
+from twistedcaldav.util import bestAcceptType
import collections
"""
@@ -626,14 +627,17 @@
# Read in all data
data = (yield allDataFromStream(request.stream))
- components = self.componentsFromData(data)
+ format = request.headers.getHeader("content-type")
+ if format:
+ format = "%s/%s" % (format.mediaType, format.mediaSubtype,)
+ components = self.componentsFromData(data, format)
if components is None:
raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body"))
# Build response
xmlresponses = [None] * len(components)
indexedComponents = [idxComponent for idxComponent in enumerate(components)]
- yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses)
+ yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses, format)
result = MultiStatusResponse(xmlresponses)
@@ -650,7 +654,7 @@
@inlineCallbacks
- def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses):
+ def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses, format):
"""
Do create from simpleBatchPOST or crudCreate()
Subclasses may override
@@ -664,7 +668,7 @@
# Get a resource for the new item
newchildURL = joinURL(request.path, name)
newchild = (yield request.locateResource(newchildURL))
- changedData = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
+ changedComponent = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
except HTTPError, e:
# Extract the pre-condition
@@ -674,30 +678,30 @@
error = (error.namespace, error.name,)
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error, format)
)
except Exception:
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, None, code=BAD_REQUEST, error=None)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, None, BAD_REQUEST, None, format)
)
else:
if not return_changed:
- changedData = None
+ changedComponent = None
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, changedComponent, None, None, format)
)
@inlineCallbacks
- def bulkCreateResponse(self, component, newchildURL, newchild, changedData, code, error):
+ def bulkCreateResponse(self, component, newchildURL, newchild, changedComponent, code, error, format):
"""
generate one xmlresponse for bulk create
"""
if code is None:
etag = (yield newchild.etag())
- if changedData is None:
+ if changedComponent is None:
returnValue(
davxml.PropertyStatusResponse(
davxml.HRef.fromString(newchildURL),
@@ -717,7 +721,7 @@
davxml.PropertyStatus(
davxml.PropertyContainer(
davxml.GETETag.fromString(etag.generate()),
- self.xmlDataElementType().fromTextData(changedData),
+ self.xmlDataElementType().fromComponent(changedComponent, format),
),
davxml.Status.fromResponseCode(OK),
)
@@ -822,6 +826,7 @@
for index, xmldata in crudCreateInfo:
component = xmldata.generateComponent()
+ format = xmldata.content_type
if hasPrivilege is not True:
e = hasPrivilege # use same code pattern as exception
@@ -830,13 +835,13 @@
error = e.response.error
error = (error.namespace, error.name,)
- xmlresponse = yield self.bulkCreateResponse(component, None, None, None, code, error)
+ xmlresponse = yield self.bulkCreateResponse(component, None, None, None, code, error, format)
xmlresponses[index] = xmlresponse
else:
indexedComponents.append((index, component,))
- yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses)
+ yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses, format)
@inlineCallbacks
@@ -847,8 +852,8 @@
code = None
error = None
try:
- componentdata = xmldata.textData()
component = xmldata.generateComponent()
+ format = xmldata.content_type
updateResource = (yield request.locateResource(href))
if not updateResource.exists():
@@ -862,7 +867,7 @@
if ifmatch and ifmatch != etag.generate():
raise HTTPError(PRECONDITION_FAILED)
- changedData = yield self.storeResourceData(updateResource, component, componentdata)
+ changedComponent = yield self.storeResourceData(updateResource, component, returnChangedData=return_changed)
except HTTPError, e:
# Extract the pre-condition
@@ -875,7 +880,7 @@
code = BAD_REQUEST
if code is None:
- if not return_changed or changedData is None:
+ if changedComponent is None:
xmlresponses[index] = davxml.PropertyStatusResponse(
davxml.HRef.fromString(href),
davxml.PropertyStatus(
@@ -891,7 +896,7 @@
davxml.PropertyStatus(
davxml.PropertyContainer(
davxml.GETETag.fromString(etag.generate()),
- self.xmlDataElementType().fromTextData(changedData),
+ self.xmlDataElementType().fromComponentData(changedComponent, format),
),
davxml.Status.fromResponseCode(OK),
)
@@ -1053,6 +1058,8 @@
if config.EnableBatchUpload:
self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
+ if config.EnableJSONData:
+ self._postHandlers[("application", "calendar+json")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
@@ -1088,6 +1095,11 @@
def iCalendarRolledup(self, request):
# FIXME: uncached: implement cache in the storage layer
+ # Accept header handling
+ accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
+ if accepted_type is None:
+ raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
# Generate a monolithic calendar
calendar = VCalendar("VCALENDAR")
calendar.addProperty(VProperty("VERSION", "2.0"))
@@ -1138,17 +1150,13 @@
calendar.addComponent(component)
- # Cache the data
- data = str(calendar)
- data = (yield self.getInternalSyncToken()) + "\r\n" + data
+ returnValue((calendar, accepted_type,))
- returnValue(calendar)
-
createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
@classmethod
- def componentsFromData(cls, data):
+ def componentsFromData(cls, data, format):
"""
Need to split a single VCALENDAR into separate ones based on UID with the
appropriate VTIEMZONES included.
@@ -1158,7 +1166,7 @@
# Split into components by UID and TZID
try:
- vcal = VCalendar.fromString(data)
+ vcal = VCalendar.fromString(data, format)
except InvalidICalendarDataError:
return None
@@ -1242,7 +1250,8 @@
elif qname == caldavxml.CalendarTimeZone.qname():
timezone = self._newStoreObject.getTimezone()
- returnValue(caldavxml.CalendarTimeZone.fromString(str(timezone)) if timezone else None)
+ format = property.content_type if isinstance(property, caldavxml.CalendarTimeZone) else None
+ returnValue(caldavxml.CalendarTimeZone.fromCalendar(timezone, format=format) if timezone else None)
result = (yield super(CalendarCollectionResource, self).readProperty(property, request))
returnValue(result)
@@ -1288,7 +1297,7 @@
yield newchild.storeComponent(component)
if returnChangedData and newchild._newStoreObject._componentChanged:
result = (yield newchild.componentForUser())
- returnValue(str(result))
+ returnValue(result)
else:
returnValue(None)
@@ -2210,16 +2219,38 @@
return self._newStoreObject.component()
+ def allowedTypes(self):
+ """
+ Return a dict of allowed MIME types for storing, mapped to equivalent PyCalendar types.
+ """
+ raise NotImplementedError
+
+
+ def determineType(self, content_type):
+ """
+ Determine if the supplied content-type is valid for storing and return the matching PyCalendar type.
+ """
+ format = None
+ if content_type is not None:
+ format = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+ return format if format in self.allowedTypes() else None
+
+
@inlineCallbacks
def render(self, request):
if not self.exists():
log.debug("Resource not found: %s" % (self,))
raise HTTPError(NOT_FOUND)
+ # Accept header handling
+ accepted_type = bestAcceptType(request.headers.getHeader("accept"), self.allowedTypes())
+ if accepted_type is None:
+ raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
output = yield self.component()
- response = Response(OK, {}, str(output))
- response.headers.setHeader("content-type", self.contentType())
+ response = Response(OK, {}, output.getText(accepted_type))
+ response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
returnValue(response)
# The following are used to map store exceptions into HTTP error responses
@@ -2344,10 +2375,10 @@
@inlineCallbacks
- def storeStream(self, stream):
+ def storeStream(self, stream, format):
# FIXME: direct tests
- component = self._componentFromStream((yield allDataFromStream(stream)))
+ component = self._componentFromStream((yield allDataFromStream(stream)), format)
result = (yield self.storeComponent(component))
returnValue(result)
@@ -2479,6 +2510,13 @@
_componentFromStream = VCalendar.fromString
+ def allowedTypes(self):
+ """
+ Return a tuple of allowed MIME types for storing.
+ """
+ return Component.allowedTypes()
+
+
@inlineCallbacks
def inNewTransaction(self, request, label=""):
"""
@@ -2600,7 +2638,8 @@
# Content-type check
content_type = request.headers.getHeader("content-type")
- if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+ format = self.determineType(content_type)
+ if format is None:
log.error("MIME type %s not allowed in calendar collection" % (content_type,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
@@ -2628,7 +2667,7 @@
))
try:
- component = Component.fromString(calendardata)
+ component = Component.fromString(calendardata, format)
except ValueError, e:
log.error(str(e))
raise HTTPError(ErrorResponse(
@@ -2899,6 +2938,8 @@
if config.EnableBatchUpload:
self._postHandlers[("text", "vcard")] = AddressBookCollectionResource.simpleBatchPOST
+ if config.EnableJSONData:
+ self._postHandlers[("application", "vcard+json")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
self.xmlDocHandlers[customxml.Multiput] = AddressBookCollectionResource.crudBatchPOST
@@ -2930,9 +2971,9 @@
@classmethod
- def componentsFromData(cls, data):
+ def componentsFromData(cls, data, format):
try:
- return VCard.allFromString(data)
+ return VCard.allFromString(data, format)
except InvalidVCardDataError:
return None
@@ -2953,7 +2994,7 @@
yield newchild.storeComponent(component)
if returnChangedData and newchild._newStoreObject._componentChanged:
result = (yield newchild.component())
- returnValue(str(result))
+ returnValue(result)
else:
returnValue(None)
@@ -2994,7 +3035,7 @@
@inlineCallbacks
- def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses):
+ def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses, format):
"""
bulk create allowing groups to contain member UIDs added during the same bulk create
"""
@@ -3009,7 +3050,7 @@
# Get a resource for the new item
newchildURL = joinURL(request.path, name)
newchild = (yield request.locateResource(newchildURL))
- changedData = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
+ changedComponent = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
except GroupWithUnsharedAddressNotAllowedError, e:
# save off info and try again below
@@ -3024,20 +3065,20 @@
error = (error.namespace, error.name,)
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error, format)
)
except Exception:
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, None, code=BAD_REQUEST, error=None)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, None, BAD_REQUEST, None, format)
)
else:
if not return_changed:
- changedData = None
+ changedComponent = None
coaddedUIDs |= set([component.resourceUID()])
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, changedComponent, None, None, format)
)
if groupRetries:
@@ -3055,7 +3096,7 @@
# give FORBIDDEN response
index, component, newchildURL, newchild, missingUIDs = groupRetry
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, changedData=None, code=FORBIDDEN, error=None)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, None, FORBIDDEN, None, format)
)
coaddedUIDs -= set([component.resourceUID()]) # group uid not added
groupRetries.remove(groupRetry) # remove this retry
@@ -3065,11 +3106,11 @@
newchild._metadata["coaddedUIDs"] = coaddedUIDs
# don't catch errors, abort the whole transaction
- changedData = yield self.storeResourceData(newchild, component, returnChangedData=return_changed)
+ changedComponent = yield self.storeResourceData(newchild, component, returnChangedData=return_changed)
if not return_changed:
- changedData = None
+ changedComponent = None
xmlresponses[index] = (
- yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
+ yield self.bulkCreateResponse(component, newchildURL, newchild, changedComponent, None, None, format)
)
@@ -3164,6 +3205,13 @@
_componentFromStream = VCard.fromString
+ def allowedTypes(self):
+ """
+ Return a tuple of allowed MIME types for storing.
+ """
+ return VCard.allowedTypes()
+
+
@inlineCallbacks
def vCardText(self):
data = yield self.vCard()
@@ -3238,7 +3286,8 @@
# Content-type check
content_type = request.headers.getHeader("content-type")
- if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "vcard"):
+ format = self.determineType(content_type)
+ if format is None:
log.error("MIME type %s not allowed in vcard collection" % (content_type,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
@@ -3263,7 +3312,7 @@
))
try:
- component = VCard.fromString(vcarddata)
+ component = VCard.fromString(vcarddata, format)
except ValueError, e:
log.error(str(e))
raise HTTPError(ErrorResponse(
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -19,17 +19,19 @@
from twext.web2 import responsecode
from twext.web2.iweb import IResponse
from twext.web2.stream import MemoryStream
-from txdav.xml import element as davxml
from twext.web2.dav.util import davXMLFromStream, joinURL
+from twext.web2.http_headers import Headers, MimeType
from twistedcaldav import carddavxml
from twistedcaldav import vcard
-
from twistedcaldav.config import config
from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
+
from twisted.python.filepath import FilePath
from twisted.internet.defer import inlineCallbacks, returnValue
+from txdav.xml import element as davxml
+
class AddressBookMultiget (StoreTestCase):
"""
addressbook-multiget REPORT
@@ -214,7 +216,13 @@
'''
if data:
for filename, icaldata in data.iteritems():
- request = SimpleStoreRequest(self, "PUT", joinURL(addressbook_uri, filename + ".vcf"), authid="wsanchez")
+ request = SimpleStoreRequest(
+ self,
+ "PUT",
+ joinURL(addressbook_uri, filename + ".vcf"),
+ headers=Headers({"content-type": MimeType.fromString("text/vcard")}),
+ authid="wsanchez"
+ )
request.stream = MemoryStream(icaldata)
yield self.send(request)
else:
@@ -222,7 +230,13 @@
for child in FilePath(self.vcards_dir).children():
if os.path.splitext(child.basename())[1] != ".vcf":
continue
- request = SimpleStoreRequest(self, "PUT", joinURL(addressbook_uri, child.basename()), authid="wsanchez")
+ request = SimpleStoreRequest(
+ self,
+ "PUT",
+ joinURL(addressbook_uri, child.basename()),
+ headers=Headers({"content-type": MimeType.fromString("text/vcard")}),
+ authid="wsanchez"
+ )
request.stream = MemoryStream(child.getContent())
yield self.send(request)
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -14,9 +14,16 @@
# limitations under the License.
##
+import twistedcaldav.test.util
from twistedcaldav import caldavxml
-import twistedcaldav.test.util
+from twistedcaldav.caldavxml import CalendarData
+from twistedcaldav.ical import normalize_iCalStr, Component
+def normalizeJSON(j):
+ return "".join(map(str.strip, j.splitlines())).replace(", ", ",").replace(": ", ":")
+
+
+
class CustomXML (twistedcaldav.test.util.TestCase):
@@ -50,3 +57,99 @@
tr = caldavxml.CalDAVTimeRangeElement(start="20110201T120000Z", end="20110202")
self.assertFalse(tr.valid())
+
+
+ def test_CalendarDataTextAndJSON(self):
+ """
+ Text that we can both parse and generate CalendarData elements with both text and json formats.
+ """
+ dataText = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+EXDATE:20080602T120000Z
+EXDATE:20080603T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=400
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+ dataXML = """<?xml version='1.0' encoding='UTF-8'?>
+<calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[%s]]></calendar-data>""" % (dataText,)
+
+ jsonText = """[
+ "vcalendar",
+ [
+ ["version", {}, "text", "2.0"],
+ ["prodid", {}, "text", "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"]
+ ],
+ [
+ ["vevent",
+ [
+ ["uid", {}, "text", "12345-67890"],
+ ["dtstart", {}, "date-time", "2008-06-01T12:00:00Z"],
+ ["dtend", {}, "date-time", "2008-06-01T13:00:00Z"],
+ ["attendee", {}, "cal-address", "mailto:user1 at example.com"],
+ ["attendee", {}, "cal-address", "mailto:user2 at example.com"],
+ ["dtstamp", {}, "date-time", "2008-06-01T12:00:00Z"],
+ ["exdate", {}, "date-time", "2008-06-02T12:00:00Z"],
+ ["exdate", {}, "date-time", "2008-06-03T12:00:00Z"],
+ ["organizer", {"cn": "User 01"}, "cal-address", "mailto:user1 at example.com"],
+ ["rrule", {}, "recur", {"count": 400, "freq": "DAILY"}],
+ ["summary", {}, "text", "Test"]
+ ],
+ [
+ ]
+ ]
+ ]
+]
+"""
+
+ jsonXML = """<?xml version='1.0' encoding='UTF-8'?>
+<calendar-data content-type='application/calendar+json' xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[%s]]></calendar-data>""" % (jsonText,)
+
+ cd = CalendarData.fromTextData(dataText)
+ self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+ self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+ self.assertEqual(cd.content_type, "text/calendar")
+ self.assertEqual(cd.toxml(), dataXML)
+
+ comp = Component.fromString(dataText)
+ cd = CalendarData.fromCalendar(comp)
+ self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+ self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+ self.assertEqual(cd.content_type, "text/calendar")
+ self.assertEqual(cd.toxml(), dataXML)
+
+ cd = CalendarData.fromCalendar(comp, format="application/calendar+json")
+ self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+ self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+ self.assertEqual(cd.content_type, "application/calendar+json")
+ self.assertEqual(normalizeJSON(cd.toxml()), normalizeJSON(jsonXML))
+
+ cd = CalendarData.fromTextData(jsonText, format="application/calendar+json")
+ self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+ self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+ self.assertEqual(cd.content_type, "application/calendar+json")
+ self.assertEqual(cd.toxml(), jsonXML)
+
+ comp = Component.fromString(jsonText, format="application/calendar+json")
+ cd = CalendarData.fromCalendar(comp)
+ self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+ self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+ self.assertEqual(cd.content_type, "text/calendar")
+ self.assertEqual(cd.toxml(), dataXML)
+
+ cd = CalendarData.fromCalendar(comp, format="application/calendar+json")
+ self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+ self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+ self.assertEqual(cd.content_type, "application/calendar+json")
+ self.assertEqual(normalizeJSON(cd.toxml()), normalizeJSON(jsonXML))
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -20,6 +20,7 @@
from twistedcaldav.ical import Component
from twistedcaldav.test.util import TestCase
from twistedcaldav.config import ConfigDict
+from twistedcaldav.timezones import TimezoneCache
from pycalendar.datetime import DateTime
import os
@@ -52,6 +53,11 @@
class LocalizationTests(TestCase):
+ def setUp(self):
+ super(LocalizationTests, self).setUp()
+ TimezoneCache.create()
+
+
def test_BasicStringLocalization(self):
with translationTo('pig', localeDir=localeDir):
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -16,6 +16,7 @@
from twext.python.filepath import CachingFilePath as FilePath
from twext.web2 import responsecode
from twext.web2.dav.util import davXMLFromStream, joinURL
+from twext.web2.http_headers import Headers, MimeType
from twext.web2.iweb import IResponse
from twext.web2.stream import MemoryStream
@@ -268,7 +269,13 @@
if data:
for filename, icaldata in data.iteritems():
- request = SimpleStoreRequest(self, "PUT", joinURL(calendar_uri, filename + ".ics"), authid="wsanchez")
+ request = SimpleStoreRequest(
+ self,
+ "PUT",
+ joinURL(calendar_uri, filename + ".ics"),
+ headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
+ authid="wsanchez"
+ )
request.stream = MemoryStream(icaldata)
yield self.send(request)
else:
@@ -276,7 +283,13 @@
for child in FilePath(self.holidays_dir).children():
if os.path.splitext(child.basename())[1] != ".ics":
continue
- request = SimpleStoreRequest(self, "PUT", joinURL(calendar_uri, child.basename()), authid="wsanchez")
+ request = SimpleStoreRequest(
+ self,
+ "PUT",
+ joinURL(calendar_uri, child.basename()),
+ headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
+ authid="wsanchez"
+ )
request.stream = MemoryStream(child.getContent())
yield self.send(request)
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -88,7 +88,7 @@
self.fail("Expected CalDAV:supported-calendar-data element; but got none.")
for calendar in supported_calendar.children:
- if calendar.content_type != "text/calendar":
+ if calendar.content_type not in ("text/calendar", "application/calendar+json"):
self.fail("Expected a text/calendar calendar-data type restriction")
if calendar.version != "2.0":
self.fail("Expected a version 2.0 calendar-data restriction")
Added: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -0,0 +1,144 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2.http_headers import Headers
+
+import twistedcaldav.test.util
+from twistedcaldav.util import bestAcceptType
+
+class AcceptType(twistedcaldav.test.util.TestCase):
+ """
+ L{bestAcceptType} tests
+ """
+ def test_bestAcceptType(self):
+
+ data = (
+ (
+ "#1.1",
+ ("Accept", "text/plain"),
+ ["text/plain"],
+ "text/plain",
+ ),
+ (
+ "#1.2",
+ ("Accept", "text/plain"),
+ ["text/calendar"],
+ None,
+ ),
+ (
+ "#1.3",
+ ("Accept", "text/*"),
+ ["text/plain"],
+ "text/plain",
+ ),
+ (
+ "#1.4",
+ ("Accept", "*/*"),
+ ["text/plain"],
+ "text/plain",
+ ),
+ (
+ "#2.1",
+ ("Accept", "text/plain"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#2.2",
+ ("Accept", "text/plain"),
+ ["text/calendar", "application/text", ],
+ None,
+ ),
+ (
+ "#2.3",
+ ("Accept", "text/*"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#2.4",
+ ("Accept", "*/*"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#2.5",
+ ("Accept", "application/text"),
+ ["text/plain", "application/text", ],
+ "application/text",
+ ),
+ (
+ "#2.6",
+ ("Accept", "application/*"),
+ ["text/plain", "application/text", ],
+ "application/text",
+ ),
+ (
+ "#3.1",
+ ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#3.2",
+ ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+ ["text/calendar", "application/calendar", ],
+ None,
+ ),
+ (
+ "#3.3",
+ ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#3.4",
+ ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#3.5",
+ ("Accept", "text/plain;q=0.3, application/text;q=0.5"),
+ ["text/plain", "application/text", ],
+ "application/text",
+ ),
+ (
+ "#3.6",
+ ("Accept", "text/plain;q=0.5, application/*;q=0.3"),
+ ["text/plain", "application/text", ],
+ "text/plain",
+ ),
+ (
+ "#4.1",
+ ("Accept", "text/plain;q=0.5, application/text;q=0.2, text/*;q=0.3"),
+ ["text/calendar", "application/text", ],
+ "text/calendar",
+ ),
+ (
+ "#5.1",
+ None,
+ ["text/calendar", "application/text", ],
+ "text/calendar",
+ ),
+ )
+
+ for title, hdr, allowedTypes, result in data:
+ hdrs = Headers()
+ if hdr:
+ hdrs.addRawHeader(*hdr)
+ check = bestAcceptType(hdrs.getHeader("accept"), allowedTypes)
+ self.assertEqual(check, result, msg="Failed %s" % (title,))
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -89,7 +89,13 @@
else:
raise ValueError("Invalid TimezoneService mode: %s" % (config.TimezoneService.Mode,))
+ self.formats = []
+ self.formats.append("text/calendar")
+ self.formats.append("text/plain")
+ if config.EnableJSONData:
+ self.formats.append("application/calendar+json")
+
def _initPrimaryService(self):
tzpath = TimezoneCache.getDBPath()
xmlfile = os.path.join(tzpath, "timezones.xml")
@@ -268,7 +274,7 @@
{
"name": "get",
"parameters": [
- {"name": "format", "required": False, "multi": False, "values": ["text/calendar", "text/plain", ], },
+ {"name": "format", "required": False, "multi": False, "values": self.formats, },
{"name": "tzid", "required": True, "multi": False, },
],
},
@@ -348,7 +354,7 @@
))
format = request.args.get("format", ("text/calendar",))
- if len(format) != 1 or format[0] not in ("text/calendar", "text/plain",):
+ if len(format) != 1 or format[0] not in self.formats:
raise HTTPError(JSONResponse(
responsecode.BAD_REQUEST,
{
@@ -368,7 +374,7 @@
}
))
- tzdata = calendar.getText()
+ tzdata = calendar.getText(format=format if format != "text/plain" else None)
response = Response()
response.stream = MemoryStream(tzdata)
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -517,3 +517,52 @@
cuas = principal.record.calendarUserAddresses
return (fullName, rec.guid, cuas)
+
+
+
+def bestAcceptType(accepts, allowedTypes):
+ """
+ Given a set of Accept headers and the set of types the server can return, determine the best choice
+ of format to return to the client.
+
+ @param accepts: parsed accept headers
+ @type accepts: C{dict}
+ @param allowedTypes: list of allowed types in server preferred order
+ @type allowedTypes: C{list}
+ """
+
+ # If no Accept present just use the first allowed type - the server's preference
+ if not accepts:
+ return allowedTypes[0]
+
+ # Get mapping for ordered top-level types for use in subtype wildcard match
+ toptypes = {}
+ for allowed in allowedTypes:
+ mediaType = allowed.split("/")[0]
+ if mediaType not in toptypes:
+ toptypes[mediaType] = allowed
+
+ result = None
+ result_qval = 0.0
+ for content_type, qval in accepts.items():
+ # Exact match
+ ctype = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+ if ctype in allowedTypes:
+ if qval > result_qval:
+ result = ctype
+ result_qval = qval
+
+ # Subtype wildcard match
+ elif content_type.mediaType != "*" and content_type.mediaSubtype == "*":
+ if content_type.mediaType in toptypes:
+ if qval > result_qval:
+ result = toptypes[content_type.mediaType]
+ result_qval = qval
+
+ # Full wildcard match
+ elif content_type.mediaType == "*" and content_type.mediaSubtype == "*":
+ if qval > result_qval:
+ result = allowedTypes[0]
+ result_qval = qval
+
+ return result
Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -31,6 +31,8 @@
from twext.web2.stream import IStream
from twext.web2.dav.util import allDataFromStream
+from twistedcaldav.config import config
+
from pycalendar.parameter import Parameter
from pycalendar.componentbase import ComponentBase
from pycalendar.exceptions import ErrorBase
@@ -219,8 +221,20 @@
"""
X{vCard} component.
"""
+ allowedTypesList = None
+
+
@classmethod
- def allFromString(clazz, string):
+ def allowedTypes(cls):
+ if cls.allowedTypesList is None:
+ cls.allowedTypesList = ["text/vcard"]
+ if config.EnableJSONData:
+ cls.allowedTypesList.append("application/vcard+json")
+ return cls.allowedTypesList
+
+
+ @classmethod
+ def allFromString(clazz, string, format=None):
"""
FIXME: Just default to reading a single VCARD - actually need more
"""
@@ -234,16 +248,16 @@
if string[:3] == codecs.BOM_UTF8:
string = string[3:]
- return clazz.allFromStream(StringIO.StringIO(string))
+ return clazz.allFromStream(StringIO.StringIO(string), format)
@classmethod
- def allFromStream(clazz, stream):
+ def allFromStream(clazz, stream, format=None):
"""
FIXME: Just default to reading a single VCARD - actually need more
"""
try:
- results = Card.parseMultiple(stream)
+ results = Card.parseMultipleData(stream, format)
except ErrorBase:
results = None
if not results:
@@ -253,47 +267,66 @@
@classmethod
- def fromString(clazz, string):
+ def fromString(clazz, string, format=None):
"""
Construct a L{Component} from a string.
@param string: a string containing vCard data.
@return: a L{Component} representing the first component described by
C{string}.
"""
- if type(string) is unicode:
- string = string.encode("utf-8")
- else:
- # Valid utf-8 please
- string.decode("utf-8")
+ return clazz._fromData(string, False, format)
- # No BOMs please
- if string[:3] == codecs.BOM_UTF8:
- string = string[3:]
- return clazz.fromStream(StringIO.StringIO(string))
+ @classmethod
+ def fromStream(clazz, stream, format=None):
+ """
+ Construct a L{Component} from a stream.
+ @param stream: a C{read()}able stream containing vCard data.
+ @return: a L{Component} representing the first component described by
+ C{stream}.
+ """
+ return clazz._fromData(stream, True, format)
@classmethod
- def fromStream(clazz, stream):
+ def _fromData(clazz, data, isstream, format=None):
"""
Construct a L{Component} from a stream.
@param stream: a C{read()}able stream containing vCard data.
+ @param format: a C{str} indicating whether the data is vCard or jCard
@return: a L{Component} representing the first component described by
C{stream}.
"""
- cal = Card()
+
+ if isstream:
+ pass
+ else:
+ if type(data) is unicode:
+ data = data.encode("utf-8")
+ else:
+ # Valid utf-8 please
+ data.decode("utf-8")
+
+ # No BOMs please
+ if data[:3] == codecs.BOM_UTF8:
+ data = data[3:]
+
+ errmsg = "Unknown"
try:
- result = cal.parse(stream)
- except ErrorBase:
+ result = Card.parseData(data, format)
+ except ErrorBase, e:
+ errmsg = "%s: %s" % (e.mReason, e.mData,)
result = None
if not result:
- stream.seek(0)
- raise InvalidVCardDataError("%s" % (stream.read(),))
- return clazz(None, pycard=cal)
+ if isstream:
+ data.seek(0)
+ data = data.read()
+ raise InvalidVCardDataError("%s\n%s" % (errmsg, data,))
+ return clazz(None, pycard=result)
@classmethod
- def fromIStream(clazz, stream):
+ def fromIStream(clazz, stream, format=None):
"""
Construct a L{Component} from a stream.
@param stream: an L{IStream} containing vCard data.
@@ -307,7 +340,7 @@
# request stream.
#
def parse(data):
- return clazz.fromString(data)
+ return clazz.fromString(data, format)
return allDataFromStream(IStream(stream), parse)
@@ -372,6 +405,18 @@
return self._pycard == other._pycard
+ def getText(self, format=None):
+ """
+ Return text representation
+ """
+ assert self.name() == "VCARD", "Must be a VCARD: %r" % (self,)
+
+ result = self._pycard.getText(format)
+ if result is None:
+ raise ValueError("Unknown format requested for address data.")
+ return result
+
+
# FIXME: Should this not be in __eq__?
def same(self, other):
return self._pycard == other._pycard
@@ -379,7 +424,7 @@
def name(self):
"""
- @return: the name of the iCalendar type of this component.
+ @return: the name of the vCard type of this component.
"""
return self._pycard.getType()
@@ -527,7 +572,7 @@
if unfixed:
log.debug("vCard data had unfixable problems:\n %s" % ("\n ".join(unfixed),))
if doRaise:
- raise InvalidVCardDataError("Calendar data had unfixable problems:\n %s" % ("\n ".join(unfixed),))
+ raise InvalidVCardDataError("Address data had unfixable problems:\n %s" % ("\n ".join(unfixed),))
if fixed:
log.debug("vCard data had fixable problems:\n %s" % ("\n ".join(fixed),))
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -170,9 +170,9 @@
scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
self.assertEqual(len(result.responses), 1)
- self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
- self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
- self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+ self.assertEqual(str(result.responses[0].recipient.children[0]), "mailto:user01 at example.com")
+ self.assertTrue(str(result.responses[0].reqstatus).startswith("2"))
+ self.assertEqual(normalizeiCalendarText(str(result.responses[0].calendar)), data_reply.replace("\n", "\r\n"))
@inlineCallbacks
@@ -227,9 +227,9 @@
scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
self.assertEqual(len(result.responses), 1)
- self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
- self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
- self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+ self.assertEqual(str(result.responses[0].recipient.children[0]), "mailto:user01 at example.com")
+ self.assertTrue(str(result.responses[0].reqstatus).startswith("2"))
+ self.assertEqual(normalizeiCalendarText(str(result.responses[0].calendar)), data_reply.replace("\n", "\r\n"))
@inlineCallbacks
@@ -294,6 +294,6 @@
scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
self.assertEqual(len(result.responses), 1)
- self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
- self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
- self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+ self.assertEqual(str(result.responses[0].recipient.children[0]), "mailto:user01 at example.com")
+ self.assertTrue(str(result.responses[0].reqstatus).startswith("2"))
+ self.assertEqual(normalizeiCalendarText(str(result.responses[0].calendar)), data_reply.replace("\n", "\r\n"))
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -64,7 +64,7 @@
yield delivery.generateSchedulingResponses()
self.assertEqual(len(responses.responses), 1)
- self.assertEqual(str(responses.responses[0].children[1]), iTIPRequestStatus.SERVICE_UNAVAILABLE)
+ self.assertEqual(str(responses.responses[0].reqstatus), iTIPRequestStatus.SERVICE_UNAVAILABLE)
@inlineCallbacks
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -297,7 +297,7 @@
yield txn.commit()
self.assertEquals(
"1.2;Scheduling message has been delivered",
- result.responses[0].children[1].toString()
+ result.responses[0].reqstatus.toString()
)
@@ -330,7 +330,7 @@
yield txn.commit()
self.assertEquals(
"3.7;Invalid Calendar User",
- result.responses[0].children[1].toString()
+ result.responses[0].reqstatus.toString()
)
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -21,7 +21,6 @@
from twext.web2 import responsecode
from twext.web2.http import HTTPError
-from twistedcaldav import caldavxml
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.config import config
from twistedcaldav.ical import Property
@@ -1047,9 +1046,8 @@
# Map each recipient in the response to a status code
responses = {}
for item in response.responses:
- assert isinstance(item, caldavxml.Response), "Wrong element in response"
- recipient = str(item.children[0].children[0])
- status = str(item.children[1])
+ recipient = str(item.recipient.children[0])
+ status = str(item.reqstatus)
responses[recipient] = status
# Now apply to each ATTENDEE/ORGANIZER in the original data
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -163,6 +163,20 @@
maxDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone(utc=True))
maxDateTime.offsetYear(10)
+ dataTypes = []
+ dataTypes.append(
+ ischedulexml.CalendarDataType(**{
+ "content-type": "text/calendar",
+ "version": "2.0",
+ })
+ )
+ if config.EnableJSONData:
+ dataTypes.append(
+ ischedulexml.CalendarDataType(**{
+ "content-type": "application/calendar+json",
+ "version": "2.0",
+ })
+ )
result = ischedulexml.QueryResult(
ischedulexml.Capabilities(
@@ -188,12 +202,7 @@
name="VFREEBUSY"
),
),
- ischedulexml.CalendarDataTypes(
- ischedulexml.CalendarDataType(**{
- "content-type": "text/calendar",
- "version": "2.0",
- }),
- ),
+ ischedulexml.CalendarDataTypes(*dataTypes),
ischedulexml.Attachments(
ischedulexml.External(),
),
@@ -222,24 +231,48 @@
# This is a server-to-server scheduling operation.
scheduler = IScheduleScheduler(txn, None)
+ # Check content first
+ contentType = request.headers.getHeader("content-type")
+ format = self.determineType(contentType)
+
+ if format is None:
+ msg = "MIME type %s not allowed in iSchedule request" % (contentType,)
+ self.log.error(msg)
+ raise HTTPError(scheduler.errorResponse(
+ responsecode.FORBIDDEN,
+ (ischedule_namespace, "invalid-calendar-data-type"),
+ msg,
+ ))
+
originator = self.loadOriginatorFromRequestHeaders(request)
recipients = self.loadRecipientsFromRequestHeaders(request)
body = (yield allDataFromStream(request.stream))
+ calendar = Component.fromString(body, format=format)
# Do the POST processing treating this as a non-local schedule
try:
- result = (yield scheduler.doSchedulingViaPOST(request.remoteAddr, request.headers, body, originator, recipients))
+ result = (yield scheduler.doSchedulingViaPOST(request.remoteAddr, request.headers, body, calendar, originator, recipients))
except Exception:
ex = Failure()
yield txn.abort()
ex.raiseException()
else:
yield txn.commit()
- response = result.response()
+ response = result.response(format=format)
response.headers.addRawHeader(ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber))
returnValue(response)
+ def determineType(self, content_type):
+ """
+ Determine if the supplied content-type is valid for storing and return the matching PyCalendar type.
+ """
+ format = None
+ if content_type is not None:
+ format = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+ return format if format in Component.allowedTypes() else None
+
+
def loadOriginatorFromRequestHeaders(self, request):
# Must have Originator header
originator = request.headers.getRawHeaders("originator")
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -23,7 +23,7 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twistedcaldav.config import config
-from twistedcaldav.ical import normalizeCUAddress, Component
+from twistedcaldav.ical import normalizeCUAddress
from txdav.caldav.datastore.scheduling import addressmapping
from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser
@@ -139,7 +139,7 @@
}
@inlineCallbacks
- def doSchedulingViaPOST(self, remoteAddr, headers, body, originator, recipients):
+ def doSchedulingViaPOST(self, remoteAddr, headers, body, calendar, originator, recipients):
"""
Carry out iSchedule specific processing.
"""
@@ -172,8 +172,6 @@
msg,
))
- calendar = Component.fromString(body)
-
if self.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
self.txn.doing_attendee_refresh = 1
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -94,6 +94,7 @@
headers=http_headers.Headers(rawHeaders={
"Originator": ("mailto:wsanchez at example.com",),
"Recipient": ("mailto:cdaboo at example.com",),
+ "Content-Type": "text/calendar",
}),
content="""BEGIN:VCALENDAR
CALSCALE:GREGORIAN
@@ -144,6 +145,7 @@
headers=http_headers.Headers(rawHeaders={
"Originator": ("mailto:user01 at example.org",),
"Recipient": ("mailto:user02 at example.com",),
+ "Content-Type": ("text/calendar",)
}),
content="""BEGIN:VCALENDAR
CALSCALE:GREGORIAN
Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py 2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py 2013-08-26 16:43:05 UTC (rev 11639)
@@ -42,7 +42,9 @@
from txdav.caldav.datastore.scheduling.imip.delivery import ScheduleViaIMip
from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
+
import hashlib
+from collections import namedtuple
"""
CalDAV/Server-to-Server scheduling behavior.
@@ -703,6 +705,11 @@
response_description_element = davxml.ResponseDescription
calendar_data_element = caldavxml.CalendarData
+ ScheduleResonseDetails = namedtuple(
+ "ScheduleResonseDetails",
+ ["recipient", "reqstatus", "calendar", "error", "message", ]
+ )
+
def __init__(self, method, success_response):
"""
@param method: the name of the method generating the queue.
@@ -748,16 +755,14 @@
if not suppressErrorLog and code > 400: # Error codes only
self.log.error("Error during %s for %s: %s" % (self.method, recipient, message))
- children = []
- children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
- children.append(self.request_status_element(reqstatus))
- if calendar is not None:
- children.append(self.calendar_data_element.fromCalendar(calendar))
- if error is not None:
- children.append(error)
- if message is not None:
- children.append(self.response_description_element(message))
- self.responses.append(self.response_element(*children))
+ details = ScheduleResponseQueue.ScheduleResonseDetails(
+ self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient),
+ self.request_status_element(reqstatus),
+ calendar,
+ error,
+ self.response_description_element(message) if message is not None else None,
+ )
+ self.responses.append(details)
def errorForFailure(self, failure):
@@ -773,19 +778,17 @@
@param clone: the response to clone.
"""
- children = []
- children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
- children.append(self.request_status_element.fromString(request_status))
- if calendar_data is not None:
- children.append(self.calendar_data_element.fromCalendar(calendar_data))
- if error is not None:
- children.append(self.error_element(*error))
- if desc is not None:
- children.append(self.response_description_element.fromString(desc))
- self.responses.append(self.response_element(*children))
+ details = ScheduleResponseQueue.ScheduleResonseDetails(
+ self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient),
+ self.request_status_element.fromString(request_status),
+ calendar_data,
+ self.error_element(*error) if error is not None else None,
+ self.response_description_element.fromString(desc) if desc is not None else None,
+ )
+ self.responses.append(details)
- def response(self):
+ def response(self, format=None):
"""
Generate a L{ScheduleResponseResponse} with the responses contained in the
queue or, if no such responses, return the C{success_response} provided
@@ -793,6 +796,20 @@
@return: the response.
"""
if self.responses:
- return ScheduleResponseResponse(self.schedule_response_element, self.responses, self.location)
+ # Convert our queue to all XML elements
+ xml_responses = []
+ for response in self.responses:
+ children = []
+ children.append(response.recipient)
+ children.append(response.reqstatus)
+ if response.calendar is not None:
+ children.append(self.calendar_data_element.fromCalendar(response.calendar, format))
+ if response.error is not None:
+ children.append(response.error)
+ if response.message is not None:
+ children.append(response.message)
+ xml_responses.append(self.response_element(*children))
+
+ return ScheduleResponseResponse(self.schedule_response_element, xml_responses, self.location)
else:
return self.success_response
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130826/e9365ab0/attachment-0001.html>
More information about the calendarserver-changes
mailing list