Page MenuHomePhorge

No OneTemporary

diff --git a/lib/ext/Syncroton/Backend/ABackend.php b/lib/ext/Syncroton/Backend/ABackend.php
index eb5e605..44f410f 100644
--- a/lib/ext/Syncroton/Backend/ABackend.php
+++ b/lib/ext/Syncroton/Backend/ABackend.php
@@ -1,211 +1,211 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Backend
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* 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
* @throws Syncroton_Exception_NotFound
* @return Syncroton_Model_IDevice
*/
public function get($id)
{
$id = $id instanceof $this->_modelInterfaceName ? $id->id : $id;
if (empty($id)) {
throw new Syncroton_Exception_NotFound('id can not be empty');
}
$select = $this->_db->select()
->from($this->_tablePrefix . $this->_tableName)
->where('id = ?', $id);
$stmt = $this->_db->query($select);
$data = $stmt->fetch();
$stmt = null; # see https://bugs.php.net/bug.php?id=44081
if ($data === false) {
throw new Syncroton_Exception_NotFound('id not found');
}
return $this->_getObject($data);
}
/**
* convert array to object
*
* @param array $data
* @return object
*/
protected function _getObject($data)
{
foreach ($data as $key => $value) {
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'));
+ $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;
$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
));
return $this->get($model->id);
}
/**
* Returns list of user accounts
*
* @param Syncroton_Model_Device $device The device
*
* @return array List of Syncroton_Model_Account objects
*/
public function userAccounts($device)
{
return array();
}
/**
* convert from camelCase to camel_case
* @param string $string
* @return string
*/
protected function _fromCamelCase($string)
{
$string = lcfirst($string);
return preg_replace_callback('/([A-Z])/', function ($string) {return '_' . strtolower($string[0]);}, $string);
}
/**
* convert from camel_case to camelCase
*
* @param string $string
* @param bool $ucFirst
* @return string
*/
protected function _toCamelCase($string, $ucFirst = true)
{
if ($ucFirst === true) {
$string = ucfirst($string);
}
return preg_replace_callback('/_([a-z])/', function ($string) {return strtoupper($string[1]);}, $string);
}
}
diff --git a/lib/ext/Syncroton/Command/ItemOperations.php b/lib/ext/Syncroton/Command/ItemOperations.php
index a60a02c..1353467 100644
--- a/lib/ext/Syncroton/Command/ItemOperations.php
+++ b/lib/ext/Syncroton/Command/ItemOperations.php
@@ -1,306 +1,307 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync ItemOperations command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_ItemOperations extends Syncroton_Command_Wbxml
{
const STATUS_SUCCESS = 1;
const STATUS_PROTOCOL_ERROR = 2;
const STATUS_SERVER_ERROR = 3;
const STATUS_ITEM_FAILED_CONVERSION = 14;
protected $_defaultNameSpace = 'uri:ItemOperations';
protected $_documentElement = 'ItemOperations';
/**
* list of items to move
*
* @var array
*/
protected $_fetches = array();
/**
* list of folder to empty
*
* @var array
*/
protected $_emptyFolderContents = array();
/**
* parse MoveItems request
*
*/
public function handle()
{
$xml = simplexml_import_dom($this->_requestBody);
if (isset($xml->Fetch)) {
foreach ($xml->Fetch as $fetch) {
$this->_fetches[] = $this->_handleFetch($fetch);
}
}
if (isset($xml->EmptyFolderContents)) {
foreach ($xml->EmptyFolderContents as $emptyFolderContents) {
$this->_emptyFolderContents[] = $this->_handleEmptyFolderContents($emptyFolderContents);
}
}
}
/**
* generate ItemOperations response
*
* @todo add multipart support to all types of fetches
*/
public function getResponse()
{
// add aditional namespaces
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase' , 'uri:AirSyncBase');
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSync' , 'uri:AirSync');
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Search' , 'uri:Search');
$itemOperations = $this->_outputDom->documentElement;
$itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS));
$response = $itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Response'));
foreach ($this->_fetches as $fetch) {
$fetchTag = $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Fetch'));
try {
$dataController = Syncroton_Data_Factory::factory($fetch['store'], $this->_device, $this->_syncTimeStamp);
if (isset($fetch['collectionId'])) {
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS));
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $fetch['collectionId']));
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $fetch['serverId']));
$properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties');
$dataController
->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['collectionId'], 'options' => $fetch['options'])), $fetch['serverId'])
->appendXML($properties, $this->_device);
$fetchTag->appendChild($properties);
} elseif (isset($fetch['longId'])) {
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS));
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:Search', 'LongId', $fetch['longId']));
$properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties');
$dataController
->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['longId'], 'options' => $fetch['options'])), $fetch['longId'])
->appendXML($properties, $this->_device);
$fetchTag->appendChild($properties);
} elseif (isset($fetch['fileReference'])) {
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS));
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSyncBase', 'FileReference', $fetch['fileReference']));
$properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties');
$fileReference = $dataController->getFileReference($fetch['fileReference']);
// unset data field and move content to stream
if (!empty($this->_requestParameters['acceptMultipart'])) {
$this->_headers['Content-Type'] = 'application/vnd.ms-sync.multipart';
$partStream = fopen("php://temp", 'r+');
if (is_resource($fileReference->data)) {
stream_copy_to_stream($fileReference->data, $partStream);
} else {
fwrite($partStream, $fileReference->data);
}
unset($fileReference->data);
$this->_parts[] = $partStream;
$fileReference->part = count($this->_parts);
}
/**
* the client requested a range. But we return the whole file.
*
* That's not correct, but allowed. The server is allowed to overwrite the range.
*
* @todo implement cutting $fileReference->data into pieces
*/
if (isset($fetch['options']['range'])) {
$dataSize = $this->_getDataSize($fileReference->data);
$total = $this->_outputDom->createElementNS('uri:ItemOperations', 'Total', $dataSize);
$properties->appendChild($total);
$rangeEnd = $dataSize > 0 ? $dataSize - 1 : 0;
$range = $this->_outputDom->createElementNS('uri:ItemOperations', 'Range', '0-' . $rangeEnd);
$properties->appendChild($range);
}
$fileReference->appendXML($properties, $this->_device);
$fetchTag->appendChild($properties);
}
} catch (Syncroton_Exception_NotFound $e) {
$response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_ITEM_FAILED_CONVERSION));
} catch (Exception $e) {
//echo __LINE__; echo $e->getMessage(); echo $e->getTraceAsString();
$response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SERVER_ERROR));
}
}
foreach ($this->_emptyFolderContents as $emptyFolderContents) {
try {
$folder = $this->_folderBackend->getFolder($this->_device, $emptyFolderContents['collectionId']);
$dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
$dataController->emptyFolderContents($emptyFolderContents['collectionId'], $emptyFolderContents['options']);
$status = Syncroton_Command_ItemOperations::STATUS_SUCCESS;
}
catch (Syncroton_Exception_Status_ItemOperations $e) {
$status = $e->getCode();
}
catch (Exception $e) {
$status = Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR;
}
$emptyFolderContentsTag = $this->_outputDom->createElementNS('uri:ItemOperations', 'EmptyFolderContents');
$emptyFolderContentsTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', $status));
$emptyFolderContentsTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $emptyFolderContents['collectionId']));
$response->appendChild($emptyFolderContentsTag);
}
return $this->_outputDom;
}
/**
* parse fetch request
*
* @param SimpleXMLElement $fetch
* @return array
*/
protected function _handleFetch(SimpleXMLElement $fetch)
{
$fetchArray = array(
'store' => (string)$fetch->Store,
'options' => array()
);
// try to fetch element from namespace AirSync
$airSync = $fetch->children('uri:AirSync');
if (isset($airSync->CollectionId)) {
$fetchArray['collectionId'] = (string)$airSync->CollectionId;
$fetchArray['serverId'] = (string)$airSync->ServerId;
}
// try to fetch element from namespace Search
$search = $fetch->children('uri:Search');
if (isset($search->LongId)) {
$fetchArray['longId'] = (string)$search->LongId;
}
// try to fetch element from namespace AirSyncBase
$airSyncBase = $fetch->children('uri:AirSyncBase');
if (isset($airSyncBase->FileReference)) {
$fetchArray['fileReference'] = (string)$airSyncBase->FileReference;
}
if (isset($fetch->Options)) {
// try to fetch element from namespace AirSyncBase
$airSyncBase = $fetch->Options->children('uri:AirSyncBase');
if (isset($airSyncBase->BodyPreference)) {
foreach ($airSyncBase->BodyPreference as $bodyPreference) {
$type = (int) $bodyPreference->Type;
$fetchArray['options']['bodyPreferences'][$type] = array(
'type' => $type
);
// optional
if (isset($bodyPreference->TruncationSize)) {
$fetchArray['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
}
// optional
if (isset($bodyPreference->AllOrNone)) {
$fetchArray['options']['bodyPreferences'][$type]['allOrNone'] = (int) $bodyPreference->AllOrNone;
}
}
}
- if (isset($fetch->Options->MIMESupport)){
- $fetchArray['options']['mimeSupport'] = (int) $fetch->Options->MIMESupport;
+ $airSync = $fetch->Options->children('uri:AirSync');
+ if (isset($airSync->MIMESupport)) {
+ $fetchArray['options']['mimeSupport'] = (int) $airSync->MIMESupport;
}
- if (isset($airSyncBase->Range)) {
- $fetchArray['options']['range'] = (string) $airSyncBase->Range;
+ if (isset($fetch->Options->Range)) {
+ $fetchArray['options']['range'] = (string) $fetch->Options->Range;
}
}
return $fetchArray;
}
/**
* handle empty folder request
*
* @param SimpleXMLElement $emptyFolderContent
* @return array
*/
protected function _handleEmptyFolderContents(SimpleXMLElement $emptyFolderContent)
{
$folderArray = array(
'collectiondId' => null,
'options' => array('deleteSubFolders' => FALSE)
);
// try to fetch element from namespace AirSync
$airSync = $emptyFolderContent->children('uri:AirSync');
$folderArray['collectionId'] = (string)$airSync->CollectionId;
if (isset($emptyFolderContent->Options)) {
$folderArray['options']['deleteSubFolders'] = isset($emptyFolderContent->Options->DeleteSubFolders);
}
return $folderArray;
}
/**
* return length of data
*
* @param string|resource $data
* @return number
*/
protected function _getDataSize($data)
{
if (is_resource($data)) {
rewind($data);
fseek($data, 0, SEEK_END);
return ftell($data);
} else {
return strlen($data);
}
}
}
diff --git a/lib/ext/Syncroton/Command/Ping.php b/lib/ext/Syncroton/Command/Ping.php
index 90b6d8b..c14fcca 100644
--- a/lib/ext/Syncroton/Command/Ping.php
+++ b/lib/ext/Syncroton/Command/Ping.php
@@ -1,283 +1,283 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Ping command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
{
const STATUS_NO_CHANGES_FOUND = 1;
const STATUS_CHANGES_FOUND = 2;
const STATUS_MISSING_PARAMETERS = 3;
const STATUS_REQUEST_FORMAT_ERROR = 4;
const STATUS_INTERVAL_TO_GREAT_OR_SMALL = 5;
const STATUS_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 $_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'));
+ $this->_device->lastping = new DateTime('now', new DateTimeZone('UTC'));
if ($status == self::STATUS_NO_CHANGES_FOUND) {
$this->_device = $this->_deviceBackend->update($this->_device);
}
$lifeTime = $this->_device->pinglifetime;
$maxInterval = Syncroton_Registry::getPingInterval();
if ($maxInterval <= 0 || $maxInterval > Syncroton_Server::MAX_HEARTBEAT_INTERVAL) {
$maxInterval = Syncroton_Server::MAX_HEARTBEAT_INTERVAL;
}
if ($lifeTime > $maxInterval) {
$ping = $this->_outputDom->documentElement;
$ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', self::STATUS_INTERVAL_TO_GREAT_OR_SMALL));
$ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'HeartbeatInterval', $maxInterval));
return;
}
$intervalEnd = $intervalStart + $lifeTime;
$secondsLeft = $intervalEnd;
- $folders = unserialize($this->_device->pingfolder);
+ $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 {
$device = $this->_deviceBackend->get($this->_device->id);
} catch (Syncroton_Exception_NotFound $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
$status = self::STATUS_FOLDER_NOT_FOUND;
break;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->err(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
// do nothing, maybe temporal issue, should we stop?
continue;
}
// if another Ping command updated lastping property, we can stop processing this Ping command request
if ((isset($device->lastping) && $device->lastping instanceof DateTime) &&
$device->pingfolder === $this->_device->pingfolder &&
$device->lastping->getTimestamp() > $this->_device->lastping->getTimestamp() ) {
break;
}
// 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'));
+ $now = new DateTime('now', new DateTimeZone('UTC'));
foreach ($folders as $folderId) {
try {
$folder = $this->_folderBackend->get($folderId);
$dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
} catch (Syncroton_Exception_NotFound $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
$status = self::STATUS_FOLDER_NOT_FOUND;
break;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->err(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
// do nothing, maybe temporal issue, should we stop?
continue;
}
try {
$syncState = $this->_syncStateBackend->getSyncState($this->_device, $folder);
// another process synchronized data of this folder already. let's skip it
if ($syncState->lastsync > $this->_syncTimeStamp) {
continue;
}
// safe battery time by skipping folders which got synchronied less than Syncroton_Registry::getQuietTime() seconds ago
if (($now->getTimestamp() - $syncState->lastsync->getTimestamp()) < Syncroton_Registry::getQuietTime()) {
continue;
}
$foundChanges = $dataController->hasChanges($this->_contentStateBackend, $folder, $syncState);
} catch (Syncroton_Exception_NotFound $e) {
// folder got never synchronized to client
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' syncstate not found. enforce sync for folder: ' . $folder->serverId);
$foundChanges = true;
}
if ($foundChanges == true) {
$this->_foldersWithChanges[] = $folder;
$status = self::STATUS_CHANGES_FOUND;
}
}
if ($status != self::STATUS_NO_CHANGES_FOUND) {
break;
}
// 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/Search.php b/lib/ext/Syncroton/Command/Search.php
index 7f7f412..191892a 100644
--- a/lib/ext/Syncroton/Command/Search.php
+++ b/lib/ext/Syncroton/Command/Search.php
@@ -1,82 +1,88 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Search command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Search extends Syncroton_Command_Wbxml
{
const STATUS_SUCCESS = 1;
const STATUS_SERVER_ERROR = 3;
protected $_defaultNameSpace = 'uri:Search';
protected $_documentElement = 'Search';
/**
* store data
*
* @var Syncroton_Model_StoreRequest
*/
protected $_store;
/**
* parse search command request
*
+ * @throws Syncroton_Exception_UnexpectedValue
*/
public function handle()
{
+ if (! $this->_requestBody instanceof DOMDocument) {
+ throw new Syncroton_Exception_UnexpectedValue(
+ 'request body is no DOMDocument. got: ' . print_r($this->_requestBody, true));
+ }
+
$xml = simplexml_import_dom($this->_requestBody);
$this->_store = new Syncroton_Model_StoreRequest($xml->Store);
}
/**
* generate search command response
*
*/
public function getResponse()
{
$dataController = Syncroton_Data_Factory::factory($this->_store->name, $this->_device, new DateTime());
if (! $dataController instanceof Syncroton_Data_IDataSearch) {
throw new RuntimeException('class must be instanceof Syncroton_Data_IDataSearch');
}
try {
// Search
$storeResponse = $dataController->search($this->_store);
$storeResponse->status = self::STATUS_SUCCESS;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " search exception: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " saerch exception trace : " . $e->getTraceAsString());
$storeResponse = new Syncroton_Model_StoreResponse(array(
'status' => self::STATUS_SERVER_ERROR
));
}
$search = $this->_outputDom->documentElement;
$search->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Status', self::STATUS_SUCCESS));
$response = $search->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Response'));
$store = $response->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Store'));
$storeResponse->appendXML($store, $this->_device);
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Model/AXMLEntry.php b/lib/ext/Syncroton/Model/AXMLEntry.php
index 8a02707..bb2971c 100644
--- a/lib/ext/Syncroton/Model/AXMLEntry.php
+++ b/lib/ext/Syncroton/Model/AXMLEntry.php
@@ -1,328 +1,328 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* 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
* @throws InvalidArgumentException
*/
public function setFromSimpleXMLElement(SimpleXMLElement $properties)
{
if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) {
throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
}
foreach (array_keys($this->_properties) as $namespace) {
if ($namespace == 'Internal') {
continue;
}
$this->_parseNamespace($namespace, $properties);
}
return;
}
/**
* add needed xml namespaces to DomDocument
*
* @param unknown_type $domParrent
*/
protected function _addXMLNamespaces(DOMElement $domParrent)
{
foreach($this->_properties as $namespace => $namespaceProperties) {
// don't add default namespace again
if($domParrent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) {
$domParrent->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:'.$namespace, 'uri:'.$namespace);
}
}
}
protected function _appendXMLElement(Syncroton_Model_IDevice $device, DOMElement $element, $elementProperties, $value)
{
if ($value instanceof Syncroton_Model_IEntry) {
$value->appendXML($element, $device);
} else {
if ($value instanceof DateTime) {
$value = $value->format($this->_dateTimeFormat);
} elseif (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {
if (is_resource($value)) {
rewind($value);
$value = stream_get_contents($value);
}
$value = base64_encode($value);
}
if ($elementProperties['type'] == 'byteArray') {
$element->setAttributeNS('uri:Syncroton', 'Syncroton:encoding', 'opaque');
// encode to base64; the wbxml encoder will base64_decode it again
// this way we can also transport data, which would break the xmlparser otherwise
$element->appendChild($element->ownerDocument->createCDATASection(base64_encode($value)));
} else {
// strip off any non printable control characters
- if (!ctype_print($value)) {
+ 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
* @throws InvalidArgumentException
* @return multitype:unknown
*/
protected function _getElementProperties($element)
{
foreach($this->_properties as $namespace => $namespaceProperties) {
if (array_key_exists($element, $namespaceProperties)) {
return array($namespace, $namespaceProperties[$element]);
}
}
throw new InvalidArgumentException("$element is no valid property of " . get_class($this));
}
protected function _parseNamespace($nameSpace, SimpleXMLElement $properties)
{
// fetch data from Contacts namespace
$children = $properties->children("uri:$nameSpace");
foreach ($children as $elementName => $xmlElement) {
$elementName = lcfirst($elementName);
if (!isset($this->_properties[$nameSpace][$elementName])) {
continue;
}
list (, $elementProperties) = $this->_getElementProperties($elementName);
switch ($elementProperties['type']) {
case 'container':
if (!empty($elementProperties['multiple'])) {
$property = (array) $this->$elementName;
if (isset($elementProperties['class'])) {
$property[] = new $elementProperties['class']($xmlElement);
} else {
$property[] = (string) $xmlElement;
}
} else if (isset($elementProperties['childElement'])) {
$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/StoreRequest.php b/lib/ext/Syncroton/Model/StoreRequest.php
index c6a158f..16728a3 100644
--- a/lib/ext/Syncroton/Model/StoreRequest.php
+++ b/lib/ext/Syncroton/Model/StoreRequest.php
@@ -1,247 +1,247 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @copyright Copyright (c) 2012 Kolab Systems AG (http://kolabsys.com)
* @author Lars Kneschke <l.kneschke@metaways.de>
* @author Aleksander Machniak <machniak@kolabsys.com>
*/
/**
* class to handle ActiveSync Search Store request
*
* @package Syncroton
* @subpackage Model
* @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:
// <Store><Name>GAL</Name><Query>string</Query><Options><Range>0-11</Range></Options></Store>
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:
// <GreaterThan>
// <DateReceived>
// <Value>2012-08-02T16:54:11.000Z</Value>
// </GreaterThan>
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': //?
- '0-999';
+ $this->_store['options']['range'] = '0-999';
break;
case 'Mailbox':
case 'GAL':
default:
- '0-99';
+ $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/Server.php b/lib/ext/Syncroton/Server.php
index bb77e00..c97c50c 100644
--- a/lib/ext/Syncroton/Server.php
+++ b/lib/ext/Syncroton/Server.php
@@ -1,461 +1,453 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle incoming http ActiveSync requests
*
* @package Syncroton
*/
class Syncroton_Server
{
const PARAMETER_ATTACHMENTNAME = 0;
const PARAMETER_COLLECTIONID = 1;
const PARAMETER_ITEMID = 3;
const PARAMETER_OPTIONS = 7;
const MAX_HEARTBEAT_INTERVAL = 3540; // 59 minutes
protected $_body;
/**
* informations about the currently device
*
* @var Syncroton_Backend_IDevice
*/
protected $_deviceBackend;
/**
* @var Zend_Log
*/
protected $_logger;
/**
* @var Zend_Controller_Request_Http
*/
protected $_request;
protected $_userId;
public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null)
{
if (Syncroton_Registry::isRegistered('loggerBackend')) {
$this->_logger = Syncroton_Registry::get('loggerBackend');
}
$this->_userId = $userId;
$this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http();
$this->_body = $body !== null ? $body : fopen('php://input', 'r');
$this->_deviceBackend = Syncroton_Registry::getDeviceBackend();
}
public function handle()
{
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod());
switch($this->_request->getMethod()) {
case 'OPTIONS':
$this->_handleOptions();
break;
case 'POST':
$this->_handlePost();
break;
case 'GET':
echo "It works!<br>Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}.";
break;
}
}
/**
* handle options request
*/
protected function _handleOptions()
{
$command = new Syncroton_Command_Options();
$this->_sendHeaders($command->getHeaders());
}
protected function _sendHeaders(array $headers)
{
foreach ($headers as $name => $value) {
header($name . ': ' . $value);
}
}
/**
* handle post request
*/
protected function _handlePost()
{
$requestParameters = $this->_getRequestParameters($this->_request);
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true));
$className = 'Syncroton_Command_' . $requestParameters['command'];
- if(!class_exists($className)) {
+ if (!class_exists($className)) {
if ($this->_logger instanceof Zend_Log)
- $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']);
+ $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) {
$requestBody->formatOutput = true;
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML());
}
} catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) {
$requestBody = NULL;
}
} else {
$requestBody = $this->_body;
}
header("MS-Server-ActiveSync: 14.00.0536.000");
// avoid sending HTTP header "Content-Type: text/html" for empty sync responses
ini_set('default_mimetype', 'application/vnd.ms-sync.wbxml');
try {
$command = new $className($requestBody, $device, $requestParameters);
$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->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
+ $this->_logger->err(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
if ($this->_logger instanceof Zend_Log)
- $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage());
+ $this->_logger->err(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
- $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
+ $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(Zend_Log::DEBUG, $response, __METHOD__, __LINE__);
}
if (isset($command) && $command instanceof Syncroton_Command_ICommand) {
$this->_sendHeaders($command->getHeaders());
}
$outputStream = fopen("php://temp", 'r+');
$encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3);
try {
$encoder->encode($response);
} catch (Syncroton_Wbxml_Exception $swe) {
if ($this->_logger instanceof Zend_Log) {
$this->_logger->err(__METHOD__ . '::' . __LINE__ . " Could not encode output: " . $swe);
- $this->_logDomDocument(Zend_Log::WARN, $response, __METHOD__, __LINE__);
}
header("HTTP/1.1 500 Internal server error");
return;
}
if ($requestParameters['acceptMultipart'] == true) {
$parts = $command->getParts();
// output multipartheader
$bodyPartCount = 1 + count($parts);
// number of parts (4 bytes)
$header = pack('i', $bodyPartCount);
$partOffset = 4 + (($bodyPartCount * 2) * 4);
// wbxml body start and length
$streamStat = fstat($outputStream);
$header .= pack('ii', $partOffset, $streamStat['size']);
$partOffset += $streamStat['size'];
// calculate start and length of parts
foreach ($parts as $partId => $partStream) {
rewind($partStream);
$streamStat = fstat($partStream);
// part start and length
$header .= pack('ii', $partOffset, $streamStat['size']);
$partOffset += $streamStat['size'];
}
echo $header;
}
// output body
rewind($outputStream);
fpassthru($outputStream);
// output multiparts
if (isset($parts)) {
foreach ($parts as $partStream) {
rewind($partStream);
fpassthru($partStream);
}
}
}
}
/**
* write (possible big) DOMDocument in smaller chunks to log file
*
- * @param unknown $priority
+ * @param int $priority
* @param DOMDocument $dom
* @param string $method
* @param int $line
*/
protected function _logDomDocument($priority, DOMDocument $dom, $method, $line)
{
- $loops = 0;
-
$tempStream = tmpfile();
$meta_data = stream_get_meta_data($tempStream);
$filename = $meta_data["uri"];
$dom->formatOutput = true;
$dom->save($filename);
$dom->formatOutput = false;
rewind($tempStream);
- // log data in 1MByte chunks
- while (!feof($tempStream)) {
- $this->_logger->log($method . '::' . $line . " xml response($loops):\n" . fread($tempStream, 1048576), $priority);
-
- $loops++;
- }
+ $this->_logger->log($method . '::' . $line . " xml response(first 4k):\n" . fread($tempStream, 4 * 1024), $priority);
fclose($tempStream);
}
/**
* return request params
*
* @return array
*/
protected function _getRequestParameters(Zend_Controller_Request_Http $request)
{
if (strpos($request->getRequestUri(), '&') === false) {
$commands = array(
0 => 'Sync',
1 => 'SendMail',
2 => 'SmartForward',
3 => 'SmartReply',
4 => 'GetAttachment',
9 => 'FolderSync',
10 => 'FolderCreate',
11 => 'FolderDelete',
12 => 'FolderUpdate',
13 => 'MoveItems',
14 => 'GetItemEstimate',
15 => 'MeetingResponse',
16 => 'Search',
17 => 'Settings',
18 => 'Ping',
19 => 'ItemOperations',
20 => 'Provision',
21 => 'ResolveRecipients',
22 => 'ValidateCert'
);
$requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?'));
$stream = fopen("php://temp", 'r+');
fwrite($stream, base64_decode($requestParameters));
rewind($stream);
// unpack the first 4 bytes
$unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4));
// 140 => 14.0
$protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1);
$command = $commands[$unpacked['command']];
$locale = $unpacked['locale'];
// unpack deviceId
$length = ord(fread($stream, 1));
if ($length > 0) {
$toUnpack = fread($stream, $length);
$unpacked = unpack("H" . ($length * 2) . "string", $toUnpack);
$deviceId = $unpacked['string'];
}
// unpack policyKey
$length = ord(fread($stream, 1));
if ($length > 0) {
$unpacked = unpack('Vstring', fread($stream, $length));
$policyKey = $unpacked['string'];
}
// unpack device type
$length = ord(fread($stream, 1));
if ($length > 0) {
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$deviceType = $unpacked['string'];
}
while (! feof($stream)) {
$tag = ord(fread($stream, 1));
$length = ord(fread($stream, 1));
// 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 unknown_type $ownerId
* @param unknown_type $deviceId
* @param unknown_type $deviceType
* @param unknown_type $userAgent
* @param unknown_type $protocolVersion
* @return Syncroton_Model_Device
*/
protected function _getUserDevice($ownerId, $requestParameters)
{
try {
$device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']);
$device->useragent = $requestParameters['userAgent'];
$device->acsversion = $requestParameters['protocolVersion'];
if ($device->isDirty()) {
$device = $this->_deviceBackend->update($device);
}
} catch (Syncroton_Exception_NotFound $senf) {
$device = $this->_deviceBackend->create(new Syncroton_Model_Device(array(
'owner_id' => $ownerId,
'deviceid' => $requestParameters['deviceId'],
'devicetype' => $requestParameters['deviceType'],
'useragent' => $requestParameters['userAgent'],
'acsversion' => $requestParameters['protocolVersion'],
'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null
)));
}
return $device;
}
public static function validateSession()
{
$validatorFunction = Syncroton_Registry::getSessionValidator();
return $validatorFunction();
}
}
diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index 6360937..9608277 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -1,1056 +1,1056 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class kolab_sync_backend
{
/**
* Singleton instace of kolab_sync_backend
*
* @var kolab_sync_backend
*/
static protected $instance;
protected $storage;
protected $folder_meta;
protected $folder_uids;
protected $root_meta;
static protected $types = array(
1 => '',
2 => 'mail.inbox',
3 => 'mail.drafts',
4 => 'mail.wastebasket',
5 => 'mail.sentitems',
6 => 'mail.outbox',
7 => 'task.default',
8 => 'event.default',
9 => 'contact.default',
10 => 'note.default',
11 => 'journal.default',
12 => 'mail',
13 => 'event',
14 => 'contact',
15 => 'task',
16 => 'journal',
17 => 'note',
);
static protected $classes = array(
Syncroton_Data_Factory::CLASS_CALENDAR => 'event',
Syncroton_Data_Factory::CLASS_CONTACTS => 'contact',
Syncroton_Data_Factory::CLASS_EMAIL => 'mail',
Syncroton_Data_Factory::CLASS_NOTES => 'note',
Syncroton_Data_Factory::CLASS_TASKS => 'task',
);
const ROOT_MAILBOX = 'INBOX';
// const ROOT_MAILBOX = '';
const ASYNC_KEY = '/private/vendor/kolab/activesync';
const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync_backend The one and only instance
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_sync_backend;
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Class initialization
*/
public function startup()
{
$this->storage = rcube::get_instance()->get_storage();
// @TODO: reset cache? if we do this for every request the cache would be useless
// There's no session here
//$this->storage->clear_cache('mailboxes.', true);
// set additional header used by libkolab
$this->storage->set_options(array(
// @TODO: there can be Roundcube plugins defining additional headers,
// we maybe would need to add them here
'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION',
'skip_deleted' => true,
'threading' => false,
));
// Disable paging
$this->storage->set_pagesize(999999);
}
/**
* List known devices
*
* @return array Device list as hash array
*/
public function devices_list()
{
if ($this->root_meta === null) {
// @TODO: consider server annotation instead of INBOX
if ($meta = $this->storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) {
$this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]);
}
else {
$this->root_meta = array();
}
}
if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) {
return $this->root_meta['DEVICE'];
}
return array();
}
/**
* Get list of folders available for sync
*
* @param string $deviceid Device identifier
* @param string $type Folder 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)
{
// get all folders of specified type
$folders = kolab_storage::list_folders('', '*', $type, false, $typedata);
// get folders activesync config
$folderdata = $this->folder_meta();
if (!is_array($folders) || !is_array($folderdata)) {
return false;
}
$folders_list = array();
// 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;
}
// force numeric folder name to be a string (T1283)
$folder = (string) $folder;
if (!empty($type) && !in_array($folder, $folders)) {
continue;
}
// Activesync folder identifier (serverId)
- $folder_type = ($typedata[$folder] ?? null) ?: 'mail';
+ $folder_type = !empty($typedata[$folder]) ? $typedata[$folder] : 'mail';
$folder_id = self::folder_id($folder, $folder_type);
$folders_list[$folder_id] = $this->folder_data($folder, $folder_type);
}
if ($flat_mode) {
$folders_list = $this->folders_list_flat($folders_list, $type, $typedata);
}
return $folders_list;
}
/**
* Converts list of folders to a "flat" list
*/
private function folders_list_flat($folders, $type, $typedata)
{
$delim = $this->storage->get_hierarchy_delimiter();
foreach ($folders as $idx => $folder) {
if ($folder['parentId']) {
// for non-mail folders we make the list completely flat
if ($type != 'mail') {
$display_name = kolab_storage::object_name($folder['imap_name']);
$display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
$folders[$idx]['parentId'] = 0;
$folders[$idx]['displayName'] = $display_name;
}
// for mail folders we modify only folders with non-existing parents
else if (!isset($folders[$folder['parentId']])) {
$items = explode($delim, $folder['imap_name']);
$parent = 0;
// find existing parent
while (count($items) > 0) {
array_pop($items);
$parent_name = implode($delim, $items);
$parent_type = !empty($typedata[$parent_name]) ? $typedata[$parent_name] : 'mail';
$parent_id = self::folder_id($parent_name, $parent_type);
if (isset($folders[$parent_id])) {
$parent = $parent_id;
break;
}
}
if (!$parent) {
$display_name = kolab_storage::object_name($folder['imap_name']);
$display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
}
else {
$parent_name = $folders[$parent_id]['imap_name'];
$display_name = substr($folder['imap_name'], strlen($parent_name)+1);
$display_name = rcube_charset::convert($display_name, 'UTF7-IMAP');
$display_name = str_replace($delim, ' » ', $display_name);
}
$folders[$idx]['parentId'] = $parent;
$folders[$idx]['displayName'] = $display_name;
}
}
}
return $folders;
}
/**
* Getter for folder metadata
*
* @return array|bool Hash array with meta data for each folder, False on backend failure
*/
public function folder_meta()
{
if (!isset($this->folder_meta)) {
// get folders activesync config
$folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY);
if (!is_array($folderdata)) {
return $this->folder_meta = false;
}
$this->folder_meta = array();
foreach ($folderdata as $folder => $meta) {
if ($asyncdata = $meta[self::ASYNC_KEY]) {
if ($metadata = $this->unserialize_metadata($asyncdata)) {
$this->folder_meta[$folder] = $metadata;
}
}
}
}
return $this->folder_meta;
}
/**
* Creates folder and subscribes to the device
*
* @param string $name Folder name (UTF7-IMAP)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
*
* @return bool True on success, False on failure
*/
public function folder_create($name, $type, $deviceid)
{
if ($this->storage->folder_exists($name)) {
$created = true;
}
else {
$type = self::type_activesync2kolab($type);
$created = kolab_storage::folder_create($name, $type, true);
}
if ($created) {
// Set ActiveSync subscription flag
$this->folder_set($name, $deviceid, 1);
return true;
}
return false;
}
/**
* Renames a folder
*
* @param string $old_name Old folder name (UTF7-IMAP)
* @param string $new_name New folder name (UTF7-IMAP)
* @param int $type Folder (ActiveSync) type
*
* @return bool True on success, False on failure
*/
public function folder_rename($old_name, $new_name, $type)
{
$this->folder_meta = null;
$type = self::type_activesync2kolab($type);
// don't use kolab_storage for moving mail folders
if (preg_match('/^mail/', $type)) {
return $this->storage->rename_folder($old_name, $new_name);
}
else {
return kolab_storage::folder_rename($old_name, $new_name);
}
}
/**
* Deletes folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $deviceid Device identifier
*
*/
public function folder_delete($name, $deviceid)
{
unset($this->folder_meta[$name]);
return kolab_storage::folder_delete($name);
}
/**
* Sets ActiveSync subscription flag on a folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $deviceid Device identifier
* @param int $flag Flag value (0|1|2)
*/
public function folder_set($name, $deviceid, $flag)
{
if (empty($deviceid)) {
return false;
}
// get folders activesync config
$metadata = $this->folder_meta();
if (!is_array($metadata)) {
return false;
}
$metadata = $metadata[$name];
if ($flag) {
if (empty($metadata)) {
$metadata = array();
}
if (empty($metadata['FOLDER'])) {
$metadata['FOLDER'] = array();
}
if (empty($metadata['FOLDER'][$deviceid])) {
$metadata['FOLDER'][$deviceid] = array();
}
// Z-Push uses:
// 1 - synchronize, no alarms
// 2 - synchronize with alarms
$metadata['FOLDER'][$deviceid]['S'] = $flag;
}
if (!$flag) {
unset($metadata['FOLDER'][$deviceid]['S']);
if (empty($metadata['FOLDER'][$deviceid])) {
unset($metadata['FOLDER'][$deviceid]);
}
if (empty($metadata['FOLDER'])) {
unset($metadata['FOLDER']);
}
if (empty($metadata)) {
$metadata = null;
}
}
// Return if nothing's been changed
if (!self::data_array_diff($this->folder_meta[$name], $metadata)) {
return true;
}
$this->folder_meta[$name] = $metadata;
return $this->storage->set_metadata($name, array(
self::ASYNC_KEY => $this->serialize_metadata($metadata)));
}
public function device_get($id)
{
$devices_list = $this->devices_list();
return $devices_list[$id] ?? null;
}
/**
* Registers new device on server
*
* @param array $device Device data
* @param string $id Device ID
*
* @return bool True on success, False on failure
*/
public function device_create($device, $id)
{
// Fill local cache
$this->devices_list();
// Some devices create dummy devices with name "validate" (#1109)
// This device entry is used in two initial requests, but later
// the device registers a real name. We can remove this dummy entry
// on new device creation
$this->device_delete('validate');
// Old Kolab_ZPush device parameters
// MODE: -1 | 0 | 1 (not set | flatmode | foldermode)
// TYPE: device type string
// ALIAS: user-friendly device name
// Syncroton (kolab_sync_backend_device) uses
// ID: internal identifier in syncroton database
// TYPE: device type string
// ALIAS: user-friendly device name
$metadata = $this->root_meta;
$metadata['DEVICE'][$id] = $device;
$metadata = array(self::ASYNC_KEY => $this->serialize_metadata($metadata));
$result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
if ($result) {
// Update local cache
$this->root_meta['DEVICE'][$id] = $device;
// subscribe default set of folders
$this->device_init_subscriptions($id);
}
return $result;
}
/**
* Device update.
*
* @param array $device Device data
* @param string $id Device ID
*
* @return bool True on success, False on failure
*/
public function device_update($device, $id)
{
$devices_list = $this->devices_list();
$old_device = $devices_list[$id];
if (!$old_device) {
return false;
}
// Do nothing if nothing is changed
if (!self::data_array_diff($old_device, $device)) {
return true;
}
$device = array_merge($old_device, $device);
$metadata = $this->root_meta;
$metadata['DEVICE'][$id] = $device;
$metadata = array(self::ASYNC_KEY => $this->serialize_metadata($metadata));
$result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
if ($result) {
// Update local cache
$this->root_meta['DEVICE'][$id] = $device;
}
return $result;
}
/**
* Device delete.
*
* @param string $id Device ID
*
* @return bool True on success, False on failure
*/
public function device_delete($id)
{
$device = $this->device_get($id);
if (!$device) {
return false;
}
unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]);
if (empty($this->root_meta['DEVICE'])) {
unset($this->root_meta['DEVICE']);
}
if (empty($this->root_meta['FOLDER'])) {
unset($this->root_meta['FOLDER']);
}
$metadata = $this->serialize_metadata($this->root_meta);
$metadata = array(self::ASYNC_KEY => $metadata);
// update meta data
$result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
if ($result) {
// remove device annotation for every folder
foreach ($this->folder_meta() as $folder => $meta) {
// skip root folder (already handled above)
if ($folder == self::ROOT_MAILBOX)
continue;
if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) {
unset($meta['FOLDER'][$id]);
if (empty($meta['FOLDER'])) {
unset($this->folder_meta[$folder]['FOLDER']);
unset($meta['FOLDER']);
}
if (empty($meta)) {
unset($this->folder_meta[$folder]);
$meta = null;
}
$metadata = array(self::ASYNC_KEY => $this->serialize_metadata($meta));
$res = $this->storage->set_metadata($folder, $metadata);
if ($res && $meta) {
$this->folder_meta[$folder] = $meta;
}
}
}
}
return $result;
}
/**
* Subscribe default set of folders on device registration
*/
private function device_init_subscriptions($deviceid)
{
// INBOX always exists
$this->folder_set('INBOX', $deviceid, 1);
$supported_types = array(
'mail.drafts',
'mail.wastebasket',
'mail.sentitems',
'mail.outbox',
'event.default',
'contact.default',
'note.default',
'task.default',
'event',
'contact',
'note',
'task',
'event.confidential',
'event.private',
'task.confidential',
'task.private',
);
// This default set can be extended by adding following values:
$modes = array(
'SUB_PERSONAL' => 1, // all subscribed folders in personal namespace
'ALL_PERSONAL' => 2, // all folders in personal namespace
'SUB_OTHER' => 4, // all subscribed folders in other users namespace
'ALL_OTHER' => 8, // all folders in other users namespace
'SUB_SHARED' => 16, // all subscribed folders in shared namespace
'ALL_SHARED' => 32, // all folders in shared namespace
);
$rcube = rcube::get_instance();
$config = $rcube->config;
$mode = (int) $config->get('activesync_init_subscriptions');
$folders = array();
// Subscribe to default folders
$foldertypes = kolab_storage::folders_typedata();
if (!empty($foldertypes)) {
$_foldertypes = array_intersect($foldertypes, $supported_types);
// get default folders
foreach ($_foldertypes as $folder => $type) {
// only personal folders
if ($this->storage->folder_namespace($folder) == 'personal') {
$flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
$this->folder_set($folder, $deviceid, $flag);
$folders[] = $folder;
}
}
}
// we're in default mode, exit
if (!$mode) {
return;
}
// below we support additionally all mail folders
$supported_types[] = 'mail';
$supported_types[] = 'mail.junkemail';
// get configured special folders
$special_folders = array();
$map = array(
'drafts' => 'mail.drafts',
'junk' => 'mail.junkemail',
'sent' => 'mail.sentitems',
'trash' => 'mail.wastebasket',
);
foreach ($map as $folder => $type) {
if ($folder = $config->get($folder . '_mbox')) {
$special_folders[$folder] = $type;
}
}
// get folders list(s)
if (($mode & $modes['ALL_PERSONAL']) || ($mode & $modes['ALL_OTHER']) || ($mode & $modes['ALL_SHARED'])) {
$all_folders = $this->storage->list_folders();
if (($mode & $modes['SUB_PERSONAL']) || ($mode & $modes['SUB_OTHER']) || ($mode & $modes['SUB_SHARED'])) {
$subscribed_folders = $this->storage->list_folders_subscribed();
}
}
else {
$all_folders = $this->storage->list_folders_subscribed();
}
foreach ($all_folders as $folder) {
// folder already subscribed
if (in_array($folder, $folders)) {
continue;
}
$type = ($foldertypes[$folder] ?? null) ?: 'mail';
if ($type == 'mail' && isset($special_folders[$folder])) {
$type = $special_folders[$folder];
}
if (!in_array($type, $supported_types)) {
continue;
}
$ns = strtoupper($this->storage->folder_namespace($folder));
// subscribe the folder according to configured mode
// and folder namespace/subscription status
if (($mode & $modes["ALL_$ns"])
|| (($mode & $modes["SUB_$ns"])
&& (!isset($subscribed_folders) || in_array($folder, $subscribed_folders)))
) {
$flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
$this->folder_set($folder, $deviceid, $flag);
}
}
}
/**
* Helper method to decode saved IMAP metadata
*/
private function unserialize_metadata($str)
{
if (!empty($str)) {
// Support old Z-Push annotation format
if ($str[0] != '{') {
$str = base64_decode($str);
}
$data = json_decode($str, true);
return $data;
}
return null;
}
/**
* Helper method to encode IMAP metadata for saving
*/
private function serialize_metadata($data)
{
if (!empty($data) && is_array($data)) {
$data = json_encode($data);
// $data = base64_encode($data);
return $data;
}
return null;
}
/**
* Returns Kolab folder type for specified ActiveSync type ID
*/
public static function type_activesync2kolab($type)
{
if (!empty(self::$types[$type])) {
return self::$types[$type];
}
return '';
}
/**
* Returns ActiveSync folder type for specified Kolab type
*/
public static function type_kolab2activesync($type)
{
$type = preg_replace('/\.(confidential|private)$/i', '', $type);
if ($key = array_search($type, self::$types)) {
return $key;
}
return key(self::$types);
}
/**
* Returns Kolab folder type for specified ActiveSync class name
*/
public static function class_activesync2kolab($class)
{
if (!empty(self::$classes[$class])) {
return self::$classes[$class];
}
return '';
}
/**
* Returns folder data in Syncroton format
*/
private function folder_data($folder, $type)
{
// Folder name parameters
$delim = $this->storage->get_hierarchy_delimiter();
$items = explode($delim, $folder);
$name = array_pop($items);
// Folder UID
$folder_id = $this->folder_id($folder, $type);
// Folder type
if (strcasecmp($folder, 'INBOX') === 0) {
// INBOX is always inbox, prevent from issues related with a change of
// folder type annotation (it can be initially unset).
$type = 2;
}
else {
$type = self::type_kolab2activesync($type);
// fix type, if there's no type annotation it's detected as UNKNOWN we'll use 'mail' (12)
if ($type == 1) {
$type = 12;
}
}
// Syncroton folder data array
return array(
'serverId' => $folder_id,
'parentId' => count($items) ? self::folder_id(implode($delim, $items)) : 0,
'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET),
'type' => $type,
// for internal use
'imap_name' => $folder,
);
}
/**
* Builds folder ID based on folder name
*/
public function folder_id($name, $type = null)
{
// 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 (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.
// 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';
}
else {
if ($type === null) {
$type = kolab_storage::folder_type($name);
}
$type = preg_replace('/\.(confidential|private)$/i', '', $type);
}
// 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 string Folder name (UTF7-IMAP)
*/
public function folder_id2name($id, $deviceid)
{
// check in cache first
if (!empty($this->folder_uids)) {
if (($name = array_search($id, $this->folder_uids)) !== false) {
return $name;
}
}
/*
@TODO: see folder_id()
// get folders unique identifier
$folderdata = $this->storage->get_metadata('*', self::UID_KEY);
foreach ((array)$folderdata as $folder => $data) {
if (!empty($data[self::UID_KEY])) {
$uid = $data[self::UID_KEY];
$this->folder_uids[$folder] = $uid;
if ($uid == $id) {
$name = $folder;
}
}
}
*/
// get all folders of specified type
$folderdata = $this->folder_meta();
if (!is_array($folderdata) || $id === null) {
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 = self::folder_id($folder)) {
$this->folder_uids[$folder] = $uid;
}
if ($uid === $id) {
$name = $folder;
}
}
return $name;
}
/**
*/
public function modseq_set($deviceid, $folderid, $synctime, $data)
{
$synctime = $synctime->format('Y-m-d H:i:s');
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$old_data = $this->modseq[$folderid][$synctime] ?? null;
if (empty($old_data)) {
$this->modseq[$folderid][$synctime] = $data;
$data = json_encode($data);
$db->set_option('ignore_key_errors', true);
$db->query("INSERT INTO `syncroton_modseq` (`device_id`, `folder_id`, `synctime`, `data`)"
." VALUES (?, ?, ?, ?)",
$deviceid, $folderid, $synctime, $data);
$db->set_option('ignore_key_errors', false);
}
}
public function modseq_get($deviceid, $folderid, $synctime)
{
$synctime = $synctime->format('Y-m-d H:i:s');
if (empty($this->modseq[$folderid][$synctime])) {
$this->modseq[$folderid] = array();
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$db->limitquery("SELECT `data`, `synctime` FROM `syncroton_modseq`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?"
." ORDER BY `synctime` DESC",
0, 1, $deviceid, $folderid, $synctime);
if ($row = $db->fetch_assoc()) {
$synctime = $row['synctime'];
// @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format
$this->modseq[$folderid][$synctime] = json_decode($row['data'], true);
}
// Cleanup: remove all records except the current one
$db->query("DELETE FROM `syncroton_modseq`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
$deviceid, $folderid, $synctime);
}
return @$this->modseq[$folderid][$synctime];
}
/**
* Set state of relation objects at specified point in time
*/
public function relations_state_set($deviceid, $folderid, $synctime, $relations)
{
$synctime = $synctime->format('Y-m-d H:i:s');
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$old_data = $this->relations[$folderid][$synctime] ?? null;
if (empty($old_data)) {
$this->relations[$folderid][$synctime] = $relations;
$data = rcube_charset::clean(json_encode($relations));
$db->set_option('ignore_key_errors', true);
$db->query("INSERT INTO `syncroton_relations_state`"
." (`device_id`, `folder_id`, `synctime`, `data`)"
." VALUES (?, ?, ?, ?)",
$deviceid, $folderid, $synctime, $data);
$db->set_option('ignore_key_errors', false);
}
}
/**
* Get state of relation objects at specified point in time
*/
public function relations_state_get($deviceid, $folderid, $synctime)
{
$synctime = $synctime->format('Y-m-d H:i:s');
if (empty($this->relations[$folderid][$synctime])) {
$this->relations[$folderid] = array();
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$db->limitquery("SELECT `data`, `synctime` FROM `syncroton_relations_state`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?"
." ORDER BY `synctime` DESC",
0, 1, $deviceid, $folderid, $synctime);
if ($row = $db->fetch_assoc()) {
$synctime = $row['synctime'];
// @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format
$this->relations[$folderid][$synctime] = json_decode($row['data'], true);
}
// Cleanup: remove all records except the current one
$db->query("DELETE FROM `syncroton_relations_state`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
$deviceid, $folderid, $synctime);
}
return @$this->relations[$folderid][$synctime];
}
/**
* Return last storage error
*/
public static function last_error()
{
return kolab_storage::$last_error;
}
/**
* Compares two arrays
*
* @param array $array1
* @param array $array2
*
* @return bool True if arrays differs, False otherwise
*/
private static function data_array_diff($array1, $array2)
{
if (!is_array($array1) || !is_array($array2)) {
return $array1 != $array2;
}
if (count($array1) != count($array2)) {
return true;
}
foreach ($array1 as $key => $val) {
if (!array_key_exists($key, $array2)) {
return true;
}
if ($val !== $array2[$key]) {
return true;
}
}
return false;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Sep 15, 5:26 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
287484
Default Alt Text
(110 KB)

Event Timeline