From 862156f17f94ff1192faecaea0b9ab5c42a4483d Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Sun, 28 Aug 2016 21:06:11 -0400 Subject: [PATCH 01/15] ws/wrap/etc... --- .../lib/Horde/ActiveSync/Connector/Importer.php | 3 ++- .../lib/Horde/ActiveSync/Message/Base.php | 3 ++- .../lib/Horde/ActiveSync/Message/Mail.php | 2 +- .../lib/Horde/ActiveSync/Request/Sync.php | 2 +- .../Core/lib/Horde/Core/ActiveSync/Driver.php | 14 +++++++++++--- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php index b3ef61dfd8e..e27beeabcde 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php @@ -152,7 +152,8 @@ public function importMessageChange( { // Don't support SMS, but can't tell client that. Send back a phoney // UID for any imported SMS objects. - if ($class == Horde_ActiveSync::CLASS_SMS || strpos($id, 'IGNORESMS_') === 0) { + if ($class == Horde_ActiveSync::CLASS_SMS || + strpos($id, 'IGNORESMS_') === 0) { return 'IGNORESMS_' . $clientid; } diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php index 04ba8e0b4c2..42a92b9ef16 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php @@ -375,7 +375,8 @@ public function decodeStream(Horde_ActiveSync_Wbxml_Decoder &$decoder) // Handle arrays of attribute values while (1) { // Do not get start tag for an array without a container - if (!(isset($map[self::KEY_PROPERTY]) && $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER) && + if (!(isset($map[self::KEY_PROPERTY]) && + $map[self::KEY_PROPERTY] == self::PROPERTY_NO_CONTAINER) && !$decoder->getElementStartTag($map[self::KEY_VALUES])) { break; } diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Mail.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Mail.php index 81931eff1f2..c93266c5ef2 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Mail.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Mail.php @@ -255,7 +255,7 @@ public function __construct(array $options = array()) $this->_mapping += array( Horde_ActiveSync::AIRSYNCBASE_NATIVEBODYTYPE => array(self::KEY_ATTRIBUTE => 'airsyncbasenativebodytype'), Horde_ActiveSync::AIRSYNCBASE_BODY => array(self::KEY_ATTRIBUTE => 'airsyncbasebody', self::KEY_TYPE=> 'Horde_ActiveSync_Message_AirSyncBaseBody'), - Horde_ActiveSync::AIRSYNCBASE_ATTACHMENTS => array(self::KEY_ATTRIBUTE => 'airsyncbaseattachments', self::KEY_TYPE => 'Horde_ActiveSync_Message_AirSyncBaseAttachment', self::KEY_VALUES => Horde_ActiveSync::AIRSYNCBASE_ATTACHMENT ), + Horde_ActiveSync::AIRSYNCBASE_ATTACHMENTS => array(self::KEY_ATTRIBUTE => 'airsyncbaseattachments', self::KEY_TYPE => 'Horde_ActiveSync_Message_AirSyncBaseAttachment', self::KEY_VALUES => Horde_ActiveSync::AIRSYNCBASE_ATTACHMENT), self::POOMMAIL_FLAG => array(self::KEY_ATTRIBUTE => 'flag', self::KEY_TYPE => 'Horde_ActiveSync_Message_Flag'), self::POOMMAIL_CONTENTCLASS => array(self::KEY_ATTRIBUTE => 'contentclass'), ); diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php index d6ce5ae303e..3d64a1fc464 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php @@ -1048,9 +1048,9 @@ protected function _parseSyncCommands(&$collection) case Horde_ActiveSync::CLASS_CALENDAR: $appdata = Horde_ActiveSync::messageFactory('Appointment'); $appdata->decodeStream($this->_decoder); + // EAS 16.0 sends instanceid/serverid for exceptions. if (!empty($instanceid) && $commandType == Horde_ActiveSync::SYNC_MODIFY) { - // EAS 16.0 sends instanceid/serverid for exceptions. $appdata->instanceid = $instanceid; } break; diff --git a/framework/Core/lib/Horde/Core/ActiveSync/Driver.php b/framework/Core/lib/Horde/Core/ActiveSync/Driver.php index 7d4fa3d79dd..96fe843d85a 100644 --- a/framework/Core/lib/Horde/Core/ActiveSync/Driver.php +++ b/framework/Core/lib/Horde/Core/ActiveSync/Driver.php @@ -1967,7 +1967,10 @@ public function changeMessage($folderid, $id, Horde_ActiveSync_Message_Base $mes if ($message->read !== '') { $this->setReadFlag($folderid, $id, $message->read); - $stat['flags'] = array_merge($stat['flags'], array('read' => $message->read)); + $stat['flags'] = array_merge( + $stat['flags'], + array('read' => $message->read) + ); // Do RFC 3798 MDN checks. If $message->read is being set to // FLAG_READ_SEEN, then we *might* be able to send one. @@ -1976,7 +1979,9 @@ public function changeMessage($folderid, $id, Horde_ActiveSync_Message_Base $mes "[%s] Checking for MDN", $this->_pid) ); - $mdn = new Horde_Core_ActiveSync_Mdn($folderid, $id, $this->_imap, $this->_connector); + $mdn = new Horde_Core_ActiveSync_Mdn( + $folderid, $id, $this->_imap, $this->_connector + ); if ($mdn->mdnCheck()) { $this->_logger->info(sprintf( "[%s] Sending MDN", @@ -1991,7 +1996,10 @@ public function changeMessage($folderid, $id, Horde_ActiveSync_Message_Base $mes $message->flag = Horde_ActiveSync::messageFactory('Flag'); } $this->_imap->setMessageFlag($folderid, $id, $message->flag); - $stat['flags'] = array_merge($stat['flags'], array('flagged' => $message->flag->flagstatus)); + $stat['flags'] = array_merge( + $stat['flags'], + array('flagged' => $message->flag->flagstatus) + ); } if ($message->propertyExists('categories')) { // We *try* to make sure the category is added as a custom From 2a8a8f75f11de7bb47cd1512943c6307c96a6f15 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 11:54:25 -0400 Subject: [PATCH 02/15] Allow returning a hash from import(). Needed since attachment addition/deletions are handled by the message objects in activesync, instead of by a folder command as the rest of addition/deletions are. Clients need to be able to map their clientId with the returned server UID. Will also allow us to return the syncstamp when the rest of the API is refactored to allow this. --- kronolith/lib/Api.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/kronolith/lib/Api.php b/kronolith/lib/Api.php index 1cc6e8e0bef..e1c455f9607 100644 --- a/kronolith/lib/Api.php +++ b/kronolith/lib/Api.php @@ -784,11 +784,14 @@ public function getHighestModSeq($id = null) * activesync * * @param string $calendar What calendar should the event be added to? + * @param boolean $hash If true, return a hash for EAS additions. + * @since 4.3.0 @todo Remove for 5.0 and make + * this the normal return. * * @return array The event's UID. * @throws Kronolith_Exception */ - public function import($content, $contentType, $calendar = null) + public function import($content, $contentType, $calendar = null, $hash = false) { if (!isset($calendar)) { $calendar = Kronolith::getDefaultCalendar(Horde_Perms::EDIT); @@ -818,7 +821,21 @@ public function import($content, $contentType, $calendar = null) $event = $kronolith_driver->getEvent(); $event->fromASAppointment($content); $event->save(); - return $event->uid; + // Handle attachment data after we commit changes since we + // are required to have a saved event to attach files. Also, + // we can only handle files if we are returning a hash since EAS + // needs the information returned to attach filereferences to + // the attachments. + if (!$hash) { + return $event->uid; + } + $atc_hash = $event->addEASFiles($content); + return array( + 'uid' => $event->uid, + 'atchash' => $atc_hash, + // See Bug #12567 + //'syncstamp' => $stamp + ); } throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType)); From dcc66c80a8f13fe02b36613c0ee083267e1805b4 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 11:59:26 -0400 Subject: [PATCH 03/15] Return event attachments via the API. --- kronolith/lib/Api.php | 47 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/kronolith/lib/Api.php b/kronolith/lib/Api.php index e1c455f9607..a1dd2e1ebda 100644 --- a/kronolith/lib/Api.php +++ b/kronolith/lib/Api.php @@ -1010,6 +1010,43 @@ public function exportCalendar($calendar, $contentType) ); } + /** + * Return an event attachment. + * + * @param string $calendar The calendar ID. + * @param string $uid The UID of the event the file is attached to. + * @param string $filename The name of the file. + * + * @return array An array containing the following keys: + * data (stream): A file pointer to the attachment data. + * content-type (string): The mime-type of the contents. + * + * @throws Kronolith_Exception + * @since 4.3.0 + */ + public function getAttachment($calendar, $uid, $filename) + { + $event = $this->eventFromUID($uid, $calendar); + // Use localfile so we can use a stream. + try { + $local_file = $event->vfsInit()->readFile( + Kronolith::VFS_PATH . '/' . $event->getVfsUid(), + $filename + ); + if (!$fp = @fopen($local_file, 'rb')) { + throw new Kronolith_Exception('Unable to open attachment.'); + } + } catch (Horde_Vfs_Exception $e) { + throw new Kronolith_Exception($e); + } + + // Try to determine type. + return array( + 'data' => $fp, + 'content-type' => Horde_Mime_Magic::filenameToMime($filename, false) + ); + } + /** * Deletes an event identified by UID. * @@ -1251,14 +1288,18 @@ public function lookupFreeBusy($email, $json = false) /** * Retrieves a Kronolith_Event object, given an event UID. * - * @param string $uid The event's UID. + * @param string $uid The event's UID. + * @param string $claendar The calendar id to restrict to. @since 4.3.0 * * @return Kronolith_Event A valid Kronolith_Event. * @throws Kronolith_Exception */ - public function eventFromUID($uid) + public function eventFromUID($uid, $calendar = null) { - $event = Kronolith::getDriver()->getByUID($uid); + if (!empty($calendar)) { + $calendar = array($calendar); + } + $event = Kronolith::getDriver()->getByUID($uid, $calendar); if (!$event->hasPermission(Horde_Perms::SHOW)) { throw new Horde_Exception_PermissionDenied(); } From eaceecd23ca45f3fdeb273100fa6e6656d552810 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 12:00:00 -0400 Subject: [PATCH 04/15] Must return the atchash from replace operations as well. --- kronolith/lib/Api.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kronolith/lib/Api.php b/kronolith/lib/Api.php index a1dd2e1ebda..9d1d8a06347 100644 --- a/kronolith/lib/Api.php +++ b/kronolith/lib/Api.php @@ -1187,6 +1187,8 @@ public function move($uid, $source, $target) * @param string $calendar Ensure the event is replaced in the specified * calendar. @since 4.2.0 * + * @return mixed For EAS operations, an array of 'uid' and 'atchash' + * are returned. @since 4.3.0 * @throws Kronolith_Exception */ public function replace($uid, $content, $contentType, $calendar = null) @@ -1202,8 +1204,15 @@ public function replace($uid, $content, $contentType, $calendar = null) $component = $content; } elseif ($content instanceof Horde_ActiveSync_Message_Appointment) { $event->fromASAppointment($content); + $atc_hash = $event->addEASFiles($content); $event->save(); $event->uid = $uid; + return array( + 'uid' => $event->uid, + 'atchash' => $atc_hash, + // See Bug #12567 + //'syncstamp' => $stamp + ); return; } else { switch ($contentType) { From 5d0a1b2b3a074ab1bdb29c999ac3da54d77a34ca Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 12:02:56 -0400 Subject: [PATCH 05/15] Add calendar_import16 for importing EAS 16 calendar message objects. Need it's own method for BC. Roll into calendar_import for H6. --- .../lib/Horde/Core/ActiveSync/Connector.php | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/framework/Core/lib/Horde/Core/ActiveSync/Connector.php b/framework/Core/lib/Horde/Core/ActiveSync/Connector.php index e58785b17f8..e4bcad90018 100644 --- a/framework/Core/lib/Horde/Core/ActiveSync/Connector.php +++ b/framework/Core/lib/Horde/Core/ActiveSync/Connector.php @@ -136,7 +136,36 @@ public function calendar_export($uid, array $options = array(), $calendar = null public function calendar_import( Horde_ActiveSync_Message_Appointment $content, $calendar = null) { - return $this->_registry->calendar->import($content, 'activesync', $calendar); + return $this->_registry->calendar->import( + $content, 'activesync', $calendar); + } + + /** + * Version of calendar_import capable of returning an array of values. + * Needed for EAS 16 support in order to deal with the fact that + * attachment actions are handled within the Message object. + * + * @param Horde_ActiveSync_Message_Appointment $content The event content + * @param string $calendar The calendar id. + * + * @return array + * @since 2.27.0 + * @todo Remove for H6 and make calendar_import return this structure. + */ + public function calendar_import16( + Horde_ActiveSync_Message_Appointment $content, $calendar = null) + { + $result = $this->_registry->calendar->import( + $content, 'activesync', $calendar, true); + + if (!is_array($result)) { + $result = array( + 'uid' => $result, + 'atchash' => false + ); + } + + return $result; } /** From 393fc4e50d66cf533f419bf724baa770fd498c7d Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 12:04:53 -0400 Subject: [PATCH 06/15] Have calendar_replace return the uid/atchash data. --- framework/Core/lib/Horde/Core/ActiveSync/Connector.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/Core/lib/Horde/Core/ActiveSync/Connector.php b/framework/Core/lib/Horde/Core/ActiveSync/Connector.php index e4bcad90018..fdb6b97adcb 100644 --- a/framework/Core/lib/Horde/Core/ActiveSync/Connector.php +++ b/framework/Core/lib/Horde/Core/ActiveSync/Connector.php @@ -230,10 +230,12 @@ public function calendar_import_attendee(Horde_Icalendar_vEvent $vEvent, * @param Horde_ActiveSync_Message_Appointment $content * The new event. * @param string $calendar The calendar id. @since 2.12.0 + * + * @return null|array May return an array of 'uid' and 'atchash' or null. */ public function calendar_replace($uid, Horde_ActiveSync_Message_Appointment $content, $calendar = null) { - $this->_registry->calendar->replace($uid, $content, 'activesync', $calendar); + return $this->_registry->calendar->replace($uid, $content, 'activesync', $calendar); } /** From 195b03b077d5112f578e1f5c979ab5fe1eba3ed2 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 12:06:33 -0400 Subject: [PATCH 07/15] Changes to support synchronizing event attachments. --- .../lib/Horde/Core/ActiveSync/Connector.php | 26 +++++++++++ .../Core/lib/Horde/Core/ActiveSync/Driver.php | 43 ++++++++++++++++--- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/framework/Core/lib/Horde/Core/ActiveSync/Connector.php b/framework/Core/lib/Horde/Core/ActiveSync/Connector.php index fdb6b97adcb..345dccee069 100644 --- a/framework/Core/lib/Horde/Core/ActiveSync/Connector.php +++ b/framework/Core/lib/Horde/Core/ActiveSync/Connector.php @@ -294,6 +294,32 @@ public function calendar_move($uid, $source, $target) return $uid; } + /** + * Return an event attachment. + * + * @param string $filereference A filereference pointing to the file: + * calendar:{calendar_id}:{event_uid}:{filename} + * + * @return array An array containing: + * 'content-type' and 'data'. + */ + public function calendar_getAttachment($filereference) + { + if (!$this->_registry->hasMethod( + 'getAttachment', + $this->_registry->hasInterface('calendar'))) { + return false; + } + $fileinfo = explode(':', $filereference, 4); + try { + return $this->_registry->calendar->getAttachment( + $fileinfo[1], $fileinfo[2], $fileinfo[3] + ); + } catch (Horde_Exception $e) { + return false; + } + } + /** * Get a list of all contacts a user can see * diff --git a/framework/Core/lib/Horde/Core/ActiveSync/Driver.php b/framework/Core/lib/Horde/Core/ActiveSync/Driver.php index 96fe843d85a..ffcd823d145 100644 --- a/framework/Core/lib/Horde/Core/ActiveSync/Driver.php +++ b/framework/Core/lib/Horde/Core/ActiveSync/Driver.php @@ -1559,7 +1559,22 @@ public function itemOperationsFetchMailbox( */ public function itemOperationsGetAttachmentData($filereference) { - $att = $this->getAttachment($filereference); + // @todo Slight issue here. Since the filereferences that had previously + // been returned to the client for email attachments only contain + // the mailbox name/uid/part of the message, leaving things as-is would + // mean that a mailbox named 'calendar' would break this code. + // To deal with that cleanly, we really need to prepend 'mail' + // to all email attachments, but this would require a resync of all + // clients to be sure the new filereferences are sent. For now, do + // some sniffing to try to figure it out. + // + // Calendar would have 4 parts, so check that first. + $name_parts = explode(':', $filereference, 4); + if ($name_parts[0] == 'calendar' && !empty($name_parts[3])) { + $att = $atc = $this->_connector->calendar_getAttachment($filereference); + } else { + $att = $this->getAttachment($filereference); + } $airatt = Horde_ActiveSync::messageFactory('AirSyncBaseFileAttachment'); $airatt->data = $att['data']; $airatt->contenttype = $att['content-type']; @@ -1816,8 +1831,12 @@ public function moveMessage($folderid, array $ids, $newfolderid) * Contains the following keys: * - id: (mixed) The UID of the message/item. * - mod: (mixed) A value to indicate the last modification. - * - flags: (array) an empty array if no flag changes. - * - categories: (array|boolean) false if no changes. + * - flags: (array) An array of flag chagnes, empty array if no changes. + * - categories: (array|boolean) An array of EAS categories for email + * messages that exist as IMAP flags, false if no changes. + * - atchash: (array|boolean) An array of clientid->filereference + * mappings for file attachment changes made to appointment + * or draft email folders. @since 2.27.0 */ public function changeMessage($folderid, $id, Horde_ActiveSync_Message_Base $message, $device) { @@ -1849,25 +1868,37 @@ public function changeMessage($folderid, $id, Horde_ActiveSync_Message_Base $mes case Horde_ActiveSync::CLASS_CALENDAR: if (!$id) { try { - $id = $this->_connector->calendar_import($message, $server_id); + // @todo, remove 'import16' hack for H6 + $results = $this->_connector->calendar_import16($message, $server_id); } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return false; } - $stat = array('mod' => $this->getSyncStamp($folderid), 'id' => $id, 'flags' => 1); + $stat = array( + 'mod' => $this->getSyncStamp($folderid), + 'id' => $results['uid'], + 'flags' => 1, + 'atchash' => $results['atchash'] + ); } else { // ActiveSync messages do NOT contain the serverUID value, put // it in ourselves so we can have it during import/change. $message->setServerUID($id); try { - $this->_connector->calendar_replace($id, $message, $server_id); + $results = $this->_connector->calendar_replace($id, $message, $server_id); } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return false; } $stat = $this->_smartStatMessage($folderid, $id, false); + // @todo Remove this check in H6, when the API always returns. + if (!empty($results)) { + $stat['atchash'] = !empty($results['atchash']) + ? $results['atchash'] + : false; + } } break; From 38f16dfcf771f13c510d915a589ed13d2721fc41 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 12:08:00 -0400 Subject: [PATCH 08/15] Support synchronizing event attachments over EAS --- kronolith/lib/Event.php | 109 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/kronolith/lib/Event.php b/kronolith/lib/Event.php index a66159c1997..250867aebb3 100644 --- a/kronolith/lib/Event.php +++ b/kronolith/lib/Event.php @@ -1926,6 +1926,60 @@ public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message) $this->initialized = true; } + /** + * @todo Do we need to update History here too? + */ + public function addEASFiles($message) + { + $results = array( + 'add' => array(), + 'delete' => array() + ); + // EAS 16.0 + $supported = true; + if ($message->getProtocolVersion() < Horde_ActiveSync::VERSION_SIXTEEN || + !$this->id) { + $not_supported = true; + } + + if ($att = $message->airsyncbaseattachments) { + if ($att->add) { + foreach ($att->add as $add) { + if (!$supported) { + $results['add'][$add->clientid] = false; + continue; + } + $info = $this->_addEASFile($add); + $results['add'][$add->clientid] = $this->_getEASFileReference($info['name']); + } + } + if ($att->delete) { + foreach ($atcs->delete as $del_ob) { + $file_parts = explode(':', $del_ob->filereference, 4); + $this->deleteFile($file_parts[3]); + } + } + } + + return $results; + } + + protected function _getEASFileReference($filename) + { + return sprintf('calendar:%s:%s:%s', $this->calendar, $this->uid, $filename); + } + + protected function _addEASFile(Horde_ActiveSync_Message_AirSyncBaseAdd $add) + { + $info = array( + 'name' => empty($add->displayname) ? 'Untitled' : $add->displayname, + 'data' => $add->content + ); + $this->addFileFromData($info); + + return $info; + } + /** * Export this event as a MS ActiveSync Message * @@ -2278,6 +2332,33 @@ public function toASAppointment(array $options = array()) $message->onlinemeetingexternallink = $this->url; } + // 16.0 + if ($options['protocolversion'] >= Horde_ActiveSync::VERSION_SIXTEEN) { + $files = $this->listFiles(); + if (count($files)) { + $message->airsyncbaseattachments = new Horde_ActiveSync_Message_AirSyncBaseAttachments( + array( + 'logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger'), + 'protocolversion' => $options['protocolversion'] + ) + ); + $message->airsyncbaseattachments->attachment = array(); + foreach ($files as $file) { + $atc = new Horde_ActiveSync_Message_AirSyncBaseAttachment( + array( + 'logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger'), + 'protocolversion' => $options['protocolversion'] + ) + ); + $atc->displayname = $file['name']; + $atc->attname = $this->_getEASFileReference($file['name']); + $atc->attmethod = Horde_ActiveSync_Message_AirSyncBaseAttachment::ATT_TYPE_NORMAL; + $atc->attsize = $file['size']; + $message->airsyncbaseattachments->attachment[] = $atc; + } + } + } + return $message; } @@ -4211,6 +4292,34 @@ public function addFile(array $info) } } + public function addFileFromData($info) + { + if (empty($this->uid)) { + throw new Kronolith_Exception("VFS not supported until object saved"); + } + + $vfs = $this->vfsInit(); + $dir = Kronolith::VFS_PATH . '/' . $this->getVfsUid(); + $file = $info['name']; + while ($vfs->exists($dir, $file)) { + if (preg_match('/(.*)\[(\d+)\](\.[^.]*)?$/', $file, $match)) { + $file = $match[1] . '[' . ++$match[2] . ']' . $match[3]; + } else { + $dot = strrpos($file, '.'); + if ($dot === false) { + $file .= '[1]'; + } else { + $file = substr($file, 0, $dot) . '[1]' . substr($file, $dot); + } + } + } + try { + $vfs->writeData($dir, $file, $info['data'], true); + } catch (Horde_Vfs_Exception $e) { + throw new Kronolith_Exception($e); + } + } + /** * Deletes a file from the VFS backend associated with this event. * From ac5d8c3bf4dfb81cea0cf434ea4b52497d2113c0 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 12:09:27 -0400 Subject: [PATCH 09/15] Support synchronizing event attachments. --- .../ActiveSync/doc/Horde/ActiveSync/TODO | 6 +- .../Horde/ActiveSync/Connector/Importer.php | 15 +-- .../ActiveSync/Message/AirSyncBaseAdd.php | 71 ++++++++++++++ .../Message/AirSyncBaseAttachment.php | 21 ++++ .../Message/AirSyncBaseAttachments.php | 93 ++++++++++++++++++ .../ActiveSync/Message/AirSyncBaseDelete.php | 57 +++++++++++ .../Horde/ActiveSync/Message/Appointment.php | 6 +- .../lib/Horde/ActiveSync/Message/Base.php | 15 ++- .../ActiveSync/Request/ItemOperations.php | 5 + .../lib/Horde/ActiveSync/Request/Sync.php | 97 +++++++++++++++++-- framework/ActiveSync/package.xml | 10 +- 11 files changed, 374 insertions(+), 22 deletions(-) create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAdd.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachments.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseDelete.php diff --git a/framework/ActiveSync/doc/Horde/ActiveSync/TODO b/framework/ActiveSync/doc/Horde/ActiveSync/TODO index 9126d3c8dcb..585264c4f3d 100644 --- a/framework/ActiveSync/doc/Horde/ActiveSync/TODO +++ b/framework/ActiveSync/doc/Horde/ActiveSync/TODO @@ -80,6 +80,9 @@ BC BREAKING (i.e., Horde 6). living in the RPC layer (sending back certain headers, etc...) into this class. +- Split out code for sending response in Sync.php to it's own class, or at the + very least, it's own method. ?? + - Implement a Horde_ActiveSync_Change_Filter class/interface. Used to implement workarounds for broken clients. E.g., filter out the ADD commands sent in response to MOVEITEMS for Outlook clients. Use a similar pattern for other @@ -112,7 +115,8 @@ BC BREAKING (i.e., Horde 6). and transporting the various collection options/bodyprefs around. - Likewise, implement a collection object instead of using an array to define - each collection. + each collection and have it be responsible for providing some of the return + objects/values (See comments in Sync.php). - Implement Horde_ActiveSync_SyncKey. diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php index e27beeabcde..d3e6340d758 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php @@ -142,8 +142,8 @@ public function setLogger($logger) * @todo Revisit passing $class for SMS. Probably pass class in the * const'r. * - * @return string|array|boolean The server message id, an array containing - * the serverid and failure code, or false + * @return array|boolean A stat array, or an array containing the 'error' + * key on error, or false on duplicate addition. */ public function importMessageChange( $id, Horde_ActiveSync_Message_Base $message, @@ -178,7 +178,10 @@ public function importMessageChange( '[%s] Conflict when updating %s, will overwrite client version on next sync.', $this->_procid, $id) ); - return array($id, Horde_ActiveSync_Request_Sync::STATUS_CONFLICT); + return array( + $id, + 'error' => array(Horde_ActiveSync_Request_Sync::STATUS_CONFLICT) + ); } } } elseif (!$id && $uid = $this->_state->isDuplicatePIMAddition($clientid)) { @@ -202,8 +205,8 @@ public function importMessageChange( $this->_procid, $id) ); return $id - ? array($id, Horde_ActiveSync_Request_Sync::STATUS_NOTFOUND) - : array(false, Horde_ActiveSync_Request_Sync::STATUS_SERVERERROR); + ? array(0 => $id, 'error' => Horde_ActiveSync_Request_Sync::STATUS_NOTFOUND) + : array(0 => false, 'error' => Horde_ActiveSync_Request_Sync::STATUS_SERVERERROR); } $stat['serverid'] = $this->_folderId; @@ -221,7 +224,7 @@ public function importMessageChange( $this->_as->driver->getUser(), $clientid); - return $stat['id']; + return $stat; } /** diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAdd.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAdd.php new file mode 100644 index 00000000000..a50912b0c25 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAdd.php @@ -0,0 +1,71 @@ + + * @package ActiveSync + */ +/** + * Horde_ActiveSync_Message_AirSyncBaseAdd:: + * + * @license http://www.horde.org/licenses/gpl GPLv2 + * NOTE: According to sec. 8 of the GENERAL PUBLIC LICENSE (GPL), + * Version 2, the distribution of the Horde_ActiveSync module in or + * to the United States of America is excluded from the scope of this + * license. + * @copyright 2011-2016 Horde LLC (http://www.horde.org) + * @author Michael J Rubinsky + * @package ActiveSync + */ +class Horde_ActiveSync_Message_AirSyncBaseAdd extends Horde_ActiveSync_Message_Base +{ + + /** + * Property mappings + * + * @var array + */ + protected $_mapping = array( + Horde_ActiveSync::AIRSYNCBASE_CLIENTID => array(self::KEY_ATTRIBUTE => 'clientid'), + Horde_ActiveSync::AIRSYNCBASE_CONTENT => array(self::KEY_ATTRIBUTE => 'content'), + Horde_ActiveSync::AIRSYNCBASE_CONTENTID => array(self::KEY_ATTRIBUTE => 'contentid'), + Horde_ActiveSync::AIRSYNCBASE_CONTENTLOCATION => array(self::KEY_ATTRIBUTE => 'contentlocation'), + Horde_ActiveSync::AIRSYNCBASE_CONTENTTYPE => array(self::KEY_ATTRIBUTE => 'contenttype'), + Horde_ActiveSync::AIRSYNCBASE_DISPLAYNAME => array(self::KEY_ATTRIBUTE => 'displayname'), + Horde_ActiveSync::AIRSYNCBASE_ISINLINE => array(self::KEY_ATTRIBUTE => 'isinline'), + Horde_ActiveSync::AIRSYNCBASE_METHOD => array(self::KEY_ATTRIBUTE => 'method') + ); + + /** + * Property mapping. + * + * @var array + */ + protected $_properties = array( + 'clientid' => false, + 'content' => false, + 'contentid' => false, + 'contentlocation' => false, + 'contenttype' => false, + 'displayname' => false, + 'isinline' => false, + 'method' => false, + ); + + /** + * Return the type of message. + * + * @return string + */ + public function getClass() + { + return 'AirSyncBaseAdd'; + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachment.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachment.php index 32e33fde3f8..1a83fd9ad79 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachment.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachment.php @@ -83,6 +83,27 @@ class Horde_ActiveSync_Message_AirSyncBaseAttachment extends Horde_ActiveSync_Me '_data' => false ); + /** + * Const'r + * + * @see Horde_ActiveSync_Message_Base::__construct() + */ + public function __construct(array $options = array()) + { + parent::__construct($options); + if ($this->_version >= Horde_ActiveSync::VERSION_SIXTEEN) { + $this->_mapping += array( + Horde_ActiveSync::AIRSYNCBASE_CLIENTID=> array(self::KEY_ATTRIBUTE => 'clientid') + ); + + $this->_properties += array( + 'clientid' => false, + 'filereference' => false, + ); + } + } + + /** * Return the type of message. * diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachments.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachments.php new file mode 100644 index 00000000000..ccdb97d18b5 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseAttachments.php @@ -0,0 +1,93 @@ + + * @package ActiveSync + */ +/** + * Horde_ActiveSync_Message_AirSyncBaseAttachment:: + * + * @license http://www.horde.org/licenses/gpl GPLv2 + * NOTE: According to sec. 8 of the GENERAL PUBLIC LICENSE (GPL), + * Version 2, the distribution of the Horde_ActiveSync module in or + * to the United States of America is excluded from the scope of this + * license. + * @copyright 2011-2016 Horde LLC (http://www.horde.org) + * @author Michael J Rubinsky + * @package ActiveSync + */ +class Horde_ActiveSync_Message_AirSyncBaseAttachments extends Horde_ActiveSync_Message_Base +{ + /* Attachement types */ + const ATT_TYPE_NORMAL = 1; + const ATT_TYPE_EMBEDDED = 5; + + /** + * Property mappings + * + * @var array + */ + protected $_mapping = array( + Horde_ActiveSync::AIRSYNCBASE_ATTACHMENT => array( + self::KEY_ATTRIBUTE => 'attachment', + self::KEY_VALUES => Horde_ActiveSync::AIRSYNCBASE_ATTACHMENT, + self::KEY_PROPERTY => self::PROPERTY_NO_CONTAINER + ) + ); + + /** + * Property mapping. + * + * @var array + */ + protected $_properties = array( + 'attachment' => false, + ); + + /** + * Const'r + * + * @see Horde_ActiveSync_Message_Base::__construct() + */ + public function __construct(array $options = array()) + { + parent::__construct($options); + if ($this->_version >= Horde_ActiveSync::VERSION_SIXTEEN) { + $this->_mapping += array( + Horde_ActiveSync::AIRSYNCBASE_ADD => array( + self::KEY_ATTRIBUTE => 'add', + self::KEY_TYPE => 'Horde_ActiveSync_Message_AirSyncBaseAdd', + self::KEY_PROPERTY => self::PROPERTY_MULTI_ARRAY + ), + Horde_ActiveSync::AIRSYNCBASE_DELETE => array( + self::KEY_ATTRIBUTE => 'delete', + self::KEY_TYPE => 'Horde_ActiveSync_Message_AirSyncBaseDelete', + self::KEY_PROPERTY => self::PROPERTY_MULTI_ARRAY, + ) + ); + + $this->_properties += array( + 'add' => false, + 'delete' => false, + ); + } + } + + /** + * Return the type of message. + * + * @return string + */ + public function getClass() + { + return 'AirSyncBaseAttachments'; + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseDelete.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseDelete.php new file mode 100644 index 00000000000..a10a85a1d23 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseDelete.php @@ -0,0 +1,57 @@ + + * @package ActiveSync + */ +/** + * Horde_ActiveSync_Message_AirSyncBaseAdd:: + * + * @license http://www.horde.org/licenses/gpl GPLv2 + * NOTE: According to sec. 8 of the GENERAL PUBLIC LICENSE (GPL), + * Version 2, the distribution of the Horde_ActiveSync module in or + * to the United States of America is excluded from the scope of this + * license. + * @copyright 2011-2016 Horde LLC (http://www.horde.org) + * @author Michael J Rubinsky + * @package ActiveSync + */ +class Horde_ActiveSync_Message_AirSyncBaseDelete extends Horde_ActiveSync_Message_Base +{ + + /** + * Property mappings + * + * @var array + */ + protected $_mapping = array( + Horde_ActiveSync::AIRSYNCBASE_FILEREFERENCE => array(self::KEY_ATTRIBUTE => 'filereference') + ); + + /** + * Property mapping. + * + * @var array + */ + protected $_properties = array( + 'filereference' => false + ); + + /** + * Return the type of message. + * + * @return string + */ + public function getClass() + { + return 'AirSyncBaseDelete'; + } + +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php index a7df4abed5d..5505b12b6aa 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php @@ -179,7 +179,7 @@ class Horde_ActiveSync_Message_Appointment extends Horde_ActiveSync_Message_Base 'exceptions' => array(), 'organizeremail' => false, 'organizername' => false, - 'meetingstatus' => self::MEETING_NOT_MEETING, + 'meetingstatus' => false, 'recurrence' => false, 'reminder' => false, 'sensitivity' => false, @@ -259,11 +259,13 @@ public function __construct(array $options = array()) Horde_ActiveSync::AIRSYNCBASE_LOCATION => array(self::KEY_ATTRIBUTE => 'location', self::KEY_TYPE => 'Horde_ActiveSync_Message_AirSyncBaseLocation'), self::POOMCAL_CLIENTUID => array(self::KEY_ATTRIBUTE => 'clientuid'), Horde_ActiveSync::AIRSYNCBASE_INSTANCEID => array(self::KEY_ATTRIBUTE => 'instanceid', self::KEY_TYPE => self::TYPE_DATE), + Horde_ActiveSync::AIRSYNCBASE_ATTACHMENTS => array(self::KEY_ATTRIBUTE => 'airsyncbaseattachments', self::KEY_TYPE => 'Horde_ActiveSync_Message_AirSyncBaseAttachments') ); $this->_properties += array( 'location' => false, 'clientuid' => false, 'instanceid' => false, + 'airsyncbaseattachments' => false ); } } @@ -899,7 +901,7 @@ public function setMeetingStatus($status) */ public function getMeetingStatus() { - return $this->_getAttribute('meetingstatus', self::MEETING_NOT_MEETING); + return $this->_getAttribute('meetingstatus'); } /** diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php index 42a92b9ef16..56fbe07cd20 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php @@ -50,8 +50,8 @@ class Horde_ActiveSync_Message_Base const TYPE_MAPI_STREAM = 4; const TYPE_MAPI_GOID = 5; const TYPE_DATE_LOCAL = 6; - const PROPERTY_NO_CONTAINER = 7; + const PROPERTY_MULTI_ARRAY = 8; /** * Holds the mapping for object properties @@ -455,7 +455,18 @@ public function decodeStream(Horde_ActiveSync_Wbxml_Decoder &$decoder) ); throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag'); } - $this->{$map[self::KEY_ATTRIBUTE]} = $decoded; + // If we have a container that can hold multiple + // properties that are also containers, but not all of + // the same type, we have to hanlde separately. + if (isset($map[self::KEY_PROPERTY]) && + $map[self::KEY_PROPERTY] == self::PROPERTY_MULTI_ARRAY) { + if (!is_array($this->{$map[self::KEY_ATTRIBUTE]})) { + $this->{$map[self::KEY_ATTRIBUTE]} = array(); + } + $this->{$map[self::KEY_ATTRIBUTE]}[] = $decoded; + } else { + $this->{$map[self::KEY_ATTRIBUTE]} = $decoded; + } } } } elseif ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) { diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/ItemOperations.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/ItemOperations.php index 5d9ba5d4cfd..266996abb6b 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/ItemOperations.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/ItemOperations.php @@ -210,6 +210,11 @@ protected function _handle() case 'fetch' : switch(Horde_String::lower($value['store'])) { case 'mailbox' : + // Yes, even though this is a "mailbox" store, this is + // how EAS identifies calendar attachments too since + // they are not documentLibrary items. The backend + // needs to be able to identify where to get the + // item from based solely on the filereference. $this->_encoder->startTag(self::ITEMOPERATIONS_FETCH); if (isset($value['airsyncbasefilereference'])) { // filereference is already in the backend serverid format diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php index 3d64a1fc464..8fd807353c2 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php @@ -513,7 +513,8 @@ protected function _handle() } if (!empty($collection['clientids']) || !empty($collection['fetchids']) - || !empty($collection['missing']) || !empty($collection['importfailures'])) { + || !empty($collection['missing']) || !empty($collection['importfailures']) + || !empty($collection['modifiedids'])) { $this->_encoder->startTag(Horde_ActiveSync::SYNC_REPLIES); @@ -538,6 +539,47 @@ protected function _handle() } } + // EAS 16. CHANGED responses for items that need one. This + // is basically the results of any AirSyncBaseAttachments + // actions on Appointment or Draft Email items. + if (!empty($collection['modifiedids'])) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); + + foreach ($collection['modifiedids'] as $serverid) { + // @TODO FIXME - don't do this here, make $collection + // a full object and have it be responsible for + // returning the necessary message objects for the + // response. @todo Instanceid? + if ($collection['class'] == Horde_ActiveSync::CLASS_CALENDAR && + $this->_activeSync->device->version >= Horde_ActiveSync::VERSION_SIXTEEN && + !empty($collection['atchash'][$serverid])) { + + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($serverid); + $this->_encoder->endTag(); + + $msg = $this->_activeSync->messageFactory('Appointment'); + $msg->uid = $serverid; + $msg->airsyncbaseattachments = $this->_activeSync->messageFactory('AirSyncBaseAttachments'); + $msg->airsyncbaseattachments->attachment = array(); + foreach ($collection['atchash'][$serverid]['add'] as $clientid => $filereference) { + $atc = $this->_activeSync->messageFactory('AirSyncBaseAttachment'); + $atc->clientid = $clientid; + $atc->attname = $filereference; + $msg->airsyncbaseattachments->attachment[] = $atc; + } + $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); + $msg->encodeStream($this->_encoder); + $this->_encoder->endTag(); + + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content(self::STATUS_SUCCESS); + $this->_encoder->endTag(); + } + } + $this->_encoder->endTag(); + } + // Server IDs for new items we received from client if (!empty($collection['clientids'])) { foreach ($collection['clientids'] as $clientid => $serverid) { @@ -549,6 +591,8 @@ protected function _handle() $this->_encoder->startTag(Horde_ActiveSync::SYNC_ADD); // If we have clientids and a CLASS_EMAIL, this is // a SMS response. + // @TODO: have collection classes be able to + // generate their own responses?? if ($collection['class'] == Horde_ActiveSync::CLASS_EMAIL) { $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); $this->_encoder->content(Horde_ActiveSync::CLASS_SMS); @@ -562,6 +606,27 @@ protected function _handle() $this->_encoder->content($serverid); $this->_encoder->endTag(); } + + // @TODO. FIX ME. Don't do this here. + if ($collection['class'] == Horde_ActiveSync::CLASS_CALENDAR && + $this->_activeSync->device->version >= Horde_ActiveSync::VERSION_SIXTEEN) { + $msg = $this->_activeSync->messageFactory('Appointment'); + $msg->uid = $serverid; + if (!empty($collection['atchash'][$serverid])) { + $msg->airsyncbaseattachments = $this->_activeSync->messageFactory('AirSyncBaseAttachments'); + $msg->airsyncbaseattachments->attachment = array(); + foreach ($collection['atchash'][$serverid]['add'] as $clientid => $filereference) { + $atc = $this->_activeSync->messageFactory('AirSyncBaseAttachment'); + $atc->clientid = $clientid; + $atc->attname = $filereference; + $msg->airsyncbaseattachments->attachment[] = $atc; + } + } + $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); + $msg->encodeStream($this->_encoder); + $this->_encoder->endTag(); + } + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); $this->_encoder->content($status); $this->_encoder->endTag(); @@ -1082,24 +1147,38 @@ protected function _parseSyncCommands(&$collection) switch ($commandType) { case Horde_ActiveSync::SYNC_MODIFY: if (isset($appdata)) { - $id = $importer->importMessageChange( + $ires = $importer->importMessageChange( $serverid, $appdata, $this->_device, false, $collection['class'], $collection['synckey'] ); - if ($id && !is_array($id)) { + if (is_array($ires) && !empty($ires['error'])) { + $collection['importedfailures'][$ires[0]] = $ires['error']; + } elseif (is_array($ires)) { $collection['importedchanges'] = true; - } elseif (is_array($id)) { - $collection['importfailures'][$id[0]] = $id[1]; + if (empty($collection['modifiedids'])) { + $collection['modifiedids'] = array(); + } + $collection['modifiedids'][] = $ires['id']; + $collection['atchash'][$serverid] = !empty($ires['atchash']) + ? $ires['atchash'] + : array(); } } break; case Horde_ActiveSync::SYNC_ADD: if (isset($appdata)) { - $id = $importer->importMessageChange( - false, $appdata, $this->_device, $clientid, $collection['class']); - if ($clientid && $id && !is_array($id)) { - $collection['clientids'][$clientid] = $id; + $ires = $importer->importMessageChange( + false, $appdata, $this->_device, $clientid, + $collection['class'] + ); + if (!$ires || !empty($ires['error'])) { + $collection['clientids'][$clientid] = false; + } elseif ($clientid && is_array($ires)) { + $collection['clientids'][$clientid] = $ires['id']; + $collection['atchash'][$ires['id']] = !empty($ires['atchash']) + ? $ires['atchash'] + : array(); $collection['importedchanges'] = true; } elseif (!$id || is_array($id)) { $collection['clientids'][$clientid] = false; diff --git a/framework/ActiveSync/package.xml b/framework/ActiveSync/package.xml index 11f40a88be2..428f76c0bab 100644 --- a/framework/ActiveSync/package.xml +++ b/framework/ActiveSync/package.xml @@ -10,7 +10,7 @@ mrubinsk@horde.org yes - 2016-07-01 + 2016-08-27 2.35.1 2.35.0 @@ -72,9 +72,12 @@ + + + @@ -473,9 +476,12 @@ + + + @@ -3214,7 +3220,7 @@ stable stable - 2016-07-01 + 2016-08-27 GPL-2.0 * From f6932b3b4077ebb715107471ba85cd69b4c98467 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 15:16:26 -0400 Subject: [PATCH 10/15] location is an AirSyncBaseLocation object in EAS 16. --- kronolith/lib/Event.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/kronolith/lib/Event.php b/kronolith/lib/Event.php index 250867aebb3..37db22c54d1 100644 --- a/kronolith/lib/Event.php +++ b/kronolith/lib/Event.php @@ -1677,8 +1677,14 @@ public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message) } } - if (!$message->isGhosted('location') && - strlen($location = $message->getLocation())) { + // EAS 16 location property is an AirSyncBaseLocation object, not + // a string. + $location = $message->getLocation(); + if (is_object($location)) { + // @todo - maybe build a more complete name based on city/country? + $location = $location->displayname; + } + if (!$message->isGhosted('location') && strlen($location)) { $this->location = $location; } @@ -2037,7 +2043,19 @@ public function toASAppointment(array $options = array()) } else { $message->setBody($this->description); } - $message->setLocation($this->location); + if ($options['protocolversion'] >= Horde_ActiveSync::VERSION_SIXTEEN && !empty($this->location)) { + $message->location = new Horde_ActiveSync_Message_AirSyncBaseLocation( + array( + 'logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger'), + 'protocolversion' => $options['protocolversion'] + ) + ); + // @todo - worth it to try to get full city/country etc... + // from geotagging service if available?? + $message->location->displayname = $this->location; + } else { + $message->setLocation($this->location); + } } $message->setSubject($this->getTitle()); From cfd2958b872bf3115ea9a57ead1f3d48a215008d Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 15:47:31 -0400 Subject: [PATCH 11/15] Fix deleting attachments. --- kronolith/lib/Event.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/kronolith/lib/Event.php b/kronolith/lib/Event.php index 37db22c54d1..aba29753ecb 100644 --- a/kronolith/lib/Event.php +++ b/kronolith/lib/Event.php @@ -1960,9 +1960,14 @@ public function addEASFiles($message) } } if ($att->delete) { - foreach ($atcs->delete as $del_ob) { + foreach ($att->delete as $del_ob) { $file_parts = explode(':', $del_ob->filereference, 4); - $this->deleteFile($file_parts[3]); + try { + $this->deleteFile($file_parts[3]); + $results['delete'][] = $del_ob->filereference; + } catch (Kronolith_Exception $e) { + Horde::log('Unable to remove VFS file.', 'ERR'); + } } } } From 073fca88e2efb1fa02e95e94d53326e85e63de77 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 16:14:00 -0400 Subject: [PATCH 12/15] Refactor exporter out into separate classes for Sync and FolderSync. --- .../ActiveSync/Connector/Exporter/Base.php | 104 +++++ .../Connector/Exporter/FolderSync.php | 136 ++++++ .../{Exporter.php => Exporter/Sync.php} | 423 ++++++++---------- .../Horde/ActiveSync/Request/FolderSync.php | 4 +- .../lib/Horde/ActiveSync/Request/Sync.php | 95 +--- framework/ActiveSync/package.xml | 14 +- 6 files changed, 436 insertions(+), 340 deletions(-) create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Base.php create mode 100644 framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php rename framework/ActiveSync/lib/Horde/ActiveSync/Connector/{Exporter.php => Exporter/Sync.php} (69%) diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Base.php new file mode 100644 index 00000000000..04f95d26ade --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Base.php @@ -0,0 +1,104 @@ + + * @package ActiveSync + */ +/** + * Horde_ActiveSync_Connector_Exporter_Base:: Base class contains common + * code for outputing common blocks of WBXML data in server responses. + * + * @license http://www.horde.org/licenses/gpl GPLv2 + * NOTE: According to sec. 8 of the GENERAL PUBLIC LICENSE (GPL), + * Version 2, the distribution of the Horde_ActiveSync module in or + * to the United States of America is excluded from the scope of this + * license. + * @copyright 2009-2016 Horde LLC (http://www.horde.org) + * @author Michael J Rubinsky + * @package ActiveSync + */ +abstract class Horde_ActiveSync_Connector_Exporter_Base +{ + /** + * The wbxml encoder + * + * @var Horde_ActiveSync_Wbxml_Encoder + */ + protected $_encoder; + + /** + * Local cache of changes to send. + * + * @var array + */ + protected $_changes = array(); + + /** + * Counter of changes sent. + * + * @var integer + */ + protected $_step = 0; + + /** + * The ActiveSync server object. + * + * @var Horde_ActiveSync + */ + protected $_as; + + /** + * Process id for logging. + * + * @var integer + */ + protected $_procid; + + /** + * Const'r + * + * @param Horde_ActiveSync $as The ActiveSync server. + * @param Horde_ActiveSync_Wbxml_Encoder $encoder The encoder + * + * @return Horde_ActiveSync_Connector_Exporter + */ + public function __construct( + Horde_ActiveSync $as, + Horde_ActiveSync_Wbxml_Encoder $encoder = null) + { + $this->_as = $as; + $this->_encoder = $encoder; + $this->_logger = $as->logger; + $this->_procid = getmypid(); + } + + /** + * Set the changes to send to the client. + * + * @param array $changes The changes array returned from the collection + * handler. + * @param array $collection The collection we are currently syncing. + */ + public function setChanges($changes, $collection = null) + { + $this->_changes = $changes; + $this->_seenObjects = array(); + $this->_step = 0; + } + + /** + * Sends the next change in the set to the client. + * + * @return boolean|Horde_Exception True if more changes can be sent false if + * all changes were sent, Horde_Exception if + * there was an error sending an item. + */ + abstract public function sendNextChange(); +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php new file mode 100644 index 00000000000..6221eef6c01 --- /dev/null +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php @@ -0,0 +1,136 @@ + + * @package ActiveSync + */ +/** + * Horde_ActiveSync_Connector_Exporter_FolderSync:: Responsible for outputing + * blocks of WBXML responses in FOLDER_SYNC responses. + * + * @license http://www.horde.org/licenses/gpl GPLv2 + * NOTE: According to sec. 8 of the GENERAL PUBLIC LICENSE (GPL), + * Version 2, the distribution of the Horde_ActiveSync module in or + * to the United States of America is excluded from the scope of this + * license. + * @copyright 2009-2016 Horde LLC (http://www.horde.org) + * @author Michael J Rubinsky + * @package ActiveSync + */ +class Horde_ActiveSync_Connector_Exporter_Sync extends Horde_ActiveSync_Connector_Exporter_Base +{ + + /** + * Array of folder objects that have changed. + * Used when exporting folder structure changes since they are not streamed + * from this object. + * + * @var array + */ + public $changed = array(); + + /** + * Array of folder ids that have been deleted on the server. + * + * @var array + */ + public $deleted = array(); + + /** + * Tracks the total number of folder changes + * + * @var integer + */ + public $count = 0; + + /** + * Sends the next change in the set to the client. + * + * @return boolean|Horde_Exception True if more changes can be sent false if + * all changes were sent, Horde_Exception if + * there was an error sending an item. + */ + public function sendNextChange() + { + return $this->_sendNextFolderSyncChange(); + } + + /** + * Sends the next folder change to the client. + * + * @return @see self::sendNextChange() + */ + protected function _sendNextFolderSyncChange() + { + if ($this->_step < count($this->_changes)) { + $change = $this->_changes[$this->_step]; + switch($change['type']) { + case Horde_ActiveSync::CHANGE_TYPE_CHANGE: + // Folder add/change. + if ($folder = $this->_as->driver->getFolder($change['serverid'])) { + // @TODO BC HACK. Need to ensure we have a _serverid here. + // REMOVE IN H6. + if (empty($folder->_serverid)) { + $folder->_serverid = $folder->serverid; + } + $stat = $this->_as->driver->statFolder( + $change['id'], + $folder->parentid, + $folder->displayname, + $folder->_serverid, + $folder->type); + $this->folderChange($folder); + } else { + $this->_logger->err(sprintf( + '[%s] Error stating %s: ignoring.', + $this->_procid, $change['id'])); + $stat = array('id' => $change['id'], 'mod' => $change['id'], 0); + } + // Update the state. + $this->_as->state->updateState( + Horde_ActiveSync::CHANGE_TYPE_FOLDERSYNC, $stat); + break; + + case Horde_ActiveSync::CHANGE_TYPE_DELETE: + $this->folderDeletion($change['id']); + $this->_as->state->updateState( + Horde_ActiveSync::CHANGE_TYPE_DELETE, $change); + break; + } + $this->_step++; + return true; + } else { + return false; + } + } + + /** + * Add a folder change to the cache (used during FolderSync requests). + * + * @param Horde_ActiveSync_Message_Folder $folder + */ + public function folderChange(Horde_ActiveSync_Message_Folder $folder) + { + $this->changed[] = $folder; + $this->count++; + } + + /** + * Add a folder deletion to the cache (used during FolderSync Requests). + * + * @param string $id The folder id + */ + public function folderDeletion($id) + { + $this->deleted[] = $id; + $this->count++; + } + +} \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php similarity index 69% rename from framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter.php rename to framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php index 46d14a533a2..f303eac01b1 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php @@ -1,17 +1,6 @@ * @package ActiveSync */ -class Horde_ActiveSync_Connector_Exporter +class Horde_ActiveSync_Connector_Exporter_Sync extends Horde_ActiveSync_Connector_Exporter_Base { - /** - * The wbxml encoder - * - * @var Horde_ActiveSync_Wbxml_Encoder - */ - protected $_encoder; /** * Local cache of object ids we have already dealt with. @@ -51,109 +36,210 @@ class Horde_ActiveSync_Connector_Exporter protected $_seenObjects = array(); /** - * Array of folder objects that have changed. - * Used when exporting folder structure changes since they are not streamed - * from this object. + * Currently syncing collection. * * @var array */ - public $changed = array(); + protected $_currentCollection; - /** - * Array of folder ids that have been deleted on the server. - * - * @var array - */ - public $deleted = array(); /** - * Tracks the total number of folder changes + * Set the changes to send to the client. * - * @var integer + * @param array $changes The changes array returned from the collection + * handler. + * @param array $collection The collection we are currently syncing. */ - public $count = 0; + public function setChanges($changes, $collection = null) + { + parent::setChanges($changes, $collection); + $this->_currentCollection = $collection; + } /** - * Local cache of changes to send. + * Sends the next change in the set to the client. * - * @var array + * @return boolean|Horde_Exception True if more changes can be sent false if + * all changes were sent, Horde_Exception if + * there was an error sending an item. */ - protected $_changes = array(); + public function sendNextChange() + { + return $this->_sendNextChange(); + } /** - * Counter of changes sent. + * Send a message change over the wbxml stream * - * @var integer + * @param string $id The uid of the message + * @param Horde_ActiveSync_Message_Base $message The message object */ - protected $_step = 0; + public function messageChange($id, Horde_ActiveSync_Message_Base $message) + { + // Just ignore any messages that are not from this collection and + // prevent sending the same object twice in one request. + if ($message->getClass() != $this->_currentCollection['class'] || + in_array($id, $this->_seenObjects)) { + $this->_logger->notice(sprintf( + '[%s] IGNORING message %s since it looks like it was already sent or does not belong to this collection. Class: %s, CurrentClass: %s', + $this->_procid, + $id, + $message->getClass(), + $this->_currentCollection['class'])); + return; + } - /** - * Currently syncing collection. - * - * @var array - */ - protected $_currentCollection; + // Ignore any empty objects. + if ($message->isEmpty()) { + $this->_logger->notice(sprintf( + '[%s] IGNORING message %s since it looks like it does not contain any data. Class: %s, CurrentClass: %s', + $this->_procid, + $id, + $message->getClass(), + $this->_currentCollection['class'])); + return; + } - /** - * The ActiveSync server object. - * - * @var Horde_ActiveSync - */ - protected $_as; + // Remember this message + $this->_seenObjects[] = $id; - /** - * Process id for logging. - * - * @var integer - */ - protected $_procid; + // Specify if this is an ADD or a MODIFY change? + if ($message->flags === Horde_ActiveSync::FLAG_NEWMESSAGE) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_ADD); + } else { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); + } + + // Send the message + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); + try { + $message->encodeStream($this->_encoder); + } catch (Horde_ActiveSync_Exception $e) { + $this->_logger->err($e); + } + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } /** - * Const'r - * - * @param Horde_ActiveSync $as The ActiveSync server. - * @param Horde_ActiveSync_Wbxml_Encoder $encoder The encoder + * Stream a message deletion to the client. * - * @return Horde_ActiveSync_Connector_Exporter + * @param string $id The uid of the message we are deleting. + * @param boolean $soft If true, send a SOFTDELETE, otherwise a REMOVE. */ - public function __construct( - Horde_ActiveSync $as, - Horde_ActiveSync_Wbxml_Encoder $encoder = null) + public function messageDeletion($id, $soft = false) { - $this->_as = $as; - $this->_encoder = $encoder; - $this->_logger = $as->logger; - $this->_procid = getmypid(); + if ($soft) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SOFTDELETE); + } else { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_REMOVE); + } + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + $this->_encoder->endTag(); } /** - * Set the changes to send to the client. + * Move a message to a different folder. * - * @param array $changes The changes array returned from the collection - * handler. - * @param array $collection The collection we are currently syncing. + * @param Horde_ActiveSync_Message_Base $message The message */ - public function setChanges($changes, $collection) + public function messageMove($message) { - $this->_changes = $changes; - $this->_seenObjects = array(); - $this->_step = 0; - $this->_currentCollection = $collection; } - /** - * Sends the next change in the set to the client. - * - * @return boolean|Horde_Exception True if more changes can be sent false if - * all changes were sent, Horde_Exception if - * there was an error sending an item. - */ - public function sendNextChange() + public function syncAddResponse($collection) { - if (empty($this->_currentCollection)) { - return $this->_sendNextFolderSyncChange(); - } else { - return $this->_sendNextChange(); + foreach ($collection['clientids'] as $clientid => $serverid) { + if ($serverid) { + $status = Horde_ActiveSync_Request_Sync::STATUS_SUCCESS; + } else { + $status = Horde_ActiveSync_Request_Sync::STATUS_INVALID; + } + // Start SYNC_ADD + $this->_encoder->startTag(Horde_ActiveSync::SYNC_ADD); + + // If we have clientids and a CLASS_EMAIL, this is + // a SMS response. + // @TODO: have collection classes be able to + // generate their own responses?? + if ($collection['class'] == Horde_ActiveSync::CLASS_EMAIL) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); + $this->_encoder->content(Horde_ActiveSync::CLASS_SMS); + $this->_encoder->endTag(); + } + + // CLIENTENTRYID + $this->_encoder->startTag(Horde_ActiveSync::SYNC_CLIENTENTRYID); + $this->_encoder->content($clientid); + $this->_encoder->endTag(); + + // SERVERENTRYID + if ($status == Horde_ActiveSync_Request_Sync::STATUS_SUCCESS) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($serverid); + $this->_encoder->endTag(); + } + + // EAS 16? + $this->_sendEas16MessageResponse($serverid, $collection); + + // STATUS + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content($status); + $this->_encoder->endTag(); + + // END SYNC_ADD + $this->_encoder->endTag(); + } + } + + protected function _sendEas16MessageResponse($serverid, $collection) + { + if ($this->_as->device->version >= Horde_ActiveSync::VERSION_SIXTEEN && + $collection['class'] == Horde_ActiveSync::CLASS_CALENDAR && + !empty($collection['atchash'][$serverid])) { + + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($serverid); + $this->_encoder->endTag(); + + $msg = $this->_as->messageFactory('Appointment'); + $msg->uid = $serverid; + $msg->airsyncbaseattachments = $this->_as->messageFactory('AirSyncBaseAttachments'); + $msg->airsyncbaseattachments->attachment = array(); + foreach ($collection['atchash'][$serverid]['add'] as $clientid => $filereference) { + $atc = $this->_as->messageFactory('AirSyncBaseAttachment'); + $atc->clientid = $clientid; + $atc->attname = $filereference; + $msg->airsyncbaseattachments->attachment[] = $atc; + } + $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); + $msg->encodeStream($this->_encoder); + $this->_encoder->endTag(); + } + } + + public function syncModifiedResponse($collection) + { + foreach ($collection['modifiedids'] as $serverid) { + // Start SYNC_MODIFY + $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); + + // EAS 16? + $this->_sendEas16MessageResponse($serverid, $collection); + + // SYNC_STATUS + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content(Horde_ActiveSync_Request_Sync::STATUS_SUCCESS); + $this->_encoder->endTag(); + + // End SYNC_MODIFY + $this->_encoder->endTag(); } } @@ -285,159 +371,4 @@ protected function _sendNextChange() return true; } - /** - * Sends the next folder change to the client. - * - * @return @see self::sendNextChange() - */ - protected function _sendNextFolderSyncChange() - { - if ($this->_step < count($this->_changes)) { - $change = $this->_changes[$this->_step]; - switch($change['type']) { - case Horde_ActiveSync::CHANGE_TYPE_CHANGE: - // Folder add/change. - if ($folder = $this->_as->driver->getFolder($change['serverid'])) { - // @TODO BC HACK. Need to ensure we have a _serverid here. - // REMOVE IN H6. - if (empty($folder->_serverid)) { - $folder->_serverid = $folder->serverid; - } - $stat = $this->_as->driver->statFolder( - $change['id'], - $folder->parentid, - $folder->displayname, - $folder->_serverid, - $folder->type); - $this->folderChange($folder); - } else { - $this->_logger->err(sprintf( - '[%s] Error stating %s: ignoring.', - $this->_procid, $change['id'])); - $stat = array('id' => $change['id'], 'mod' => $change['id'], 0); - } - // Update the state. - $this->_as->state->updateState( - Horde_ActiveSync::CHANGE_TYPE_FOLDERSYNC, $stat); - break; - - case Horde_ActiveSync::CHANGE_TYPE_DELETE: - $this->folderDeletion($change['id']); - $this->_as->state->updateState( - Horde_ActiveSync::CHANGE_TYPE_DELETE, $change); - break; - } - $this->_step++; - return true; - } else { - return false; - } - } - - /** - * Send a message change over the wbxml stream - * - * @param string $id The uid of the message - * @param Horde_ActiveSync_Message_Base $message The message object - */ - public function messageChange($id, Horde_ActiveSync_Message_Base $message) - { - // Just ignore any messages that are not from this collection and - // prevent sending the same object twice in one request. - if ($message->getClass() != $this->_currentCollection['class'] || - in_array($id, $this->_seenObjects)) { - $this->_logger->notice(sprintf( - '[%s] IGNORING message %s since it looks like it was already sent or does not belong to this collection. Class: %s, CurrentClass: %s', - $this->_procid, - $id, - $message->getClass(), - $this->_currentCollection['class'])); - return; - } - - // Ignore any empty objects. - if ($message->isEmpty()) { - $this->_logger->notice(sprintf( - '[%s] IGNORING message %s since it looks like it does not contain any data. Class: %s, CurrentClass: %s', - $this->_procid, - $id, - $message->getClass(), - $this->_currentCollection['class'])); - return; - } - - // Remember this message - $this->_seenObjects[] = $id; - - // Specify if this is an ADD or a MODIFY change? - if ($message->flags === Horde_ActiveSync::FLAG_NEWMESSAGE) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_ADD); - } else { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); - } - - // Send the message - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($id); - $this->_encoder->endTag(); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); - try { - $message->encodeStream($this->_encoder); - } catch (Horde_ActiveSync_Exception $e) { - $this->_logger->err($e); - } - $this->_encoder->endTag(); - $this->_encoder->endTag(); - } - - /** - * Stream a message deletion to the client. - * - * @param string $id The uid of the message we are deleting. - * @param boolean $soft If true, send a SOFTDELETE, otherwise a REMOVE. - */ - public function messageDeletion($id, $soft = false) - { - if ($soft) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SOFTDELETE); - } else { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_REMOVE); - } - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($id); - $this->_encoder->endTag(); - $this->_encoder->endTag(); - } - - /** - * Move a message to a different folder. - * - * @param Horde_ActiveSync_Message_Base $message The message - */ - function messageMove($message) - { - } - - /** - * Add a folder change to the cache (used during FolderSync requests). - * - * @param Horde_ActiveSync_Message_Folder $folder - */ - public function folderChange(Horde_ActiveSync_Message_Folder $folder) - { - $this->changed[] = $folder; - $this->count++; - } - - /** - * Add a folder deletion to the cache (used during FolderSync Requests). - * - * @param string $id The folder id - */ - public function folderDeletion($id) - { - $this->deleted[] = $id; - $this->count++; - } - -} \ No newline at end of file +} diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php index 6ffe5f1bf24..005c398ce86 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php @@ -182,8 +182,8 @@ protected function _handle() // Start sending server -> client changes $newsynckey = $this->_state->getNewSyncKey($synckey); - $exporter = new Horde_ActiveSync_Connector_Exporter($this->_activeSync); - $exporter->setChanges($collections->getHierarchyChanges(), false); + $exporter = new Horde_ActiveSync_Connector_Exporter_FolderSync($this->_activeSync); + $exporter->setChanges($collections->getHierarchyChanges()); // Perform the actual sync operation while($exporter->sendNextChange()); diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php index 8fd807353c2..9f9bffb8a94 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php @@ -338,9 +338,10 @@ protected function _handle() $this->_encoder->endTag(); $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERS); - $exporter = new Horde_ActiveSync_Connector_Exporter( + $exporter = new Horde_ActiveSync_Connector_Exporter_Sync( $this->_activeSync, - $this->_encoder); + $this->_encoder + ); $cnt_global = 0; $over_window = false; @@ -542,96 +543,14 @@ protected function _handle() // EAS 16. CHANGED responses for items that need one. This // is basically the results of any AirSyncBaseAttachments // actions on Appointment or Draft Email items. - if (!empty($collection['modifiedids'])) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); - - foreach ($collection['modifiedids'] as $serverid) { - // @TODO FIXME - don't do this here, make $collection - // a full object and have it be responsible for - // returning the necessary message objects for the - // response. @todo Instanceid? - if ($collection['class'] == Horde_ActiveSync::CLASS_CALENDAR && - $this->_activeSync->device->version >= Horde_ActiveSync::VERSION_SIXTEEN && - !empty($collection['atchash'][$serverid])) { - - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($serverid); - $this->_encoder->endTag(); - - $msg = $this->_activeSync->messageFactory('Appointment'); - $msg->uid = $serverid; - $msg->airsyncbaseattachments = $this->_activeSync->messageFactory('AirSyncBaseAttachments'); - $msg->airsyncbaseattachments->attachment = array(); - foreach ($collection['atchash'][$serverid]['add'] as $clientid => $filereference) { - $atc = $this->_activeSync->messageFactory('AirSyncBaseAttachment'); - $atc->clientid = $clientid; - $atc->attname = $filereference; - $msg->airsyncbaseattachments->attachment[] = $atc; - } - $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); - $msg->encodeStream($this->_encoder); - $this->_encoder->endTag(); - - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content(self::STATUS_SUCCESS); - $this->_encoder->endTag(); - } - } - $this->_encoder->endTag(); + if ($this->_device->version >= Horde_ActiveSync::VERSION_SIXTEEN && + !empty($collection['modifiedids'])) { + $exporter->syncModifiedResponse($collection); } // Server IDs for new items we received from client if (!empty($collection['clientids'])) { - foreach ($collection['clientids'] as $clientid => $serverid) { - if ($serverid) { - $status = self::STATUS_SUCCESS; - } else { - $status = self::STATUS_INVALID; - } - $this->_encoder->startTag(Horde_ActiveSync::SYNC_ADD); - // If we have clientids and a CLASS_EMAIL, this is - // a SMS response. - // @TODO: have collection classes be able to - // generate their own responses?? - if ($collection['class'] == Horde_ActiveSync::CLASS_EMAIL) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); - $this->_encoder->content(Horde_ActiveSync::CLASS_SMS); - $this->_encoder->endTag(); - } - $this->_encoder->startTag(Horde_ActiveSync::SYNC_CLIENTENTRYID); - $this->_encoder->content($clientid); - $this->_encoder->endTag(); - if ($status == self::STATUS_SUCCESS) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($serverid); - $this->_encoder->endTag(); - } - - // @TODO. FIX ME. Don't do this here. - if ($collection['class'] == Horde_ActiveSync::CLASS_CALENDAR && - $this->_activeSync->device->version >= Horde_ActiveSync::VERSION_SIXTEEN) { - $msg = $this->_activeSync->messageFactory('Appointment'); - $msg->uid = $serverid; - if (!empty($collection['atchash'][$serverid])) { - $msg->airsyncbaseattachments = $this->_activeSync->messageFactory('AirSyncBaseAttachments'); - $msg->airsyncbaseattachments->attachment = array(); - foreach ($collection['atchash'][$serverid]['add'] as $clientid => $filereference) { - $atc = $this->_activeSync->messageFactory('AirSyncBaseAttachment'); - $atc->clientid = $clientid; - $atc->attname = $filereference; - $msg->airsyncbaseattachments->attachment[] = $atc; - } - } - $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); - $msg->encodeStream($this->_encoder); - $this->_encoder->endTag(); - } - - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content($status); - $this->_encoder->endTag(); - $this->_encoder->endTag(); - } + $exporter->syncAddResponse($collection); } // Errors from missing messages in REMOVE requests. diff --git a/framework/ActiveSync/package.xml b/framework/ActiveSync/package.xml index 428f76c0bab..656e8e281cc 100644 --- a/framework/ActiveSync/package.xml +++ b/framework/ActiveSync/package.xml @@ -10,7 +10,7 @@ mrubinsk@horde.org yes - 2016-08-27 + 2016-08-31 2.35.1 2.35.0 @@ -37,7 +37,11 @@ - + + + + + @@ -455,8 +459,10 @@ - + + + @@ -3220,7 +3226,7 @@ stable stable - 2016-08-27 + 2016-08-31 GPL-2.0 * From 083fa32e3b247913595fc5943ebef1004314c9d0 Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 16:29:15 -0400 Subject: [PATCH 13/15] Fix classname. --- framework/ActiveSync/lib/Horde/ActiveSync/Collections.php | 4 ++-- .../lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Collections.php b/framework/ActiveSync/lib/Horde/ActiveSync/Collections.php index f27209854db..766f61cf2c9 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Collections.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Collections.php @@ -708,10 +708,10 @@ public function getHierarchyChanges() * Validate and perform some sanity checks on the hierarchy changes before * being sent to the client. * - * @param Horde_ActiveSync_Connector_Exporter $exporter The exporter. + * @param Horde_ActiveSync_Connector_Exporter_FolderSync $exporter The exporter. * @param array $seenFolders An array of folders. */ - public function validateHierarchyChanges(Horde_ActiveSync_Connector_Exporter $exporter, array $seenFolders) + public function validateHierarchyChanges(Horde_ActiveSync_Connector_Exporter_FolderSync $exporter, array $seenFolders) { if ($this->_as->device->version < Horde_ActiveSync::VERSION_TWELVEONE || count($exporter->changed)) { diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php index 6221eef6c01..7eb6a6d7630 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/FolderSync.php @@ -24,7 +24,7 @@ * @author Michael J Rubinsky * @package ActiveSync */ -class Horde_ActiveSync_Connector_Exporter_Sync extends Horde_ActiveSync_Connector_Exporter_Base +class Horde_ActiveSync_Connector_Exporter_FolderSync extends Horde_ActiveSync_Connector_Exporter_Base { /** From df2e02700c22ec330e772f417986e024b05aba5f Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 16:29:37 -0400 Subject: [PATCH 14/15] Fix attribute names. --- .../lib/Horde/ActiveSync/Message/AirSyncBaseLocation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseLocation.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseLocation.php index 03129de9b20..4731ef488fa 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseLocation.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Message/AirSyncBaseLocation.php @@ -36,8 +36,8 @@ class Horde_ActiveSync_Message_AirSyncBaseLocation extends Horde_ActiveSync_Mess Horde_ActiveSync::AIRSYNCBASE_ALTITUDE => array(self::KEY_ATTRIBUTE => 'altitude'), Horde_ActiveSync::AIRSYNCBASE_ALTITUDEACCURACY => array(self::KEY_ATTRIBUTE => 'altitudeaccuracy'), Horde_ActiveSync::AIRSYNCBASE_ANNOTATION => array(self::KEY_ATTRIBUTE => 'annotation'), - Horde_ActiveSync::AIRSYNCBASE_CITY => array(self::KEY_ATTRIBUTE => 'annotation'), - Horde_ActiveSync::AIRSYNCBASE_COUNTRY => array(self::KEY_ATTRIBUTE => 'annotation'), + Horde_ActiveSync::AIRSYNCBASE_CITY => array(self::KEY_ATTRIBUTE => 'city'), + Horde_ActiveSync::AIRSYNCBASE_COUNTRY => array(self::KEY_ATTRIBUTE => 'country'), Horde_ActiveSync::AIRSYNCBASE_DISPLAYNAME => array(self::KEY_ATTRIBUTE => 'displayname'), Horde_ActiveSync::AIRSYNCBASE_LATITUDE => array(self::KEY_ATTRIBUTE => 'latitude'), Horde_ActiveSync::AIRSYNCBASE_LOCATIONURI => array(self::KEY_ATTRIBUTE => 'locationuri'), From dea8bae0b923125896efa691353ae9fe5de0201e Mon Sep 17 00:00:00 2001 From: Michael J Rubinsky Date: Wed, 31 Aug 2016 17:05:25 -0400 Subject: [PATCH 15/15] Move more output responsibility to the exporter object. --- .../ActiveSync/Connector/Exporter/Sync.php | 198 ++++++++++++++++-- .../lib/Horde/ActiveSync/Request/Sync.php | 105 +++------- 2 files changed, 206 insertions(+), 97 deletions(-) diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php index f303eac01b1..99c6a6b3b18 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Exporter/Sync.php @@ -152,6 +152,11 @@ public function messageMove($message) { } + /** + * Send the SYNC_ADD response for any items added from the client. + * + * @param array $collection The collection array. + */ public function syncAddResponse($collection) { foreach ($collection['clientids'] as $clientid => $serverid) { @@ -198,6 +203,180 @@ public function syncAddResponse($collection) } } + /** + * Send the SYNC_MODIFY response for each modified item that requires this + * response. @see self::syncAddResponse() + * + * @param array $collection The collection array. + */ + public function syncModifiedResponse($collection) + { + foreach ($collection['modifiedids'] as $serverid) { + // Start SYNC_MODIFY + $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); + + // EAS 16? + $this->_sendEas16MessageResponse($serverid, $collection); + + // SYNC_STATUS + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content(Horde_ActiveSync_Request_Sync::STATUS_SUCCESS); + $this->_encoder->endTag(); + + // End SYNC_MODIFY + $this->_encoder->endTag(); + } + } + + /** + * Send the SYNC_MODIFY response for any items modified from the client + * that require this response. Basically any items that have + * AirSyncBaseAttachments changes. + * + * @param array $collection The collection array. + */ + public function modifyFailures($collection) + { + foreach ($collection['importfailures'] as $id => $reason) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); + + $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); + $this->_encoder->content($collection['class']); + $this->_encoder->endTag(); + + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($id); + $this->_encoder->endTag(); + + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content($reason); + $this->_encoder->endTag(); + + $this->_encoder->endTag(); + } + } + + /** + * Send the SYNC_REMOVE response for any items deleted from the client but + * were unable to be removed on the server. Usually due to not being found. + * + * @param array $collection The collection array. + */ + public function missingRemove($collection) + { + foreach ($collection['missing'] as $uid) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_REMOVE); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_CLIENTENTRYID); + $this->_encoder->content($uid); + $this->_encoder->endTag(); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content(Horde_ActiveSync_Request_Sync::STATUS_NOTFOUND); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } + } + + /** + * Send the SYNC_FETCH response for any items requested by the client. + * + * @param array $collection The collection array. + */ + public function fetchIds($driver, $collection) + { + foreach ($collection['fetchids'] as $fetch_id) { + try { + $data = $driver->fetch($collection['serverid'], $fetch_id, $collection); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_FETCH); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($fetch_id); + $this->_encoder->endTag(); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content(Horde_ActiveSync_Request_Sync::STATUS_SUCCESS); + $this->_encoder->endTag(); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); + $data->encodeStream($this->_encoder); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } catch (Horde_Exception_NotFound $e) { + $this->_logger->err(sprintf( + '[%s] Unable to fetch %s', + getmypid(), + $fetch_id) + ); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_FETCH); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); + $this->_encoder->content($fetch_id); + $this->_encoder->endTag(); + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content(Horde_ActiveSync_Request_Sync::STATUS_NOTFOUND); + $this->_encoder->endTag(); + $this->_encoder->endTag(); + } + } + } + + /** + * Send the SYNC_FOLDERTYPE node if needed. + * + * @param array $collection The collection array. + */ + public function syncFolderType($collection) + { + // Not sent in > 12.0 + if ($this->_device->version <= Horde_ActiveSync::VERSION_TWELVE) { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); + $this->_encoder->content($collection['class']); + $this->_encoder->endTag(); + } + } + + /** + * Send the SYNC_SYNCKEY response appropriate for this response. + * + * @param array $collection The collection array. + */ + public function syncKey($collection) + { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_SYNCKEY); + if (!empty($collection['newsynckey'])) { + $this->_encoder->content($collection['newsynckey']); + } else { + $this->_encoder->content($collection['synckey']); + } + $this->_encoder->endTag(); + } + + /** + * Send the SYNC_FOLDERID value. + * + * @param array $collection The collection array. + */ + public function syncFolderId($collection) + { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERID); + $this->_encoder->content($collection['id']); + $this->_encoder->endTag(); + } + + /** + * Send the SYNC_STATUS value. + * + * @param array $collection The collection array. + */ + public function syncStatus($statusCode) + { + $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); + $this->_encoder->content($statusCode); + $this->_encoder->endTag(); + } + + /** + * Sends the appropriate message object reply within a SYNC_ADD or + * SYNC_MODIFY block in a SYNC_REPLIES block for EAS >= 16 clients. + * + * @param string $serverid The serverid of the message being sent. + * @param array $collection The collection array. + */ protected function _sendEas16MessageResponse($serverid, $collection) { if ($this->_as->device->version >= Horde_ActiveSync::VERSION_SIXTEEN && @@ -224,25 +403,6 @@ protected function _sendEas16MessageResponse($serverid, $collection) } } - public function syncModifiedResponse($collection) - { - foreach ($collection['modifiedids'] as $serverid) { - // Start SYNC_MODIFY - $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); - - // EAS 16? - $this->_sendEas16MessageResponse($serverid, $collection); - - // SYNC_STATUS - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content(Horde_ActiveSync_Request_Sync::STATUS_SUCCESS); - $this->_encoder->endTag(); - - // End SYNC_MODIFY - $this->_encoder->endTag(); - } - } - protected function _getNextChange() { $change = $this->_changes[$this->_step]; diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php index 9f9bffb8a94..3645a5d1b52 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php @@ -336,13 +336,18 @@ protected function _handle() $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); $this->_encoder->content(self::STATUS_SUCCESS); $this->_encoder->endTag(); + + // Start SYNC_FOLDERS $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERS); + // Get the exporter. $exporter = new Horde_ActiveSync_Connector_Exporter_Sync( $this->_activeSync, $this->_encoder ); + // Loop through each collection and send all changes, replies, fetchids + // etc... $cnt_global = 0; $over_window = false; foreach ($this->_collections as $id => $collection) { @@ -448,33 +453,22 @@ protected function _handle() } } + // Start SYNC_FOLDER $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDER); - // Not sent in > 12.0 - if ($this->_device->version <= Horde_ActiveSync::VERSION_TWELVE) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); - $this->_encoder->content($collection['class']); - $this->_encoder->endTag(); - } + //SYNC_FOLDERTYPE + $exporter->syncFolderType($collection); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SYNCKEY); - if (!empty($collection['newsynckey'])) { - $this->_encoder->content($collection['newsynckey']); - } else { - $this->_encoder->content($collection['synckey']); - } - $this->_encoder->endTag(); + // SYNC_KEY + $exporter->syncKey($collection); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERID); - $this->_encoder->content($collection['id']); - $this->_encoder->endTag(); + // SYNC_FOLDERID + $exporter->syncFolderId($collection); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content($statusCode); - $this->_encoder->endTag(); + // SYNC_STATUS + $exporter->syncStatus($statusCode); if ($statusCode == self::STATUS_SUCCESS) { - // Server changes if ($statusCode == self::STATUS_SUCCESS && empty($forceChanges) && @@ -484,6 +478,7 @@ protected function _handle() ? min($collection['windowsize'], $pingSettings['maximumwindowsize']) : $collection['windowsize']; + // MOREAVAILABLE? if (!empty($changecount) && (($changecount > $max_windowsize) || $cnt_global + $changecount > $this->_collections->getDefaultWindowSize())) { $this->_logger->info(sprintf( @@ -493,14 +488,15 @@ protected function _handle() $over_window = ($cnt_global + $changecount > $this->_collections->getDefaultWindowSize()); } + // Send each message now. if (!empty($changecount)) { $exporter->setChanges($this->_collections->getCollectionChanges(false), $collection); + // Start SYNC_COMMANDS $this->_encoder->startTag(Horde_ActiveSync::SYNC_COMMANDS); $cnt_collection = 0; while ($cnt_collection < $max_windowsize && $cnt_global < $this->_collections->getDefaultWindowSize() && $progress = $exporter->sendNextChange()) { - $this->_logger->info(sprintf( '[%s] Peak memory usage after message: %d', $this->_procid, memory_get_peak_usage(true))); @@ -509,6 +505,7 @@ protected function _handle() ++$cnt_global; } } + // End SYNC_COMMANDS $this->_encoder->endTag(); } } @@ -517,27 +514,12 @@ protected function _handle() || !empty($collection['missing']) || !empty($collection['importfailures']) || !empty($collection['modifiedids'])) { + // Start SYNC_REPLIES $this->_encoder->startTag(Horde_ActiveSync::SYNC_REPLIES); // SYNC_MODIFY failures if (!empty($collection['importfailures'])) { - foreach ($collection['importfailures'] as $id => $reason) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_MODIFY); - - $this->_encoder->startTag(Horde_ActiveSync::SYNC_FOLDERTYPE); - $this->_encoder->content($collection['class']); - $this->_encoder->endTag(); - - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($id); - $this->_encoder->endTag(); - - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content($reason); - $this->_encoder->endTag(); - - $this->_encoder->endTag(); - } + $exporter->modifyFailures($collection); } // EAS 16. CHANGED responses for items that need one. This @@ -555,51 +537,14 @@ protected function _handle() // Errors from missing messages in REMOVE requests. if (!empty($collection['missing'])) { - foreach ($collection['missing'] as $uid) { - $this->_encoder->startTag(Horde_ActiveSync::SYNC_REMOVE); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_CLIENTENTRYID); - $this->_encoder->content($uid); - $this->_encoder->endTag(); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content(self::STATUS_NOTFOUND); - $this->_encoder->endTag(); - $this->_encoder->endTag(); - } + $exporter->missingRemove($collection); } if (!empty($collection['fetchids'])) { - foreach ($collection['fetchids'] as $fetch_id) { - try { - $data = $this->_driver->fetch($collection['serverid'], $fetch_id, $collection); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_FETCH); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($fetch_id); - $this->_encoder->endTag(); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content(self::STATUS_SUCCESS); - $this->_encoder->endTag(); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_DATA); - $data->encodeStream($this->_encoder); - $this->_encoder->endTag(); - $this->_encoder->endTag(); - } catch (Horde_Exception_NotFound $e) { - $this->_logger->err(sprintf( - '[%s] Unable to fetch %s', - $this->_procid, - $fetch_id) - ); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_FETCH); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_SERVERENTRYID); - $this->_encoder->content($fetch_id); - $this->_encoder->endTag(); - $this->_encoder->startTag(Horde_ActiveSync::SYNC_STATUS); - $this->_encoder->content(self::STATUS_NOTFOUND); - $this->_encoder->endTag(); - $this->_encoder->endTag(); - } - } + $exporter->fetchIds($this->_driver, $collection); } + // End SYNC_REPLIES $this->_encoder->endTag(); } @@ -626,13 +571,17 @@ protected function _handle() } } + // End SYNC_FOLDER $this->_encoder->endTag(); $this->_logger->info(sprintf( '[%s] Collection output peak memory usage: %d', $this->_procid, memory_get_peak_usage(true))); } + // End SYNC_FOLDERS $this->_encoder->endTag(); + + // End SYNC_SYNCHRONIZE $this->_encoder->endTag(); if ($this->_device->version >= Horde_ActiveSync::VERSION_TWELVEONE) {