diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acc9953 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.patch +/config/config.inc.php +/config/defaults.inc.php +/composer.json +/composer.lock +/lib/plugins +/lib/ext/Roundcube +/logs/*.log +/tests/.phpunit.result.cache +/vendor diff --git a/lib/ext/Syncroton/Backend/ABackend.php b/lib/ext/Syncroton/Backend/ABackend.php index 44f410f..7e63d8d 100644 --- a/lib/ext/Syncroton/Backend/ABackend.php +++ b/lib/ext/Syncroton/Backend/ABackend.php @@ -1,211 +1,212 @@ */ /** * 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 $_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_AEntry $model * @return Syncroton_Model_AEntry */ public function create($model) { if (! $model instanceof $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 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; } return $data; } /** - * @param string $_id + * @param string $id + * * @throws Syncroton_Exception_NotFound - * @return Syncroton_Model_IDevice + * @return Syncroton_Model_AEntry */ 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) { 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')); } $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; + $id = $id instanceof $this->_modelInterfaceName ? $id->id : $id; // @phpstan-ignore-line $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); } $data = $this->_convertModelToArray($model); $this->_db->update($this->_tablePrefix . $this->_tableName, $data, array( - 'id = ?' => $model->id + 'id = ?' => $model->id // @phpstan-ignore-line )); - return $this->get($model->id); + return $this->get($model->id); // @phpstan-ignore-line } /** * 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/Backend/Content.php b/lib/ext/Syncroton/Backend/Content.php index b186b00..f43231f 100644 --- a/lib/ext/Syncroton/Backend/Content.php +++ b/lib/ext/Syncroton/Backend/Content.php @@ -1,116 +1,120 @@ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * sql backend class for the folder state * * @package Syncroton * @subpackage Backend */ class Syncroton_Backend_Content extends Syncroton_Backend_ABackend implements Syncroton_Backend_IContent { protected $_tableName = 'content'; protected $_modelClassName = 'Syncroton_Model_Content'; protected $_modelInterfaceName = 'Syncroton_Model_IContent'; /** * mark state as deleted. The state gets removed finally, * when the synckey gets validated during next sync. * - * @param Syncroton_Model_IContent|string $_id + * @param Syncroton_Model_IContent|string $id + * + * @return bool */ public function delete($id) { $id = $id instanceof $this->_modelInterfaceName ? $id->id : $id; $this->_db->update($this->_tablePrefix . 'content', array( 'is_deleted' => 1 ), array( 'id = ?' => $id )); - + + return true; } /** * @param Syncroton_Model_IDevice|string $deviceId * @param Syncroton_Model_IFolder|string $folderId - * @param string $_contentId + * @param string $contentId * @return Syncroton_Model_IContent */ public function getContentState($deviceId, $folderId, $contentId) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $folderId = $folderId instanceof Syncroton_Model_IFolder ? $folderId->id : $folderId; $select = $this->_db->select() ->from($this->_tablePrefix . 'content') ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('folder_id') . ' = ?', $folderId) ->where($this->_db->quoteIdentifier('contentid') . ' = ?', $contentId) ->where($this->_db->quoteIdentifier('is_deleted') . ' = ?', 0); $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); } /** * get array of ids which got send to the client for a given class * * @param Syncroton_Model_IDevice|string $deviceId * @param Syncroton_Model_IFolder|string $folderId + * @param int $syncKey * @return array */ - public function getFolderState($deviceId, $folderId) + public function getFolderState($deviceId, $folderId, $syncKey = null) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $folderId = $folderId instanceof Syncroton_Model_IFolder ? $folderId->id : $folderId; $select = $this->_db->select() ->from($this->_tablePrefix . 'content', 'contentid') ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('folder_id') . ' = ?', $folderId) ->where($this->_db->quoteIdentifier('is_deleted') . ' = ?', 0); $stmt = $this->_db->query($select); $result = $stmt->fetchAll(Zend_Db::FETCH_COLUMN); return $result; } /** * reset list of stored id * * @param Syncroton_Model_IDevice|string $deviceId * @param Syncroton_Model_IFolder|string $folderId */ public function resetState($deviceId, $folderId) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $folderId = $folderId instanceof Syncroton_Model_IFolder ? $folderId->id : $folderId; $where = array( $this->_db->quoteInto($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId), $this->_db->quoteInto($this->_db->quoteIdentifier('folder_id') . ' = ?', $folderId) ); $this->_db->delete($this->_tablePrefix . 'content', $where); } } diff --git a/lib/ext/Syncroton/Backend/Device.php b/lib/ext/Syncroton/Backend/Device.php index 0daea27..e668bb0 100644 --- a/lib/ext/Syncroton/Backend/Device.php +++ b/lib/ext/Syncroton/Backend/Device.php @@ -1,94 +1,94 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Backend */ class Syncroton_Backend_Device extends Syncroton_Backend_ABackend implements Syncroton_Backend_IDevice { protected $_tableName = 'device'; protected $_modelClassName = 'Syncroton_Model_Device'; protected $_modelInterfaceName = 'Syncroton_Model_IDevice'; /** * return device for this user * - * @param string $userId + * @param string $ownerId * @param string $deviceId * @throws Syncroton_Exception_NotFound * @return Syncroton_Model_Device */ public function getUserDevice($ownerId, $deviceId) { $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where('owner_id = ?', $ownerId) ->where('deviceid = ?', $deviceId); $stmt = $this->_db->query($select); $data = $stmt->fetch(); if ($data === false) { throw new Syncroton_Exception_NotFound('id not found'); } foreach ($data as $key => $value) { unset($data[$key]); $data[$this->_toCamelCase($key, false)] = $value; } $model = new $this->_modelClassName($data); return $model; } /** * 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(); } /** * Returns OOF information * * @param array $request Oof/Get request data * - * @return Syncroton_Model_Oof Response object or NULL if OOF is not supported + * @return Syncroton_Model_Oof|null Response object or NULL if OOF is not supported * @throws Syncroton_Exception_Status */ public function getOOF($request) { return null; // not implemented } /** * Sets OOF information * * @param Syncroton_Model_Oof $request Request object * * @throws Syncroton_Exception_Status */ public function setOOF($request) { // not implemented } } diff --git a/lib/ext/Syncroton/Backend/Folder.php b/lib/ext/Syncroton/Backend/Folder.php index b7bf967..da940e0 100644 --- a/lib/ext/Syncroton/Backend/Folder.php +++ b/lib/ext/Syncroton/Backend/Folder.php @@ -1,145 +1,136 @@ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * sql backend class for the folder state * * @package Syncroton * @subpackage Backend */ class Syncroton_Backend_Folder extends Syncroton_Backend_ABackend implements Syncroton_Backend_IFolder { protected $_tableName = 'folder'; protected $_modelClassName = 'Syncroton_Model_Folder'; protected $_modelInterfaceName = 'Syncroton_Model_IFolder'; /** * (non-PHPdoc) * @see Syncroton_Backend_IFolder::getFolder() */ public function getFolder($deviceId, $folderId) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('folderid') . ' = ?', $folderId); $stmt = $this->_db->query($select); $data = $stmt->fetch(); if ($data === false) { throw new Syncroton_Exception_NotFound('id not found'); } return $this->_getObject($data); } /** * (non-PHPdoc) * @see Syncroton_Backend_IFolder::getFolderState() */ public function getFolderState($deviceId, $class) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('class') . ' = ?', $class); $result = array(); $stmt = $this->_db->query($select); while ($data = $stmt->fetch()) { $result[$data['folderid']] = $this->_getObject($data); } return $result; } /** * (non-PHPdoc) * @see Syncroton_Backend_IFolder::resetState() */ public function resetState($deviceId) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $where = array( $this->_db->quoteInto($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ); $this->_db->delete($this->_tablePrefix . $this->_tableName, $where); } /** * (non-PHPdoc) * @see Syncroton_Backend_IFolder::hasHierarchyChanges() */ public function hasHierarchyChanges($device) { return false; // not implemented } /** * (non-PHPdoc) * @see Syncroton_Backend_ABackend::_fromCamelCase() */ protected function _fromCamelCase($string) { switch ($string) { case 'displayName': case 'parentId': return strtolower($string); - break; case 'serverId': return 'folderid'; - break; default: return parent::_fromCamelCase($string); - - break; } } /** * (non-PHPdoc) * @see Syncroton_Backend_ABackend::_toCamelCase() */ protected function _toCamelCase($string, $ucFirst = true) { switch ($string) { case 'displayname': return 'displayName'; - break; case 'parentid': return 'parentId'; - break; case 'folderid': return 'serverId'; - break; default: return parent::_toCamelCase($string, $ucFirst); - - break; } } } diff --git a/lib/ext/Syncroton/Backend/IBackend.php b/lib/ext/Syncroton/Backend/IBackend.php index ec111cd..6763f51 100644 --- a/lib/ext/Syncroton/Backend/IBackend.php +++ b/lib/ext/Syncroton/Backend/IBackend.php @@ -1,51 +1,51 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Backend */ interface Syncroton_Backend_IBackend { /** * Create a new device * - * @param Syncroton_Model_IDevice $device - * @return Syncroton_Model_IDevice + * @param Syncroton_Model_IEntry $model + * @return Syncroton_Model_IEntry */ public function create($model); /** * Deletes one or more existing devices * - * @param string|array $_id - * @return void + * @param string|Syncroton_Model_IEntry $id + * @return bool */ public function delete($id); /** * Return a single device * - * @param string $_id - * @return Syncroton_Model_IDevice + * @param string $id + * @return Syncroton_Model_IEntry */ public function get($id); /** * Upates an existing persistent record * - * @param Syncroton_Model_IDevice $_device - * @return Syncroton_Model_IDevice + * @param Syncroton_Model_IEntry $model + * @return Syncroton_Model_IEntry */ public function update($model); } diff --git a/lib/ext/Syncroton/Backend/IContent.php b/lib/ext/Syncroton/Backend/IContent.php index 8974e49..b3c2590 100644 --- a/lib/ext/Syncroton/Backend/IContent.php +++ b/lib/ext/Syncroton/Backend/IContent.php @@ -1,45 +1,46 @@ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * sql backend class for the folder state * * @package Syncroton * @subpackage Backend */ interface Syncroton_Backend_IContent extends Syncroton_Backend_IBackend { /** * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId * @param string $_contentId * @return Syncroton_Model_IContent */ public function getContentState($_deviceId, $_folderId, $_contentId); /** * get array of ids which got send to the client for a given class * * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId + * @param int $_syncKey * @return array */ - public function getFolderState($_deviceId, $_folderId); + public function getFolderState($_deviceId, $_folderId, $_syncKey = null); /** * reset list of stored id * * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId */ public function resetState($_deviceId, $_folderId); } diff --git a/lib/ext/Syncroton/Backend/ISyncState.php b/lib/ext/Syncroton/Backend/ISyncState.php index 2874ef9..a52f3aa 100644 --- a/lib/ext/Syncroton/Backend/ISyncState.php +++ b/lib/ext/Syncroton/Backend/ISyncState.php @@ -1,49 +1,44 @@ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * sql backend class for the folder state * * @package Syncroton * @subpackage Backend */ interface Syncroton_Backend_ISyncState extends Syncroton_Backend_IBackend { - /** - * create new sync state - * - * @param Syncroton_Model_IDevice $model - * @param boolean $keepPreviousSyncState - */ - #public function create($model, $keepPreviousSyncState = true); - /** * always returns the latest syncstate * * @param Syncroton_Model_IDevice|string $deviceId * @param Syncroton_Model_IFolder|string $folderId * @return Syncroton_Model_SyncState */ public function getSyncState($deviceId, $folderId); - public function resetState($_deviceId, $_type); + public function haveNext($deviceId, $folderId, $syncKey); + + public function resetState($deviceId, $type); /** * get array of ids which got send to the client for a given class * - * @param Syncroton_Model_Device $_deviceId - * @param string $_class + * @param Syncroton_Model_IDevice|string $deviceId + * @param Syncroton_Model_IFolder|string $folderId + * @param int $syncKey * * @return Syncroton_Model_SyncState|false */ - public function validate($_deviceId, $_syncKey, $_type); + public function validate($deviceId, $folderId, $syncKey); } diff --git a/lib/ext/Syncroton/Backend/Policy.php b/lib/ext/Syncroton/Backend/Policy.php index 7322fad..dee8d95 100644 --- a/lib/ext/Syncroton/Backend/Policy.php +++ b/lib/ext/Syncroton/Backend/Policy.php @@ -1,71 +1,71 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Backend */ -class Syncroton_Backend_Policy extends Syncroton_Backend_ABackend #implements Syncroton_Backend_IDevice +class Syncroton_Backend_Policy extends Syncroton_Backend_ABackend { protected $_tableName = 'policy'; protected $_modelClassName = 'Syncroton_Model_Policy'; protected $_modelInterfaceName = 'Syncroton_Model_IPolicy'; /** * convert iteratable object to array * - * @param unknown $model + * @param Syncroton_Model_IXMLEntry $model * @return array */ protected function _convertModelToArray($model) { $policyValues = $model->getProperties('Provision'); $policy = array(); foreach ($policyValues as $policyName) { if ($model->$policyName !== NULL) { $policy[$policyName] = $model->$policyName; } unset($model->$policyName); } $data = parent::_convertModelToArray($model); $data['json_policy'] = Zend_Json::encode($policy); return $data; } /** * convert array to object * * @param array $data * @return object */ protected function _getObject($data) { $policy = Zend_Json::decode($data['json_policy']); foreach ($policy as $policyKey => $policyValue) { $data[$policyKey] = $policyValue; } unset($data['json_policy']); return parent::_getObject($data); } } diff --git a/lib/ext/Syncroton/Backend/SyncState.php b/lib/ext/Syncroton/Backend/SyncState.php index 3fa5a94..6f5fa0e 100644 --- a/lib/ext/Syncroton/Backend/SyncState.php +++ b/lib/ext/Syncroton/Backend/SyncState.php @@ -1,209 +1,215 @@ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * sql backend class for the folder state * * @package Syncroton * @subpackage Backend */ class Syncroton_Backend_SyncState extends Syncroton_Backend_ABackend implements Syncroton_Backend_ISyncState { protected $_tableName = 'synckey'; protected $_modelClassName = 'Syncroton_Model_SyncState'; protected $_modelInterfaceName = 'Syncroton_Model_ISyncState'; /** * (non-PHPdoc) * @see Syncroton_Backend_ISyncState::create() */ public function create($model, $keepPreviousSyncState = true) { $state = parent::create($model); if ($keepPreviousSyncState !== true) { // remove all other synckeys $this->_deleteOtherStates($state); } return $state; } /** * (non-PHPdoc) * @see Syncroton_Backend_ABackend::_convertModelToArray() */ protected function _convertModelToArray($model) { $model = parent::_convertModelToArray($model); $model['pendingdata'] = isset($model['pendingdata']) && is_array($model['pendingdata']) ? Zend_Json::encode($model['pendingdata']) : null; return $model; } /** * * @param Syncroton_Model_ISyncState $state */ protected function _deleteOtherStates(Syncroton_Model_ISyncState $state) { // remove all other synckeys $where = array( 'device_id = ?' => $state->deviceId, 'type = ?' => $state->type, 'counter != ?' => $state->counter ); $this->_db->delete($this->_tablePrefix . $this->_tableName, $where); return true; } /** * (non-PHPdoc) * @see Syncroton_Backend_ABackend::_getObject() */ protected function _getObject($data) { $model = parent::_getObject($data); if ($model->pendingdata !== NULL) { $model->pendingdata = Zend_Json::decode($model->pendingdata); } return $model; } /** * (non-PHPdoc) * @see Syncroton_Backend_ISyncState::getSyncState() */ public function getSyncState($deviceId, $folderId) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $folderId = $folderId instanceof Syncroton_Model_IFolder ? $folderId->id : $folderId; $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('type') . ' = ?', $folderId) ->order('counter DESC') ->limit(1); $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); } /** * delete all stored synckeys for given type * * @param Syncroton_Model_IDevice|string $deviceId * @param Syncroton_Model_IFolder|string $folderId */ public function resetState($deviceId, $folderId) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $folderId = $folderId instanceof Syncroton_Model_IFolder ? $folderId->id : $folderId; $where = array( $this->_db->quoteInto($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId), $this->_db->quoteInto($this->_db->quoteIdentifier('type') . ' = ?', $folderId) ); $this->_db->delete($this->_tablePrefix . $this->_tableName, $where); } /** * get array of ids which got send to the client for a given class * - * @param Syncroton_Model_IDevice|string $deviceId - * @param Syncroton_Model_IFolder|string $folderId + * @param Syncroton_Model_IDevice|string $deviceId + * @param Syncroton_Model_IFolder|string $folderId + * @param int $syncKey * * @return Syncroton_Model_SyncState|false */ public function validate($deviceId, $folderId, $syncKey) { $deviceId = $deviceId instanceof Syncroton_Model_IDevice ? $deviceId->id : $deviceId; $folderId = $folderId instanceof Syncroton_Model_IFolder ? $folderId->id : $folderId; $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('counter') . ' = ?', $syncKey) ->where($this->_db->quoteIdentifier('type') . ' = ?', $folderId); $stmt = $this->_db->query($select); $data = $stmt->fetch(); $stmt = null; # see https://bugs.php.net/bug.php?id=44081 if ($data === false) { return false; } $state = $this->_getObject($data); // check if this was the latest syncKey $select = $this->_db->select() ->from($this->_tablePrefix . $this->_tableName) ->where($this->_db->quoteIdentifier('device_id') . ' = ?', $deviceId) ->where($this->_db->quoteIdentifier('counter') . ' = ?', $syncKey + 1) ->where($this->_db->quoteIdentifier('type') . ' = ?', $folderId); $stmt = $this->_db->query($select); $moreRecentStateData = $stmt->fetch(); $stmt = null; # see https://bugs.php.net/bug.php?id=44081 // found more recent synckey => the last sync repsone got not received by the client if ($moreRecentStateData !== false) { // undelete entries marked as deleted in Syncroton_content table $this->_db->update($this->_tablePrefix . 'content', array( 'is_deleted' => 0, ), array( 'device_id = ?' => $deviceId, 'folder_id = ?' => $folderId, 'creation_synckey = ?' => $state->counter, 'is_deleted = ?' => 1 )); } else { // finally delete all entries marked for removal in Syncroton_content table $this->_db->delete($this->_tablePrefix . 'content', array( 'device_id = ?' => $deviceId, 'folder_id = ?' => $folderId, 'is_deleted = ?' => 1 )); } // remove all other synckeys $this->_deleteOtherStates($state); // remove entries from Syncroton_content table with an creation_synckey bigger than current one $this->_db->delete($this->_tablePrefix . 'content', array( 'device_id = ?' => $deviceId, 'folder_id = ?' => $folderId, 'creation_synckey > ?' => $state->counter, )); return $state; } + + public function haveNext($deviceid, $folderid, $sync_key) + { + return false; // not implemented + } } diff --git a/lib/ext/Syncroton/Command/AutoDiscover.php b/lib/ext/Syncroton/Command/AutoDiscover.php index c2415a1..30d748c 100644 --- a/lib/ext/Syncroton/Command/AutoDiscover.php +++ b/lib/ext/Syncroton/Command/AutoDiscover.php @@ -1,126 +1,127 @@ */ /** * class to handle AutoDiscover command * * @package Syncroton * @subpackage Command */ -class Syncroton_Command_AutoDiscover implements Syncroton_Command_ICommand +class Syncroton_Command_AutoDiscover extends Syncroton_Command_Wbxml { /** * the domDocucment containing the xml request from the client * * @var DOMDocument */ protected $requestBody; protected $emailAddress; public $mobileSyncUrl; public $certEnrollUrl; /** * constructor of this class * - * @param DOMDocument $_requestBody - * @param Syncroton_Model_IDevice $_device - * @param string $_policyKey + * @param DOMDocument $requestBody + * @param Syncroton_Model_IDevice $device + * @param array $requestParams */ - public function __construct($requestBody, Syncroton_Model_IDevice $device = null, $policyKey = null) + // @phpstan-ignore-next-line + public function __construct($requestBody, Syncroton_Model_IDevice $device, $requestParams = []) { $this->requestBody = $requestBody; } /** * process the incoming data */ public function handle() { $xpath = new DomXPath($this->requestBody); $xpath->registerNamespace('2006', 'http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006'); $nodes = $xpath->query('//2006:Autodiscover/2006:Request/2006:EMailAddress'); if ($nodes->length === 0) { throw new Syncroton_Exception(); } $this->emailAddress = $nodes->item(0)->nodeValue; } /** * create the response * * @return DOMDocument */ public function getResponse() { // Creates an instance of the DOMImplementation class $imp = new DOMImplementation(); // Creates a DOMDocument instance $document = $imp->createDocument("http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006", 'Autodiscover'); $document->xmlVersion = '1.0'; $document->encoding = 'UTF-8'; $document->formatOutput = false; $response = $document->documentElement->appendChild($document->createElement('Response')); $user = $response->appendChild($document->createElement('User')); $user->appendChild($document->createElement('EMailAddress', $this->emailAddress)); $settings = $document->createElement('Settings'); if (!empty($this->mobileSyncUrl)) { $server = $document->createElement('Server'); $server->appendChild($document->createElement('Type', 'MobileSync')); $server->appendChild($document->createElement('Url', $this->mobileSyncUrl)); $server->appendChild($document->createElement('Name', $this->mobileSyncUrl)); $settings->appendChild($server); } if (!empty($this->certEnrollUrl)) { $server = $document->createElement('Server'); $server->appendChild($document->createElement('Type', 'CertEnroll')); $server->appendChild($document->createElement('Url', $this->certEnrollUrl)); $server->appendChild($document->createElement('Name')); $server->appendChild($document->createElement('ServerData', 'CertEnrollTemplate')); $settings->appendChild($server); } if ($settings->hasChildNodes()) { $action = $response->appendChild($document->createElement('Action')); $action->appendChild($settings); } return $document; } /** * return headers of command * * @return array list of headers */ public function getHeaders() { return array( 'Content-Type' => 'text/xml;charset=utf-8' ); } } diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php index 5e966b8..d9f8489 100644 --- a/lib/ext/Syncroton/Command/FolderCreate.php +++ b/lib/ext/Syncroton/Command/FolderCreate.php @@ -1,155 +1,155 @@ */ /** * class to handle ActiveSync FolderCreate command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml { protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderCreate'; /** - * @var Syncroton_Model_Folder + * @var Syncroton_Model_IFolder */ protected $_folder; /** * @var int */ protected $_status; /** * 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) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed."); $this->_status = Syncroton_Command_FolderSync::STATUS_INVALID_SYNC_KEY; return; } $folder = new Syncroton_Model_Folder($xml); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " parentId: {$folder->parentId} displayName: {$folder->displayName}"); if (!strlen($folder->displayName)) { $this->_status = Syncroton_Command_FolderSync::STATUS_MISFORMATTED; return; } 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; } try { $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); $this->_folder = $dataController->createFolder($folder); if (!$this->_folder) { $this->_status = Syncroton_Command_FolderSync::STATUS_UNKNOWN_ERROR; } else { $this->_folder->class = $folder->class; - $this->_folder->deviceId = $this->_device; + $this->_folder->deviceId = $this->_device->id; $this->_folder->creationTime = $this->_syncTimeStamp; // Check if the folder already exists to avoid a duplicate insert attempt in db try { $this->_folderBackend->getFolder($this->_device, $this->_folder->serverId); if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " Attempted to create a folder that already exists. parentId: {$folder->parentId} displayName: {$folder->displayName}"); // The folder already exists $this->_status = Syncroton_Command_FolderSync::STATUS_FOLDER_EXISTS; } catch (Syncroton_Exception_NotFound $e) { // This is the normal case if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); $this->_folderBackend->create($this->_folder); } } } catch (Syncroton_Exception_Status $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); $this->_status = $e->getCode(); } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); $this->_status = Syncroton_Command_FolderSync::STATUS_UNKNOWN_ERROR; } } /** * generate FolderCreate response */ public function getResponse() { $folderCreate = $this->_outputDom->documentElement; if ($this->_status) { $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', $this->_status)); } 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/FolderDelete.php b/lib/ext/Syncroton/Command/FolderDelete.php index f294a86..314d451 100644 --- a/lib/ext/Syncroton/Command/FolderDelete.php +++ b/lib/ext/Syncroton/Command/FolderDelete.php @@ -1,98 +1,98 @@ */ /** * class to handle ActiveSync FolderDelete command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderDelete extends Syncroton_Command_Wbxml { protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderDelete'; /** - * @var Syncroton_Model_SyncState + * @var Syncroton_Model_IFolder */ protected $_folder; /** * parse FolderDelete request */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); // parse xml request $syncKey = (int)$xml->SyncKey; $serverId = (string)$xml->ServerId; 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; } try { $folder = $this->_folderBackend->getFolder($this->_device, $serverId); $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); // delete folder in data backend $dataController->deleteFolder($folder); } catch (Syncroton_Exception_NotFound $senf) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage()); return; } // delete folder in syncState backend $this->_folderBackend->delete($folder); $this->_folder = $folder; } /** * generate FolderDelete response * * @todo currently we support only the main folder which contains all contacts/tasks/events/notes per class */ public function getResponse() { $folderDelete = $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."); $folderDelete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_INVALID_SYNC_KEY)); } elseif (!$this->_folder instanceof Syncroton_Model_IFolder) { $folderDelete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_FOLDER_NOT_FOUND)); } else { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; // store folder in state backend $this->_syncStateBackend->update($this->_syncState); // create xml output $folderDelete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_SUCCESS)); $folderDelete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php index 6319ab0..13218cf 100644 --- a/lib/ext/Syncroton/Command/FolderSync.php +++ b/lib/ext/Syncroton/Command/FolderSync.php @@ -1,287 +1,287 @@ */ /** * 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(); } // 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; + $add->deviceId = $this->_device->id; 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; } // Find changes in case backend does not support folder changes detection. // On some backends getChangedFolders() can return an empty result. // We make sure all is up-to-date comparing folder properties. foreach ($clientFoldersIds as $folderId) { if (isset($serverFolders[$folderId])) { $c = $clientFolders[$folderId]; $s = $serverFolders[$folderId]; if ($c->displayName !== $s->displayName || strval($c->parentId) !== strval($s->parentId) || $c->type != $s->type ) { $c->displayName = $s->displayName; $c->parentId = $s->parentId; $c->type = $s->type; $updates[] = $c; } } } // 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/FolderUpdate.php b/lib/ext/Syncroton/Command/FolderUpdate.php index 55d1e54..e3134bc 100644 --- a/lib/ext/Syncroton/Command/FolderUpdate.php +++ b/lib/ext/Syncroton/Command/FolderUpdate.php @@ -1,109 +1,109 @@ */ /** * class to handle ActiveSync FolderUpdate command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderUpdate extends Syncroton_Command_Wbxml { protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderUpdate'; /** - * @var Syncroton_Model_SyncState + * @var Syncroton_Model_IFolder */ protected $_folder; /** * * @var Syncroton_Model_Folder */ protected $_folderUpdate; /** * parse FolderUpdate 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; } $updatedFolder = new Syncroton_Model_Folder($xml); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " parentId: {$updatedFolder->parentId} displayName: {$updatedFolder->displayName}"); try { $folder = $this->_folderBackend->getFolder($this->_device, $updatedFolder->serverId); $folder->displayName = $updatedFolder->displayName; $folder->parentId = $updatedFolder->parentId; $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); // update folder in data backend $dataController->updateFolder($folder); } catch (Syncroton_Exception_NotFound $senf) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage()); return; } // update folder status in Syncroton backend - $this->_folder = $this->_folderBackend->update($folder); + $this->_folder = $this->_folderBackend->update($folder); // @phpstan-ignore-line } /** * generate FolderUpdate response * * @todo currently we support only the main folder which contains all contacts/tasks/events/notes per class */ public function getResponse($_keepSession = FALSE) { $folderUpdate = $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."); $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_INVALID_SYNC_KEY)); } elseif (!$this->_folder instanceof Syncroton_Model_IFolder) { $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_FOLDER_NOT_FOUND)); } else { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; // store folder in state backend $this->_syncStateBackend->update($this->_syncState); // create xml output $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_SUCCESS)); $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/GetAttachment.php b/lib/ext/Syncroton/Command/GetAttachment.php index f098dec..e7d66fb 100644 --- a/lib/ext/Syncroton/Command/GetAttachment.php +++ b/lib/ext/Syncroton/Command/GetAttachment.php @@ -1,69 +1,67 @@ */ /** * class to handle ActiveSync GetAttachment command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_GetAttachment extends Syncroton_Command_Wbxml { /** * * @var string */ protected $_attachmentName; protected $_skipValidatePolicyKey = true; /** * process the XML file and add, change, delete or fetches data - * - * @return resource */ public function handle() { $this->_attachmentName = $this->_requestParameters['attachmentName']; } /** * this function generates the response for the client * * @return void */ public function getResponse() { $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_EMAIL, $this->_device, $this->_syncTimeStamp); $attachment = $dataController->getFileReference($this->_attachmentName); if (PHP_SAPI !== 'cli') { // cache for 3600 seconds $maxAge = 3600; $now = new DateTime('now', new DateTimeZone('UTC')); header('Cache-Control: private, max-age=' . $maxAge); header("Expires: " . gmdate('D, d M Y H:i:s', $now->modify("+{$maxAge} sec")->getTimestamp()) . " GMT"); // overwrite Pragma header from session header("Pragma: cache"); #header('Content-Disposition: attachment; filename="' . $filename . '"'); header("Content-Type: " . $attachment->contentType); } if (is_resource($attachment->data)) { fpassthru($attachment->data); } else { echo $attachment->data; } } } diff --git a/lib/ext/Syncroton/Command/ICommand.php b/lib/ext/Syncroton/Command/ICommand.php index d6edb23..b3df712 100644 --- a/lib/ext/Syncroton/Command/ICommand.php +++ b/lib/ext/Syncroton/Command/ICommand.php @@ -1,45 +1,45 @@ */ /** * interface for all Syncroton command classes * * @package Syncroton * @subpackage Command */ interface Syncroton_Command_ICommand { /** * constructor of this class * * @param resource $_requestBody * @param Syncroton_Model_IDevice $_device - * @param string $_policyKey + * @param array $_requestParams */ - public function __construct($_requestBody, Syncroton_Model_IDevice $_device, $_policyKey); + public function __construct($_requestBody, Syncroton_Model_IDevice $_device, $_requestParams = []); /** * process the incoming data */ public function handle(); /** * create the response */ public function getResponse(); /** * return headers of command * * @return array list of headers */ public function getHeaders(); } diff --git a/lib/ext/Syncroton/Command/MeetingResponse.php b/lib/ext/Syncroton/Command/MeetingResponse.php index 51883a5..696295f 100644 --- a/lib/ext/Syncroton/Command/MeetingResponse.php +++ b/lib/ext/Syncroton/Command/MeetingResponse.php @@ -1,84 +1,86 @@ */ /** * class to handle ActiveSync MeetingResponse command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_MeetingResponse extends Syncroton_Command_Wbxml { protected $_results = array(); protected $_defaultNameSpace = 'uri:MeetingResponse'; + protected $_documentElement = 'MeetingResponse'; /** * parse MeetingResponse request */ public function handle() { + /** @var Syncroton_Data_IDataCalendar $dataController */ $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_CALENDAR, $this->_device, $this->_syncTimeStamp); $xml = simplexml_import_dom($this->_requestBody); foreach ($xml as $meetingResponse) { $request = new Syncroton_Model_MeetingResponse($meetingResponse); try { $calendarId = $dataController->setAttendeeStatus($request); $this->_results[] = array( 'calendarId' => $calendarId, 'request' => $request, 'status' => 1 ); } catch (Syncroton_Exception_Status_MeetingResponse $sesmr) { $this->_results[] = array( 'request' => $request, 'status' => $sesmr->getCode() ); } } } /** * generate MeetingResponse response */ public function getResponse() { $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Search', 'uri:Search'); $meetingResponse = $this->_outputDom->documentElement; foreach ($this->_results as $result) { $resultElement = $this->_outputDom->createElementNS('uri:MeetingResponse', 'Result'); if (isset($result['request']->requestId)) { $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'RequestId', $result['request']->requestId)); } elseif (isset($result['request']->longId)) { $resultElement->appendChild($this->_outputDom->createElementNS('uri:Search', 'LongId', $result['request']->longId)); } $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'Status', $result['status'])); if (isset($result['calendarId'])) { $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'CalendarId', $result['calendarId'])); } $meetingResponse->appendChild($resultElement); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/Options.php b/lib/ext/Syncroton/Command/Options.php index 52f33ad..415ff6b 100644 --- a/lib/ext/Syncroton/Command/Options.php +++ b/lib/ext/Syncroton/Command/Options.php @@ -1,34 +1,34 @@ */ /** * class to handle ActiveSync http options request * * @package Syncroton * @subpackage Command */ class Syncroton_Command_Options { /** * this function generates the response for the client * - * @return void + * @return array */ public function getHeaders() { // same header like Exchange 2xxx??? return array( 'MS-Server-ActiveSync' => '14.00.0536.000', 'MS-ASProtocolVersions' => '2.5,12.0,12.1,14.0,14.1', 'MS-ASProtocolCommands' => 'FolderCreate,FolderDelete,FolderSync,FolderUpdate,GetAttachment,GetItemEstimate,ItemOperations,MeetingResponse,MoveItems,Provision,ResolveRecipients,Ping,SendMail,Search,Settings,SmartForward,SmartReply,Sync,ValidateCert' ); } } diff --git a/lib/ext/Syncroton/Command/Ping.php b/lib/ext/Syncroton/Command/Ping.php index c14fcca..ca6227d 100644 --- a/lib/ext/Syncroton/Command/Ping.php +++ b/lib/ext/Syncroton/Command/Ping.php @@ -1,283 +1,280 @@ */ /** * 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_TOO_MANY_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 $_defaultNameSpace = 'uri:Ping'; + + protected $_documentElement = 'Ping'; + 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)) { $maxCollections = Syncroton_Registry::getMaxCollections(); if ($maxCollections && count($xml->Folders->Folder) > $maxCollections) { $ping = $this->_outputDom->documentElement; $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', self::STATUS_TOO_MANY_FOLDERS)); $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'MaxFolders', $maxCollections)); return; } $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); + $this->_device = $this->_deviceBackend->update($this->_device); // @phpstan-ignore-line } $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 = $this->_device->pingfolder ? unserialize($this->_device->pingfolder) : array(); 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) { $sleepCallback = Syncroton_Registry::getSleepCallback(); $wakeupCallback = Syncroton_Registry::getWakeupCallback(); do { // take a break to save battery lifetime call_user_func($sleepCallback); sleep(Syncroton_Registry::getPingTimeout()); // make sure the connection is still alive, abort otherwise if (connection_aborted()) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Exiting on aborted connection"); exit; } // reconnect external connections, etc. call_user_func($wakeupCallback); // Calculate secondsLeft before any loop break just to have a correct value // for logging purposes in case we breaked from the loop early $secondsLeft = $intervalEnd - time(); try { + /** @var Syncroton_Model_Device $device */ $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) && + if ((isset($device->lastping) && + $device->lastping instanceof DateTime) && $device->pingfolder === $this->_device->pingfolder && - $device->lastping->getTimestamp() > $this->_device->lastping->getTimestamp() ) { + $device->lastping->getTimestamp() > $this->_device->lastping->getTimestamp() + ) { break; } // If folders hierarchy changed, break the loop and ask the client for FolderSync try { if ($this->_folderBackend->hasHierarchyChanges($this->_device)) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' Detected changes in folders hierarchy'); $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; } $now = new DateTime('now', new DateTimeZone('UTC')); foreach ($folders as $folderId) { try { + /** @var Syncroton_Model_Folder $folder */ $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; } // Update secondsLeft (again) $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 (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/Provision.php b/lib/ext/Syncroton/Command/Provision.php index d6a83dd..ae07cd7 100644 --- a/lib/ext/Syncroton/Command/Provision.php +++ b/lib/ext/Syncroton/Command/Provision.php @@ -1,199 +1,199 @@ */ /** * class to handle ActiveSync Provision command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_Provision extends Syncroton_Command_Wbxml { protected $_defaultNameSpace = 'uri:Provision'; protected $_documentElement = 'Provision'; const POLICYTYPE_WBXML = 'MS-EAS-Provisioning-WBXML'; const STATUS_SUCCESS = 1; const STATUS_PROTOCOL_ERROR = 2; const STATUS_GENERAL_SERVER_ERROR = 3; const STATUS_DEVICE_MANAGED_EXTERNALLY = 4; const STATUS_POLICY_SUCCESS = 1; const STATUS_POLICY_NOPOLICY = 2; const STATUS_POLICY_UNKNOWNTYPE = 3; const STATUS_POLICY_CORRUPTED = 4; const STATUS_POLICY_WRONGPOLICYKEY = 5; const REMOTEWIPE_REQUESTED = 1; const REMOTEWIPE_CONFIRMED = 2; protected $_skipValidatePolicyKey = true; protected $_policyType; protected $_sendPolicyKey; /** * @var Syncroton_Model_DeviceInformation */ protected $_deviceInformation; /** * process the XML file and add, change, delete or fetches data - * - * @return resource */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); $this->_policyType = isset($xml->Policies->Policy->PolicyType) ? (string) $xml->Policies->Policy->PolicyType : null; $this->_sendPolicyKey = isset($xml->Policies->Policy->PolicyKey) ? (int) $xml->Policies->Policy->PolicyKey : null; if ($this->_device->remotewipe == self::REMOTEWIPE_REQUESTED && isset($xml->RemoteWipe->Status) && (int)$xml->RemoteWipe->Status == self::STATUS_SUCCESS) { $this->_device->remotewipe = self::REMOTEWIPE_CONFIRMED; } // try to fetch element from Settings namespace $settings = $xml->children('uri:Settings'); if (isset($settings->DeviceInformation) && isset($settings->DeviceInformation->Set)) { $this->_deviceInformation = new Syncroton_Model_DeviceInformation($settings->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); + $this->_device = $this->_deviceBackend->update($this->_device); // @phpstan-ignore-line } } /** * generate search command response * */ public function getResponse() { $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Settings', 'uri:Settings'); // should we wipe the device if ($this->_device->remotewipe >= self::REMOTEWIPE_REQUESTED) { $this->_sendRemoteWipe(); } else { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' PolicyType: ' . $this->_policyType . ' PolicyKey: ' . $this->_sendPolicyKey); if (!$this->_sendPolicyKey) { $this->_sendPolicy(); } elseif ($this->_sendPolicyKey == $this->_device->policykey) { $this->_acknowledgePolicy(); } } return $this->_outputDom; } /** * function the send policy to client * * 4131 (Enforce password on device) 0: enabled 1: disabled * 4133 (Unlock from computer) 0: disabled 1: enabled * AEFrequencyType 0: no inactivity time 1: inactivity time is set * AEFrequencyValue inactivity time in minutes * DeviceWipeThreshold after how many worng password to device should get wiped * CodewordFrequency validate every 3 wrong passwords, that a person is using the device which is able to read and write. should be half of DeviceWipeThreshold * MinimumPasswordLength minimum password length * PasswordComplexity 0: Require alphanumeric 1: Require only numeric, 2: anything goes * */ protected function _sendPolicy() { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' send policy to device'); $provision = $sync = $this->_outputDom->documentElement; $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1)); // settings if ($this->_deviceInformation instanceof Syncroton_Model_DeviceInformation) { $deviceInformation = $provision->appendChild($this->_outputDom->createElementNS('uri:Settings', 'DeviceInformation')); $deviceInformation->appendChild($this->_outputDom->createElementNS('uri:Settings', 'Status', 1)); } // policies $policies = $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policies')); $policy = $policies->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policy')); $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyType', $this->_policyType)); if ($this->_policyType != self::POLICYTYPE_WBXML) { $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', self::STATUS_POLICY_UNKNOWNTYPE)); } elseif (empty($this->_device->policyId)) { $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', self::STATUS_POLICY_NOPOLICY)); } else { $this->_device->policykey = $this->generatePolicyKey(); $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', self::STATUS_POLICY_SUCCESS)); $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyKey', $this->_device->policykey)); $data = $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Data')); $easProvisionDoc = $data->appendChild($this->_outputDom->createElementNS('uri:Provision', 'EASProvisionDoc')); - $this->_policyBackend - ->get($this->_device->policyId) - ->appendXML($easProvisionDoc, $this->_device); + + /** @var Syncroton_Model_Policy $_policy */ + $_policy = $this->_policyBackend->get($this->_device->policyId); + $_policy->appendXML($easProvisionDoc, $this->_device); $this->_deviceBackend->update($this->_device); } } /** * function the send remote wipe command */ protected function _sendRemoteWipe() { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . ' send remote wipe to device'); $provision = $sync = $this->_outputDom->documentElement; $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1)); $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'RemoteWipe')); } protected function _acknowledgePolicy() { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' acknowledge policy'); - $policykey = $this->_policyBackend->get($this->_device->policyId)->policyKey; + /** @var Syncroton_Model_Policy $_policy */ + $_policy = $this->_policyBackend->get($this->_device->policyId); $provision = $sync = $this->_outputDom->documentElement; $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1)); $policies = $provision->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policies')); $policy = $policies->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Policy')); $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyType', $this->_policyType)); $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'Status', 1)); - $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyKey', $policykey)); + $policy->appendChild($this->_outputDom->createElementNS('uri:Provision', 'PolicyKey', $_policy->policyKey)); - $this->_device->policykey = $policykey; + $this->_device->policykey = $_policy->policyKey; $this->_deviceBackend->update($this->_device); } /** * generate a random string used as PolicyKey */ public static function generatePolicyKey() { return mt_rand(1, 2147483647); } } diff --git a/lib/ext/Syncroton/Command/Settings.php b/lib/ext/Syncroton/Command/Settings.php index 63e36b7..bc74fbe 100644 --- a/lib/ext/Syncroton/Command/Settings.php +++ b/lib/ext/Syncroton/Command/Settings.php @@ -1,158 +1,159 @@ */ /** * 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)) { + 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()) { + // @phpstan-ignore-next-line $this->_device = $this->_deviceBackend->update($this->_device); } } - if(isset($xml->UserInformation->Get)) { + 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)) { $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)) { $OofGet = null; try { $OofGet = $this->_deviceBackend->getOOF($this->_OofGet); $OofStatus = self::STATUS_SUCCESS; } catch (Exception $e) { if ($e instanceof Syncroton_Exception_Status) { $OofStatus = $e->getCode(); } 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)) { try { $this->_deviceBackend->setOOF($this->_OofSet); $OofStatus = self::STATUS_SUCCESS; } catch (Exception $e) { if ($e instanceof Syncroton_Exception_Status) { $OofStatus = $e->getCode(); } 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 f548160..ca50a7f 100644 --- a/lib/ext/Syncroton/Command/Sync.php +++ b/lib/ext/Syncroton/Command/Sync.php @@ -1,1247 +1,1230 @@ */ /** * 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 STATUS_TOO_MANY_COLLECTIONS = 15; 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 + * @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; 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; } - + + $intervalDiv = 1; 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; return; } $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(); } } $maxCollections = Syncroton_Registry::getMaxCollections(); if ($maxCollections && count($requestXML->Collections->Collection) > $maxCollections) { $sync = $this->_outputDom->documentElement; $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_TOO_MANY_COLLECTIONS)); $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Limit', $maxCollections)); return; } $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; } $syncKeyReused = $this->_syncStateBackend->haveNext($this->_device, $collectionData->folder, $collectionData->syncKey); if ($syncKeyReused) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " already known synckey {$collectionData->syncKey} provided"); } // check for invalid synckey 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"); $clientIdMap = []; if ($syncKeyReused && $collectionData->syncState->clientIdMap) { $clientIdMap = Zend_Json::decode($collectionData->syncState->clientIdMap); } 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"); $clientId = (string)$add->ClientId; // If the sync key was reused, but we don't have a $clientId mapping, // this means the client sent a new item with the same sync_key. if ($syncKeyReused && array_key_exists($clientId, $clientIdMap)) { // We don't normally store the clientId, so if a command with Add's is resent, // we have to look-up the corresponding serverId using a cached clientId => serverId mapping, // otherwise we would duplicate all added items on resend. $serverId = $clientIdMap[$clientId]; $clientModifications['added'][$serverId] = array( 'clientId' => (string)$add->ClientId, 'serverId' => $serverId, 'status' => self::STATUS_SUCCESS, 'contentState' => null ); } else { $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(!$syncKeyReused && $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(!$syncKeyReused && $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"); $toBeFetched = 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(); $sleepCallback = Syncroton_Registry::getSleepCallback(); $wakeupCallback = Syncroton_Registry::getWakeupCallback(); do { // take a break to save battery lifetime $sleepCallback(); sleep(Syncroton_Registry::getPingTimeout()); // make sure the connection is still alive, abort otherwise if (connection_aborted()) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Exiting on aborted connection"); exit; } $wakeupCallback(); $now = new DateTime('now', 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 (Syncroton_Server::validateSession() && time() - $intervalStart < $this->_heartbeatInterval - (Syncroton_Registry::getPingTimeout() + 10)); } // First check for folders hierarchy changes foreach ($this->_collections as $collectionData) { if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Detected a folder hierarchy change on {$collectionData->collectionId}."); $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED)); return $this->_outputDom; } } - foreach($this->_collections as $collectionData) { + 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 synckey provided if (! ($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(), ); $status = self::STATUS_SUCCESS; $hasChanges = 0; - if($collectionData->getChanges === true) { + 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; } else { try { $hasChanges = $dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState); } catch (Syncroton_Exception_NotFound $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folder changes checking failed (not found): " . $e->getTraceAsString()); $status = self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED; } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Folder changes checking failed: " . $e->getMessage()); // Prevent from removing client entries when getServerEntries() fails // @todo: should we break the loop here? $status = self::STATUS_SERVER_ERROR; } } if ($hasChanges) { // update _syncTimeStamp as $dataController->hasChanges might have spent some time $this->_syncTimeStamp = new DateTime('now', new DateTimeZone('UTC')); try { // fetch entries added since last sync $allClientEntries = $this->_contentStateBackend->getFolderState( $this->_device, $collectionData->folder, $collectionData->syncState->counter ); // fetch entries changed since last sync $allChangedEntries = $dataController->getChangedEntries( $collectionData->collectionId, $collectionData->syncState, $collectionData->options['filterType'] ); // fetch all entries $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); // entries changed since last sync $serverModifications['changed'] = array_merge($allChangedEntries, $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 break the loop here? $status = self::STATUS_SERVER_ERROR; } 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', $status)); $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 if 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); $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 )); $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); break; } 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()); // We bump collectionChanges anyways to make sure the windowSize still applies. $collectionChanges++; } // mark as sent to the client, even the conversion to xml might have failed 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); break; } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage()); // We bump collectionChanges anyways to make sure the windowSize still applies. $collectionChanges++; } 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()); // We bump collectionChanges anyways to make sure the windowSize still applies. $collectionChanges++; } unset($serverModifications['deleted'][$id]); } $countOfPendingChanges = (count($serverModifications['added']) + count($serverModifications['changed']) + count($serverModifications['deleted'])); if ($countOfPendingChanges > 0) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " there are ". $countOfPendingChanges . " more items available"); $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable')); } else { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " there are no more items available"); $serverModifications = null; } if ($commands->hasChildNodes() === true) { $collection->appendChild($commands); } $totalChanges += $collectionChanges; // If the client resent an old sync-key, we should still respond with the latest sync key if (isset($collectionData->syncState->counterNext)) { //TODO we're not resending the changes in between, but I'm not sure we have to. $collectionData->syncState->counter = $collectionData->syncState->counterNext; } // 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); } //Store next $collectionData->syncState->extraData = $dataController->getExtraData($collectionData->folder); } 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) { + if (isset($countOfPendingChanges) && $countOfPendingChanges > 0) { $collectionData->syncState->pendingdata = array( - 'added' => (array)$serverModifications['added'], - 'changed' => (array)$serverModifications['changed'], - 'deleted' => (array)$serverModifications['deleted'] + 'added' => $serverModifications['added'] ?? [], + 'changed' => $serverModifications['changed'] ?? [], + 'deleted' => $serverModifications['deleted'] ?? [] ); } else { $collectionData->syncState->pendingdata = null; } $collectionData->syncState->lastsync = clone $this->_syncTimeStamp; // increment sync timestamp by 1 second $collectionData->syncState->lastsync->modify('+1 sec'); if (!empty($clientModifications['added'])) { // Store a client id mapping in case we encounter a reused sync_key in a future request. $newClientIdMap = []; foreach($clientModifications['added'] as $entryData) { // No serverId if we failed to add if ($entryData['status'] == self::STATUS_SUCCESS) { $newClientIdMap[$entryData['clientId']] = $entryData['serverId']; } } $collectionData->syncState->clientIdMap = Zend_Json::encode($newClientIdMap); } //Retry in case of deadlock $retryCounter = 0; while (True) { try { $transactionId = Syncroton_Registry::getTransactionManager()->startTransaction(Syncroton_Registry::getDatabase()); // store new synckey - $this->_syncStateBackend->create($collectionData->syncState, true); + $this->_syncStateBackend->create($collectionData->syncState, true); // @phpstan-ignore-line // store contentstates for new entries added to client foreach($newContentStates as $state) { try { //This can happen if we rerun a previous sync-key $state = $this->_contentStateBackend->getContentState($state->device_id, $state->folder_id, $state->contentid); $this->_contentStateBackend->update($state); } catch(Exception $zdse) { $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); break; } catch (Syncroton_Exception_DeadlockDetected $zdse) { $retryCounter++; if ($retryCounter > 60) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . ' exception while storing new synckey. Aborting after 5 retries.'); // something went wrong // maybe another parallel request added a new synckey // we must remove data added from client - if (!empty($clientModifications['added'])) { + if (!empty($clientModifications['added']) && isset($dataController)) { foreach ($clientModifications['added'] as $added) { $this->_contentStateBackend->delete($added['contentState']); $dataController->deleteEntry($collectionData->collectionId, $added['serverId']); } } Syncroton_Registry::getTransactionManager()->rollBack(); throw $zdse; } Syncroton_Registry::getTransactionManager()->rollBack(); // Give the other transactions some time before we try again sleep(1); if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . ' error during transaction, trying again.'); } catch (Exception $zdse) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . ' exception while storing new synckey.'); // something went wrong // maybe another parallel request added a new synckey // we must remove data added from client - if (!empty($clientModifications['added'])) { + if (!empty($clientModifications['added']) && isset($dataController)) { foreach ($clientModifications['added'] as $added) { $this->_contentStateBackend->delete($added['contentState']); $dataController->deleteEntry($collectionData->collectionId, $added['serverId']); } } Syncroton_Registry::getTransactionManager()->rollBack(); throw $zdse; } } } // store current filter type try { + /** @var Syncroton_Model_Folder $folderState */ $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) { + /** @var DOMElement $collection */ $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 + * @return DOMDocument */ 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 instanceof DOMDocument) { + if (!empty($lastXML)) { + $requestBody = $lastXML; + } else { + throw new Syncroton_Exception_UnexpectedValue('no xml body found'); + } } if ($requestBody->getElementsByTagName('Partial')->length > 0) { $partialBody = clone $requestBody; - $requestBody = $lastXML; + $requestBody = $lastXML ?? (new DOMDocument()); $xpath = new DomXPath($requestBody); $xpath->registerNamespace('AirSync', 'uri:AirSync'); foreach ($partialBody->documentElement->childNodes as $child) { if (! $child instanceof DOMElement) { continue; } - + + /** @var DOMElement $child */ 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) { + /** @var DOMElement $existingCollection */ $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/Command/Wbxml.php b/lib/ext/Syncroton/Command/Wbxml.php index 89e2bb2..fc32dbf 100644 --- a/lib/ext/Syncroton/Command/Wbxml.php +++ b/lib/ext/Syncroton/Command/Wbxml.php @@ -1,222 +1,222 @@ */ /** * abstract class for all commands using wbxml encoded content * * @package Syncroton * @subpackage Command */ abstract class Syncroton_Command_Wbxml implements Syncroton_Command_ICommand { /** * informations about the currently device * - * @var Syncroton_Model_Device + * @var Syncroton_Model_IDevice */ protected $_device; /** * informations about the currently device * * @var Syncroton_Backend_IDevice */ protected $_deviceBackend; /** * informations about the currently device * * @var Syncroton_Backend_IFolder */ protected $_folderBackend; /** * @var Syncroton_Backend_ISyncState */ protected $_syncStateBackend; /** * @var Syncroton_Backend_IContent */ protected $_contentStateBackend; /** - * - * @var Syncroton_Backend_IPolicy + * @var Syncroton_Backend_Policy */ protected $_policyBackend; /** * the domDocument containing the xml response from the server * * @var DOMDocument */ protected $_outputDom; /** * the domDocucment containing the xml request from the client * * @var DOMDocument */ protected $_requestBody; /** * the default namespace * - * @var string + * @var ?string */ protected $_defaultNameSpace; /** * the main xml tag * - * @var string + * @var ?string */ protected $_documentElement; /** * @var array */ protected $_requestParameters; /** * @var Syncroton_Model_SyncState */ protected $_syncState; protected $_skipValidatePolicyKey = false; /** * timestamp to use for all sync requests * * @var DateTime */ protected $_syncTimeStamp; /** * @var string */ protected $_transactionId; /** * @var string */ protected $_policyKey; /** * @var Zend_Log */ protected $_logger; /** * list of part streams * * @var array */ protected $_parts = array(); /** * list of headers * * @var array */ protected $_headers = array(); /** * the constructor * * @param mixed $requestBody * @param Syncroton_Model_Device $device * @param array $requestParameters */ - public function __construct($requestBody, Syncroton_Model_IDevice $device, $requestParameters) + public function __construct($requestBody, Syncroton_Model_IDevice $device, $requestParameters = []) { $this->_requestBody = $requestBody; $this->_device = $device; $this->_requestParameters = $requestParameters; $this->_policyKey = isset($requestParameters['policyKey']) ? $requestParameters['policyKey'] : null; $this->_deviceBackend = Syncroton_Registry::getDeviceBackend(); $this->_folderBackend = Syncroton_Registry::getFolderBackend(); $this->_syncStateBackend = Syncroton_Registry::getSyncStateBackend(); $this->_contentStateBackend = Syncroton_Registry::getContentStateBackend(); $this->_policyBackend = Syncroton_Registry::getPolicyBackend(); if (Syncroton_Registry::isRegistered('loggerBackend')) { $this->_logger = Syncroton_Registry::get('loggerBackend'); } $this->_syncTimeStamp = new DateTime('now', new DateTimeZone('UTC')); // set default content type $this->_headers['Content-Type'] = 'application/vnd.ms-sync.wbxml'; if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " sync timestamp: " . $this->_syncTimeStamp->format('Y-m-d H:i:s')); if (isset($this->_defaultNameSpace) && isset($this->_documentElement)) { // Creates an instance of the DOMImplementation class $imp = new DOMImplementation(); // Creates a DOMDocumentType instance $dtd = $imp->createDocumentType('AirSync', "-//AIRSYNC//DTD AirSync//EN", "http://www.microsoft.com/"); // Creates a DOMDocument instance $this->_outputDom = $imp->createDocument($this->_defaultNameSpace, $this->_documentElement, $dtd); $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Syncroton', 'uri:Syncroton'); $this->_outputDom->formatOutput = false; $this->_outputDom->encoding = 'utf-8'; } if ($this->_skipValidatePolicyKey != true) { if (!empty($this->_device->policyId)) { + /** @var Syncroton_Model_Policy $policy */ $policy = $this->_policyBackend->get($this->_device->policyId); - - if((int)$policy->policyKey != (int)$this->_policyKey) { + + if ((int) $policy->policyKey != (int) $this->_policyKey) { $this->_outputDom->documentElement->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Status', 142)); $sepn = new Syncroton_Exception_ProvisioningNeeded(); $sepn->domDocument = $this->_outputDom; throw $sepn; } // should we wipe the mobile phone? if ($this->_device->remotewipe >= Syncroton_Command_Provision::REMOTEWIPE_REQUESTED) { $this->_outputDom->documentElement->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Status', 140)); $sepn = new Syncroton_Exception_ProvisioningNeeded(); $sepn->domDocument = $this->_outputDom; throw $sepn; } } } } /** * (non-PHPdoc) * @see Syncroton_Command_ICommand::getHeaders() */ public function getHeaders() { return $this->_headers; } /** * return array of part streams * * @return array */ public function getParts() { return $this->_parts; } } diff --git a/lib/ext/Syncroton/Data/AData.php b/lib/ext/Syncroton/Data/AData.php index b38a56c..39ca19a 100644 --- a/lib/ext/Syncroton/Data/AData.php +++ b/lib/ext/Syncroton/Data/AData.php @@ -1,368 +1,384 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Data */ abstract class Syncroton_Data_AData implements Syncroton_Data_IData { - const LONGID_DELIMITER = "\xe2\x87\x94"; # UTF-8 character ⇔ - - /** - * @var DateTime - */ + const LONGID_DELIMITER = "\xe2\x87\x94"; // UTF-8 character ⇔ + + /** @var DateTime */ protected $_timeStamp; - + + /** @var Syncroton_Model_IDevice */ + protected $_device; + + /** @var Zend_Db_Adapter_Abstract */ + protected $_db; + + /** @var string */ + protected $_tablePrefix; + + /** @var string */ + protected $_ownerId; + + /** @var array */ + protected $_supportedFolderTypes = []; + + /** * the constructor * * @param Syncroton_Model_IDevice $_device * @param DateTime $_timeStamp */ public function __construct(Syncroton_Model_IDevice $_device, DateTime $_timeStamp) { $this->_device = $_device; $this->_timeStamp = $_timeStamp; $this->_db = Syncroton_Registry::getDatabase(); $this->_tablePrefix = 'Syncroton_'; $this->_ownerId = '1234'; } /** * return one folder identified by id * * @param string $id * @throws Syncroton_Exception_NotFound * @return Syncroton_Model_Folder */ public function getFolder($id) { $select = $this->_db->select() ->from($this->_tablePrefix . 'data_folder') ->where('owner_id = ?', $this->_ownerId) ->where('id = ?', $id); $stmt = $this->_db->query($select); $folder = $stmt->fetch(); $stmt = null; # see https://bugs.php.net/bug.php?id=44081 if ($folder === false) { throw new Syncroton_Exception_NotFound("folder $id not found"); } return new Syncroton_Model_Folder(array( 'serverId' => $folder['id'], 'displayName' => $folder['name'], 'type' => $folder['type'], 'parentId' => !empty($folder['parent_id']) ? $folder['parent_id'] : null )); } /** * (non-PHPdoc) * @see Syncroton_Data_IData::createFolder() */ public function createFolder(Syncroton_Model_IFolder $folder) { if (!in_array($folder->type, $this->_supportedFolderTypes)) { throw new Syncroton_Exception_UnexpectedValue(); } $id = !empty($folder->serverId) ? $folder->serverId : sha1(mt_rand(). microtime()); $this->_db->insert($this->_tablePrefix . 'data_folder', array( 'id' => $id, 'type' => $folder->type, 'name' => $folder->displayName, 'owner_id' => $this->_ownerId, 'parent_id' => $folder->parentId, 'creation_time' => $this->_timeStamp->format("Y-m-d H:i:s") )); return $this->getFolder($id); } /** * (non-PHPdoc) * @see Syncroton_Data_IData::createEntry() */ public function createEntry($_folderId, Syncroton_Model_IEntry $_entry) { $id = sha1(mt_rand(). microtime()); $this->_db->insert($this->_tablePrefix . 'data', array( 'id' => $id, 'class' => get_class($_entry), 'folder_id' => $_folderId, 'creation_time' => $this->_timeStamp->format("Y-m-d H:i:s"), 'data' => serialize($_entry) )); return $id; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::deleteEntry() */ - public function deleteEntry($_folderId, $_serverId, $_collectionData) + public function deleteEntry($_folderId, $_serverId, $_collectionData = null) { $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId; $result = $this->_db->delete($this->_tablePrefix . 'data', array('id = ?' => $_serverId)); return (bool) $result; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::deleteFolder() */ public function deleteFolder($_folderId) { $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId; $result = $this->_db->delete($this->_tablePrefix . 'data', array('folder_id = ?' => $folderId)); $result = $this->_db->delete($this->_tablePrefix . 'data_folder', array('id = ?' => $folderId)); return (bool) $result; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::emptyFolderContents() */ public function emptyFolderContents($folderId, $options) { return true; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::getAllFolders() */ public function getAllFolders() { $select = $this->_db->select() ->from($this->_tablePrefix . 'data_folder') ->where('type IN (?)', $this->_supportedFolderTypes) ->where('owner_id = ?', $this->_ownerId); $stmt = $this->_db->query($select); $folders = $stmt->fetchAll(); $stmt = null; # see https://bugs.php.net/bug.php?id=44081 $result = array(); foreach ((array) $folders as $folder) { $result[$folder['id']] = new Syncroton_Model_Folder(array( 'serverId' => $folder['id'], 'displayName' => $folder['name'], 'type' => $folder['type'], 'parentId' => $folder['parent_id'] )); } return $result; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::getChangedEntries() */ public function getChangedEntries($_folderId, Syncroton_Model_ISyncState $syncState, $filterType = NULL) { - $_startTimeStamp = $syncState->lastSync; - $_endTimeStamp = null; + $_startTimeStamp = $syncState->lastsync; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; $select = $this->_db->select() ->from($this->_tablePrefix . 'data', array('id')) ->where('folder_id = ?', $_folderId) ->where('last_modified_time > ?', $_startTimeStamp->format("Y-m-d H:i:s")); - if ($_endTimeStamp instanceof DateTime) { - $select->where('last_modified_time < ?', $_endTimeStamp->format("Y-m-d H:i:s")); - } - $ids = array(); $stmt = $this->_db->query($select); while ($id = $stmt->fetchColumn()) { $ids[] = $id; } return $ids; } /** * retrieve folders which were modified since last sync * * @param DateTime $startTimeStamp * @param DateTime $endTimeStamp * @return array list of Syncroton_Model_Folder */ public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp) { $select = $this->_db->select() ->from($this->_tablePrefix . 'data_folder') ->where('type IN (?)', $this->_supportedFolderTypes) ->where('owner_id = ?', $this->_ownerId) ->where('last_modified_time > ?', $startTimeStamp->format('Y-m-d H:i:s')) ->where('last_modified_time <= ?', $endTimeStamp->format('Y-m-d H:i:s')); $stmt = $this->_db->query($select); $folders = $stmt->fetchAll(); $stmt = null; # see https://bugs.php.net/bug.php?id=44081 $result = array(); foreach ((array) $folders as $folder) { $result[$folder['id']] = new Syncroton_Model_Folder(array( 'serverId' => $folder['id'], 'displayName' => $folder['name'], 'type' => $folder['type'], 'parentId' => $folder['parent_id'] )); } return $result; } /** * @param Syncroton_Model_IFolder|string $_folderId * @param string $_filter * @return array */ public function getServerEntries($_folderId, $_filter) { $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; $select = $this->_db->select() ->from($this->_tablePrefix . 'data', array('id')) ->where('folder_id = ?', $_folderId); $ids = array(); $stmt = $this->_db->query($select); while ($id = $stmt->fetchColumn()) { $ids[] = $id; } return $ids; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::getCountOfChanges() */ public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { $allClientEntries = $contentBackend->getFolderState($this->_device, $folder); $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype); $addedEntries = array_diff($allServerEntries, $allClientEntries); $deletedEntries = array_diff($allClientEntries, $allServerEntries); $changedEntries = $this->getChangedEntries($folder->serverId, $syncState, $folder->lastfiltertype); return count($addedEntries) + count($deletedEntries) + count($changedEntries); } /** * (non-PHPdoc) * @see Syncroton_Data_IData::getFileReference() */ public function getFileReference($fileReference) { throw new Syncroton_Exception_NotFound('filereference not found'); } /** * (non-PHPdoc) * @see Syncroton_Data_IData::getEntry() */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId) { $select = $this->_db->select() ->from($this->_tablePrefix . 'data', array('data')) ->where('id = ?', $serverId); $stmt = $this->_db->query($select); $entry = $stmt->fetchColumn(); if ($entry === false) { throw new Syncroton_Exception_NotFound("entry $serverId not found in folder {$collection->collectionId}"); } return unserialize($entry); } /** * (non-PHPdoc) * @see Syncroton_Data_IData::hasChanges() */ public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { return !!$this->getCountOfChanges($contentBackend, $folder, $syncState); } /** * (non-PHPdoc) * @see Syncroton_Data_IData::moveItem() */ public function moveItem($_srcFolderId, $_serverId, $_dstFolderId) { $this->_db->update($this->_tablePrefix . 'data', array( 'folder_id' => $_dstFolderId, ), array( 'id = ?' => $_serverId )); return $_serverId; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::updateEntry() */ public function updateEntry($_folderId, $_serverId, Syncroton_Model_IEntry $_entry) { $this->_db->update($this->_tablePrefix . 'data', array( 'folder_id' => $_folderId, 'last_modified_time' => $this->_timeStamp->format("Y-m-d H:i:s"), 'data' => serialize($_entry) ), array( 'id = ?' => $_serverId )); + + return $_serverId; } /** * (non-PHPdoc) * @see Syncroton_Data_IData::updateFolder() */ public function updateFolder(Syncroton_Model_IFolder $folder) { $this->_db->update($this->_tablePrefix . 'data_folder', array( 'name' => $folder->displayName, 'parent_id' => $folder->parentId, 'last_modified_time' => $this->_timeStamp->format("Y-m-d H:i:s"), ), array( 'id = ?' => $folder->serverId, 'owner_id = ?' => $this->_ownerId )); return $this->getFolder($folder->serverId); } + + public function getExtraData(Syncroton_Model_IFolder $folder) + { + return null; + } } diff --git a/lib/ext/Syncroton/Data/Calendar.php b/lib/ext/Syncroton/Data/Calendar.php index 04f3529..bf6d224 100644 --- a/lib/ext/Syncroton/Data/Calendar.php +++ b/lib/ext/Syncroton/Data/Calendar.php @@ -1,36 +1,37 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Data */ class Syncroton_Data_Calendar extends Syncroton_Data_AData implements Syncroton_Data_IDataCalendar { protected $_supportedFolderTypes = array( Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR, Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED ); /** * set attendee status for meeting * - * @param Syncroton_Model_MeetingResponse $request the meeting response - * @return string id of new calendar entry + * @param Syncroton_Model_MeetingResponse $response The meeting response + * + * @return string ID of new calendar entry */ - public function setAttendeeStatus(Syncroton_Model_MeetingResponse $reponse) + public function setAttendeeStatus(Syncroton_Model_MeetingResponse $response) { - return $reponse->requestId; + return $response->requestId; } } diff --git a/lib/ext/Syncroton/Data/Contacts.php b/lib/ext/Syncroton/Data/Contacts.php index 86258a0..e593d9b 100644 --- a/lib/ext/Syncroton/Data/Contacts.php +++ b/lib/ext/Syncroton/Data/Contacts.php @@ -1,83 +1,84 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Data */ - class Syncroton_Data_Contacts extends Syncroton_Data_AData implements Syncroton_Data_IDataSearch { protected $_supportedFolderTypes = array( Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT, Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED ); /** * (non-PHPdoc) * @see Syncroton_Data_IDataSearch::getSearchEntry() */ public function getSearchEntry($longId, $options) { list($collectionId, $serverId) = explode(Syncroton_Data_AData::LONGID_DELIMITER, $longId, 2); - + + /** @var Syncroton_Model_Contact $contact */ $contact = $this->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $collectionId)), $serverId); return new Syncroton_Model_GAL(array( 'firstName' => $contact->firstName, 'lastName' => $contact->lastName, 'picture' => new Syncroton_Model_GALPicture(array('status' => 1, 'data' => 'abc')) )); } /** * (non-PHPdoc) * @see Syncroton_Data_IDataSearch::search() */ public function search(Syncroton_Model_StoreRequest $store) { $storeResponse = new Syncroton_Model_StoreResponse(); $serverIds = $this->getServerEntries('addressbookFolderId', Syncroton_Command_Sync::FILTER_NOTHING); $total = 0; $found = array(); foreach ($serverIds as $serverId) { + /** @var Syncroton_Model_Contact $contact */ $contact = $this->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => 'addressbookFolderId')), $serverId); if ($contact->firstName == $store->query) { $total++; if (count($found) == $store->options['range'][1]+1) { continue; } $found[] = new Syncroton_Model_StoreResponseResult(array( 'longId' => 'addressbookFolderId' . Syncroton_Data_AData::LONGID_DELIMITER . $serverId, 'properties' => $this->getSearchEntry('addressbookFolderId' . Syncroton_Data_AData::LONGID_DELIMITER . $serverId, $store->options) )); } } if (count($found) > 0) { $storeResponse->result = $found; $storeResponse->range = array(0, count($found) - 1); $storeResponse->total = $total; } else { $storeResponse->total = $total; } return $storeResponse; } } diff --git a/lib/ext/Syncroton/Data/Email.php b/lib/ext/Syncroton/Data/Email.php index b55f9e8..6ca78ef 100644 --- a/lib/ext/Syncroton/Data/Email.php +++ b/lib/ext/Syncroton/Data/Email.php @@ -1,87 +1,88 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Data */ class Syncroton_Data_Email extends Syncroton_Data_AData implements Syncroton_Data_IDataEmail { protected $_supportedFolderTypes = array( Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS, Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS, Syncroton_Command_FolderSync::FOLDERTYPE_INBOX, Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED, Syncroton_Command_FolderSync::FOLDERTYPE_OUTBOX, Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL ); /** * (non-PHPdoc) * @see Syncroton_Data_IDataEmail::forwardEmail() */ public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime) { if ($inputStream == 'triggerException') { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAILBOX_SERVER_OFFLINE); } // forward email } /** * (non-PHPdoc) * @see Syncroton_Data_AData::getFileReference() */ public function getFileReference($fileReference) { list($messageId, $partId) = explode(Syncroton_Data_AData::LONGID_DELIMITER, $fileReference, 2); // example code return new Syncroton_Model_FileReference(array( 'contentType' => 'text/plain', 'data' => 'Lars' )); } /** * (non-PHPdoc) * @see Syncroton_Data_IDataEmail::replyEmail() */ public function replyEmail($source, $inputStream, $saveInSent, $replaceMime) { // forward email } /** * (non-PHPdoc) * @see Syncroton_Data_AData::updateEntry() */ public function updateEntry($_folderId, $_serverId, Syncroton_Model_IEntry $_entry) { // not used by email + return ''; } /** * (non-PHPdoc) * @see Syncroton_Data_IDataEmail::sendEmail() */ public function sendEmail($inputStream, $saveInSent) { if ($inputStream == 'triggerException') { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAILBOX_SERVER_OFFLINE); } // send email } } diff --git a/lib/ext/Syncroton/Data/Factory.php b/lib/ext/Syncroton/Data/Factory.php index 994cf46..e9be6c4 100644 --- a/lib/ext/Syncroton/Data/Factory.php +++ b/lib/ext/Syncroton/Data/Factory.php @@ -1,80 +1,79 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Data */ class Syncroton_Data_Factory { const CLASS_CALENDAR = 'Calendar'; const CLASS_CONTACTS = 'Contacts'; const CLASS_EMAIL = 'Email'; const CLASS_NOTES = 'Notes'; const CLASS_TASKS = 'Tasks'; const STORE_EMAIL = 'Mailbox'; const STORE_GAL = 'GAL'; protected static $_classMap = array(); /** - * @param unknown_type $_class + * @param string $_classFactory * @param Syncroton_Model_IDevice $_device - * @param DateTime $_timeStamp + * @param DateTime $_timeStamp + * * @throws InvalidArgumentException * @return Syncroton_Data_IData */ public static function factory($_classFactory, Syncroton_Model_IDevice $_device, DateTime $_timeStamp) { - switch($_classFactory) { + switch ($_classFactory) { case self::CLASS_CALENDAR: $className = Syncroton_Registry::get(Syncroton_Registry::CALENDAR_DATA_CLASS); break; case self::CLASS_CONTACTS: $className = Syncroton_Registry::get(Syncroton_Registry::CONTACTS_DATA_CLASS); break; case self::STORE_EMAIL: case self::CLASS_EMAIL: $className = Syncroton_Registry::get(Syncroton_Registry::EMAIL_DATA_CLASS); break; case self::CLASS_NOTES: $className = Syncroton_Registry::get(Syncroton_Registry::NOTES_DATA_CLASS); break; case self::CLASS_TASKS: $className = Syncroton_Registry::get(Syncroton_Registry::TASKS_DATA_CLASS); break; case self::STORE_GAL: $className = Syncroton_Registry::get(Syncroton_Registry::GAL_DATA_CLASS); break; default: throw new Syncroton_Exception_UnexpectedValue('invalid class type provided'); - breeak; } - + $class = new $className($_device, $_timeStamp); - + if (! $class instanceof Syncroton_Data_IData) { throw new RuntimeException('class must be instanceof Syncroton_Data_IData'); } - + return $class; } } - diff --git a/lib/ext/Syncroton/Data/IData.php b/lib/ext/Syncroton/Data/IData.php index 8ea1f4c..2c1642b 100644 --- a/lib/ext/Syncroton/Data/IData.php +++ b/lib/ext/Syncroton/Data/IData.php @@ -1,134 +1,134 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Data */ interface Syncroton_Data_IData { /** * create new entry * * @param string $folderId * @param Syncroton_Model_IEntry $entry * @return string id of created entry */ public function createEntry($folderId, Syncroton_Model_IEntry $entry); /** * create a new folder in backend * * @param Syncroton_Model_IFolder $folder * @return Syncroton_Model_IFolder */ public function createFolder(Syncroton_Model_IFolder $folder); /** * delete entry in backend * * @param string $_folderId * @param string $_serverId * @param ?Syncroton_Model_SyncCollection $_collectionData */ public function deleteEntry($_folderId, $_serverId, $_collectionData = null); /** * delete folder * * @param string $folderId */ public function deleteFolder($folderId); /** * empty folder * * @param string $folderId * @param array $options */ public function emptyFolderContents($folderId, $options); /** * return list off all folders * @return array of Syncroton_Model_IFolder */ public function getAllFolders(); public function getChangedEntries($folderId, Syncroton_Model_ISyncState $syncState, $filterType = NULL); /** * Retrieve extra data that is stored with the sync key * @return string|null **/ public function getExtraData(Syncroton_Model_IFolder $folder); /** * retrieve folders which were modified since last sync * * @param DateTime $startTimeStamp * @param DateTime $endTimeStamp */ public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp); public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState); /** * * @param Syncroton_Model_SyncCollection $collection * @param string $serverId - * @return Syncroton_Model_IEntry + * @return Syncroton_Model_IXMLEntry */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId); /** * * @param string $fileReference * @return Syncroton_Model_FileReference */ public function getFileReference($fileReference); /** * return array of all id's stored in folder * * @param Syncroton_Model_IFolder|string $folderId * @param string $filter * @return array */ public function getServerEntries($folderId, $filter); /** * return true if any data got modified in the backend * * @param Syncroton_Backend_IContent $contentBackend * @param Syncroton_Model_IFolder $folder * @param Syncroton_Model_ISyncState $syncState * @return bool */ public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState); public function moveItem($srcFolderId, $serverId, $dstFolderId); /** * update existing entry * * @param string $folderId * @param string $serverId * @param Syncroton_Model_IEntry $entry * @return string id of updated entry */ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry); public function updateFolder(Syncroton_Model_IFolder $folder); } diff --git a/lib/ext/Syncroton/Exception/Status.php b/lib/ext/Syncroton/Exception/Status.php index 2a21ef3..5ae96a1 100644 --- a/lib/ext/Syncroton/Exception/Status.php +++ b/lib/ext/Syncroton/Exception/Status.php @@ -1,219 +1,219 @@ * @author Aleksander Machniak */ /** * Exception for Status element * * @package Syncroton * @subpackage Exception */ class Syncroton_Exception_Status extends Syncroton_Exception { // http://msdn.microsoft.com/en-us/library/ee218647%28v=exchg.80%29 const INVALID_CONTENT = 101; const INVALID_WBXML = 102; const INVALID_XML = 103; const INVALID_DATE_TIME = 104; const INVALID_COMBINATION_OF_IDS = 105; const INVALID_IDS = 106; const INVALID_MIME = 107; const DEVICE_MISSING_OR_INVALID = 108; const DEVICE_TYPE_MISSING_OR_INVALID = 109; const SERVER_ERROR = 110; const SERVER_ERROR_RETRY_LATER = 111; const ACTIVE_DIRECTORY_ACCESS_DENIED = 112; const MAILBOX_QUOTA_EXCEEDED = 113; const MAILBOX_SERVER_OFFLINE = 114; const SEND_QUOTA_EXCEEDED = 115; const MESSAGE_RECIPIENT_UNRESOLVED = 116; const MESSAGE_REPLY_NOT_ALLOWED = 117; const MESSAGE_PREVIOUSLY_SENT = 118; const MESSAGE_HAS_NO_RECIPIENT = 119; const MAIL_SUBMISSION_FAILED = 120; const MESSAGE_REPLY_FAILED = 121; const ATTACHMENT_IS_TOO_LARGE = 122; const USER_HAS_NO_MAILBOX = 123; const USER_CANNOT_BE_ANONYMOUS = 124; const USER_PRINCIPAL_COULD_NOT_BE_FOUND = 125; const USER_DISABLED_FOR_SYNC = 126; const USER_ON_NEW_MAILBOX_CANNOT_SYNC = 127; const USER_ON_LEGACY_MAILBOX_CANNOT_SYNC = 128; const DEVICE_IS_BLOCKED_FOR_THIS_USER = 129; const ACCESS_DENIED = 130; const ACCOUNT_DISABLED = 131; const SYNC_STATE_NOT_FOUND = 132; const SYNC_STATE_LOCKED = 133; const SYNC_STATE_CORRUPT = 134; const SYNC_STATE_ALREADY_EXISTS = 135; const SYNC_STATE_VERSION_INVALID = 136; const COMMAND_NOT_SUPPORTED = 137; const VERSION_NOT_SUPPORTED = 138; const DEVICE_NOT_FULLY_PROVISIONABLE = 139; const REMOTE_WIPE_REQUESTED = 140; const LEGACY_DEVICE_ON_STRICT_POLICY = 141; const DEVICE_NOT_PROVISIONED = 142; const POLICY_REFRESH = 143; const INVALID_POLICY_KEY = 144; const EXTERNALLY_MANAGED_DEVICES_NOT_ALLOWED = 145; const NO_RECURRENCE_IN_CALENDAR = 146; const UNEXPECTED_ITEM_CLASS = 147; const REMOTE_SERVER_HAS_NO_SSL = 148; const INVALID_STORED_REQUEST = 149; const ITEM_NOT_FOUND = 150; const TOO_MANY_FOLDERS = 151; const NO_FOLDERS_FOUND = 152; const ITEMS_LOST_AFTER_MOVE = 153; const FAILURE_IN_MOVE_OPERATION = 154; const MOVE_COMMAND_DISALLOWED = 155; const MOVE_COMMAND_INVALID_DESTINATION = 156; const AVAILABILITY_TO_MANY_RECIPIENTS = 160; const AVAILABILITY_DL_LIMIT_REACHED = 161; const AVAILABILITY_TRANSIENT_FAILURE = 162; const AVAILABILITY_FAILURE = 163; const BODY_PART_PREFERENCE_TYPE_NOT_SUPPORTED = 164; const DEVICE_INFORMATION_REQUIRED = 165; const INVALID_ACCOUNT_ID = 166; const ACCOUNT_SEND_DISABLED = 167; CONST IRM_FEATURE_DISABLED = 168; const IRM_TRANSIENT_ERROR = 169; const IRM_PERMANENT_ERROR = 170; const IRM_INVALID_TEMPLATE_ID = 171; const IRM_OPERATION_NOT_PERMITTED = 172; const NO_PICTURE = 173; const PICTURE_TO_LARGE = 174; const PICTURE_LIMIT_REACHED = 175; const BODY_PART_CONVERSATION_TOO_LARGE = 176; const MAXIMUM_DEVICES_REACHED = 177; /** * Common error messages assigned to error codes * * @var array */ protected $_commonMessages = array( self::INVALID_CONTENT => "Invalid request body", self::INVALID_WBXML => "Invalid WBXML request", self::INVALID_XML => "Invalid XML request", self::INVALID_DATE_TIME => "Invalid datetime string", self::INVALID_COMBINATION_OF_IDS => "Invalid combination of parameters", self::INVALID_IDS => "Invalid one or more IDs", self::INVALID_MIME => "Invalid MIME content", self::DEVICE_MISSING_OR_INVALID => "Device ID invalid or missing", self::DEVICE_TYPE_MISSING_OR_INVALID => "Device type invalid or missing", self::SERVER_ERROR => "Unknown server error", self::SERVER_ERROR_RETRY_LATER => "Unknown server error. Device should retry later", self::ACTIVE_DIRECTORY_ACCESS_DENIED => "No access to an object in the directory service", self::MAILBOX_QUOTA_EXCEEDED => "The mailbox quota size exceeded", self::MAILBOX_SERVER_OFFLINE => "The mailbox server is offline", self::SEND_QUOTA_EXCEEDED => "The request would exceed the send quota", self::MESSAGE_RECIPIENT_UNRESOLVED => "Recipient could not be resolved to an e-mail address", self::MESSAGE_REPLY_NOT_ALLOWED => "The mailbox server doesn't allow a reply of this message", self::MESSAGE_PREVIOUSLY_SENT => "The message was already sent in a previous request", self::MESSAGE_HAS_NO_RECIPIENT => "The message being sent contains no recipient", self::MAIL_SUBMISSION_FAILED => "The server failed to submit the message for delivery", self::MESSAGE_REPLY_FAILED => "The server failed to create a reply message", self::ATTACHMENT_IS_TOO_LARGE => "The attachment is too large", self::USER_HAS_NO_MAILBOX => "A mailbox could not be found for the user", self::USER_CANNOT_BE_ANONYMOUS => "The request was sent without credentials. Anonymous requests are not allowed", self::USER_PRINCIPAL_COULD_NOT_BE_FOUND => "The user was not found in the directory service", self::USER_DISABLED_FOR_SYNC => "This user is not allowed to use ActiveSync", self::USER_ON_NEW_MAILBOX_CANNOT_SYNC => "The server is configured to prevent users from syncing", self::USER_ON_LEGACY_MAILBOX_CANNOT_SYNC => "The server is configured to prevent users on legacy servers from syncing", self::DEVICE_IS_BLOCKED_FOR_THIS_USER => "This device is not the allowed device", self::ACCESS_DENIED => "The user is not allowed to perform that request", self::ACCOUNT_DISABLED => "The user's account is disabled", self::SYNC_STATE_NOT_FOUND => "Missing data file that contains the state of the client", self::SYNC_STATE_LOCKED => "Locked data file that contains the state of the client", self::SYNC_STATE_CORRUPT => "Corrupted data file that contains the state of the client", self::SYNC_STATE_ALREADY_EXISTS => "The data file that contains the state of the client already exists", self::SYNC_STATE_VERSION_INVALID => "Version of the data file that contains the state of the client is invalid", self::COMMAND_NOT_SUPPORTED => "The command is not supported by this server", self::VERSION_NOT_SUPPORTED => "The command is not supported in the protocol version specified", self::DEVICE_NOT_FULLY_PROVISIONABLE => "The device uses a protocol version that cannot send all the policy settings the admin enabled", self::REMOTE_WIPE_REQUESTED => "A remote wipe was requested", self::LEGACY_DEVICE_ON_STRICT_POLICY => "A policy is in place but the device is not provisionable", self::DEVICE_NOT_PROVISIONED => "There is a policy in place", self::POLICY_REFRESH => "The policy is configured to be refreshed every few hours", self::INVALID_POLICY_KEY => "The device's policy key is invalid", self::EXTERNALLY_MANAGED_DEVICES_NOT_ALLOWED => "The server doesn't allow externally managed devices to sync", self::NO_RECURRENCE_IN_CALENDAR => "The request tried to forward an occurrence of a meeting that has no recurrence", self::UNEXPECTED_ITEM_CLASS => "The request tried to operate on a type of items unknown to the server", self::REMOTE_SERVER_HAS_NO_SSL => "Remote server doesn't have SSL enabled", self::INVALID_STORED_REQUEST => "The stored result is invalid. The device should send the full request again", self::ITEM_NOT_FOUND => "Item not found", self::TOO_MANY_FOLDERS => "The mailbox contains too many folders", self::NO_FOLDERS_FOUND => "The mailbox contains no folders", self::ITEMS_LOST_AFTER_MOVE => "Items lost after move", self::FAILURE_IN_MOVE_OPERATION => "The mailbox server returned an unknown error while moving items", self::MOVE_COMMAND_DISALLOWED => "An ItemOperations command request to move a conversation is missing the MoveAlways element", self::MOVE_COMMAND_INVALID_DESTINATION => "The destination folder for the move is invalid", self::AVAILABILITY_TO_MANY_RECIPIENTS => "The command has reached the maximum number of recipients that it can request availability for", self::AVAILABILITY_DL_LIMIT_REACHED => "The size of the distribution list is larger than the availability service is configured to process", self::AVAILABILITY_TRANSIENT_FAILURE => "Availability service request failed with a transient error", self::AVAILABILITY_FAILURE => "Availability service request failed with an error", self::BODY_PART_PREFERENCE_TYPE_NOT_SUPPORTED => "The BodyPartPreference node has an unsupported Type element", self::DEVICE_INFORMATION_REQUIRED => "The required DeviceInformation element is missing in the Provision request", self::INVALID_ACCOUNT_ID => "Invalid AccountId value", self::ACCOUNT_SEND_DISABLED => "The AccountId value specified in the request does not support sending e-mail", self::IRM_FEATURE_DISABLED => "The Information Rights Management feature is disabled", self::IRM_TRANSIENT_ERROR => "Information Rights Management encountered a transient error", self::IRM_PERMANENT_ERROR => "Information Rights Management encountered a permanent error", self::IRM_INVALID_TEMPLATE_ID => "The Template ID value is not valid", self::IRM_OPERATION_NOT_PERMITTED => "Information Rights Management does not support the specified operation", self::NO_PICTURE => "The user does not have a contact photo", self::PICTURE_TO_LARGE => "The contact photo exceeds the size limit set by the MaxSize element", self::PICTURE_LIMIT_REACHED => "The number of contact photos returned exceeds the size limit set by the MaxPictures element", self::BODY_PART_CONVERSATION_TOO_LARGE => "The conversation is too large to compute the body parts", self::MAXIMUM_DEVICES_REACHED => "The user's account has too many device partnerships", ); /** * Error messages assigned to class-specific error codes * * @var array */ protected $_errorMessages = array(); /** * Constructor */ function __construct() { $args = func_get_args(); if (isset($args[1])) { $code = $args[1]; $message = $args[0]; } elseif (is_int($args[0])) { $code = $args[0]; $message = null; } else { $message = $args[0]; } - if (!$code) { + if (empty($code)) { $code = self::SERVER_ERROR; } if (!$message) { if (isset($this->_errorMessages[$code])) { $message = $this->_errorMessages[$code]; } elseif (isset($this->_commonMessages[$code])) { $message = $this->_commonMessages[$code]; } } parent::__construct($message, $code); } } diff --git a/lib/ext/Syncroton/Model/AEntry.php b/lib/ext/Syncroton/Model/AEntry.php index 13e7610..bf79769 100644 --- a/lib/ext/Syncroton/Model/AEntry.php +++ b/lib/ext/Syncroton/Model/AEntry.php @@ -1,107 +1,106 @@ */ /** * abstract class to handle ActiveSync entry * * @package Syncroton * @subpackage Model */ - abstract class Syncroton_Model_AEntry implements Syncroton_Model_IEntry, IteratorAggregate, Countable { protected $_elements = array(); protected $_isDirty; /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::__construct() */ public function __construct($properties = null) { if (is_array($properties)) { $this->setFromArray($properties); } $this->_isDirty = false; } /** * (non-PHPdoc) * @see Countable::count() */ #[\ReturnTypeWillChange] public function count() { return count($this->_elements); } /** * (non-PHPdoc) * @see IteratorAggregate::getIterator() */ #[\ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->_elements); } /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::isDirty() */ public function isDirty() { return $this->_isDirty; } /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::setFromArray() */ public function setFromArray(array $properties) { foreach($properties as $key => $value) { try { $this->$key = $value; //echo __LINE__ . PHP_EOL; } catch (InvalidArgumentException $iae) { //ignore invalid properties //echo __LINE__ . PHP_EOL; echo $iae->getMessage(); echo $iae->getTraceAsString(); } } } public function &__get($name) { return $this->_elements[$name]; } public function __set($name, $value) { if (!array_key_exists($name, $this->_elements) || $this->_elements[$name] != $value) { $this->_elements[$name] = $value; $this->_isDirty = true; } } public function __isset($name) { return isset($this->_elements[$name]); } public function __unset($name) { unset($this->_elements[$name]); } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/AXMLEntry.php b/lib/ext/Syncroton/Model/AXMLEntry.php index bb2971c..c23aa02 100644 --- a/lib/ext/Syncroton/Model/AXMLEntry.php +++ b/lib/ext/Syncroton/Model/AXMLEntry.php @@ -1,328 +1,328 @@ */ /** * 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'])) { foreach ($value as $element) { $container = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $element->appendXML($container, $device); $domParrent->appendChild($container); } } else if ($elementProperties['type'] == 'none') { if ($value) { $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $domParrent->appendChild($element); } } else { $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $this->_appendXMLElement($device, $element, $elementProperties, $value); $domParrent->appendChild($element); } } } /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::getProperties() */ public function getProperties($selectedNamespace = null) { $properties = array(); foreach($this->_properties as $namespace => $namespaceProperties) { if ($selectedNamespace !== null && $namespace != $selectedNamespace) { continue; } $properties = array_merge($properties, array_keys($namespaceProperties)); } return $properties; } /** * set properties from SimpleXMLElement object * - * @param SimpleXMLElement $xmlCollection + * @param SimpleXMLElement $properties * @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 + * @param DOMElement $domParent */ - protected function _addXMLNamespaces(DOMElement $domParrent) + protected function _addXMLNamespaces(DOMElement $domParent) { - foreach($this->_properties as $namespace => $namespaceProperties) { + 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); - } + if ($domParent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) { + $domParent->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); + $value->appendXML($element, $device); // @phpstan-ignore-line } 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((string)$value)) { $value = $this->_removeControlChars($value); } $element->appendChild($element->ownerDocument->createTextNode($this->_enforceUTF8($value))); } } } /** * remove control chars from a string which are not allowed in XML values * * @param string $dirty An input string * @return string Cleaned up string */ protected function _removeControlChars($dirty) { // Replace non-character UTF-8 sequences that cause XML Parser to fail // https://git.kolab.org/T1311 $dirty = str_replace(array("\xEF\xBF\xBE", "\xEF\xBF\xBF"), '', $dirty); // Replace ASCII control-characters return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $dirty); } /** * enforce >valid< utf-8 encoding * * @param string $dirty the string with maybe invalid utf-8 data * @return string string with valid utf-8 */ protected function _enforceUTF8($dirty) { if (function_exists('iconv')) { if (($clean = @iconv('UTF-8', 'UTF-8//IGNORE', $dirty)) !== false) { return $clean; } } if (function_exists('mb_convert_encoding')) { if (($clean = mb_convert_encoding($dirty, 'UTF-8', 'UTF-8')) !== false) { return $clean; } } return $dirty; } /** * - * @param unknown_type $element + * @param string $element * @throws InvalidArgumentException - * @return multitype:unknown + * @return array */ 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'])) { $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/Model/Account.php b/lib/ext/Syncroton/Model/Account.php index 0ed9d65..11b6453 100644 --- a/lib/ext/Syncroton/Model/Account.php +++ b/lib/ext/Syncroton/Model/Account.php @@ -1,76 +1,76 @@ */ /** * class to handle (Settings/UserInformation/Get/Accounts/) Account element * * @package Syncroton * @subpackage Model - * @property string accountId - * @property string accountName - * @property string userDisplayName - * @property bool sendDisabled - * @property string primaryAddress - * @property array addresses + * @property string $accountId + * @property string $accountName + * @property string $userDisplayName + * @property bool $sendDisabled + * @property string $primaryAddress + * @property array $addresses */ class Syncroton_Model_Account extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Account'; protected $_properties = array( 'Settings' => array( 'accountId' => array('type' => 'string'), 'accountName' => array('type' => 'string'), 'userDisplayName' => array('type' => 'string'), 'sendDisabled' => array('type' => 'number'), // 'emailAddresses' => array('type' => 'container'), ), 'Internal' => array( 'primaryAddress' => array('type' => 'string'), 'addresses' => array('type' => 'array'), ), ); /** * (non-PHPdoc) * @see Syncroton_Model_AXMLEntry::appendXML() */ public function appendXML(DOMElement $_domParent, Syncroton_Model_IDevice $device) { parent::appendXML($_domParent, $device); $nameSpace = 'uri:Settings'; $document = $_domParent->ownerDocument; // handle EmailAddresses element $list = $document->createElementNS($nameSpace, 'EmailAddresses'); if (!empty($this->_elements['primaryAddress'])) { $element = $document->createElementNS($nameSpace, 'PrimarySmtpAddress', $this->_elements['primaryAddress']); $list->appendChild($element); } foreach ((array)$this->_elements['addresses'] as $address) { // skip empty values if (empty($address)) { continue; } $element = $document->createElementNS($nameSpace, 'SMTPAddress', $address); $list->appendChild($element); } if ($list->hasChildNodes()) { $_domParent->appendChild($list); } } } diff --git a/lib/ext/Syncroton/Model/Contact.php b/lib/ext/Syncroton/Model/Contact.php index 6b90a07..8fb77d4 100644 --- a/lib/ext/Syncroton/Model/Contact.php +++ b/lib/ext/Syncroton/Model/Contact.php @@ -1,103 +1,104 @@ */ /** * class to handle ActiveSync contact * * @package Syncroton * @subpackage Model - * @property string Alias - * @property DateTime Anniversary - * @property string AssistantName - * @property string AssistantPhoneNumber - * @property DateTime Birthday - * @property string Business2PhoneNumber - * @property string BusinessAddressCity - * @property Syncroton_Model_EmailBody Body + * @property string $alias + * @property DateTime $anniversary + * @property string $assistantName + * @property string $assistantPhoneNumber + * @property DateTime $birthday + * @property string $business2PhoneNumber + * @property string $businessAddressCity + * @property Syncroton_Model_EmailBody $body + * @property string $firstName + * @property string $lastName */ - class Syncroton_Model_Contact extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'AirSyncBase' => array( 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody') ), 'Contacts' => array( 'alias' => array('type' => 'string', 'supportedSince' => '14.0'), 'anniversary' => array('type' => 'datetime'), 'assistantName' => array('type' => 'string'), 'assistantPhoneNumber' => array('type' => 'string'), 'birthday' => array('type' => 'datetime'), 'business2PhoneNumber' => array('type' => 'string'), 'businessAddressCity' => array('type' => 'string'), 'businessAddressCountry' => array('type' => 'string'), 'businessAddressPostalCode' => array('type' => 'string'), 'businessAddressState' => array('type' => 'string'), 'businessAddressStreet' => array('type' => 'string'), 'businessFaxNumber' => array('type' => 'string'), 'businessPhoneNumber' => array('type' => 'string'), 'carPhoneNumber' => array('type' => 'string'), 'categories' => array('type' => 'container', 'childElement' => 'category'), 'children' => array('type' => 'container', 'childElement' => 'child'), 'companyName' => array('type' => 'string'), 'department' => array('type' => 'string'), 'email1Address' => array('type' => 'string'), 'email2Address' => array('type' => 'string'), 'email3Address' => array('type' => 'string'), 'fileAs' => array('type' => 'string'), 'firstName' => array('type' => 'string'), 'home2PhoneNumber' => array('type' => 'string'), 'homeAddressCity' => array('type' => 'string'), 'homeAddressCountry' => array('type' => 'string'), 'homeAddressPostalCode' => array('type' => 'string'), 'homeAddressState' => array('type' => 'string'), 'homeAddressStreet' => array('type' => 'string'), 'homeFaxNumber' => array('type' => 'string'), 'homePhoneNumber' => array('type' => 'string'), 'jobTitle' => array('type' => 'string'), 'lastName' => array('type' => 'string'), 'middleName' => array('type' => 'string'), 'mobilePhoneNumber' => array('type' => 'string'), 'officeLocation' => array('type' => 'string'), 'otherAddressCity' => array('type' => 'string'), 'otherAddressCountry' => array('type' => 'string'), 'otherAddressPostalCode' => array('type' => 'string'), 'otherAddressState' => array('type' => 'string'), 'otherAddressStreet' => array('type' => 'string'), 'pagerNumber' => array('type' => 'string'), 'picture' => array('type' => 'string', 'encoding' => 'base64'), 'padioPhoneNumber' => array('type' => 'string'), 'rtf' => array('type' => 'string'), 'spouse' => array('type' => 'string'), 'suffix' => array('type' => 'string'), 'title' => array('type' => 'string'), 'webPage' => array('type' => 'string'), 'weightedRank' => array('type' => 'string', 'supportedSince' => '14.0'), 'yomiCompanyName' => array('type' => 'string'), 'yomiFirstName' => array('type' => 'string'), 'yomiLastName' => array('type' => 'string'), ), 'Contacts2' => array( 'accountName' => array('type' => 'string'), 'companyMainPhone' => array('type' => 'string'), 'customerId' => array('type' => 'string'), 'governmentId' => array('type' => 'string'), 'iMAddress' => array('type' => 'string'), 'iMAddress2' => array('type' => 'string'), 'iMAddress3' => array('type' => 'string'), 'managerName' => array('type' => 'string'), 'mMS' => array('type' => 'string'), 'nickName' => array('type' => 'string'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/Device.php b/lib/ext/Syncroton/Model/Device.php index 342c1fa..b7b062d 100644 --- a/lib/ext/Syncroton/Model/Device.php +++ b/lib/ext/Syncroton/Model/Device.php @@ -1,53 +1,52 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model */ class Syncroton_Model_Device extends Syncroton_Model_AEntry implements Syncroton_Model_IDevice { const TYPE_IPHONE = 'iphone'; const TYPE_WEBOS = 'webos'; const TYPE_ANDROID = 'android'; const TYPE_ANDROID_40 = 'android40'; const TYPE_SMASUNGGALAXYS2 = 'samsunggti9100'; // Samsung Galaxy S-3 const TYPE_BLACKBERRY = 'blackberry'; - + /** * Returns major firmware version of this device - * - * @return int/string + * + * @return int|string */ public function getMajorVersion() { switch (strtolower($this->devicetype)) { case Syncroton_Model_Device::TYPE_BLACKBERRY: if (preg_match('/(.+)\/(.+)/', $this->useragent, $matches)) { list(, $name, $version) = $matches; return $version; } break; - + case Syncroton_Model_Device::TYPE_IPHONE: if (preg_match('/(.+)\/(\d+)\.(\d+)/', $this->useragent, $matches)) { list(, $name, $majorVersion, $minorVersion) = $matches; return $majorVersion; } break; } - + return 0; } } - diff --git a/lib/ext/Syncroton/Model/DeviceInformation.php b/lib/ext/Syncroton/Model/DeviceInformation.php index d1c8fff..7eb040d 100644 --- a/lib/ext/Syncroton/Model/DeviceInformation.php +++ b/lib/ext/Syncroton/Model/DeviceInformation.php @@ -1,42 +1,42 @@ */ /** * class to handle ActiveSync device information * * @package Syncroton * @subpackage Model - * @property string friendlyName - * @property string iMEI - * @property string mobileOperator - * @property string model - * @property string oS - * @property string oSLanguage - * @property string phoneNumber + * @property string $friendlyName + * @property string $iMEI + * @property string $mobileOperator + * @property string $model + * @property string $oS + * @property string $oSLanguage + * @property string $phoneNumber */ class Syncroton_Model_DeviceInformation extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Set'; protected $_properties = array( 'Settings' => array( 'enableOutboundSMS' => array('type' => 'number'), 'friendlyName' => array('type' => 'string'), 'iMEI' => array('type' => 'string'), 'mobileOperator' => array('type' => 'string'), 'model' => array('type' => 'string'), 'oS' => array('type' => 'string'), 'oSLanguage' => array('type' => 'string'), 'phoneNumber' => array('type' => 'string') ), ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/Email.php b/lib/ext/Syncroton/Model/Email.php index cd448c8..3e980e7 100644 --- a/lib/ext/Syncroton/Model/Email.php +++ b/lib/ext/Syncroton/Model/Email.php @@ -1,90 +1,90 @@ */ /** * class to handle ActiveSync email * * @package Syncroton * @subpackage Model - * @property array attachments - * @property string contentType - * @property array flag - * @property Syncroton_Model_EmailBody body - * @property array cc - * @property array to - * @property int lastVerbExecuted - * @property DateTime lastVerbExecutionTime - * @property int read + * @property array $attachments + * @property string $contentType + * @property array $flag + * @property Syncroton_Model_EmailBody $body + * @property array $cc + * @property array $to + * @property int $lastVerbExecuted + * @property DateTime $lastVerbExecutionTime + * @property int $read */ class Syncroton_Model_Email extends Syncroton_Model_AXMLEntry { const LASTVERB_UNKNOWN = 0; const LASTVERB_REPLYTOSENDER = 1; const LASTVERB_REPLYTOALL = 2; const LASTVERB_FORWARD = 3; protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'AirSyncBase' => array( 'attachments' => array('type' => 'container', 'childElement' => 'attachment', 'class' => 'Syncroton_Model_EmailAttachment'), 'contentType' => array('type' => 'string'), 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody'), 'nativeBodyType' => array('type' => 'number'), ), 'Email' => array( 'busyStatus' => array('type' => 'number'), 'categories' => array('type' => 'container', 'childElement' => 'category', 'supportedSince' => '14.0'), 'cc' => array('type' => 'string'), 'completeTime' => array('type' => 'datetime'), 'contentClass' => array('type' => 'string'), 'dateReceived' => array('type' => 'datetime'), 'disallowNewTimeProposal' => array('type' => 'number'), 'displayTo' => array('type' => 'string'), 'dTStamp' => array('type' => 'datetime'), 'endTime' => array('type' => 'datetime'), 'flag' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailFlag'), 'from' => array('type' => 'string'), 'globalObjId' => array('type' => 'string'), 'importance' => array('type' => 'number'), 'instanceType' => array('type' => 'number'), 'internetCPID' => array('type' => 'string'), 'location' => array('type' => 'string'), 'meetingRequest' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailMeetingRequest'), 'messageClass' => array('type' => 'string'), 'organizer' => array('type' => 'string'), 'read' => array('type' => 'number'), 'recurrences' => array('type' => 'container'), 'reminder' => array('type' => 'number'), 'replyTo' => array('type' => 'string'), 'responseRequested' => array('type' => 'number'), 'sensitivity' => array('type' => 'number'), 'startTime' => array('type' => 'datetime'), 'status' => array('type' => 'number'), 'subject' => array('type' => 'string'), 'threadTopic' => array('type' => 'string'), 'timeZone' => array('type' => 'timezone'), 'to' => array('type' => 'string'), ), 'Email2' => array( 'accountId' => array('type' => 'string', 'supportedSince' => '14.1'), 'conversationId' => array('type' => 'byteArray', 'supportedSince' => '14.0'), 'conversationIndex' => array('type' => 'byteArray', 'supportedSince' => '14.0'), 'lastVerbExecuted' => array('type' => 'number', 'supportedSince' => '14.0'), 'lastVerbExecutionTime' => array('type' => 'datetime', 'supportedSince' => '14.0'), 'meetingMessageType' => array('type' => 'number', 'supportedSince' => '14.1'), 'receivedAsBcc' => array('type' => 'number', 'supportedSince' => '14.0'), 'sender' => array('type' => 'string', 'supportedSince' => '14.0'), 'umCallerID' => array('type' => 'string', 'supportedSince' => '14.0'), 'umUserNotes' => array('type' => 'string', 'supportedSince' => '14.0'), ), ); } diff --git a/lib/ext/Syncroton/Model/EmailAttachment.php b/lib/ext/Syncroton/Model/EmailAttachment.php index bea0c1e..53446b5 100644 --- a/lib/ext/Syncroton/Model/EmailAttachment.php +++ b/lib/ext/Syncroton/Model/EmailAttachment.php @@ -1,43 +1,43 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property bool $getChanges + * @property string $syncKey + * @property int $windowSize */ class Syncroton_Model_EmailAttachment extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Attachment'; protected $_properties = array( 'AirSyncBase' => array( 'contentId' => array('type' => 'string'), 'contentLocation' => array('type' => 'string'), 'displayName' => array('type' => 'string'), 'estimatedDataSize' => array('type' => 'string'), 'fileReference' => array('type' => 'string'), 'isInline' => array('type' => 'number'), 'method' => array('type' => 'string'), ), 'Email2' => array( 'umAttDuration' => array('type' => 'number', 'supportedSince' => '14.0'), 'umAttOrder' => array('type' => 'number', 'supportedSince' => '14.0'), ), ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/EmailBody.php b/lib/ext/Syncroton/Model/EmailBody.php index b4d29ab..f274838 100644 --- a/lib/ext/Syncroton/Model/EmailBody.php +++ b/lib/ext/Syncroton/Model/EmailBody.php @@ -1,44 +1,44 @@ */ /** * class to handle AirSyncBase:Body * * @package Syncroton * @subpackage Model - * @property int EstimatedDataSize - * @property string Data - * @property string Part - * @property string Preview - * @property bool Truncated - * @property string Type + * @property int $estimatedDataSize + * @property string $data + * @property string $part + * @property string $preview + * @property bool $truncated + * @property string $type */ class Syncroton_Model_EmailBody extends Syncroton_Model_AXMLEntry { const TYPE_PLAINTEXT = 1; const TYPE_HTML = 2; const TYPE_RTF = 3; const TYPE_MIME = 4; protected $_xmlBaseElement = 'Body'; protected $_properties = array( 'AirSyncBase' => array( 'type' => array('type' => 'string'), 'estimatedDataSize' => array('type' => 'string'), 'data' => array('type' => 'string'), 'truncated' => array('type' => 'number'), 'part' => array('type' => 'number'), 'preview' => array('type' => 'string', 'supportedSince' => '14.0'), ), ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/EmailFlag.php b/lib/ext/Syncroton/Model/EmailFlag.php index 685623e..55c2bf5 100644 --- a/lib/ext/Syncroton/Model/EmailFlag.php +++ b/lib/ext/Syncroton/Model/EmailFlag.php @@ -1,60 +1,60 @@ * @author Aleksander Machniak */ /** * class to handle ActiveSync Flag element * * @package Syncroton * @subpackage Model - * @property DateTime CompleteTime - * @property DateTime DateCompleted - * @property DateTime DueDate - * @property string FlagType - * @property DateTime OrdinalDate - * @property int ReminderSet - * @property DateTime ReminderTime - * @property DateTime StartDate - * @property string Status - * @property string Subject - * @property string SubOrdinalDate - * @property DateTime UtcDueDate - * @property DateTime UtcStartDate + * @property DateTime $completeTime + * @property DateTime $dateCompleted + * @property DateTime $dueDate + * @property string $flagType + * @property DateTime $ordinalDate + * @property int $reminderSet + * @property DateTime $reminderTime + * @property DateTime $startDate + * @property string $status + * @property string $subject + * @property string $subOrdinalDate + * @property DateTime $utcDueDate + * @property DateTime $utcStartDate */ class Syncroton_Model_EmailFlag extends Syncroton_Model_AXMLEntry { const STATUS_CLEARED = 0; const STATUS_COMPLETE = 1; const STATUS_ACTIVE = 2; protected $_xmlBaseElement = 'Flag'; protected $_properties = array( 'Email' => array( 'completeTime' => array('type' => 'datetime'), 'flagType' => array('type' => 'string'), 'status' => array('type' => 'number'), ), 'Tasks' => array( 'dateCompleted' => array('type' => 'datetime'), 'dueDate' => array('type' => 'datetime'), 'ordinalDate' => array('type' => 'datetime'), 'reminderSet' => array('type' => 'number'), 'reminderTime' => array('type' => 'datetime'), 'startDate' => array('type' => 'datetime'), 'subject' => array('type' => 'string'), 'subOrdinalDate' => array('type' => 'string'), 'utcStartDate' => array('type' => 'datetime'), 'utcDueDate' => array('type' => 'datetime'), ), ); } diff --git a/lib/ext/Syncroton/Model/EmailMeetingRequest.php b/lib/ext/Syncroton/Model/EmailMeetingRequest.php index d3023e5..ece7755 100644 --- a/lib/ext/Syncroton/Model/EmailMeetingRequest.php +++ b/lib/ext/Syncroton/Model/EmailMeetingRequest.php @@ -1,96 +1,96 @@ */ /** * class to handle Email:MeetingRequest * * @package Syncroton * @subpackage Model - * @property bool AllDayEvent - * @property int BusyStatus - * @property int DisallowNewTimeProposal - * @property DateTime DtStamp - * @property DateTime EndTime - * @property string GlobalObjId - * @property int InstanceType - * @property int MeetingMessageType - * @property string Organizer - * @property string RecurrenceId - * @property array Recurrences - * @property int Reminder - * @property int ResponseRequested - * @property int Sensitivity - * @property DateTime StartTime - * @property string Timezone + * @property bool $allDayEvent + * @property int $busyStatus + * @property int $disallowNewTimeProposal + * @property DateTime $dtStamp + * @property DateTime $endTime + * @property string $globalObjId + * @property int $instanceType + * @property int $meetingMessageType + * @property string $organizer + * @property string $recurrenceId + * @property array $recurrences + * @property int $reminder + * @property int $responseRequested + * @property int $sensitivity + * @property DateTime $startTime + * @property string $timezone */ class Syncroton_Model_EmailMeetingRequest extends Syncroton_Model_AXMLEntry { /** * busy status constants */ const BUSY_STATUS_FREE = 0; const BUSY_STATUS_TENATTIVE = 1; const BUSY_STATUS_BUSY = 2; const BUSY_STATUS_OUT = 3; /** * sensitivity constants */ const SENSITIVITY_NORMAL = 0; const SENSITIVITY_PERSONAL = 1; const SENSITIVITY_PRIVATE = 2; const SENSITIVITY_CONFIDENTIAL = 3; /** * instanceType constants */ const TYPE_NORMAL = 0; const TYPE_RECURRING_MASTER = 1; const TYPE_RECURRING_SINGLE = 2; const TYPE_RECURRING_EXCEPTION = 3; /** * messageType constants */ const MESSAGE_TYPE_NORMAL = 0; const MESSAGE_TYPE_REQUEST = 1; const MESSAGE_TYPE_FULL_UPDATE = 2; const MESSAGE_TYPE_INFO_UPDATE = 3; const MESSAGE_TYPE_OUTDATED = 4; const MESSAGE_TYPE_COPY = 5; const MESSAGE_TYPE_DELEGATED = 6; protected $_xmlBaseElement = 'MeetingRequest'; protected $_properties = array( 'Email' => array( 'allDayEvent' => array('type' => 'number'), 'busyStatus' => array('type' => 'number'), 'disallowNewTimeProposal' => array('type' => 'number'), 'dtStamp' => array('type' => 'datetime'), 'endTime' => array('type' => 'datetime'), 'globalObjId' => array('type' => 'string'), 'instanceType' => array('type' => 'number'), 'location' => array('type' => 'string'), 'organizer' => array('type' => 'string'), //e-mail address 'recurrenceId' => array('type' => 'datetime'), 'recurrences' => array('type' => 'container'), 'reminder' => array('type' => 'number'), 'responseRequested' => array('type' => 'number'), 'sensitivity' => array('type' => 'number'), 'startTime' => array('type' => 'datetime'), 'timeZone' => array('type' => 'timezone'), ), 'Email2' => array( 'meetingMessageType' => array('type' => 'number'), ), ); } diff --git a/lib/ext/Syncroton/Model/EmailRecurrence.php b/lib/ext/Syncroton/Model/EmailRecurrence.php index b90cf19..698cb28 100644 --- a/lib/ext/Syncroton/Model/EmailRecurrence.php +++ b/lib/ext/Syncroton/Model/EmailRecurrence.php @@ -1,74 +1,73 @@ */ /** * class to handle Email::Recurrence * * @package Syncroton * @subpackage Model - * @property int CalendarType - * @property int DayOfMonth - * @property int DayOfWeek - * @property int FirstDayOfWeek - * @property int Interval - * @property int IsLeapMonth - * @property int MonthOfYear - * @property int Occurrences - * @property int Type - * @property DateTime Until - * @property int WeekOfMonth + * @property int $calendarType + * @property int $dayOfMonth + * @property int $dayOfWeek + * @property int $firstDayOfWeek + * @property int $interval + * @property int $isLeapMonth + * @property int $monthOfYear + * @property int $occurrences + * @property int $type + * @property DateTime $until + * @property int $weekOfMonth */ - class Syncroton_Model_EmailRecurrence extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Recurrence'; /** * recur types */ const TYPE_DAILY = 0; // Recurs daily const TYPE_WEEKLY = 1; // Recurs weekly const TYPE_MONTHLY = 3; // Recurs monthly const TYPE_MONTHLY_DAYN = 2; // Recurs monthly on the nth day const TYPE_YEARLY = 5; // Recurs yearly on the nth day of the nth month each year const TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day of the week of the nth month /** * day of week constants */ const RECUR_DOW_SUNDAY = 1; const RECUR_DOW_MONDAY = 2; const RECUR_DOW_TUESDAY = 4; const RECUR_DOW_WEDNESDAY = 8; const RECUR_DOW_THURSDAY = 16; const RECUR_DOW_FRIDAY = 32; const RECUR_DOW_SATURDAY = 64; protected $_dateTimeFormat = "Ymd\THis\Z"; protected $_properties = array( 'Email' => array( 'dayOfMonth' => array('type' => 'number'), 'dayOfWeek' => array('type' => 'number'), 'interval' => array('type' => 'number'), // 1 or 2 'monthOfYear' => array('type' => 'number'), 'occurrences' => array('type' => 'number'), 'type' => array('type' => 'number'), 'until' => array('type' => 'datetime'), 'weekOfMonth' => array('type' => 'number'), ), 'Email2' => array( 'calendarType' => array('type' => 'number'), 'firstDayOfWeek' => array('type' => 'number'), 'isLeapMonth' => array('type' => 'number'), ) ); } diff --git a/lib/ext/Syncroton/Model/Event.php b/lib/ext/Syncroton/Model/Event.php index 79686a9..55879f2 100644 --- a/lib/ext/Syncroton/Model/Event.php +++ b/lib/ext/Syncroton/Model/Event.php @@ -1,127 +1,130 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * + * @property bool $allDayEvent + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property DateTime $endTime + * @property bool $getChanges + * @property DateTime $startTime + * @property string $syncKey + * @property int $windowSize */ - class Syncroton_Model_Event extends Syncroton_Model_AXMLEntry { /** * busy status constants */ const BUSY_STATUS_FREE = 0; const BUSY_STATUS_TENATTIVE = 1; const BUSY_STATUS_BUSY = 2; protected $_dateTimeFormat = "Ymd\THis\Z"; protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'AirSyncBase' => array( 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody') ), 'Calendar' => array( 'allDayEvent' => array('type' => 'number'), 'appointmentReplyTime' => array('type' => 'datetime'), 'attendees' => array('type' => 'container', 'childElement' => 'attendee', 'class' => 'Syncroton_Model_EventAttendee'), 'busyStatus' => array('type' => 'number'), 'categories' => array('type' => 'container', 'childElement' => 'category'), 'disallowNewTimeProposal' => array('type' => 'number'), 'dtStamp' => array('type' => 'datetime'), 'endTime' => array('type' => 'datetime'), 'exceptions' => array('type' => 'container', 'childElement' => 'exception', 'class' => 'Syncroton_Model_EventException'), 'location' => array('type' => 'string'), 'meetingStatus' => array('type' => 'number'), 'onlineMeetingConfLink' => array('type' => 'string'), 'onlineMeetingExternalLink' => array('type' => 'string'), 'organizerEmail' => array('type' => 'string'), 'organizerName' => array('type' => 'string'), 'recurrence' => array('type' => 'container'), 'reminder' => array('type' => 'number'), 'responseRequested' => array('type' => 'number'), 'responseType' => array('type' => 'number'), 'sensitivity' => array('type' => 'number'), 'startTime' => array('type' => 'datetime'), 'subject' => array('type' => 'string'), 'timezone' => array('type' => 'timezone'), 'uID' => array('type' => 'string'), ) ); /** * (non-PHPdoc) * @see Syncroton_Model_IEntry::appendXML() * @todo handle Attendees element */ public function appendXML(DOMElement $domParrent, Syncroton_Model_IDevice $device) { parent::appendXML($domParrent, $device); $exceptionElements = $domParrent->getElementsByTagName('Exception'); $parentFields = array('AllDayEvent'/*, 'Attendees'*/, 'Body', 'BusyStatus'/*, 'Categories'*/, 'DtStamp', 'EndTime', 'Location', 'MeetingStatus', 'Reminder', 'ResponseType', 'Sensitivity', 'StartTime', 'Subject'); if ($exceptionElements->length > 0) { $mainEventElement = $exceptionElements->item(0)->parentNode->parentNode; foreach ($mainEventElement->childNodes as $childNode) { if (in_array($childNode->localName, $parentFields)) { foreach ($exceptionElements as $exception) { $elementsToLeftOut = $exception->getElementsByTagName($childNode->localName); foreach ($elementsToLeftOut as $elementToLeftOut) { if ($elementToLeftOut->nodeValue == $childNode->nodeValue) { $exception->removeChild($elementToLeftOut); } } } } } } } /** * some elements of an exception can be left out, if they have the same value * like the main event * * this function copies these elements to the exception for backends which need * this elements in the exceptions too. Tine 2.0 needs this for example. */ public function copyFieldsFromParent() { if (isset($this->_elements['exceptions']) && is_array($this->_elements['exceptions'])) { foreach ($this->_elements['exceptions'] as $exception) { // no need to update deleted exceptions if ($exception->deleted == 1) { continue; } $parentFields = array('allDayEvent', 'attendees', 'body', 'busyStatus', 'categories', 'dtStamp', 'endTime', 'location', 'meetingStatus', 'reminder', 'responseType', 'sensitivity', 'startTime', 'subject'); foreach ($parentFields as $field) { if (!isset($exception->$field) && isset($this->_elements[$field])) { $exception->$field = $this->_elements[$field]; } } } } } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/EventAttendee.php b/lib/ext/Syncroton/Model/EventAttendee.php index 8803427..27328b0 100644 --- a/lib/ext/Syncroton/Model/EventAttendee.php +++ b/lib/ext/Syncroton/Model/EventAttendee.php @@ -1,53 +1,52 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property bool $getChanges + * @property string $syncKey + * @property int $windowSize */ - class Syncroton_Model_EventAttendee extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Attendee'; /** * attendee status */ const ATTENDEE_STATUS_UNKNOWN = 0; const ATTENDEE_STATUS_TENTATIVE = 2; const ATTENDEE_STATUS_ACCEPTED = 3; const ATTENDEE_STATUS_DECLINED = 4; const ATTENDEE_STATUS_NOTRESPONDED = 5; /** * attendee types */ const ATTENDEE_TYPE_REQUIRED = 1; const ATTENDEE_TYPE_OPTIONAL = 2; const ATTENDEE_TYPE_RESOURCE = 3; protected $_properties = array( 'Calendar' => array( 'attendeeStatus' => array('type' => 'number'), 'attendeeType' => array('type' => 'number'), 'email' => array('type' => 'string'), 'name' => array('type' => 'string'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/EventException.php b/lib/ext/Syncroton/Model/EventException.php index 06aece9..9b82e15 100644 --- a/lib/ext/Syncroton/Model/EventException.php +++ b/lib/ext/Syncroton/Model/EventException.php @@ -1,54 +1,57 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * + * @property bool $allDayEvent + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property DateTime $endTime + * @property bool $getChanges + * @property DateTime $startTime + * @property string $syncKey + * @property int $windowSize */ - class Syncroton_Model_EventException extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Exception'; protected $_dateTimeFormat = "Ymd\THis\Z"; protected $_properties = array( 'AirSyncBase' => array( 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody') ), 'Calendar' => array( 'allDayEvent' => array('type' => 'number'), 'appointmentReplyTime' => array('type' => 'datetime'), 'attendees' => array('type' => 'container', 'childElement' => 'attendee', 'class' => 'Syncroton_Model_EventAttendee'), 'busyStatus' => array('type' => 'number'), 'categories' => array('type' => 'container', 'childElement' => 'category'), 'deleted' => array('type' => 'number'), 'dtStamp' => array('type' => 'datetime'), 'endTime' => array('type' => 'datetime'), 'exceptionStartTime' => array('type' => 'datetime'), 'location' => array('type' => 'string'), 'meetingStatus' => array('type' => 'number'), 'reminder' => array('type' => 'number'), 'responseType' => array('type' => 'number'), 'sensitivity' => array('type' => 'number'), 'startTime' => array('type' => 'datetime'), 'subject' => array('type' => 'string'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/EventRecurrence.php b/lib/ext/Syncroton/Model/EventRecurrence.php index ac53984..80c3bb0 100644 --- a/lib/ext/Syncroton/Model/EventRecurrence.php +++ b/lib/ext/Syncroton/Model/EventRecurrence.php @@ -1,72 +1,71 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property int CalendarType - * @property int DayOfMonth - * @property int DayOfWeek - * @property int FirstDayOfWeek - * @property int Interval - * @property int IsLeapMonth - * @property int MonthOfYear - * @property int Occurrences - * @property int Type - * @property DateTime Until - * @property int WeekOfMonth + * @property int $calendarType + * @property int $dayOfMonth + * @property int $dayOfWeek + * @property int $firstDayOfWeek + * @property int $interval + * @property int $isLeapMonth + * @property int $monthOfYear + * @property int $occurrences + * @property int $type + * @property DateTime $until + * @property int $weekOfMonth */ - class Syncroton_Model_EventRecurrence extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Recurrence'; /** * recur types */ const TYPE_DAILY = 0; // Recurs daily. const TYPE_WEEKLY = 1; // Recurs weekly const TYPE_MONTHLY = 2; // Recurs monthly const TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day const TYPE_YEARLY = 5; // Recurs yearly const TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day /** * day of week constants */ const RECUR_DOW_SUNDAY = 1; const RECUR_DOW_MONDAY = 2; const RECUR_DOW_TUESDAY = 4; const RECUR_DOW_WEDNESDAY = 8; const RECUR_DOW_THURSDAY = 16; const RECUR_DOW_FRIDAY = 32; const RECUR_DOW_SATURDAY = 64; protected $_dateTimeFormat = "Ymd\THis\Z"; protected $_properties = array( 'Calendar' => array( 'calendarType' => array('type' => 'number'), 'dayOfMonth' => array('type' => 'number'), 'dayOfWeek' => array('type' => 'number'), 'firstDayOfWeek' => array('type' => 'number'), 'interval' => array('type' => 'number'), 'isLeapMonth' => array('type' => 'number'), 'monthOfYear' => array('type' => 'number'), 'occurrences' => array('type' => 'number'), 'type' => array('type' => 'number'), 'until' => array('type' => 'datetime'), 'weekOfMonth' => array('type' => 'number'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/FileReference.php b/lib/ext/Syncroton/Model/FileReference.php index a34d37c..7302aad 100644 --- a/lib/ext/Syncroton/Model/FileReference.php +++ b/lib/ext/Syncroton/Model/FileReference.php @@ -1,46 +1,45 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property string ContentType - * @property string Data + * @property string $contentType + * @property string $data + * @property int $part */ - class Syncroton_Model_FileReference extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'ApplicationData'; - + protected $_properties = array( 'AirSyncBase' => array( 'contentType' => array('type' => 'string'), ), 'ItemOperations' => array( 'data' => array('type' => 'string', 'encoding' => 'base64'), 'part' => array('type' => 'number') ) ); - + /** - * - * @param SimpleXMLElement $xmlCollection + * + * @param SimpleXMLElement $properties * @throws InvalidArgumentException */ public function setFromSimpleXMLElement(SimpleXMLElement $properties) { - //do nothing - + // do nothing return; } -} \ No newline at end of file +} diff --git a/lib/ext/Syncroton/Model/GAL.php b/lib/ext/Syncroton/Model/GAL.php index 64765ce..3a9807d 100644 --- a/lib/ext/Syncroton/Model/GAL.php +++ b/lib/ext/Syncroton/Model/GAL.php @@ -1,51 +1,51 @@ * @author Aleksander Machniak */ /** * class to handle ActiveSync GAL result * * @package Syncroton * @subpackage Model * - * @property string Alias - * @property string Company - * @property string DisplayName - * @property string EmailAddress - * @property string FirstName - * @property string LastName - * @property string MobilePhone - * @property string Office - * @property string Phone - * @property string Picture - * @property string Title + * @property string $alias + * @property string $company + * @property string $displayName + * @property string $emailAddress + * @property string $firstName + * @property string $lastName + * @property string $mobilePhone + * @property string $office + * @property string $phone + * @property string $picture + * @property string $title */ class Syncroton_Model_GAL extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'GAL' => array( 'alias' => array('type' => 'string', 'supportedSince' => '2.5'), 'company' => array('type' => 'string', 'supportedSince' => '2.5'), 'displayName' => array('type' => 'string', 'supportedSince' => '2.5'), 'emailAddress' => array('type' => 'string', 'supportedSince' => '2.5'), 'firstName' => array('type' => 'string', 'supportedSince' => '2.5'), 'lastName' => array('type' => 'string', 'supportedSince' => '2.5'), 'mobilePhone' => array('type' => 'string', 'supportedSince' => '2.5'), 'office' => array('type' => 'string', 'supportedSince' => '2.5'), 'phone' => array('type' => 'string', 'supportedSince' => '2.5'), 'picture' => array('type' => 'container', 'supportedSince' => '14.0'), 'title' => array('type' => 'string', 'supportedSince' => '2.5'), ) ); } diff --git a/lib/ext/Syncroton/Model/GALPicture.php b/lib/ext/Syncroton/Model/GALPicture.php index 8122490..f6801a1 100644 --- a/lib/ext/Syncroton/Model/GALPicture.php +++ b/lib/ext/Syncroton/Model/GALPicture.php @@ -1,40 +1,40 @@ * @author Aleksander Machniak */ /** * class to handle ActiveSync GAL Picture element * * @package Syncroton * @subpackage Model * - * @property string Status - * @property string Data + * @property string $status + * @property string $data */ class Syncroton_Model_GALPicture extends Syncroton_Model_AXMLEntry { const STATUS_SUCCESS = 1; const STATUS_NOPHOTO = 173; const STATUS_TOOLARGE = 174; const STATUS_OVERLIMIT = 175; protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'AirSync' => array( 'status' => array('type' => 'number'), ), 'GAL' => array( 'data' => array('type' => 'byteArray'), ), ); } diff --git a/lib/ext/Syncroton/Model/IContent.php b/lib/ext/Syncroton/Model/IContent.php index 62eab66..2aa29af 100644 --- a/lib/ext/Syncroton/Model/IContent.php +++ b/lib/ext/Syncroton/Model/IContent.php @@ -1,28 +1,28 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model - * @property string id - * @property string device_id - * @property string folder_id - * @property string contentid - * @property DateTime creation_time - * @property string creation_synckey - * @property string is_deleted + * @property string $id + * @property string $device_id + * @property string $folder_id + * @property string $contentid + * @property DateTime $creation_time + * @property string $creation_synckey + * @property string $is_deleted */ interface Syncroton_Model_IContent { } diff --git a/lib/ext/Syncroton/Model/IDevice.php b/lib/ext/Syncroton/Model/IDevice.php index 9765345..02b5080 100644 --- a/lib/ext/Syncroton/Model/IDevice.php +++ b/lib/ext/Syncroton/Model/IDevice.php @@ -1,52 +1,50 @@ + * @copyright Copyright (c) 2012-2014 Kolab Systems AG + * @author Aleksander Machniak */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model - * @property string id - * @property string deviceid - * @property string devicetype - * @property string policykey - * @property string policyId - * @property string ownerId - * @property string acsversion - * @property string pingfolder - * @property string pinglifetime - * @property string remotewipe - * @property string useragent - * @property string imei - * @property string model - * @property string friendlyname - * @property string os - * @property string oslanguage - * @property string phonenumber - * @property string pinglifetime - * @property string pingfolder - * @property string contactsfilter_id - * @property string calendarfilter_id - * @property string tasksfilter_id - * @property string emailfilter_id - * @property string lastsynccollection - * @property DateTime lastping + * @property string $acsversion + * @property string $deviceid + * @property string $devicetype + * @property string $friendlyname + * @property string $id + * @property string $imei + * @property string $model + * @property string $os + * @property string $oslanguage + * @property string $ownerId + * @property string $phonenumber + * @property string $pingfolder + * @property int $pinglifetime + * @property string $policykey + * @property string $policyId + * @property int $remotewipe + * @property string $useragent + * @property string $contactsfilter_id + * @property string $calendarfilter_id + * @property string $tasksfilter_id + * @property string $emailfilter_id + * @property string $lastsynccollection + * @property DateTime $lastping */ interface Syncroton_Model_IDevice extends Syncroton_Model_IEntry { /** * Returns major firmware version of this device * * @return int/string */ public function getMajorVersion(); } - diff --git a/lib/ext/Syncroton/Model/IEntry.php b/lib/ext/Syncroton/Model/IEntry.php index 158f389..528121e 100644 --- a/lib/ext/Syncroton/Model/IEntry.php +++ b/lib/ext/Syncroton/Model/IEntry.php @@ -1,42 +1,42 @@ */ /** - * class to handle ActiveSync contact + * class to handle ActiveSync entry * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property bool $getChanges + * @property string $syncKey + * @property int $windowSize */ interface Syncroton_Model_IEntry { /** * - * @param unknown_type $properties + * @param SimpleXMLElement|array|null $properties */ public function __construct($properties = null); /** * return true if data have got changed after initial data got loaded via constructor */ public function isDirty(); /** * * @param array $properties */ public function setFromArray(array $properties); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/IFolder.php b/lib/ext/Syncroton/Model/IFolder.php index 113887d..14f2851 100644 --- a/lib/ext/Syncroton/Model/IFolder.php +++ b/lib/ext/Syncroton/Model/IFolder.php @@ -1,30 +1,30 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model - * @property string id - * @property string deviceId - * @property string class - * @property string serverId - * @property string parentId - * @property string displayName - * @property string creationTime - * @property string lastfiltertype - * @property string type + * @property string $id + * @property string $deviceId + * @property string $class + * @property string $serverId + * @property string $parentId + * @property string $displayName + * @property DateTime $creationTime + * @property int $lastfiltertype + * @property int $type */ interface Syncroton_Model_IFolder { } diff --git a/lib/ext/Syncroton/Model/IPolicy.php b/lib/ext/Syncroton/Model/IPolicy.php index 999ecc5..416cf71 100644 --- a/lib/ext/Syncroton/Model/IPolicy.php +++ b/lib/ext/Syncroton/Model/IPolicy.php @@ -1,32 +1,32 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model - * @property string id - * @property string deviceid - * @property string devicetype - * @property string policyKey - * @property string policyId - * @property string ownerId - * @property string acsversion - * @property string pingfolder - * @property string pinglifetime - * @property string remotewipe - * @property string useragent + * @property string $id + * @property string $deviceid + * @property string $devicetype + * @property string $policyKey + * @property string $policyId + * @property string $ownerId + * @property string $acsversion + * @property string $pingfolder + * @property string $pinglifetime + * @property string $remotewipe + * @property string $useragent */ interface Syncroton_Model_IPolicy { } diff --git a/lib/ext/Syncroton/Model/ISyncState.php b/lib/ext/Syncroton/Model/ISyncState.php index 2f50b7c..418de85 100644 --- a/lib/ext/Syncroton/Model/ISyncState.php +++ b/lib/ext/Syncroton/Model/ISyncState.php @@ -1,28 +1,29 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model - * @property string device_id - * @property string type - * @property string counter - * @property DateTime lastsync - * @property string pendingdata - * @property string client_id_map - * @property string extraData + * @property string $deviceId + * @property string $type + * @property int $counter + * @property DateTime $lastsync + * @property string $id + * @property ?array $pendingdata + * @property string $clientIdMap JSON-encoded array + * @property string $extraData JSON-encoded array */ interface Syncroton_Model_ISyncState { } diff --git a/lib/ext/Syncroton/Model/IXMLEntry.php b/lib/ext/Syncroton/Model/IXMLEntry.php index adc18cd..2233f9b 100644 --- a/lib/ext/Syncroton/Model/IXMLEntry.php +++ b/lib/ext/Syncroton/Model/IXMLEntry.php @@ -1,40 +1,40 @@ */ /** * class to handle ActiveSync contact * * @package Syncroton * @subpackage Model */ interface Syncroton_Model_IXMLEntry extends Syncroton_Model_IEntry { /** * - * @param DOMElement $_domParrent + * @param DOMElement $_domParent * @param Syncroton_Model_IDevice $device */ - public function appendXML(DOMElement $_domParrent, Syncroton_Model_IDevice $device); + public function appendXML(DOMElement $_domParent, Syncroton_Model_IDevice $device); /** * return array of valid properties * * @return array */ - public function getProperties(); + public function getProperties($selectedNamespace = null); /** * - * @param SimpleXMLElement $xmlCollection + * @param SimpleXMLElement $properties * @throws InvalidArgumentException */ public function setFromSimpleXMLElement(SimpleXMLElement $properties); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/MeetingResponse.php b/lib/ext/Syncroton/Model/MeetingResponse.php index 0a8d321..d3b120d 100644 --- a/lib/ext/Syncroton/Model/MeetingResponse.php +++ b/lib/ext/Syncroton/Model/MeetingResponse.php @@ -1,47 +1,47 @@ */ /** * class to handle MeetingResponse request * * @package Syncroton * @subpackage Model - * @property int userResponse - * @property string collectionId - * @property string calendarId - * @property string requestId - * @property string instanceId - * @property string longId + * @property int $userResponse + * @property string $collectionId + * @property string $calendarId + * @property string $requestId + * @property string $instanceId + * @property string $longId */ class Syncroton_Model_MeetingResponse extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Request'; /** * attendee status */ const RESPONSE_ACCEPTED = 1; const RESPONSE_TENTATIVE = 2; const RESPONSE_DECLINED = 3; protected $_properties = array( 'MeetingResponse' => array( 'userResponse' => array('type' => 'number'), 'collectionId' => array('type' => 'string'), 'calendarId' => array('type' => 'string'), 'requestId' => array('type' => 'string'), 'instanceId' => array('type' => 'datetime'), ), 'Search' => array( 'longId' => array('type' => 'string') ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/Note.php b/lib/ext/Syncroton/Model/Note.php index 5f4b542..4425391 100644 --- a/lib/ext/Syncroton/Model/Note.php +++ b/lib/ext/Syncroton/Model/Note.php @@ -1,38 +1,38 @@ */ /** * class to handle ActiveSync note * * @package Syncroton * @subpackage Model - * @property Syncroton_Model_EmailBody body - * @property array categories - * @property DateTime lastModifiedDate - * @property string messageClass - * @property string subject + * @property Syncroton_Model_EmailBody $body + * @property array $categories + * @property DateTime $lastModifiedDate + * @property string $messageClass + * @property string $subject */ class Syncroton_Model_Note extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'AirSyncBase' => array( 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody') ), 'Notes' => array( 'categories' => array('type' => 'container', 'childElement' => 'category'), 'lastModifiedDate' => array('type' => 'datetime'), 'messageClass' => array('type' => 'string'), 'subject' => array('type' => 'string'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/Oof.php b/lib/ext/Syncroton/Model/Oof.php index 65354f9..8a6c1fc 100644 --- a/lib/ext/Syncroton/Model/Oof.php +++ b/lib/ext/Syncroton/Model/Oof.php @@ -1,35 +1,40 @@ */ /** * Class to handle ActiveSync Settings/Oof/Get|Set element * * @package Syncroton * @subpackage Model + * + * @property DateTime $endTime + * @property Syncroton_Model_OofMessage $oofMessage + * @property int $oofState + * @property DateTime $startTime */ class Syncroton_Model_Oof extends Syncroton_Model_AXMLEntry { const STATUS_DISABLED = 0; const STATUS_GLOBAL = 1; const STATUS_TIME_BASED = 2; protected $_xmlBaseElement = array('Get', 'Set'); protected $_properties = array( 'Settings' => array( 'endTime' => array('type' => 'datetime'), 'oofMessage' => array('type' => 'container', 'multiple' => true, 'class' => 'Syncroton_Model_OofMessage'), 'oofState' => array('type' => 'number'), 'startTime' => array('type' => 'datetime'), ) ); } diff --git a/lib/ext/Syncroton/Model/Policy.php b/lib/ext/Syncroton/Model/Policy.php index 41ae331..b292de4 100644 --- a/lib/ext/Syncroton/Model/Policy.php +++ b/lib/ext/Syncroton/Model/Policy.php @@ -1,76 +1,74 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model */ class Syncroton_Model_Policy extends Syncroton_Model_AXMLEntry implements Syncroton_Model_IPolicy { protected $_xmlBaseElement = 'EASProvisionDoc'; protected $_properties = array( 'Internal' => array( 'id' => array('type' => 'string'), 'description' => array('type' => 'string'), 'name' => array('type' => 'string'), 'policyKey' => array('type' => 'string'), ), 'Provision' => array( 'allowBluetooth' => array('type' => 'number'), - 'allowSMIMEEncryptionAlgorithmNegotiation' => array('type' => 'number'), 'allowBrowser' => array('type' => 'number'), 'allowCamera' => array('type' => 'number'), 'allowConsumerEmail' => array('type' => 'number'), 'allowDesktopSync' => array('type' => 'number'), 'allowHTMLEmail' => array('type' => 'number'), 'allowInternetSharing' => array('type' => 'number'), 'allowIrDA' => array('type' => 'number'), 'allowPOPIMAPEmail' => array('type' => 'number'), 'allowRemoteDesktop' => array('type' => 'number'), 'allowSimpleDevicePassword' => array('type' => 'number'), 'allowSMIMEEncryptionAlgorithmNegotiation' => array('type' => 'number'), 'allowSMIMESoftCerts' => array('type' => 'number'), 'allowStorageCard' => array('type' => 'number'), 'allowTextMessaging' => array('type' => 'number'), 'allowUnsignedApplications' => array('type' => 'number'), 'allowUnsignedInstallationPackages' => array('type' => 'number'), 'allowWifi' => array('type' => 'number'), 'alphanumericDevicePasswordRequired' => array('type' => 'number'), 'approvedApplicationList' => array('type' => 'container', 'childName' => 'Hash'), 'attachmentsEnabled' => array('type' => 'number'), 'devicePasswordEnabled' => array('type' => 'number'), 'devicePasswordExpiration' => array('type' => 'number'), 'devicePasswordHistory' => array('type' => 'number'), 'maxAttachmentSize' => array('type' => 'number'), 'maxCalendarAgeFilter' => array('type' => 'number'), 'maxDevicePasswordFailedAttempts' => array('type' => 'number'), 'maxEmailAgeFilter' => array('type' => 'number'), 'maxEmailBodyTruncationSize' => array('type' => 'number'), 'maxEmailHTMLBodyTruncationSize' => array('type' => 'number'), 'maxInactivityTimeDeviceLock' => array('type' => 'number'), 'minDevicePasswordComplexCharacters' => array('type' => 'number'), 'minDevicePasswordLength' => array('type' => 'number'), 'passwordRecoveryEnabled' => array('type' => 'number'), 'requireDeviceEncryption' => array('type' => 'number'), 'requireEncryptedSMIMEMessages' => array('type' => 'number'), 'requireEncryptionSMIMEAlgorithm' => array('type' => 'number'), 'requireManualSyncWhenRoaming' => array('type' => 'number'), 'requireSignedSMIMEAlgorithm' => array('type' => 'number'), 'requireSignedSMIMEMessages' => array('type' => 'number'), 'requireStorageCardEncryption' => array('type' => 'number'), 'unapprovedInROMApplicationList' => array('type' => 'container', 'childName' => 'ApplicationName') ) ); } - diff --git a/lib/ext/Syncroton/Model/StoreRequest.php b/lib/ext/Syncroton/Model/StoreRequest.php index 16728a3..a04db19 100644 --- a/lib/ext/Syncroton/Model/StoreRequest.php +++ b/lib/ext/Syncroton/Model/StoreRequest.php @@ -1,247 +1,247 @@ * @author Aleksander Machniak */ /** * class to handle ActiveSync Search Store request * * @package Syncroton * @subpackage Model - * @property string name - * @property array options - * @property array query + * @property string $name + * @property array $options + * @property array $query */ class Syncroton_Model_StoreRequest { protected $_store = array(); protected $_xmlStore; public function __construct($properties = null) { if ($properties instanceof SimpleXMLElement) { $this->setFromSimpleXMLElement($properties); } elseif (is_array($properties)) { $this->setFromArray($properties); } } public function setFromArray(array $properties) { $this->_store = array( 'options' => array( 'mimeSupport' => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME, 'bodyPreferences' => array() ), ); foreach ($properties as $key => $value) { try { $this->$key = $value; //echo __LINE__ . PHP_EOL; } catch (InvalidArgumentException $iae) { //ignore invalid properties //echo __LINE__ . PHP_EOL; } } } /** * * @param SimpleXMLElement $xmlStore * @throws InvalidArgumentException */ public function setFromSimpleXMLElement(SimpleXMLElement $xmlStore) { if ($xmlStore->getName() !== 'Store') { throw new InvalidArgumentException('Unexpected element name: ' . $xmlStore->getName()); } $this->_xmlStore = $xmlStore; $this->_store = array( 'name' => (string) $xmlStore->Name, 'options' => array( 'mimeSupport' => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME, 'bodyPreferences' => array(), ), ); // Process Query if ($this->_store['name'] == 'GAL') { // @FIXME: In GAL search request Query is a string: // GALstring0-11 if (isset($xmlStore->Query)) { $this->_store['query'] = (string) $xmlStore->Query; } } elseif (isset($xmlStore->Query)) { if (isset($xmlStore->Query->And)) { if (isset($xmlStore->Query->And->FreeText)) { $this->_store['query']['and']['freeText'] = (string) $xmlStore->Query->And->FreeText; } if (isset($xmlStore->Query->And->ConversationId)) { $this->_store['query']['and']['conversationId'] = (string) $xmlStore->Query->And->ConversationId; } // Protocol specification defines Value as string and DateReceived as datetime, but // PocketPC device I tested sends XML as follows: // // // 2012-08-02T16:54:11.000Z // if (isset($xmlStore->Query->And->GreaterThan)) { if (isset($xmlStore->Query->And->GreaterThan->Value)) { $value = (string) $xmlStore->Query->And->GreaterThan->Value; $this->_store['query']['and']['greaterThan']['value'] = new DateTime($value, new DateTimeZone('UTC')); } $email = $xmlStore->Query->And->GreaterThan->children('uri:Email'); if (isset($email->DateReceived)) { $this->_store['query']['and']['greaterThan']['dateReceived'] = true; } } if (isset($xmlStore->Query->And->LessThan)) { if (isset($xmlStore->Query->And->LessThan->Value)) { $value = (string) $xmlStore->Query->And->LessThan->Value; $this->_store['query']['and']['lessThan']['value'] = new DateTime($value, new DateTimeZone('UTC')); } $email = $xmlStore->Query->And->LessThan->children('uri:Email'); if (isset($email->DateReceived)) { $this->_store['query']['and']['leasThan']['dateReceived'] = true; } } $airSync = $xmlStore->Query->And->children('uri:AirSync'); foreach ($airSync as $name => $value) { if ($name == 'Class') { $this->_store['query']['and']['classes'][] = (string) $value; } elseif ($name == 'CollectionId') { $this->_store['query']['and']['collections'][] = (string) $value; } } } if (isset($xmlStore->Query->EqualTo)) { if (isset($xmlStore->Query->EqualTo->Value)) { $this->_store['query']['equalTo']['value'] = (string) $xmlStore->Query->EqualTo->Value; } $doclib = $xmlStore->Query->EqualTo->children('uri:DocumentLibrary'); if (isset($doclib->LinkId)) { $this->_store['query']['equalTo']['linkId'] = (string) $doclib->LinkId; } } } // Process options if (isset($xmlStore->Options)) { // optional parameters if (isset($xmlStore->Options->DeepTraversal)) { $this->_store['options']['deepTraversal'] = true; } if (isset($xmlStore->Options->RebuildResults)) { $this->_store['options']['rebuildResults'] = true; } if (isset($xmlStore->Options->UserName)) { $this->_store['options']['userName'] = (string) $xmlStore->Options->UserName; } if (isset($xmlStore->Options->Password)) { $this->_store['options']['password'] = (string) $xmlStore->Options->Password; } if (isset($xmlStore->Options->Picture)) { if (isset($xmlStore->Options->Picture->MaxSize)) { $this->_store['options']['picture']['maxSize'] = (int) $xmlStore->Options->Picture->MaxSize; } if (isset($xmlStore->Options->Picture->MaxPictures)) { $this->_store['options']['picture']['maxPictures'] = (int) $xmlStore->Options->Picture->MaxPictures; } } if (!empty($xmlStore->Options->Range)) { $this->_store['options']['range'] = (string) $xmlStore->Options->Range; } else { switch ($this->_store['name']) { case 'DocumentLibrary': case 'Document Library': //? $this->_store['options']['range'] = '0-999'; break; case 'Mailbox': case 'GAL': default: $this->_store['options']['range'] = '0-99'; break; } } $this->_store['options']['range'] = explode('-', $this->_store['options']['range']); if (isset($xmlStore->Options->MIMESupport)) { $this->_store['options']['mimeSupport'] = (int) $xmlStore->Options->MIMESupport; } /* if (isset($xmlStore->Options->MIMETruncation)) { $this->_store['options']['mimeTruncation'] = (int)$xmlStore->Options->MIMETruncation; } */ // try to fetch element from AirSyncBase:BodyPreference $airSyncBase = $xmlStore->Options->children('uri:AirSyncBase'); if (isset($airSyncBase->BodyPreference)) { foreach ($airSyncBase->BodyPreference as $bodyPreference) { $type = (int) $bodyPreference->Type; $this->_store['options']['bodyPreferences'][$type] = array( 'type' => $type ); // optional if (isset($bodyPreference->TruncationSize)) { $this->_store['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize; } } } if (isset($airSyncBase->BodyPartPreference)) { // process BodyPartPreference elements } } } public function &__get($name) { if (array_key_exists($name, $this->_store)) { return $this->_store[$name]; } //echo $name . PHP_EOL; return null; } public function __set($name, $value) { $this->_store[$name] = $value; } public function __isset($name) { return isset($this->_store[$name]); } public function __unset($name) { unset($this->_store[$name]); } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/StoreResponse.php b/lib/ext/Syncroton/Model/StoreResponse.php index bd8855e..eeb14eb 100644 --- a/lib/ext/Syncroton/Model/StoreResponse.php +++ b/lib/ext/Syncroton/Model/StoreResponse.php @@ -1,91 +1,93 @@ + * @copyright Copyright (c) 2012-2014 Kolab Systems AG + * @author Aleksander Machniak */ /** * Class to handle ActiveSync Search/Response/Store element * * @package Syncroton * @subpackage Model - * @property string status - * @property array result - * @property array range - * @property int total + * + * @property int $status + * @property array $result + * @property array $range + * @property int $total */ class Syncroton_Model_StoreResponse extends Syncroton_Model_AXMLEntry { /** * status constants */ const STATUS_SUCCESS = 1; const STATUS_INVALIDREQUEST = 2; const STATUS_SERVERERROR = 3; const STATUS_BADLINK = 4; const STATUS_ACCESSDENIED = 5; const STATUS_NOTFOUND = 6; const STATUS_CONNECTIONFAILED = 7; const STATUS_TOOCOMPLEX = 8; const STATUS_TIMEDOUT = 10; const STATUS_FOLDERSYNCREQUIRED = 11; const STATUS_ENDOFRANGE = 12; const STATUS_ACCESSBLOCKED = 13; const STATUS_CREDENTIALSREQUIRED = 14; protected $_xmlBaseElement = 'Store'; protected $_properties = array( 'Search' => array( 'status' => array('type' => 'number'), 'result' => array('type' => 'container', 'multiple' => true), 'range' => array('type' => 'string'), 'total' => array('type' => 'number'), ) ); /** * (non-PHPdoc) * @see Syncroton_Model_AXMLEntry::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 === '') { continue; } list ($nameSpace, $elementProperties) = $this->_getElementProperties($elementName); $nameSpace = 'uri:' . $nameSpace; switch ($elementName) { case 'result': foreach ($value as $result) { $element = $_domParrent->ownerDocument->createElementNS($nameSpace, 'Result'); $result->appendXML($element, $device); $_domParrent->appendChild($element); } break; case 'range': if (is_array($value) && count($value) == 2) { $value = implode('-', $value); } default: $element = $_domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName)); $element->appendChild($_domParrent->ownerDocument->createTextNode($value)); $_domParrent->appendChild($element); } } } } diff --git a/lib/ext/Syncroton/Model/SyncCollection.php b/lib/ext/Syncroton/Model/SyncCollection.php index 1eaaa07..0cbd0fb 100644 --- a/lib/ext/Syncroton/Model/SyncCollection.php +++ b/lib/ext/Syncroton/Model/SyncCollection.php @@ -1,319 +1,323 @@ */ /** * class to handle ActiveSync Sync collection * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property Syncroton_Model_IFolder $folder + * @property bool $getChanges + * @property array $options + * @property int $syncKey + * @property ?Syncroton_Model_ISyncState $syncState + * @property array $toBeFetched + * @property int $windowSize */ - class Syncroton_Model_SyncCollection extends Syncroton_Model_AXMLEntry { protected $_elements = array( 'syncState' => null, 'folder' => null ); protected $_xmlCollection; protected $_xmlBaseElement = 'Collection'; public function __construct($properties = null) { if ($properties instanceof SimpleXMLElement) { $this->setFromSimpleXMLElement($properties); } elseif (is_array($properties)) { $this->setFromArray($properties); } if (!isset($this->_elements['options'])) { $this->_elements['options'] = array(); } if (!isset($this->_elements['options']['filterType'])) { $this->_elements['options']['filterType'] = Syncroton_Command_Sync::FILTER_NOTHING; } if (!isset($this->_elements['options']['mimeSupport'])) { $this->_elements['options']['mimeSupport'] = Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME; } if (!isset($this->_elements['options']['mimeTruncation'])) { $this->_elements['options']['mimeTruncation'] = Syncroton_Command_Sync::TRUNCATE_NOTHING; } if (!isset($this->_elements['options']['bodyPreferences'])) { $this->_elements['options']['bodyPreferences'] = array(); } } /** * return XML element which holds all client Add commands * * @return SimpleXMLElement */ public function getClientAdds() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { throw new InvalidArgumentException('no collection xml element set'); } return $this->_xmlCollection->Commands->Add; } /** * return XML element which holds all client Change commands * * @return SimpleXMLElement */ public function getClientChanges() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { throw new InvalidArgumentException('no collection xml element set'); } return $this->_xmlCollection->Commands->Change; } /** * return XML element which holds all client Delete commands * * @return SimpleXMLElement */ public function getClientDeletes() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { throw new InvalidArgumentException('no collection xml element set'); } return $this->_xmlCollection->Commands->Delete; } /** * return XML element which holds all client Fetch commands * * @return SimpleXMLElement */ public function getClientFetches() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { throw new InvalidArgumentException('no collection xml element set'); } return $this->_xmlCollection->Commands->Fetch; } /** * check if client sent a Add command * * @throws InvalidArgumentException * @return bool */ public function hasClientAdds() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { return false; } return isset($this->_xmlCollection->Commands->Add); } /** * check if client sent a Change command * * @throws InvalidArgumentException * @return bool */ public function hasClientChanges() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { return false; } return isset($this->_xmlCollection->Commands->Change); } /** * check if client sent a Delete command * * @throws InvalidArgumentException * @return bool */ public function hasClientDeletes() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { return false; } return isset($this->_xmlCollection->Commands->Delete); } /** * check if client sent a Fetch command * * @throws InvalidArgumentException * @return bool */ public function hasClientFetches() { if (! $this->_xmlCollection instanceof SimpleXMLElement) { return false; } return isset($this->_xmlCollection->Commands->Fetch); } /** * this functions does not only set from SimpleXMLElement but also does merge from SimpleXMLElement * to support partial sync requests * * @param SimpleXMLElement $properties * @throws InvalidArgumentException */ public function setFromSimpleXMLElement(SimpleXMLElement $properties) { if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) { throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName()); } $this->_xmlCollection = $properties; if (isset($properties->CollectionId)) { $this->_elements['collectionId'] = (string)$properties->CollectionId; } if (isset($properties->SyncKey)) { $this->_elements['syncKey'] = (int)$properties->SyncKey; } if (isset($properties->Class)) { $this->_elements['class'] = (string)$properties->Class; } elseif (!array_key_exists('class', $this->_elements)) { $this->_elements['class'] = null; } if (isset($properties->WindowSize)) { $this->_elements['windowSize'] = (string)$properties->WindowSize; } elseif (!array_key_exists('windowSize', $this->_elements)) { $this->_elements['windowSize'] = 100; } if (isset($properties->DeletesAsMoves)) { if ((string)$properties->DeletesAsMoves === '0') { $this->_elements['deletesAsMoves'] = false; } else { $this->_elements['deletesAsMoves'] = true; } } elseif (!array_key_exists('deletesAsMoves', $this->_elements)) { $this->_elements['deletesAsMoves'] = true; } if (isset($properties->ConversationMode)) { if ((string)$properties->ConversationMode === '0') { $this->_elements['conversationMode'] = false; } else { $this->_elements['conversationMode'] = true; } } elseif (!array_key_exists('conversationMode', $this->_elements)) { $this->_elements['conversationMode'] = true; } if (isset($properties->GetChanges)) { if ((string)$properties->GetChanges === '0') { $this->_elements['getChanges'] = false; } else { $this->_elements['getChanges'] = true; } } elseif (!array_key_exists('getChanges', $this->_elements)) { $this->_elements['getChanges'] = true; } if (isset($properties->Supported)) { // @todo collect supported elements } // process options if (isset($properties->Options)) { $this->_elements['options'] = array(); // optional parameters if (isset($properties->Options->FilterType)) { $this->_elements['options']['filterType'] = (int)$properties->Options->FilterType; } if (isset($properties->Options->MIMESupport)) { $this->_elements['options']['mimeSupport'] = (int)$properties->Options->MIMESupport; } if (isset($properties->Options->MIMETruncation)) { $this->_elements['options']['mimeTruncation'] = (int)$properties->Options->MIMETruncation; } if (isset($properties->Options->Class)) { $this->_elements['options']['class'] = (string)$properties->Options->Class; } // try to fetch element from AirSyncBase:BodyPreference $airSyncBase = $properties->Options->children('uri:AirSyncBase'); if (isset($airSyncBase->BodyPreference)) { foreach ($airSyncBase->BodyPreference as $bodyPreference) { $type = (int) $bodyPreference->Type; $this->_elements['options']['bodyPreferences'][$type] = array( 'type' => $type ); // optional if (isset($bodyPreference->TruncationSize)) { $this->_elements['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize; } // optional if (isset($bodyPreference->Preview)) { $this->_elements['options']['bodyPreferences'][$type]['preview'] = (int) $bodyPreference->Preview; } } } if (isset($airSyncBase->BodyPartPreference)) { // process BodyPartPreference elements } } } public function toArray() { $result = array(); foreach (array('syncKey', 'collectionId', 'deletesAsMoves', 'conversationMode', 'getChanges', 'windowSize', 'class', 'options') as $key) { if (isset($this->$key)) { $result[$key] = $this->$key; } } return $result; } public function &__get($name) { if (array_key_exists($name, $this->_elements)) { return $this->_elements[$name]; } echo $name . PHP_EOL; return null; } public function __set($name, $value) { $this->_elements[$name] = $value; } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/Task.php b/lib/ext/Syncroton/Model/Task.php index b70f569..da2a93b 100644 --- a/lib/ext/Syncroton/Model/Task.php +++ b/lib/ext/Syncroton/Model/Task.php @@ -1,48 +1,50 @@ */ /** * class to handle ActiveSync task * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property bool $getChanges + * @property string $syncKey + * @property DateTime $utcDueDate + * @property DateTime $utcStartDate + * @property int $windowSize */ class Syncroton_Model_Task extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'ApplicationData'; protected $_properties = array( 'AirSyncBase' => array( 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody') ), 'Tasks' => array( 'categories' => array('type' => 'container', 'childElement' => 'category'), 'complete' => array('type' => 'number'), 'dateCompleted' => array('type' => 'datetime'), 'dueDate' => array('type' => 'datetime'), 'importance' => array('type' => 'number'), 'recurrence' => array('type' => 'container'), 'reminderSet' => array('type' => 'number'), 'reminderTime' => array('type' => 'datetime'), 'sensitivity' => array('type' => 'number'), 'startDate' => array('type' => 'datetime'), 'subject' => array('type' => 'string'), 'utcDueDate' => array('type' => 'datetime'), 'utcStartDate' => array('type' => 'datetime'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Model/TaskRecurrence.php b/lib/ext/Syncroton/Model/TaskRecurrence.php index 0361b72..d8c0713 100644 --- a/lib/ext/Syncroton/Model/TaskRecurrence.php +++ b/lib/ext/Syncroton/Model/TaskRecurrence.php @@ -1,67 +1,67 @@ */ /** * class to handle ActiveSync event * * @package Syncroton * @subpackage Model - * @property string class - * @property string collectionId - * @property bool deletesAsMoves - * @property bool getChanges - * @property string syncKey - * @property int windowSize + * @property string $class + * @property string $collectionId + * @property bool $deletesAsMoves + * @property bool $getChanges + * @property string $syncKey + * @property int $windowSize */ class Syncroton_Model_TaskRecurrence extends Syncroton_Model_AXMLEntry { protected $_xmlBaseElement = 'Recurrence'; /** * recur types */ const TYPE_DAILY = 0; // Recurs daily. const TYPE_WEEKLY = 1; // Recurs weekly const TYPE_MONTHLY = 2; // Recurs monthly const TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day const TYPE_YEARLY = 5; // Recurs yearly const TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day /** * day of week constants */ const RECUR_DOW_SUNDAY = 1; const RECUR_DOW_MONDAY = 2; const RECUR_DOW_TUESDAY = 4; const RECUR_DOW_WEDNESDAY = 8; const RECUR_DOW_THURSDAY = 16; const RECUR_DOW_FRIDAY = 32; const RECUR_DOW_SATURDAY = 64; protected $_properties = array( 'Tasks' => array( 'calendarType' => array('type' => 'number'), 'dayOfMonth' => array('type' => 'number'), 'dayOfWeek' => array('type' => 'number'), 'deadOccur' => array('type' => 'number'), 'firstDayOfWeek' => array('type' => 'number'), 'interval' => array('type' => 'number'), 'isLeapMonth' => array('type' => 'number'), 'monthOfYear' => array('type' => 'number'), 'occurrences' => array('type' => 'number'), 'regenerate' => array('type' => 'number'), 'start' => array('type' => 'datetime'), 'type' => array('type' => 'number'), 'until' => array('type' => 'datetime'), 'weekOfMonth' => array('type' => 'number'), ) ); } \ No newline at end of file diff --git a/lib/ext/Syncroton/Registry.php b/lib/ext/Syncroton/Registry.php index 0ad94f1..3f2641a 100644 --- a/lib/ext/Syncroton/Registry.php +++ b/lib/ext/Syncroton/Registry.php @@ -1,520 +1,521 @@ 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); } /** * Return maximum number of collections in Sync/Ping request * * @return int */ public static function getMaxCollections() { return self::get(self::MAX_COLLECTIONS); } /** * 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 + * @return Syncroton_Backend_Policy */ 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); } /** * Returns sleep callback function * * This function is used in long running requests like ping & sync to * close connections to external sources (e.g. database) before we * call sleep() to wait some time for next iteration. * Callback should throw exceptions on errors. * * @return callable */ public static function getSleepCallback() { if (!self::isRegistered(self::SLEEP_CALLBACK)) { self::set(self::SLEEP_CALLBACK, function() {}); } return self::get(self::SLEEP_CALLBACK); } /** * Returns wakeup callback function * * This function is used in long running requests like ping & sync to * re-connect to external sources (e.g. database) closed by sleep callback. * Callback should throw exceptions on errors. * * @return callable */ public static function getWakeupCallback() { if (!self::isRegistered(self::WAKEUP_CALLBACK)) { self::set(self::WAKEUP_CALLBACK, function() {}); } return self::get(self::WAKEUP_CALLBACK); } /** * 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); } } diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php index cdb84b1..24f0ff6 100644 --- a/lib/ext/Syncroton/Server.php +++ b/lib/ext/Syncroton/Server.php @@ -1,461 +1,463 @@ */ /** * 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->notice(__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) { $this->_logDomDocument($requestBody, 'request', __METHOD__, __LINE__); } } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unexpected end of file."); $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); $response = $command->handle(); if (!$response) { $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->err(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e)); if ($this->_logger instanceof Zend_Log) $this->_logger->err(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage()); if ($this->_logger instanceof Zend_Log) $this->_logger->err(__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($response, '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); } header("HTTP/1.1 500 Internal server error"); return; } - if ($requestParameters['acceptMultipart'] == true) { + if ($requestParameters['acceptMultipart'] == true && isset($command)) { $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 DOMDocument $dom * @param string $action * @param string $method * @param int $line */ protected function _logDomDocument(DOMDocument $dom, $action, $method, $line) { if (method_exists($this->_logger, 'hasDebug') && !$this->_logger->hasDebug()) { return; } $tempStream = tmpfile(); $meta_data = stream_get_meta_data($tempStream); $filename = $meta_data["uri"]; $dom->formatOutput = true; $dom->save($filename); $dom->formatOutput = false; rewind($tempStream); $loops = 0; while (!feof($tempStream)) { $this->_logger->debug("{$method}::{$line} xml {$action} ({$loops}):\n" . fread($tempStream, 1048576)); $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']; + $deviceId = null; // unpack deviceId $length = ord(fread($stream, 1)); if ($length > 0) { $toUnpack = fread($stream, $length); $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack); $deviceId = $unpacked['string']; } // unpack policyKey $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('Vstring', fread($stream, $length)); $policyKey = $unpacked['string']; } // unpack device type $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $deviceType = $unpacked['string']; } while (! feof($stream)) { $tag = ord(fread($stream, 1)); $length = ord(fread($stream, 1)); // If the stream is at the end we'll get a 0-length if (!$length) { continue; } switch ($tag) { case self::PARAMETER_ATTACHMENTNAME: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $attachmentName = $unpacked['string']; break; case self::PARAMETER_COLLECTIONID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $collectionId = $unpacked['string']; break; case self::PARAMETER_ITEMID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $itemId = $unpacked['string']; break; case self::PARAMETER_OPTIONS: $options = ord(fread($stream, 1)); $saveInSent = !!($options & 0x01); $acceptMultiPart = !!($options & 0x02); break; default: if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters"); } } $result = array( 'protocolVersion' => $protocolVersion, 'command' => $command, 'deviceId' => $deviceId, 'deviceType' => isset($deviceType) ? $deviceType : null, 'policyKey' => isset($policyKey) ? $policyKey : null, 'saveInSent' => isset($saveInSent) ? $saveInSent : false, 'collectionId' => isset($collectionId) ? $collectionId : null, 'itemId' => isset($itemId) ? $itemId : null, 'attachmentName' => isset($attachmentName) ? $attachmentName : null, 'acceptMultipart' => isset($acceptMultiPart) ? $acceptMultiPart : false ); } else { $result = array( 'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'), 'command' => $request->getQuery('Cmd'), 'deviceId' => $request->getQuery('DeviceId'), 'deviceType' => $request->getQuery('DeviceType'), 'policyKey' => $request->getServer('HTTP_X_MS_POLICYKEY'), 'saveInSent' => $request->getQuery('SaveInSent') == 'T', 'collectionId' => $request->getQuery('CollectionId'), 'itemId' => $request->getQuery('ItemId'), 'attachmentName' => $request->getQuery('AttachmentName'), 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T' ); } $result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']); $result['contentType'] = $request->getServer('CONTENT_TYPE'); return $result; } /** * get existing device of owner or create new device for owner * * @param string $ownerId * @param array $requestParameters * - * @return Syncroton_Model_Device + * @return Syncroton_Model_IDevice */ protected function _getUserDevice($ownerId, $requestParameters) { try { $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']); $device->useragent = $requestParameters['userAgent']; $device->acsversion = $requestParameters['protocolVersion']; $device->devicetype = $requestParameters['deviceType']; 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 ))); } + /** @var Syncroton_Model_Device $device */ return $device; } public static function validateSession() { $validatorFunction = Syncroton_Registry::getSessionValidator(); return $validatorFunction(); } } diff --git a/lib/ext/Syncroton/TransactionManager.php b/lib/ext/Syncroton/TransactionManager.php index ae5e394..3f3690c 100644 --- a/lib/ext/Syncroton/TransactionManager.php +++ b/lib/ext/Syncroton/TransactionManager.php @@ -1,168 +1,168 @@ */ /** * Transaction Manger for Syncroton * * This is the central class, all transactions within Syncroton must be handled with. * For each supported transactionable (backend) this class start a real transaction on * the first startTransaction request. * * Transactions of all transactionable will be commited at once when all requested transactions * are being commited using this class. * * Transactions of all transactionable will be roll back when one rollBack is requested * using this class. * * @package Syncroton */ class Syncroton_TransactionManager implements Syncroton_TransactionManagerInterface { /** * @var array holds all transactionables with open transactions */ protected $_openTransactionables = array(); /** * @var array list of all open (not commited) transactions */ protected $_openTransactions = array(); /** - * @var Syncroton_TransactionManager + * @var ?Syncroton_TransactionManager */ private static $_instance = NULL; /** * @var Zend_Log */ protected $_logger; /** * don't clone. Use the singleton. */ private function __clone() { } - + /** * constructor */ private function __construct() { if (Syncroton_Registry::isRegistered('loggerBackend')) { $this->_logger = Syncroton_Registry::get('loggerBackend'); } } /** - * @return Tinebase_TransactionManager + * @return Syncroton_TransactionManager */ public static function getInstance() { if (self::$_instance === NULL) { self::$_instance = new Syncroton_TransactionManager; } return self::$_instance; } /** * starts a transaction * * @param mixed $_transactionable * @return string transactionId - * @throws Tinebase_Exception_UnexpectedValue + * @throws Exception */ public function startTransaction($_transactionable) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " startTransaction request"); if (! in_array($_transactionable, $this->_openTransactionables)) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " new transactionable. Starting transaction on this resource"); if ($_transactionable instanceof Zend_Db_Adapter_Abstract) { #Tinebase_Backend_Sql_Command::setAutocommit($_transactionable,false); $_transactionable->beginTransaction(); } else { $this->rollBack(); throw new Syncroton_Exception_UnexpectedValue('Unsupported transactionable!'); } array_push($this->_openTransactionables, $_transactionable); } $transactionId = sha1(mt_rand(). microtime()); array_push($this->_openTransactions, $transactionId); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " queued transaction with id $transactionId"); return $transactionId; } /** * commits a transaction * * @param string $_transactionId * @return void */ public function commitTransaction($_transactionId) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " commitTransaction request for $_transactionId"); $transactionIdx = array_search($_transactionId, $this->_openTransactions); if ($transactionIdx !== false) { unset($this->_openTransactions[$transactionIdx]); } $numOpenTransactions = count($this->_openTransactions); if ($numOpenTransactions === 0) { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " no more open transactions in queue commiting all transactionables"); foreach ($this->_openTransactionables as $transactionableIdx => $transactionable) { if ($transactionable instanceof Zend_Db_Adapter_Abstract) { $transactionable->commit(); #Tinebase_Backend_Sql_Command::setAutocommit($transactionable,true); } } $this->_openTransactionables = array(); $this->_openTransactions = array(); } else { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " commiting defered, as there are still $numOpenTransactions in the queue"); } } /** * perform rollBack on all transactionables with open transactions * * @return void */ public function rollBack() { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " rollBack request, rollBack all transactionables"); foreach ($this->_openTransactionables as $transactionable) { if ($transactionable instanceof Zend_Db_Adapter_Abstract) { $transactionable->rollBack(); #Tinebase_Backend_Sql_Command::setAutocommit($transactionable,true); } } $this->_openTransactionables = array(); $this->_openTransactions = array(); } } diff --git a/lib/ext/Syncroton/TransactionManagerInterface.php b/lib/ext/Syncroton/TransactionManagerInterface.php index c3d0840..7ea94d3 100644 --- a/lib/ext/Syncroton/TransactionManagerInterface.php +++ b/lib/ext/Syncroton/TransactionManagerInterface.php @@ -1,56 +1,56 @@ */ /** * Transaction Manger for Syncroton * * This is the central class, all transactions within Syncroton must be handled with. * For each supported transactionable (backend) this class start a real transaction on * the first startTransaction request. * * Transactions of all transactionable will be commited at once when all requested transactions * are being commited using this class. * * Transactions of all transactionable will be roll back when one rollBack is requested * using this class. * * @package Syncroton */ interface Syncroton_TransactionManagerInterface { /** - * @return Tinebase_TransactionManager + * @return mixed */ public static function getInstance(); /** * starts a transaction * * @param mixed $_transactionable * @return string transactionId - * @throws Tinebase_Exception_UnexpectedValue + * @throws Exception */ public function startTransaction($_transactionable); /** * commits a transaction * * @param string $_transactionId * @return void */ public function commitTransaction($_transactionId); /** * perform rollBack on all transactionables with open transactions * * @return void */ public function rollBack(); } diff --git a/lib/ext/Syncroton/Wbxml/Abstract.php b/lib/ext/Syncroton/Wbxml/Abstract.php index 1ffa640..13656f8 100644 --- a/lib/ext/Syncroton/Wbxml/Abstract.php +++ b/lib/ext/Syncroton/Wbxml/Abstract.php @@ -1,256 +1,256 @@ * @version $Id:Abstract.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ */ /** * class documentation * * @package Wbxml * @subpackage Wbxml */ abstract class Syncroton_Wbxml_Abstract { /** * stream containing the wbxml encoded data * * @var resource */ protected $_stream; /** * the wbxml version * - * @var string + * @var int */ protected $_version; /** * the Document Public Identifier * * @var string */ protected $_dpi; /** * the current active dtd * - * @var Syncroton_Wbxml_Dtd_Syncml_Abstract + * @var Syncroton_Wbxml_Dtd_ActiveSync */ protected $_dtd; /** * the charSet used in the wbxml file * * @var string */ protected $_charSet; /** * currently active code page * - * @var array + * @var Syncroton_Wbxml_Dtd_ActiveSync_Abstract */ protected $_codePage; /** * see section 5.5 * */ const DPI_WELLKNOWN = 'WELLKNOWN'; /** * see section 5.5 * */ const DPI_STRINGTABLE = 'STRINGTABLE'; const SWITCH_PAGE = 0x00; const END = 0x01; const ENTITY = 0x02; const STR_I = 0x03; const LITERAL = 0x04; const EXT_I_0 = 0x40; const EXT_I_1 = 0x41; const EXT_I_2 = 0x42; const PI = 0x43; const LITERAL_C = 0x44; const EXT_T_0 = 0x80; const EXT_T_1 = 0x81; const EXT_T_2 = 0x82; const STR_T = 0x83; const LITERAL_A = 0x84; const EXT_0 = 0xC0; const EXT_1 = 0xC1; const EXT_2 = 0xC2; const OPAQUE = 0xC3; const LITERAL_AC = 0xC4; /** * the real name for this DPI is "unknown" * But Microsoft is using them for their ActiveSync stuff * instead defining their own DPI like the sycnml creators did * */ const DPI_1 = '-//AIRSYNC//DTD AirSync//EN'; /** * return wellknown identifiers * * @param integer $_uInt * @todo add well known identifiers from section 7.2 * @return string */ public function getDPI($_uInt = 0) { if(!defined('Syncroton_Wbxml_Abstract::DPI_' . $_uInt)) { throw new Syncroton_Wbxml_Exception('unknown wellknown identifier: ' . $_uInt); } $dpi = constant('Syncroton_Wbxml_Abstract::DPI_' . $_uInt); return $dpi; } /** * return multibyte integer * * @return integer */ protected function _getMultibyteUInt() { $uInt = 0; do { $byte = $this->_getByte(); $uInt <<= 7; $uInt += ($byte & 127); } while (($byte & 128) != 0); return $uInt; } protected function _getByte() { $byte = fread($this->_stream, 1); if($byte === false) { throw new Syncroton_Wbxml_Exception("failed reading one byte"); } return ord($byte); } protected function _getOpaque($_length) { $string = ''; // it might happen that not complete data is read from stream. // loop until all data is read or EOF while ($_length) { $chunk = fread($this->_stream, $_length); if ($chunk === false) { throw new Syncroton_Wbxml_Exception("failed reading opaque data"); } if ($len = strlen($chunk)) { $string .= $chunk; $_length -= $len; } if (feof($this->_stream)) { break; } } return $string; } /** * get a 0 terminated string * * @return string */ protected function _getTerminatedString() { $string = ''; while (($byte = $this->_getByte()) != 0) { $string .= chr($byte); } return $string; } protected function _writeByte($_byte) { fwrite($this->_stream, chr($_byte)); } protected function _writeMultibyteUInt($_integer) { $multibyte = NULL; $remainder = $_integer; do { $byte = ($remainder & 127); $remainder >>= 7; if($multibyte === NULL) { $multibyte = chr($byte); } else { $multibyte = chr($byte | 128) . $multibyte; } } while ($remainder != 0); fwrite($this->_stream, $multibyte); } protected function _writeString($_string) { fwrite($this->_stream, $_string); } /** * write opaque string to stream * * @param string|resource $_string * @throws Syncroton_Wbxml_Exception */ protected function _writeOpaqueString($_string) { if (is_resource($_string)) { $stream = $_string; } else { $stream = fopen("php://temp", 'r+'); fwrite($stream, $_string); } $length = ftell($stream); rewind($stream); $this->_writeByte(Syncroton_Wbxml_Abstract::OPAQUE); $this->_writeMultibyteUInt($length); $writenBytes = stream_copy_to_stream($stream, $this->_stream); if($writenBytes !== $length) { throw new Syncroton_Wbxml_Exception('blow'); } fclose($stream); } protected function _writeTerminatedString($_string) { $this->_writeByte(Syncroton_Wbxml_Abstract::STR_I); fwrite($this->_stream, $_string); fwrite($this->_stream, chr(0)); } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Decoder.php b/lib/ext/Syncroton/Wbxml/Decoder.php index 8b845e4..99dfbf1 100644 --- a/lib/ext/Syncroton/Wbxml/Decoder.php +++ b/lib/ext/Syncroton/Wbxml/Decoder.php @@ -1,306 +1,304 @@ * @version $Id:Decoder.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ */ /** * class to convert WBXML to XML * * @package Wbxml * @subpackage Wbxml */ class Syncroton_Wbxml_Decoder extends Syncroton_Wbxml_Abstract { /** * type of Document Public Identifier * * @var string the type can be Syncroton_Wbxml_Abstract::DPI_STRINGTABLE or Syncroton_Wbxml_Abstract::DPI_WELLKNOWN */ protected $_dpiType; /** * the string table * * @var array */ protected $_stringTable = array(); /** * the xml document * * @var DOMDocument */ protected $_dom; /** * the main name space / aka the namespace of first tag * * @var string */ protected $_mainNameSpace; /** * the constructor will try to read all data until the first tag * * @param resource $_stream */ public function __construct($_stream, $_dpi = NULL) { if(!is_resource($_stream) || get_resource_type($_stream) != 'stream') { throw new Syncroton_Wbxml_Exception('$_stream must be a stream'); } if($_dpi !== NULL) { $this->_dpi = $_dpi; } $this->_stream = $_stream; $this->_version = $this->_getByte(); if(feof($this->_stream)) { throw new Syncroton_Wbxml_Exception_UnexpectedEndOfFile(); } $this->_getDPI(); $this->_getCharset(); $this->_getStringTable(); // resolve DPI as we have read the stringtable now // this->_dpi contains the string table index if($this->_dpiType === Syncroton_Wbxml_Abstract::DPI_STRINGTABLE) { $this->_dpi = $this->_stringTable[$this->_dpi]; } #$this->_dtd = Syncroton_Wbxml_Dtd_Factory::factory($this->_dpi); $this->_dtd = Syncroton_Wbxml_Dtd_Factory::factory(Syncroton_Wbxml_Dtd_Factory::ACTIVESYNC); } /** * return the Document Public Identifier * * @param integer $_uInt unused param, needed to satisfy abstract class method signature * @return string */ public function getDPI($_uInt = 0) { return $this->_dpi; } /** * return the wbxml version * - * @return string + * @return int */ public function getVersion() { return $this->_version; } /** * decodes the tags * * @return DOMDocument the decoded xml */ public function decode() { $openTags = NULL; $node = NULL; $this->_codePage = $this->_dtd->getCurrentCodePage(); while (!feof($this->_stream)) { $byte = $this->_getByte(); switch($byte) { case Syncroton_Wbxml_Abstract::END: $node = $node->parentNode; $openTags--; break; case Syncroton_Wbxml_Abstract::OPAQUE: $length = $this->_getMultibyteUInt(); if($length > 0) { $opaque = $this->_getOpaque($length); try { // let see if we can decode it. maybe the opaque data is wbxml encoded content $opaqueDataStream = fopen("php://temp", 'r+'); fputs($opaqueDataStream, $opaque); rewind($opaqueDataStream); $opaqueContentDecoder = new Syncroton_Wbxml_Decoder($opaqueDataStream); $dom = $opaqueContentDecoder->decode(); fclose($opaqueDataStream); foreach($dom->childNodes as $newNode) { if($newNode instanceof DOMElement) { $newNode = $this->_dom->importNode($newNode, true); $node->appendChild($newNode); } } } catch (Exception $e) { // if not, just treat it as a string $node->appendChild($this->_dom->createTextNode($opaque)); } } break; case Syncroton_Wbxml_Abstract::STR_I: $string = $this->_getTerminatedString(); $node->appendChild($this->_dom->createTextNode($string)); break; case Syncroton_Wbxml_Abstract::SWITCH_PAGE: $page = $this->_getByte(); $this->_codePage = $this->_dtd->switchCodePage($page); #echo "switched to codepage $page\n"; break; default: $tagHasAttributes = (($byte & 0x80) != 0); $tagHasContent = (($byte & 0x40) != 0); // get rid of bit 7+8 $tagHexCode = $byte & 0x3F; try { $tag = $this->_codePage->getTag($tagHexCode); } catch (Syncroton_Wbxml_Exception $swe) { // tag can not be converted to ASCII name $tag = sprintf('unknown tag 0x%x', $tagHexCode); } $nameSpace = $this->_codePage->getNameSpace(); $codePageName = $this->_codePage->getCodePageName(); #echo "Tag: $nameSpace:$tag\n"; if ($node === NULL) { // create the domdocument $node = $this->_createDomDocument($nameSpace, $tag); $newNode = $node->documentElement; } else { if (!$this->_dom->isDefaultNamespace($nameSpace)) { $this->_dom->documentElement->setAttribute('xmlns:' . $codePageName, $nameSpace); } $newNode = $node->appendChild($this->_dom->createElementNS('uri:' . $codePageName, $tag)); } if ($tagHasAttributes) { $attributes = $this->_getAttributes(); } if ($tagHasContent == true) { $node = $newNode; $openTags++; } break; } } return $this->_dom; } /** * creates the root of the xml document * * @return DOMDocument */ protected function _createDomDocument($_nameSpace, $_tag) { $this->_dom = $this->_dtd->getDomDocument($_nameSpace, $_tag); return $this->_dom; } /** * read the attributes of the current tag * * @todo implement logic */ protected function _getAttributes() { die("fetching attributes not yet implemented!\n"); } /** * get document public identifier * * the identifier can be all welknown identifier (see section 7.2) or a string from the stringtable */ protected function _getDPI() { $uInt = $this->_getMultibyteUInt(); if($uInt == 0) { // get identifier from stringtable $this->_dpiType = Syncroton_Wbxml_Abstract::DPI_STRINGTABLE; // string table identifier, can be resolved only after reading string table $this->_dpi = $this->_getByte(); } else { // wellknown identifier $this->_dpiType = Syncroton_Wbxml_Abstract::DPI_WELLKNOWN; $this->_dpi = Syncroton_Wbxml_Abstract::getDPI($uInt); } } /** * see http://www.iana.org/assignments/character-sets (MIBenum) * 106: UTF-8 - * */ protected function _getCharset() { $uInt = $this->_getMultibyteUInt(); switch($uInt) { case 106: $this->_charSet = 'UTF-8'; break; - + default: throw new Syncroton_Wbxml_Exception('unsuported charSet: ' . $uInt); - break; } } /** * get string table and store strings indexed by start * * @todo validate spliting at 0 value */ protected function _getStringTable() { $length = $this->_getMultibyteUInt(); if($length > 0) { $rawStringTable = $this->_getOpaque($length); $index = NULL; $string = NULL; for($i = 0; $i < strlen($rawStringTable); $i++) { if($index === NULL) { $index = $i; } if(ord($rawStringTable[$i]) != 0) { $string .= $rawStringTable[$i]; } // either the string has ended or we reached a \0 if($i+1 == strlen($rawStringTable) || ord($rawStringTable[$i]) == 0){ $this->_stringTable[$index] = $string; $index = NULL; $string = NULL; } } } } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/Abstract.php b/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/Abstract.php index 0c679c6..60734ef 100644 --- a/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/Abstract.php +++ b/lib/ext/Syncroton/Wbxml/Dtd/ActiveSync/Abstract.php @@ -1,114 +1,114 @@ * @version $Id:AirSync.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ */ /** * class documentation * * @package Wbxml * @subpackage ActiveSync */ abstract class Syncroton_Wbxml_Dtd_ActiveSync_Abstract { /** * codepage number * - * @var integer + * @var ?integer */ protected $_codePageNumber = NULL; /** * codepage name * - * @var string + * @var ?string */ protected $_codePageName = NULL; /** * document page identifier * not needed for ActiveSync * - * @var integer + * @var ?integer */ protected $_dpi = NULL; /** * mapping of tags to id's * * @var array */ protected $_tags = array(); /** * return document page identifier * is always NULL for activesync * - * @return unknown + * @return int|null */ public function getDPI() { return $this->_dpi; } /** * get codepage name * * @return string */ public function getCodePageName() { return $this->_codePageName; } /** * get namespace identifier * * @return string */ public function getNameSpace() { return 'uri:' . $this->_codePageName; } /** * get tag identifier * * @param string $_tag the tag name * @return integer */ public function getIdentity($_tag) { if(!isset($this->_tags[$_tag])) { //var_dump($this->_tags); throw new Syncroton_Wbxml_Exception("tag $_tag not found"); } return $this->_tags[$_tag]; } /** * return tag by given identity * - * @param unknown_type $_identity - * @return unknown + * @param int $_identity + * @return mixed */ public function getTag($_identity) { $tag = array_search($_identity, $this->_tags); if($tag === false) { throw new Syncroton_Wbxml_Exception("identity $_identity not found"); } return $tag; } } \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Factory.php b/lib/ext/Syncroton/Wbxml/Dtd/Factory.php index 2d4c815..b517316 100755 --- a/lib/ext/Syncroton/Wbxml/Dtd/Factory.php +++ b/lib/ext/Syncroton/Wbxml/Dtd/Factory.php @@ -1,49 +1,43 @@ * @version $Id:Factory.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ */ /** * class documentation * * @package Wbxml * @subpackage Wbxml */ - class Syncroton_Wbxml_Dtd_Factory { - const ACTIVESYNC='AirSync'; - - const SYNCML='SyncML'; - + const ACTIVESYNC = 'AirSync'; + const SYNCML = 'SyncML'; + /** * factory function to return a selected contacts backend class * * @param string $type - * @return Addressbook_Backend_Interface + * @return Syncroton_Wbxml_Dtd_ActiveSync */ - static public function factory ($_type) + static public function factory($type) { - switch ($_type) { + switch ($type) { case self::ACTIVESYNC: $instance = new Syncroton_Wbxml_Dtd_ActiveSync(); break; - - case self::SYNCML: - $instance = new Syncroton_Wbxml_Dtd_Syncml(); - break; - + default: - throw new Syncroton_Wbxml_Exception('unsupported DTD: ' . $_type); - break; + throw new Syncroton_Wbxml_Exception('unsupported DTD: ' . $type); } + return $instance; } -} +} diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Syncml.php b/lib/ext/Syncroton/Wbxml/Dtd/Syncml.php deleted file mode 100755 index 5c30e14..0000000 --- a/lib/ext/Syncroton/Wbxml/Dtd/Syncml.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @version $Id:Factory.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ - */ - -/** - * class documentation - * - * @package Wbxml - * @subpackage Syncml - */ - -class Syncroton_Wbxml_Dtd_Syncml -{ - /** - * factory function to return a selected contacts backend class - * - * @param string $type - * @return Addressbook_Backend_Interface - */ - static public function factory ($_type) - { - switch ($_type) { - case 'syncml:syncml1.1': - case 'syncml:syncml1.2': - case 'syncml:metinf1.1': - case 'syncml:metinf1.2': - case 'syncml:devinf1.1': - case 'syncml:devinf1.2': - throw new Syncroton_Wbxml_Exception('unsupported DTD: ' . $_type); - break; - } - return $instance; - } -} diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Abstract.php b/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Abstract.php deleted file mode 100644 index c99a534..0000000 --- a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Abstract.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @version $Id:Abstract.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ - */ - -/** - * class documentation - * - * @package Wbxml - * @subpackage Syncml - */ - -class Syncroton_Wbxml_Dtd_Syncml_Abstract -{ - protected $_tags; - - protected $_identity; - - protected $_codePages; - - protected $_currentCodePage; - - public function __construct($_initialCodePage = 0x00) - { - $this->switchCodePage($_initialCodePage); - } - - /** - * switch codepage - * - * @param integer $_id id of the codePage - * @return array - */ - public function switchCodePage($_id) - { - if(!isset($this->_codePages[$_id])) { - throw new Syncroton_Wbxml_Dtd_Exception_CodePageNotFound('invalid codePage id: ' . $_id); - } - $this->_currentCodePage = $_id; - $this->_tags = $this->_codePages[$this->_currentCodePage]['tags']; - $this->_identity = array_flip($this->_tags); - - return $this->_codePages[$this->_currentCodePage]; - } - - /** - * get currently active codepage - * - * @return array - */ - public function getCurrentCodePage() - { - return $this->_codePages[$this->_currentCodePage]; - } - - public function getTag($_identity) - { - if(!isset($this->_identity[$_identity])) { - throw new Syncroton_Wbxml_Exception("identity $_identity not found"); - } - - return $this->_identity[$_identity]; - } - - public function getIdentity($_tag) - { - if(!isset($this->_tags[$_tag])) { - var_dump($this->_tags); - throw new Syncroton_Wbxml_Exception("tag $_tag not found"); - } - - return $this->_tags[$_tag]; - } - - /** - * switch codepage by urn - * - * @param string $_urn - * @return array - */ - public function switchCodePageByUrn($_urn) - { - $codePageNumber = NULL; - foreach($this->_codePages as $codePage) { - if($codePage['urn'] == $_urn) { - $codePageNumber = $codePage['codePageNumber']; - } - } - - if($codePageNumber === NULL) { - throw new Syncroton_Wbxml_Dtd_Exception_CodePageNotFound("codePage with URN $_urn not found"); - } - - return $this->switchCodePage($codePageNumber); - } -} \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/DevInfo11.php b/lib/ext/Syncroton/Wbxml/Dtd/Syncml/DevInfo11.php deleted file mode 100644 index 1a27ba3..0000000 --- a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/DevInfo11.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @version $Id:DevInfo11.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ - */ - -/** - * class documentation - * - * @package Wbxml - * @subpackage Syncml - */ - -class Syncroton_Wbxml_Dtd_Syncml_DevInfo11 extends Syncroton_Wbxml_Dtd_Syncml_Abstract -{ - protected $_codePages = array( - 0x00 => array( - 'codePageNumber'=> 0x00, - 'dtdname' => 'DevInf', - 'dpi' => '-//SYNCML//DTD DevInf 1.1//EN', - 'url' => 'http://www.syncml.org/docs/devinf_v11_20020215.dtd', - 'urn' => 'syncml:devinf1.1', - 'tags' => array( - "CTCap" => 0x05, - "CTType" => 0x06, - "DataStore" => 0x07, - "DataType" => 0x08, - "DevID" => 0x09, - "DevInf" => 0x0a, - "DevTyp" => 0x0b, - "DisplayName" => 0x0c, - "DSMem" => 0x0d, - "Ext" => 0x0e, - "FwV" => 0x0f, - "HwV" => 0x10, - "Man" => 0x11, - "MaxGUIDSize" => 0x12, - "MaxID" => 0x13, - "MaxMem" => 0x14, - "Mod" => 0x15, - "OEM" => 0x16, - "ParamName" => 0x17, - "PropName" => 0x18, - "Rx" => 0x19, - "Rx-Pref" => 0x1a, - "SharedMem" => 0x1b, - "Size" => 0x1c, - "SourceRef" => 0x1d, - "SwV" => 0x1e, - "SyncCap" => 0x1f, - "SyncType" => 0x20, - "Tx" => 0x21, - "Tx-Pref" => 0x22, - "ValEnum" => 0x23, - "VerCT" => 0x24, - "VerDTD" => 0x25, - "XNam" => 0x26, - "XVal" => 0x27, - "UTC" => 0x28, - "SupportNumberOfChanges"=> 0x29, - "SupportLargeObjs" => 0x2a - ) - ) - ); -} \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/DevInfo12.php b/lib/ext/Syncroton/Wbxml/Dtd/Syncml/DevInfo12.php deleted file mode 100644 index 88c3ae2..0000000 --- a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/DevInfo12.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @version $Id:DevInfo12.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ - */ - -/** - * class documentation - * - * @package Wbxml - * @subpackage Syncml - */ - -class Syncroton_Wbxml_Dtd_Syncml_DevInfo12 extends Syncroton_Wbxml_Dtd_Syncml_Abstract -{ - protected $_codePages = array( - 0x00 => array( - 'codePageNumber'=> 0x00, - 'dtdname' => 'DevInf', - 'dpi' => '-//OMA//DTD SYNCML-DEVINF 1.2//EN', - 'url' => 'http://www.openmobilealliance.org/tech/DTD/OMA-SyncML-Device_Information-DTD-1.2.dtd', - 'urn' => 'syncml:devinf1.2', - 'tags' => array( - "CTCap" => 0x05, - "CTType" => 0x06, - "DataStore" => 0x07, - "DataType" => 0x08, - "DevID" => 0x09, - "DevInf" => 0x0a, - "DevTyp" => 0x0b, - "DisplayName" => 0x0c, - "DSMem" => 0x0d, - "Ext" => 0x0e, - "FwV" => 0x0f, - "HwV" => 0x10, - "Man" => 0x11, - "MaxGUIDSize" => 0x12, - "MaxID" => 0x13, - "MaxMem" => 0x14, - "Mod" => 0x15, - "OEM" => 0x16, - "ParamName" => 0x17, - "PropName" => 0x18, - "Rx" => 0x19, - "Rx-Pref" => 0x1a, - "SharedMem" => 0x1b, - "Size" => 0x1c, - "SourceRef" => 0x1d, - "SwV" => 0x1e, - "SyncCap" => 0x1f, - "SyncType" => 0x20, - "Tx" => 0x21, - "Tx-Pref" => 0x22, - "ValEnum" => 0x23, - "VerCT" => 0x24, - "VerDTD" => 0x25, - "XNam" => 0x26, - "XVal" => 0x27, - "UTC" => 0x28, - "SupportNumberOfChanges"=> 0x29, - "SupportLargeObjs" => 0x2a, - "Property" => 0x2b, - "PropParam" => 0x2c, - "MaxOccur" => 0x2d, - "NoTruncate" => 0x2e, - "Filter-Rx" => 0x30, - "FilterCap" => 0x31, - "FilterKeyword" => 0x32, - "FieldLevel" => 0x33, - "SupportHierarchicalSync"=> 0x34 - ) - ) - ); -} \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Syncml11.php b/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Syncml11.php deleted file mode 100644 index f74a356..0000000 --- a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Syncml11.php +++ /dev/null @@ -1,107 +0,0 @@ - - * @version $Id:Syncml11.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ - */ - -/** - * class documentation - * - * @package Wbxml - * @subpackage Syncml - */ - -class Syncroton_Wbxml_Dtd_Syncml_Syncml11 extends Syncroton_Wbxml_Dtd_Syncml_Abstract -{ - protected $_codePages = array( - 0x00 => array( - 'codePageNumber'=> 0x00, - 'dtdname' => 'SyncML', - 'dpi' => '-//SYNCML//DTD SyncML 1.1//EN', - 'url' => "http://www.syncml.org/docs/syncml_represent_v11_20020213.dtd", - 'urn' => 'syncml:syncml1.1', - 'tags' => array( - 'Add' => 0x05, - 'Alert' => 0x06, - 'Archive' => 0x07, - 'Atomic' => 0x08, - 'Chal' => 0x09, - 'Cmd' => 0x0a, - 'CmdID' => 0x0b, - 'CmdRef' => 0x0c, - 'Copy' => 0x0d, - 'Cred' => 0x0e, - 'Data' => 0x0f, - 'Delete' => 0x10, - 'Exec' => 0x11, - 'Final' => 0x12, - 'Get' => 0x13, - 'Item' => 0x14, - 'Lang' => 0x15, - 'LocName' => 0x16, - 'LocURI' => 0x17, - 'Map' => 0x18, - 'MapItem' => 0x19, - 'Meta' => 0x1a, - 'MsgID' => 0x1b, - 'MsgRef' => 0x1c, - 'NoResp' => 0x1d, - 'NoResults' => 0x1e, - 'Put' => 0x1f, - 'Replace' => 0x20, - 'RespURI' => 0x21, - 'Results' => 0x22, - 'Search' => 0x23, - 'Sequence' => 0x24, - 'SessionID' => 0x25, - 'SftDel' => 0x26, - 'Source' => 0x27, - 'SourceRef' => 0x28, - 'Status' => 0x29, - 'Sync' => 0x2a, - 'SyncBody' => 0x2b, - 'SyncHdr' => 0x2c, - 'SyncML' => 0x2d, - 'Target' => 0x2e, - 'TargetRef' => 0x2f, - 'Reserved for future use.' => 0x30, - 'VerDTD' => 0x31, - 'VerProto' => 0x32, - 'NumberOfChanges' => 0x33, - 'MoreData' => 0x34 - ) - ), - 0x01 => array( - 'codePageNumber'=> 0x01, - 'dtdname' => 'MetInf', - 'dpi' => '-//SYNCML//DTD MetInf 1.1//EN', - 'url' => 'http://www.syncml.org/docs/syncml_metinf_v11_20020215.dtd ', - 'urn' => 'syncml:metinf1.1', - 'tags' => array( - 'Anchor' => 0x05, - 'EMI' => 0x06, - 'Format' => 0x07, - 'FreeID' => 0x08, - 'FreeMem' => 0x09, - 'Last' => 0x0a, - 'Mark' => 0x0b, - 'MaxMsgSize' => 0x0c, - 'Mem' => 0x0d, - 'MetInf' => 0x0e, - 'Next' => 0x0f, - 'NextNonce' => 0x10, - 'SharedMem' => 0x11, - 'Size' => 0x12, - 'Type' => 0x13, - 'Version' => 0x14, - 'MaxObjSize' => 0x15 - ) - ) - ); -} \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Syncml12.php b/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Syncml12.php deleted file mode 100644 index 22eeb69..0000000 --- a/lib/ext/Syncroton/Wbxml/Dtd/Syncml/Syncml12.php +++ /dev/null @@ -1,122 +0,0 @@ - - * @version $Id:Syncml12.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ - */ - -/** - * class documentation - * - * @package Wbxml - * @subpackage Syncml - */ - -class Syncroton_Wbxml_Dtd_Syncml_Syncml12 extends Syncroton_Wbxml_Dtd_Syncml_Abstract -{ - - /** - * section 8.2 - * - * @var array - */ - protected $_codePages = array( - 0x00 => array( - 'codePageNumber'=> 0x00, - 'dtdname' => 'SyncML', - 'dpi' => '-//SYNCML//DTD SyncML 1.2//EN', - 'url' => 'http://www.openmobilealliance.org/tech/DTD/OMA-TS-SyncML_RepPro_DTD-V1_2.dtd', - 'urn' => 'SYNCML:SYNCML1.2', - 'tags' => array( - 'Add' => 0x05, - 'Alert' => 0x06, - 'Archive' => 0x07, - 'Atomic' => 0x08, - 'Chal' => 0x09, - 'Cmd' => 0x0a, - 'CmdID' => 0x0b, - 'CmdRef' => 0x0c, - 'Copy' => 0x0d, - 'Cred' => 0x0e, - 'Data' => 0x0f, - 'Delete' => 0x10, - 'Exec' => 0x11, - 'Final' => 0x12, - 'Get' => 0x13, - 'Item' => 0x14, - 'Lang' => 0x15, - 'LocName' => 0x16, - 'LocURI' => 0x17, - 'Map' => 0x18, - 'MapItem' => 0x19, - 'Meta' => 0x1a, - 'MsgID' => 0x1b, - 'MsgRef' => 0x1c, - 'NoResp' => 0x1d, - 'NoResults' => 0x1e, - 'Put' => 0x1f, - 'Replace' => 0x20, - 'RespURI' => 0x21, - 'Results' => 0x22, - 'Search' => 0x23, - 'Sequence' => 0x24, - 'SessionID' => 0x25, - 'SftDel' => 0x26, - 'Source' => 0x27, - 'SourceRef' => 0x28, - 'Status' => 0x29, - 'Sync' => 0x2a, - 'SyncBody' => 0x2b, - 'SyncHdr' => 0x2c, - 'SyncML' => 0x2d, - 'Target' => 0x2e, - 'TargetRef' => 0x2f, - 'Reserved for future use.' => 0x30, - 'VerDTD' => 0x31, - 'VerProto' => 0x32, - 'NumberOfChanges' => 0x33, - 'MoreData' => 0x34, - 'Field' => 0x35, - 'Filter' => 0x36, - 'Record' => 0x37, - 'FilterType' => 0x38, - 'SourceParent' => 0x39, - 'TargetParent' => 0x3a, - 'Move' => 0x3b, - 'Correlator' => 0x3c - ) - ), - 0x01 => array( - 'codePageNumber'=> 0x01, - 'dtdname' => 'MetInf', - 'dpi' => '-//OMA//DTD SYNCML-METINF 1.2//EN', - 'url' => 'http://www.openmobilealliance.org/tech/DTD/OMA-TS-SyncML_MetaInfo_DTD-V1_2.dtd', - 'urn' => 'syncml:metinf1.2', - 'tags' => array( - 'Anchor' => 0x05, - 'EMI' => 0x06, - 'Format' => 0x07, - 'FreeID' => 0x08, - 'FreeMem' => 0x09, - 'Last' => 0x0a, - 'Mark' => 0x0b, - 'MaxMsgSize' => 0x0c, - 'Mem' => 0x0d, - 'MetInf' => 0x0e, - 'Next' => 0x0f, - 'NextNonce' => 0x10, - 'SharedMem' => 0x11, - 'Size' => 0x12, - 'Type' => 0x13, - 'Version' => 0x14, - 'MaxObjSize' => 0x15, - 'FieldLevel' => 0x16 - ) - ) - ); -} \ No newline at end of file diff --git a/lib/ext/Syncroton/Wbxml/Encoder.php b/lib/ext/Syncroton/Wbxml/Encoder.php index f38d4e4..ef2144c 100644 --- a/lib/ext/Syncroton/Wbxml/Encoder.php +++ b/lib/ext/Syncroton/Wbxml/Encoder.php @@ -1,246 +1,244 @@ * @version $Id:Encoder.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ */ /** * class to convert XML to WBXML * * @package Wbxml * @subpackage Wbxml */ class Syncroton_Wbxml_Encoder extends Syncroton_Wbxml_Abstract { /** * count level of tags * - * @var string + * @var int */ protected $_level = 0; /** * the constructor * * @param resource $_stream - * @param string $_charSet - * @param integer $_version + * @param string $_charSet + * @param int $_version */ public function __construct($_stream, $_charSet = 'UTF-8', $_version = 2) { $this->_stream = $_stream; $this->_charSet = $_charSet; $this->_version = $_version; } /** * initialize internal variables and write wbxml header to stream * - * @param string $_urn + * @param DOMDocument $_dom * @todo check if dpi > 0, instead checking the urn */ protected function _initialize($_dom) { $this->_dtd = Syncroton_Wbxml_Dtd_Factory::factory($_dom->doctype->name); $this->_codePage = $this->_dtd->getCurrentCodePage(); // the WBXML version $this->_writeByte($this->_version); if($this->_codePage->getDPI() === NULL) { // the document public identifier $this->_writeMultibyteUInt(1); } else { // the document public identifier // defined in string table $this->_writeMultibyteUInt(0); // the offset of the DPI in the string table $this->_writeByte(0); } // write the charSet $this->_writeCharSet($this->_charSet); if($this->_codePage->getDPI() === NULL) { // the length of the string table $this->_writeMultibyteUInt(0); } else { // the length of the string table $this->_writeMultibyteUInt(strlen($this->_codePage->getDPI())); // the dpi $this->_writeString($this->_codePage->getDPI()); } } /** * write charset to stream * * @param string $_charSet * @todo add charset lookup table. currently only utf-8 is supported */ protected function _writeCharSet($_charSet) { switch(strtoupper($_charSet)) { case 'UTF-8': $this->_writeMultibyteUInt(106); break; default: throw new Syncroton_Wbxml_Exception('unsuported charSet ' . strtoupper($_charSet)); - break; } - } /** * start encoding of xml to wbxml * - * @param string $_xml the xml string - * @return resource stream + * @param DOMDocument $_dom the DOM document */ public function encode(DOMDocument $_dom) { $_dom->formatOutput = false; $this->_initialize($_dom); $this->_traverseDom($_dom); } private function getAttributes($node) { $attributes = array(); if ($node->attributes) { for ($i = 0; $i < $node->attributes->length; ++$i) { $attributes[$node->attributes->item($i)->name] = $node->attributes->item($i)->value; } } return $attributes; } private function writeNode($node, $withContent = false, $data = null) { if($this->_codePage->getNameSpace() != $node->namespaceURI) { $this->_switchCodePage($node->namespaceURI); } $this->_writeTag($node->localName, $this->getAttributes($node), $withContent, $data); } protected function _traverseDom($_dom) { if ($_dom->childNodes->length == 0) { return false; } // print(str_pad("", $this->_level, " ") . "traversing {$_dom->nodeName}" . "\n"); $this->_level++; $prevNode = $_dom; $foundElementNode = false; foreach ($_dom->childNodes as $node) { if ($node->nodeType == XML_ELEMENT_NODE) { $foundElementNode = true; if ($prevNode && $this->_level > 1) { // print(str_pad("", $this->_level, " ") . "{$node->nodeName} creating parent {$prevNode->nodeName}" . "\n"); $this->writeNode($prevNode, true); $prevNode = null; } if (!$this->_traverseDom($node)) { // print(str_pad("", $this->_level, " ") . "{$node->nodeName} content {$node->nodeValue}" . "\n"); $data = $node->nodeValue; if (strlen($data) == 0) { $this->writeNode($node); } else { $this->writeNode($node, true, $data); $this->_writeByte(Syncroton_Wbxml_Abstract::END); // print("Closing tag after writing tag\n"); } } else { $this->_writeByte(Syncroton_Wbxml_Abstract::END); // print("Closing tag\n"); } } } $this->_level--; return $foundElementNode; } /** * strip uri: from nameSpace * - * @param unknown_type $_nameSpace - * @return unknown + * @param string $_nameSpace + * + * @return string */ protected function _stripNameSpace($_nameSpace) { return substr($_nameSpace, 4); } /** * writes tag with data to stream * * @param string $_tag * @param array $_attributes * @param bool $_hasContent * @param string $_data */ protected function _writeTag($_tag, $_attributes=NULL, $_hasContent=false, $_data=NULL) { if($_hasContent == false && $_data !== NULL) { throw new Syncroton_Wbxml_Exception('$_hasContent can not be false, when $_data !== NULL'); } // handle the tag $identity = $this->_codePage->getIdentity($_tag); if (is_array($_attributes) && isset($_attributes['uri:Syncroton;encoding'])) { $encoding = 'opaque'; unset($_attributes['uri:Syncroton;encoding']); } else { $encoding = 'termstring'; } if(!empty($_attributes)) { $identity |= 0x80; } if($_hasContent == true) { $identity |= 0x40; } $this->_writeByte($identity); // handle the data if($_data !== NULL) { if ($encoding == 'opaque') { $this->_writeOpaqueString(base64_decode($_data)); } else { $this->_writeTerminatedString($_data); } } } /** * switch code page * - * @param string $_urn + * @param string $_nameSpace */ protected function _switchCodePage($_nameSpace) { $codePageName = $this->_stripNameSpace($_nameSpace); if(!defined('Syncroton_Wbxml_Dtd_ActiveSync::CODEPAGE_'. strtoupper($codePageName))) { throw new Syncroton_Wbxml_Exception('codepage ' . $codePageName . ' not found'); } // switch to another codepage // no need to write the wbxml header again $codePageId = constant('Syncroton_Wbxml_Dtd_ActiveSync::CODEPAGE_'. strtoupper($codePageName)); $this->_codePage = $this->_dtd->switchCodePage($codePageId); $this->_writeByte(Syncroton_Wbxml_Abstract::SWITCH_PAGE); $this->_writeByte($codePageId); } } diff --git a/lib/kolab_sync_backend_common.php b/lib/kolab_sync_backend_common.php index 75a4793..9553fe8 100644 --- a/lib/kolab_sync_backend_common.php +++ b/lib/kolab_sync_backend_common.php @@ -1,282 +1,283 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Parent backend class for kolab backends */ class kolab_sync_backend_common implements Syncroton_Backend_IBackend { /** * Table name * * @var string */ protected $table_name; /** * Model interface name * * @var string */ protected $interface_name; /** * Backend interface name * * @var string */ protected $class_name; /** * SQL Database engine * * @var rcube_db */ protected $db; /** * Internal cache (in-memory) * * @var array */ protected $cache = []; /** * Constructor */ public function __construct() { $this->db = rcube::get_instance()->get_dbh(); if (empty($this->class_name)) { $this->class_name = str_replace('Model_I', 'Model_', $this->interface_name); } } /** * Creates new Syncroton object in database * * @param object $object Object * * @return object Object * @throws InvalidArgumentException|Syncroton_Exception_DeadlockDetected|Exception */ public function create($object) { if (! $object instanceof $this->interface_name) { throw new InvalidArgumentException('$object must be instance of ' . $this->interface_name); } $data = $this->object_to_array($object); $cols = []; $data['id'] = $object->id = sha1(mt_rand() . microtime()); foreach (array_keys($data) as $key) { $cols[] = $this->db->quote_identifier($key); } $result = $this->db->query( 'INSERT INTO `' . $this->table_name . '`' . ' (' . implode(', ', $cols) . ')' . ' VALUES(' . implode(', ', array_fill(0, count($cols), '?')) . ')', array_values($data) ); if ($err = $this->db->is_error($result)) { $err = "Failed to save instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { throw new Syncroton_Exception_DeadlockDetected($err); } else { throw new Exception($err); } } return $object; } /** * Returns Syncroton data object * * @param string $id * * @throws Syncroton_Exception_NotFound * @return object */ public function get($id) { $id = $id instanceof $this->interface_name ? $id->id : $id; if ($id) { $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE `id` = ?', [$id]); $data = $this->db->fetch_assoc($select); } if (empty($data)) { throw new Syncroton_Exception_NotFound('Object not found'); } return $this->get_object($data); } /** * Deletes Syncroton data object * - * @param string|object $id Object or identifier + * @param string|Syncroton_Model_IEntry $id Object or identifier * * @return bool True on success, False on failure * @throws Syncroton_Exception_DeadlockDetected|Exception */ public function delete($id) { + // @phpstan-ignore-next-line $id = $id instanceof $this->interface_name ? $id->id : $id; if (!$id) { return false; } $result = $this->db->query('DELETE FROM `' . $this->table_name . '` WHERE `id` = ?', [$id]); if ($err = $this->db->is_error($result)) { $err = "Failed to delete instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { throw new Syncroton_Exception_DeadlockDetected($err); } else { throw new Exception($err); } } return (bool) $this->db->affected_rows($result); } /** * Updates Syncroton data object * * @param object $object * * @return object Object * @throws InvalidArgumentException|Syncroton_Exception_DeadlockDetected|Exception */ public function update($object) { if (! $object instanceof $this->interface_name) { throw new InvalidArgumentException('$object must be instanace of ' . $this->interface_name); } $data = $this->object_to_array($object); $set = []; foreach (array_keys($data) as $key) { $set[] = $this->db->quote_identifier($key) . ' = ?'; } $result = $this->db->query('UPDATE `' . $this->table_name . '` SET ' . implode(', ', $set) . ' WHERE `id` = ' . $this->db->quote($object->id), array_values($data)); if ($err = $this->db->is_error($result)) { $err = "Failed to update instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { throw new Syncroton_Exception_DeadlockDetected($err); } else { throw new Exception($err); } } return $object; } /** * Returns list of user accounts * * @param Syncroton_Model_Device $device The current device * * @return array List of Syncroton_Model_Account objects */ public function userAccounts($device) { // this method is overwritten by kolab_sync_backend class return []; } /** * Convert array into model object */ protected function get_object($data) { 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')); } $data[$this->to_camelcase($key, false)] = $value; } return new $this->class_name($data); } /** * Converts model object into array */ protected function object_to_array($object) { $data = []; foreach ($object 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->from_camelcase($key)] = $value; } return $data; } /** * Convert property name from camel-case to lower-case-with-underscore */ protected function from_camelcase($string) { $string = lcfirst($string); return preg_replace_callback('/([A-Z])/', function ($string) { return '_' . strtolower($string[0]); }, $string); } /** * Convert property name from lower-case-with-underscore to camel-case */ protected function to_camelcase($string, $ucFirst = true) { if ($ucFirst) { $string = ucfirst($string); } return preg_replace_callback('/_([a-z])/', function ($string) { return strtoupper($string[1]); }, $string); } } diff --git a/lib/kolab_sync_backend_device.php b/lib/kolab_sync_backend_device.php index 9c4101c..bdba1ce 100644 --- a/lib/kolab_sync_backend_device.php +++ b/lib/kolab_sync_backend_device.php @@ -1,320 +1,320 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for device storage */ class kolab_sync_backend_device extends kolab_sync_backend_common implements Syncroton_Backend_IDevice { protected $table_name = 'syncroton_device'; protected $interface_name = 'Syncroton_Model_IDevice'; /** * Kolab Sync storage backend * * @var kolab_sync_storage */ protected $backend; /** * Constructor */ public function __construct() { parent::__construct(); $this->backend = kolab_sync::storage(); } /** * Create (register) a new device * * @param Syncroton_Model_IDevice $device Device object * * @return Syncroton_Model_IDevice Device object */ public function create($device) { $device = parent::create($device); // Create device entry in kolab backend $created = $this->backend->device_create([ 'ID' => $device->id, 'TYPE' => $device->devicetype, 'ALIAS' => $device->friendlyname, ], $device->deviceid); if (!$created) { throw new Syncroton_Exception_NotFound('Device creation failed'); } return $device; } /** * Delete a device * - * @param Syncroton_Model_IDevice $device Device object + * @param string|Syncroton_Model_IDevice $device Device object * * @return bool True on success, False on failure */ public function delete($device) { // Update IMAP annotation $this->backend->device_delete($device->deviceid); return parent::delete($device); } /** * Return device for a given user * * @param string $ownerid User identifier * @param string $deviceid Device identifier * * @throws Syncroton_Exception_NotFound * @return Syncroton_Model_Device Device object */ public function getUserDevice($ownerid, $deviceid) { $where[] = $this->db->quote_identifier('deviceid') . ' = ' . $this->db->quote($deviceid); $where[] = $this->db->quote_identifier('owner_id') . ' = ' . $this->db->quote($ownerid); $select = $this->db->query('SELECT * FROM ' . $this->table_name . ' WHERE ' . implode(' AND ', $where)); $device = $this->db->fetch_assoc($select); if (empty($device)) { throw new Syncroton_Exception_NotFound('Device not found'); } $device = $this->get_object($device); // Make sure device exists (could be deleted by the user) $dev = $this->backend->device_get($deviceid); if (empty($dev)) { // Remove the device (and related cached data) from database $this->delete($device); throw new Syncroton_Exception_NotFound('Device not found'); } return $device; } /** * Returns list of user accounts * * @param Syncroton_Model_Device $device The device * * @return array List of Syncroton_Model_Account objects */ public function userAccounts($device) { $engine = kolab_sync::get_instance(); $identities = $engine->user->list_identities(); $email = $engine->get_user_email(); $addresses = []; $displayname = null; // read email addresses and display name (default ident comes first) foreach ((array)$identities as $ident) { if ($ident['name'] && !isset($displayname)) { $displayname = $ident['name']; } $addresses[] = $ident['email']; } if (empty($displayname) && empty($email) && empty($addresses)) { return []; } $account = new Syncroton_Model_Account(); if ($email) { $addresses = array_diff($addresses, [$email]); } $account->userDisplayName = $displayname; $account->primaryAddress = $email; $account->addresses = array_unique($addresses); return [$account]; } /** * Returns OOF information * * @param array $request Oof/Get request data * * @return Syncroton_Model_Oof|null Response object or NULL if OOF is not supported * @throws Syncroton_Exception_Status */ public function getOOF($request) { $vacation_engine = $this->vacation_engine(); if (!$vacation_engine) { return null; } $vacation = $vacation_engine->get_vacation(); if (!$vacation['enabled']) { $status = Syncroton_Model_Oof::STATUS_DISABLED; $vacation['start'] = $vacation['end'] = null; } elseif ($vacation['start'] || $vacation['end']) { // in Activesync both or none time are required if (!$vacation['start'] && $vacation['end']) { $vacation['start'] = new DateTime('1970-01-01', new DateTimeZone('UTC')); } if (!$vacation['end'] && $vacation['start']) { $vacation['end'] = new DateTime('2100-01-01', new DateTimeZone('UTC')); } // convert timezone to UTC if ($vacation['start']) { $vacation['start']->setTimezone(new DateTimeZone('UTC')); } if ($vacation['end']) { $vacation['end']->setTimezone(new DateTimeZone('UTC')); } $status = Syncroton_Model_Oof::STATUS_TIME_BASED; } else { $status = Syncroton_Model_Oof::STATUS_GLOBAL; } $message = null; if ($vacation['message']) { $message = []; // convert message format, Roundcube supports plain text only if ($request['bodyType'] == 'HTML') { $text2html = new rcube_text2html($vacation['message']); $vacation['message'] = $text2html->get_html(); } foreach (['Internal', 'ExternalKnown', 'ExternalUnknown'] as $type) { $message[] = new Syncroton_Model_OofMessage([ "appliesTo$type" => true, 'enabled' => 1, 'bodyType' => 'Text', 'replyMessage' => rcube_charset::clean($vacation['message']), ]); } } return new Syncroton_Model_Oof([ 'oofState' => $status, 'startTime' => $vacation['start'], 'endTime' => $vacation['end'], 'oofMessage' => $message, ]); } /** * Sets OOF information * * @param Syncroton_Model_Oof $request Request object * * @throws Syncroton_Exception_Status */ public function setOOF($request) { $vacation_engine = $this->vacation_engine(); if (!$vacation_engine) { return; } $vacation = $vacation_engine->get_vacation(); // enable out-of-office if (!empty($request->oofState)) { if ($request->oofState == Syncroton_Model_Oof::STATUS_TIME_BASED) { $vacation['start'] = $request->startTime; $vacation['end'] = $request->endTime; if (empty($vacation['start']) || empty($vacation['end'])) { throw new Syncroton_Exception_Status_Settings(Syncroton_Exception_Status_Settings::INVALID_ARGUMENTS); } } else { $vacation['start'] = $vacation['end'] = null; } foreach ($request->oofMessage as $msg) { if ($msg->enabled && ($message = $msg->replyMessage)) { $message_type = $msg->bodyType; // convert message format, Roundcube supports plain text only if ($message_type == 'HTML') { $html2text = new rcube_html2text($message, false, true); $message = $html2text->get_text(); } break; } } if (empty($message)) { throw new Syncroton_Exception_Status_Settings(Syncroton_Exception_Status_Settings::INVALID_ARGUMENTS); } $vacation['message'] = $message; $vacation['subject'] = null; $vacation['enabled'] = true; $vacation_engine->set_vacation($vacation); } // disable out-of-office elseif (isset($request->oofState)) { if ($vacation['enabled']) { $vacation['enabled'] = false; $vacation_engine->set_vacation($vacation); } } } /** * Load managesieve plugin and return vacation engine class */ private function vacation_engine() { $engine = kolab_sync::get_instance(); $engine->plugins->load_plugin('managesieve', true, false); if (class_exists('managesieve')) { $plugin = $engine->plugins->get_plugin('managesieve'); $vacation = $plugin->get_engine('vacation'); // @phpstan-ignore-line if ($vacation->connect($engine->username, $engine->password)) { throw new Exception("Connection to managesieve server failed"); } return $vacation; } } } diff --git a/lib/kolab_sync_backend_state.php b/lib/kolab_sync_backend_state.php index 425f667..deb4c4a 100644 --- a/lib/kolab_sync_backend_state.php +++ b/lib/kolab_sync_backend_state.php @@ -1,229 +1,228 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for the folder state storage */ class kolab_sync_backend_state extends kolab_sync_backend_common implements Syncroton_Backend_ISyncState { protected $table_name = 'syncroton_synckey'; protected $interface_name = 'Syncroton_Model_ISyncState'; /** * Create new sync state of a folder * * @param Syncroton_Model_ISyncState $object State object * @param bool $keep_previous_state Don't remove other states * * @return Syncroton_Model_SyncState */ public function create($object, $keep_previous_state = true) { unset($object->counterNext); $object = parent::create($object); if ($keep_previous_state !== true) { // remove all other synckeys $this->_deleteOtherStates($object); } return $object; } /** * Deletes states other than specified one */ protected function _deleteOtherStates(Syncroton_Model_ISyncState $state) { // remove all other synckeys $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($state->deviceId); $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($state->type); $where[] = $this->db->quote_identifier('counter') . ' <> ' . $this->db->quote($state->counter); $this->db->query("DELETE FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); } /** * @see kolab_sync_backend_common::object_to_array() */ protected function object_to_array($object) { $data = parent::object_to_array($object); if (is_array($object->pendingdata)) { $data['pendingdata'] = gzdeflate(json_encode($object->pendingdata)); } return $data; } /** * @see kolab_sync_backend_common::get_object() */ protected function get_object($data) { $object = parent::get_object($data); if ($object->pendingdata) { $inflated = gzinflate($object->pendingdata); // Inflation may fail for backward compatiblity $data = $inflated ? $inflated : $object->pendingdata; $object->pendingdata = json_decode($data, true); } return $object; } /** * Returns the latest sync state * * @param Syncroton_Model_IDevice|string $deviceid Device object or identifier * @param Syncroton_Model_IFolder|string $folderid Folder object or identifier * * @return Syncroton_Model_SyncState */ public function getSyncState($deviceid, $folderid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $select = $this->db->limitquery("SELECT * FROM `{$this->table_name}` WHERE " . implode(' AND ', $where) . " ORDER BY `counter` DESC", 0, 1); $state = $this->db->fetch_assoc($select); if (empty($state)) { throw new Syncroton_Exception_NotFound('SyncState not found'); } return $this->get_object($state); } /** * Delete all stored synckeys of given type * * @param Syncroton_Model_IDevice|string $deviceid Device object or identifier * @param Syncroton_Model_IFolder|string $folderid Folder object or identifier */ public function resetState($deviceid, $folderid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $this->db->query("DELETE FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); } /** * Validates specified sync state by checking for existance of newer keys * * @param Syncroton_Model_IDevice|string $deviceid Device object or identifier * @param Syncroton_Model_IFolder|string $folderid Folder object or identifier * @param int $sync_key State key * * @return Syncroton_Model_SyncState|false */ public function validate($deviceid, $folderid, $sync_key) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $states = []; // get sync data // we'll get all records, thanks to this we'll be able to // skip _deleteOtherStates() call below (one DELETE query less) $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $where['type'] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); while ($row = $this->db->fetch_assoc($select)) { $states[$row['counter']] = $this->get_object($row); } // last state not found if (empty($states) || empty($states[$sync_key])) { return false; } $state = $states[$sync_key]; $next = max(array_keys($states)); $where = []; $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $where['folder_id'] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folder_id); $where['is_deleted'] = $this->db->quote_identifier('is_deleted') . ' = 1'; // found more recent synckey => the last sync response was not received by the client if ($next > $sync_key) { // We store the clientIdMap with the "next" sync state, so we need to copy it back. $state->clientIdMap = $states[$next]->clientIdMap; $state->counterNext = $next; } else { // finally delete all entries marked for removal in syncroton_content table $retryCounter = 0; while (true) { $result = $this->db->query("DELETE FROM `syncroton_content` WHERE " . implode(' AND ', $where)); if ($this->db->is_error($result)) { $retryCounter++; // Retry on deadlock if ($this->db->error_info()[0] != '40001' || $retryCounter > 60) { throw new Exception('Failed to delete entries in sync_key check'); } } else { break; } //Give the other transactions some time before we try again sleep(1); } } // remove all other synckeys if (count($states) > 1) { $this->_deleteOtherStates($state); } return $state; } public function haveNext($deviceid, $folderid, $sync_key) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $where['type'] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $where['counter'] = $this->db->quote_identifier('counter') . ' > ' . $this->db->quote($sync_key); $select = $this->db->query("SELECT id FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); return $this->db->num_rows($select) > 0; } - } diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php index ac75fee..cabddb9 100644 --- a/lib/kolab_sync_data.php +++ b/lib/kolab_sync_data.php @@ -1,1565 +1,1563 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Base class for Syncroton data backends */ abstract class kolab_sync_data implements Syncroton_Data_IData { /** * ActiveSync protocol version * * @var float */ protected $asversion = 0; /** * The storage backend * * @var kolab_sync_storage */ protected $backend; /** * information about the current device * * @var Syncroton_Model_IDevice */ protected $device; /** * timestamp to use for all sync requests * * @var DateTime */ protected $syncTimeStamp; /** * name of model to use * * @var string */ protected $modelName; /** * type of the default folder * * @var int */ protected $defaultFolderType; /** * default container for new entries * * @var string */ protected $defaultFolder; /** * default root folder * * @var string */ protected $defaultRootFolder; /** * type of user created folders * * @var int */ protected $folderType; /** * Internal cache for storage folders list * * @var array */ protected $folders = []; /** * Logger instance. * * @var kolab_sync_logger */ protected $logger; /** * Timezone * * @var string */ protected $timezone; /** * List of device types with multiple folders support * * @var array */ protected $ext_devices = [ 'iphone', 'ipad', 'thundertine', 'windowsphone', 'wp', 'wp8', 'playbook', ]; protected $lastsync_folder = null; protected $lastsync_time = null; public const RESULT_OBJECT = 0; public const RESULT_UID = 1; public const RESULT_COUNT = 2; /** * Recurrence types */ public const RECUR_TYPE_DAILY = 0; // Recurs daily. public const RECUR_TYPE_WEEKLY = 1; // Recurs weekly public const RECUR_TYPE_MONTHLY = 2; // Recurs monthly public const RECUR_TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day public const RECUR_TYPE_YEARLY = 5; // Recurs yearly public const RECUR_TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day /** * Day of week constants */ public const RECUR_DOW_SUNDAY = 1; public const RECUR_DOW_MONDAY = 2; public const RECUR_DOW_TUESDAY = 4; public const RECUR_DOW_WEDNESDAY = 8; public const RECUR_DOW_THURSDAY = 16; public const RECUR_DOW_FRIDAY = 32; public const RECUR_DOW_SATURDAY = 64; public const RECUR_DOW_LAST = 127; // The last day of the month. Used as a special value in monthly or yearly recurrences. /** * Mapping of recurrence types * * @var array */ protected $recurTypeMap = [ self::RECUR_TYPE_DAILY => 'DAILY', self::RECUR_TYPE_WEEKLY => 'WEEKLY', self::RECUR_TYPE_MONTHLY => 'MONTHLY', self::RECUR_TYPE_MONTHLY_DAYN => 'MONTHLY', self::RECUR_TYPE_YEARLY => 'YEARLY', self::RECUR_TYPE_YEARLY_DAYN => 'YEARLY', ]; /** * Mapping of weekdays * NOTE: ActiveSync uses a bitmask * * @var array */ protected $recurDayMap = [ 'SU' => self::RECUR_DOW_SUNDAY, 'MO' => self::RECUR_DOW_MONDAY, 'TU' => self::RECUR_DOW_TUESDAY, 'WE' => self::RECUR_DOW_WEDNESDAY, 'TH' => self::RECUR_DOW_THURSDAY, 'FR' => self::RECUR_DOW_FRIDAY, 'SA' => self::RECUR_DOW_SATURDAY, ]; /** * the constructor * * @param Syncroton_Model_IDevice $device * @param DateTime $syncTimeStamp */ public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp) { $this->backend = kolab_sync::storage(); $this->device = $device; $this->asversion = floatval($device->acsversion); $this->syncTimeStamp = $this->backend->syncTimeStamp = $syncTimeStamp; $this->logger = Syncroton_Registry::get(Syncroton_Registry::LOGGERBACKEND); $this->defaultRootFolder = $this->defaultFolder . '::Syncroton'; // set internal timezone of kolab_format to user timezone try { $this->timezone = rcube::get_instance()->config->get('timezone', 'GMT'); kolab_format::$timezone = new DateTimeZone($this->timezone); } catch (Exception $e) { //rcube::raise_error($e, true); $this->timezone = 'GMT'; kolab_format::$timezone = new DateTimeZone('GMT'); } } /** * return list of supported folders for this backend * * @return array */ public function getAllFolders() { $list = []; // device supports multiple folders ? if ($this->isMultiFolder()) { // get the folders the user has access to $list = $this->listFolders(); } elseif ($default = $this->getDefaultFolder()) { $list = [$default['serverId'] => $default]; } // getAllFolders() is called only in FolderSync // throw Syncroton_Exception_Status_FolderSync exception if (!is_array($list)) { throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR); } foreach ($list as $idx => $folder) { $list[$idx] = new Syncroton_Model_Folder($folder); } return $list; } /** * Retrieve folders which were modified since last sync * * @param DateTime $startTimeStamp * @param DateTime $endTimeStamp * * @return array List of folders */ public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp) { // FIXME/TODO: Can we get mtime of a DAV folder? // Without this, we have a problem if folder ID does not change on rename return []; } /** * Returns true if the device supports multiple folders or it was configured so */ protected function isMultiFolder() { $config = rcube::get_instance()->config; $blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName); if (!is_array($blacklist)) { $blacklist = $config->get('activesync_multifolder_blacklist'); } if (is_array($blacklist)) { return !$this->deviceTypeFilter($blacklist); } return in_array_nocase($this->device->devicetype, $this->ext_devices); } /** * Returns default folder for current class type. */ protected function getDefaultFolder() { // Check if there's any folder configured for sync $folders = $this->listFolders(); if (empty($folders)) { return $folders; } foreach ($folders as $folder) { if ($folder['type'] == $this->defaultFolderType) { $default = $folder; break; } } // Return first on the list if there's no default if (empty($default)) { $default = array_first($folders); // make sure the type is default here $default['type'] = $this->defaultFolderType; } // Remember real folder ID and set ID/name to root folder $default['realid'] = $default['serverId']; $default['serverId'] = $this->defaultRootFolder; $default['displayName'] = $this->defaultFolder; return $default; } /** * Creates a folder */ public function createFolder(Syncroton_Model_IFolder $folder) { $result = $this->backend->folder_create($folder->displayName, $folder->type, $this->device->deviceid, $folder->parentId); if ($result) { $folder->serverId = $result; return $folder; } // Note: Looks like Outlook 2013 ignores any errors on FolderCreate command throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::UNKNOWN_ERROR); } /** * Updates a folder */ public function updateFolder(Syncroton_Model_IFolder $folder) { $result = $this->backend->folder_rename($folder->serverId, $this->device->deviceid, $folder->displayName, $folder->parentId); if ($result) { return $folder; } // @TODO: throw exception } /** * Deletes a folder */ public function deleteFolder($folder) { if ($folder instanceof Syncroton_Model_IFolder) { $folder = $folder->serverId; } // @TODO: throw exception return $this->backend->folder_delete($folder, $this->device->deviceid); } /** * Empty folder (remove all entries and optionally subfolders) * * @param string $folderid Folder identifier * @param array $options Options */ public function emptyFolderContents($folderid, $options) { // ActiveSync spec.: Clients use EmptyFolderContents to empty the Deleted Items folder. // The client can clear out all items in the Deleted Items folder when the user runs out of storage quota // (indicated by the return of an MailboxQuotaExceeded (113) status code from the server. // FIXME: Does that mean we don't need this to work on any other folder? // TODO: Respond with MailboxQuotaExceeded status. Where exactly? foreach ($this->extractFolders($folderid) as $folderid) { if (!$this->backend->folder_empty($folderid, $this->device->deviceid, !empty($options['deleteSubFolders']))) { throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR); } } } /** * Moves object into another location (folder) * * @param string $srcFolderId Source folder identifier * @param string $serverId Object identifier * @param string $dstFolderId Destination folder identifier * * @throws Syncroton_Exception_Status * @return string New object identifier */ public function moveItem($srcFolderId, $serverId, $dstFolderId) { // TODO: Optimize, we just need to find the folder ID and UID, we do not need to "fetch" it. $item = $this->getObject($srcFolderId, $serverId); if (!$item) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } $uid = $this->backend->moveItem($item['folderId'], $this->device->deviceid, $this->modelName, $item['uid'], $dstFolderId); return $this->serverId($uid, $dstFolderId); } /** * Add entry * * @param string $folderId Folder identifier * @param Syncroton_Model_IEntry $entry Entry object * * @return string ID of the created entry */ public function createEntry($folderId, Syncroton_Model_IEntry $entry) { $entry = $this->toKolab($entry, $folderId); if ($folderId == $this->defaultRootFolder) { $default = $this->getDefaultFolder(); if (!is_array($default)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } $folderId = $default['realid'] ?? $default['serverId']; } $uid = $this->backend->createItem($folderId, $this->device->deviceid, $this->modelName, $entry); if (empty($uid)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } return $this->serverId($uid, $folderId); } /** * update existing entry * * @param string $folderId * @param string $serverId * @param Syncroton_Model_IEntry $entry * * @return string ID of the updated entry */ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) { $oldEntry = $this->getObject($folderId, $serverId); if (empty($oldEntry)) { throw new Syncroton_Exception_NotFound('entry not found'); } $entry = $this->toKolab($entry, $folderId, $oldEntry); $uid = $this->backend->updateItem($oldEntry['folderId'], $this->device->deviceid, $this->modelName, $oldEntry['uid'], $entry); if (empty($uid)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } return $this->serverId($uid, $oldEntry['folderId']); } /** * Delete entry * * @param string $folderId * @param string $serverId * @param ?Syncroton_Model_SyncCollection $collectionData */ public function deleteEntry($folderId, $serverId, $collectionData = null) { // TODO: Optimize, we just need to find the folder ID and UID, we do not need to "fetch" it. $object = $this->getObject($folderId, $serverId); if ($object) { $deleted = $this->backend->deleteItem($object['folderId'], $this->device->deviceid, $this->modelName, $object['uid']); if (!$deleted) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } } } /** * Get attachment data from the server. * * @param string $fileReference * * @return Syncroton_Model_FileReference */ public function getFileReference($fileReference) { // to be implemented by Email data class throw new Syncroton_Exception_NotFound('File references not supported'); } /** * Search for existing entries * * @param string $folderid Folder identifier * @param array $filter Search filter * @param int $result_type Type of the result (see RESULT_* constants) * * @return array|int Search result as count or array of uids/objects */ protected function searchEntries($folderid, $filter = [], $result_type = self::RESULT_UID, $extraData = null) { $result = $result_type == self::RESULT_COUNT ? 0 : []; $ts = time(); $force = $this->lastsync_folder != $folderid || $this->lastsync_time <= $ts - Syncroton_Registry::getPingTimeout(); $found = false; foreach ($this->extractFolders($folderid) as $fid) { $search = $this->backend->searchEntries($fid, $this->device->deviceid, $this->modelName, $filter, $result_type, $force, $extraData); $found = true; switch ($result_type) { case self::RESULT_COUNT: $result += $search; break; case self::RESULT_UID: foreach ($search as $idx => $uid) { $search[$idx] = $this->serverId($uid, $fid); } $result = array_unique(array_merge($result, $search)); break; } } if (!$found) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $this->lastsync_folder = $folderid; $this->lastsync_time = $ts; return $result; } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { // overwrite by child class according to specified type return []; } /** * get all entries changed between two dates * * @param string $folderId * @param Syncroton_Model_ISyncState $syncState * @param int $filter_type * * @return array */ public function getChangedEntries($folderId, Syncroton_Model_ISyncState $syncState, $filter_type = null) { $start = $syncState->lastsync; $filter = $this->filter($filter_type); $filter[] = ['changed', '>', $start]; return $this->searchEntries($folderId, $filter, self::RESULT_UID, $syncState->extraData); } /** * Get count of entries changed between two dates * * @param string $folderId * @param Syncroton_Model_ISyncState $syncState * @param int $filter_type * * @return int */ private function getChangedEntriesCount($folderId, Syncroton_Model_ISyncState $syncState, $filter_type = null) { $start = $syncState->lastsync; $filter = $this->filter($filter_type); $filter[] = ['changed', '>', $start]; return $this->searchEntries($folderId, $filter, self::RESULT_COUNT, $syncState->extraData); } - + public function getExtraData(Syncroton_Model_IFolder $folder) { return $this->backend->getExtraData($folder->serverId, $this->device->deviceid); } /** * get id's of all entries available on the server * * @param string $folder_id * @param string $filter_type * * @return array */ public function getServerEntries($folder_id, $filter_type) { $filter = $this->filter($filter_type); $result = $this->searchEntries($folder_id, $filter, self::RESULT_UID); return $result; } /** * get count of all entries available on the server * * @param string $folder_id * @param string $filter_type * * @return int */ public function getServerEntriesCount($folder_id, $filter_type) { $filter = $this->filter($filter_type); $result = $this->searchEntries($folder_id, $filter, self::RESULT_COUNT); return $result; } /** * Returns number of changed objects in the backend folder * * @param Syncroton_Backend_IContent $contentBackend * @param Syncroton_Model_IFolder $folder * @param Syncroton_Model_ISyncState $syncState * * @return int */ public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { - // @phpstan-ignore-next-line $allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter); $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype); $changedEntries = $this->getChangedEntriesCount($folder->serverId, $syncState, $folder->lastfiltertype); $addedEntries = array_diff($allServerEntries, $allClientEntries); $deletedEntries = array_diff($allClientEntries, $allServerEntries); return count($addedEntries) + count($deletedEntries) + $changedEntries; } /** * Returns true if any data got modified in the backend folder * * @param Syncroton_Backend_IContent $contentBackend * @param Syncroton_Model_IFolder $folder * @param Syncroton_Model_ISyncState $syncState * * @return bool */ public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { try { if ($this->getChangedEntriesCount($folder->serverId, $syncState, $folder->lastfiltertype)) { return true; } - // @phpstan-ignore-next-line $allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter); // @TODO: Consider looping over all folders here, not in getServerEntries() and // getChangedEntriesCount(). This way we could break the loop and not check all folders // or at least skip redundant cache sync of the same folder $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype); $addedEntries = array_diff($allServerEntries, $allClientEntries); $deletedEntries = array_diff($allClientEntries, $allServerEntries); return count($addedEntries) > 0 || count($deletedEntries) > 0; } catch (Exception $e) { // return "no changes" if something failed return false; } } /** * Fetches the entry from the backend */ protected function getObject($folderid, $entryid) { foreach ($this->extractFolders($folderid) as $fid) { $crc = null; $uid = $entryid; // See self::serverId() for full explanation // Use (slower) UID prefix matching... if (preg_match('/^CRC([0-9A-Fa-f]{8})(.+)$/', $uid, $matches)) { $crc = $matches[1]; $uid = $matches[2]; if (strlen($entryid) >= 64) { $objects = $this->backend->getItemsByUidPrefix($fid, $this->device->deviceid, $this->modelName, $uid); foreach ($objects as $object) { if (($object['uid'] === $uid || strpos($object['uid'], $uid) === 0) && $crc == $this->objectCRC($object['uid'], $fid) ) { $object['folderId'] = $fid; return $object; } } continue; } } // Or (faster) strict UID matching... $object = $this->backend->getItem($fid, $this->device->deviceid, $this->modelName, $uid); if (!empty($object) && ($crc === null || $crc == $this->objectCRC($object['uid'], $fid))) { $object['folderId'] = $fid; return $object; } } } /** * Returns internal folder IDs * * @param string $folderid Folder identifier * * @return array List of folder identifiers */ protected function extractFolders($folderid) { if ($folderid instanceof Syncroton_Model_IFolder) { $folderid = $folderid->serverId; } if ($folderid === $this->defaultRootFolder) { $folders = $this->listFolders(); if (!is_array($folders)) { throw new Syncroton_Exception_NotFound('Folder not found'); } $folders = array_keys($folders); } else { $folders = [$folderid]; } return $folders; } /** * List of all IMAP folders (or subtree) * * @param string $parentid Parent folder identifier * * @return array List of folder identifiers */ protected function listFolders($parentid = null) { if (empty($this->folders)) { $this->folders = $this->backend->folders_list( $this->device->deviceid, $this->modelName, $this->isMultiFolder() ); } if ($parentid === null || !is_array($this->folders)) { return $this->folders; } $folders = []; $parents = [$parentid]; foreach ($this->folders as $folder_id => $folder) { if ($folder['parentId'] && in_array($folder['parentId'], $parents)) { $folders[$folder_id] = $folder; $parents[] = $folder_id; } } return $folders; } /** * Returns ActiveSync settings of specified folder * * @param string $folderid Folder identifier * * @return array Folder settings */ protected function getFolderConfig($folderid) { if ($folderid == $this->defaultRootFolder) { $default = $this->getDefaultFolder(); if (!is_array($default)) { return []; } $folderid = $default['realid'] ?? $default['serverId']; } return $this->backend->getFolderConfig($folderid, $this->device->deviceid, $this->modelName); } /** * Convert contact from xml to kolab format * * @param mixed $data Contact data * @param string $folderId Folder identifier * @param array $entry Old Contact data for merge * * @return array */ abstract public function toKolab($data, $folderId, $entry = null); /** * Extracts data from kolab data array */ protected function getKolabDataItem($data, $name) { $name_items = explode('.', $name); $count = count($name_items); // multi-level array (e.g. address, phone) if ($count == 3) { $name = $name_items[0]; $type = $name_items[1]; $key_name = $name_items[2]; if (!empty($data[$name]) && is_array($data[$name])) { foreach ($data[$name] as $element) { if ($element['type'] == $type) { return $element[$key_name]; } } } return null; } // custom properties if ($count == 2 && $name_items[0] == 'x-custom') { $value = null; if (!empty($data['x-custom']) && is_array($data['x-custom'])) { foreach ($data['x-custom'] as $val) { if (is_array($val) && $val[0] == $name_items[1]) { $value = $val[1]; break; } } } return $value; } $name_items = explode(':', $name); $name = $name_items[0]; if (empty($data[$name])) { return null; } // simple array (e.g. email) if (count($name_items) == 2) { return $data[$name][$name_items[1]]; } return $data[$name]; } /** * Saves data in kolab data array */ protected function setKolabDataItem(&$data, $name, $value) { if (empty($value)) { return $this->unsetKolabDataItem($data, $name); } $name_items = explode('.', $name); $count = count($name_items); // multi-level array (e.g. address, phone) if ($count == 3) { $name = $name_items[0]; $type = $name_items[1]; $key_name = $name_items[2]; if (!isset($data[$name])) { $data[$name] = []; } foreach ($data[$name] as $idx => $element) { if ($element['type'] == $type) { $found = $idx; break; } } if (!isset($found)) { $data[$name] = array_values($data[$name]); $found = count($data[$name]); $data[$name][$found] = ['type' => $type]; } $data[$name][$found][$key_name] = $value; return; } // custom properties if ($count == 2 && $name_items[0] == 'x-custom') { $data['x-custom'] = isset($data['x-custom']) ? ((array) $data['x-custom']) : []; foreach ($data['x-custom'] as $idx => $val) { if (is_array($val) && $val[0] == $name_items[1]) { $data['x-custom'][$idx][1] = $value; return; } } $data['x-custom'][] = [$name_items[1], $value]; return; } $name_items = explode(':', $name); $name = $name_items[0]; // simple array (e.g. email) if (count($name_items) == 2) { $data[$name][$name_items[1]] = $value; return; } $data[$name] = $value; } /** * Unsets data item in kolab data array */ protected function unsetKolabDataItem(&$data, $name) { $name_items = explode('.', $name); $count = count($name_items); // multi-level array (e.g. address, phone) if ($count == 3) { $name = $name_items[0]; $type = $name_items[1]; $key_name = $name_items[2]; if (!isset($data[$name])) { return; } foreach ($data[$name] as $idx => $element) { if ($element['type'] == $type) { $found = $idx; break; } } if (!isset($found)) { return; } unset($data[$name][$found][$key_name]); // if there's only one element and it's 'type', remove it if (count($data[$name][$found]) == 1 && isset($data[$name][$found]['type'])) { unset($data[$name][$found]['type']); } if (empty($data[$name][$found])) { unset($data[$name][$found]); } if (empty($data[$name])) { unset($data[$name]); } return; } // custom properties if ($count == 2 && $name_items[0] == 'x-custom') { foreach ((array) $data['x-custom'] as $idx => $val) { if (is_array($val) && $val[0] == $name_items[1]) { unset($data['x-custom'][$idx]); } } } $name_items = explode(':', $name); $name = $name_items[0]; // simple array (e.g. email) if (count($name_items) == 2) { unset($data[$name][$name_items[1]]); if (empty($data[$name])) { unset($data[$name]); } return; } unset($data[$name]); } /** * Setter for Body attribute according to client version * * @param string $value Body * @param array $params Body parameters * * @reurn Syncroton_Model_EmailBody Body element */ protected function setBody($value, $params = []) { if (empty($value) && empty($params)) { return; } // Old protocol version doesn't support AirSyncBase:Body, it's eg. WindowsCE if ($this->asversion < 12) { return; } if (!empty($value)) { // cast to string to workaround issue described in Bug #1635 $params['data'] = (string) $value; } if (!isset($params['type'])) { $params['type'] = Syncroton_Model_EmailBody::TYPE_PLAINTEXT; } return new Syncroton_Model_EmailBody($params); } /** * Getter for Body attribute value according to client version * * @param mixed $body Body element * @param int $type Result data type (to which the body will be converted, if specified). * One or array of Syncroton_Model_EmailBody constants. * * @return string|null Body value */ protected function getBody($body, $type = null) { $data = null; if ($body && $body->data) { $data = $body->data; } if (!$data || empty($type)) { return null; } $type = (array) $type; // Convert to specified type if (!in_array($body->type, $type)) { $converter = new kolab_sync_body_converter($data, $body->type); $data = $converter->convert($type[0]); } return $data; } /** * Converts text (plain or html) into ActiveSync Body element. * Takes bodyPreferences into account and detects if the text is plain or html. */ protected function body_from_kolab($body, $collection) { if (empty($body)) { return; } $opts = $collection->options; $prefs = $opts['bodyPreferences']; $html_type = Syncroton_Command_Sync::BODY_TYPE_HTML; $type = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT; $params = []; // HTML? check for opening and closing or tags $is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, '') > 0; // here we assume that all devices support plain text if ($is_html) { // device supports HTML... if (!empty($prefs[$html_type])) { $type = $html_type; } // ...else convert to plain text else { $txt = new rcube_html2text($body, false, true); $body = $txt->get_text(); } } // strip out any non utf-8 characters $body = rcube_charset::clean($body); $real_length = $body_length = strlen($body); // truncate the body if needed if (isset($prefs[$type]['truncationSize']) && ($truncateAt = $prefs[$type]['truncationSize']) && $body_length > $truncateAt) { $body = mb_strcut($body, 0, $truncateAt); $body_length = strlen($body); $params['truncated'] = 1; $params['estimatedDataSize'] = $real_length; } $params['type'] = $type; return $this->setBody($body, $params); } /** * Converts PHP DateTime, date (YYYY-MM-DD) or unixtimestamp into PHP DateTime in UTC * * @param DateTime|int|string $date Unix timestamp, date (YYYY-MM-DD) or PHP DateTime object * * @return DateTime|null Datetime object */ protected static function date_from_kolab($date) { if (!empty($date)) { if (is_numeric($date)) { $date = new DateTime('@' . $date); } elseif (is_string($date)) { $date = new DateTime($date, new DateTimeZone('UTC')); } elseif ($date instanceof DateTime) { $date = clone $date; $tz = $date->getTimezone(); $tz_name = $tz->getName(); // convert to UTC if needed if ($tz_name != 'UTC') { $utc = new DateTimeZone('UTC'); // safe dateonly object conversion to UTC // note: _dateonly flag is set by libkolab e.g. for birthdays if (!empty($date->_dateonly)) { // avoid time change $date = new DateTime($date->format('Y-m-d'), $utc); // set time to noon to avoid timezone troubles $date->setTime(12, 0, 0); } else { $date->setTimezone($utc); } } } else { return null; // invalid input } return $date; } return null; } /** * Convert Kolab event/task recurrence into ActiveSync */ protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event') { if (empty($data['recurrence']) || !empty($data['recurrence_date']) || empty($data['recurrence']['FREQ'])) { return; } $recurrence = []; $r = $data['recurrence']; // required fields switch($r['FREQ']) { case 'DAILY': $recurrence['type'] = self::RECUR_TYPE_DAILY; break; case 'WEEKLY': $day = $r['BYDAY'] ?? 0; if (!$day && (!empty($data['_start']) || !empty($data['start']))) { $days = ['', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA','SU']; $start = $data['_start'] ?? $data['start']; $day = $days[$start->format('N')]; } $recurrence['type'] = self::RECUR_TYPE_WEEKLY; $recurrence['dayOfWeek'] = $this->day2bitmask($day); break; case 'MONTHLY': if (!empty($r['BYMONTHDAY'])) { // @TODO: ActiveSync doesn't support multi-valued month days, // should we replicate the recurrence element for each day of month? [$month_day, ] = explode(',', $r['BYMONTHDAY']); $recurrence['type'] = self::RECUR_TYPE_MONTHLY; $recurrence['dayOfMonth'] = $month_day; } elseif (!empty($r['BYDAY'])) { $week = (int) substr($r['BYDAY'], 0, -2); $week = ($week == -1) ? 5 : $week; $day = substr($r['BYDAY'], -2); $recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN; $recurrence['weekOfMonth'] = $week; $recurrence['dayOfWeek'] = $this->day2bitmask($day); } else { return; } break; case 'YEARLY': // @TODO: ActiveSync doesn't support multi-valued months, // should we replicate the recurrence element for each month? [$month, ] = explode(',', $r['BYMONTH']); if (!empty($r['BYDAY'])) { $week = (int) substr($r['BYDAY'], 0, -2); $week = ($week == -1) ? 5 : $week; $day = substr($r['BYDAY'], -2); $recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN; $recurrence['weekOfMonth'] = $week; $recurrence['dayOfWeek'] = $this->day2bitmask($day); $recurrence['monthOfYear'] = $month; } elseif (!empty($r['BYMONTHDAY'])) { // @TODO: ActiveSync doesn't support multi-valued month days, // should we replicate the recurrence element for each day of month? [$month_day, ] = explode(',', $r['BYMONTHDAY']); $recurrence['type'] = self::RECUR_TYPE_YEARLY; $recurrence['dayOfMonth'] = $month_day; $recurrence['monthOfYear'] = $month; } else { $recurrence['type'] = self::RECUR_TYPE_YEARLY; $recurrence['monthOfYear'] = $month; } break; } // Skip all empty values (T2519) if ($recurrence['type'] != self::RECUR_TYPE_DAILY) { $recurrence = array_filter($recurrence); } // required field $recurrence['interval'] = $r['INTERVAL'] ?: 1; if (!empty($r['UNTIL'])) { $recurrence['until'] = self::date_from_kolab($r['UNTIL']); } elseif (!empty($r['COUNT'])) { $recurrence['occurrences'] = $r['COUNT']; } $class = 'Syncroton_Model_' . $type . 'Recurrence'; $result['recurrence'] = new $class($recurrence); // Tasks do not support exceptions if ($type == 'Event') { $result['exceptions'] = $this->exceptions_from_kolab($collection, $data); } } /** * Convert ActiveSync event/task recurrence into Kolab */ protected function recurrence_to_kolab($data, $folderid, $timezone = null) { if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence) && !($data->recurrence instanceof Syncroton_Model_TaskRecurrence) ) { return; } if (!isset($data->recurrence->type)) { return; } $recurrence = $data->recurrence; $type = $recurrence->type; switch ($type) { case self::RECUR_TYPE_DAILY: break; case self::RECUR_TYPE_WEEKLY: $rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek); break; case self::RECUR_TYPE_MONTHLY: $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth; break; case self::RECUR_TYPE_MONTHLY_DAYN: $week = $recurrence->weekOfMonth; $day = $recurrence->dayOfWeek; $byDay = $week == 5 ? -1 : $week; $byDay .= $this->bitmask2day($day); $rrule['BYDAY'] = $byDay; break; case self::RECUR_TYPE_YEARLY: $rrule['BYMONTH'] = $recurrence->monthOfYear; $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth; break; case self::RECUR_TYPE_YEARLY_DAYN: $rrule['BYMONTH'] = $recurrence->monthOfYear; $week = $recurrence->weekOfMonth; $day = $recurrence->dayOfWeek; $byDay = $week == 5 ? -1 : $week; $byDay .= $this->bitmask2day($day); $rrule['BYDAY'] = $byDay; break; } $rrule['FREQ'] = $this->recurTypeMap[$type]; $rrule['INTERVAL'] = $recurrence->interval ?? 1; if (isset($recurrence->until)) { if ($timezone) { $recurrence->until->setTimezone($timezone); } $rrule['UNTIL'] = $recurrence->until; } elseif (!empty($recurrence->occurrences)) { $rrule['COUNT'] = $recurrence->occurrences; } // recurrence exceptions (not supported by Tasks) if ($data instanceof Syncroton_Model_Event) { $this->exceptions_to_kolab($data, $rrule, $folderid, $timezone); } return $rrule; } /** * Convert Kolab event recurrence exceptions into ActiveSync */ protected function exceptions_from_kolab($collection, $data) { if (empty($data['recurrence']['EXCEPTIONS']) && empty($data['recurrence']['EXDATE'])) { return null; } $ex_list = []; // exceptions (modified occurences) if (!empty($data['recurrence']['EXCEPTIONS'])) { foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) { $exception['_mailbox'] = $data['_mailbox']; $ex = $this->getEntry($collection, $exception, true); // @phpstan-ignore-line $date = clone ($exception['recurrence_date'] ?: $ex['startTime']); $ex['exceptionStartTime'] = self::set_exception_time($date, $data['_start'] ?? null); // remove fields not supported by Syncroton_Model_EventException unset($ex['uID']); // @TODO: 'thisandfuture=true' is not supported in Activesync // we'd need to slit the event into two separate events $ex_list[] = new Syncroton_Model_EventException($ex); } } // exdate (deleted occurences) if (!empty($data['recurrence']['EXDATE'])) { foreach ((array)$data['recurrence']['EXDATE'] as $exception) { if (!($exception instanceof DateTime)) { continue; } $ex = [ 'deleted' => 1, 'exceptionStartTime' => self::set_exception_time($exception, $data['_start'] ?? null), ]; $ex_list[] = new Syncroton_Model_EventException($ex); } } return $ex_list; } /** * Convert ActiveSync event recurrence exceptions into Kolab */ protected function exceptions_to_kolab($data, &$rrule, $folderid, $timezone = null) { $rrule['EXDATE'] = []; $rrule['EXCEPTIONS'] = []; // handle exceptions from recurrence if (!empty($data->exceptions)) { foreach ($data->exceptions as $exception) { $date = clone $exception->exceptionStartTime; if ($timezone) { $date->setTimezone($timezone); } if ($exception->deleted) { $date->setTime(0, 0, 0); $rrule['EXDATE'][] = $date; } else { $ex = $this->toKolab($exception, $folderid, null, $timezone); // @phpstan-ignore-line $ex['recurrence_date'] = $date; if (!empty($data->allDayEvent)) { $ex['allday'] = 1; } $rrule['EXCEPTIONS'][] = $ex; } } } if (empty($rrule['EXDATE'])) { unset($rrule['EXDATE']); } if (empty($rrule['EXCEPTIONS'])) { unset($rrule['EXCEPTIONS']); } } /** * Sets ExceptionStartTime according to occurrence date and event start time */ protected static function set_exception_time($exception_date, $event_start) { if ($exception_date && $event_start) { $hour = $event_start->format('H'); $minute = $event_start->format('i'); $second = $event_start->format('s'); $exception_date->setTime($hour, $minute, $second); $exception_date->_dateonly = false; return self::date_from_kolab($exception_date); } } /** * Converts string of days (TU,TH) to bitmask used by ActiveSync * * @param string $days * * @return int */ protected function day2bitmask($days) { $days = explode(',', $days); $result = 0; foreach ($days as $day) { if ($day) { $result = $result + ($this->recurDayMap[$day] ?? 0); } } return $result; } /** * Convert bitmask used by ActiveSync to string of days (TU,TH) * * @param int $days * * @return string */ protected function bitmask2day($days) { $days_arr = []; for ($bitmask = 1; $bitmask <= self::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) { $dayMatch = $days & $bitmask; if ($dayMatch === $bitmask) { $days_arr[] = array_search($bitmask, $this->recurDayMap); } } $result = implode(',', $days_arr); return $result; } /** * Check if current device type string matches any of options */ protected function deviceTypeFilter($options) { foreach ($options as $option) { if ($option[0] == '/') { if (preg_match($option, $this->device->devicetype)) { return true; } } elseif (stripos($this->device->devicetype, $option) !== false) { return true; } } return false; } /** * Returns all email addresses of the current user */ protected function user_emails() { $user_emails = kolab_sync::get_instance()->user->list_emails(); $user_emails = array_map(function ($v) { return $v['email']; }, $user_emails); return $user_emails; } /** * Generate CRC-based ServerId from object UID */ protected function serverId($uid, $folder) { // When ActiveSync communicates with the client, it refers to objects with a ServerId // We can't use object UID for ServerId because: // - ServerId is limited to 64 chars, // - there can be multiple calendars with a copy of the same event. // // The solution is to; Take the original UID, and regardless of its length, execute the following: // - Hash the UID concatenated with the Folder ID using CRC32b, // - Prefix the UID with 'CRC' and the hash string, // - Tryncate the result to 64 characters. // // Searching for the server-side copy of the object now follows the logic; // - If the ServerId is prefixed with 'CRC', strip off the first 11 characters // and we search for the UID using the remainder; // - if the UID is shorter than 53 characters, it'll be the complete UID, // - if the UID is longer than 53 characters, it'll be the truncated UID, // and we search for a wildcard match of * // When multiple copies of the same event are found, the same CRC32b hash can be used // on the events metadata (i.e. the copy's UID and Folder ID), and compared with the CRC from the ServerId. // ServerId is max. 64 characters, below we generate a string of max. 64 chars // Note: crc32b is always 8 characters return 'CRC' . $this->objectCRC($uid, $folder) . substr($uid, 0, 53); } /** * Calculate checksum on object UID and folder UID */ protected function objectCRC($uid, $folder) { if (!is_object($folder)) { $folder = $this->backend->getFolder($folder, $this->device->deviceid, $this->modelName); } $folder_uid = $folder->get_uid(); return strtoupper(hash('crc32b', $folder_uid . $uid)); // always 8 chars } } diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php index 12e660e..3a164b1 100644 --- a/lib/kolab_sync_data_calendar.php +++ b/lib/kolab_sync_data_calendar.php @@ -1,1346 +1,1346 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Calendar (Events) data class for Syncroton */ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data_IDataCalendar { /** * Mapping from ActiveSync Calendar namespace fields */ protected $mapping = [ 'allDayEvent' => 'allday', 'startTime' => 'start', // keep it before endTime here //'attendees' => 'attendees', 'body' => 'description', //'bodyTruncated' => 'bodytruncated', 'busyStatus' => 'free_busy', //'categories' => 'categories', 'dtStamp' => 'changed', 'endTime' => 'end', //'exceptions' => 'exceptions', 'location' => 'location', //'meetingStatus' => 'meetingstatus', //'organizerEmail' => 'organizeremail', //'organizerName' => 'organizername', //'recurrence' => 'recurrence', //'reminder' => 'reminder', //'responseRequested' => 'responserequested', //'responseType' => 'responsetype', 'sensitivity' => 'sensitivity', 'subject' => 'title', //'timezone' => 'timezone', 'uID' => 'uid', ]; /** * Kolab object type * * @var string */ protected $modelName = 'event'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'Calendar'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED; /** * attendee status */ public const ATTENDEE_STATUS_UNKNOWN = 0; public const ATTENDEE_STATUS_TENTATIVE = 2; public const ATTENDEE_STATUS_ACCEPTED = 3; public const ATTENDEE_STATUS_DECLINED = 4; public const ATTENDEE_STATUS_NOTRESPONDED = 5; /** * attendee types */ public const ATTENDEE_TYPE_REQUIRED = 1; public const ATTENDEE_TYPE_OPTIONAL = 2; public const ATTENDEE_TYPE_RESOURCE = 3; /** * busy status constants */ public const BUSY_STATUS_FREE = 0; public const BUSY_STATUS_TENTATIVE = 1; public const BUSY_STATUS_BUSY = 2; public const BUSY_STATUS_OUTOFOFFICE = 3; /** * Sensitivity values */ public const SENSITIVITY_NORMAL = 0; public const SENSITIVITY_PERSONAL = 1; public const SENSITIVITY_PRIVATE = 2; public const SENSITIVITY_CONFIDENTIAL = 3; /** * Internal iTip states */ public const ITIP_ACCEPTED = 'ACCEPTED'; public const ITIP_DECLINED = 'DECLINED'; public const ITIP_TENTATIVE = 'TENTATIVE'; public const ITIP_CANCELLED = 'CANCELLED'; public const KEY_DTSTAMP = 'x-custom.X-ACTIVESYNC-DTSTAMP'; public const KEY_REPLYTIME = 'x-custom.X-ACTIVESYNC-REPLYTIME'; /** * Mapping of attendee status * * @var array */ protected $attendeeStatusMap = [ 'UNKNOWN' => self::ATTENDEE_STATUS_UNKNOWN, 'TENTATIVE' => self::ATTENDEE_STATUS_TENTATIVE, 'ACCEPTED' => self::ATTENDEE_STATUS_ACCEPTED, 'DECLINED' => self::ATTENDEE_STATUS_DECLINED, 'DELEGATED' => self::ATTENDEE_STATUS_UNKNOWN, 'NEEDS-ACTION' => self::ATTENDEE_STATUS_NOTRESPONDED, ]; /** * Mapping of attendee type * * NOTE: recurrences need extra handling! * @var array */ protected $attendeeTypeMap = [ 'REQ-PARTICIPANT' => self::ATTENDEE_TYPE_REQUIRED, 'OPT-PARTICIPANT' => self::ATTENDEE_TYPE_OPTIONAL, // 'NON-PARTICIPANT' => self::ATTENDEE_TYPE_RESOURCE, // 'CHAIR' => self::ATTENDEE_TYPE_RESOURCE, ]; /** * Mapping of busy status * * @var array */ protected $busyStatusMap = [ 'free' => self::BUSY_STATUS_FREE, 'tentative' => self::BUSY_STATUS_TENTATIVE, 'busy' => self::BUSY_STATUS_BUSY, 'outofoffice' => self::BUSY_STATUS_OUTOFOFFICE, ]; /** * mapping of sensitivity * * @var array */ protected $sensitivityMap = [ 'public' => self::SENSITIVITY_PERSONAL, 'private' => self::SENSITIVITY_PRIVATE, 'confidential' => self::SENSITIVITY_CONFIDENTIAL, ]; /** * Appends contact data to xml element * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * @param bool $as_array Return entry as array * * @return array|Syncroton_Model_Event Event object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false) { $event = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId); $config = $this->getFolderConfig($event['folderId']); $result = []; $is_outlook = stripos($this->device->devicetype, 'outlook') !== false; $is_android = stripos($this->device->devicetype, 'android') !== false; // Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows // only one timezone per-event. We'll use timezone of the start date $result['timezone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']); // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getKolabDataItem($event, $name); switch ($name) { case 'changed': case 'end': case 'start': // For all-day events Kolab uses different times // At least Android doesn't display such event as all-day event if ($value && is_a($value, 'DateTime')) { $date = clone $value; if (!empty($event['allday'])) { // need this for self::date_from_kolab() $date->_dateonly = false; // @phpstan-ignore-line if ($name == 'start') { $date->setTime(0, 0, 0); } elseif ($name == 'end') { $date->setTime(0, 0, 0); $date->modify('+1 day'); } } // set this date for use in recurrence exceptions handling if ($name == 'start') { $event['_start'] = $date; } $value = self::date_from_kolab($date); } break; case 'sensitivity': if (!empty($value)) { $value = intval($this->sensitivityMap[$value]); } break; case 'free_busy': if (!empty($value)) { $value = $this->busyStatusMap[$value]; } break; case 'description': $value = $this->body_from_kolab($value, $collection); break; } // Ignore empty values (but not integer 0) if ((empty($value) || is_array($value)) && $value !== 0) { continue; } $result[$key] = $value; } // Event reminder time if (!empty($config['ALARMS'])) { $result['reminder'] = $this->from_kolab_alarm($event); } $result['categories'] = []; $result['attendees'] = []; // Categories, Roundcube Calendar plugin supports only one category at a time if (!empty($event['categories'])) { $result['categories'] = (array) $event['categories']; } // Organizer if (!empty($event['attendees'])) { foreach ($event['attendees'] as $idx => $attendee) { if ($attendee['role'] == 'ORGANIZER') { if (!empty($attendee['name'])) { $result['organizerName'] = $attendee['name']; } if (!empty($attendee['email'])) { $result['organizerEmail'] = $attendee['email']; } unset($event['attendees'][$idx]); break; } } } $resp_type = self::ATTENDEE_STATUS_UNKNOWN; $user_rsvp = false; // Attendees if (!empty($event['attendees'])) { $user_emails = $this->user_emails(); foreach ($event['attendees'] as $idx => $attendee) { if (empty($attendee['email'])) { // In Activesync email is required continue; } $email = $attendee['email']; $att = [ 'email' => $email, 'name' => !empty($attendee['name']) ? $attendee['name'] : $email, ]; $type = isset($attendee['role']) ? $this->attendeeTypeMap[$attendee['role']] : null; $status = isset($attendee['status']) ? $this->attendeeStatusMap[$attendee['status']] : null; if ($this->asversion >= 12) { if (isset($attendee['cutype']) && strtolower($attendee['cutype']) == 'resource') { $att['attendeeType'] = self::ATTENDEE_TYPE_RESOURCE; } else { $att['attendeeType'] = $type ?: self::ATTENDEE_TYPE_REQUIRED; } $att['attendeeStatus'] = $status ?: self::ATTENDEE_STATUS_UNKNOWN; } if (in_array_nocase($email, $user_emails)) { $user_rsvp = !empty($attendee['rsvp']); $resp_type = $status ?: self::ATTENDEE_STATUS_UNKNOWN; // Synchronize the attendee status to the event status to get the same behaviour as outlook. if (($is_outlook || $is_android) && isset($attendee['status'])) { if ($attendee['status'] == 'ACCEPTED') { $result['busyStatus'] = self::BUSY_STATUS_BUSY; } if ($attendee['status'] == 'TENTATIVE') { $result['busyStatus'] = self::BUSY_STATUS_TENTATIVE; } } } $result['attendees'][] = new Syncroton_Model_EventAttendee($att); } } // Event meeting status $this->meeting_status_from_kolab($event, $result); // Recurrence (and exceptions) $this->recurrence_from_kolab($collection, $event, $result); // RSVP status $result['responseRequested'] = $result['meetingStatus'] == 3 && $user_rsvp ? 1 : 0; $result['responseType'] = $result['meetingStatus'] == 3 ? $resp_type : null; // Appointment Reply Time (without it Outlook displays e.g. "Accepted on None") if ($resp_type != self::ATTENDEE_STATUS_UNKNOWN) { if ($reply_time = $this->getKolabDataItem($event, self::KEY_REPLYTIME)) { $result['appointmentReplyTime'] = new DateTime($reply_time, new DateTimeZone('UTC')); } elseif (!empty($event['changed'])) { $reply_time = clone $event['changed']; $reply_time->setTimezone(new DateTimeZone('UTC')); $result['appointmentReplyTime'] = $reply_time; } } return $as_array ? $result : new Syncroton_Model_Event($result); } /** * Convert an event from xml to libkolab array * * @param Syncroton_Model_Event|Syncroton_Model_EventException $data Event or event exception to convert * @param string $folderid Folder identifier * @param array $entry Existing entry * @param DateTimeZone $timezone Timezone of the event * * @return array */ public function toKolab($data, $folderid, $entry = null, $timezone = null) { if (empty($entry) && !empty($data->uID)) { // If we don't have an existing event (not a modification) we nevertheless check for conflicts. // This is necessary so we don't overwrite the server-side copy in case the client did not have it available // when generating an Add command. try { $entry = $this->getObject($folderid, $data->uID); if ($entry) { $this->logger->debug('Found and existing event for UID: ' . $data->uID); } } catch (Exception $e) { // uID is not available on exceptions, so we guard for that and silently ignore. } } $config = $this->getFolderConfig($entry ? $entry['folderId'] : $folderid); $event = !empty($entry) ? $entry : []; $is_exception = $data instanceof Syncroton_Model_EventException; $dummy_tz = str_repeat('A', 230) . '=='; $is_outlook = stripos($this->device->devicetype, 'outlook') !== false; $is_android = stripos($this->device->devicetype, 'android') !== false; // check data validity (of a new event) if (empty($event)) { $this->check_event($data); } if (!empty($event['start']) && ($event['start'] instanceof DateTime)) { $old_timezone = $event['start']->getTimezone(); } // Timezone if (!$timezone && isset($data->timezone) && $data->timezone != $dummy_tz) { $tzc = kolab_sync_timezone_converter::getInstance(); $expected = !empty($old_timezone) ? $old_timezone : kolab_format::$timezone; try { $timezone = $tzc->getTimezone($data->timezone, $expected->getName()); $timezone = new DateTimeZone($timezone); } catch (Exception $e) { $this->logger->warn('Failed to convert the timezone information. UID: ' . $event['uid'] . 'Timezone: ' . $data->timezone); $timezone = null; } } if (empty($timezone)) { $timezone = !empty($old_timezone) ? $old_timezone : new DateTimeZone('UTC'); } $event['allday'] = 0; // Calendar namespace fields foreach ($this->mapping as $key => $name) { // skip UID field, unsupported in event exceptions // we need to do this here, because the next line (data getter) will throw an exception if ($is_exception && $key == 'uID') { continue; } $value = $data->$key; // Skip ghosted (unset) properties, (but make sure 'changed' timestamp is reset) if ($value === null && $name != 'changed') { continue; } switch ($name) { case 'changed': $value = null; break; case 'end': case 'start': if ($timezone && $value) { $value->setTimezone($timezone); } if ($value && $data->allDayEvent) { $value->_dateonly = true; // In ActiveSync all-day event ends on 00:00:00 next day // In Kolab we just ignore the time spec. if ($name == 'end') { $diff = date_diff($event['start'], $value); $value = clone $event['start']; if ($diff->days > 1) { $value->add(new DateInterval('P' . ($diff->days - 1) . 'D')); } } } break; case 'sensitivity': $map = array_flip($this->sensitivityMap); $value = $map[$value] ?? null; break; case 'free_busy': // Outlook sets the busy state to the attendance state, and we don't want to change the event state based on that. // Outlook doesn't have the concept of an event state, so we just ignore this. if ($is_outlook || $is_android) { continue 2; } $map = array_flip($this->busyStatusMap); $value = $map[$value] ?? null; break; case 'description': $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT); // If description isn't specified keep old description if ($value === null) { continue 2; } break; } $this->setKolabDataItem($event, $name, $value); } // Try to fix allday events from Android // It doesn't set all-day flag but the period is a whole day if (empty($event['allday']) && !empty($event['end']) && !empty($event['start'])) { $interval = @date_diff($event['start'], $event['end']); if ($interval->format('%y%m%d%h%i%s') === '001000') { $event['allday'] = 1; $event['end'] = clone $event['start']; } } // Reminder // @TODO: should alarms be used when importing event from phone? if (!empty($config['ALARMS'])) { $event['valarms'] = $this->to_kolab_alarm($data->reminder, $event); } $attendees = []; $categories = []; // Categories if (isset($data->categories)) { foreach ($data->categories as $category) { $categories[] = $category; } } // Organizer if (!$is_exception) { // Organizer specified if ($organizer_email = $data->organizerEmail) { $attendees[] = [ 'role' => 'ORGANIZER', 'name' => $data->organizerName, 'email' => $organizer_email, ]; } elseif (!empty($event['attendees'])) { // Organizer not specified, use one from the original event if that's an update foreach ($event['attendees'] as $idx => $attendee) { if (!empty($attendee['email']) && !empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') { $organizer_email = $attendee['email']; $attendees[] = [ 'role' => 'ORGANIZER', 'name' => $attendee['name'] ?? '', 'email' => $organizer_email, ]; } } } } // Attendees // Whenever Outlook sends dummy timezone it is an event where the user is an attendee. // In these cases Attendees element is bogus: contains invalid status and does not // contain all attendees. We have to ignore it. if ($is_outlook && !$is_exception && $data->timezone === $dummy_tz) { $this->logger->debug('Dummy outlook update detected, ignoring attendee changes.'); $attendees = $entry['attendees']; } elseif (isset($data->attendees)) { foreach ($data->attendees as $attendee) { if (!empty($organizer_email) && $attendee->email && !strcasecmp($attendee->email, $organizer_email)) { // skip the organizer continue; } $role = false; if (isset($attendee->attendeeType)) { $role = array_search($attendee->attendeeType, $this->attendeeTypeMap); } if ($role === false) { $role = array_search(self::ATTENDEE_TYPE_REQUIRED, $this->attendeeTypeMap); } $_attendee = [ 'role' => $role, 'name' => $attendee->name != $attendee->email ? $attendee->name : '', 'email' => $attendee->email, ]; if (isset($attendee->attendeeType) && $attendee->attendeeType == self::ATTENDEE_TYPE_RESOURCE) { $_attendee['cutype'] = 'RESOURCE'; } if (isset($attendee->attendeeStatus)) { $_attendee['status'] = $attendee->attendeeStatus ? array_search($attendee->attendeeStatus, $this->attendeeStatusMap) : null; if (!$_attendee['status']) { $_attendee['status'] = 'NEEDS-ACTION'; $_attendee['rsvp'] = true; } } elseif (!empty($event['attendees']) && !empty($attendee->email)) { // copy the old attendee status foreach ($event['attendees'] as $old_attendee) { if ($old_attendee['email'] == $_attendee['email'] && isset($old_attendee['status'])) { $_attendee['status'] = $old_attendee['status']; $_attendee['rsvp'] = $old_attendee['rsvp']; break; } } } $attendees[] = $_attendee; } } // Outlook does not send the correct attendee status when changing between accepted and tentative, but it toggles the busyStatus. if ($is_outlook || $is_android) { $status = null; if ($data->busyStatus == self::BUSY_STATUS_BUSY) { $status = "ACCEPTED"; } elseif ($data->busyStatus == self::BUSY_STATUS_TENTATIVE) { $status = "TENTATIVE"; } if ($status) { $this->logger->debug("Updating our attendee status based on the busy status to {$status}."); $emails = $this->user_emails(); $this->find_and_update_attendee_status($attendees, $status, $emails); } } if (!$is_exception) { // Make sure the event has the organizer set if (!$organizer_email && ($identity = kolab_sync::get_instance()->user->get_identity())) { $attendees[] = [ 'role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], ]; } // recurrence (and exceptions) $event['recurrence'] = $this->recurrence_to_kolab($data, $folderid, $timezone); } $event['attendees'] = $attendees; $event['categories'] = $categories; $event['exceptions'] = $event['recurrence']['EXCEPTIONS'] ?? []; // Bump SEQUENCE number on update (Outlook only). // It's been confirmed that any change of the event that has attendees specified // bumps SEQUENCE number of the event (we can see this in sent iTips). // Unfortunately Outlook also sends an update when no SEQUENCE bump // is needed, e.g. when updating attendee status. // We try our best to bump the SEQUENCE only when expected // @phpstan-ignore-next-line if (!empty($entry) && !$is_exception && !empty($data->attendees) && $data->timezone != $dummy_tz) { if ($last_update = $this->getKolabDataItem($event, self::KEY_DTSTAMP)) { $last_update = new DateTime($last_update); } if (!empty($data->dtStamp) && $data->dtStamp != $last_update) { if ($this->has_significant_changes($event, $entry)) { $event['sequence']++; $this->logger->debug('Found significant changes in the updated event. Bumping SEQUENCE to ' . $event['sequence']); } } } // Because we use last event modification time above, we make sure // the event modification time is not (re)set by the server, // we use the original Outlook's timestamp. if ($is_outlook && !empty($data->dtStamp)) { $this->setKolabDataItem($event, self::KEY_DTSTAMP, $data->dtStamp->format(DateTime::ATOM)); } // This prevents kolab_format code to bump the sequence when not needed if (!isset($event['sequence'])) { $event['sequence'] = 0; } return $event; } /** * Set attendee status for meeting * * @param Syncroton_Model_MeetingResponse $request The meeting response * * @return string ID of new calendar entry */ public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request) { $status_map = [ 1 => 'ACCEPTED', 2 => 'TENTATIVE', 3 => 'DECLINED', ]; $status = $status_map[$request->userResponse] ?? null; if (empty($status)) { throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR); } // extract event from the invitation [$event, $existing] = $this->get_event_from_invitation($request); /* switch ($status) { case 'ACCEPTED': $event['free_busy'] = 'busy'; break; case 'TENTATIVE': $event['free_busy'] = 'tentative'; break; case 'DECLINED': $event['free_busy'] = 'free'; break; } */ // Store response timestamp for further use $reply_time = new DateTime('now', new DateTimeZone('UTC')); $this->setKolabDataItem($event, self::KEY_REPLYTIME, $reply_time->format('Ymd\THis\Z')); // Update/Save the event if (empty($existing)) { $folderId = $this->save_event($event, $status); // Create SyncState for the new event, so it is not synced twice if ($folderId) { try { $syncBackend = Syncroton_Registry::getSyncStateBackend(); $folderBackend = Syncroton_Registry::getFolderBackend(); $contentBackend = Syncroton_Registry::getContentStateBackend(); $syncFolder = $folderBackend->getFolder($this->device->id, $folderId); $syncState = $syncBackend->getSyncState($this->device->id, $syncFolder->id); $contentBackend->create(new Syncroton_Model_Content([ 'device_id' => $this->device->id, 'folder_id' => $syncFolder->id, 'contentid' => $this->serverId($event['uid'], $folderId), 'creation_time' => $syncState->lastsync, 'creation_synckey' => $syncState->counter, ])); } catch (Exception $e) { // ignore } } } else { $folderId = $this->update_event($event, $existing, $status, $request->instanceId); } if (!$folderId) { throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR); } // TODO: ActiveSync version >= 16, send the iTip response. if (isset($request->sendResponse)) { // SendResponse can contain Body to use as email body (can be empty) // TODO: Activesync >= 16.1 proposedStartTime and proposedEndTime. } // FIXME: We should not return an UID when status=DECLINED // as it's expected by the specification. Server // should delete an event in such a case, but we // keep the event copy with appropriate attendee status instead. return $this->serverId($event['uid'], $folderId); } /** * Process an event from an iTip message - update the event in the recipient's calendar * * @param array $event Event data from the iTip * * @return string|null Attendee status from the iTip (self::ITIP_* constant value) */ public function processItipReply($event) { // FIXME: This does not prevent from spoofing, i.e. an iTip message // could be sent by anyone impersonating an organizer or attendee // FIXME: This will not work with Kolab delegation, as we do look // for the event instance in personal folders only (for now) // We also do not use SENT-BY,DELEGATED-TO,DELEGATED-FROM here at all. // FIXME: This is potential performance problem - we update an event // whenever we sync an email message. User can have multiple AC clients // or many iTip messages in INBOX. Should we remember which email was // already processed? // FIXME: Should we check SEQUENCE or something else to prevent // overwriting the attendee status with outdated status (on REPLY)? // Here we're handling CANCEL message, find the event (or occurrence) and remove it if ($event['_method'] == 'CANCEL') { // TODO: Performance: When we're going to delete the event we don't have to fetch it, // we just need to find that it exists and in which folder. if ($existing = $this->find_event_by_uid($event['uid'])) { // Note: Normally we'd just set the event status to canceled, but // ActiveSync clients do not understand that, we have to delete it if (!empty($event['recurrence_date'])) { // A single recurring event occurrence $rec_day = $event['recurrence_date']->format('Ymd'); // Remove the matching RDATE entry if (!empty($existing['recurrence']['RDATE'])) { foreach ($existing['recurrence']['RDATE'] as $j => $rdate) { if ($rdate->format('Ymd') == $rec_day) { unset($existing['recurrence']['RDATE'][$j]); break; } } } // Check EXDATE list, maybe already cancelled if (!empty($existing['recurrence']['EXDATE'])) { foreach ($existing['recurrence']['EXDATE'] as $j => $exdate) { if ($exdate->format('Ymd') == $rec_day) { return self::ITIP_CANCELLED; // skip update } } } else { $existing['recurrence']['EXDATE'] = []; } if (!isset($existing['exceptions'])) { $existing['exceptions'] = []; } if (!empty($existing['exceptions'])) { foreach ($existing['exceptions'] as $i => $exception) { if (libcalendaring::is_recurrence_exception($event, $exception)) { unset($existing['exceptions'][$i]); } } } // Add an exception to the master event $existing['recurrence']['EXDATE'][] = $event['recurrence_date']; // TODO: Handle errors $this->save_event($existing, null); } else { $folder = $this->backend->getFolder($existing['folderId'], $this->device->deviceid, $this->modelName); if ($folder && $folder->valid) { // TODO: Handle errors $folder->delete($event['uid']); } } } return self::ITIP_CANCELLED; } // Here we're handling REPLY message if (empty($event['attendees']) || $event['_method'] != 'REPLY') { return null; } $attendeeStatus = null; $attendeeEmail = null; // Get the attendee/status foreach ($event['attendees'] as $attendee) { if (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') { if (!empty($attendee['email']) && !empty($attendee['status'])) { // Per iTip spec. there should be only one (non-organizer) attendee here // FIXME: Verify is it realy the case with e.g. Kolab webmail, If not, we should // probably use the message sender from the From: header $attendeeStatus = strtoupper($attendee['status']); $attendeeEmail = $attendee['email']; break; } } } // Find the event (or occurrence) and update it if ($attendeeStatus && ($existing = $this->find_event_by_uid($event['uid']))) { // TODO: We should probably check the SEQUENCE to not reset status to an outdated value if (!empty($event['recurrence_date'])) { // A single recurring event occurrence // Find the exception entry, it should exist, if not ignore if (!empty($existing['exceptions'])) { foreach ($existing['exceptions'] as $i => $exception) { if (!empty($exception['attendees']) && libcalendaring::is_recurrence_exception($event, $exception)) { $attendees = &$existing['exceptions'][$i]['attendees']; break; } } } } elseif (!empty($existing['attendees'])) { $attendees = &$existing['attendees']; } if (isset($attendees)) { $found = $this->find_and_update_attendee_status($attendees, $attendeeStatus, [$attendeeEmail], $changed); if ($found && $changed) { // TODO: error handling $this->save_event($existing, null); } } } return $attendeeStatus; } /** * Get an event from the invitation email or calendar folder */ protected function get_event_from_invitation(Syncroton_Model_MeetingResponse $request) { // Limitation: LongId might be used instead of RequestId, this is not supported if ($request->requestId) { $mail_class = new kolab_sync_data_email($this->device, $this->syncTimeStamp); // Event from an invitation email if ($event = $mail_class->get_invitation_event($request->requestId)) { // find the event in calendar $existing = $this->find_event_by_uid($event['uid']); return [$event, $existing]; } // Event from calendar folder if ($event = $this->getObject($request->collectionId, $request->requestId)) { return [$event, $event]; } throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); } throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR); } /** * Find the Kolab event in any (of subscribed personal calendars) folder */ protected function find_event_by_uid($uid) { if (empty($uid)) { return; } // TODO: should we check every existing event folder even if not subscribed for sync? if ($folders = $this->listFolders()) { foreach ($folders as $_folder) { $folder = $this->backend->getFolder($_folder['serverId'], $this->device->deviceid, $this->modelName); if ($folder && $folder->get_namespace() == 'personal' && ($result = $this->backend->getItem($_folder['serverId'], $this->device->deviceid, $this->modelName, $uid)) ) { $result['folderId'] = $_folder['serverId']; return $result; } } } } /** * Wrapper to update an event object */ protected function update_event($event, $old, $status, $instanceId = null) { // TODO: instanceId - DateTime - of the exception to be processed, if not set process all occurrences if ($instanceId) { throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); } // A single recurring event occurrence if (!empty($event['recurrence_date'])) { $event['recurrence'] = []; if ($status) { $this->update_attendee_status($event, $status); $status = null; } if (!isset($old['exceptions'])) { $old['exceptions'] = []; } $existing = false; foreach ($old['exceptions'] as $i => $exception) { if (libcalendaring::is_recurrence_exception($event, $exception)) { $old['exceptions'][$i] = $event; $existing = true; } } // TODO: In case organizer first cancelled an occurrence and then invited // an attendee to the same date, and attendee accepts, we should remove EXDATE entry. // FIXME: We have to check with ActiveSync clients whether it is better // to have an exception with DECLINED attendee status, or an EXDATE entry if (!$existing) { $old['exceptions'][] = $event; } } // A main event update elseif (isset($event['sequence']) && $event['sequence'] > $old['sequence']) { // FIXME: Can we be smarter here? Should we update everything? What about e.g. new attendees? // And do we need to check the sequence? $props = ['start', 'end', 'title', 'description', 'location', 'free_busy']; foreach ($props as $prop) { if (isset($event[$prop])) { $old[$prop] = $event[$prop]; } } // Copy new custom properties if (!empty($event['x-custom'])) { foreach ($event['x-custom'] as $key => $val) { $old['x-custom'][$key] = $val; } } } // Updating an existing event is most-likely a response // to an iTip request with bumped SEQUENCE $old['sequence'] = ($old['sequence'] ?? 0) + 1; // Update the event return $this->save_event($old, $status); } /** * Save the Kolab event (create if not exist) * If an event does not exist it will be created in the default folder */ protected function save_event(&$event, $status = null) { $first = null; $default = null; if (!isset($event['folderId'])) { // Find the folder to which we'll save the event if ($folders = $this->listFolders()) { foreach ($folders as $_folder) { $folder = $this->backend->getFolder($_folder['serverId'], $this->device->deviceid, $this->modelName); if ($folder && $folder->get_namespace() == 'personal') { if ($_folder['type'] == 8) { $default = $_folder['serverId']; break; } if (!$first) { $first = $_folder['serverId']; } } } } // TODO: what if the user has no subscribed event folders for this device // should we use any existing event folder even if not subscribed for sync? } if ($status) { $this->update_attendee_status($event, $status); } // TODO: Free/busy trigger? $old_uid = isset($event['folderId']) ? $event['uid'] : null; $folder_id = $event['folderId'] ?? ($default ?? $first); $folder = $this->backend->getFolder($folder_id, $this->device->deviceid, $this->modelName); if (!empty($folder) && $folder->valid && $folder->save($event, $this->modelName, $old_uid)) { return $folder_id; } return false; } /** * Update the attendee status of the user matching $emails */ protected function find_and_update_attendee_status(&$attendees, $status, $emails, &$changed = false) { $found = false; foreach ((array) $attendees as $i => $attendee) { if (!empty($attendee['email']) && (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') && in_array_nocase($attendee['email'], $emails) ) { $changed = $changed || ($status != ($attendee['status'] ?? '')); $attendees[$i]['status'] = $status; $attendees[$i]['rsvp'] = false; $this->logger->debug('Updating existing attendee: ' . $attendee['email'] . ' status: ' . $status); $found = true; } } return $found; } /** * Update the attendee status of the user */ protected function update_attendee_status(&$event, $status) { $emails = $this->user_emails(); if (!$this->find_and_update_attendee_status($event['attendees'], $status, $emails)) { $this->logger->debug('Adding new attendee ' . $emails[0] . ' status: ' . $status); // Add the user to the attendees list $event['attendees'][] = [ 'role' => 'OPT-PARTICIPANT', 'name' => '', 'email' => $emails[0], 'status' => $status, 'rsvp' => false, ]; } } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { $filter = [['type', '=', $this->modelName]]; switch ($filter_type) { case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK: $mod = '-2 weeks'; break; case Syncroton_Command_Sync::FILTER_1_MONTH_BACK: $mod = '-1 month'; break; case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK: $mod = '-3 months'; break; case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK: $mod = '-6 months'; break; } if (!empty($mod)) { $dt = new DateTime('now', new DateTimeZone('UTC')); $dt->modify($mod); $filter[] = ['dtend', '>', $dt]; } return $filter; } /** * Set MeetingStatus according to event data */ protected function meeting_status_from_kolab($event, &$result) { // 0 - The event is an appointment, which has no attendees. // 1 - The event is a meeting and the user is the meeting organizer. // 3 - This event is a meeting, and the user is not the meeting organizer. // 5 - The meeting has been canceled and the user was the meeting organizer. // 7 - The meeting has been canceled. The user was not the meeting organizer. $status = 0; if (!empty($event['attendees'])) { // Find out if the user is an organizer // TODO: Delegation/aliases support $user_emails = $this->user_emails(); $is_organizer = false; if ($event['organizer'] && $event['organizer']['email']) { $is_organizer = in_array_nocase($event['organizer']['email'], $user_emails); } if ($event['status'] == 'CANCELLED') { $status = $is_organizer ? 5 : 7; } else { $status = $is_organizer ? 1 : 3; } } $result['meetingStatus'] = $status; } /** * Converts libkolab alarms spec. into a number of minutes */ protected function from_kolab_alarm($event) { if (isset($event['valarms'])) { foreach ($event['valarms'] as $alarm) { if (in_array($alarm['action'], ['DISPLAY', 'AUDIO'])) { $value = $alarm['trigger']; break; } } } if (!empty($value) && $value instanceof DateTime) { if (!empty($event['start']) && ($interval = $event['start']->diff($value))) { if ($interval->invert && !$interval->m && !$interval->y) { return intval(round($interval->s / 60) + $interval->i + $interval->h * 60 + $interval->d * 60 * 24); } } } elseif (!empty($value) && preg_match('/^([-+]*)[PT]*([0-9]+)([WDHMS])$/', $value, $matches)) { $value = intval($matches[2]); if ($value && $matches[1] != '-') { return null; } switch ($matches[3]) { case 'S': $value = intval(round($value / 60)); break; case 'H': $value *= 60; break; case 'D': $value *= 24 * 60; break; case 'W': $value *= 7 * 24 * 60; break; } return $value; } } /** * Converts ActiveSync reminder into libkolab alarms spec. */ protected function to_kolab_alarm($value, $event) { if ($value === null || $value === '') { return isset($event['valarms']) ? (array) $event['valarms'] : []; } $valarms = []; $unsupported = []; if (!empty($event['valarms'])) { foreach ($event['valarms'] as $alarm) { if (empty($current) && in_array($alarm['action'], ['DISPLAY', 'AUDIO'])) { $current = $alarm; } else { $unsupported[] = $alarm; } } } $valarms[] = [ 'action' => !empty($current['action']) ? $current['action'] : 'DISPLAY', 'description' => !empty($current['description']) ? $current['description'] : '', 'trigger' => sprintf('-PT%dM', $value), ]; if (!empty($unsupported)) { $valarms = array_merge($valarms, $unsupported); } return $valarms; } /** * Sanity checks on event input * - * @param Syncroton_Model_IEntry &$entry Entry object + * @param Syncroton_Model_Event|Syncroton_Model_EventException &$entry Entry object * * @throws Syncroton_Exception_Status_Sync */ protected function check_event(Syncroton_Model_IEntry &$entry) { // https://msdn.microsoft.com/en-us/library/jj194434(v=exchg.80).aspx $now = new DateTime('now'); $rounded = new DateTime('now'); $min = (int) $rounded->format('i'); $add = $min > 30 ? (60 - $min) : (30 - $min); $rounded->add(new DateInterval('PT' . $add . 'M')); if (empty($entry->startTime) && empty($entry->endTime)) { // use current time rounded to 30 minutes $end = clone $rounded; $end->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M')); $entry->startTime = $rounded; $entry->endTime = $end; } elseif (empty($entry->startTime)) { if ($entry->endTime < $now || $entry->endTime < $rounded) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM); } $entry->startTime = $rounded; } elseif (empty($entry->endTime)) { if ($entry->startTime < $now) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM); } $rounded->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M')); $entry->endTime = $rounded; } } /** * Check if the new event version has any significant changes */ protected function has_significant_changes($event, $old) { // Calendar namespace fields foreach (['allday', 'start', 'end', 'location', 'recurrence'] as $key) { if (($event[$key] ?? null) != ($old[$key] ?? null)) { // Comparing recurrence is tricky as there can be differences in default // value handling. Let's try to handle most common cases if ($key == 'recurrence' && $this->fixed_recurrence($event) == $this->fixed_recurrence($old)) { continue; } return true; } } if (count($event['attendees']) != count($old['attendees'])) { return true; } foreach ($event['attendees'] as $idx => $attendee) { $old_attendee = $old['attendees'][$idx]; if ($old_attendee['email'] != $attendee['email'] || ($attendee['role'] != 'ORGANIZER' && $attendee['status'] != $old_attendee['status'] && $attendee['status'] == 'NEEDS-ACTION') ) { return true; } } return false; } /** * Unify recurrence spec. for comparison */ protected function fixed_recurrence($event) { $rec = (array) $event['recurrence']; // Add BYDAY if not exists if ($rec['FREQ'] == 'WEEKLY' && empty($rec['BYDAY'])) { $days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; $day = $event['start']->format('w'); $rec['BYDAY'] = $days[$day]; } if (!$rec['INTERVAL']) { $rec['INTERVAL'] = 1; } ksort($rec); return $rec; } } diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php index 312bb1b..c9fc819 100644 --- a/lib/kolab_sync_data_email.php +++ b/lib/kolab_sync_data_email.php @@ -1,1566 +1,1566 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Email data class for Syncroton */ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_IDataSearch { public const MAX_SEARCH_RESULT = 200; /** * Mapping from ActiveSync Email namespace fields */ protected $mapping = [ 'cc' => 'cc', //'contentClass' => 'contentclass', 'dateReceived' => 'internaldate', //'displayTo' => 'displayto', //? //'flag' => 'flag', 'from' => 'from', //'importance' => 'importance', 'internetCPID' => 'charset', //'messageClass' => 'messageclass', 'replyTo' => 'replyto', //'read' => 'read', 'subject' => 'subject', //'threadTopic' => 'threadtopic', 'to' => 'to', ]; public static $memory_accumulated = 0; /** * Special folder type/name map * * @var array */ protected $folder_types = [ 2 => 'Inbox', 3 => 'Drafts', 4 => 'Deleted Items', 5 => 'Sent Items', 6 => 'Outbox', ]; /** * Kolab object type * * @var string */ protected $modelName = 'mail'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_INBOX; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'INBOX'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED; protected $storage; /** * the constructor * * @param Syncroton_Model_IDevice $device * @param DateTime $syncTimeStamp */ public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp) { parent::__construct($device, $syncTimeStamp); $this->storage = rcube::get_instance()->get_storage(); // Outlook 2013 support multi-folder $this->ext_devices[] = 'windowsoutlook15'; } /** * Encode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3 * * @param array $data An array with the data to encode * * @return string the encoded globalObjId */ public static function encodeGlobalObjId(array $data): string { $classid = "040000008200e00074c5b7101a82e008"; if (!empty($data['data'])) { $payload = $data['data']; } else { $uid = $data['uid']; $payload = "vCal-Uid\1\0\0\0{$uid}\0"; } $packed = pack( "H32nCCPx8Va*", $classid, $data['year'] ?? 0, $data['month'] ?? 0, $data['day'] ?? 0, $data['now'] ?? 0, strlen($payload), $payload ); return base64_encode($packed); } /** * Decode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3 * * @param string $globalObjId The encoded globalObjId * * @return array An array with the decoded data */ public static function decodeGlobalObjId(string $globalObjId): array { $unpackString = 'H32classid/nyear/Cmonth/Cday/Pnow/x8/Vbytecount/a*data'; $decoded = unpack($unpackString, base64_decode($globalObjId)); $decoded['uid'] = substr($decoded['data'], strlen("vCal-Uid\1\0\0\0"), -1); return $decoded; } /** * Creates model object * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * * @return Syncroton_Model_Email Email object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId) { $message = $this->getObject($serverId); // error (message doesn't exist?) if (empty($message)) { throw new Syncroton_Exception_NotFound("Message $serverId not found"); } $headers = $message->headers; // rcube_message_header $this->storage->set_folder($message->folder); $this->logger->debug(sprintf("Processing message %s (size: %.2f MB)", $serverId, $headers->size / 1024 / 1024)); // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = null; switch ($name) { case 'internaldate': $value = self::date_from_kolab(rcube_utils::strtotime($headers->internaldate)); break; case 'cc': case 'to': case 'replyto': case 'from': $addresses = rcube_mime::decode_address_list($headers->$name, null, true, $headers->charset); foreach ($addresses as $idx => $part) { // @FIXME: set name + address or address only? $addresses[$idx] = format_email_recipient($part['mailto'], $part['name']); } $value = implode(',', $addresses); break; case 'subject': $value = $headers->get('subject'); break; case 'charset': $value = self::charset_to_cp($headers->charset); break; } if (empty($value) || is_array($value)) { continue; } if (is_string($value)) { $value = rcube_charset::clean($value); } $result[$key] = $value; } // $result['ConversationId'] = 'FF68022058BD485996BE15F6F6D99320'; // $result['ConversationIndex'] = 'CA2CFA8A23'; // Read flag $result['read'] = intval(!empty($headers->flags['SEEN'])); // Flagged message if (!empty($headers->flags['FLAGGED'])) { // Use FollowUp flag which is used in Android when message is marked with a star $result['flag'] = new Syncroton_Model_EmailFlag([ 'flagType' => 'FollowUp', 'status' => Syncroton_Model_EmailFlag::STATUS_ACTIVE, ]); } else { $result['flag'] = new Syncroton_Model_EmailFlag(); } // Importance/Priority if ($headers->priority) { if ($headers->priority < 3) { $result['importance'] = 2; // High } elseif ($headers->priority > 3) { $result['importance'] = 0; // Low } } // get truncation and body type $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT; $truncateAt = null; $opts = $collection->options; $prefs = $opts['bodyPreferences']; if ($opts['mimeSupport'] == Syncroton_Command_Sync::MIMESUPPORT_SEND_MIME) { $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_MIME; if (isset($prefs[Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize'])) { $truncateAt = $prefs[Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize']; } elseif (isset($opts['mimeTruncation']) && $opts['mimeTruncation'] < Syncroton_Command_Sync::TRUNCATE_NOTHING) { switch ($opts['mimeTruncation']) { case Syncroton_Command_Sync::TRUNCATE_ALL: $truncateAt = 0; break; case Syncroton_Command_Sync::TRUNCATE_4096: $truncateAt = 4096; break; case Syncroton_Command_Sync::TRUNCATE_5120: $truncateAt = 5120; break; case Syncroton_Command_Sync::TRUNCATE_7168: $truncateAt = 7168; break; case Syncroton_Command_Sync::TRUNCATE_10240: $truncateAt = 10240; break; case Syncroton_Command_Sync::TRUNCATE_20480: $truncateAt = 20480; break; case Syncroton_Command_Sync::TRUNCATE_51200: $truncateAt = 51200; break; case Syncroton_Command_Sync::TRUNCATE_102400: $truncateAt = 102400; break; } } } else { // The spec is not very clear, but it looks that if MimeSupport is not set // we can't add Syncroton_Command_Sync::BODY_TYPE_MIME to the supported types // list below (Bug #1688) $types = [ Syncroton_Command_Sync::BODY_TYPE_HTML, Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT, ]; // @TODO: if client can support both HTML and TEXT use one of // them which is better according to the real message body type foreach ($types as $type) { if (!empty($prefs[$type])) { if (!empty($prefs[$type]['truncationSize'])) { $truncateAt = $prefs[$type]['truncationSize']; } $preview = (int) ($prefs[$type]['preview'] ?? 0); $airSyncBaseType = $type; break; } } } $body_params = ['type' => $airSyncBaseType]; // Message body // In Sync examples there's one in which bodyPreferences is not defined // in such case Truncated=1 and there's no body sent to the client // only it's estimated size $isTruncated = 0; if (empty($prefs)) { $messageBody = ''; $real_length = $headers->size; $truncateAt = 0; $body_length = 0; $isTruncated = 1; } elseif ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) { // Check if we have enough memory to handle the message $messageBody = $this->message_mem_check($message, $headers->size); static::$memory_accumulated += $headers->size; if (empty($messageBody)) { $messageBody = $this->storage->get_raw_body($message->uid); } // make the source safe (Bug #2715, #2757) $messageBody = kolab_sync_message::recode_message($messageBody); // strip out any non utf-8 characters $messageBody = rcube_charset::clean($messageBody); $real_length = $body_length = strlen($messageBody); } else { $messageBody = $this->getMessageBody($message, $airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_HTML); // strip out any non utf-8 characters $messageBody = rcube_charset::clean($messageBody); $real_length = $body_length = strlen($messageBody); } // add Preview element to the Body result if (!empty($preview) && $body_length) { $body_params['preview'] = $this->getPreview($messageBody, $airSyncBaseType, $preview); } // truncate the body if needed if ($truncateAt && $body_length > $truncateAt) { $messageBody = mb_strcut($messageBody, 0, $truncateAt); $body_length = strlen($messageBody); $isTruncated = 1; } if ($isTruncated) { $body_params['truncated'] = 1; $body_params['estimatedDataSize'] = $real_length; } // add Body element to the result $result['body'] = $this->setBody($messageBody, $body_params); // original body type // @TODO: get this value from getMessageBody() $result['nativeBodyType'] = $message->has_html_part() ? 2 : 1; // Message class $result['messageClass'] = 'IPM.Note'; $result['contentClass'] = 'urn:content-classes:message'; if ($headers->ctype == 'multipart/signed' && !empty($message->parts[1]) && $message->parts[1]->mimetype == 'application/pkcs7-signature' ) { $result['messageClass'] = 'IPM.Note.SMIME.MultipartSigned'; } elseif ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') { $result['messageClass'] = 'IPM.Note.SMIME'; } elseif ($event = $this->get_invitation_event_from_message($message)) { // Note: Depending on MessageClass a client will display a proper set of buttons // Either Accept/Maybe/Decline (REQUEST), or "Remove from Calendar" (CANCEL) or none (REPLY). $result['messageClass'] = 'IPM.Schedule.Meeting.Request'; $result['contentClass'] = 'urn:content-classes:calendarmessage'; $meeting = []; $meeting['allDayEvent'] = $event['allday'] ?? null ? 1 : 0; $meeting['startTime'] = self::date_from_kolab($event['start']); $meeting['dtStamp'] = self::date_from_kolab($event['dtstamp'] ?? null); $meeting['endTime'] = self::date_from_kolab($event['end'] ?? null); $meeting['location'] = $event['location'] ?? null; $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_NORMAL; if (!empty($event['recurrence_date'])) { $meeting['recurrenceId'] = self::date_from_kolab($event['recurrence_date']); if (!empty($event['status']) && $event['status'] == 'CANCELLED') { $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_EXCEPTION; } else { $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_SINGLE; } } elseif (!empty($event['recurrence'])) { $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_MASTER; // TODO: MeetingRequest recurrence is different that the one in Calendar // $this->recurrence_from_kolab($collection, $event, $meeting); } // Organizer if (!empty($event['attendees'])) { foreach ($event['attendees'] as $attendee) { if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER' && !empty($attendee['email'])) { $meeting['organizer'] = $attendee['email']; break; } } } // Current time as a number of 100-nanosecond units since 1601-01-01 $fileTime = ($event['start']->getTimestamp() + 11644473600) * 10000000; // Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows // only one timezone per-event. We'll use timezone of the start date $meeting['timeZone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']); $meeting['globalObjId'] = self::encodeGlobalObjId([ 'uid' => $event['uid'], 'year' => intval($event['start']->format('Y')), 'month' => intval($event['start']->format('n')), 'day' => intval($event['start']->format('j')), 'now' => $fileTime, ]); if ($event['_method'] == 'REQUEST') { $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_REQUEST; // Some clients (iOS) without this flag do not send the invitation reply to the organizer. // Note: Microsoft says "the value of the ResponseRequested element comes from the PARTSTAT // parameter value of "NEEDS-ACTION" in the request". I think it is safe to do this for all requests. // Note: This does not have impact on the existence of Accept/Decline buttons in the client. $meeting['responseRequested'] = 1; } else { // REPLY or CANCEL $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_NORMAL; $itip_processing = kolab_sync::get_instance()->config->get('activesync_itip_processing'); $attendeeStatus = null; if ($itip_processing && empty($headers->flags['SEEN'])) { // Optionally process the message and update the event in recipient's calendar // Warning: Only for development purposes, for now it's better to use wallace $calendar_class = new kolab_sync_data_calendar($this->device, $this->syncTimeStamp); $attendeeStatus = $calendar_class->processItipReply($event); } elseif ($event['_method'] == 'CANCEL') { $attendeeStatus = kolab_sync_data_calendar::ITIP_CANCELLED; } elseif (!empty($event['attendees'])) { // Get the attendee/status in the REPLY foreach ($event['attendees'] as $attendee) { if (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') { if (!empty($attendee['email']) && !empty($attendee['status'])) { // Per iTip spec. there should be only one (non-organizer) attendee here // FIXME: Verify is it realy the case with e.g. Kolab webmail, If not, we should // probably use the message sender from the From: header $attendeeStatus = strtoupper($attendee['status']); break; } } } } switch ($attendeeStatus) { case kolab_sync_data_calendar::ITIP_CANCELLED: $result['messageClass'] = 'IPM.Schedule.Meeting.Canceled'; break; case kolab_sync_data_calendar::ITIP_DECLINED: $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Neg'; break; case kolab_sync_data_calendar::ITIP_TENTATIVE: $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Tent'; break; case kolab_sync_data_calendar::ITIP_ACCEPTED: $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Pos'; break; default: $skipRequest = true; } } // New time proposals aren't supported by Kolab. // This disables the UI elements related to this on the client side $meeting['disallowNewTimeProposal'] = 1; if (empty($skipRequest)) { $result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting); } } // Categories (Tags) $result['categories'] = $message->headers->others['categories'] ?? []; $is_ios = preg_match('/(iphone|ipad)/i', $this->device->devicetype); // attachments $attachments = array_merge($message->attachments, $message->inline_parts); if (!empty($attachments)) { $result['attachments'] = []; foreach ($attachments as $attachment) { $att = []; if ($is_ios && !empty($event) && $attachment->mime_id == $event['_mime_id']) { continue; } $filename = rcube_charset::clean($attachment->filename); if (empty($filename) && $attachment->mimetype == 'text/html') { $filename = 'HTML Part'; } $att['displayName'] = $filename; $att['fileReference'] = $serverId . '::' . $attachment->mime_id; $att['method'] = 1; $att['estimatedDataSize'] = $attachment->size; if (!empty($attachment->content_id)) { $att['contentId'] = rcube_charset::clean($attachment->content_id); } if (!empty($attachment->content_location)) { $att['contentLocation'] = rcube_charset::clean($attachment->content_location); } if (in_array($attachment, $message->inline_parts)) { $att['isInline'] = 1; } $result['attachments'][] = new Syncroton_Model_EmailAttachment($att); } } return new Syncroton_Model_Email($result); } /** * Returns properties of a message for Search response * * @param string $longId Message identifier * @param array $options Search options * * @return Syncroton_Model_Email Email object */ public function getSearchEntry($longId, $options) { $collection = new Syncroton_Model_SyncCollection([ 'options' => $options, ]); return $this->getEntry($collection, $longId); } /** * convert email from xml to libkolab array * * @param Syncroton_Model_Email $data Email to convert * @param string $folderid Folder identifier * @param array $entry Existing entry * * @return array */ public function toKolab($data, $folderid, $entry = null) { // does nothing => you can't add emails via ActiveSync return []; } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { $filter = []; switch ($filter_type) { case Syncroton_Command_Sync::FILTER_1_DAY_BACK: $mod = '-1 day'; break; case Syncroton_Command_Sync::FILTER_3_DAYS_BACK: $mod = '-3 days'; break; case Syncroton_Command_Sync::FILTER_1_WEEK_BACK: $mod = '-1 week'; break; case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK: $mod = '-2 weeks'; break; case Syncroton_Command_Sync::FILTER_1_MONTH_BACK: $mod = '-1 month'; break; } if (!empty($mod)) { $dt = new DateTime('now', new DateTimeZone('UTC')); $dt->modify($mod); // RFC3501: IMAP SEARCH $filter[] = 'SINCE ' . $dt->format('d-M-Y'); } return $filter; } /** * Return list of supported folders for this backend * * @return array */ public function getAllFolders() { $list = $this->listFolders(); if (!is_array($list)) { throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR); } // device doesn't support multiple folders if (!$this->isMultiFolder()) { // We'll return max. one folder of supported type $result = []; $types = $this->folder_types; foreach ($list as $idx => $folder) { $type = $folder['type'] == 12 ? 2 : $folder['type']; // unknown to Inbox if ($folder_id = $types[$type]) { $result[$folder_id] = [ 'displayName' => $folder_id, 'serverId' => $folder_id, 'parentId' => 0, 'type' => $type, ]; } } $list = $result; } foreach ($list as $idx => $folder) { $list[$idx] = new Syncroton_Model_Folder($folder); } return $list; } /** * Return list of folders for specified folder ID * * @param string $folder_id Folder identifier * * @return array Folder identifiers list */ protected function extractFolders($folder_id) { $list = $this->listFolders(); $result = []; if (!is_array($list)) { throw new Syncroton_Exception_NotFound('Folder not found'); } // device supports multiple folders? if ($this->isMultiFolder()) { if ($list[$folder_id]) { $result[] = $folder_id; } } elseif ($type = array_search($folder_id, $this->folder_types)) { foreach ($list as $id => $folder) { if ($folder['type'] == $type || ($folder_id == 'Inbox' && $folder['type'] == 12)) { $result[] = $id; } } } if (empty($result)) { throw new Syncroton_Exception_NotFound('Folder not found'); } return $result; } /** * Moves object into another location (folder) * * @param string $srcFolderId Source folder identifier * @param string $serverId Object identifier * @param string $dstFolderId Destination folder identifier * * @throws Syncroton_Exception_Status * @return string New object identifier */ public function moveItem($srcFolderId, $serverId, $dstFolderId) { $msg = $this->parseMessageId($serverId); $dest = $this->extractFolders($dstFolderId); $dest_id = array_shift($dest); if (empty($msg)) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } $uid = $this->backend->moveItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], $dest_id); return $uid ? $this->serverId($uid, $dest_id) : null; } /** * add entry from xml data * - * @param string $folderId Folder identifier - * @param Syncroton_Model_IEntry $entry Entry + * @param string $folderId Folder identifier + * @param Syncroton_Model_Email $entry Entry * * @return string ID of the created entry */ public function createEntry($folderId, Syncroton_Model_IEntry $entry) { $params = ['flags' => [!empty($entry->read) ? 'SEEN' : 'UNSEEN']]; $uid = $this->backend->createItem($folderId, $this->device->deviceid, $this->modelName, $entry->body->data, $params); if (!$uid) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } return $this->serverId($uid, $folderId); } /** * Update existing message * * @param string $folderId Folder identifier * @param string $serverId Entry identifier * @param Syncroton_Model_IEntry $entry Entry */ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) { $msg = $this->parseMessageId($serverId); if (empty($msg)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } $params = ['flags' => []]; if (isset($entry->categories)) { $params['categories'] = $entry->categories; } // Read status change if (isset($entry->read)) { $params['flags'][] = !empty($entry->read) ? 'SEEN' : 'UNSEEN'; } // Flag change if (isset($entry->flag)) { if (empty($entry->flag) || empty($entry->flag->flagType)) { $params['flags'][] = 'UNFLAGGED'; } elseif (preg_match('/follow\s*up/i', $entry->flag->flagType)) { $params['flags'][] = 'FLAGGED'; } } $this->backend->updateItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], null, $params); return $serverId; } /** * Delete an email (or move to Trash) * * @param string $folderId * @param string $serverId * @param ?Syncroton_Model_SyncCollection $collection */ public function deleteEntry($folderId, $serverId, $collection = null) { $trash = kolab_sync::get_instance()->config->get('trash_mbox'); $msg = $this->parseMessageId($serverId); if (empty($msg)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } // Note: If DeletesAsMoves is not specified in the request, its default is 1 (true). $moveToTrash = !isset($collection->deletesAsMoves) || !empty($collection->deletesAsMoves); $deleted = $this->backend->deleteItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], $moveToTrash); if (!$deleted) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } } /** * Send an email * * @param mixed $message MIME message * @param boolean $saveInSent Enables saving the sent message in Sent folder * * @throws Syncroton_Exception_Status */ public function sendEmail($message, $saveInSent) { if (!($message instanceof kolab_sync_message)) { $message = new kolab_sync_message($message); } $sent = $message->send($smtp_error); if (!$sent) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAIL_SUBMISSION_FAILED); } // Save sent message in Sent folder if ($saveInSent) { $sent_folder = kolab_sync::get_instance()->config->get('sent_mbox'); if (strlen($sent_folder) && $this->storage->folder_exists($sent_folder)) { return $this->storage->save_message($sent_folder, $message->source(), '', false, ['SEEN']); } } } /** * Forward an email * * @param array|string $itemId A string LongId or an array with following properties: * collectionId, itemId and instanceId * @param resource|string $body MIME message * @param boolean $saveInSent Enables saving the sent message in Sent folder * @param boolean $replaceMime If enabled, original message would be appended * * @throws Syncroton_Exception_Status */ public function forwardEmail($itemId, $body, $saveInSent, $replaceMime) { /* @TODO: The SmartForward command can be applied to a meeting. When SmartForward is applied to a recurring meeting, the InstanceId element (section 2.2.3.83.2) specifies the ID of a particular occurrence in the recurring meeting. If SmartForward is applied to a recurring meeting and the InstanceId element is absent, the server SHOULD forward the entire recurring meeting. If the value of the InstanceId element is invalid, the server responds with Status element (section 2.2.3.162.15) value 104, as specified in section 2.2.4. When the SmartForward command is used for an appointment, the original message is included by the server as an attachment to the outgoing message. When the SmartForward command is used for a normal message or a meeting, the behavior of the SmartForward command is the same as that of the SmartReply command (section 2.2.2.18). */ $msg = $this->parseMessageId($itemId); $message = $this->getObject($itemId); if (empty($message)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND); } // Parse message $sync_msg = new kolab_sync_message($body); // forward original message as attachment if (!$replaceMime) { $this->storage->set_folder($message->folder); $attachment = $this->storage->get_raw_body($msg['uid']); if (empty($attachment)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND); } $sync_msg->add_attachment($attachment, [ 'encoding' => '8bit', 'content_type' => 'message/rfc822', 'disposition' => 'inline', //'name' => 'message.eml', ]); } // Send message $this->sendEmail($sync_msg, $saveInSent); // Set FORWARDED flag on the replied message if (empty($message->headers->flags['FORWARDED'])) { $params = ['flags' => ['FORWARDED']]; $this->backend->updateItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], null, $params); } } /** * Reply to an email * * @param array|string $itemId A string LongId or an array with following properties: * collectionId, itemId and instanceId * @param resource|string $body MIME message * @param boolean $saveInSent Enables saving the sent message in Sent folder * @param boolean $replaceMime If enabled, original message would be appended * * @throws Syncroton_Exception_Status */ public function replyEmail($itemId, $body, $saveInSent, $replaceMime) { $msg = $this->parseMessageId($itemId); $message = $this->getObject($itemId); if (empty($message)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND); } $sync_msg = new kolab_sync_message($body); $headers = $sync_msg->headers(); // Add References header if (empty($headers['References'])) { $sync_msg->set_header('References', trim($message->headers->references . ' ' . $message->headers->messageID)); } // Get original message body if (!$replaceMime) { // @TODO: here we're assuming that reply message is in text/plain format // So, original message will be converted to plain text if needed $message_body = $this->getMessageBody($message, false); // Quote original message body $message_body = self::wrap_and_quote(trim($message_body), 72); // Join bodies $sync_msg->append("\n" . ltrim($message_body)); } // Send message $this->sendEmail($sync_msg, $saveInSent); // Set ANSWERED flag on the replied message if (empty($message->headers->flags['ANSWERED'])) { $params = ['flags' => ['ANSWERED']]; $this->backend->updateItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], null, $params); } } /** * ActiveSync Search handler * * @param Syncroton_Model_StoreRequest $store Search query * * @return Syncroton_Model_StoreResponse Complete Search response */ public function search(Syncroton_Model_StoreRequest $store) { [$folders, $search_str] = $this->parse_search_query($store); if (empty($search_str)) { throw new Exception('Empty/invalid search request'); } if (!is_array($folders)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $result = []; // @TODO: caching with Options->RebuildResults support foreach ($folders as $folderid) { $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); if ($foldername === null) { continue; } // $this->storage->set_folder($foldername); // $this->storage->folder_sync($foldername); $search = $this->storage->search_once($foldername, $search_str); if (!($search instanceof rcube_result_index)) { continue; } $search->revert(); $uids = $search->get(); foreach ($uids as $idx => $uid) { $uids[$idx] = new Syncroton_Model_StoreResponseResult([ 'longId' => $this->serverId($uid, $folderid), 'collectionId' => $folderid, 'class' => 'Email', ]); } $result = array_merge($result, $uids); // We don't want to search all folders if we've got already a lot messages if (count($result) >= self::MAX_SEARCH_RESULT) { break; } } $result = array_values($result); $response = new Syncroton_Model_StoreResponse(); // Calculate requested range $start = (int) $store->options['range'][0]; $limit = (int) $store->options['range'][1] + 1; $total = count($result); $response->total = $total; // Get requested chunk of data set if ($total) { if ($start > $total) { $start = $total; } if ($limit > $total) { $limit = max($start + 1, $total); } if ($start > 0 || $limit < $total) { $result = array_slice($result, $start, $limit - $start); } $response->range = [$start, $start + count($result) - 1]; } // Build result array, convert to ActiveSync format foreach ($result as $idx => $rec) { $rec->properties = $this->getSearchEntry($rec->longId, $store->options); $response->result[] = $rec; unset($result[$idx]); } return $response; } /** * Converts ActiveSync search parameters into IMAP search string */ protected function parse_search_query($store) { $options = $store->options; $query = $store->query; $search_str = ''; $folders = []; if (empty($query) || !is_array($query)) { return []; } if (!empty($query['and']['collections'])) { foreach ($query['and']['collections'] as $collection) { $folders = array_merge($folders, $this->extractFolders($collection)); } } if (!empty($query['and']['greaterThan']) && !empty($query['and']['greaterThan']['dateReceived']) && !empty($query['and']['greaterThan']['value']) ) { $search_str .= ' SINCE ' . $query['and']['greaterThan']['value']->format('d-M-Y'); } if (!empty($query['and']['lessThan']) && !empty($query['and']['lessThan']['dateReceived']) && !empty($query['and']['lessThan']['value']) ) { $search_str .= ' BEFORE ' . $query['and']['lessThan']['value']->format('d-M-Y'); } if (isset($query['and']['freeText']) && strlen($query['and']['freeText'])) { // @FIXME: Should we use TEXT/BODY search? ActiveSync protocol specification says "indexed fields" $search = $query['and']['freeText']; $search_keys = ['SUBJECT', 'TO', 'FROM', 'CC']; $search_str .= str_repeat(' OR', count($search_keys) - 1); foreach ($search_keys as $key) { $search_str .= sprintf(" %s {%d}\r\n%s", $key, strlen($search), $search); } } if (!strlen($search_str)) { return []; } $search_str = 'ALL UNDELETED ' . trim($search_str); // @TODO: DeepTraversal if (empty($folders)) { $folders = $this->listFolders(); if (!is_array($folders)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $folders = array_keys($folders); } return [$folders, $search_str]; } /** * Fetches the entry from the backend */ protected function getObject($entryid, $dummy = null) { $message = $this->parseMessageId($entryid); if (empty($message)) { // @TODO: exception? return null; } return $this->backend->getItem($message['folderId'], $this->device->deviceid, $this->modelName, $message['uid']); } /** * Get attachment data from the server. * * @param string $fileReference * * @return Syncroton_Model_FileReference */ public function getFileReference($fileReference) { [$folderid, $uid, $part_id] = explode('::', $fileReference); $message = $this->getObject($fileReference); if (!$message) { throw new Syncroton_Exception_NotFound('Message not found'); } $part = $message->mime_parts[$part_id]; $body = $message->get_part_body($part_id); return new Syncroton_Model_FileReference([ 'contentType' => $part->mimetype, 'data' => $body, ]); } /** * Parses entry ID to get folder name and UID of the message */ protected function parseMessageId($entryid) { // replyEmail/forwardEmail if (is_array($entryid)) { $entryid = $entryid['itemId']; } if (!is_string($entryid) || !strpos($entryid, '::')) { return; } // Note: the id might be in a form of ::[::] [$folderid, $uid] = explode('::', $entryid); return [ 'uid' => $uid, 'folderId' => $folderid, ]; } /** * Creates entry ID of the message */ protected function serverId($uid, $folderid) { return $folderid . '::' . $uid; } /** * Returns body of the message in specified format * * @param rcube_message $message * @param bool $html */ protected function getMessageBody($message, $html = false) { if (!is_array($message->parts) && empty($message->body)) { return ''; } if (!empty($message->parts)) { foreach ($message->parts as $part) { // skip no-content and attachment parts (#1488557) if ($part->type != 'content' || !$part->size || $message->is_attachment($part)) { continue; } return $this->getMessagePartBody($message, $part, $html); } } return $this->getMessagePartBody($message, $message, $html); } /** * Returns body of the message part in specified format * * @param rcube_message $message * @param rcube_message_part $part * @param bool $html */ protected function getMessagePartBody($message, $part, $html = false) { if (empty($part->size) || empty($part->mime_id)) { // TODO: Throw an exception? return ''; } // Check if we have enough memory to handle the message in it $body = $this->message_mem_check($message, $part->size, false); if ($body !== false) { $body = $message->get_part_body($part->mime_id, true); } // message is cached but not exists, or other error if ($body === false) { return ''; } $ctype_secondary = !empty($part->ctype_secondary) ? $part->ctype_secondary : null; if ($html) { if ($ctype_secondary == 'html') { // charset was converted to UTF-8 in rcube_storage::get_message_part(), // change/add charset specification in HTML accordingly $meta = ''; // remove old meta tag and add the new one, making sure // that it is placed in the head $body = preg_replace('/]+charset=[a-z0-9-_]+[^>]*>/Ui', '', $body); $body = preg_replace('/(]*>)/Ui', '\\1' . $meta, $body, -1, $rcount); if (!$rcount) { $body = '' . $meta . '' . $body; } } elseif ($ctype_secondary == 'enriched') { $body = rcube_enriched::to_html($body); } else { // Roundcube >= 1.2 if (class_exists('rcube_text2html')) { $flowed = isset($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed'; $delsp = isset($part->ctype_parameters['delsp']) && $part->ctype_parameters['delsp'] == 'yes'; $options = ['flowed' => $flowed, 'wrap' => false, 'delsp' => $delsp]; $text2html = new rcube_text2html($body, false, $options); $body = '' . $text2html->get_html() . ''; } else { $body = '
' . $body . '
'; } } } else { if ($ctype_secondary == 'enriched') { $body = rcube_enriched::to_html($body); $part->ctype_secondary = 'html'; } if ($ctype_secondary == 'html') { $txt = new rcube_html2text($body, false, true); $body = $txt->get_text(); } else { if ($ctype_secondary == 'plain' && !empty($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed' ) { $body = rcube_mime::unfold_flowed($body); } } } return $body; } /** * Converts and truncates message body for use in * * @return string Truncated plain text message */ protected function getPreview($body, $type, $size) { if ($type == Syncroton_Command_Sync::BODY_TYPE_HTML) { $txt = new rcube_html2text($body, false, true); $body = $txt->get_text(); } // size limit defined in ActiveSync protocol if ($size > 255) { $size = 255; } return mb_strcut(trim($body), 0, $size); } public static function charset_to_cp($charset) { // @TODO: ????? // The body is converted to utf-8 in get_part_body(), what about headers? return 65001; // UTF-8 /* $aliases = array( 'asmo708' => 708, 'shiftjis' => 932, 'gb2312' => 936, 'ksc56011987' => 949, 'big5' => 950, 'utf16' => 1200, 'utf16le' => 1200, 'unicodefffe' => 1201, 'utf16be' => 1201, 'johab' => 1361, 'macintosh' => 10000, 'macjapanese' => 10001, 'macchinesetrad' => 10002, 'mackorean' => 10003, 'macarabic' => 10004, 'machebrew' => 10005, 'macgreek' => 10006, 'maccyrillic' => 10007, 'macchinesesimp' => 10008, 'macromanian' => 10010, 'macukrainian' => 10017, 'macthai' => 10021, 'macce' => 10029, 'macicelandic' => 10079, 'macturkish' => 10081, 'maccroatian' => 10082, 'utf32' => 12000, 'utf32be' => 12001, 'chinesecns' => 20000, 'chineseeten' => 20002, 'ia5' => 20105, 'ia5german' => 20106, 'ia5swedish' => 20107, 'ia5norwegian' => 20108, 'usascii' => 20127, 'ibm273' => 20273, 'ibm277' => 20277, 'ibm278' => 20278, 'ibm280' => 20280, 'ibm284' => 20284, 'ibm285' => 20285, 'ibm290' => 20290, 'ibm297' => 20297, 'ibm420' => 20420, 'ibm423' => 20423, 'ibm424' => 20424, 'ebcdickoreanextended' => 20833, 'ibmthai' => 20838, 'koi8r' => 20866, 'ibm871' => 20871, 'ibm880' => 20880, 'ibm905' => 20905, 'ibm00924' => 20924, 'cp1025' => 21025, 'koi8u' => 21866, 'iso88591' => 28591, 'iso88592' => 28592, 'iso88593' => 28593, 'iso88594' => 28594, 'iso88595' => 28595, 'iso88596' => 28596, 'iso88597' => 28597, 'iso88598' => 28598, 'iso88599' => 28599, 'iso885913' => 28603, 'iso885915' => 28605, 'xeuropa' => 29001, 'iso88598i' => 38598, 'iso2022jp' => 50220, 'csiso2022jp' => 50221, 'iso2022jp' => 50222, 'iso2022kr' => 50225, 'eucjp' => 51932, 'euccn' => 51936, 'euckr' => 51949, 'hzgb2312' => 52936, 'gb18030' => 54936, 'isciide' => 57002, 'isciibe' => 57003, 'isciita' => 57004, 'isciite' => 57005, 'isciias' => 57006, 'isciior' => 57007, 'isciika' => 57008, 'isciima' => 57009, 'isciigu' => 57010, 'isciipa' => 57011, 'utf7' => 65000, 'utf8' => 65001, ); $charset = strtolower($charset); $charset = preg_replace(array('/^x-/', '/[^a-z0-9]/'), '', $charset); if (isset($aliases[$charset])) { return $aliases[$charset]; } if (preg_match('/^(ibm|dos|cp|windows|win)[0-9]+/', $charset, $m)) { return substr($charset, strlen($m[1]) + 1); } */ } /** * Wrap text to a given number of characters per line * but respect the mail quotation of replies messages (>). * Finally add another quotation level by prepending the lines * with > * * @param string $text Text to wrap * @param int $length The line width * * @return string The wrapped text */ protected static function wrap_and_quote($text, $length = 72) { // Function stolen from Roundcube ;) // Rebuild the message body with a maximum of $max chars, while keeping quoted message. $max = min(77, $length + 8); $lines = preg_split('/\r?\n/', trim($text)); $out = ''; foreach ($lines as $line) { // don't wrap already quoted lines if (isset($line[0]) && $line[0] == '>') { $line = '>' . rtrim($line); } elseif (mb_strlen($line) > $max) { $newline = ''; foreach (explode("\n", rcube_mime::wordwrap($line, $length - 2)) as $l) { if (strlen($l)) { $newline .= '> ' . $l . "\n"; } else { $newline .= ">\n"; } } $line = rtrim($newline); } else { $line = '> ' . $line; } // Append the line $out .= $line . "\n"; } return $out; } /** * Returns calendar event data from the iTip invitation attached to a mail message */ public function get_invitation_event_from_message($message) { // Parse the message and find iTip attachments $libcal = libcalendaring::get_instance(); $libcal->mail_message_load(['object' => $message]); $ical_objects = $libcal->get_mail_ical_objects(); // Skip methods we do not support here if (!in_array($ical_objects->method, ['REQUEST', 'CANCEL', 'REPLY'])) { return null; } // We support only one event in the iTip foreach ($ical_objects as $mime_id => $event) { if ($event['_type'] == 'event') { $event['_method'] = $ical_objects->method; $event['_mime_id'] = $ical_objects->mime_id; return $event; } } return null; } /** * Returns calendar event data from the iTip invitation attached to a mail message */ public function get_invitation_event($messageId) { // Get the mail message object if ($message = $this->getObject($messageId)) { return $this->get_invitation_event_from_message($message); } return null; } private function mem_check($need) { $mem_limit = (int) parse_bytes(ini_get('memory_limit')); $memory = static::$memory_accumulated; // @phpstan-ignore-next-line return ($mem_limit > 0 && $memory + $need > $mem_limit) ? false : true; } /** * Checks if the message can be processed, depending on its size and * memory_limit, otherwise throws an exception or returns fake body. */ protected function message_mem_check($message, $size, $result = null) { static $memory_rised; // @FIXME: we need up to 5x more memory than the body // Note: Biggest memory multiplication happens in recode_message() // and the Syncroton engine (which also does not support passing bodies // as streams). It also happens when parsing the plain/html text body // in getMessagePartBody() though the footprint there is probably lower. if (!$this->mem_check($size * 5)) { // If we already rised the memory we throw an exception, so the message // will be synchronized in the next run (then we might have enough memory) if ($memory_rised) { throw new Syncroton_Exception_MemoryExhausted(); } $memory_rised = true; $memory_max = 512; // maximum in MB $memory_limit = round(parse_bytes(ini_get('memory_limit')) / 1024 / 1024); // current limit (in MB) $memory_add = round($size * 5 / 1024 / 1024); // how much we need (in MB) $memory_needed = min($memory_limit + $memory_add, $memory_max) . "M"; if ($memory_limit < $memory_max) { $this->logger->debug("Setting memory_limit=$memory_needed"); if (ini_set('memory_limit', $memory_needed) !== false) { // Memory has been rised, check again // @phpstan-ignore-next-line if ($this->mem_check($size * 5)) { return; } } } $this->logger->warn("Not enough memory. Using fake email body."); if ($result !== null) { return $result; } // Let's return a fake message. If we return an empty body Outlook // will not list the message at all. This way user can do something // with the message (flag, delete, move) and see the reason why it's fake // and importantly see its subject, sender, etc. // TODO: Localization? $msg = "This message is too large for ActiveSync."; // $msg .= "See https://kb.kolabenterprise.com/documentation/some-place for more information."; // Get original message headers $headers = $this->storage->get_raw_headers($message->uid); // Build a fake message with original headers, but changed body return kolab_sync_message::fake_message($headers, $msg); } } } diff --git a/lib/kolab_sync_logger.php b/lib/kolab_sync_logger.php index 33b64aa..00ec254 100644 --- a/lib/kolab_sync_logger.php +++ b/lib/kolab_sync_logger.php @@ -1,212 +1,212 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Class for logging messages into log file(s) */ class kolab_sync_logger extends Zend_Log { public $mode; protected $log_driver; protected $logfile; protected $format; protected $log_dir; protected $username; /** * Constructor */ public function __construct($mode = null) { $rcube = rcube::get_instance(); $this->mode = intval($mode); $this->logfile = $rcube->config->get('activesync_log_file'); $this->log_driver = $rcube->config->get('log_driver'); $this->format = $rcube->config->get('log_date_format', 'd-M-Y H:i:s O'); $this->log_dir = $rcube->config->get('log_dir'); $r = new ReflectionClass($this); $this->_priorities = $r->getConstants(); } public function __call($method, $params) { $method = strtoupper($method); if ($this->_priorities[$method] <= $this->mode) { $this->log(array_shift($params), $method); } } /** * Check whether debug logging is enabled * * @return bool */ public function hasDebug() { // This is what we check in self::log() below return !empty($this->log_dir) && $this->mode >= self::NOTICE; } /** * Message logger * * @param string $message Log message * @param int|string $method Message severity */ public function log($message, $method, $extras = null) { if (is_numeric($method)) { $mode = $method; $method = array_search($method, $this->_priorities); } else { $mode = $this->_priorities[$method]; } // Don't log messages with lower prio than the configured one if ($mode > $this->mode) { return; } // Don't log debug messages if it's disabled e.g. by per_user_logging if (empty($this->log_dir) && $mode >= self::NOTICE) { return; } $rcube = rcube::get_instance(); $log_dir = $this->log_dir ?: $rcube->config->get('log_dir'); $logfile = $this->logfile; // if log_file is configured all logs will go to it // otherwise use separate file for info/debug and warning/error - $file="undefined"; + $file = "undefined"; if (!$logfile) { switch ($mode) { case self::DEBUG: case self::INFO: case self::NOTICE: $file = 'console'; break; default: $file = 'errors'; break; } $logfile = $log_dir . DIRECTORY_SEPARATOR . $file; if (version_compare(version_parse(RCUBE_VERSION), '1.4.0') >= 0) { $logfile .= $rcube->config->get('log_file_ext', '.log'); } } elseif ($logfile[0] != '/') { $logfile = $log_dir . DIRECTORY_SEPARATOR . $logfile; } if (!is_string($message)) { $message = var_export($message, true); } // write message in logfmt format with extra info when configured to log to STDOUT if ($this->log_driver == 'logfmt') { $user_name = $this->username; $output = "name=$file component=syncroton"; $params = ['cmd' => 'Cmd', 'device' => 'DeviceId', 'type' => 'DeviceType']; foreach ($params as $key => $val) { if ($val = $_GET[$val]) { $output .= " $key=$val"; } } if (!empty($user_name)) { $output .= " user=$user_name"; } $line = json_encode($message); $output .= " log=$line\n"; file_put_contents("php://stdout", $output, FILE_APPEND) !== false; return; } // add user/request information to the log if ($mode <= self::WARN) { $device = []; $params = ['cmd' => 'Cmd', 'device' => 'DeviceId', 'type' => 'DeviceType']; if (!empty($this->username)) { $device['user'] = $this->username; } foreach ($params as $key => $val) { if ($val = $_GET[$val]) { $device[$key] = $val; } } if (!empty($device)) { $message = @json_encode($device) . ' ' . $message; } } $date = rcube_utils::date_format($this->format); $logline = sprintf("[%s]: [%s] %s\n", $date, $method, $message); // write message with file name when configured to log to STDOUT if ($this->log_driver == 'stdout') { $stdout = "php://stdout"; file_put_contents($stdout, $logline, FILE_APPEND); return; } if ($fp = @fopen($logfile, 'a')) { fwrite($fp, $logline); fflush($fp); fclose($fp); return; } if ($mode <= self::WARN) { // send error to PHPs error handler if write to file didn't succeed trigger_error($message, E_USER_WARNING); } } /** * Set current user name to add into error log */ public function set_username($username) { $this->username = $username; } /** * Set log directory */ public function set_log_dir($dir) { $this->log_dir = $dir; } } diff --git a/lib/kolab_sync_storage_kolab4.php b/lib/kolab_sync_storage_kolab4.php index d5e5b18..2218362 100644 --- a/lib/kolab_sync_storage_kolab4.php +++ b/lib/kolab_sync_storage_kolab4.php @@ -1,570 +1,571 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Storage handling class with Kolab 4 support (IMAP + CalDAV + CardDAV) */ class kolab_sync_storage_kolab4 extends kolab_sync_storage { protected $davStorage = null; protected $relationSupport = false; /** * This implements the 'singleton' design pattern * * @return kolab_sync_storage_kolab4 The one and only instance */ public static function get_instance() { if (!self::$instance) { self::$instance = new kolab_sync_storage_kolab4(); self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $sync = kolab_sync::get_instance(); if ($sync->username === null || $sync->password === null) { throw new Exception("Unsupported storage handler use!"); } $url = $sync->config->get('activesync_dav_server', 'http://localhost'); if (strpos($url, '://') === false) { $url = 'http://' . $url; } // Inject user+password to the URL, there's no other way to pass it to the DAV client $url = str_replace('://', '://' . rawurlencode($sync->username) . ':' . rawurlencode($sync->password) . '@', $url); $this->davStorage = new kolab_storage_dav($url); // DAV $this->storage = $sync->get_storage(); // IMAP // set additional header used by libkolab $this->storage->set_options([ 'skip_deleted' => true, 'threading' => false, ]); // Disable paging $this->storage->set_pagesize(999999); } /** * Get list of folders available for sync * * @param string $deviceid Device identifier * @param string $type Folder (class) type * @param bool $flat_mode Enables flat-list mode * * @return array|bool List of mailbox folders, False on backend failure */ public function folders_list($deviceid, $type, $flat_mode = false) { $list = []; // get mail folders subscribed for sync if ($type === self::MODEL_EMAIL) { $folderdata = $this->folder_meta(); if (!is_array($folderdata)) { return false; } $special_folders = $this->storage->get_special_folders(true); $type_map = [ 'drafts' => 3, 'trash' => 4, 'sent' => 5, ]; // Get the folders "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } // Force numeric folder name to be a string (T1283) $folder = (string) $folder; // Activesync folder properties $folder_data = $this->folder_data($folder, 'mail'); // Set proper type for special folders if (($type = array_search($folder, $special_folders)) && isset($type_map[$type])) { $folder_data['type'] = $type_map[$type]; } $list[$folder_data['serverId']] = $folder_data; } } elseif (in_array($type, [self::MODEL_CONTACTS, self::MODEL_CALENDAR, self::MODEL_TASKS])) { if (!empty($this->folders)) { foreach ($this->folders as $unique_key => $folder) { if (strpos($unique_key, "DAV:$type:") === 0) { $folder_data = $this->folder_data($folder, $type); $list[$folder_data['serverId']] = $folder_data; } } } // TODO: For now all DAV folders are subscribed if (empty($list)) { foreach ($this->davStorage->get_folders($type) as $folder) { $folder_data = $this->folder_data($folder, $type); $list[$folder_data['serverId']] = $folder_data; // Store all folder objects in internal cache, otherwise // Any access to the folder (or list) will invoke excessive DAV requests $unique_key = $folder_data['serverId'] . ":$deviceid:$type"; $this->folders[$unique_key] = $folder; } } } /* // TODO if ($flat_mode) { $list = $this->folders_list_flat($list, $type, $typedata); } */ return $list; } /** * Creates folder and subscribes to the device * * @param string $name Folder name (UTF8) * @param int $type Folder (ActiveSync) type * @param string $deviceid Device identifier * @param ?string $parentid Parent folder identifier * * @return string|false New folder identifier on success, False on failure */ public function folder_create($name, $type, $deviceid, $parentid = null) { // Mail folder if ($type <= 6 || $type == 12) { $parent = null; $name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP'); if ($parentid) { $parent = $this->folder_id2name($parentid, $deviceid); if ($parent === null) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND); } } if ($parent !== null) { $delim = $this->storage->get_hierarchy_delimiter(); $name = $parent . $delim . $name; } if ($this->storage->folder_exists($name)) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS); } // TODO: Support setting folder types? $created = $this->storage->create_folder($name, true); if ($created) { // Set ActiveSync subscription flag $this->folder_set($name, $deviceid, 1); return $this->folder_id($name, 'mail'); } // Special case when client tries to create a subfolder of INBOX // which is not possible on Cyrus-IMAP (T2223) if ($parent == 'INBOX' && stripos($this->last_error(), 'invalid') !== false) { throw new Syncroton_Exception('', Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER); } return false; } elseif ($type == 8 || $type == 13 || $type == 7 || $type == 15 || $type == 9 || $type == 14) { // DAV folder $type = preg_replace('|\..*|', '', self::type_activesync2kolab($type)); // TODO: Folder hierarchy support // Check if folder exists foreach ($this->davStorage->get_folders($type) as $folder) { if ($folder->get_name() == $name) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS); } } $props = ['name' => $name, 'type' => $type]; if ($id = $this->davStorage->folder_update($props)) { return "DAV:{$type}:{$id}"; } return false; } throw new \Exception("Not implemented"); } /** * Renames a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $new_name New folder name (UTF8) * @param ?string $parentid Folder parent identifier * * @return bool True on success, False on failure */ public function folder_rename($folderid, $deviceid, $new_name, $parentid) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { [, $type, $id] = explode(':', $folderid); $props = [ 'id' => $id, 'name' => $new_name, 'type' => $type, ]; // TODO: Folder hierarchy support return $this->davStorage->folder_update($props) !== false; } // Mail folder $old_name = $this->folder_id2name($folderid, $deviceid); if ($parentid) { $parent = $this->folder_id2name($parentid, $deviceid); } $name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP'); if (isset($parent)) { $delim = $this->storage->get_hierarchy_delimiter(); $name = $parent . $delim . $name; } if ($name === $old_name) { return true; } $this->folder_meta = null; return $this->storage->rename_folder($old_name, $name); } /** * Deletes folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * * @return bool True on success, False otherwise */ public function folder_delete($folderid, $deviceid) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { [, $type, $id] = explode(':', $folderid); return $this->davStorage->folder_delete($id, $type) !== false; } // Mail folder $name = $this->folder_id2name($folderid, $deviceid); unset($this->folder_meta[$name]); return $this->storage->delete_folder($name); } /** * Deletes contents of a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param bool $recursive Apply to the folder and its subfolders * * @return bool True on success, False otherwise */ public function folder_empty($folderid, $deviceid, $recursive = false) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { [, $type, $id] = explode(':', $folderid); if ($folder = $this->davStorage->get_folder($id, $type)) { return $folder->delete_all(); } // TODO: $recursive=true return false; } // Mail folder return parent::folder_empty($folderid, $deviceid, $recursive); } /** * Returns folder data in Syncroton format */ protected function folder_data($folder, $type) { // Mail folders if (strpos($type, 'mail') === 0) { return parent::folder_data($folder, $type); } // DAV folders return [ 'serverId' => "DAV:{$type}:{$folder->id}", 'parentId' => 0, // TODO: Folder hierarchy 'displayName' => $folder->get_name(), 'type' => $this->type_kolab2activesync($type), ]; } /** * Builds folder ID based on folder name * * @param string $name Folder name (UTF7-IMAP) * @param string $type Kolab folder type * * @return string|null Folder identifier (up to 64 characters) */ protected function folder_id($name, $type = null) { if (!$type) { $type = 'mail'; } // ActiveSync expects folder identifiers to be max.64 characters // So we can't use just folder name $name = (string) $name; if ($name === '') { return null; } if (strpos($type, 'mail') !== 0) { throw new Exception("Unsupported folder_id() call on a DAV folder"); } if (isset($this->folder_uids[$name])) { return $this->folder_uids[$name]; } /* @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves. There's one inconvenience of this solution: folder name/type change would be handled in ActiveSync as delete + create. @TODO: Consider using MAILBOXID (RFC8474) that Cyrus v3 supports // get folders unique identifier $folderdata = $this->storage->get_metadata($name, self::UID_KEY); if ($folderdata && !empty($folderdata[$name])) { $uid = $folderdata[$name][self::UID_KEY]; return $this->folder_uids[$name] = $uid; } */ if (strcasecmp($name, 'INBOX') === 0) { // INBOX is always inbox, prevent from issues related with a change of // folder type annotation (it can be initially unset). $type = 'mail.inbox'; } // Add type to folder UID hash, so type change can be detected by Syncroton $uid = $name . '!!' . $type; $uid = md5($uid); return $this->folder_uids[$name] = $uid; } /** * Returns IMAP folder name * * @param string $id Folder identifier * @param string $deviceid Device dentifier * * @return null|string Folder name (UTF7-IMAP) */ public function folder_id2name($id, $deviceid) { // TODO: This method should become protected and be used for mail folders only if (strpos($id, 'DAV:') === 0) { throw new Exception("Unsupported folder_id2name() call on a DAV folder"); } // check in cache first if (!empty($this->folder_uids)) { if (($name = array_search($id, $this->folder_uids)) !== false) { return $name; } } // get all folders of specified type $folderdata = $this->folder_meta(); if (!is_array($folderdata) || empty($id)) { return null; } // check if folders are "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } if ($uid = $this->folder_id($folder, 'mail')) { $this->folder_uids[$folder] = $uid; } if ($uid === $id) { $name = $folder; } } return $name ?? null; } /** * Gets kolab_storage_folder object from Activesync folder ID. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * * @return ?kolab_storage_folder */ public function getFolder($folderid, $deviceid, $type) { if (strpos($folderid, 'DAV:') !== 0) { throw new Exception("Unsupported getFolder() call on a mail folder"); } $unique_key = "$folderid:$deviceid:$type"; if (array_key_exists($unique_key, $this->folders)) { return $this->folders[$unique_key]; } [, $type, $id] = explode(':', $folderid); return $this->folders[$unique_key] = $this->davStorage->get_folder($id, $type); } /** * Gets Activesync preferences for a folder. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * * @return array Folder preferences */ public function getFolderConfig($folderid, $deviceid, $type) { // TODO: Get "alarms" from the DAV folder props, or implement // a storage for folder properties return [ 'ALARMS' => true, ]; } /** * Return last storage error */ public static function last_error() { // TODO return null; } /** * Subscribe default set of folders on device registration */ protected function device_init_subscriptions($deviceid) { $config = rcube::get_instance()->config; $mode = (int) $config->get('activesync_init_subscriptions'); $subscribed_folders = null; // Special folders only if (!$mode) { $all_folders = $this->storage->get_special_folders(true); // We do not subscribe to the Spam folder by default, same as the old Kolab driver does unset($all_folders['junk']); $all_folders = array_unique(array_merge(['INBOX'], array_values($all_folders))); } // other modes elseif (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) { $all_folders = $this->storage->list_folders(); if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) { $subscribed_folders = $this->storage->list_folders_subscribed(); } } else { $all_folders = $this->storage->list_folders_subscribed(); } foreach ($all_folders as $folder) { $ns = strtoupper($this->storage->folder_namespace($folder)); // subscribe the folder according to configured mode // and folder namespace/subscription status if (!$mode || ($mode & constant("self::INIT_ALL_{$ns}")) || (($mode & constant("self::INIT_SUB_{$ns}")) && ($subscribed_folders === null || in_array($folder, $subscribed_folders))) ) { $this->folder_set($folder, $deviceid, 1); } } // TODO: Subscribe personal DAV folders, for now we assume all are subscribed // TODO: Subscribe shared DAV folders } - public function getExtraData($folderid, $deviceid) { + public function getExtraData($folderid, $deviceid) + { if (strpos($folderid, 'DAV:') === 0) { return null; } return parent::getExtraData($folderid, $deviceid); } } diff --git a/phpstan.neon b/phpstan.neon index 3ce6533..c112b4e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,24 +1,18 @@ parameters: bootstrapFiles: - phpstan.bootstrap.php excludePaths: - - vendor - lib/plugins - lib/ext/Roundcube - - lib/ext/Syncroton - - ignoreErrors: - - |Access to an undefined property Syncroton_Model_.*| - - |Access to offset .* on an unknown class An.| paths: - lib - tests level: 4 scanDirectories: - lib/ext treatPhpDocTypesAsCertain: false diff --git a/tests/Sync/Sync/EmailTest.php b/tests/Sync/Sync/EmailTest.php index 924137f..414968d 100644 --- a/tests/Sync/Sync/EmailTest.php +++ b/tests/Sync/Sync/EmailTest.php @@ -1,468 +1,468 @@ emptyTestFolder('INBOX', 'mail'); $this->registerDevice(); // Test invalid collection identifier $request = << 0 1111111111 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('12', $xpath->query("//ns:Sync/ns:Status")->item(0)->nodeValue); // Test INBOX $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $syncKey = 0; $request = << {$syncKey} {$folderId} EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:SyncKey")->item(0)->nodeValue); $this->assertSame('Email', $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:Class")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:CollectionId")->item(0)->nodeValue); // Test listing mail in INBOX, use WindowSize=1 // Append two mail messages $this->appendMail('INBOX', 'mail.sync1'); $this->appendMail('INBOX', 'mail.sync2'); $request = << {$syncKey} {$folderId} 1 1 1 0 1 2 51200 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); // Note: We assume messages are in IMAP default order, it may change in future $root .= "/ns:Commands/ns:Add"; $this->assertStringMatchesFormat("{$folderId}::%d", $xpath->query("{$root}/ns:ServerId")->item(0)->nodeValue); $this->assertSame('test sync', $xpath->query("{$root}/ns:ApplicationData/Email:Subject")->item(0)->nodeValue); // List the rest of the mail $request = << {$syncKey} {$folderId} 1 1 0 1 2 51200 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); // Note: We assume messages are in IMAP default order, it may change in future $root .= "/ns:Commands/ns:Add"; $this->assertStringMatchesFormat("{$folderId}::%d", $xpath->query("{$root}/ns:ServerId")->item(0)->nodeValue); $this->assertSame('sync test with attachment', $xpath->query("{$root}/ns:ApplicationData/Email:Subject")->item(0)->nodeValue); return $syncKey; } /** * Test empty sync response * * @depends testSync */ public function testEmptySync($syncKey) { $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $request = << {$syncKey} {$folderId} 1 1 0 1 2 51200 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); // We expect an empty response without a change $this->assertEquals(0, $response->getBody()->getSize()); return $syncKey; } /** * Test flag change * * @depends testEmptySync */ public function testFlagChange($syncKey) { $this->assertTrue($this->markMailAsRead('INBOX', '*')); $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $request = << {$syncKey} {$folderId} 1 1 0 1 2 51200 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // print($dom->saveXML()); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); $this->assertSame(2, $xpath->query("{$root}/ns:Commands/ns:Change")->count()); return $syncKey; } /** * Retry flag change * Resending the same syncKey should result in the same changes. * * @depends testFlagChange */ public function testRetryFlagChange($syncKey) { $syncKey--; $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $request = << {$syncKey} {$folderId} 1 1 0 1 2 51200 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // print($dom->saveXML()); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); //FIXME I'm not sure why we get syncKey + 2, I suppose we just always increase the current synckey by one. $this->assertSame(strval($syncKey += 2), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); $this->assertSame(2, $xpath->query("{$root}/ns:Commands/ns:Change")->count()); $serverId1 = $xpath->query("{$root}/ns:Commands/ns:Change/ns:ServerId")->item(0)->nodeValue; return [ 'syncKey' => $syncKey, - 'serverId' => $serverId1 + 'serverId' => $serverId1, ]; } /** * Test updating message properties from client * * @depends testRetryFlagChange */ public function testChangeFromClient($values) { $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $syncKey = $values['syncKey']; $serverId = $values['serverId']; $request = << {$syncKey} {$folderId} {$serverId} 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // print($dom->saveXML()); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); //The server doesn't have to report back successful changes $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Change")->count()); $emails = $this->listEmails('INBOX', '*'); $uid = explode("::", $serverId)[1]; $this->assertSame(2, count($emails)); $this->assertTrue(!array_key_exists('SEEN', $emails[$uid])); return [ 'syncKey' => $syncKey, - 'serverId' => $serverId + 'serverId' => $serverId, ]; } /** * Test deleting messages from client * * @depends testChangeFromClient */ public function testDeleteFromClient($values) { $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $syncKey = $values['syncKey']; $serverId = $values['serverId']; $request = << {$syncKey} {$folderId} {$serverId} EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // print($dom->saveXML()); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Change")->count()); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Delete")->count()); $emails = $this->listEmails('INBOX', '*'); $uid = explode("::", $serverId)[1]; $this->assertSame(2, count($emails)); $this->assertTrue($emails[$uid]['DELETED']); return $syncKey; } /** * Test a sync key that doesn't exist yet. * @depends testDeleteFromClient */ public function testInvalidSyncKey($syncKey) { $syncKey++; $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $request = << {$syncKey} {$folderId} 1 1 0 1 2 51200 0 EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // print($dom->saveXML()); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('3', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame('0', $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); //We have to start over after this. The sync state was removed. return 0; } }