Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1974853
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
110 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment