Page MenuHomePhorge

No OneTemporary

Size
88 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/ext/Syncroton/Command/Ping.php b/lib/ext/Syncroton/Command/Ping.php
index a2bd926..2e71fd2 100644
--- a/lib/ext/Syncroton/Command/Ping.php
+++ b/lib/ext/Syncroton/Command/Ping.php
@@ -1,233 +1,237 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Ping command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
{
const STATUS_NO_CHANGES_FOUND = 1;
const STATUS_CHANGES_FOUND = 2;
const STATUS_MISSING_PARAMETERS = 3;
const STATUS_REQUEST_FORMAT_ERROR = 4;
const STATUS_INTERVAL_TO_GREAT_OR_SMALL = 5;
const STATUS_TO_MUCH_FOLDERS = 6;
const STATUS_FOLDER_NOT_FOUND = 7;
const STATUS_GENERAL_ERROR = 8;
protected $_skipValidatePolicyKey = true;
protected $_changesDetected = false;
/**
* @var Syncroton_Backend_StandAlone_Abstract
*/
protected $_dataBackend;
protected $_defaultNameSpace = 'uri:Ping';
protected $_documentElement = 'Ping';
protected $_foldersWithChanges = array();
/**
* process the XML file and add, change, delete or fetches data
*
* @todo can we get rid of LIBXML_NOWARNING
* @todo we need to stored the initial data for folders and lifetime as the phone is sending them only when they change
* @return resource
*/
public function handle()
{
$intervalStart = time();
$status = self::STATUS_NO_CHANGES_FOUND;
// the client does not send a wbxml document, if the Ping parameters did not change compared with the last request
if ($this->_requestBody instanceof DOMDocument) {
$xml = simplexml_import_dom($this->_requestBody);
$xml->registerXPathNamespace('Ping', 'Ping');
if(isset($xml->HeartbeatInterval)) {
$this->_device->pinglifetime = (int)$xml->HeartbeatInterval;
}
if (isset($xml->Folders->Folder)) {
$folders = array();
foreach ($xml->Folders->Folder as $folderXml) {
try {
// does the folder exist?
$folder = $this->_folderBackend->getFolder($this->_device, (string)$folderXml->Id);
$folders[$folder->id] = $folder;
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage());
$status = self::STATUS_FOLDER_NOT_FOUND;
break;
}
}
$this->_device->pingfolder = serialize(array_keys($folders));
}
}
$this->_device->lastping = new DateTime('now', new DateTimeZone('utc'));
if ($status == self::STATUS_NO_CHANGES_FOUND) {
$this->_device = $this->_deviceBackend->update($this->_device);
}
$lifeTime = $this->_device->pinglifetime;
- $maxLifeTime = Syncroton_Registry::getPingInterval();
+ $maxInterval = Syncroton_Registry::getPingInterval();
- if ($maxLifeTime > 0 && $lifeTime > $maxLifeTime) {
+ if ($maxInterval <= 0 || $maxInterval > Syncroton_Server::MAX_HEARTBEAT_INTERVAL) {
+ $maxInterval = Syncroton_Server::MAX_HEARTBEAT_INTERVAL;
+ }
+
+ if ($lifeTime > $maxInterval) {
$ping = $this->_outputDom->documentElement;
$ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', self::STATUS_INTERVAL_TO_GREAT_OR_SMALL));
- $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'HeartbeatInterval', $maxLifeTime));
+ $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'HeartbeatInterval', $maxInterval));
return;
}
$intervalEnd = $intervalStart + $lifeTime;
$secondsLeft = $intervalEnd;
$folders = unserialize($this->_device->pingfolder);
if ($status === self::STATUS_NO_CHANGES_FOUND && (!is_array($folders) || count($folders) == 0)) {
$status = self::STATUS_MISSING_PARAMETERS;
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folders to monitor($lifeTime / $intervalStart / $intervalEnd / $status): " . print_r($folders, true));
if ($status === self::STATUS_NO_CHANGES_FOUND) {
do {
// take a break to save battery lifetime
sleep(Syncroton_Registry::getPingTimeout());
try {
$device = $this->_deviceBackend->get($this->_device->id);
} catch (Syncroton_Exception_NotFound $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
$status = self::STATUS_FOLDER_NOT_FOUND;
break;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->err(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
// do nothing, maybe temporal issue, should we stop?
continue;
}
// if another Ping command updated lastping property, we can stop processing this Ping command request
if ((isset($device->lastping) && $device->lastping instanceof DateTime) &&
$device->pingfolder === $this->_device->pingfolder &&
$device->lastping->getTimestamp() > $this->_device->lastping->getTimestamp() ) {
break;
}
$now = new DateTime('now', new DateTimeZone('utc'));
foreach ($folders as $folderId) {
try {
$folder = $this->_folderBackend->get($folderId);
$dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
} catch (Syncroton_Exception_NotFound $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
$status = self::STATUS_FOLDER_NOT_FOUND;
break;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->err(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
// do nothing, maybe temporal issue, should we stop?
continue;
}
try {
$syncState = $this->_syncStateBackend->getSyncState($this->_device, $folder);
// another process synchronized data of this folder already. let's skip it
if ($syncState->lastsync > $this->_syncTimeStamp) {
continue;
}
// safe battery time by skipping folders which got synchronied less than Syncroton_Registry::getQuietTime() seconds ago
if (($now->getTimestamp() - $syncState->lastsync->getTimestamp()) < Syncroton_Registry::getQuietTime()) {
continue;
}
$foundChanges = $dataController->hasChanges($this->_contentStateBackend, $folder, $syncState);
} catch (Syncroton_Exception_NotFound $e) {
// folder got never synchronized to client
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' syncstate not found. enforce sync for folder: ' . $folder->serverId);
$foundChanges = true;
}
if ($foundChanges == true) {
$this->_foldersWithChanges[] = $folder;
$status = self::STATUS_CHANGES_FOUND;
}
}
if ($status != self::STATUS_NO_CHANGES_FOUND) {
break;
}
$secondsLeft = $intervalEnd - time();
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " seconds left: " . $secondsLeft);
// See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
//
// break if there are less than PingTimeout + 10 seconds left for the next loop
// otherwise the response will be returned after the client has finished his Ping
// request already maybe
} while ($secondsLeft > (Syncroton_Registry::getPingTimeout() + 10));
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " Lifetime: $lifeTime SecondsLeft: $secondsLeft Status: $status)");
$ping = $this->_outputDom->documentElement;
$ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', $status));
if($status === self::STATUS_CHANGES_FOUND) {
$folders = $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Folders'));
foreach($this->_foldersWithChanges as $changedFolder) {
$folder = $folders->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Folder', $changedFolder->serverId));
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " changes in folder: " . $changedFolder->serverId);
}
}
}
/**
* generate ping command response
*
*/
public function getResponse()
{
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/Sync.php b/lib/ext/Syncroton/Command/Sync.php
index 3a3bc26..e528999 100644
--- a/lib/ext/Syncroton/Command/Sync.php
+++ b/lib/ext/Syncroton/Command/Sync.php
@@ -1,1108 +1,1121 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
{
const STATUS_SUCCESS = 1;
const STATUS_PROTOCOL_VERSION_MISMATCH = 2;
const STATUS_INVALID_SYNC_KEY = 3;
const STATUS_PROTOCOL_ERROR = 4;
const STATUS_SERVER_ERROR = 5;
const STATUS_ERROR_IN_CLIENT_SERVER_CONVERSION = 6;
const STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT = 7;
const STATUS_OBJECT_NOT_FOUND = 8;
const STATUS_USER_ACCOUNT_MAYBE_OUT_OF_DISK_SPACE = 9;
const STATUS_ERROR_SETTING_NOTIFICATION_GUID = 10;
const STATUS_DEVICE_NOT_PROVISIONED_FOR_NOTIFICATIONS = 11;
const STATUS_FOLDER_HIERARCHY_HAS_CHANGED = 12;
const STATUS_RESEND_FULL_XML = 13;
const STATUS_WAIT_INTERVAL_OUT_OF_RANGE = 14;
const CONFLICT_OVERWRITE_SERVER = 0;
const CONFLICT_OVERWRITE_PIM = 1;
const MIMESUPPORT_DONT_SEND_MIME = 0;
const MIMESUPPORT_SMIME_ONLY = 1;
const MIMESUPPORT_SEND_MIME = 2;
const BODY_TYPE_PLAIN_TEXT = 1;
const BODY_TYPE_HTML = 2;
const BODY_TYPE_RTF = 3;
const BODY_TYPE_MIME = 4;
/**
* truncate types
*/
const TRUNCATE_ALL = 0;
const TRUNCATE_4096 = 1;
const TRUNCATE_5120 = 2;
const TRUNCATE_7168 = 3;
const TRUNCATE_10240 = 4;
const TRUNCATE_20480 = 5;
const TRUNCATE_51200 = 6;
const TRUNCATE_102400 = 7;
const TRUNCATE_NOTHING = 8;
/**
* filter types
*/
const FILTER_NOTHING = 0;
const FILTER_1_DAY_BACK = 1;
const FILTER_3_DAYS_BACK = 2;
const FILTER_1_WEEK_BACK = 3;
const FILTER_2_WEEKS_BACK = 4;
const FILTER_1_MONTH_BACK = 5;
const FILTER_3_MONTHS_BACK = 6;
const FILTER_6_MONTHS_BACK = 7;
const FILTER_INCOMPLETE = 8;
protected $_defaultNameSpace = 'uri:AirSync';
protected $_documentElement = 'Sync';
/**
* list of collections
*
* @var array
*/
protected $_collections = array();
protected $_modifications = array();
/**
* the global WindowSize
*
* @var integer
*/
protected $_globalWindowSize;
/**
* there are more entries than WindowSize available
* the MoreAvailable tag hot added to the xml output
*
* @var boolean
*/
protected $_moreAvailable = false;
/**
* @var Syncroton_Model_SyncState
*/
protected $_syncState;
protected $_maxWindowSize = 100;
protected $_heartbeatInterval = null;
/**
* process the XML file and add, change, delete or fetches data
*/
public function handle()
{
// input xml
$requestXML = simplexml_import_dom($this->_mergeSyncRequest($this->_requestBody, $this->_device));
if (! isset($requestXML->Collections)) {
$this->_outputDom->documentElement->appendChild(
$this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_RESEND_FULL_XML)
);
return $this->_outputDom;
}
-
if (isset($requestXML->HeartbeatInterval)) {
+ $intervalDiv = 1;
$this->_heartbeatInterval = (int)$requestXML->HeartbeatInterval;
- } elseif (isset($requestXML->Wait)) {
- $this->_heartbeatInterval = (int)$requestXML->Wait * 60;
+ } else if (isset($requestXML->Wait)) {
+ $intervalDiv = 60;
+ $this->_heartbeatInterval = (int)$requestXML->Wait * $intervalDiv;
+ }
+
+ $maxInterval = Syncroton_Registry::getPingInterval();
+ if ($maxInterval <= 0 || $maxInterval > Syncroton_Server::MAX_HEARTBEAT_INTERVAL) {
+ $maxInterval = Syncroton_Server::MAX_HEARTBEAT_INTERVAL;
+ }
+
+ if ($this->_heartbeatInterval && $this->_heartbeatInterval > $maxInterval) {
+ $sync = $this->_outputDom->documentElement;
+ $sync->appendChild($this->_outputDom->createElementNS('uri:Sync', 'Status', self::STATUS_WAIT_INTERVAL_OUT_OF_RANGE));
+ $sync->appendChild($this->_outputDom->createElementNS('uri:Sync', 'Limit', floor($maxInterval/$intervalDiv)));
+ $this->_heartbeatInterval = null;
}
$this->_globalWindowSize = isset($requestXML->WindowSize) ? (int)$requestXML->WindowSize : 100;
if (!$this->_globalWindowSize || $this->_globalWindowSize > 512) {
$this->_globalWindowSize = 512;
}
if ($this->_globalWindowSize > $this->_maxWindowSize) {
$this->_globalWindowSize = $this->_maxWindowSize;
}
// load options from lastsynccollection
$lastSyncCollection = array('options' => array());
if (!empty($this->_device->lastsynccollection)) {
$lastSyncCollection = Zend_Json::decode($this->_device->lastsynccollection);
if (!array_key_exists('options', $lastSyncCollection) || !is_array($lastSyncCollection['options'])) {
$lastSyncCollection['options'] = array();
}
}
$collections = array();
foreach ($requestXML->Collections->Collection as $xmlCollection) {
$collectionId = (string)$xmlCollection->CollectionId;
$collections[$collectionId] = new Syncroton_Model_SyncCollection($xmlCollection);
// do we have to reuse the options from the previous request?
if (!isset($xmlCollection->Options) && array_key_exists($collectionId, $lastSyncCollection['options'])) {
$collections[$collectionId]->options = $lastSyncCollection['options'][$collectionId];
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " restored options to " . print_r($collections[$collectionId]->options, TRUE));
}
// store current options for next Sync command request (sticky options)
$lastSyncCollection['options'][$collectionId] = $collections[$collectionId]->options;
}
$this->_device->lastsynccollection = Zend_Json::encode($lastSyncCollection);
if ($this->_device->isDirty()) {
Syncroton_Registry::getDeviceBackend()->update($this->_device);
}
foreach ($collections as $collectionData) {
// has the folder been synchronised to the device already
try {
$collectionData->folder = $this->_folderBackend->getFolder($this->_device, $collectionData->collectionId);
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData->collectionId} not found");
// trigger INVALID_SYNCKEY instead of OBJECT_NOTFOUND when synckey is higher than 0
// to avoid a syncloop for the iPhone
if ($collectionData->syncKey > 0) {
$collectionData->folder = new Syncroton_Model_Folder(array(
'deviceId' => $this->_device,
'serverId' => $collectionData->collectionId
));
}
$this->_collections[$collectionData->collectionId] = $collectionData;
continue;
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData->syncKey} Class: {$collectionData->folder->class} CollectionId: {$collectionData->collectionId}");
// initial synckey
if($collectionData->syncKey === 0) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
// reset sync state for this folder
$this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
$this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
$collectionData->syncState = new Syncroton_Model_SyncState(array(
'device_id' => $this->_device,
'counter' => 0,
'type' => $collectionData->folder,
'lastsync' => $this->_syncTimeStamp
));
$this->_collections[$collectionData->collectionId] = $collectionData;
continue;
}
// check for invalid sycnkey
if(($collectionData->syncState = $this->_syncStateBackend->validate($this->_device, $collectionData->folder, $collectionData->syncKey)) === false) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData->syncKey} provided");
// reset sync state for this folder
$this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
$this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
$this->_collections[$collectionData->collectionId] = $collectionData;
continue;
}
$dataController = Syncroton_Data_Factory::factory($collectionData->folder->class, $this->_device, $this->_syncTimeStamp);
switch($collectionData->folder->class) {
case Syncroton_Data_Factory::CLASS_CALENDAR:
$dataClass = 'Syncroton_Model_Event';
break;
case Syncroton_Data_Factory::CLASS_CONTACTS:
$dataClass = 'Syncroton_Model_Contact';
break;
case Syncroton_Data_Factory::CLASS_EMAIL:
$dataClass = 'Syncroton_Model_Email';
break;
case Syncroton_Data_Factory::CLASS_NOTES:
$dataClass = 'Syncroton_Model_Note';
break;
case Syncroton_Data_Factory::CLASS_TASKS:
$dataClass = 'Syncroton_Model_Task';
break;
default:
throw new Syncroton_Exception_UnexpectedValue('invalid class provided');
break;
}
$clientModifications = array(
'added' => array(),
'changed' => array(),
'deleted' => array(),
'forceAdd' => array(),
'forceChange' => array(),
'toBeFetched' => array(),
);
// handle incoming data
if($collectionData->hasClientAdds()) {
$adds = $collectionData->getClientAdds();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
foreach ($adds as $add) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
try {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " adding entry as new");
$serverId = $dataController->createEntry($collectionData->collectionId, new $dataClass($add->ApplicationData));
$clientModifications['added'][$serverId] = array(
'clientId' => (string)$add->ClientId,
'serverId' => $serverId,
'status' => self::STATUS_SUCCESS,
'contentState' => $this->_contentStateBackend->create(new Syncroton_Model_Content(array(
'device_id' => $this->_device,
'folder_id' => $collectionData->folder,
'contentid' => $serverId,
'creation_time' => $this->_syncTimeStamp,
'creation_synckey' => $collectionData->syncKey + 1
)))
);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
$clientModifications['added'][] = array(
'clientId' => (string)$add->ClientId,
'status' => self::STATUS_SERVER_ERROR
);
}
}
}
// handle changes, but only if not first sync
if($collectionData->syncKey > 1 && $collectionData->hasClientChanges()) {
$changes = $collectionData->getClientChanges();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
foreach ($changes as $change) {
$serverId = (string)$change->ServerId;
try {
$dataController->updateEntry($collectionData->collectionId, $serverId, new $dataClass($change->ApplicationData));
$clientModifications['changed'][$serverId] = self::STATUS_SUCCESS;
} catch (Syncroton_Exception_AccessDenied $e) {
$clientModifications['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
$clientModifications['forceChange'][$serverId] = $serverId;
} catch (Syncroton_Exception_NotFound $e) {
// entry does not exist anymore, will get deleted automaticaly
$clientModifications['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
// something went wrong while trying to update the entry
$clientModifications['changed'][$serverId] = self::STATUS_SERVER_ERROR;
}
}
}
// handle deletes, but only if not first sync
if($collectionData->hasClientDeletes()) {
$deletes = $collectionData->getClientDeletes();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
foreach ($deletes as $delete) {
$serverId = (string)$delete->ServerId;
try {
// check if we have sent this entry to the phone
$state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
try {
$dataController->deleteEntry($collectionData->collectionId, $serverId, $collectionData);
} catch(Syncroton_Exception_NotFound $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
} catch (Syncroton_Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
$clientModifications['forceAdd'][$serverId] = $serverId;
}
$this->_contentStateBackend->delete($state);
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
// should we send a special status???
//$collectionData->deleted[$serverId] = self::STATUS_SUCCESS;
}
$clientModifications['deleted'][$serverId] = self::STATUS_SUCCESS;
}
}
// handle fetches, but only if not first sync
if($collectionData->syncKey > 1 && $collectionData->hasClientFetches()) {
// the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
// some prevoius versions of iOS did not set GetChanges to 0 for fetches. Let's enforce getChanges to false here.
$collectionData->getChanges = false;
$fetches = $collectionData->getClientFetches();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
$toBeFecthed = array();
foreach ($fetches as $fetch) {
$serverId = (string)$fetch->ServerId;
$toBeFetched[$serverId] = $serverId;
}
$collectionData->toBeFetched = $toBeFetched;
}
$this->_collections[$collectionData->collectionId] = $collectionData;
$this->_modifications[$collectionData->collectionId] = $clientModifications;
}
}
/**
* (non-PHPdoc)
* @see Syncroton_Command_Wbxml::getResponse()
*/
public function getResponse()
{
$sync = $this->_outputDom->documentElement;
$collections = $this->_outputDom->createElementNS('uri:AirSync', 'Collections');
$totalChanges = 0;
// Detect devices that do not support empty Sync reponse
$emptySyncSupported = !preg_match('/(meego|nokian800)/i', $this->_device->useragent);
// continue only if there are changes or no time is left
if ($this->_heartbeatInterval > 0) {
$intervalStart = time();
do {
// take a break to save battery lifetime
sleep(Syncroton_Registry::getPingTimeout());
$now = new DateTime(null, new DateTimeZone('utc'));
foreach($this->_collections as $collectionData) {
// continue immediately if folder does not exist
if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) {
break 2;
// countinue immediately if syncstate is invalid
} elseif (! ($collectionData->syncState instanceof Syncroton_Model_ISyncState)) {
break 2;
} else {
if ($collectionData->getChanges !== true) {
continue;
}
try {
// just check if the folder still exists
$this->_folderBackend->get($collectionData->folder);
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " collection does not exist anymore: " . $collectionData->collectionId);
$collectionData->getChanges = false;
// make sure this is the last while loop
// no break 2 here, as we like to check the other folders too
$intervalStart -= $this->_heartbeatInterval;
}
// check that the syncstate still exists and is still valid
try {
$syncState = $this->_syncStateBackend->getSyncState($this->_device, $collectionData->folder);
// another process synchronized data of this folder already. let's skip it
if ($syncState->id !== $collectionData->syncState->id) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " syncstate changed during heartbeat interval for collection: " . $collectionData->folder->serverId);
$collectionData->getChanges = false;
// make sure this is the last while loop
// no break 2 here, as we like to check the other folders too
$intervalStart -= $this->_heartbeatInterval;
}
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " no syncstate found anymore for collection: " . $collectionData->folder->serverId);
$collectionData->syncState = null;
// make sure this is the last while loop
// no break 2 here, as we like to check the other folders too
$intervalStart -= $this->_heartbeatInterval;
}
// safe battery time by skipping folders which got synchronied less than Syncroton_Command_Ping::$quietTime seconds ago
if ( ! $collectionData->syncState instanceof Syncroton_Model_SyncState ||
($now->getTimestamp() - $collectionData->syncState->lastsync->getTimestamp()) < Syncroton_Registry::getQuietTime()) {
continue;
}
$dataController = Syncroton_Data_Factory::factory($collectionData->folder->class , $this->_device, $this->_syncTimeStamp);
// countinue immediately if there are any changes available
if($dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState)) {
break 2;
}
}
}
// See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
//
// break if there are less than PingTimeout + 10 seconds left for the next loop
// otherwise the response will be returned after the client has finished his Ping
// request already maybe
} while (time() - $intervalStart < $this->_heartbeatInterval - (Syncroton_Registry::getPingTimeout() + 10));
}
foreach($this->_collections as $collectionData) {
$collectionChanges = 0;
/**
* keep track of entries added on server side
*/
$newContentStates = array();
/**
* keep track of entries deleted on server side
*/
$deletedContentStates = array();
// invalid collectionid provided
if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) {
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
// invalid synckey provided
} elseif (! ($collectionData->syncState instanceof Syncroton_Model_ISyncState)) {
// set synckey to 0
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
// initial sync
} elseif ($collectionData->syncState->counter === 0) {
$collectionData->syncState->counter++;
// initial sync
// send back a new SyncKey only
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
if (!empty($collectionData->folder->class)) {
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
}
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData->syncState->counter));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
} else {
$dataController = Syncroton_Data_Factory::factory($collectionData->folder->class , $this->_device, $this->_syncTimeStamp);
$clientModifications = $this->_modifications[$collectionData->collectionId];
$serverModifications = array(
'added' => array(),
'changed' => array(),
'deleted' => array(),
);
if($collectionData->getChanges === true) {
// continue sync session?
if(is_array($collectionData->syncState->pendingdata)) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
$serverModifications = $collectionData->syncState->pendingdata;
} elseif ($dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState)) {
// update _syncTimeStamp as $dataController->hasChanges might have spent some time
$this->_syncTimeStamp = new DateTime(null, new DateTimeZone('utc'));
try {
// fetch entries added since last sync
$allClientEntries = $this->_contentStateBackend->getFolderState(
$this->_device,
$collectionData->folder
);
$allServerEntries = $dataController->getServerEntries(
$collectionData->collectionId,
$collectionData->options['filterType']
);
// add entries
$serverDiff = array_diff($allServerEntries, $allClientEntries);
// add entries which produced problems during delete from client
$serverModifications['added'] = $clientModifications['forceAdd'];
// add entries not yet sent to client
$serverModifications['added'] = array_unique(array_merge($serverModifications['added'], $serverDiff));
// @todo still needed?
foreach($serverModifications['added'] as $id => $serverId) {
// skip entries added by client during this sync session
if(isset($clientModifications['added'][$serverId]) && !isset($clientModifications['forceAdd'][$serverId])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
unset($serverModifications['added'][$id]);
}
}
// entries to be deleted
$serverModifications['deleted'] = array_diff($allClientEntries, $allServerEntries);
// fetch entries changed since last sync
$serverModifications['changed'] = $dataController->getChangedEntries(
$collectionData->collectionId,
$collectionData->syncState->lastsync,
$this->_syncTimeStamp,
$collectionData->options['filterType']
);
$serverModifications['changed'] = array_merge($serverModifications['changed'], $clientModifications['forceChange']);
foreach($serverModifications['changed'] as $id => $serverId) {
// skip entry, if it got changed by client during current sync
if(isset($clientModifications['changed'][$serverId]) && !isset($clientModifications['forceChange'][$serverId])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
unset($serverModifications['changed'][$id]);
}
// skip entry, make sure we don't sent entries already added by client in this request
else if (isset($clientModifications['added'][$serverId]) && !isset($clientModifications['forceAdd'][$serverId])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped change for added entry: " . $serverId);
unset($serverModifications['changed'][$id]);
}
}
// entries comeing in scope are already in $serverModifications['added'] and do not need to
// be send with $serverCanges
$serverModifications['changed'] = array_diff($serverModifications['changed'], $serverModifications['added']);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getTraceAsString());
// Prevent from removing client entries when getServerEntries() fails
// @todo: should we set Status and break the loop here?
$serverModifications = array(
'added' => array(),
'changed' => array(),
'deleted' => array(),
);
}
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverModifications['added']) . '/' . count($serverModifications['changed']) . '/' . count($serverModifications['deleted']) . ' entries for sync from server to client');
}
// collection header
$collection = $this->_outputDom->createElementNS('uri:AirSync', 'Collection');
if (!empty($collectionData->folder->class)) {
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
}
$syncKeyElement = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey'));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
$responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
// send reponse for newly added entries
if(!empty($clientModifications['added'])) {
foreach($clientModifications['added'] as $entryData) {
$add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
// we have no serverId is the add failed
if(isset($entryData['serverId'])) {
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
}
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
}
}
// send reponse for changed entries
if(!empty($clientModifications['changed'])) {
foreach($clientModifications['changed'] as $serverId => $status) {
if ($status !== Syncroton_Command_Sync::STATUS_SUCCESS) {
$change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
$change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
}
}
}
// send response for to be fetched entries
if(!empty($collectionData->toBeFetched)) {
// unset all truncation settings as entries are not allowed to be truncated during fetch
$fetchCollectionData = clone $collectionData;
// unset truncationSize
if (isset($fetchCollectionData->options['bodyPreferences']) && is_array($fetchCollectionData->options['bodyPreferences'])) {
foreach($fetchCollectionData->options['bodyPreferences'] as $key => $bodyPreference) {
unset($fetchCollectionData->options['bodyPreferences'][$key]['truncationSize']);
}
}
$fetchCollectionData->options['mimeTruncation'] = Syncroton_Command_Sync::TRUNCATE_NOTHING;
foreach($collectionData->toBeFetched as $serverId) {
$fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
$fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
try {
$applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
$dataController
->getEntry($fetchCollectionData, $serverId)
->appendXML($applicationData, $this->_device);
$fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
$fetch->appendChild($applicationData);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getTraceAsString());
$fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
}
}
}
if ($responses->hasChildNodes() === true) {
$collection->appendChild($responses);
}
$commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
foreach($serverModifications['added'] as $id => $serverId) {
if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
break;
}
#/**
# * somewhere is a problem in the logic for handling moreAvailable
# *
# * it can happen, that we have a contentstate (which means we sent the entry to the client
# * and that this entry is yet in $collectionData->syncState->pendingdata['serverAdds']
# * I have no idea how this can happen, but the next lines of code work around this problem
# */
#try {
# $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
#
# if ($this->_logger instanceof Zend_Log)
# $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client");
#
# unset($serverModifications['added'][$id]);
# continue;
#
#} catch (Syncroton_Exception_NotFound $senf) {
# // do nothing => content state should not exist yet
#}
try {
$add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
$dataController
->getEntry($collectionData, $serverId)
->appendXML($applicationData, $this->_device);
$commands->appendChild($add);
$collectionChanges++;
} catch (Syncroton_Exception_MemoryExhausted $seme) {
// continue to next entry, as there is not enough memory left for the current entry
// this will lead to MoreAvailable at the end and the entry will be synced during the next Sync command
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " memory exhausted for entry: " . $serverId);
continue;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getTraceAsString());
}
// mark as sent to the client, even the conversion to xml might have failed
$newContentStates[] = new Syncroton_Model_Content(array(
'device_id' => $this->_device,
'folder_id' => $collectionData->folder,
'contentid' => $serverId,
'creation_time' => $this->_syncTimeStamp,
'creation_synckey' => $collectionData->syncState->counter + 1
));
unset($serverModifications['added'][$id]);
}
/**
* process entries changed on server side
*/
foreach($serverModifications['changed'] as $id => $serverId) {
if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
break;
}
try {
$change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
$change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
$dataController
->getEntry($collectionData, $serverId)
->appendXML($applicationData, $this->_device);
$commands->appendChild($change);
$collectionChanges++;
} catch (Syncroton_Exception_MemoryExhausted $seme) {
// continue to next entry, as there is not enough memory left for the current entry
// this will lead to MoreAvailable at the end and the entry will be synced during the next Sync command
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " memory exhausted for entry: " . $serverId);
continue;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
}
unset($serverModifications['changed'][$id]);
}
foreach($serverModifications['deleted'] as $id => $serverId) {
if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
break;
}
try {
// check if we have sent this entry to the phone
$state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
$delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
$delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$deletedContentStates[] = $state;
$commands->appendChild($delete);
$collectionChanges++;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
}
unset($serverModifications['deleted'][$id]);
}
$countOfPendingChanges = (count($serverModifications['added']) + count($serverModifications['changed']) + count($serverModifications['deleted']));
if ($countOfPendingChanges > 0) {
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
} else {
$serverModifications = null;
}
if ($commands->hasChildNodes() === true) {
$collection->appendChild($commands);
}
$totalChanges += $collectionChanges;
// increase SyncKey if needed
if ((
// sent the clients updates... ?
!empty($clientModifications['added']) ||
!empty($clientModifications['changed']) ||
!empty($clientModifications['deleted'])
) || (
// is the server sending updates to the client... ?
$commands->hasChildNodes() === true
) || (
// changed the pending data... ?
$collectionData->syncState->pendingdata != $serverModifications
)
) {
// ...then increase SyncKey
$collectionData->syncState->counter++;
}
$syncKeyElement->appendChild($this->_outputDom->createTextNode($collectionData->syncState->counter));
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " current synckey is ". $collectionData->syncState->counter);
if (!$emptySyncSupported || $collection->childNodes->length > 4 || $collectionData->syncState->counter != $collectionData->syncKey) {
$collections->appendChild($collection);
}
}
if (isset($collectionData->syncState) &&
$collectionData->syncState instanceof Syncroton_Model_ISyncState &&
$collectionData->syncState->counter != $collectionData->syncKey
) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " update syncState for collection: " . $collectionData->collectionId);
// store pending data in sync state when needed
if(isset($countOfPendingChanges) && $countOfPendingChanges > 0) {
$collectionData->syncState->pendingdata = array(
'added' => (array)$serverModifications['added'],
'changed' => (array)$serverModifications['changed'],
'deleted' => (array)$serverModifications['deleted']
);
} else {
$collectionData->syncState->pendingdata = null;
}
if (!empty($clientModifications['added'])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
$keepPreviousSyncKey = false;
} else {
$keepPreviousSyncKey = true;
}
$collectionData->syncState->lastsync = clone $this->_syncTimeStamp;
// increment sync timestamp by 1 second
$collectionData->syncState->lastsync->modify('+1 sec');
try {
$transactionId = Syncroton_Registry::getTransactionManager()->startTransaction(Syncroton_Registry::getDatabase());
// store new synckey
$this->_syncStateBackend->create($collectionData->syncState, $keepPreviousSyncKey);
// store contentstates for new entries added to client
foreach($newContentStates as $state) {
$this->_contentStateBackend->create($state);
}
// remove contentstates for entries to be deleted on client
foreach($deletedContentStates as $state) {
$this->_contentStateBackend->delete($state);
}
Syncroton_Registry::getTransactionManager()->commitTransaction($transactionId);
} catch (Zend_Db_Statement_Exception $zdse) {
// something went wrong
// maybe another parallel request added a new synckey
// we must remove data added from client
if (!empty($clientModifications['added'])) {
foreach ($clientModifications['added'] as $added) {
$this->_contentStateBackend->delete($added['contentState']);
$dataController->deleteEntry($collectionData->collectionId, $added['serverId'], array());
}
}
Syncroton_Registry::getTransactionManager()->rollBack();
throw $zdse;
}
}
// store current filter type
try {
$folderState = $this->_folderBackend->get($collectionData->folder);
$folderState->lastfiltertype = $collectionData->options['filterType'];
if ($folderState->isDirty()) {
$this->_folderBackend->update($folderState);
}
} catch (Syncroton_Exception_NotFound $senf) {
// failed to get folderstate => should not happen but is also no problem in this state
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . ' failed to get folder state for: ' . $collectionData->collectionId);
}
}
if ($collections->hasChildNodes() === true) {
$sync->appendChild($collections);
}
if ($sync->hasChildNodes()) {
return $this->_outputDom;
}
return null;
}
/**
* remove Commands and Supported from collections XML tree
*
* @param DOMDocument $document
* @return DOMDocument
*/
protected function _cleanUpXML(DOMDocument $document)
{
$cleanedDocument = clone $document;
$xpath = new DomXPath($cleanedDocument);
$xpath->registerNamespace('AirSync', 'uri:AirSync');
$collections = $xpath->query("//AirSync:Sync/AirSync:Collections/AirSync:Collection");
// remove Commands and Supported elements
foreach ($collections as $collection) {
foreach (array('Commands', 'Supported') as $element) {
$childrenToRemove = $collection->getElementsByTagName($element);
foreach ($childrenToRemove as $childToRemove) {
$collection->removeChild($childToRemove);
}
}
}
return $cleanedDocument;
}
/**
* merge a partial XML document with the XML document from the previous request
*
* @param DOMDocument|null $requestBody
* @return SimpleXMLElement
*/
protected function _mergeSyncRequest($requestBody, Syncroton_Model_Device $device)
{
$lastSyncCollection = array();
if (!empty($device->lastsynccollection)) {
$lastSyncCollection = Zend_Json::decode($device->lastsynccollection);
if (!empty($lastSyncCollection['lastXML'])) {
$lastXML = new DOMDocument();
$lastXML->loadXML($lastSyncCollection['lastXML']);
}
}
if (! $requestBody instanceof DOMDocument && isset($lastXML) && $lastXML instanceof DOMDocument) {
$requestBody = $lastXML;
} elseif (! $requestBody instanceof DOMDocument) {
throw new Syncroton_Exception_UnexpectedValue('no xml body found');
}
if ($requestBody->getElementsByTagName('Partial')->length > 0) {
$partialBody = clone $requestBody;
$requestBody = $lastXML;
$xpath = new DomXPath($requestBody);
$xpath->registerNamespace('AirSync', 'uri:AirSync');
foreach ($partialBody->documentElement->childNodes as $child) {
if (! $child instanceof DOMElement) {
continue;
}
if ($child->tagName == 'Partial') {
continue;
}
if ($child->tagName == 'Collections') {
foreach ($child->getElementsByTagName('Collection') as $updatedCollection) {
$collectionId = $updatedCollection->getElementsByTagName('CollectionId')->item(0)->nodeValue;
$existingCollections = $xpath->query("//AirSync:Sync/AirSync:Collections/AirSync:Collection[AirSync:CollectionId='$collectionId']");
if ($existingCollections->length > 0) {
$existingCollection = $existingCollections->item(0);
foreach ($updatedCollection->childNodes as $updatedCollectionChild) {
if (! $updatedCollectionChild instanceof DOMElement) {
continue;
}
$duplicateChild = $existingCollection->getElementsByTagName($updatedCollectionChild->tagName);
if ($duplicateChild->length > 0) {
$existingCollection->replaceChild($requestBody->importNode($updatedCollectionChild, TRUE), $duplicateChild->item(0));
} else {
$existingCollection->appendChild($requestBody->importNode($updatedCollectionChild, TRUE));
}
}
} else {
$importedCollection = $requestBody->importNode($updatedCollection, TRUE);
}
}
} else {
$duplicateChild = $xpath->query("//AirSync:Sync/AirSync:{$child->tagName}");
if ($duplicateChild->length > 0) {
$requestBody->documentElement->replaceChild($requestBody->importNode($child, TRUE), $duplicateChild->item(0));
} else {
$requestBody->documentElement->appendChild($requestBody->importNode($child, TRUE));
}
}
}
}
$lastSyncCollection['lastXML'] = $this->_cleanUpXML($requestBody)->saveXML();
$device->lastsynccollection = Zend_Json::encode($lastSyncCollection);
return $requestBody;
}
}
diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php
index e89b0fe..0fa4855 100644
--- a/lib/ext/Syncroton/Server.php
+++ b/lib/ext/Syncroton/Server.php
@@ -1,445 +1,446 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle incoming http ActiveSync requests
*
* @package Syncroton
*/
class Syncroton_Server
{
const PARAMETER_ATTACHMENTNAME = 0;
const PARAMETER_COLLECTIONID = 1;
const PARAMETER_ITEMID = 3;
const PARAMETER_OPTIONS = 7;
+ const MAX_HEARTBEAT_INTERVAL = 3540; // 59 minutes
protected $_body;
/**
* informations about the currently device
*
* @var Syncroton_Backend_IDevice
*/
protected $_deviceBackend;
/**
* @var Zend_Log
*/
protected $_logger;
/**
* @var Zend_Controller_Request_Http
*/
protected $_request;
protected $_userId;
public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null)
{
if (Syncroton_Registry::isRegistered('loggerBackend')) {
$this->_logger = Syncroton_Registry::get('loggerBackend');
}
$this->_userId = $userId;
$this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http();
$this->_body = $body !== null ? $body : fopen('php://input', 'r');
$this->_deviceBackend = Syncroton_Registry::getDeviceBackend();
}
public function handle()
{
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod());
switch($this->_request->getMethod()) {
case 'OPTIONS':
$this->_handleOptions();
break;
case 'POST':
$this->_handlePost();
break;
case 'GET':
echo "It works!<br>Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}.";
break;
}
}
/**
* handle options request
*/
protected function _handleOptions()
{
$command = new Syncroton_Command_Options();
$this->_sendHeaders($command->getHeaders());
}
protected function _sendHeaders(array $headers)
{
foreach ($headers as $name => $value) {
header($name . ': ' . $value);
}
}
/**
* handle post request
*/
protected function _handlePost()
{
$requestParameters = $this->_getRequestParameters($this->_request);
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true));
$className = 'Syncroton_Command_' . $requestParameters['command'];
if(!class_exists($className)) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']);
header("HTTP/1.1 501 not implemented");
return;
}
// get user device
$device = $this->_getUserDevice($this->_userId, $requestParameters);
if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') {
// decode wbxml request
try {
$decoder = new Syncroton_Wbxml_Decoder($this->_body);
$requestBody = $decoder->decode();
if ($this->_logger instanceof Zend_Log) {
$requestBody->formatOutput = true;
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML());
}
} catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) {
$requestBody = NULL;
}
} else {
$requestBody = $this->_body;
}
header("MS-Server-ActiveSync: 14.00.0536.000");
// avoid sending HTTP header "Content-Type: text/html" for empty sync responses
ini_set('default_mimetype', 'application/vnd.ms-sync.wbxml');
try {
$command = new $className($requestBody, $device, $requestParameters);
$command->handle();
$response = $command->getResponse();
} catch (Syncroton_Exception_ProvisioningNeeded $sepn) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed");
header("HTTP/1.1 449 Retry after sending a PROVISION command");
if (version_compare($device->acsversion, '14.0', '>=')) {
$response = $sepn->domDocument;
} else {
// pre 14.0 method
return;
}
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
header("HTTP/1.1 500 Internal server error");
return;
}
if ($response instanceof DOMDocument) {
if ($this->_logger instanceof Zend_Log) {
$this->_logDomDocument(Zend_Log::DEBUG, $response, __METHOD__, __LINE__);
}
if (isset($command) && $command instanceof Syncroton_Command_ICommand) {
$this->_sendHeaders($command->getHeaders());
}
$outputStream = fopen("php://temp", 'r+');
$encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3);
try {
$encoder->encode($response);
} catch (Syncroton_Wbxml_Exception $swe) {
if ($this->_logger instanceof Zend_Log) {
$this->_logger->err(__METHOD__ . '::' . __LINE__ . " Could not encode output: " . $swe);
$this->_logDomDocument(Zend_Log::ERR, $response, __METHOD__, __LINE__);
}
header("HTTP/1.1 500 Internal server error");
return;
}
if ($requestParameters['acceptMultipart'] == true) {
$parts = $command->getParts();
// output multipartheader
$bodyPartCount = 1 + count($parts);
// number of parts (4 bytes)
$header = pack('i', $bodyPartCount);
$partOffset = 4 + (($bodyPartCount * 2) * 4);
// wbxml body start and length
$streamStat = fstat($outputStream);
$header .= pack('ii', $partOffset, $streamStat['size']);
$partOffset += $streamStat['size'];
// calculate start and length of parts
foreach ($parts as $partId => $partStream) {
rewind($partStream);
$streamStat = fstat($partStream);
// part start and length
$header .= pack('ii', $partOffset, $streamStat['size']);
$partOffset += $streamStat['size'];
}
echo $header;
}
// output body
rewind($outputStream);
fpassthru($outputStream);
// output multiparts
if (isset($parts)) {
foreach ($parts as $partStream) {
rewind($partStream);
fpassthru($partStream);
}
}
}
}
/**
* write (possible big) DOMDocument in smaller chunks to log file
*
* @param unknown $priority
* @param DOMDocument $dom
* @param string $method
* @param int $line
*/
protected function _logDomDocument($priority, DOMDocument $dom, $method, $line)
{
$loops = 0;
$tempStream = fopen('php://temp/maxmemory:5242880', 'r+');
$dom->formatOutput = true;
fwrite($tempStream, $dom->saveXML());
$dom->formatOutput = false;
rewind($tempStream);
// log data in 1MByte chunks
while (!feof($tempStream)) {
$this->_logger->log($method . '::' . $line . " xml response($loops):\n" . fread($tempStream, 1048576), $priority);
$loops++;
}
fclose($tempStream);
}
/**
* return request params
*
* @return array
*/
protected function _getRequestParameters(Zend_Controller_Request_Http $request)
{
if (strpos($request->getRequestUri(), '&') === false) {
$commands = array(
0 => 'Sync',
1 => 'SendMail',
2 => 'SmartForward',
3 => 'SmartReply',
4 => 'GetAttachment',
9 => 'FolderSync',
10 => 'FolderCreate',
11 => 'FolderDelete',
12 => 'FolderUpdate',
13 => 'MoveItems',
14 => 'GetItemEstimate',
15 => 'MeetingResponse',
16 => 'Search',
17 => 'Settings',
18 => 'Ping',
19 => 'ItemOperations',
20 => 'Provision',
21 => 'ResolveRecipients',
22 => 'ValidateCert'
);
$requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?'));
$stream = fopen("php://temp", 'r+');
fwrite($stream, base64_decode($requestParameters));
rewind($stream);
// unpack the first 4 bytes
$unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4));
// 140 => 14.0
$protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1);
$command = $commands[$unpacked['command']];
$locale = $unpacked['locale'];
// unpack deviceId
$length = ord(fread($stream, 1));
if ($length > 0) {
$toUnpack = fread($stream, $length);
$unpacked = unpack("H" . ($length * 2) . "string", $toUnpack);
$deviceId = $unpacked['string'];
}
// unpack policyKey
$length = ord(fread($stream, 1));
if ($length > 0) {
$unpacked = unpack('Vstring', fread($stream, $length));
$policyKey = $unpacked['string'];
}
// unpack device type
$length = ord(fread($stream, 1));
if ($length > 0) {
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$deviceType = $unpacked['string'];
}
while (! feof($stream)) {
$tag = ord(fread($stream, 1));
$length = ord(fread($stream, 1));
switch ($tag) {
case self::PARAMETER_ATTACHMENTNAME:
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$attachmentName = $unpacked['string'];
break;
case self::PARAMETER_COLLECTIONID:
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$collectionId = $unpacked['string'];
break;
case self::PARAMETER_ITEMID:
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$itemId = $unpacked['string'];
break;
case self::PARAMETER_OPTIONS:
$options = ord(fread($stream, 1));
$saveInSent = !!($options & 0x01);
$acceptMultiPart = !!($options & 0x02);
break;
default:
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters");
}
}
$result = array(
'protocolVersion' => $protocolVersion,
'command' => $command,
'deviceId' => $deviceId,
'deviceType' => isset($deviceType) ? $deviceType : null,
'policyKey' => isset($policyKey) ? $policyKey : null,
'saveInSent' => isset($saveInSent) ? $saveInSent : false,
'collectionId' => isset($collectionId) ? $collectionId : null,
'itemId' => isset($itemId) ? $itemId : null,
'attachmentName' => isset($attachmentName) ? $attachmentName : null,
'acceptMultipart' => isset($acceptMultiPart) ? $acceptMultiPart : false
);
} else {
$result = array(
'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'),
'command' => $request->getQuery('Cmd'),
'deviceId' => $request->getQuery('DeviceId'),
'deviceType' => $request->getQuery('DeviceType'),
'policyKey' => $request->getServer('HTTP_X_MS_POLICYKEY'),
'saveInSent' => $request->getQuery('SaveInSent') == 'T',
'collectionId' => $request->getQuery('CollectionId'),
'itemId' => $request->getQuery('ItemId'),
'attachmentName' => $request->getQuery('AttachmentName'),
'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T'
);
}
$result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']);
$result['contentType'] = $request->getServer('CONTENT_TYPE');
return $result;
}
/**
* get existing device of owner or create new device for owner
*
* @param unknown_type $ownerId
* @param unknown_type $deviceId
* @param unknown_type $deviceType
* @param unknown_type $userAgent
* @param unknown_type $protocolVersion
* @return Syncroton_Model_Device
*/
protected function _getUserDevice($ownerId, $requestParameters)
{
try {
$device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']);
$device->useragent = $requestParameters['userAgent'];
$device->acsversion = $requestParameters['protocolVersion'];
if ($device->isDirty()) {
$device = $this->_deviceBackend->update($device);
}
} catch (Syncroton_Exception_NotFound $senf) {
$device = $this->_deviceBackend->create(new Syncroton_Model_Device(array(
'owner_id' => $ownerId,
'deviceid' => $requestParameters['deviceId'],
'devicetype' => $requestParameters['deviceType'],
'useragent' => $requestParameters['userAgent'],
'acsversion' => $requestParameters['protocolVersion'],
'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null
)));
}
return $device;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Feb 3, 6:21 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427078
Default Alt Text
(88 KB)

Event Timeline