diff --git a/lib/ext/Syncroton/Model/AXMLEntry.php b/lib/ext/Syncroton/Model/AXMLEntry.php
index 8a02707..4b60e26 100644
--- a/lib/ext/Syncroton/Model/AXMLEntry.php
+++ b/lib/ext/Syncroton/Model/AXMLEntry.php
@@ -1,328 +1,342 @@
*/
/**
* abstract class to handle ActiveSync entry
*
* @package Syncroton
* @subpackage Model
*/
-
abstract class Syncroton_Model_AXMLEntry extends Syncroton_Model_AEntry implements Syncroton_Model_IXMLEntry
{
protected $_xmlBaseElement;
-
+
protected $_properties = array();
-
+
protected $_dateTimeFormat = "Y-m-d\TH:i:s.000\Z";
-
+
/**
* (non-PHPdoc)
* @see Syncroton_Model_IEntry::__construct()
*/
public function __construct($properties = null)
{
if ($properties instanceof SimpleXMLElement) {
$this->setFromSimpleXMLElement($properties);
} elseif (is_array($properties)) {
$this->setFromArray($properties);
}
-
+
$this->_isDirty = false;
}
-
+
/**
* (non-PHPdoc)
* @see Syncroton_Model_IEntry::appendXML()
*/
public function appendXML(DOMElement $domParrent, Syncroton_Model_IDevice $device)
{
$this->_addXMLNamespaces($domParrent);
-
- foreach($this->_elements as $elementName => $value) {
+
+ foreach ($this->_elements as $elementName => $value) {
// skip empty values
- if($value === null || $value === '' || (is_array($value) && empty($value))) {
+ if ($value === null || $value === '' || (is_array($value) && empty($value))) {
continue;
}
-
- list ($nameSpace, $elementProperties) = $this->_getElementProperties($elementName);
-
- if ($nameSpace == 'Internal') {
- continue;
- }
-
- $elementVersion = isset($elementProperties['supportedSince']) ? $elementProperties['supportedSince'] : '12.0';
-
- if (version_compare($device->acsversion, $elementVersion, '<')) {
+
+ list ($nameSpace, $elementProperties) = $this->_getElementProperties($elementName, $device->acsversion);
+
+ if ($nameSpace == 'Internal') {
continue;
}
-
+
$nameSpace = 'uri:' . $nameSpace;
-
+
if (isset($elementProperties['childElement'])) {
$element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
foreach($value as $subValue) {
$subElement = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementProperties['childElement']));
$this->_appendXMLElement($device, $subElement, $elementProperties, $subValue);
$element->appendChild($subElement);
}
$domParrent->appendChild($element);
} else if ($elementProperties['type'] == 'container' && !empty($elementProperties['multiple'])) {
foreach ($value as $element) {
$container = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
$element->appendXML($container, $device);
$domParrent->appendChild($container);
}
} else if ($elementProperties['type'] == 'none') {
if ($value) {
$element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
$domParrent->appendChild($element);
}
} else {
$element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
$this->_appendXMLElement($device, $element, $elementProperties, $value);
$domParrent->appendChild($element);
}
}
}
-
+
/**
* (non-PHPdoc)
* @see Syncroton_Model_IEntry::getProperties()
*/
public function getProperties($selectedNamespace = null)
{
$properties = array();
-
- foreach($this->_properties as $namespace => $namespaceProperties) {
+
+ foreach ($this->_properties as $namespace => $namespaceProperties) {
if ($selectedNamespace !== null && $namespace != $selectedNamespace) {
continue;
}
- $properties = array_merge($properties, array_keys($namespaceProperties));
+ $properties = array_merge($properties, array_keys($namespaceProperties));
}
-
- return $properties;
-
+
+ return array_unique($properties);
}
-
- /**
- * set properties from SimpleXMLElement object
- *
- * @param SimpleXMLElement $xmlCollection
- * @throws InvalidArgumentException
- */
- public function setFromSimpleXMLElement(SimpleXMLElement $properties)
- {
+
+ /**
+ * set properties from SimpleXMLElement object
+ *
+ * @param SimpleXMLElement $xmlCollection
+ * @throws InvalidArgumentException
+ */
+ public function setFromSimpleXMLElement(SimpleXMLElement $properties)
+ {
if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) {
- throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
- }
-
+ throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
+ }
+
foreach (array_keys($this->_properties) as $namespace) {
if ($namespace == 'Internal') {
continue;
}
-
- $this->_parseNamespace($namespace, $properties);
- }
-
- return;
+
+ $this->_parseNamespace($namespace, $properties);
+ }
}
-
+
/**
* add needed xml namespaces to DomDocument
*
* @param unknown_type $domParrent
*/
- protected function _addXMLNamespaces(DOMElement $domParrent)
+ protected function _addXMLNamespaces(DOMElement $domParrent)
{
- foreach($this->_properties as $namespace => $namespaceProperties) {
+ foreach (array_keys($this->_properties) as $namespace) {
// don't add default namespace again
- if($domParrent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) {
+ if ($domParrent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) {
$domParrent->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:'.$namespace, 'uri:'.$namespace);
- }
+ }
}
- }
-
+ }
+
protected function _appendXMLElement(Syncroton_Model_IDevice $device, DOMElement $element, $elementProperties, $value)
{
- if ($value instanceof Syncroton_Model_IEntry) {
- $value->appendXML($element, $device);
- } else {
- if ($value instanceof DateTime) {
+ if ($value instanceof Syncroton_Model_IEntry && $elementProperties['type'] === 'container') {
+ $value->appendXML($element, $device);
+ } else {
+ if ($value instanceof DateTime) {
$value = $value->format($this->_dateTimeFormat);
-
- } elseif (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {
- if (is_resource($value)) {
+ } elseif (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {
+ if (is_resource($value)) {
rewind($value);
- $value = stream_get_contents($value);
- }
- $value = base64_encode($value);
- }
-
+ $value = stream_get_contents($value);
+ }
+ $value = base64_encode($value);
+ }
+
if ($elementProperties['type'] == 'byteArray') {
$element->setAttributeNS('uri:Syncroton', 'Syncroton:encoding', 'opaque');
// encode to base64; the wbxml encoder will base64_decode it again
// this way we can also transport data, which would break the xmlparser otherwise
$element->appendChild($element->ownerDocument->createCDATASection(base64_encode($value)));
+ } else if ($elementProperties['type'] == 'double') {
+ $element->appendChild($element->ownerDocument->createTextNode((string) floatval($value)));
} else {
+ $value = (string) $value;
// strip off any non printable control characters
if (!ctype_print($value)) {
$value = $this->_removeControlChars($value);
}
-
+
$element->appendChild($element->ownerDocument->createTextNode($this->_enforceUTF8($value)));
- }
- }
+ }
+ }
}
-
+
/**
* remove control chars from a string which are not allowed in XML values
*
* @param string $dirty An input string
* @return string Cleaned up string
*/
protected function _removeControlChars($dirty)
{
// Replace non-character UTF-8 sequences that cause XML Parser to fail
// https://git.kolab.org/T1311
$dirty = str_replace(array("\xEF\xBF\xBE", "\xEF\xBF\xBF"), '', $dirty);
// Replace ASCII control-characters
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $dirty);
}
-
+
/**
* enforce >valid< utf-8 encoding
- *
+ *
* @param string $dirty the string with maybe invalid utf-8 data
* @return string string with valid utf-8
*/
protected function _enforceUTF8($dirty)
{
if (function_exists('iconv')) {
if (($clean = @iconv('UTF-8', 'UTF-8//IGNORE', $dirty)) !== false) {
return $clean;
}
}
-
+
if (function_exists('mb_convert_encoding')) {
if (($clean = mb_convert_encoding($dirty, 'UTF-8', 'UTF-8')) !== false) {
return $clean;
}
}
-
+
return $dirty;
}
-
+
/**
- *
- * @param unknown_type $element
+ *
+ * @param unknown_type $element Element name
+ * @param string $version Protocol version
* @throws InvalidArgumentException
- * @return multitype:unknown
+ * @return array
*/
- protected function _getElementProperties($element)
+ protected function _getElementProperties($element, $version = null)
{
- foreach($this->_properties as $namespace => $namespaceProperties) {
- if (array_key_exists($element, $namespaceProperties)) {
- return array($namespace, $namespaceProperties[$element]);
- }
- }
-
- throw new InvalidArgumentException("$element is no valid property of " . get_class($this));
+
+ foreach ($this->_properties as $namespace => $namespaceProperties) {
+ if (array_key_exists($element, $namespaceProperties)) {
+ $elementProperties = $namespaceProperties[$element];
+
+ if ($version) {
+ $supportedSince = isset($elementProperties['supportedSince']) ? $elementProperties['supportedSince'] : '12.0';
+ $supportedUntil = isset($elementProperties['supportedUntil']) ? $elementProperties['supportedUntil'] : '9999';
+
+ if (version_compare($version, $supportedSince, '<') || version_compare($version, $supportedUntil, '>')) {
+ continue;
+ }
+ }
+
+ return array($namespace, $elementProperties);
+ }
+ }
+
+ throw new InvalidArgumentException("$element is no valid property of " . get_class($this));
}
-
+
protected function _parseNamespace($nameSpace, SimpleXMLElement $properties)
{
- // fetch data from Contacts namespace
- $children = $properties->children("uri:$nameSpace");
-
+ // fetch data from the specified namespace
+ $children = $properties->children("uri:$nameSpace");
+
foreach ($children as $elementName => $xmlElement) {
$elementName = lcfirst($elementName);
-
+
if (!isset($this->_properties[$nameSpace][$elementName])) {
continue;
}
-
+
list (, $elementProperties) = $this->_getElementProperties($elementName);
-
+
switch ($elementProperties['type']) {
case 'container':
if (!empty($elementProperties['multiple'])) {
$property = (array) $this->$elementName;
-
+
if (isset($elementProperties['class'])) {
$property[] = new $elementProperties['class']($xmlElement);
} else {
$property[] = (string) $xmlElement;
}
} else if (isset($elementProperties['childElement'])) {
$property = array();
-
$childElement = ucfirst($elementProperties['childElement']);
-
+
foreach ($xmlElement->$childElement as $subXmlElement) {
if (isset($elementProperties['class'])) {
$property[] = new $elementProperties['class']($subXmlElement);
} else {
$property[] = (string) $subXmlElement;
}
}
} else {
- $subClassName = isset($elementProperties['class']) ? $elementProperties['class'] : get_class($this) . ucfirst($elementName);
-
- $property = new $subClassName($xmlElement);
+ $subClassName = isset($elementProperties['class']) ? $elementProperties['class'] : get_class($this) . ucfirst($elementName);
+
+ $property = new $subClassName($xmlElement);
}
-
+
break;
-
+
case 'datetime':
$property = new DateTime((string) $xmlElement, new DateTimeZone('UTC'));
break;
case 'number':
$property = (int) $xmlElement;
break;
+ case 'double':
+ $property = (float) $xmlElement;
+ break;
+
default:
$property = (string) $xmlElement;
break;
}
if (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {
$property = base64_decode($property);
}
-
+
$this->$elementName = $property;
- }
+ }
+ }
+
+ public function &__get($name)
+ {
+ $this->_getElementProperties($name);
+
+ return $this->_elements[$name];
}
-
- public function &__get($name)
- {
- $this->_getElementProperties($name);
-
- return $this->_elements[$name];
- }
-
- public function __set($name, $value)
- {
- list ($nameSpace, $properties) = $this->_getElementProperties($name);
-
- if ($properties['type'] == 'datetime' && !$value instanceof DateTime) {
- throw new InvalidArgumentException("value for $name must be an instance of DateTime");
- }
-
- if (!array_key_exists($name, $this->_elements) || $this->_elements[$name] != $value) {
+
+ public function __set($name, $value)
+ {
+ list ($nameSpace, $properties) = $this->_getElementProperties($name);
+
+ if ($properties['type'] == 'datetime' && !$value instanceof DateTime) {
+ throw new InvalidArgumentException("value for $name must be an instance of DateTime");
+ }
+
+ if (!array_key_exists($name, $this->_elements) || $this->_elements[$name] != $value) {
+ // Always use location object
+ if ($name === 'location' && !($value instanceof Syncroton_Model_Location)) {
+ $value = new Syncroton_Model_Location($value);
+ }
+
$this->_elements[$name] = $value;
-
+
$this->_isDirty = true;
- }
- }
-}
\ No newline at end of file
+ }
+ }
+}
diff --git a/lib/ext/Syncroton/Model/Email.php b/lib/ext/Syncroton/Model/Email.php
index cd448c8..76de3f7 100644
--- a/lib/ext/Syncroton/Model/Email.php
+++ b/lib/ext/Syncroton/Model/Email.php
@@ -1,90 +1,129 @@
*/
/**
* class to handle ActiveSync email
*
* @package Syncroton
* @subpackage Model
- * @property array attachments
- * @property string contentType
- * @property array flag
- * @property Syncroton_Model_EmailBody body
- * @property array cc
- * @property array to
- * @property int lastVerbExecuted
- * @property DateTime lastVerbExecutionTime
- * @property int read
+ *
+ * @property string $accountId
+ * @property Syncroton_Model_EmailAttachment[] $attachments
+ * @property Syncroton_Model_EmailBody $body
+ * @property int $busyStatus
+ * @property array $categories
+ * @property string $cc
+ * @property DateTime $completeTime
+ * @property string $contentClass
+ * @property string $contentType
+ * @property string $conversationId
+ * @property string $conversationIndex
+ * @property DateTime $dateReceived
+ * @property bool $disallowNewTimeProposal
+ * @property string $displayTo
+ * @property DateTime $dtStamp
+ * @property DateTime $endTime
+ * @property Syncroton_Model_EmailFlag $flag
+ * @property string $from
+ * @property string $globalObjId
+ * @property int $importance
+ * @property int $instanceType
+ * @property string $internetCPID
+ * @property int $lastVerbExecuted
+ * @property DateTime $lastVerbExecutionTime
+ * @property Syncroton_Model_Location $location
+ * @property int $meetingMessageType
+ * @property Syncroton_Model_EmailMeetingRequest $meetingRequest
+ * @property string $messageClass
+ * @property int $nativeBodyType
+ * @property string $organizer
+ * @property int $read
+ * @property int $receivedAsBcc
+ * @property Syncroton_Model_EventRecurrence[] $recurrences
+ * @property int $reminder
+ * @property string $replyTo
+ * @property int $responseRequested
+ * @property string $sender
+ * @property int $sensitivity
+ * @property DateTime $startTime
+ * @property int $status
+ * @property string $subject
+ * @property string $threadTopic
+ * @property string $timeZone
+ * @property string $to
+ * @property string $umCallerID
+ * @property string $umUserNotes
*/
class Syncroton_Model_Email extends Syncroton_Model_AXMLEntry
{
const LASTVERB_UNKNOWN = 0;
const LASTVERB_REPLYTOSENDER = 1;
const LASTVERB_REPLYTOALL = 2;
const LASTVERB_FORWARD = 3;
-
+
protected $_xmlBaseElement = 'ApplicationData';
-
+
protected $_properties = array(
'AirSyncBase' => array(
'attachments' => array('type' => 'container', 'childElement' => 'attachment', 'class' => 'Syncroton_Model_EmailAttachment'),
'contentType' => array('type' => 'string'),
'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody'),
+ 'location' => array('type' => 'container', 'class' => 'Syncroton_Model_Location', 'supportedSince' => '16.0'),
'nativeBodyType' => array('type' => 'number'),
),
'Email' => array(
'busyStatus' => array('type' => 'number'),
'categories' => array('type' => 'container', 'childElement' => 'category', 'supportedSince' => '14.0'),
'cc' => array('type' => 'string'),
'completeTime' => array('type' => 'datetime'),
'contentClass' => array('type' => 'string'),
'dateReceived' => array('type' => 'datetime'),
'disallowNewTimeProposal' => array('type' => 'number'),
'displayTo' => array('type' => 'string'),
'dTStamp' => array('type' => 'datetime'),
'endTime' => array('type' => 'datetime'),
'flag' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailFlag'),
'from' => array('type' => 'string'),
'globalObjId' => array('type' => 'string'),
'importance' => array('type' => 'number'),
'instanceType' => array('type' => 'number'),
'internetCPID' => array('type' => 'string'),
- 'location' => array('type' => 'string'),
+ 'location' => array('type' => 'string', 'supportedUntil' => '16.0'),
'meetingRequest' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailMeetingRequest'),
'messageClass' => array('type' => 'string'),
'organizer' => array('type' => 'string'),
'read' => array('type' => 'number'),
'recurrences' => array('type' => 'container'),
'reminder' => array('type' => 'number'),
'replyTo' => array('type' => 'string'),
'responseRequested' => array('type' => 'number'),
'sensitivity' => array('type' => 'number'),
'startTime' => array('type' => 'datetime'),
'status' => array('type' => 'number'),
'subject' => array('type' => 'string'),
'threadTopic' => array('type' => 'string'),
'timeZone' => array('type' => 'timezone'),
'to' => array('type' => 'string'),
),
'Email2' => array(
'accountId' => array('type' => 'string', 'supportedSince' => '14.1'),
'conversationId' => array('type' => 'byteArray', 'supportedSince' => '14.0'),
'conversationIndex' => array('type' => 'byteArray', 'supportedSince' => '14.0'),
'lastVerbExecuted' => array('type' => 'number', 'supportedSince' => '14.0'),
'lastVerbExecutionTime' => array('type' => 'datetime', 'supportedSince' => '14.0'),
'meetingMessageType' => array('type' => 'number', 'supportedSince' => '14.1'),
'receivedAsBcc' => array('type' => 'number', 'supportedSince' => '14.0'),
'sender' => array('type' => 'string', 'supportedSince' => '14.0'),
'umCallerID' => array('type' => 'string', 'supportedSince' => '14.0'),
'umUserNotes' => array('type' => 'string', 'supportedSince' => '14.0'),
),
);
}
diff --git a/lib/ext/Syncroton/Model/EmailMeetingRequest.php b/lib/ext/Syncroton/Model/EmailMeetingRequest.php
index 2786be4..7123382 100644
--- a/lib/ext/Syncroton/Model/EmailMeetingRequest.php
+++ b/lib/ext/Syncroton/Model/EmailMeetingRequest.php
@@ -1,98 +1,102 @@
*/
/**
* class to handle Email:MeetingRequest
*
* @package Syncroton
* @subpackage Model
- * @property bool AllDayEvent
- * @property int BusyStatus
- * @property int DisallowNewTimeProposal
- * @property DateTime DtStamp
- * @property DateTime EndTime
- * @property string GlobalObjId
- * @property int InstanceType
- * @property int MeetingMessageType
- * @property string Organizer
- * @property string RecurrenceId
- * @property array Recurrences
- * @property int Reminder
- * @property int ResponseRequested
- * @property int Sensitivity
- * @property DateTime StartTime
- * @property string Timezone
+ * @property bool $allDayEvent
+ * @property int $busyStatus
+ * @property int $disallowNewTimeProposal
+ * @property DateTime $dtStamp
+ * @property DateTime $endTime
+ * @property string $globalObjId
+ * @property int $instanceType
+ * @property Syncroton_Model_Location $location
+ * @property int $meetingMessageType
+ * @property string $organizer
+ * @property string $recurrenceId
+ * @property array $recurrences
+ * @property int $reminder
+ * @property int $responseRequested
+ * @property int $sensitivity
+ * @property DateTime $startTime
+ * @property string $timezone
*/
class Syncroton_Model_EmailMeetingRequest extends Syncroton_Model_AXMLEntry
{
/**
* busy status constants
*/
const BUSY_STATUS_FREE = 0;
const BUSY_STATUS_TENATTIVE = 1;
const BUSY_STATUS_BUSY = 2;
const BUSY_STATUS_OUT = 3;
/**
* sensitivity constants
*/
const SENSITIVITY_NORMAL = 0;
const SENSITIVITY_PERSONAL = 1;
const SENSITIVITY_PRIVATE = 2;
const SENSITIVITY_CONFIDENTIAL = 3;
/**
* instanceType constants
*/
const TYPE_NORMAL = 0;
const TYPE_RECURRING_MASTER = 1;
const TYPE_RECURRING_SINGLE = 2;
const TYPE_RECURRING_EXCEPTION = 3;
/**
* messageType constants
*/
const MESSAGE_TYPE_NORMAL = 0;
const MESSAGE_TYPE_REQUEST = 1;
const MESSAGE_TYPE_FULL_UPDATE = 2;
const MESSAGE_TYPE_INFO_UPDATE = 3;
const MESSAGE_TYPE_OUTDATED = 4;
const MESSAGE_TYPE_COPY = 5;
const MESSAGE_TYPE_DELEGATED = 6;
protected $_dateTimeFormat = "Ymd\THis\Z";
protected $_xmlBaseElement = 'MeetingRequest';
protected $_properties = array(
+ 'AirSyncBase' => array(
+ 'location' => array('type' => 'container', 'class' => 'Syncroton_Model_Location', 'supportedSince' => '16.0'),
+ ),
'Email' => array(
'allDayEvent' => array('type' => 'number'),
'busyStatus' => array('type' => 'number'),
'disallowNewTimeProposal' => array('type' => 'number'),
'dtStamp' => array('type' => 'datetime'),
'endTime' => array('type' => 'datetime'),
'globalObjId' => array('type' => 'string'),
'instanceType' => array('type' => 'datetime'),
- 'location' => array('type' => 'string'),
+ 'location' => array('type' => 'string', 'supportedUntil' => '16.0'),
'organizer' => array('type' => 'string'), //e-mail address
'recurrenceId' => array('type' => 'datetime'),
'recurrences' => array('type' => 'container'),
'reminder' => array('type' => 'number'),
'responseRequested' => array('type' => 'number'),
'sensitivity' => array('type' => 'number'),
'startTime' => array('type' => 'datetime'),
'timeZone' => array('type' => 'timezone'),
),
'Email2' => array(
'meetingMessageType' => array('type' => 'number'),
),
);
}
diff --git a/lib/ext/Syncroton/Model/Event.php b/lib/ext/Syncroton/Model/Event.php
index 679a2a1..2289f79 100644
--- a/lib/ext/Syncroton/Model/Event.php
+++ b/lib/ext/Syncroton/Model/Event.php
@@ -1,125 +1,147 @@
*/
/**
- * class to handle ActiveSync event
+ * Class to handle ActiveSync event
*
* @package Syncroton
* @subpackage Model
- * @property string class
- * @property string collectionId
- * @property bool deletesAsMoves
- * @property bool getChanges
- * @property string syncKey
- * @property int windowSize
+ *
+ * @property Syncroton_Model_EmailBody $body
+ * @property bool $allDayEvent
+ * @property DateTime $appointmentReplyTime
+ * @property Syncroton_Model_EventAttendee[] $attendees
+ * @property int $busyStatus
+ * @property array $categories
+ * @property string $clientUid
+ * @property bool $disallowNewTimeProposal
+ * @property DateTime $dtStamp
+ * @property DateTime $endTime
+ * @property Syncroton_Model_EventException[] $exceptions
+ * @property Syncroton_Model_Location $location
+ * @property int $meetingStatus
+ * @property string $onlineMeetingConfLink
+ * @property string $onlineMeetingExternalLink
+ * @property string $organizerEmail
+ * @property string $organizerName
+ * @property Syncroton_Model_EventRecurrence $recurrence
+ * @property int $reminder
+ * @property int $responseRequested
+ * @property int $responseType
+ * @property int $sensitivity
+ * @property DateTime $startTime
+ * @property string $subject
+ * @property string $timezone
+ * @property string $uID
*/
class Syncroton_Model_Event extends Syncroton_Model_AXMLEntry
{
/**
* busy status constants
*/
const BUSY_STATUS_FREE = 0;
const BUSY_STATUS_TENATTIVE = 1;
const BUSY_STATUS_BUSY = 2;
protected $_dateTimeFormat = "Ymd\THis\Z";
protected $_xmlBaseElement = 'ApplicationData';
protected $_properties = array(
'AirSyncBase' => array(
- 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody')
+ 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody'),
+ 'location' => array('type' => 'container', 'class' => 'Syncroton_Model_Location', 'supportedSince' => '16.0'),
),
'Calendar' => array(
'allDayEvent' => array('type' => 'number'),
'appointmentReplyTime' => array('type' => 'datetime'),
'attendees' => array('type' => 'container', 'childElement' => 'attendee', 'class' => 'Syncroton_Model_EventAttendee'),
'busyStatus' => array('type' => 'number'),
'categories' => array('type' => 'container', 'childElement' => 'category'),
'clientUid' => array('type' => 'string', 'supportedSince' => '16.0'),
'disallowNewTimeProposal' => array('type' => 'number'),
'dtStamp' => array('type' => 'datetime'),
'endTime' => array('type' => 'datetime'),
'exceptions' => array('type' => 'container', 'childElement' => 'exception', 'class' => 'Syncroton_Model_EventException'),
- 'location' => array('type' => 'string'),
+ 'location' => array('type' => 'string', 'supportedUntil' => '16.0'),
'meetingStatus' => array('type' => 'number'),
'onlineMeetingConfLink' => array('type' => 'string'),
'onlineMeetingExternalLink' => array('type' => 'string'),
'organizerEmail' => array('type' => 'string'),
'organizerName' => array('type' => 'string'),
- 'recurrence' => array('type' => 'container'),
+ 'recurrence' => array('type' => 'container', 'class' => 'Syncroton_Model_EventRecurrence'),
'reminder' => array('type' => 'number'),
'responseRequested' => array('type' => 'number'),
'responseType' => array('type' => 'number'),
'sensitivity' => array('type' => 'number'),
'startTime' => array('type' => 'datetime'),
'subject' => array('type' => 'string'),
'timezone' => array('type' => 'timezone'),
'uID' => array('type' => 'string'),
)
);
/**
* (non-PHPdoc)
* @see Syncroton_Model_IEntry::appendXML()
* @todo handle Attendees element
*/
public function appendXML(DOMElement $domParrent, Syncroton_Model_IDevice $device)
{
parent::appendXML($domParrent, $device);
$exceptionElements = $domParrent->getElementsByTagName('Exception');
$parentFields = array('AllDayEvent'/*, 'Attendees'*/, 'Body', 'BusyStatus'/*, 'Categories'*/, 'DtStamp', 'EndTime', 'Location', 'MeetingStatus', 'Reminder', 'ResponseType', 'Sensitivity', 'StartTime', 'Subject');
if ($exceptionElements->length > 0) {
$mainEventElement = $exceptionElements->item(0)->parentNode->parentNode;
foreach ($mainEventElement->childNodes as $childNode) {
if (in_array($childNode->localName, $parentFields)) {
foreach ($exceptionElements as $exception) {
$elementsToLeftOut = $exception->getElementsByTagName($childNode->localName);
foreach ($elementsToLeftOut as $elementToLeftOut) {
if ($elementToLeftOut->nodeValue == $childNode->nodeValue) {
$exception->removeChild($elementToLeftOut);
}
}
}
}
}
}
}
/**
* some elements of an exception can be left out, if they have the same value
* like the main event
*
* this function copies these elements to the exception for backends which need
* this elements in the exceptions too. Tine 2.0 needs this for example.
*/
public function copyFieldsFromParent()
{
if (isset($this->_elements['exceptions']) && is_array($this->_elements['exceptions'])) {
foreach ($this->_elements['exceptions'] as $exception) {
// no need to update deleted exceptions
if ($exception->deleted == 1) {
continue;
}
$parentFields = array('allDayEvent', 'attendees', 'body', 'busyStatus', 'categories', 'dtStamp', 'endTime', 'location', 'meetingStatus', 'reminder', 'responseType', 'sensitivity', 'startTime', 'subject');
foreach ($parentFields as $field) {
if (!isset($exception->$field) && isset($this->_elements[$field])) {
$exception->$field = $this->_elements[$field];
}
}
}
}
}
}
diff --git a/lib/ext/Syncroton/Model/EventException.php b/lib/ext/Syncroton/Model/EventException.php
index 06aece9..3b94250 100644
--- a/lib/ext/Syncroton/Model/EventException.php
+++ b/lib/ext/Syncroton/Model/EventException.php
@@ -1,54 +1,65 @@
*/
/**
- * class to handle ActiveSync event
+ * Class to handle ActiveSync event
*
* @package Syncroton
* @subpackage Model
- * @property string class
- * @property string collectionId
- * @property bool deletesAsMoves
- * @property bool getChanges
- * @property string syncKey
- * @property int windowSize
+ *
+ * @property Syncroton_Model_EmailBody $body
+ * @property bool $allDayEvent
+ * @property DateTime $appointmentReplyTime
+ * @property Syncroton_Model_EventAttendee[] $attendees
+ * @property int $busyStatus
+ * @property array $categories
+ * @property bool $deleted
+ * @property DateTime $dtStamp
+ * @property DateTime $endTime
+ * @property DateTime exceptionStartTime
+ * @property Syncroton_Model_Location $location
+ * @property int $meetingStatus
+ * @property int $reminder
+ * @property int $responseType
+ * @property int $sensitivity
+ * @property DateTime $startTime
+ * @property string $subject
*/
-
class Syncroton_Model_EventException extends Syncroton_Model_AXMLEntry
-{
+{
protected $_xmlBaseElement = 'Exception';
-
protected $_dateTimeFormat = "Ymd\THis\Z";
-
protected $_properties = array(
'AirSyncBase' => array(
- 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody')
+ 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody'),
+ 'location' => array('type' => 'container', 'class' => 'Syncroton_Model_Location', 'supportedSince' => '16.0'),
),
'Calendar' => array(
- 'allDayEvent' => array('type' => 'number'),
- 'appointmentReplyTime' => array('type' => 'datetime'),
- 'attendees' => array('type' => 'container', 'childElement' => 'attendee', 'class' => 'Syncroton_Model_EventAttendee'),
- 'busyStatus' => array('type' => 'number'),
- 'categories' => array('type' => 'container', 'childElement' => 'category'),
- 'deleted' => array('type' => 'number'),
+ 'allDayEvent' => array('type' => 'number'),
+ 'appointmentReplyTime' => array('type' => 'datetime'),
+ 'attendees' => array('type' => 'container', 'childElement' => 'attendee', 'class' => 'Syncroton_Model_EventAttendee'),
+ 'busyStatus' => array('type' => 'number'),
+ 'categories' => array('type' => 'container', 'childElement' => 'category'),
+ 'deleted' => array('type' => 'number'),
'dtStamp' => array('type' => 'datetime'),
- 'endTime' => array('type' => 'datetime'),
- 'exceptionStartTime' => array('type' => 'datetime'),
- 'location' => array('type' => 'string'),
- 'meetingStatus' => array('type' => 'number'),
- 'reminder' => array('type' => 'number'),
- 'responseType' => array('type' => 'number'),
+ 'endTime' => array('type' => 'datetime'),
+ 'exceptionStartTime' => array('type' => 'datetime'),
+ 'location' => array('type' => 'string', 'supportedUntil' => '16.0'),
+ 'meetingStatus' => array('type' => 'number'),
+ 'reminder' => array('type' => 'number'),
+ 'responseType' => array('type' => 'number'),
'sensitivity' => array('type' => 'number'),
'startTime' => array('type' => 'datetime'),
- 'subject' => array('type' => 'string'),
+ 'subject' => array('type' => 'string'),
)
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/lib/ext/Syncroton/Model/Location.php b/lib/ext/Syncroton/Model/Location.php
new file mode 100644
index 0000000..91c5035
--- /dev/null
+++ b/lib/ext/Syncroton/Model/Location.php
@@ -0,0 +1,95 @@
+
+ */
+
+/**
+ * Class to handle ActiveSync AirSyncBase::Location element
+ *
+ * @package Syncroton
+ * @subpackage Model
+ *
+ * @property float $accuracy Accuracy of the values of the Latitude element
+ * @property float $altitude Altitude of the event's location
+ * @property float $altitudeAccuracy Accuracy of the value of the Altitude element
+ * @property string $annotation Note about the event's location
+ * @property string $city City of the event's location
+ * @property string $country Country of the event's location
+ * @property string $displayName Display name of the event's location
+ * @property float $latitude Latitude of the event's location
+ * @property string $locationUri URI for the event's location
+ * @property float $longitude Longitude of the event's location
+ * @property string $postalCode Postal code for the address of the event's location
+ * @property string $state State or province of the event's location
+ * @property string $street Street address of the event's location
+ */
+class Syncroton_Model_Location extends Syncroton_Model_AXMLEntry
+{
+ protected $_xmlBaseElement = 'Location';
+
+ protected $_properties = array(
+ 'AirSyncBase' => array(
+ 'accuracy' => array('type' => 'double', 'supportedSince' => '16.0'),
+ 'altitude' => array('type' => 'double', 'supportedSince' => '16.0'),
+ 'altitudeAccuracy' => array('type' => 'double', 'supportedSince' => '16.0'),
+ 'annotation' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'city' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'country' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'displayName' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'latitude' => array('type' => 'double', 'supportedSince' => '16.0'),
+ 'locationUri' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'longitude' => array('type' => 'double', 'supportedSince' => '16.0'),
+ 'postalCode' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'state' => array('type' => 'string', 'supportedSince' => '16.0'),
+ 'street' => array('type' => 'string', 'supportedSince' => '16.0'),
+ )
+ );
+
+ /**
+ * (non-PHPdoc)
+ * @see Syncroton_Model_IEntry::__construct()
+ */
+ public function __construct($properties = null)
+ {
+ // Support ActiveSync < 16 location as a string
+ if (($properties instanceof SimpleXMLElement && $properties->count() == 0)
+ || (is_string($properties) && strlen($properties) > 0)
+ ) {
+ $properties = (string) $properties;
+
+ // Note: iOS 12.4 does not use LocationUri, URLs are stored in DisplayName
+ $this->_elements['displayName'] = $properties;
+
+ $properties = null;
+ }
+
+ parent::__construct($properties);
+ }
+
+ /**
+ * To string converter.
+ *
+ * It will be used with ActiveSync < 16.0, where Location is a property of type string
+ *
+ * @return string String representation of the object
+ */
+ public function __toString()
+ {
+ if (!empty($this->_elements['displayName'])) {
+ return (string) $this->_elements['displayName'];
+ }
+
+ if (!empty($this->_elements['locationUri'])) {
+ return (string) $this->_elements['locationUri'];
+ }
+
+ return '';
+ }
+}
diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php
index abf2de6..b558b3a 100644
--- a/lib/kolab_sync_data_calendar.php
+++ b/lib/kolab_sync_data_calendar.php
@@ -1,1095 +1,1115 @@
|
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak |
+--------------------------------------------------------------------------+
*/
/**
* Calendar (Events) data class for Syncroton
*/
class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data_IDataCalendar
{
/**
* Mapping from ActiveSync Calendar namespace fields
*/
protected $mapping = array(
'allDayEvent' => 'allday',
'startTime' => 'start', // keep it before endTime here
//'attendees' => 'attendees',
'body' => 'description',
//'bodyTruncated' => 'bodytruncated',
'busyStatus' => 'free_busy',
//'categories' => 'categories',
'dtStamp' => 'changed',
'endTime' => 'end',
//'exceptions' => 'exceptions',
'location' => 'location',
//'meetingStatus' => 'meetingstatus',
//'organizerEmail' => 'organizeremail',
//'organizerName' => 'organizername',
//'recurrence' => 'recurrence',
//'reminder' => 'reminder',
//'responseRequested' => 'responserequested',
//'responseType' => 'responsetype',
'sensitivity' => 'sensitivity',
'subject' => 'title',
//'timezone' => 'timezone',
'uID' => 'uid',
);
/**
* Kolab object type
*
* @var string
*/
protected $modelName = 'event';
/**
* Type of the default folder
*
* @var int
*/
protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR;
/**
* Default container for new entries
*
* @var string
*/
protected $defaultFolder = 'Calendar';
/**
* Type of user created folders
*
* @var int
*/
protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED;
/**
* attendee status
*/
const ATTENDEE_STATUS_UNKNOWN = 0;
const ATTENDEE_STATUS_TENTATIVE = 2;
const ATTENDEE_STATUS_ACCEPTED = 3;
const ATTENDEE_STATUS_DECLINED = 4;
const ATTENDEE_STATUS_NOTRESPONDED = 5;
/**
* attendee types
*/
const ATTENDEE_TYPE_REQUIRED = 1;
const ATTENDEE_TYPE_OPTIONAL = 2;
const ATTENDEE_TYPE_RESOURCE = 3;
/**
* busy status constants
*/
const BUSY_STATUS_FREE = 0;
const BUSY_STATUS_TENTATIVE = 1;
const BUSY_STATUS_BUSY = 2;
const BUSY_STATUS_OUTOFOFFICE = 3;
/**
* Sensitivity values
*/
const SENSITIVITY_NORMAL = 0;
const SENSITIVITY_PERSONAL = 1;
const SENSITIVITY_PRIVATE = 2;
const SENSITIVITY_CONFIDENTIAL = 3;
const KEY_DTSTAMP = 'x-custom.X-ACTIVESYNC-DTSTAMP';
const KEY_RESPONSE_DTSTAMP = 'x-custom.X-ACTIVESYNC-RESPONSE-DTSTAMP';
/**
* Mapping of attendee status
*
* @var array
*/
protected $attendeeStatusMap = array(
'UNKNOWN' => self::ATTENDEE_STATUS_UNKNOWN,
'TENTATIVE' => self::ATTENDEE_STATUS_TENTATIVE,
'ACCEPTED' => self::ATTENDEE_STATUS_ACCEPTED,
'DECLINED' => self::ATTENDEE_STATUS_DECLINED,
'DELEGATED' => self::ATTENDEE_STATUS_UNKNOWN,
'NEEDS-ACTION' => self::ATTENDEE_STATUS_NOTRESPONDED,
);
/**
* Mapping of attendee type
*
* NOTE: recurrences need extra handling!
* @var array
*/
protected $attendeeTypeMap = array(
'REQ-PARTICIPANT' => self::ATTENDEE_TYPE_REQUIRED,
'OPT-PARTICIPANT' => self::ATTENDEE_TYPE_OPTIONAL,
// 'NON-PARTICIPANT' => self::ATTENDEE_TYPE_RESOURCE,
// 'CHAIR' => self::ATTENDEE_TYPE_RESOURCE,
);
/**
* Mapping of busy status
*
* @var array
*/
protected $busyStatusMap = array(
'free' => self::BUSY_STATUS_FREE,
'tentative' => self::BUSY_STATUS_TENTATIVE,
'busy' => self::BUSY_STATUS_BUSY,
'outofoffice' => self::BUSY_STATUS_OUTOFOFFICE,
);
/**
* mapping of sensitivity
*
* @var array
*/
protected $sensitivityMap = array(
'public' => self::SENSITIVITY_PERSONAL,
'private' => self::SENSITIVITY_PRIVATE,
'confidential' => self::SENSITIVITY_CONFIDENTIAL,
);
/**
* Appends contact data to xml element
*
* @param Syncroton_Model_SyncCollection $collection Collection data
* @param string $serverId Local entry identifier
* @param boolean $as_array Return entry as array
*
* @return array|Syncroton_Model_Event|array Event object
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false)
{
$event = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId);
$config = $this->getFolderConfig($event['_mailbox']);
$result = array();
// Timezone
// Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows
// only one timezone per-event. We'll use timezone of the start date
if ($event['start'] instanceof DateTime) {
$timezone = $event['start']->getTimezone();
if ($timezone && ($tz_name = $timezone->getName()) != 'UTC') {
$tzc = kolab_sync_timezone_converter::getInstance();
if ($tz_name = $tzc->encodeTimezone($tz_name)) {
$result['timezone'] = $tz_name;
}
}
}
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
$value = $this->getKolabDataItem($event, $name);
switch ($name) {
case 'changed':
case 'end':
case 'start':
// For all-day events Kolab uses different times
// At least Android doesn't display such event as all-day event
if ($value && is_a($value, 'DateTime')) {
$date = clone $value;
if ($event['allday']) {
// need this for self::date_from_kolab()
$date->_dateonly = false;
if ($name == 'start') {
$date->setTime(0, 0, 0);
}
else if ($name == 'end') {
$date->setTime(0, 0, 0);
$date->modify('+1 day');
}
}
// set this date for use in recurrence exceptions handling
if ($name == 'start') {
$event['_start'] = $date;
}
$value = self::date_from_kolab($date);
}
break;
case 'sensitivity':
$value = intval($this->sensitivityMap[$value]);
break;
case 'free_busy':
$value = $this->busyStatusMap[$value];
break;
case 'description':
$value = $this->body_from_kolab($value, $collection);
break;
+
+ case 'location':
+ $value = new Syncroton_Model_Location($value);
+ break;
}
// Ignore empty values (but not integer 0)
if ((empty($value) || is_array($value)) && $value !== 0) {
continue;
}
$result[$key] = $value;
}
// Event reminder time
if ($config['ALARMS']) {
$result['reminder'] = $this->from_kolab_alarm($event);
}
$result['categories'] = array();
$result['attendees'] = array();
// Categories, Roundcube Calendar plugin supports only one category at a time
if (!empty($event['categories'])) {
$result['categories'] = (array) $event['categories'];
}
// Organizer
if (!empty($event['attendees'])) {
foreach ($event['attendees'] as $idx => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
if ($name = $attendee['name']) {
$result['organizerName'] = $name;
}
if ($email = $attendee['email']) {
$result['organizerEmail'] = $email;
}
unset($event['attendees'][$idx]);
break;
}
}
}
// Attendees
if (!empty($event['attendees'])) {
$user_emails = $this->user_emails();
$user_rsvp = false;
foreach ($event['attendees'] as $idx => $attendee) {
$att = array();
if ($email = $attendee['email']) {
$att['email'] = $email;
}
else {
// In Activesync email is required
continue;
}
$att['name'] = $attendee['name'] ?: $email;
$type = isset($attendee['role']) ? $this->attendeeTypeMap[$attendee['role']] : null;
$status = isset($attendee['status']) ? $this->attendeeStatusMap[$attendee['status']] : null;
if ($this->asversion >= 12) {
$att['attendeeType'] = $type ?: self::ATTENDEE_TYPE_REQUIRED;
$att['attendeeStatus'] = $status ?: self::ATTENDEE_STATUS_UNKNOWN;
}
if ($email && in_array_nocase($email, $user_emails)) {
$user_rsvp = !empty($attendee['rsvp']);
$resp_type = $status ?: self::ATTENDEE_STATUS_UNKNOWN;
}
$result['attendees'][] = new Syncroton_Model_EventAttendee($att);
}
}
// Event meeting status
$this->meeting_status_from_kolab($collection, $event, $result);
// Recurrence (and exceptions)
$this->recurrence_from_kolab($collection, $event, $result);
// RSVP status
$result['responseRequested'] = $result['meetingStatus'] == 3 && $user_rsvp ? 1 : 0;
$result['responseType'] = $result['meetingStatus'] == 3 ? $resp_type : null;
return $as_array ? $result : new Syncroton_Model_Event($result);
}
/**
* convert contact from xml to libkolab array
*
* @param Syncroton_Model_IEntry $data Contact to convert
* @param string $folderid Folder identifier
* @param array $entry Existing entry
* @param DateTimeZone $timezone Timezone of the event
*
* @return array
*/
public function toKolab(Syncroton_Model_IEntry $data, $folderid, $entry = null, $timezone = null)
{
$event = !empty($entry) ? $entry : array();
$foldername = isset($event['_mailbox']) ? $event['_mailbox'] : $this->getFolderName($folderid);
$config = $this->getFolderConfig($foldername);
$is_exception = $data instanceof Syncroton_Model_EventException;
$dummy_tz = str_repeat('A', 230) . '==';
$is_outlook = stripos($this->device->devicetype, 'outlook') !== false;
+ $v16_update = $this->asversion >= 16 && !empty($event);
// check data validity
$this->check_event($data);
if (!empty($event['start']) && ($event['start'] instanceof DateTime)) {
$old_timezone = $event['start']->getTimezone();
}
// Timezone
if (!$timezone && isset($data->timezone) && $data->timezone != $dummy_tz) {
$tzc = kolab_sync_timezone_converter::getInstance();
$expected = $old_timezone ?: kolab_format::$timezone;
try {
$timezone = $tzc->getTimezone($data->timezone, $expected->getName());
$timezone = new DateTimeZone($timezone);
}
catch (Exception $e) {
$timezone = null;
}
}
if (empty($timezone)) {
$timezone = $old_timezone ?: new DateTimeZone('UTC');
}
- $event['allday'] = 0;
+ $event['allday'] = $v16_update && !isset($data->allDayEvent) ? $event['allday'] : 0;
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
// skip UID field, unsupported in event exceptions
// we need to do this here, because the next line (data getter) will throw an exception
if ($is_exception && $key == 'uID') {
continue;
}
+ // In ActiveSync >= v16 all properties are ghosted
+ if ($v16_update && !isset($data->$key)) {
+ continue;
+ }
+
$value = $data->$key;
switch ($name) {
case 'changed':
$value = null;
break;
case 'end':
case 'start':
if ($timezone && $value) {
$value->setTimezone($timezone);
}
if ($value && $data->allDayEvent) {
$value->_dateonly = true;
// In ActiveSync all-day event ends on 00:00:00 next day
// In Kolab we just ignore the time spec.
if ($name == 'end') {
$diff = date_diff($event['start'], $value);
$value = clone $event['start'];
if ($diff->days > 1) {
$value->add(new DateInterval('P' . ($diff->days - 1) . 'D'));
}
}
}
break;
case 'sensitivity':
$map = array_flip($this->sensitivityMap);
$value = $map[$value];
break;
case 'free_busy':
$map = array_flip($this->busyStatusMap);
$value = $map[$value];
break;
case 'description':
$value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT);
// If description isn't specified keep old description
if ($value === null) {
continue 2;
}
break;
+
+ case 'location':
+ $value = (string) $value;
+ break;
}
$this->setKolabDataItem($event, $name, $value);
}
// Try to fix allday events from Android
// It doesn't set all-day flag but the period is a whole day
if (!$event['allday'] && $event['end'] && $event['start']) {
$interval = @date_diff($event['start'], $event['end']);
if ($interval && $interval->format('%y%m%d%h%i%s') === '001000') {
$event['allday'] = 1;
$event['end'] = clone $event['start'];
}
}
// Reminder
// @TODO: should alarms be used when importing event from phone?
- if ($config['ALARMS']) {
+ if ($config['ALARMS'] && (!$v16_update || isset($data->reminder))) {
$event['valarms'] = $this->to_kolab_alarm($data->reminder, $event);
}
$attendees = array();
$categories = array();
// Categories
if (isset($data->categories)) {
foreach ($data->categories as $category) {
$categories[] = $category;
}
}
+ if (!$v16_update || isset($data->categories)) {
+ $event['categories'] = $categories;
+ }
+
// Organizer
if (!$is_exception && ($organizer_email = $data->organizerEmail)) {
$attendees[] = array(
'role' => 'ORGANIZER',
'name' => $data->organizerName,
'email' => $organizer_email,
);
}
// Attendees
// Outlook 2013 sends a dummy update just after MeetingResponse has been processed,
// this update resets attendee status set in the MeetingResponse request.
// We ignore changes to attendees data on such updates
if ($is_outlook && $this->isDummyOutlookUpdate($data, $entry, $event)) {
$attendees = $entry['attendees'];
}
else if (isset($data->attendees)) {
$statusMap = array_flip($this->attendeeStatusMap);
foreach ($data->attendees as $attendee) {
if ($attendee->email && $attendee->email == $organizer_email) {
continue;
}
$role = false;
if (isset($attendee->attendeeType)) {
$role = array_search($attendee->attendeeType, $this->attendeeTypeMap);
}
if ($role === false) {
$role = array_search(self::ATTENDEE_TYPE_REQUIRED, $this->attendeeTypeMap);
}
$_attendee = array(
'role' => $role,
'name' => $attendee->name != $attendee->email ? $attendee->name : '',
'email' => $attendee->email,
);
if (isset($attendee->attendeeStatus)) {
$_attendee['status'] = $attendee->attendeeStatus ? array_search($attendee->attendeeStatus, $this->attendeeStatusMap) : null;
if (!$_attendee['status']) {
$_attendee['status'] = 'NEEDS-ACTION';
$_attendee['rsvp'] = true;
}
}
else if (!empty($event['attendees']) && !empty($attendee->email)) {
// copy the old attendee status
foreach ($event['attendees'] as $old_attendee) {
if ($old_attendee['email'] == $_attendee['email'] && isset($old_attendee['status'])) {
$_attendee['status'] = $old_attendee['status'];
$_attendee['rsvp'] = $old_attendee['rsvp'];
break;
}
}
}
$attendees[] = $_attendee;
}
}
// Make sure the event has the organizer set
if (!$organizer_email && ($identity = kolab_sync::get_instance()->user->get_identity())) {
$attendees[] = array(
'role' => 'ORGANIZER',
'name' => $identity['name'],
'email' => $identity['email'],
);
}
- $event['attendees'] = $attendees;
- $event['categories'] = $categories;
+ // Note: In ActiveSync >= v16 all properties are ghosted
+ if (!$v16_update || isset($data->attendees) || isset($data->organizerEmail)) {
+ $event['attendees'] = $attendees;
+ }
// recurrence (and exceptions)
if (!$is_exception) {
$event['recurrence'] = $this->recurrence_to_kolab($data, $folderid, $timezone);
}
// Bump SEQUENCE number on update (Outlook only).
// It's been confirmed that any change of the event that has attendees specified
// bumps SEQUENCE number of the event (we can see this in sent iTips).
// Unfortunately Outlook also sends an update when no SEQUENCE bump
// is needed, e.g. when updating attendee status.
// We try our best to bump the SEQUENCE only when expected
if (!empty($entry) && !$is_exception && !empty($data->attendees) && $data->timezone != $dummy_tz) {
if ($last_update = $this->getKolabDataItem($event, self::KEY_DTSTAMP)) {
$last_update = new DateTime($last_update);
}
if ($data->dtStamp && $data->dtStamp != $last_update) {
if ($this->has_significant_changes($event, $entry)) {
$event['sequence']++;
$this->logger->debug('Found significant changes in the updated event. Bumping SEQUENCE to ' . $event['sequence']);
}
}
}
// Because we use last event modification time above, we make sure
// the event modification time is not (re)set by the server,
// we use the original Outlook's timestamp.
if ($is_outlook && $data->dtStamp) {
$this->setKolabDataItem($event, self::KEY_DTSTAMP, $data->dtStamp->format(DateTime::ATOM));
}
// This prevents kolab_format code to bump the sequence when not needed
if (!isset($event['sequence'])) {
$event['sequence'] = 0;
}
return $event;
}
/**
* Set attendee status for meeting
*
* @param Syncroton_Model_MeetingResponse $request The meeting response
*
* @return string ID of new calendar entry
*/
public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request)
{
$status_map = array(
1 => 'ACCEPTED',
2 => 'TENTATIVE',
3 => 'DECLINED',
);
if ($status = $status_map[$request->userResponse]) {
// extract event from the invitation
list($event, $existing) = $this->get_event_from_invitation($request);
/*
switch ($status) {
case 'ACCEPTED': $event['free_busy'] = 'busy'; break;
case 'TENTATIVE': $event['free_busy'] = 'tentative'; break;
case 'DECLINED': $event['free_busy'] = 'free'; break;
}
*/
// Store Outlook response timestamp for further use
if (stripos($this->device->devicetype, 'outlook') !== false) {
$dtstamp = new DateTime('now', new DateTimeZone('UTC'));
$dtstamp = $dtstamp->format(DateTime::ATOM);
}
// Update/Save the event
if (empty($existing)) {
if ($dtstamp) {
$this->setKolabDataItem($event, self::KEY_RESPONSE_DTSTAMP, $dtstamp);
}
$folder = $this->save_event($event, $status);
// Create SyncState for the new event, so it is not synced twice
if ($folder) {
$folderId = $this->getFolderId($folder);
try {
$syncBackend = Syncroton_Registry::getSyncStateBackend();
$folderBackend = Syncroton_Registry::getFolderBackend();
$contentBackend = Syncroton_Registry::getContentStateBackend();
$syncFolder = $folderBackend->getFolder($this->device->id, $folderId);
$syncState = $syncBackend->getSyncState($this->device->id, $syncFolder->id);
$contentBackend->create(new Syncroton_Model_Content(array(
'device_id' => $this->device->id,
'folder_id' => $syncFolder->id,
'contentid' => $this->serverId($event['uid'], $folder),
'creation_time' => $syncState->lastsync,
'creation_synckey' => $syncState->counter,
)));
}
catch (Exception $e) {
// ignore
}
}
}
else {
if ($dtstamp) {
$this->setKolabDataItem($existing, self::KEY_RESPONSE_DTSTAMP, $dtstamp);
}
$folder = $this->update_event($event, $existing, $status, $request->instanceId);
}
if (!$folder) {
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR);
}
// TODO: ActiveSync version >= 16, send the iTip response.
if (isset($request->sendResponse)) {
// SendResponse can contain Body to use as email body (can be empty)
// TODO: Activesync >= 16.1 proposedStartTime and proposedEndTime.
}
}
// FIXME: We should not return an UID when status=DECLINED
// as it's expected by the specification. Server
// should delete an event in such a case, but we
// keep the event copy with appropriate attendee status instead.
return empty($status) ? null : $this->serverId($event['uid'], $folder);
}
/**
* Get an event from the invitation email or calendar folder
*/
protected function get_event_from_invitation(Syncroton_Model_MeetingResponse $request)
{
// Limitation: LongId might be used instead of RequestId, this is not supported
if ($request->requestId) {
$mail_class = new kolab_sync_data_email($this->device, $this->syncTimeStamp);
// Event from an invitation email
if ($event = $mail_class->get_invitation_event($request->requestId)) {
// find the event in calendar
$existing = $this->find_event_by_uid($event['uid']);
return array($event, $existing);
}
// Event from calendar folder
if ($event = $this->getObject($request->collectionId, $request->requestId, $folder)) {
return array($event, $event);
}
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
}
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR);
}
/**
* Find the Kolab event in any (of subscribed personal calendars) folder
*/
protected function find_event_by_uid($uid)
{
if (empty($uid)) {
return;
}
// TODO: should we check every existing event folder even if not subscribed for sync?
foreach ($this->listFolders() as $folder) {
$storage_folder = $this->getFolderObject($folder['imap_name']);
if ($storage_folder->get_namespace() == 'personal'
&& ($result = $storage_folder->get_object($uid))
) {
return $result;
}
}
}
/**
* Wrapper to update an event object
*/
protected function update_event($event, $old, $status, $instanceId = null)
{
// TODO: instanceId - DateTime - of the exception to be processed, if not set process all occurrences
if ($instanceId) {
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
}
if ($event['free_busy']) {
$old['free_busy'] = $event['free_busy'];
}
// Updating an existing event is most-likely a response
// to an iTip request with bumped SEQUENCE
$old['sequence'] += 1;
// Update the event
return $this->save_event($old, $status);
}
/**
* Save the Kolab event (create if not exist)
* If an event does not exist it will be created in the default folder
*/
protected function save_event(&$event, $status = null)
{
// Find default folder to which we'll save the event
if (!isset($event['_mailbox'])) {
$folders = $this->listFolders();
$storage = rcube::get_instance()->get_storage();
// find the default
foreach ($folders as $folder) {
if ($folder['type'] == 8 && $storage->folder_namespace($folder['imap_name']) == 'personal') {
$event['_mailbox'] = $folder['imap_name'];
break;
}
}
// if there's no folder marked as default, use any
if (!isset($event['_mailbox']) && !empty($folders)) {
foreach ($folders as $folder) {
if ($storage->folder_namespace($folder['imap_name']) == 'personal') {
$event['_mailbox'] = $folder['imap_name'];
break;
}
}
}
// TODO: what if the user has no subscribed event folders for this device
// should we use any existing event folder even if not subscribed for sync?
}
if ($status) {
$this->update_attendee_status($event, $status);
}
// TODO: Free/busy trigger?
if (isset($event['_mailbox'])) {
$folder = $this->getFolderObject($event['_mailbox']);
if ($folder && $folder->valid && $folder->save($event)) {
return $folder;
}
}
return false;
}
/**
* Update the attendee status of the user
*/
protected function update_attendee_status(&$event, $status)
{
$organizer = null;
$emails = $this->user_emails();
foreach ((array) $event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array_nocase($attendee['email'], $emails)) {
$event['attendees'][$i]['status'] = $status;
$event['attendees'][$i]['rsvp'] = false;
$event_attendee = $attendee;
}
}
if (!$event_attendee) {
$this->logger->warn('MeetingResponse on an event where the user is not an attendee. UID: ' . $event['uid']);
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR);
}
}
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
$filter = array(array('type', '=', $this->modelName));
switch ($filter_type) {
case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
$mod = '-2 weeks';
break;
case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
$mod = '-1 month';
break;
case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK:
$mod = '-3 months';
break;
case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK:
$mod = '-6 months';
break;
}
if (!empty($mod)) {
$dt = new DateTime('now', new DateTimeZone('UTC'));
$dt->modify($mod);
$filter[] = array('dtend', '>', $dt);
}
return $filter;
}
/**
* Set MeetingStatus according to event data
*/
protected function meeting_status_from_kolab($collection, $event, &$result)
{
// 0 - The event is an appointment, which has no attendees.
// 1 - The event is a meeting and the user is the meeting organizer.
// 3 - This event is a meeting, and the user is not the meeting organizer.
// 5 - The meeting has been canceled and the user was the meeting organizer.
// 7 - The meeting has been canceled. The user was not the meeting organizer.
$status = 0;
if (!empty($event['attendees'])) {
// Find out if the user is an organizer
// TODO: Delegation/aliases support
$user_emails = $this->user_emails();
$is_organizer = false;
if ($event['organizer'] && $event['organizer']['email']) {
$is_organizer = in_array_nocase($event['organizer']['email'], $user_emails);
}
if ($event['status'] == 'CANCELLED') {
$status = $is_organizer ? 5 : 7;
}
else {
$status = $is_organizer ? 1 : 3;
}
}
$result['meetingStatus'] = $status;
}
/**
* Converts libkolab alarms spec. into a number of minutes
*/
protected function from_kolab_alarm($event)
{
if (isset($event['valarms'])) {
foreach ($event['valarms'] as $alarm) {
if (in_array($alarm['action'], array('DISPLAY', 'AUDIO'))) {
$value = $alarm['trigger'];
break;
}
}
}
if ($value && $value instanceof DateTime) {
if ($event['start'] && ($interval = $event['start']->diff($value))) {
if ($interval->invert && !$interval->m && !$interval->y) {
return intval(round($interval->s/60) + $interval->i + $interval->h * 60 + $interval->d * 60 * 24);
}
}
}
else if ($value && preg_match('/^([-+]*)[PT]*([0-9]+)([WDHMS])$/', $value, $matches)) {
$value = intval($matches[2]);
if ($value && $matches[1] != '-') {
return null;
}
switch ($matches[3]) {
case 'S': $value = intval(round($value/60)); break;
case 'H': $value *= 60; break;
case 'D': $value *= 24 * 60; break;
case 'W': $value *= 7 * 24 * 60; break;
}
return $value;
}
}
/**
* Converts ActiveSync reminder into libkolab alarms spec.
*/
protected function to_kolab_alarm($value, $event)
{
if ($value === null || $value === '') {
return (array) $event['valarms'];
}
$valarms = array();
$unsupported = array();
if (!empty($event['valarms'])) {
foreach ($event['valarms'] as $alarm) {
if (!$current && in_array($alarm['action'], array('DISPLAY', 'AUDIO'))) {
$current = $alarm;
}
else {
$unsupported[] = $alarm;
}
}
}
$valarms[] = array(
'action' => $current['action'] ?: 'DISPLAY',
'description' => $current['description'] ?: '',
'trigger' => sprintf('-PT%dM', $value),
);
if (!empty($unsupported)) {
$valarms = array_merge($valarms, $unsupported);
}
return $valarms;
}
/**
* Sanity checks on event input
*
* @param Syncroton_Model_IEntry &$entry Entry object
*
* @throws Syncroton_Exception_Status_Sync
*/
protected function check_event(Syncroton_Model_IEntry &$entry)
{
// https://msdn.microsoft.com/en-us/library/jj194434(v=exchg.80).aspx
$now = new DateTime('now');
$rounded = new DateTime('now');
$min = (int) $rounded->format('i');
$add = $min > 30 ? (60 - $min) : (30 - $min);
$rounded->add(new DateInterval('PT' . $add . 'M'));
if (empty($entry->startTime) && empty($entry->endTime)) {
// use current time rounded to 30 minutes
$end = clone $rounded;
$end->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M'));
$entry->startTime = $rounded;
$entry->endTime = $end;
}
else if (empty($entry->startTime)) {
if ($entry->endTime < $now || $entry->endTime < $rounded) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM);
}
$entry->startTime = $rounded;
}
else if (empty($entry->endTime)) {
if ($entry->startTime < $now) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM);
}
$rounded->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M'));
$entry->endTime = $rounded;
}
}
/**
* Check if the new event version has any significant changes
*/
protected function has_significant_changes($event, $old)
{
// Calendar namespace fields
foreach (array('allday', 'start', 'end', 'location', 'recurrence') as $key) {
if ($event[$key] != $old[$key]) {
// Comparing recurrence is tricky as there can be differences in default
// value handling. Let's try to handle most common cases
if ($key == 'recurrence' && $this->fixed_recurrence($event) == $this->fixed_recurrence($old)) {
continue;
}
return true;
}
}
if (count($event['attendees']) != count($old['attendees'])) {
return true;
}
foreach ($event['attendees'] as $idx => $attendee) {
$old_attendee = $old['attendees'][$idx];
if ($old_attendee['email'] != $attendee['email']
|| ($attendee['role'] != 'ORGANIZER'
&& $attendee['status'] != $old_attendee['status']
&& $attendee['status'] == 'NEEDS-ACTION')
) {
return true;
}
}
return false;
}
/**
* Unify recurrence spec. for comparison
*/
protected function fixed_recurrence($event)
{
$rec = (array) $event['recurrence'];
// Add BYDAY if not exists
if ($rec['FREQ'] == 'WEEKLY' && empty($rec['BYDAY'])) {
$days = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$day = $event['start']->format('w');
$rec['BYDAY'] = $days[$day];
}
if (!$rec['INTERVAL']) {
$rec['INTERVAL'] = 1;
}
ksort($rec);
return $rec;
}
/**
* Check if the event update request is a fake (for Outlook)
*/
protected function isDummyOutlookUpdate($data, $entry, &$result)
{
$is_dummy = false;
// Outlook 2013 sends a dummy update just after MeetingResponse has been processed,
// this update resets attendee status set in the MeetingResponse request.
// We ignore attendees data in such updates, they should not happen according to
// https://msdn.microsoft.com/en-us/library/office/hh428685(v=exchg.140).aspx
// but they will contain some data as alarms and free/busy status so we don't
// ignore them completely
if (!empty($entry) && !empty($data->attendees) && stripos($this->device->devicetype, 'outlook') !== false) {
// Some of these requests use just dummy Timezone
$dummy_tz = str_repeat('A', 230) . '==';
if ($data->timezone == $dummy_tz) {
$is_dummy = true;
}
// But some of them do not, so we have check if that is a first
// update immediately (up to 5 seconds) after MeetingResponse request
if (!$is_dummy && ($dtstamp = $this->getKolabDataItem($entry, self::KEY_RESPONSE_DTSTAMP))) {
$dtstamp = new DateTime($dtstamp);
$now = new DateTime('now', new DateTimeZone('UTC'));
$is_dummy = $now->getTimestamp() - $dtstamp->getTimestamp() <= 5;
}
$this->unsetKolabDataItem($result, self::KEY_RESPONSE_DTSTAMP);
}
return $is_dummy;
}
}