Skip to content

Commit

Permalink
Support for EAS 16 style of handling exceptions/recurrence changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
mrubinsk committed Jan 21, 2016
1 parent d0fc7d0 commit a49f4dd
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 25 deletions.
80 changes: 63 additions & 17 deletions framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php
Expand Up @@ -132,8 +132,10 @@ public function setLogger($logger)
* @param Horde_ActiveSync_Device $device A device descriptor
* @param integer $clientid Client id sent from client.
* on message addition.
* @param string $class The collection class (only needed for SMS).
* @since 2.6.0
* @param string $class The collection class (only needed for SMS).
* @since 2.6.0
* @param string $synckey The synckey currently being processed.
* @since 2.31.0
*
* @todo Revisit passing $class for SMS. Probably pass class in the
* const'r.
Expand All @@ -143,7 +145,8 @@ public function setLogger($logger)
*/
public function importMessageChange(
$id, Horde_ActiveSync_Message_Base $message,
Horde_ActiveSync_Device $device, $clientid, $class = null)
Horde_ActiveSync_Device $device, $clientid, $class = false,
$synckey = false)
{
// Don't support SMS, but can't tell client that. Send back a phoney
// UID for any imported SMS objects.
Expand All @@ -152,17 +155,35 @@ public function importMessageChange(
}

// Changing an existing object
if ($id && $this->_flags == Horde_ActiveSync::CONFLICT_OVERWRITE_PIM) {
$conflict = $this->_isConflict(
Horde_ActiveSync::CHANGE_TYPE_CHANGE,
$this->_folderId,
$id);
if ($conflict) {
$this->_logger->notice(sprintf(
'[%s] Conflict when updating %s, will overwrite client version on next sync.',
$this->_procid, $id)
);
return array($id, Horde_ActiveSync_Request_Sync::STATUS_CONFLICT);
if ($id && $synckey &&
($message instanceof Horde_ActiveSync_Message_Appointment) &&
$this->_flags == Horde_ActiveSync::CONFLICT_OVERWRITE_PIM) {

if ($this->_state->isDuplicatePIMChange($id, $synckey)) {}

} elseif ($id && $this->_flags == Horde_ActiveSync::CONFLICT_OVERWRITE_PIM) {
// This is complicated by the fact that in EAS 16.0, clients
// will send a CHANGE for adding/editing an exception along with
// a seperate change with the entire appointment - even if nothing
// else has changed. This leads to conficts (since the appointment
// is marked as changed after the first edit). Sniff that out here
// and prevent the conflict check for those messages.
if (!$synckey ||
!(($message instanceof Horde_ActiveSync_Message_Appointment) &&
$this->_state->isDuplicatePIMChange($id, $synckey))) {

$conflict = $this->_isConflict(
Horde_ActiveSync::CHANGE_TYPE_CHANGE,
$this->_folderId,
$id
);
if ($conflict) {
$this->_logger->notice(sprintf(
'[%s] Conflict when updating %s, will overwrite client version on next sync.',
$this->_procid, $id)
);
return array($id, Horde_ActiveSync_Request_Sync::STATUS_CONFLICT);
}
}
} elseif (!$id && $uid = $this->_state->isDuplicatePIMAddition($clientid)) {
// Already saw this addition, but client never received UID
Expand Down Expand Up @@ -208,18 +229,43 @@ public function importMessageChange(
* Import message deletions. This may conflict if the local object has been
* modified.
*
* @param array $ids Server message uids to delete
* @param string $class The server collection class.
* @param array $ids Server message uids to delete
* @param string $class The server collection class.
* @param boolean $instanceids If true, $ids is a hash of
* instanceids => uids. @since 2.31.0
*
* @return array An array containing ids of successfully deleted messages.
*/
public function importMessageDeletion(array $ids, $class)
public function importMessageDeletion(array $ids, $class, $instanceids = false)
{
// Don't support SMS, but can't tell client that.
if ($class == Horde_ActiveSync::CLASS_SMS) {
return array();
}

if ($instanceids) {
foreach ($ids as $uid => $iid) {
$mod = $this->_as->driver->getSyncStamp($this->_folderId);
$this->_as->driver->deleteMessage($this->_folderId, array($uid => $iid), true);
$change = array(
'id' => $uid,
'mod' => $mod,
'serverid' => $this->_folderId
);
// Log this as a change in the state, not a deletion because
// these are actually instances of recurring series being
// deleted, not the entire item being deleted.
$this->state->updateState(
Horde_ActiveSync::CHANGE_TYPE_CHANGE,
$change,
Horde_ActiveSync::CHANGE_ORIGIN_PIM,
$this->_as->driver->getUser()
);
}

return $ids;
}

// Ask the backend to delete the message.
$mod = $this->_as->driver->getSyncStamp($this->_folderId);
$ids = $this->_as->driver->deleteMessage($this->_folderId, $ids);
Expand Down
Expand Up @@ -256,11 +256,13 @@ public function __construct(array $options = array())
if ($this->_version >= Horde_ActiveSync::VERSION_SIXTEEN) {
$this->_mapping += array(
Horde_ActiveSync::AIRSYNCBASE_LOCATION => array(self::KEY_ATTRIBUTE => 'location'),
self::POOMCAL_CLIENTUID => array(self::KEY_ATTRIBUTE => 'clientuid')
self::POOMCAL_CLIENTUID => array(self::KEY_ATTRIBUTE => 'clientuid'),
Horde_ActiveSync::AIRSYNCBASE_INSTANCEID => array(self::KEY_ATTRIBUTE => 'instanceid', self::KEY_TYPE => self::TYPE_DATE),
);
$this->_properties += array(
'location' => false,
'clientuid' => false,
'instanceid' => false,
);
}
}
Expand Down
Expand Up @@ -47,10 +47,11 @@
* @property integer $alldayevent
* @property integer $reminder
* @property integer $meetingstatus
* @property Horde_Date $exceptionstarttime (EAS <= 14.1 Only).
* @property Horde_Date $exceptionstarttime (EAS <= 14.1 only).
* @property integer $deleted
* @property array $attendees
* @property array $categories
* @property Horde_Date $instanceid (EAS >= 16.0 only).
*/
class Horde_ActiveSync_Message_Exception extends Horde_ActiveSync_Message_Appointment
{
Expand Down Expand Up @@ -122,10 +123,12 @@ public function __construct(array $options = array())
}
if ($this->_version >= Horde_ActiveSync::VERSION_SIXTEEN) {
$this->_mapping += array(
Horde_ActiveSync::AIRSYNCBASE_LOCATION => array(self::KEY_ATTRIBUTE => 'location')
Horde_ActiveSync::AIRSYNCBASE_LOCATION => array(self::KEY_ATTRIBUTE => 'location'),
Horde_ActiveSync::AIRSYNCBASE_INSTANCEID => array(self::KEY_ATTRIBUTE => 'instanceid', self::KEY_TYPE => self::TYPE_DATE)
);
$this->_properties += array(
'location' => false
'location' => false,
'instanceid' => false,
);
}
}
Expand Down
35 changes: 31 additions & 4 deletions framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php
Expand Up @@ -961,7 +961,7 @@ protected function _parseSyncCommands(&$collection)
}
$nchanges++;
$commandType = $element[Horde_ActiveSync_Wbxml::EN_TAG];

$instanceid = false;
// Only sent during SYNC_MODIFY/SYNC_REMOVE/SYNC_FETCH
if (($commandType == Horde_ActiveSync::SYNC_MODIFY ||
$commandType == Horde_ActiveSync::SYNC_REMOVE ||
Expand All @@ -977,6 +977,18 @@ protected function _parseSyncCommands(&$collection)
$this->_logger->err('Parsing Error - expecting </SYNC_SERVERENTRYID>');
return false;
}

if ($this->_activeSync->device->version >= Horde_ActiveSync::VERSION_SIXTEEN) {
if ($this->_decoder->getElementStartTag(Horde_ActiveSync::AIRSYNCBASE_INSTANCEID)) {
$instanceid = $this->_decoder->getElementContent();
if ($instanceid !== false && !$this->_decoder->getElementEndTag()) {
$this->_statusCode = self::STATUS_PROTERROR;
$this->_handleGlobalSyncError();
$this->_logger->err('Parsing Error - expecting </AIRSYNCBASE_INSTANCEID>');
return false;
}
}
}
} else {
$serverid = false;
}
Expand Down Expand Up @@ -1028,6 +1040,11 @@ protected function _parseSyncCommands(&$collection)
case Horde_ActiveSync::CLASS_CALENDAR:
$appdata = Horde_ActiveSync::messageFactory('Appointment');
$appdata->decodeStream($this->_decoder);
if (!empty($instanceid) &&
$commandType == Horde_ActiveSync::SYNC_MODIFY) {
// EAS 16.0 sends instanceid/serverid for exceptions.
$appdata->instanceid = $instanceid;
}
break;
case Horde_ActiveSync::CLASS_TASKS:
$appdata = Horde_ActiveSync::messageFactory('Task');
Expand Down Expand Up @@ -1057,7 +1074,8 @@ protected function _parseSyncCommands(&$collection)
case Horde_ActiveSync::SYNC_MODIFY:
if (isset($appdata)) {
$id = $importer->importMessageChange(
$serverid, $appdata, $this->_device, false);
$serverid, $appdata, $this->_device, false, false,
$collection['synckey']);
if ($id && !is_array($id)) {
$collection['importedchanges'] = true;
} elseif (is_array($id)) {
Expand All @@ -1080,8 +1098,10 @@ protected function _parseSyncCommands(&$collection)
break;

case Horde_ActiveSync::SYNC_REMOVE:
// Work around broken clients that send empty $serverid.
if ($serverid) {
if ($instanceid) {
$collection['instanceid_removes'][$serverid] = $instanceid;
} elseif ($serverid) {
// Work around broken clients that send empty $serverid.
$collection['removes'][] = $serverid;
}
break;
Expand Down Expand Up @@ -1118,6 +1138,13 @@ protected function _parseSyncCommands(&$collection)
unset($collection['removes']);
$collection['importedchanges'] = true;
}
// EAS 16.0 instance deletions.
if (!empty($collection['instanceid_removes']) && !empty($collection['synckey'])) {
foreach ($collection['instanceid_removes'] as $uid => $instanceid) {
$importer->importMessageDeletion(array($uid => $instanceid), $collection['class'], true);
}
unset($collection['instanceid_removes']);
}

$this->_logger->info(sprintf(
'[%s] Processed %d incoming changes',
Expand Down
20 changes: 20 additions & 0 deletions framework/ActiveSync/lib/Horde/ActiveSync/State/Sql.php
Expand Up @@ -1041,6 +1041,26 @@ public function isDuplicatePIMAddition($id)
}
}

/**
* Check if the UID provided was altered during the SYNC_KEY provided.
*
* @param string $uid The UID to check.
* @param string $synckey The synckey to check.
*
* @return boolean True if the provided UID was updated during the
* SYNC for the synckey provided.
* @since 2.31.0
*/
public function isDuplicatePIMChange($uid, $synckey)
{
$sql = 'SELECT count(*) FROM ' . $this->_syncMapTable
. ' WHERE message_uid = ? AND sync_user = ? AND sync_key = ?';
try {
return $this->_db->selectValue($sql, array($uid, $this->_deviceInfo->user, $synckey));
} catch (Horde_Db_Exception $e) {
throw new Horde_ActiveSync_Exception($e);
}
}
/**
* Return the sync cache.
*
Expand Down

0 comments on commit a49f4dd

Please sign in to comment.