diff --git a/lib/ext/Syncroton/Backend/ABackend.php b/lib/ext/Syncroton/Backend/ABackend.php index 00869e4..eb5e605 100644 --- a/lib/ext/Syncroton/Backend/ABackend.php +++ b/lib/ext/Syncroton/Backend/ABackend.php @@ -1,195 +1,211 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Backend */ abstract class Syncroton_Backend_ABackend implements Syncroton_Backend_IBackend { /** * the database adapter * * @var Zend_Db_Adapter_Abstract */ protected $_db; protected $_tablePrefix; - protected $_tableName; - + protected $_tableName; + protected $_modelClassName; protected $_modelInterfaceName; /** * the constructor * * @param Zend_Db_Adapter_Abstract $_db * @param string $_tablePrefix */ public function __construct(Zend_Db_Adapter_Abstract $_db, $_tablePrefix = 'Syncroton_') { $this->_db = $_db; $this->_tablePrefix = $_tablePrefix; } /** * create new device * - * @param Syncroton_Model_IDevice $_device - * @return Syncroton_Model_IDevice + * @param Syncroton_Model_AEntry $model + * @return Syncroton_Model_AEntry */ public function create($model) { if (! $model instanceof $this->_modelInterfaceName) { - throw new InvalidArgumentException('$model must be instanace of ' . $this->_modelInterfaceName); + throw new InvalidArgumentException('$model must be instance of ' . $this->_modelInterfaceName); } $data = $this->_convertModelToArray($model); $data['id'] = sha1(mt_rand(). microtime()); - + $this->_db->insert($this->_tablePrefix . $this->_tableName, $data); return $this->get($data['id']); } /** * convert iteratable object to array * - * @param unknown $model + * @param Syncroton_Model_AEntry $model * @return array */ protected function _convertModelToArray($model) { $data = array(); - foreach ($model as $key => $value) { - if ($value instanceof DateTime) { - $value = $value->format('Y-m-d H:i:s'); - } elseif (is_object($value) && isset($value->id)) { - $value = $value->id; - } - - $data[$this->_fromCamelCase($key)] = $value; + foreach ($model as $key => $value) { + if ($value instanceof DateTime) { + $value = $value->format('Y-m-d H:i:s'); + } elseif (is_object($value) && isset($value->id)) { + $value = $value->id; + } + + $data[$this->_fromCamelCase($key)] = $value; } - return $data; + return $data; } /** * @param string $_id * @throws Syncroton_Exception_NotFound * @return Syncroton_Model_IDevice */ public function get($id) { $id = $id instanceof $this->_modelInterfaceName ? $id->id : $id; + if (empty($id)) { + throw new Syncroton_Exception_NotFound('id can not be empty'); + } + $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where('id = ?', $id); $stmt = $this->_db->query($select); $data = $stmt->fetch(); $stmt = null; # see https://bugs.php.net/bug.php?id=44081 if ($data === false) { throw new Syncroton_Exception_NotFound('id not found'); } return $this->_getObject($data); } /** * convert array to object * * @param array $data * @return object */ protected function _getObject($data) { - foreach ($data as $key => $value) { + foreach ($data as $key => $value) { unset($data[$key]); - - if (!empty($value) && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) { # 2012-08-12 07:43:26 - $value = new DateTime($value, new DateTimeZone('utc')); + + if (!empty($value) && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) { # 2012-08-12 07:43:26 + $value = new DateTime($value, new DateTimeZone('utc')); } - - $data[$this->_toCamelCase($key, false)] = $value; - } - - return new $this->_modelClassName($data); + + $data[$this->_toCamelCase($key, false)] = $value; + } + + return new $this->_modelClassName($data); } /** * (non-PHPdoc) * @see Syncroton_Backend_IBackend::delete() */ public function delete($id) { $id = $id instanceof $this->_modelInterfaceName ? $id->id : $id; $result = $this->_db->delete($this->_tablePrefix . $this->_tableName, array('id = ?' => $id)); return (bool) $result; } /** * (non-PHPdoc) * @see Syncroton_Backend_IBackend::update() */ public function update($model) { - if (! $model instanceof $this->_modelInterfaceName) { - throw new InvalidArgumentException('$model must be instanace of ' . $this->_modelInterfaceName); - } + if (! $model instanceof $this->_modelInterfaceName) { + throw new InvalidArgumentException('$model must be instanace of ' . $this->_modelInterfaceName); + } $data = $this->_convertModelToArray($model); $this->_db->update($this->_tablePrefix . $this->_tableName, $data, array( 'id = ?' => $model->id )); return $this->get($model->id); } + /** + * Returns list of user accounts + * + * @param Syncroton_Model_Device $device The device + * + * @return array List of Syncroton_Model_Account objects + */ + public function userAccounts($device) + { + return array(); + } + /** * convert from camelCase to camel_case * @param string $string * @return string */ protected function _fromCamelCase($string) { $string = lcfirst($string); return preg_replace_callback('/([A-Z])/', function ($string) {return '_' . strtolower($string[0]);}, $string); } /** * convert from camel_case to camelCase * * @param string $string * @param bool $ucFirst * @return string */ protected function _toCamelCase($string, $ucFirst = true) { if ($ucFirst === true) { $string = ucfirst($string); } return preg_replace_callback('/_([a-z])/', function ($string) {return strtoupper($string[1]);}, $string); } } diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php index f1764d5..1654b34 100644 --- a/lib/ext/Syncroton/Command/FolderCreate.php +++ b/lib/ext/Syncroton/Command/FolderCreate.php @@ -1,115 +1,117 @@ */ /** * class to handle ActiveSync FolderSync command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml { protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderCreate'; /** * * @var Syncroton_Model_Folder */ protected $_folder; /** * parse FolderCreate request * */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); $syncKey = (int)$xml->SyncKey; if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey"); if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) { return; } $folder = new Syncroton_Model_Folder($xml); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " parentId: {$folder->parentId} displayName: {$folder->displayName}"); switch($folder->type) { case Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_CALENDAR; break; case Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_CONTACTS; break; case Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_EMAIL; break; case Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_NOTES; break; case Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_TASKS; break; default: // unsupported type return; } $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); $this->_folder = $dataController->createFolder($folder); $this->_folder->class = $folder->class; $this->_folder->deviceId = $this->_device; $this->_folder->creationTime = $this->_syncTimeStamp; $this->_folderBackend->create($this->_folder); } /** * generate FolderCreate response */ public function getResponse() { $folderCreate = $this->_outputDom->documentElement; if (!$this->_syncState instanceof Syncroton_Model_SyncState) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed."); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_INVALID_SYNC_KEY)); - } else if (!$this->_folder) { + + } else if (!$this->_folder instanceof Syncroton_Model_Folder) { $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_UNKNOWN_ERROR)); + } else { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; // store folder in state backend $this->_syncStateBackend->update($this->_syncState); // create xml output $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_SUCCESS)); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $this->_folder->serverId)); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php index ed3a350..9c563bf 100644 --- a/lib/ext/Syncroton/Command/FolderSync.php +++ b/lib/ext/Syncroton/Command/FolderSync.php @@ -1,264 +1,267 @@ */ /** * class to handle ActiveSync FolderSync command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml { const STATUS_SUCCESS = 1; const STATUS_FOLDER_EXISTS = 2; const STATUS_IS_SPECIAL_FOLDER = 3; const STATUS_FOLDER_NOT_FOUND = 4; const STATUS_PARENT_FOLDER_NOT_FOUND = 5; const STATUS_SERVER_ERROR = 6; const STATUS_ACCESS_DENIED = 7; const STATUS_REQUEST_TIMED_OUT = 8; const STATUS_INVALID_SYNC_KEY = 9; const STATUS_MISFORMATTED = 10; const STATUS_UNKNOWN_ERROR = 11; /** * some usefull constants for working with the xml files */ const FOLDERTYPE_GENERIC_USER_CREATED = 1; const FOLDERTYPE_INBOX = 2; const FOLDERTYPE_DRAFTS = 3; const FOLDERTYPE_DELETEDITEMS = 4; const FOLDERTYPE_SENTMAIL = 5; const FOLDERTYPE_OUTBOX = 6; const FOLDERTYPE_TASK = 7; const FOLDERTYPE_CALENDAR = 8; const FOLDERTYPE_CONTACT = 9; const FOLDERTYPE_NOTE = 10; const FOLDERTYPE_JOURNAL = 11; const FOLDERTYPE_MAIL_USER_CREATED = 12; const FOLDERTYPE_CALENDAR_USER_CREATED = 13; const FOLDERTYPE_CONTACT_USER_CREATED = 14; const FOLDERTYPE_TASK_USER_CREATED = 15; const FOLDERTYPE_JOURNAL_USER_CREATED = 16; const FOLDERTYPE_NOTE_USER_CREATED = 17; const FOLDERTYPE_UNKOWN = 18; protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderSync'; protected $_classes = array( Syncroton_Data_Factory::CLASS_CALENDAR, Syncroton_Data_Factory::CLASS_CONTACTS, Syncroton_Data_Factory::CLASS_EMAIL, Syncroton_Data_Factory::CLASS_NOTES, Syncroton_Data_Factory::CLASS_TASKS ); /** * @var string */ protected $_syncKey; /** * parse FolderSync request * */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); $syncKey = (int)$xml->SyncKey; if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey"); if ($syncKey === 0) { $this->_syncState = new Syncroton_Model_SyncState(array( 'device_id' => $this->_device, 'counter' => 0, 'type' => 'FolderSync', 'lastsync' => $this->_syncTimeStamp )); // reset state of foldersync $this->_syncStateBackend->resetState($this->_device, 'FolderSync'); return; } if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) { $this->_syncStateBackend->resetState($this->_device, 'FolderSync'); } } /** * generate FolderSync response * * @todo changes are missing in response (folder got renamed for example) */ public function getResponse() { $folderSync = $this->_outputDom->documentElement; // invalid synckey provided if (!$this->_syncState instanceof Syncroton_Model_SyncState) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed."); $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY)); return $this->_outputDom; } // send headers from options command also when FolderSync SyncKey is 0 if ($this->_syncState->counter == 0) { $optionsCommand = new Syncroton_Command_Options(); $this->_headers = array_merge($this->_headers, $optionsCommand->getHeaders()); } $adds = array(); $updates = array(); $deletes = array(); foreach($this->_classes as $class) { try { $dataController = Syncroton_Data_Factory::factory($class, $this->_device, $this->_syncTimeStamp); } catch (Exception $e) { // backend not defined if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " no data backend defined for class: " . $class); continue; } try { // retrieve all folders available in data backend $serverFolders = $dataController->getAllFolders(); - + + // retrieve all folders sent to client + $clientFolders = $this->_folderBackend->getFolderState($this->_device, $class); + if ($this->_syncState->counter > 0) { // retrieve all folders changed since last sync $changedFolders = $dataController->getChangedFolders($this->_syncState->lastsync, $this->_syncTimeStamp); } else { $changedFolders = array(); } - // retrieve all folders sent to client - $clientFolders = $this->_folderBackend->getFolderState($this->_device, $class); + // only folders which were sent to the client already are allowed to be in $changedFolders + $changedFolders = array_intersect_key($changedFolders, $clientFolders); } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getMessage()); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getTraceAsString()); // The Status element is global for all collections. If one collection fails, // a failure status MUST be returned for all collections. if ($e instanceof Syncroton_Exception_Status) { $status = $e->getCode(); } else { $status = Syncroton_Exception_Status_FolderSync::UNKNOWN_ERROR; } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', $status)); return $this->_outputDom; } $serverFoldersIds = array_keys($serverFolders); // is this the first sync? if ($this->_syncState->counter == 0) { $clientFoldersIds = array(); } else { $clientFoldersIds = array_keys($clientFolders); } // calculate added entries $serverDiff = array_diff($serverFoldersIds, $clientFoldersIds); foreach ($serverDiff as $serverFolderId) { // have we created a folderObject in syncroton_folder before? if (isset($clientFolders[$serverFolderId])) { $add = $clientFolders[$serverFolderId]; } else { $add = $serverFolders[$serverFolderId]; $add->creationTime = $this->_syncTimeStamp; $add->deviceId = $this->_device; unset($add->id); } $add->class = $class; $adds[] = $add; } // calculate changed entries foreach ($changedFolders as $changedFolder) { $change = $clientFolders[$changedFolder->serverId]; $change->displayName = $changedFolder->displayName; $change->parentId = $changedFolder->parentId; $change->type = $changedFolder->type; $updates[] = $change; } // calculate deleted entries $serverDiff = array_diff($clientFoldersIds, $serverFoldersIds); foreach ($serverDiff as $serverFolderId) { $deletes[] = $clientFolders[$serverFolderId]; } } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS)); $count = count($adds) + count($updates) + count($deletes); if($count > 0) { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; } // create xml output $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); $changes = $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Changes')); $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Count', $count)); foreach($adds as $folder) { $add = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Add')); $folder->appendXML($add, $this->_device); // store folder in backend if (empty($folder->id)) { $this->_folderBackend->create($folder); } } foreach($updates as $folder) { $update = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Update')); $folder->appendXML($update, $this->_device); $this->_folderBackend->update($folder); } foreach($deletes as $folder) { $delete = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Delete')); $delete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder->serverId)); $this->_folderBackend->delete($folder); } if (empty($this->_syncState->id)) { $this->_syncStateBackend->create($this->_syncState); } else { $this->_syncStateBackend->update($this->_syncState); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/ItemOperations.php b/lib/ext/Syncroton/Command/ItemOperations.php index 93aa722..20401ba 100644 --- a/lib/ext/Syncroton/Command/ItemOperations.php +++ b/lib/ext/Syncroton/Command/ItemOperations.php @@ -1,253 +1,305 @@ */ /** * class to handle ActiveSync ItemOperations command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_ItemOperations extends Syncroton_Command_Wbxml { const STATUS_SUCCESS = 1; const STATUS_PROTOCOL_ERROR = 2; const STATUS_SERVER_ERROR = 3; const STATUS_ITEM_FAILED_CONVERSION = 14; protected $_defaultNameSpace = 'uri:ItemOperations'; protected $_documentElement = 'ItemOperations'; /** * list of items to move * * @var array */ protected $_fetches = array(); /** * list of folder to empty * * @var array */ protected $_emptyFolderContents = array(); /** * parse MoveItems request * */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); if (isset($xml->Fetch)) { foreach ($xml->Fetch as $fetch) { $this->_fetches[] = $this->_handleFetch($fetch); } } if (isset($xml->EmptyFolderContents)) { foreach ($xml->EmptyFolderContents as $emptyFolderContents) { $this->_emptyFolderContents[] = $this->_handleEmptyFolderContents($emptyFolderContents); } } if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " fetches: " . print_r($this->_fetches, true)); } /** * generate ItemOperations response * * @todo add multipart support to all types of fetches */ public function getResponse() { // add aditional namespaces $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase' , 'uri:AirSyncBase'); $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSync' , 'uri:AirSync'); $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Search' , 'uri:Search'); $itemOperations = $this->_outputDom->documentElement; $itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS)); $response = $itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Response')); foreach ($this->_fetches as $fetch) { $fetchTag = $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Fetch')); try { $dataController = Syncroton_Data_Factory::factory($fetch['store'], $this->_device, $this->_syncTimeStamp); if (isset($fetch['collectionId'])) { $fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS)); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $fetch['collectionId'])); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $fetch['serverId'])); $properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties'); - $dataController - ->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['collectionId'], 'options' => $fetch['options'])), $fetch['serverId']) + $dataController + ->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['collectionId'], 'options' => $fetch['options'])), $fetch['serverId']) ->appendXML($properties, $this->_device); $fetchTag->appendChild($properties); } elseif (isset($fetch['longId'])) { $fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS)); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:Search', 'LongId', $fetch['longId'])); $properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties'); - $dataController - ->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['longId'], 'options' => $fetch['options'])), $fetch['longId']) + $dataController + ->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['longId'], 'options' => $fetch['options'])), $fetch['longId']) ->appendXML($properties, $this->_device); $fetchTag->appendChild($properties); } elseif (isset($fetch['fileReference'])) { $fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS)); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSyncBase', 'FileReference', $fetch['fileReference'])); $properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties'); $fileReference = $dataController->getFileReference($fetch['fileReference']); // unset data field and move content to stream if ($this->_requestParameters['acceptMultipart'] == true) { $this->_headers['Content-Type'] = 'application/vnd.ms-sync.multipart'; $partStream = fopen("php://temp", 'r+'); if (is_resource($fileReference->data)) { stream_copy_to_stream($fileReference->data, $partStream); } else { fwrite($partStream, $fileReference->data); } unset($fileReference->data); $this->_parts[] = $partStream; $fileReference->part = count($this->_parts); + } + + /** + * the client requested a range. But we return the whole file. + * + * That's not correct, but allowed. The server is allowed to overwrite the range. + * + * @todo implement cutting $fileReference->data into pieces + */ + if (isset($fetch['options']['range'])) { + $dataSize = $this->_getDataSize($fileReference->data); + + $total = $this->_outputDom->createElementNS('uri:ItemOperations', 'Total', $dataSize); + $properties->appendChild($total); + + $rangeEnd = $dataSize > 0 ? $dataSize - 1 : 0; + $range = $this->_outputDom->createElementNS('uri:ItemOperations', 'Range', '0-' . $rangeEnd); + $properties->appendChild($range); } $fileReference->appendXML($properties, $this->_device); $fetchTag->appendChild($properties); } } catch (Syncroton_Exception_NotFound $e) { $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_ITEM_FAILED_CONVERSION)); } catch (Exception $e) { //echo __LINE__; echo $e->getMessage(); echo $e->getTraceAsString(); $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SERVER_ERROR)); } } foreach ($this->_emptyFolderContents as $emptyFolderContents) { try { $folder = $this->_folderBackend->getFolder($this->_device, $emptyFolderContents['collectionId']); $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); $dataController->emptyFolderContents($emptyFolderContents['collectionId'], $emptyFolderContents['options']); $status = Syncroton_Command_ItemOperations::STATUS_SUCCESS; } catch (Syncroton_Exception_Status_ItemOperations $e) { $status = $e->getCode(); } catch (Exception $e) { $status = Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR; } $emptyFolderContentsTag = $this->_outputDom->createElementNS('uri:ItemOperations', 'EmptyFolderContents'); $emptyFolderContentsTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', $status)); $emptyFolderContentsTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $emptyFolderContents['collectionId'])); $response->appendChild($emptyFolderContentsTag); } return $this->_outputDom; } + /** + * parse fetch request + * + * @param SimpleXMLElement $fetch + * @return array + */ protected function _handleFetch(SimpleXMLElement $fetch) { $fetchArray = array( - 'store' => (string)$fetch->Store, + 'store' => (string)$fetch->Store, 'options' => array() ); // try to fetch element from namespace AirSync $airSync = $fetch->children('uri:AirSync'); if (isset($airSync->CollectionId)) { $fetchArray['collectionId'] = (string)$airSync->CollectionId; $fetchArray['serverId'] = (string)$airSync->ServerId; } // try to fetch element from namespace Search $search = $fetch->children('uri:Search'); if (isset($search->LongId)) { $fetchArray['longId'] = (string)$search->LongId; } // try to fetch element from namespace AirSyncBase $airSyncBase = $fetch->children('uri:AirSyncBase'); if (isset($airSyncBase->FileReference)) { $fetchArray['fileReference'] = (string)$airSyncBase->FileReference; } if (isset($fetch->Options)) { // try to fetch element from namespace AirSyncBase $airSyncBase = $fetch->Options->children('uri:AirSyncBase'); if (isset($airSyncBase->BodyPreference)) { foreach ($airSyncBase->BodyPreference as $bodyPreference) { $type = (int) $bodyPreference->Type; $fetchArray['options']['bodyPreferences'][$type] = array( 'type' => $type ); - + // optional if (isset($bodyPreference->TruncationSize)) { $fetchArray['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize; } // optional if (isset($bodyPreference->AllOrNone)) { $fetchArray['options']['bodyPreferences'][$type]['allOrNone'] = (int) $bodyPreference->AllOrNone; } } } + + if (isset($airSyncBase->Range)) { + $fetchArray['options']['range'] = (string) $airSyncBase->Range; + } } return $fetchArray; } + /** + * handle empty folder request + * + * @param SimpleXMLElement $emptyFolderContent + * @return array + */ protected function _handleEmptyFolderContents(SimpleXMLElement $emptyFolderContent) { $folderArray = array( 'collectiondId' => null, 'options' => array('deleteSubFolders' => FALSE) ); // try to fetch element from namespace AirSync $airSync = $emptyFolderContent->children('uri:AirSync'); $folderArray['collectionId'] = (string)$airSync->CollectionId; if (isset($emptyFolderContent->Options)) { $folderArray['options']['deleteSubFolders'] = isset($emptyFolderContent->Options->DeleteSubFolders); } return $folderArray; } + + /** + * return length of data + * + * @param string|resource $data + * @return number + */ + protected function _getDataSize($data) + { + if (is_resource($data)) { + rewind($data); + fseek($data, 0, SEEK_END); + return ftell($data); + + } else { + return strlen($data); + } + } } diff --git a/lib/ext/Syncroton/Command/Ping.php b/lib/ext/Syncroton/Command/Ping.php index 2e71fd2..8bdbe01 100644 --- a/lib/ext/Syncroton/Command/Ping.php +++ b/lib/ext/Syncroton/Command/Ping.php @@ -1,237 +1,239 @@ */ /** * 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; + const MAX_PING_INTERVAL = 3540; // 59 minutes limit defined in Activesync protocol spec. + 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; $maxInterval = Syncroton_Registry::getPingInterval(); 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', $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)); + } while (Syncroton_Server::validateSession() && $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/Settings.php b/lib/ext/Syncroton/Command/Settings.php index cc34b44..3a9c7c5 100644 --- a/lib/ext/Syncroton/Command/Settings.php +++ b/lib/ext/Syncroton/Command/Settings.php @@ -1,162 +1,156 @@ */ /** * class to handle ActiveSync Settings command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_Settings extends Syncroton_Command_Wbxml { const STATUS_SUCCESS = 1; protected $_defaultNameSpace = 'uri:Settings'; protected $_documentElement = 'Settings'; /** * @var Syncroton_Model_DeviceInformation */ protected $_deviceInformation; protected $_userInformationRequested = false; protected $_OofGet; protected $_OofSet; /** * process the XML file and add, change, delete or fetches data * */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); if(isset($xml->DeviceInformation->Set)) { $this->_deviceInformation = new Syncroton_Model_DeviceInformation($xml->DeviceInformation->Set); $this->_device->model = $this->_deviceInformation->model; $this->_device->imei = $this->_deviceInformation->iMEI; $this->_device->friendlyname = $this->_deviceInformation->friendlyName; $this->_device->os = $this->_deviceInformation->oS; $this->_device->oslanguage = $this->_deviceInformation->oSLanguage; $this->_device->phonenumber = $this->_deviceInformation->phoneNumber; if ($this->_device->isDirty()) { $this->_device = $this->_deviceBackend->update($this->_device); } } if(isset($xml->UserInformation->Get)) { $this->_userInformationRequested = true; } if (isset($xml->Oof)) { if (isset($xml->Oof->Get)) { $this->_OofGet = array('bodyType' => $xml->Oof->Get->BodyType); - } - else if (isset($xml->Oof->Set)) { + } else if (isset($xml->Oof->Set)) { $this->_OofSet = new Syncroton_Model_Oof($xml->Oof->Set); } } } /** * this function generates the response for the client * */ public function getResponse() { $settings = $this->_outputDom->documentElement; $settings->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Status', self::STATUS_SUCCESS)); if ($this->_deviceInformation instanceof Syncroton_Model_DeviceInformation) { $deviceInformation = $settings->appendChild($this->_outputDom->createElementNS('uri:Settings', 'DeviceInformation')); $set = $deviceInformation->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Set')); $set->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Status', self::STATUS_SUCCESS)); } if ($this->_userInformationRequested === true) { $userInformation = $settings->appendChild($this->_outputDom->createElementNS('uri:Settings', 'UserInformation')); $userInformation->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Status', self::STATUS_SUCCESS)); $get = $userInformation->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Get')); /* $smtpAddresses = array(); if (!empty($smtpAddresses)) { $emailAddresses = $get->appendChild($this->_outputDom->createElementNS('uri:Settings', 'EmailAddresses')); foreach($smtpAddresses as $smtpAddress) { $emailAddresses->appendChild($this->_outputDom->createElementNS('uri:Settings', 'SMTPAddress', $smtpAddress)); } } */ $userAccounts = $this->_deviceBackend->userAccounts($this->_device); if (!empty($userAccounts)) { $accounts = $get->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Accounts')); foreach ((array) $userAccounts as $account) { $element = $accounts->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Account')); $account->appendXML($element, $this->_device); } } } // Out-of-Office if (!empty($this->_OofGet)) { try { $OofGet = $this->_deviceBackend->getOOF($this->_OofGet); - } - catch (Exception $e) { + } catch (Exception $e) { if ($e instanceof Syncroton_Exception_Status) { $OofStatus = $e->getCode(); - } - else { + } else { $OofStatus = Syncroton_Exception_Status::SERVER_ERROR; } if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Setting OOF failed: " . $e->getMessage()); } } // expected empty result if OOF is not supported by the server if ($OofGet instanceof Syncroton_Model_Oof) { $Oof = $settings->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Oof')); $Oof->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Status', $OofStatus)); $Get = $Oof->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Get')); $OofGet->appendXML($Get, $this->_device); } - } - else if (!empty($this->_OofSet)) { + } else if (!empty($this->_OofSet)) { try { $this->_deviceBackend->setOOF($this->_OofSet); $OofStatus = self::STATUS_SUCCESS; - } - catch (Exception $e) { + } catch (Exception $e) { if ($e instanceof Syncroton_Exception_Status) { $OofStatus = $e->getCode(); - } - else { + } else { $OofStatus = Syncroton_Exception_Status::SERVER_ERROR; } if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Setting OOF failed: " . $e->getMessage()); } } $Oof = $settings->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Oof')); $Oof->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Status', $OofStatus)); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/Sync.php b/lib/ext/Syncroton/Command/Sync.php index 052eb4c..b52fe5b 100644 --- a/lib/ext/Syncroton/Command/Sync.php +++ b/lib/ext/Syncroton/Command/Sync.php @@ -1,1121 +1,1121 @@ */ /** * 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; } 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:AirSync', 'Status', self::STATUS_WAIT_INTERVAL_OUT_OF_RANGE)); $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', '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)); + } while (Syncroton_Server::validateSession() && 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/Model/AXMLEntry.php b/lib/ext/Syncroton/Model/AXMLEntry.php index d243aea..e2332b9 100644 --- a/lib/ext/Syncroton/Model/AXMLEntry.php +++ b/lib/ext/Syncroton/Model/AXMLEntry.php @@ -1,326 +1,323 @@ */ /** * abstract class to handle ActiveSync entry * * @package Syncroton * @subpackage Model */ abstract class Syncroton_Model_AXMLEntry extends Syncroton_Model_AEntry implements Syncroton_Model_IXMLEntry { protected $_xmlBaseElement; protected $_properties = array(); protected $_dateTimeFormat = "Y-m-d\TH:i:s.000\Z"; /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::__construct() */ public function __construct($properties = null) { if ($properties instanceof SimpleXMLElement) { $this->setFromSimpleXMLElement($properties); } elseif (is_array($properties)) { $this->setFromArray($properties); } $this->_isDirty = false; } /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::appendXML() */ public function appendXML(DOMElement $domParrent, Syncroton_Model_IDevice $device) { $this->_addXMLNamespaces($domParrent); foreach($this->_elements as $elementName => $value) { // skip empty values if($value === null || $value === '' || (is_array($value) && empty($value))) { continue; } list ($nameSpace, $elementProperties) = $this->_getElementProperties($elementName); if ($nameSpace == 'Internal') { continue; } $elementVersion = isset($elementProperties['supportedSince']) ? $elementProperties['supportedSince'] : '12.0'; if (version_compare($device->acsversion, $elementVersion, '<')) { continue; } $nameSpace = 'uri:' . $nameSpace; if (isset($elementProperties['childElement'])) { $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); foreach($value as $subValue) { $subElement = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementProperties['childElement'])); $this->_appendXMLElement($device, $subElement, $elementProperties, $subValue); $element->appendChild($subElement); } $domParrent->appendChild($element); - } - else if ($elementProperties['type'] == 'container' && !empty($elementProperties['multiple'])) { + } else if ($elementProperties['type'] == 'container' && !empty($elementProperties['multiple'])) { foreach ($value as $element) { $container = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $element->appendXML($container, $device); $domParrent->appendChild($container); } - } - else if ($elementProperties['type'] == 'none') { + } else if ($elementProperties['type'] == 'none') { if ($value) { $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $domParrent->appendChild($element); } - } - else { + } else { $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $this->_appendXMLElement($device, $element, $elementProperties, $value); $domParrent->appendChild($element); } } } /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::getProperties() */ public function getProperties($selectedNamespace = null) { $properties = array(); foreach($this->_properties as $namespace => $namespaceProperties) { if ($selectedNamespace !== null && $namespace != $selectedNamespace) { continue; } $properties = array_merge($properties, array_keys($namespaceProperties)); } return $properties; } /** * set properties from SimpleXMLElement object * * @param SimpleXMLElement $xmlCollection * @throws InvalidArgumentException */ public function setFromSimpleXMLElement(SimpleXMLElement $properties) { if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) { throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName()); } foreach (array_keys($this->_properties) as $namespace) { if ($namespace == 'Internal') { continue; } $this->_parseNamespace($namespace, $properties); } return; } /** * add needed xml namespaces to DomDocument * * @param unknown_type $domParrent */ protected function _addXMLNamespaces(DOMElement $domParrent) { foreach($this->_properties as $namespace => $namespaceProperties) { // don't add default namespace again if($domParrent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) { $domParrent->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:'.$namespace, 'uri:'.$namespace); } } } protected function _appendXMLElement(Syncroton_Model_IDevice $device, DOMElement $element, $elementProperties, $value) { if ($value instanceof Syncroton_Model_IEntry) { $value->appendXML($element, $device); } else { if ($value instanceof DateTime) { $value = $value->format($this->_dateTimeFormat); } elseif (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') { if (is_resource($value)) { rewind($value); $value = stream_get_contents($value); } $value = base64_encode($value); } if ($elementProperties['type'] == 'byteArray') { $element->setAttributeNS('uri:Syncroton', 'Syncroton:encoding', 'opaque'); // encode to base64; the wbxml encoder will base64_decode it again // this way we can also transport data, which would break the xmlparser otherwise $element->appendChild($element->ownerDocument->createCDATASection(base64_encode($value))); } else { // strip off any non printable control characters if (!ctype_print($value)) { $value = $this->_removeControlChars($value); } $element->appendChild($element->ownerDocument->createTextNode($this->_enforceUTF8($value))); } } } /** * removed control chars from string which are not allowd in XML values * * @param string|array $_dirty * @return string */ protected function _removeControlChars($dirty) { return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', null, $dirty); } /** * enforce >valid< utf-8 encoding * * @param string $dirty the string with maybe invalid utf-8 data * @return string string with valid utf-8 */ protected function _enforceUTF8($dirty) { if (function_exists('iconv')) { if (($clean = @iconv('UTF-8', 'UTF-8//IGNORE', $dirty)) !== false) { return $clean; } } if (function_exists('mb_convert_encoding')) { if (($clean = mb_convert_encoding($dirty, 'UTF-8', 'UTF-8')) !== false) { return $clean; } } return $dirty; } /** * * @param unknown_type $element * @throws InvalidArgumentException * @return multitype:unknown */ protected function _getElementProperties($element) { foreach($this->_properties as $namespace => $namespaceProperties) { if (array_key_exists($element, $namespaceProperties)) { return array($namespace, $namespaceProperties[$element]); } } throw new InvalidArgumentException("$element is no valid property of " . get_class($this)); } protected function _parseNamespace($nameSpace, SimpleXMLElement $properties) { // fetch data from Contacts namespace $children = $properties->children("uri:$nameSpace"); foreach ($children as $elementName => $xmlElement) { $elementName = lcfirst($elementName); if (!isset($this->_properties[$nameSpace][$elementName])) { continue; } list (, $elementProperties) = $this->_getElementProperties($elementName); switch ($elementProperties['type']) { case 'container': if (!empty($elementProperties['multiple'])) { $property = (array) $this->$elementName; + if (isset($elementProperties['class'])) { $property[] = new $elementProperties['class']($xmlElement); } else { $property[] = (string) $xmlElement; } - } - else if (isset($elementProperties['childElement'])) { + } else if (isset($elementProperties['childElement'])) { $property = array(); $childElement = ucfirst($elementProperties['childElement']); foreach ($xmlElement->$childElement as $subXmlElement) { if (isset($elementProperties['class'])) { $property[] = new $elementProperties['class']($subXmlElement); } else { $property[] = (string) $subXmlElement; } } } else { $subClassName = isset($elementProperties['class']) ? $elementProperties['class'] : get_class($this) . ucfirst($elementName); $property = new $subClassName($xmlElement); } break; case 'datetime': $property = new DateTime((string) $xmlElement, new DateTimeZone('UTC')); break; case 'number': $property = (int) $xmlElement; break; default: $property = (string) $xmlElement; break; } if (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') { $property = base64_decode($property); } $this->$elementName = $property; } } public function &__get($name) { $this->_getElementProperties($name); return $this->_elements[$name]; } public function __set($name, $value) { list ($nameSpace, $properties) = $this->_getElementProperties($name); if ($properties['type'] == 'datetime' && !$value instanceof DateTime) { throw new InvalidArgumentException("value for $name must be an instance of DateTime"); } if (!array_key_exists($name, $this->_elements) || $this->_elements[$name] != $value) { $this->_elements[$name] = $value; $this->_isDirty = true; } } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Registry.php b/lib/ext/Syncroton/Registry.php index f67e2d3..e129f8d 100644 --- a/lib/ext/Syncroton/Registry.php +++ b/lib/ext/Syncroton/Registry.php @@ -1,445 +1,481 @@ offsetExists($index)) { require_once 'Zend/Exception.php'; throw new Zend_Exception("No entry is registered for key '$index'"); } return $instance->offsetGet($index); } /** * returns content state backend * * creates Syncroton_Backend_Content on the fly if not before via * Syncroton_Registry::set(self::CONTENTSTATEBACKEND, $backend); * * @return Syncroton_Backend_IContent */ public static function getContentStateBackend() { if (!self::isRegistered(self::CONTENTSTATEBACKEND)) { self::set(self::CONTENTSTATEBACKEND, new Syncroton_Backend_Content(self::getDatabase())); } return self::get(self::CONTENTSTATEBACKEND); } /** * returns device backend * * creates Syncroton_Backend_Device on the fly if not before via * Syncroton_Registry::set(self::DEVICEBACKEND, $backend); * * @return Syncroton_Backend_IDevice */ public static function getDeviceBackend() { if (!self::isRegistered(self::DEVICEBACKEND)) { self::set(self::DEVICEBACKEND, new Syncroton_Backend_Device(self::getDatabase())); } return self::get(self::DEVICEBACKEND); } /** * returns folder backend * * creates Syncroton_Backend_Folder on the fly if not before via * Syncroton_Registry::set(self::FOLDERBACKEND, $backend); * * @return Syncroton_Backend_IFolder */ public static function getFolderBackend() { if (!self::isRegistered(self::FOLDERBACKEND)) { self::set(self::FOLDERBACKEND, new Syncroton_Backend_Folder(self::getDatabase())); } return self::get(self::FOLDERBACKEND); } - + /** * Return maximum ping interval (HeartbeatInterval) value (in seconds) * * @return int */ public static function getPingInterval() { if (!self::isRegistered(self::PING_INTERVAL)) { return 3540; // 59 minutes limit defined in Activesync protocol spec. } return self::get(self::PING_INTERVAL); } + + /** + /** + * Return maximum ping interval (HeartbeatInterval) value (in seconds) + * + * @return int + */ + public static function getMaxPingInterval() + { + if (!self::isRegistered(self::MAX_PING_INTERVAL)) { + return Syncroton_Command_Ping::MAX_PING_INTERVAL; + } + + return self::get(self::MAX_PING_INTERVAL); + } /** * return ping timeout * * sleep "ping timeout" seconds between folder checks in Ping and Sync command * * @return int */ public static function getPingTimeout() { if (!self::isRegistered(self::PING_TIMEOUT)) { return 60; } return self::get(self::PING_TIMEOUT); } /** * returns policy backend * * creates Syncroton_Backend_Policy on the fly if not set before via * Syncroton_Registry::set(self::POLICYBACKEND, $backend); * * @return Syncroton_Backend_ISyncState */ public static function getPolicyBackend() { if (!self::isRegistered(self::POLICYBACKEND)) { self::set(self::POLICYBACKEND, new Syncroton_Backend_Policy(self::getDatabase())); } return self::get(self::POLICYBACKEND); } /** * return quiet time * * don't check folders if last sync was "quiet time" seconds ago * * @return int */ public static function getQuietTime() { if (!self::isRegistered(self::QUIET_TIME)) { return 180; } return self::get(self::QUIET_TIME); } - + + /** + * return session validation function + * + * This function is used in long running requests like ping & sync to + * validate user session. Returns false if session is not valid any longer + * + * @return callable + */ + public static function getSessionValidator() + { + if (!self::isRegistered(self::SESSION_VALIDATOR)) { + self::set(self::SESSION_VALIDATOR, function() { + return true; + }); + } + + return self::get(self::SESSION_VALIDATOR); + } + /** * returns syncstate backend * * creates Syncroton_Backend_SyncState on the fly if not before via * Syncroton_Registry::set(self::SYNCSTATEBACKEND, $backend); * * @return Syncroton_Backend_ISyncState */ public static function getSyncStateBackend() { if (!self::isRegistered(self::SYNCSTATEBACKEND)) { self::set(self::SYNCSTATEBACKEND, new Syncroton_Backend_SyncState(self::getDatabase())); } return self::get(self::SYNCSTATEBACKEND); } /** * setter method, basically same as offsetSet(). * * This method can be called from an object of type Syncroton_Registry, or it * can be called statically. In the latter case, it uses the default * static instance stored in the class. * * @param string $index The location in the ArrayObject in which to store * the value. * @param mixed $value The object to store in the ArrayObject. * @return void */ public static function set($index, $value) { $instance = self::getInstance(); $instance->offsetSet($index, $value); } public static function setDatabase(Zend_Db_Adapter_Abstract $db) { self::set(self::DATABASE, $db); } public static function setCalendarDataClass($className) { if (!class_exists($className)) { throw new InvalidArgumentException('invalid $_className provided'); } self::set(self::CALENDAR_DATA_CLASS, $className); } public static function setContactsDataClass($className) { if (!class_exists($className)) { throw new InvalidArgumentException('invalid $_className provided'); } self::set(self::CONTACTS_DATA_CLASS, $className); } public static function setEmailDataClass($className) { if (!class_exists($className)) { throw new InvalidArgumentException('invalid $_className provided'); } self::set(self::EMAIL_DATA_CLASS, $className); } public static function setNotesDataClass($className) { if (!class_exists($className)) { throw new InvalidArgumentException('invalid $_className provided'); } self::set(self::NOTES_DATA_CLASS, $className); } public static function setTasksDataClass($className) { if (!class_exists($className)) { throw new InvalidArgumentException('invalid $_className provided'); } self::set(self::TASKS_DATA_CLASS, $className); } public static function setGALDataClass($className) { if (!class_exists($className)) { throw new InvalidArgumentException('invalid $_className provided'); } self::set(self::GAL_DATA_CLASS, $className); } public static function setTransactionManager($manager) { self::set(self::TRANSACTIONMANAGER, $manager); } /** * Returns TRUE if the $index is a named value in the registry, * or FALSE if $index was not found in the registry. * * @param string $index * @return boolean */ public static function isRegistered($index) { if (self::$_registry === null) { return false; } return self::$_registry->offsetExists($index); } /** * Constructs a parent ArrayObject with default * ARRAY_AS_PROPS to allow acces as an object * * @param array $array data array * @param integer $flags ArrayObject flags */ public function __construct($array = array(), $flags = parent::ARRAY_AS_PROPS) { parent::__construct($array, $flags); } /** * @param string $index * @returns mixed * * Workaround for http://bugs.php.net/bug.php?id=40442 (ZF-960). */ public function offsetExists($index) { return array_key_exists($index, $this); } } diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php index 0fa4855..d60ab9a 100644 --- a/lib/ext/Syncroton/Server.php +++ b/lib/ext/Syncroton/Server.php @@ -1,446 +1,452 @@ */ /** * 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!
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']); + 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__); + $this->_logDomDocument(Zend_Log::WARN, $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 + // unpack device type $length = ord(fread($stream, 1)); - if ($length > 0) { + 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 { + } 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'), + 'saveInSent' => $request->getQuery('SaveInSent') == 'T', + 'collectionId' => $request->getQuery('CollectionId'), 'itemId' => $request->getQuery('ItemId'), 'attachmentName' => $request->getQuery('AttachmentName'), - 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T' + 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T' ); } - - $result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']); - $result['contentType'] = $request->getServer('CONTENT_TYPE'); + + $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 + 'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null ))); } return $device; } + + public static function validateSession() + { + $validatorFunction = Syncroton_Registry::getSessionValidator(); + return $validatorFunction(); + } } diff --git a/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/CodePage18.php b/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/CodePage18.php index 6d14fe9..0bd3383 100644 --- a/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/CodePage18.php +++ b/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/CodePage18.php @@ -1,65 +1,65 @@ */ /** * class documentation * * @package Wbxml * @subpackage ActiveSync */ class Syncroton_Wbxml_Dtd_ActiveSync_CodePage18 extends Syncroton_Wbxml_Dtd_ActiveSync_Abstract { protected $_codePageNumber = 18; protected $_codePageName = 'Settings'; protected $_tags = array( 'Settings' => 0x05, 'Status' => 0x06, 'Get' => 0x07, 'Set' => 0x08, 'Oof' => 0x09, 'OofState' => 0x0a, 'StartTime' => 0x0b, 'EndTime' => 0x0c, 'OofMessage' => 0x0d, 'AppliesToInternal' => 0x0e, 'AppliesToExternalKnown' => 0x0f, 'AppliesToExternalUnknown' => 0x10, 'Enabled' => 0x11, 'ReplyMessage' => 0x12, 'BodyType' => 0x13, 'DevicePassword' => 0x14, 'Password' => 0x15, 'DeviceInformation' => 0x16, 'Model' => 0x17, 'IMEI' => 0x18, 'FriendlyName' => 0x19, 'OS' => 0x1a, 'OSLanguage' => 0x1b, 'PhoneNumber' => 0x1c, 'UserInformation' => 0x1d, 'EmailAddresses' => 0x1e, 'SMTPAddress' => 0x1f, 'UserAgent' => 0x20, 'EnableOutboundSMS' => 0x21, 'MobileOperator' => 0x22, 'PrimarySmtpAddress' => 0x23, 'Accounts' => 0x24, 'Account' => 0x25, 'AccountId' => 0x26, 'AccountName' => 0x27, 'UserDisplayName' => 0x28, 'SendDisabled' => 0x29, 'RightsManagementInformation' => 0x2b, ); -} \ No newline at end of file +}