Page MenuHomePhorge

No OneTemporary

Size
258 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 77f04ac..56f5808 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -1,44 +1,52 @@
<?php
// This file lists all ActiveSync-related configuration options
// Enables ActiveSync protocol debuging
$rcmail_config['activesync_debug'] = true;
// If specified all ActiveSync-related logs will be saved to this file
// Note: This doesn't change Roundcube Framework log locations
$rcmail_config['activesync_log_file'] = null;
// Type of ActiveSync cache. Supported values: 'db', 'apc' and 'memcache'.
// Note: This is only for some additional data like timezones mapping.
// Main ActiveSync cache uses Roundcube SQL database
$rcmail_config['activesync_cache'] = null;
// lifetime of ActiveSync cache
// possible units: s, m, h, d, w
$rcmail_config['activesync_cache_lifetime'] = '10d';
// List of global addressbooks (GAL)
// Note: If empty 'autocomplete_addressbooks' setting will be used
$rcmail_config['activesync_addressbooks'] = array();
// ActiveSync => Roundcube contact fields map for GAL search
/* Default: array(
'alias' => 'nickname',
'company' => 'organization',
'displayName' => 'name',
'emailAddress' => 'email',
'firstName' => 'firstname',
'lastName' => 'surname',
'mobilePhone' => 'phone.mobile',
'office' => 'office',
'picture' => 'photo',
'phone' => 'phone',
'title' => 'jobtitle',
);
*/
$rcmail_config['activesync_gal_fieldmap'] = null;
// List of Roundcube plugins
// WARNING: Not all plugins used in Roundcube can be listed here
$rcmail_config['activesync_plugins'] = array();
+
+// Defines for how many seconds we'll sleep between every
+// action for detecting changes in folders. Default: 60
+$rcmail_config['activesync_ping_timeout'] = 60;
+
+// We start detecting changes n seconds since the last sync of a folder
+// Default: 180
+$rcmail_config['activesync_quiet_time'] = 180;
diff --git a/docs/SQL/mysql/2013011200.sql b/docs/SQL/mysql/2013011200.sql
new file mode 100644
index 0000000..bcf8453
--- /dev/null
+++ b/docs/SQL/mysql/2013011200.sql
@@ -0,0 +1,11 @@
+ALTER TABLE `syncroton_device` ADD `lastsynccollection` longblob DEFAULT NULL;
+ALTER TABLE `syncroton_folder` ADD `supportedfields` longblob DEFAULT NULL;
+ALTER TABLE `syncroton_data` CHANGE `type` `class` varchar(40) NOT NULL;
+CREATE TABLE `syncroton_data_folder` (
+ `id` varchar(40) NOT NULL,
+ `type` int(11) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `owner_id` varchar(40) NOT NULL,
+ `parent_id` varchar(40) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
diff --git a/docs/syncroton.sql b/docs/syncroton.sql
index 5e140d9..b35a6b4 100644
--- a/docs/syncroton.sql
+++ b/docs/syncroton.sql
@@ -1,125 +1,136 @@
CREATE TABLE IF NOT EXISTS `syncroton_policy` (
`id` varchar(40) NOT NULL,
`name` varchar(255) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`policy_key` varchar(64) NOT NULL,
`allow_bluetooth` int(11) DEFAULT NULL,
`allow_browser` int(11) DEFAULT NULL,
`allow_camera` int(11) DEFAULT NULL,
`allow_consumer_email` int(11) DEFAULT NULL,
`allow_desktop_sync` int(11) DEFAULT NULL,
`allow_h_t_m_l_email` int(11) DEFAULT NULL,
`allow_internet_sharing` int(11) DEFAULT NULL,
`allow_ir_d_a` int(11) DEFAULT NULL,
`allow_p_o_p_i_m_a_p_email` int(11) DEFAULT NULL,
`allow_remote_desktop` int(11) DEFAULT NULL,
`allow_simple_device_password` int(11) DEFAULT NULL,
`allow_s_m_i_m_e_encryption_algorithm_negotiation` int(11) DEFAULT NULL,
`allow_s_m_i_m_e_soft_certs` int(11) DEFAULT NULL,
`allow_storage_card` int(11) DEFAULT NULL,
`allow_text_messaging` int(11) DEFAULT NULL,
`allow_unsigned_applications` int(11) DEFAULT NULL,
`allow_unsigned_installation_packages` int(11) DEFAULT NULL,
`allow_wifi` int(11) DEFAULT NULL,
`alphanumeric_device_password_required` int(11) DEFAULT NULL,
`approved_application_list` varchar(255) DEFAULT NULL,
`attachments_enabled` int(11) DEFAULT NULL,
`device_password_enabled` int(11) DEFAULT NULL,
`device_password_expiration` int(11) DEFAULT NULL,
`device_password_history` int(11) DEFAULT NULL,
`max_attachment_size` int(11) DEFAULT NULL,
`max_calendar_age_filter` int(11) DEFAULT NULL,
`max_device_password_failed_attempts` int(11) DEFAULT NULL,
`max_email_age_filter` int(11) DEFAULT NULL,
`max_email_body_truncation_size` int(11) DEFAULT NULL,
`max_email_h_t_m_l_body_truncation_size` int(11) DEFAULT NULL,
`max_inactivity_time_device_lock` int(11) DEFAULT NULL,
`min_device_password_complex_characters` int(11) DEFAULT NULL,
`min_device_password_length` int(11) DEFAULT NULL,
`password_recovery_enabled` int(11) DEFAULT NULL,
`require_device_encryption` int(11) DEFAULT NULL,
`require_encrypted_s_m_i_m_e_messages` int(11) DEFAULT NULL,
`require_encryption_s_m_i_m_e_algorithm` int(11) DEFAULT NULL,
`require_manual_sync_when_roaming` int(11) DEFAULT NULL,
`require_signed_s_m_i_m_e_algorithm` int(11) DEFAULT NULL,
`require_signed_s_m_i_m_e_messages` int(11) DEFAULT NULL,
`require_storage_card_encryption` int(11) DEFAULT NULL,
`unapproved_in_r_o_m_application_list` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `syncroton_device` (
`id` varchar(40) NOT NULL,
`deviceid` varchar(64) NOT NULL,
`devicetype` varchar(64) NOT NULL,
`owner_id` varchar(40) NOT NULL,
`acsversion` varchar(40) NOT NULL,
`policykey` varchar(64) DEFAULT NULL,
`policy_id` varchar(40) DEFAULT NULL,
`useragent` varchar(255) DEFAULT NULL,
`imei` varchar(255) DEFAULT NULL,
`model` varchar(255) DEFAULT NULL,
`friendlyname` varchar(255) DEFAULT NULL,
`os` varchar(255) DEFAULT NULL,
`oslanguage` varchar(255) DEFAULT NULL,
`phonenumber` varchar(255) DEFAULT NULL,
`pinglifetime` int(11) DEFAULT NULL,
`remotewipe` int(11) DEFAULT '0',
`pingfolder` longblob,
+ `lastsynccollection` longblob DEFAULT NULL,
`contactsfilter_id` varchar(40) DEFAULT NULL,
`calendarfilter_id` varchar(40) DEFAULT NULL,
`tasksfilter_id` varchar(40) DEFAULT NULL,
`emailfilter_id` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `owner_id--deviceid` (`owner_id`, `deviceid`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `syncroton_folder` (
`id` varchar(40) NOT NULL,
`device_id` varchar(40) NOT NULL,
`class` varchar(64) NOT NULL,
`folderid` varchar(254) NOT NULL,
`parentid` varchar(254) DEFAULT NULL,
`displayname` varchar(254) NOT NULL,
`type` int(11) NOT NULL,
`creation_time` datetime NOT NULL,
`lastfiltertype` int(11) DEFAULT NULL,
+ `supportedfields` longblob DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `device_id--class--folderid` (`device_id`(40),`class`(40),`folderid`(40)),
KEY `folderstates::device_id--devices::id` (`device_id`),
CONSTRAINT `folderstates::device_id--devices::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `syncroton_synckey` (
`id` varchar(40) NOT NULL,
`device_id` varchar(40) NOT NULL DEFAULT '',
`type` varchar(64) NOT NULL DEFAULT '',
`counter` int(11) NOT NULL DEFAULT '0',
`lastsync` datetime DEFAULT NULL,
`pendingdata` longblob,
PRIMARY KEY (`id`),
UNIQUE KEY `device_id--type--counter` (`device_id`,`type`,`counter`),
CONSTRAINT `syncroton_synckey::device_id--syncroton_device::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `syncroton_content` (
`id` varchar(40) NOT NULL,
`device_id` varchar(40) DEFAULT NULL,
`folder_id` varchar(40) DEFAULT NULL,
`contentid` varchar(64) DEFAULT NULL,
`creation_time` datetime DEFAULT NULL,
`creation_synckey` int(11) NOT NULL,
`is_deleted` tinyint(1) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `device_id--folder_id--contentid` (`device_id`(40),`folder_id`(40),`contentid`(40)),
KEY `syncroton_contents::device_id` (`device_id`),
CONSTRAINT `syncroton_contents::device_id--syncroton_device::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `syncroton_data` (
`id` varchar(40) NOT NULL,
- `type` varchar(40) NOT NULL,
+ `class` varchar(40) NOT NULL,
`folder_id` varchar(40) NOT NULL,
`data` longblob,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
+
+CREATE TABLE IF NOT EXISTS `syncroton_data_folder` (
+ `id` varchar(40) NOT NULL,
+ `type` int(11) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `owner_id` varchar(40) NOT NULL,
+ `parent_id` varchar(40) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php
index d87cd9d..e7eda47 100644
--- a/lib/ext/Syncroton/Command/FolderCreate.php
+++ b/lib/ext/Syncroton/Command/FolderCreate.php
@@ -1,110 +1,111 @@
<?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 FolderSync command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml
{
protected $_defaultNameSpace = 'uri:FolderHierarchy';
protected $_documentElement = 'FolderCreate';
/**
*
* @var Syncroton_Model_Folder
*/
protected $_folder;
/**
* parse FolderCreate request
*
*/
public function handle()
{
$xml = simplexml_import_dom($this->_requestBody);
$syncKey = (int)$xml->SyncKey;
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey");
if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) {
return;
}
$folder = new Syncroton_Model_Folder($xml);
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " parentId: {$folder->parentId} displayName: {$folder->displayName}");
switch($folder->type) {
case Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED:
$folder->class = Syncroton_Data_Factory::CLASS_CALENDAR;
break;
case Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED:
$folder->class = Syncroton_Data_Factory::CLASS_CONTACTS;
break;
case Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED:
$folder->class = Syncroton_Data_Factory::CLASS_EMAIL;
break;
case Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED:
$folder->class = Syncroton_Data_Factory::CLASS_TASKS;
break;
default:
throw new Syncroton_Exception_UnexpectedValue('invalid type defined');
break;
}
- $folder->deviceId = $this->_device;
- $folder->creationTime = $this->_syncTimeStamp;
-
- $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
- $this->_folder = $dataController->createFolder($folder);
+ $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
+
+ $this->_folder = $dataController->createFolder($folder);
+ $this->_folder->class = $folder->class;
+ $this->_folder->deviceId = $this->_device;
+ $this->_folder->creationTime = $this->_syncTimeStamp;
$this->_folderBackend->create($this->_folder);
}
/**
* generate FolderCreate response
*/
public function getResponse()
{
$folderCreate = $this->_outputDom->documentElement;
if (!$this->_syncState instanceof Syncroton_Model_SyncState) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed.");
$folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_INVALID_SYNC_KEY));
} else {
$this->_syncState->counter++;
$this->_syncState->lastsync = $this->_syncTimeStamp;
// store folder in state backend
$this->_syncStateBackend->update($this->_syncState);
// create xml output
$folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_SUCCESS));
$folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter));
$folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $this->_folder->serverId));
}
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php
index 8c4917d..84e761e 100644
--- a/lib/ext/Syncroton/Command/FolderSync.php
+++ b/lib/ext/Syncroton/Command/FolderSync.php
@@ -1,230 +1,236 @@
<?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 FolderSync command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml
{
const STATUS_SUCCESS = 1;
const STATUS_FOLDER_EXISTS = 2;
const STATUS_IS_SPECIAL_FOLDER = 3;
const STATUS_FOLDER_NOT_FOUND = 4;
const STATUS_PARENT_FOLDER_NOT_FOUND = 5;
const STATUS_SERVER_ERROR = 6;
const STATUS_ACCESS_DENIED = 7;
const STATUS_REQUEST_TIMED_OUT = 8;
const STATUS_INVALID_SYNC_KEY = 9;
const STATUS_MISFORMATTED = 10;
const STATUS_UNKNOWN_ERROR = 11;
/**
* some usefull constants for working with the xml files
*/
const FOLDERTYPE_GENERIC_USER_CREATED = 1;
const FOLDERTYPE_INBOX = 2;
const FOLDERTYPE_DRAFTS = 3;
const FOLDERTYPE_DELETEDITEMS = 4;
const FOLDERTYPE_SENTMAIL = 5;
const FOLDERTYPE_OUTBOX = 6;
const FOLDERTYPE_TASK = 7;
const FOLDERTYPE_CALENDAR = 8;
const FOLDERTYPE_CONTACT = 9;
const FOLDERTYPE_NOTE = 10;
const FOLDERTYPE_JOURNAL = 11;
const FOLDERTYPE_MAIL_USER_CREATED = 12;
const FOLDERTYPE_CALENDAR_USER_CREATED = 13;
const FOLDERTYPE_CONTACT_USER_CREATED = 14;
const FOLDERTYPE_TASK_USER_CREATED = 15;
const FOLDERTYPE_JOURNAL_USER_CREATED = 16;
const FOLDERTYPE_NOTES_USER_CREATED = 17;
const FOLDERTYPE_UNKOWN = 18;
protected $_defaultNameSpace = 'uri:FolderHierarchy';
protected $_documentElement = 'FolderSync';
protected $_classes = array(
Syncroton_Data_Factory::CLASS_CALENDAR,
Syncroton_Data_Factory::CLASS_CONTACTS,
Syncroton_Data_Factory::CLASS_EMAIL,
Syncroton_Data_Factory::CLASS_TASKS
);
/**
* @var string
*/
protected $_syncKey;
/**
* parse FolderSync request
*
*/
public function handle()
{
$xml = simplexml_import_dom($this->_requestBody);
$syncKey = (int)$xml->SyncKey;
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey");
if ($syncKey === 0) {
$this->_syncState = new Syncroton_Model_SyncState(array(
'device_id' => $this->_device,
'counter' => 0,
'type' => 'FolderSync',
'lastsync' => $this->_syncTimeStamp
));
// reset state of foldersync
$this->_syncStateBackend->resetState($this->_device, 'FolderSync');
return;
}
if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) {
$this->_syncStateBackend->resetState($this->_device, 'FolderSync');
}
}
/**
* generate FolderSync response
*
* @todo changes are missing in response (folder got renamed for example)
*/
public function getResponse()
{
$folderSync = $this->_outputDom->documentElement;
// invalid synckey provided
if (!$this->_syncState instanceof Syncroton_Model_SyncState) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed.");
$folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY));
return $this->_outputDom;
}
+ // send headers from options command also when FolderSync SyncKey is 0
+ if ($this->_syncState->counter == 0) {
+ $optionsCommand = new Syncroton_Command_Options();
+ $this->_headers = array_merge($this->_headers, $optionsCommand->getHeaders());
+ }
+
$adds = array();
$deletes = array();
foreach($this->_classes as $class) {
try {
$dataController = Syncroton_Data_Factory::factory($class, $this->_device, $this->_syncTimeStamp);
} catch (Exception $e) {
// backend not defined
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " no data backend defined for class: " . $class);
continue;
}
try {
// retrieve all folders available in data backend
$serverFolders = $dataController->getAllFolders();
// retrieve all folders sent to client
$clientFolders = $this->_folderBackend->getFolderState($this->_device, $class);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getTraceAsString());
// The Status element is global for all collections. If one collection fails,
// a failure status MUST be returned for all collections.
if ($e instanceof Syncroton_Exception_Status) {
$status = $e->getCode();
} else {
$status = Syncroton_Exception_Status_FolderSync::UNKNOWN_ERROR;
}
$folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', $status));
return $this->_outputDom;
}
$serverFoldersIds = array_keys($serverFolders);
// is this the first sync?
if ($this->_syncState->counter == 0) {
$clientFoldersIds = array();
} else {
$clientFoldersIds = array_keys($clientFolders);
}
// calculate added entries
$serverDiff = array_diff($serverFoldersIds, $clientFoldersIds);
foreach ($serverDiff as $serverFolderId) {
// have we created a folderObject in syncroton_folder before?
if (isset($clientFolders[$serverFolderId])) {
$add = $clientFolders[$serverFolderId];
} else {
$add = $serverFolders[$serverFolderId];
$add->creationTime = $this->_syncTimeStamp;
$add->deviceId = $this->_device;
unset($add->id);
}
$add->class = $class;
$adds[] = $add;
}
// calculate deleted entries
$serverDiff = array_diff($clientFoldersIds, $serverFoldersIds);
foreach ($serverDiff as $serverFolderId) {
$deletes[] = $clientFolders[$serverFolderId];
}
}
$folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS));
$count = count($adds) + /*count($changes) + */count($deletes);
if($count > 0) {
$this->_syncState->counter++;
$this->_syncState->lastsync = $this->_syncTimeStamp;
}
// create xml output
$folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter));
$changes = $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Changes'));
$changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Count', $count));
foreach($adds as $folder) {
$add = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Add'));
$folder->appendXML($add, $this->_device);
// store folder in backend
if (empty($folder->id)) {
$this->_folderBackend->create($folder);
}
}
foreach($deletes as $folder) {
$delete = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Delete'));
$delete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder->serverId));
$this->_folderBackend->delete($folder);
}
if (empty($this->_syncState->id)) {
$this->_syncStateBackend->create($this->_syncState);
} else {
$this->_syncStateBackend->update($this->_syncState);
}
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/GetItemEstimate.php b/lib/ext/Syncroton/Command/GetItemEstimate.php
index 79ed73d..5eb0f4c 100644
--- a/lib/ext/Syncroton/Command/GetItemEstimate.php
+++ b/lib/ext/Syncroton/Command/GetItemEstimate.php
@@ -1,151 +1,147 @@
<?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 GetItemEstimate command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_GetItemEstimate extends Syncroton_Command_Wbxml
{
const STATUS_SUCCESS = 1;
const STATUS_INVALID_COLLECTION = 2;
const STATUS_SYNC_STATE_NOT_PRIMED = 3;
const STATUS_INVALID_SYNC_KEY = 4;
protected $_defaultNameSpace = 'uri:ItemEstimate';
protected $_documentElement = 'GetItemEstimate';
/**
* list of collections
*
* @var array
*/
protected $_collections = array();
/**
*/
public function handle()
{
$xml = simplexml_import_dom($this->_requestBody);
foreach ($xml->Collections->Collection as $xmlCollection) {
// fetch values from a different namespace
$airSyncValues = $xmlCollection->children('uri:AirSync');
$collectionData = array(
'syncKey' => (int)$airSyncValues->SyncKey,
'collectionId' => (string) $xmlCollection->CollectionId,
'class' => isset($xmlCollection->Class) ? (string) $xmlCollection->Class : null,
'filterType' => isset($airSyncValues->Options) && isset($airSyncValues->Options->FilterType) ? (int)$airSyncValues->Options->FilterType : 0
);
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " synckey is {$collectionData['syncKey']} class: {$collectionData['class']} collectionid: {$collectionData['collectionId']} filtertype: {$collectionData['filterType']}");
try {
// does the folder exist?
$collectionData['folder'] = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
$collectionData['folder']->lastfiltertype = $collectionData['filterType'];
if($collectionData['syncKey'] === 0) {
$collectionData['syncState'] = new Syncroton_Model_SyncState(array(
'device_id' => $this->_device,
'counter' => 0,
'type' => $collectionData['folder'],
'lastsync' => $this->_syncTimeStamp
));
// reset sync state for this folder
$this->_syncStateBackend->resetState($this->_device, $collectionData['folder']);
$this->_contentStateBackend->resetState($this->_device, $collectionData['folder']);
} else {
$collectionData['syncState'] = $this->_syncStateBackend->validate($this->_device, $collectionData['folder'], $collectionData['syncKey']);
}
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage());
}
$this->_collections[$collectionData['collectionId']] = $collectionData;
}
}
/**
* (non-PHPdoc)
* @see Syncroton_Command_Wbxml::getResponse()
*/
public function getResponse()
{
$itemEstimate = $this->_outputDom->documentElement;
foreach($this->_collections as $collectionData) {
$response = $itemEstimate->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Response'));
// invalid collectionid provided
if (empty($collectionData['folder'])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder does not exist");
$response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_INVALID_COLLECTION));
$collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
- $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 0));
} elseif (! ($collectionData['syncState'] instanceof Syncroton_Model_ISyncState)) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey ${collectionData['syncKey']} provided");
/*
* Android phones (and maybe others) don't take care about status 4(INVALID_SYNC_KEY)
* To solve the problem we always return status 1(SUCCESS) even the sync key is invalid with Estimate set to 1.
* This way the phone gets forced to sync. Handling invalid synckeys during sync command works without any problems.
*
$response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_INVALID_SYNC_KEY));
$collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
- $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 0));
*/
$response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_SUCCESS));
$collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
- $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 1));
} else {
$dataController = Syncroton_Data_Factory::factory($collectionData['folder']->class, $this->_device, $this->_syncTimeStamp);
$response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_SUCCESS));
$collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
- $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));
if($collectionData['syncState']->counter === 0) {
// this is the first sync. in most cases there are data on the server.
$count = count($dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']));
} else {
$count = $dataController->getCountOfChanges($this->_contentStateBackend, $collectionData['folder'], $collectionData['syncState']);
}
$collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', $count));
}
// folderState can be NULL in case of not existing folder
if (isset($collectionData['folder'])) {
$this->_folderBackend->update($collectionData['folder']);
}
}
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/ICommand.php b/lib/ext/Syncroton/Command/ICommand.php
index fd286d4..d6edb23 100644
--- a/lib/ext/Syncroton/Command/ICommand.php
+++ b/lib/ext/Syncroton/Command/ICommand.php
@@ -1,38 +1,45 @@
<?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>
*/
/**
* interface for all Syncroton command classes
*
* @package Syncroton
* @subpackage Command
*/
interface Syncroton_Command_ICommand
{
/**
* constructor of this class
*
* @param resource $_requestBody
* @param Syncroton_Model_IDevice $_device
* @param string $_policyKey
*/
public function __construct($_requestBody, Syncroton_Model_IDevice $_device, $_policyKey);
/**
* process the incoming data
*/
public function handle();
/**
* create the response
*/
public function getResponse();
+
+ /**
+ * return headers of command
+ *
+ * @return array list of headers
+ */
+ public function getHeaders();
}
diff --git a/lib/ext/Syncroton/Command/ItemOperations.php b/lib/ext/Syncroton/Command/ItemOperations.php
index a89bd5f..c3ee287 100644
--- a/lib/ext/Syncroton/Command/ItemOperations.php
+++ b/lib/ext/Syncroton/Command/ItemOperations.php
@@ -1,169 +1,192 @@
<?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();
/**
* parse MoveItems request
*
*/
public function handle()
{
$xml = simplexml_import_dom($this->_requestBody);
if (isset($xml->Fetch)) {
foreach ($xml->Fetch as $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;
}
}
}
}
$this->_fetches[] = $fetchArray;
}
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " fetches: " . print_r($this->_fetches, true));
}
/**
* generate ItemOperations response
+ *
+ * @todo add multipart support to all types of fetches
*/
public function getResponse()
{
// add aditional namespaces
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase' , 'uri:AirSyncBase');
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSync' , 'uri:AirSync');
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Search' , 'uri:Search');
$itemOperations = $this->_outputDom->documentElement;
$itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS));
$response = $itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Response'));
foreach ($this->_fetches as $fetch) {
$fetchTag = $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Fetch'));
try {
$dataController = Syncroton_Data_Factory::factory($fetch['store'], $this->_device, $this->_syncTimeStamp);
if (isset($fetch['collectionId'])) {
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncroton_Command_ItemOperations::STATUS_SUCCESS));
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $fetch['collectionId']));
$fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $fetch['serverId']));
$properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties');
$dataController
->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $fetch['collectionId'], 'options' => $fetch['options'])), $fetch['serverId'])
->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');
- $dataController
- ->getFileReference($fetch['fileReference'])
- ->appendXML($properties, $this->_device);
+
+ $fileReference = $dataController->getFileReference($fetch['fileReference']);
+
+ // unset data field and move content to stream
+ if ($this->_requestParameters['acceptMultipart'] == true) {
+ $this->_headers['Content-Type'] = 'application/vnd.ms-sync.multipart';
+
+ $partStream = fopen("php://temp", 'r+');
+
+ if (is_resource($fileReference->data)) {
+ stream_copy_to_stream($fileReference->data, $partStream);
+ } else {
+ fwrite($partStream, $fileReference->data);
+ }
+
+ unset($fileReference->data);
+
+ $this->_parts[] = $partStream;
+
+ $fileReference->part = count($this->_parts);
+ }
+
+ $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));
}
}
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/MeetingResponse.php b/lib/ext/Syncroton/Command/MeetingResponse.php
new file mode 100644
index 0000000..47eb12c
--- /dev/null
+++ b/lib/ext/Syncroton/Command/MeetingResponse.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package Syncroton
+ * @subpackage Command
+ * @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 MeetingResponse command
+ *
+ * @package Syncroton
+ * @subpackage Command
+ */
+class Syncroton_Command_MeetingResponse extends Syncroton_Command_Wbxml
+{
+ protected $_results = array();
+
+ protected $_defaultNameSpace = 'uri:MeetingResponse';
+ protected $_documentElement = 'MeetingResponse';
+
+ /**
+ * parse MeetingResponse request
+ */
+ public function handle()
+ {
+ $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_CALENDAR, $this->_device, $this->_syncTimeStamp);
+
+ $xml = simplexml_import_dom($this->_requestBody);
+
+ foreach ($xml as $meetingResponse) {
+ $request = new Syncroton_Model_MeetingResponse($meetingResponse);
+
+ try {
+ $calendarId = $dataController->setAttendeeStatus($request);
+
+ $this->_results[] = array(
+ 'calendarId' => $calendarId,
+ 'request' => $request,
+ 'status' => 1
+ );
+
+ } catch (Syncroton_Exception_Status_MeetingResponse $sesmr) {
+ $this->_results[] = array(
+ 'request' => $request,
+ 'status' => $sesmr->getCode()
+ );
+ }
+ }
+
+ if ($this->_logger instanceof Zend_Log)
+ $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " results: " . print_r($this->_results, true));
+ }
+
+ /**
+ * generate MeetingResponse response
+ */
+ public function getResponse()
+ {
+ $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Search', 'uri:Search');
+
+ $meetingResponse = $this->_outputDom->documentElement;
+
+ foreach ($this->_results as $result) {
+ $resultElement = $this->_outputDom->createElementNS('uri:MeetingResponse', 'Result');
+
+ if (isset($result['request']->requestId)) {
+ $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'RequestId', $result['request']->requestId));
+ } elseif (isset($result['request']->longId)) {
+ $resultElement->appendChild($this->_outputDom->createElementNS('uri:Search', 'LongId', $result['request']->longId));
+ }
+
+ $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'Status', $result['status']));
+
+ if (isset($result['calendarId'])) {
+ $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'CalendarId', $result['calendarId']));
+ }
+
+ $meetingResponse->appendChild($resultElement);
+ }
+
+ return $this->_outputDom;
+ }
+}
diff --git a/lib/ext/Syncroton/Command/Options.php b/lib/ext/Syncroton/Command/Options.php
index 252899c..f126cc5 100644
--- a/lib/ext/Syncroton/Command/Options.php
+++ b/lib/ext/Syncroton/Command/Options.php
@@ -1,32 +1,34 @@
<?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 http options request
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Options
{
/**
* this function generates the response for the client
*
* @return void
*/
- public function getResponse()
+ public function getHeaders()
{
// same header like Exchange 2xxx???
- header('MS-Server-ActiveSync: 14.00.0536.000');
- header("MS-ASProtocolVersions: 2.5,12.0,12.1,14.0,14.1");
- header("MS-ASProtocolCommands: CreateCollection,DeleteCollection,FolderCreate,FolderDelete,FolderSync,FolderUpdate,GetAttachment,GetHierarchy,GetItemEstimate,ItemOperations,MeetingResponse,MoveCollection,MoveItems,Provision,ResolveRecipients,Ping,SendMail,Search,Settings,SmartForward,SmartReply,Sync,ValidateCert");
+ return array(
+ 'MS-Server-ActiveSync' => '14.00.0536.000',
+ 'MS-ASProtocolVersions' => '2.5,12.0,12.1,14.0,14.1',
+ 'MS-ASProtocolCommands' => 'CreateCollection,DeleteCollection,FolderCreate,FolderDelete,FolderSync,FolderUpdate,GetAttachment,GetHierarchy,GetItemEstimate,ItemOperations,MeetingResponse,MoveCollection,MoveItems,Provision,ResolveRecipients,Ping,SendMail,Search,Settings,SmartForward,SmartReply,Sync,ValidateCert'
+ );
}
}
diff --git a/lib/ext/Syncroton/Command/Ping.php b/lib/ext/Syncroton/Command/Ping.php
index 3270d53..1bb7da8 100644
--- a/lib/ext/Syncroton/Command/Ping.php
+++ b/lib/ext/Syncroton/Command/Ping.php
@@ -1,198 +1,192 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Ping command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
{
const STATUS_NO_CHANGES_FOUND = 1;
const STATUS_CHANGES_FOUND = 2;
const STATUS_MISSING_PARAMETERS = 3;
const STATUS_REQUEST_FORMAT_ERROR = 4;
const STATUS_INTERVAL_TO_GREAT_OR_SMALL = 5;
const STATUS_TO_MUCH_FOLDERS = 6;
const STATUS_FOLDER_NOT_FOUND = 7;
const STATUS_GENERAL_ERROR = 8;
protected $_skipValidatePolicyKey = true;
protected $_changesDetected = false;
- /**
- *
- * @var int
- */
- public static $pingTimeout = 60;
-
- /**
- *
- * @var int
- */
- public static $quietTime = 180;
-
/**
* @var Syncroton_Backend_StandAlone_Abstract
*/
protected $_dataBackend;
protected $_defaultNameSpace = 'uri:Ping';
protected $_documentElement = 'Ping';
protected $_foldersWithChanges = array();
/**
* process the XML file and add, change, delete or fetches data
*
* @todo can we get rid of LIBXML_NOWARNING
* @todo we need to stored the initial data for folders and lifetime as the phone is sending them only when they change
* @return resource
*/
public function handle()
{
$intervalStart = time();
$status = self::STATUS_NO_CHANGES_FOUND;
// the client does not send a wbxml document, if the Ping parameters did not change compared with the last request
if($this->_requestBody instanceof DOMDocument) {
$xml = simplexml_import_dom($this->_requestBody);
$xml->registerXPathNamespace('Ping', 'Ping');
if(isset($xml->HeartBeatInterval)) {
$this->_device->pinglifetime = (int)$xml->HeartBeatInterval;
}
if(isset($xml->Folders->Folder)) {
$folders = array();
foreach ($xml->Folders->Folder as $folderXml) {
try {
// does the folder exist?
$folder = $this->_folderBackend->getFolder($this->_device, (string)$folderXml->Id);
$folders[$folder->id] = $folder;
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage());
$status = self::STATUS_FOLDER_NOT_FOUND;
break;
}
}
$this->_device->pingfolder = serialize(array_keys($folders));
}
$this->_device = $this->_deviceBackend->update($this->_device);
}
$lifeTime = $this->_device->pinglifetime;
#Tinebase_Core::setExecutionLifeTime($lifeTime);
$intervalEnd = $intervalStart + $lifeTime;
$secondsLeft = $intervalEnd;
$folders = unserialize($this->_device->pingfolder);
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) {
$folderWithChanges = array();
do {
- sleep(self::$pingTimeout);
+ // take a break to save battery lifetime
+ sleep(Syncroton_Registry::getPingTimeout());
$now = new DateTime('now', new DateTimeZone('utc'));
foreach ((array) $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 self::$quietTime seconds ago
- if (($now->getTimestamp() - $syncState->lastsync->getTimestamp()) < self::$quietTime) {
+ // 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->getCountOfChanges($this->_contentStateBackend, $folder, $syncState);
} catch (Syncroton_Exception_NotFound $e) {
// folder got never synchronized to client
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' syncstate not found. enforce sync for folder: ' . $folder->serverId);
$foundChanges = true;
}
if ($foundChanges == true) {
$this->_foldersWithChanges[] = $folder;
$status = self::STATUS_CHANGES_FOUND;
}
}
if ($status != self::STATUS_NO_CHANGES_FOUND) {
break;
}
$secondsLeft = $intervalEnd - time();
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " seconds left: " . $secondsLeft);
-
- } while ($secondsLeft > 0);
+
+ // See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
+ //
+ // break if there are less than PingTimeout + 10 seconds left for the next loop
+ // otherwise the response will be returned after the client has finished his Ping
+ // request already maybe
+ } while ($secondsLeft > (Syncroton_Registry::getPingTimeout() + 10));
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " Lifetime: $lifeTime SecondsLeft: $secondsLeft Status: $status)");
$ping = $this->_outputDom->documentElement;
$ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', $status));
if($status === self::STATUS_CHANGES_FOUND) {
$folders = $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Folders'));
foreach($this->_foldersWithChanges as $changedFolder) {
$folder = $folders->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Folder', $changedFolder->serverId));
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " changes in folder: " . $changedFolder->serverId);
}
}
}
/**
* generate ping command response
*
*/
public function getResponse()
{
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/SendMail.php b/lib/ext/Syncroton/Command/SendMail.php
index c25f813..7fff25e 100644
--- a/lib/ext/Syncroton/Command/SendMail.php
+++ b/lib/ext/Syncroton/Command/SendMail.php
@@ -1,93 +1,93 @@
<?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 Sendmail command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_SendMail extends Syncroton_Command_Wbxml
{
protected $_defaultNameSpace = 'uri:ComposeMail';
protected $_documentElement = 'SendMail';
protected $_saveInSent;
protected $_source;
protected $_replaceMime = false;
/**
* process the XML file and add, change, delete or fetches data
*
* @return resource
*/
public function handle()
{
if ($this->_requestParameters['contentType'] == 'message/rfc822') {
$this->_mime = $this->_requestBody;
$this->_saveInSent = $this->_requestParameters['saveInSent'];
$this->_replaceMime = false;
$this->_source = array(
- 'collectionId' => $this->_requestParameters['collectionId'],
- 'itemId' => $this->_requestParameters['itemId'],
- 'instanceId' => null
+ 'collectionId' => $this->_requestParameters['collectionId'],
+ 'itemId' => $this->_requestParameters['itemId'],
+ 'instanceId' => null
);
} else {
$xml = simplexml_import_dom($this->_requestBody);
$this->_mime = (string) $xml->Mime;
$this->_saveInSent = isset($xml->SaveInSentItems);
$this->_replaceMime = isset($xml->ReplaceMime);
if (isset ($xml->Source)) {
if ($xml->Source->LongId) {
$this->_source = (string)$xml->Source->LongId;
} else {
$this->_source = array(
'collectionId' => (string)$xml->Source->FolderId,
'itemId' => (string)$xml->Source->ItemId,
'instanceId' => isset($xml->Source->InstanceId) ? (string)$xml->Source->InstanceId : null
);
}
}
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " saveInSent: " . (int)$this->_saveInSent);
}
/**
* this function generates the response for the client
*
* @return void
*/
public function getResponse()
{
$dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_EMAIL, $this->_device, $this->_syncTimeStamp);
try {
$dataController->sendEmail($this->_mime, $this->_saveInSent);
} catch (Syncroton_Exception_Status $ses) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Sending email failed: " . $ses->getMessage());
$response = new Syncroton_Model_SendMail(array(
'status' => $ses->getCode(),
));
$response->appendXML($this->_outputDom->documentElement, $this->_device);
return $this->_outputDom;
}
}
}
diff --git a/lib/ext/Syncroton/Command/Sync.php b/lib/ext/Syncroton/Command/Sync.php
index 204f7ab..46e38c2 100644
--- a/lib/ext/Syncroton/Command/Sync.php
+++ b/lib/ext/Syncroton/Command/Sync.php
@@ -1,835 +1,936 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Command
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Syncroton
* @subpackage Command
*/
class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
{
const STATUS_SUCCESS = 1;
const STATUS_PROTOCOL_VERSION_MISMATCH = 2;
const STATUS_INVALID_SYNC_KEY = 3;
const STATUS_PROTOCOL_ERROR = 4;
const STATUS_SERVER_ERROR = 5;
const STATUS_ERROR_IN_CLIENT_SERVER_CONVERSION = 6;
const STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT = 7;
const STATUS_OBJECT_NOT_FOUND = 8;
const STATUS_USER_ACCOUNT_MAYBE_OUT_OF_DISK_SPACE = 9;
const STATUS_ERROR_SETTING_NOTIFICATION_GUID = 10;
const STATUS_DEVICE_NOT_PROVISIONED_FOR_NOTIFICATIONS = 11;
const STATUS_FOLDER_HIERARCHY_HAS_CHANGED = 12;
const STATUS_RESEND_FULL_XML = 13;
const STATUS_WAIT_INTERVAL_OUT_OF_RANGE = 14;
const CONFLICT_OVERWRITE_SERVER = 0;
const CONFLICT_OVERWRITE_PIM = 1;
const MIMESUPPORT_DONT_SEND_MIME = 0;
const MIMESUPPORT_SMIME_ONLY = 1;
const MIMESUPPORT_SEND_MIME = 2;
const BODY_TYPE_PLAIN_TEXT = 1;
const BODY_TYPE_HTML = 2;
const BODY_TYPE_RTF = 3;
const BODY_TYPE_MIME = 4;
/**
* truncate types
*/
const TRUNCATE_ALL = 0;
const TRUNCATE_4096 = 1;
const TRUNCATE_5120 = 2;
const TRUNCATE_7168 = 3;
const TRUNCATE_10240 = 4;
const TRUNCATE_20480 = 5;
const TRUNCATE_51200 = 6;
const TRUNCATE_102400 = 7;
const TRUNCATE_NOTHING = 8;
/**
* filter types
*/
const FILTER_NOTHING = 0;
const FILTER_1_DAY_BACK = 1;
const FILTER_3_DAYS_BACK = 2;
const FILTER_1_WEEK_BACK = 3;
const FILTER_2_WEEKS_BACK = 4;
const FILTER_1_MONTH_BACK = 5;
const FILTER_3_MONTHS_BACK = 6;
const FILTER_6_MONTHS_BACK = 7;
const FILTER_INCOMPLETE = 8;
protected $_defaultNameSpace = 'uri:AirSync';
protected $_documentElement = 'Sync';
/**
* list of collections
*
* @var array
*/
protected $_collections = array();
protected $_modifications = array();
/**
* the global WindowSize
*
* @var integer
*/
protected $_globalWindowSize;
/**
* there are more entries than WindowSize available
* the MoreAvailable tag hot added to the xml output
*
* @var boolean
*/
protected $_moreAvailable = false;
/**
* @var Syncroton_Model_SyncState
*/
protected $_syncState;
protected $_maxWindowSize = 100;
protected $_heartbeatInterval = null;
/**
* process the XML file and add, change, delete or fetches data
*/
public function handle()
{
// input xml
$xml = simplexml_import_dom($this->_requestBody);
if (isset($xml->HeartbeatInterval)) {
$this->_heartbeatInterval = (int)$xml->HeartbeatInterval;
} elseif (isset($xml->Wait)) {
$this->_heartbeatInterval = (int)$xml->Wait * 60;
}
$this->_globalWindowSize = isset($xml->WindowSize) ? (int)$xml->WindowSize : 100;
+
if ($this->_globalWindowSize > $this->_maxWindowSize) {
$this->_globalWindowSize = $this->_maxWindowSize;
}
- foreach ($xml->Collections->Collection as $xmlCollection) {
- $collectionData = new Syncroton_Model_SyncCollection($xmlCollection);
+ $collections = array();
+ $isPartialRequest = isset($xml->Partial);
+
+ // try to restore collections from previous request
+ if ($isPartialRequest) {
+ $decodedCollections = Zend_Json::decode($this->_device->lastsynccollection);
+
+ if (is_array($decodedCollections)) {
+ foreach ($decodedCollections as $collection) {
+ $collections[$collection['collectionId']] = new Syncroton_Model_SyncCollection($collection);
+ }
+ }
+ }
+
+ // Collections element is optional when Partial element is sent
+ if (isset($xml->Collections)) {
+ foreach ($xml->Collections->Collection as $xmlCollection) {
+ $collectionId = (string)$xmlCollection->CollectionId;
+
+ // do we have to update a collection sent in previous sync request?
+ if ($isPartialRequest && isset($collections[$collectionId])) {
+ $collections[$collectionId]->setFromSimpleXMLElement($xmlCollection);
+ } else {
+ $collections[$collectionId] = new Syncroton_Model_SyncCollection($xmlCollection);
+ }
+ }
- // got the folder synchronized to the device already
+ // store current value of $collections for next Sync command request
+ $collectionsToSave = array();
+
+ foreach ($collections as $collection) {
+ $collectionsToSave[$collection->collectionId] = $collection->toArray();
+ }
+
+ $this->_device->lastsynccollection = Zend_Json::encode($collectionsToSave);
+
+ Syncroton_Registry::getDeviceBackend()->update($this->_device);
+ }
+
+ foreach ($collections as $collectionData) {
+ // has the folder been synchronised to the device already
try {
$collectionData->folder = $this->_folderBackend->getFolder($this->_device, $collectionData->collectionId);
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData->collectionId} not found");
// trigger INVALID_SYNCKEY instead of OBJECT_NOTFOUND when synckey is higher than 0
// to avoid a syncloop for the iPhone
if ($collectionData->syncKey > 0) {
$collectionData->folder = new Syncroton_Model_Folder(array(
'deviceId' => $this->_device,
- 'serverId' => $collectionData->collectionId
+ 'serverId' => $collectionData->collectionId
));
}
- $this->_collections[] = $collectionData;
+ $this->_collections[$collectionData->collectionId] = $collectionData;
continue;
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData->syncKey} Class: {$collectionData->folder->class} CollectionId: {$collectionData->collectionId}");
// initial synckey
if($collectionData->syncKey === 0) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
// reset sync state for this folder
$this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
$this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
$collectionData->syncState = new Syncroton_Model_SyncState(array(
'device_id' => $this->_device,
'counter' => 0,
'type' => $collectionData->folder,
'lastsync' => $this->_syncTimeStamp
));
- $this->_collections[] = $collectionData;
+ $this->_collections[$collectionData->collectionId] = $collectionData;
continue;
}
// check for invalid sycnkey
if(($collectionData->syncState = $this->_syncStateBackend->validate($this->_device, $collectionData->folder, $collectionData->syncKey)) === false) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData->syncKey} provided");
// reset sync state for this folder
$this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
$this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
- $this->_collections[] = $collectionData;
+ $this->_collections[$collectionData->collectionId] = $collectionData;
continue;
}
$dataController = Syncroton_Data_Factory::factory($collectionData->folder->class, $this->_device, $this->_syncTimeStamp);
switch($collectionData->folder->class) {
case Syncroton_Data_Factory::CLASS_CALENDAR:
$dataClass = 'Syncroton_Model_Event';
break;
case Syncroton_Data_Factory::CLASS_CONTACTS:
$dataClass = 'Syncroton_Model_Contact';
break;
case Syncroton_Data_Factory::CLASS_EMAIL:
$dataClass = 'Syncroton_Model_Email';
break;
case Syncroton_Data_Factory::CLASS_TASKS:
$dataClass = 'Syncroton_Model_Task';
break;
default:
throw new Syncroton_Exception_UnexpectedValue('invalid class provided');
break;
}
$clientModifications = array(
'added' => array(),
'changed' => array(),
'deleted' => array(),
'forceAdd' => array(),
'forceChange' => array(),
'toBeFetched' => array(),
);
// handle incoming data
if($collectionData->hasClientAdds()) {
$adds = $collectionData->getClientAdds();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
foreach ($adds as $add) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
try {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " adding entry as new");
$serverId = $dataController->createEntry($collectionData->collectionId, new $dataClass($add->ApplicationData));
$clientModifications['added'][$serverId] = array(
'clientId' => (string)$add->ClientId,
'serverId' => $serverId,
'status' => self::STATUS_SUCCESS,
'contentState' => $this->_contentStateBackend->create(new Syncroton_Model_Content(array(
'device_id' => $this->_device,
'folder_id' => $collectionData->folder,
'contentid' => $serverId,
'creation_time' => $this->_syncTimeStamp,
'creation_synckey' => $collectionData->syncKey + 1
)))
);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
$clientModifications['added'][] = array(
'clientId' => (string)$add->ClientId,
'status' => self::STATUS_SERVER_ERROR
);
}
}
}
// handle changes, but only if not first sync
if($collectionData->syncKey > 1 && $collectionData->hasClientChanges()) {
$changes = $collectionData->getClientChanges();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
foreach ($changes as $change) {
$serverId = (string)$change->ServerId;
try {
$dataController->updateEntry($collectionData->collectionId, $serverId, new $dataClass($change->ApplicationData));
$clientModifications['changed'][$serverId] = self::STATUS_SUCCESS;
} catch (Syncroton_Exception_AccessDenied $e) {
$clientModifications['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
$clientModifications['forceChange'][$serverId] = $serverId;
} catch (Syncroton_Exception_NotFound $e) {
// entry does not exist anymore, will get deleted automaticaly
$clientModifications['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
// something went wrong while trying to update the entry
$clientModifications['changed'][$serverId] = self::STATUS_SERVER_ERROR;
}
}
}
// handle deletes, but only if not first sync
if($collectionData->hasClientDeletes()) {
$deletes = $collectionData->getClientDeletes();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
foreach ($deletes as $delete) {
$serverId = (string)$delete->ServerId;
try {
// check if we have sent this entry to the phone
$state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
try {
$dataController->deleteEntry($collectionData->collectionId, $serverId, $collectionData);
} catch(Syncroton_Exception_NotFound $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
} catch (Syncroton_Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
$clientModifications['forceAdd'][$serverId] = $serverId;
}
$this->_contentStateBackend->delete($state);
} catch (Syncroton_Exception_NotFound $senf) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
// should we send a special status???
//$collectionData->deleted[$serverId] = self::STATUS_SUCCESS;
}
$clientModifications['deleted'][$serverId] = self::STATUS_SUCCESS;
}
}
// handle fetches, but only if not first sync
if($collectionData->syncKey > 1 && $collectionData->hasClientFetches()) {
// the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
// some prevoius versions of iOS did not set GetChanges to 0 for fetches. Let's enforce getChanges to false here.
$collectionData->getChanges = false;
$fetches = $collectionData->getClientFetches();
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
$toBeFecthed = array();
foreach ($fetches as $fetch) {
$serverId = (string)$fetch->ServerId;
$toBeFetched[$serverId] = $serverId;
}
$collectionData->toBeFetched = $toBeFetched;
}
- $this->_collections[] = $collectionData;
+ $this->_collections[$collectionData->collectionId] = $collectionData;
$this->_modifications[$collectionData->collectionId] = $clientModifications;
}
}
/**
* (non-PHPdoc)
* @see Syncroton_Command_Wbxml::getResponse()
*/
public function getResponse()
{
$sync = $this->_outputDom->documentElement;
+ if (count($this->_collections) == 0) {
+ $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_RESEND_FULL_XML));
+
+ return $this->_outputDom;
+ }
+
$collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections'));
$totalChanges = 0;
// continue only if there are changes or no time is left
if ($this->_heartbeatInterval > 0) {
$intervalStart = time();
do {
+ // take a break to save battery lifetime
+ sleep(Syncroton_Registry::getPingTimeout());
+
+ $now = new DateTime(null, new DateTimeZone('utc'));
+
foreach($this->_collections as $collectionData) {
// countinue immediately if folder does not exist
if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) {
break 2;
// countinue immediately if syncstate is invalid
} elseif (! ($collectionData->syncState instanceof Syncroton_Model_ISyncState)) {
break 2;
} else {
+ if ($collectionData->getChanges !== true) {
+ continue;
+ }
+
+ try {
+ // just check if the folder still exists
+ $this->_folderBackend->get($collectionData->folder);
+ } catch (Syncroton_Exception_NotFound $senf) {
+ if ($this->_logger instanceof Zend_Log)
+ $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " collection does not exist anymore: " . $collectionData->collectionId);
+
+ $collectionData->getChanges = false;
+
+ // make sure this is the last while loop
+ // no break 2 here, as we like to check the other folders too
+ $intervalStart -= $this->_heartbeatInterval;
+ }
+
+ // check that the syncstate still exists and is still valid
+ try {
+ $syncState = $this->_syncStateBackend->getSyncState($this->_device, $collectionData->folder);
+
+ // another process synchronized data of this folder already. let's skip it
+ if ($syncState->id !== $collectionData->syncState->id) {
+ if ($this->_logger instanceof Zend_Log)
+ $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " syncstate changed during heartbeat interval for collection: " . $collectionData->folder->serverId);
+
+ $collectionData->getChanges = false;
+
+ // make sure this is the last while loop
+ // no break 2 here, as we like to check the other folders too
+ $intervalStart -= $this->_heartbeatInterval;
+ }
+ } catch (Syncroton_Exception_NotFound $senf) {
+ if ($this->_logger instanceof Zend_Log)
+ $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " no syncstate found anymore for collection: " . $collectionData->folder->serverId);
+
+ $collectionData->syncState = null;
+
+ // make sure this is the last while loop
+ // no break 2 here, as we like to check the other folders too
+ $intervalStart -= $this->_heartbeatInterval;
+ }
+
+
+ // safe battery time by skipping folders which got synchronied less than Syncroton_Command_Ping::$quietTime seconds ago
+ if (($now->getTimestamp() - $collectionData->syncState->lastsync->getTimestamp()) < Syncroton_Registry::getQuietTime()) {
+ continue;
+ }
+
$dataController = Syncroton_Data_Factory::factory($collectionData->folder->class , $this->_device, $this->_syncTimeStamp);
$estimate = $dataController->getCountOfChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState);
// countinue immediately if there are changes available
if ($estimate > 0) {
break 2;
}
}
}
- // wait some PING_TIMEOUT seconds until neext loop
- sleep(Syncroton_Command_Ping::$pingTimeout);
+ $this->_syncTimeStamp = clone $now;
- } while (time() - $intervalStart < $this->_heartbeatInterval);
+ // See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
+ //
+ // break if there are less than PingTimeout + 10 seconds left for the next loop
+ // otherwise the response will be returned after the client has finished his Ping
+ // request already maybe
+ } while (time() - $intervalStart < $this->_heartbeatInterval - (Syncroton_Registry::getPingTimeout() + 10));
}
foreach($this->_collections as $collectionData) {
$collectionChanges = 0;
+ /**
+ * keep track of entries added on server side
+ */
+ $newContentStates = array();
+
+ /**
+ * keep track of entries deleted on server side
+ */
+ $deletedContentStates = array();
+
// invalid collectionid provided
if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) {
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
// invalid synckey provided
} elseif (! ($collectionData->syncState instanceof Syncroton_Model_ISyncState)) {
// set synckey to 0
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
// initial sync
} elseif ($collectionData->syncState->counter === 0) {
$collectionData->syncState->counter++;
// initial sync
// send back a new SyncKey only
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
if (!empty($collectionData->folder->class)) {
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
}
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData->syncState->counter));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
} else {
$dataController = Syncroton_Data_Factory::factory($collectionData->folder->class , $this->_device, $this->_syncTimeStamp);
$clientModifications = $this->_modifications[$collectionData->collectionId];
$serverModifications = array(
'added' => array(),
'changed' => array(),
'deleted' => array(),
);
if($collectionData->getChanges === true) {
// continue sync session?
if(is_array($collectionData->syncState->pendingdata)) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
$serverModifications = $collectionData->syncState->pendingdata;
} else {
try {
// fetch entries added since last sync
$allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData->folder);
$allServerEntries = $dataController->getServerEntries($collectionData->collectionId, $collectionData->options['filterType']);
// add entries
$serverDiff = array_diff($allServerEntries, $allClientEntries);
// add entries which produced problems during delete from client
$serverModifications['added'] = $clientModifications['forceAdd'];
// add entries not yet sent to client
$serverModifications['added'] = array_unique(array_merge($serverModifications['added'], $serverDiff));
// @todo still needed?
foreach($serverModifications['added'] as $id => $serverId) {
// skip entries added by client during this sync session
if(isset($clientModifications['added'][$serverId]) && !isset($clientModifications['forceAdd'][$serverId])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
unset($serverModifications['added'][$id]);
}
}
// entries to be deleted
$serverModifications['deleted'] = array_diff($allClientEntries, $allServerEntries);
-
+
// fetch entries changed since last sync
$serverModifications['changed'] = $dataController->getChangedEntries($collectionData->collectionId, $collectionData->syncState->lastsync, $this->_syncTimeStamp);
$serverModifications['changed'] = array_merge($serverModifications['changed'], $clientModifications['forceChange']);
foreach($serverModifications['changed'] as $id => $serverId) {
// skip entry, if it got changed by client during current sync
if(isset($clientModifications['changed'][$serverId]) && !isset($clientModifications['forceChange'][$serverId])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
unset($serverModifications['changed'][$id]);
}
// skip entry, make sure we don't sent entries already added by client in this request
else if (isset($clientModifications['added'][$serverId]) && !isset($clientModifications['forceAdd'][$serverId])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped change for added entry: " . $serverId);
unset($serverModifications['changed'][$id]);
}
}
// entries comeing in scope are already in $serverModifications['added'] and do not need to
// be send with $serverCanges
$serverModifications['changed'] = array_diff($serverModifications['changed'], $serverModifications['added']);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getTraceAsString());
// Prevent from removing client entries when getServerEntries() fails
// @todo: should we set Status and break the loop here?
$serverModifications = array(
'added' => array(),
'changed' => array(),
'deleted' => array(),
);
}
}
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverModifications['added']) . '/' . count($serverModifications['changed']) . '/' . count($serverModifications['deleted']) . ' entries for sync from server to client');
}
// collection header
$collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
if (!empty($collectionData->folder->class)) {
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
}
- $syncKeyNode = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey'));
+ $syncKeyElement = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey'));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
$responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
// send reponse for newly added entries
if(!empty($clientModifications['added'])) {
foreach($clientModifications['added'] as $entryData) {
$add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
// we have no serverId is the add failed
if(isset($entryData['serverId'])) {
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
}
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
}
}
// send reponse for changed entries
if(!empty($clientModifications['changed'])) {
foreach($clientModifications['changed'] as $serverId => $status) {
if ($status !== Syncroton_Command_Sync::STATUS_SUCCESS) {
$change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
$change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
}
}
}
// send response for to be fetched entries
if(!empty($collectionData->toBeFetched)) {
foreach($collectionData->toBeFetched as $serverId) {
$fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
$fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
try {
$applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
$dataController
->getEntry($collectionData, $serverId)
->appendXML($applicationData, $this->_device);
$fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
$fetch->appendChild($applicationData);
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getTraceAsString());
$fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
}
}
}
if ($responses->hasChildNodes() === true) {
$collection->appendChild($responses);
}
$commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
- /**
- * process entries added on server side
- */
- $newContentStates = array();
-
foreach($serverModifications['added'] as $id => $serverId) {
- if($collectionChanges === $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
+ if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
break;
}
#/**
# * somewhere is a problem in the logic for handling moreAvailable
# *
# * it can happen, that we have a contentstate (which means we sent the entry to the client
# * and that this entry is yet in $collectionData->syncState->pendingdata['serverAdds']
# * I have no idea how this can happen, but the next lines of code work around this problem
# */
#try {
# $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
#
# if ($this->_logger instanceof Zend_Log)
# $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client");
#
# unset($serverModifications['added'][$id]);
# continue;
#
#} catch (Syncroton_Exception_NotFound $senf) {
# // do nothing => content state should not exist yet
#}
try {
$add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
$add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
$dataController
->getEntry($collectionData, $serverId)
->appendXML($applicationData, $this->_device);
$commands->appendChild($add);
$collectionChanges++;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
}
// mark as send to the client, even the conversion to xml might have failed
$newContentStates[] = new Syncroton_Model_Content(array(
'device_id' => $this->_device,
'folder_id' => $collectionData->folder,
'contentid' => $serverId,
'creation_time' => $this->_syncTimeStamp,
'creation_synckey' => $collectionData->syncState->counter + 1
));
- unset($serverModifications['added'][$id]);
+ unset($serverModifications['added'][$id]);
}
/**
* process entries changed on server side
*/
foreach($serverModifications['changed'] as $id => $serverId) {
- if($collectionChanges === $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
+ if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
break;
}
try {
$change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
$change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
$dataController
->getEntry($collectionData, $serverId)
->appendXML($applicationData, $this->_device);
$commands->appendChild($change);
$collectionChanges++;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
}
unset($serverModifications['changed'][$id]);
}
- /**
- * process entries deleted on server side
- */
- $deletedContentStates = array();
-
foreach($serverModifications['deleted'] as $id => $serverId) {
- if($collectionChanges === $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
+ if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
break;
}
try {
// check if we have sent this entry to the phone
$state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
$delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
$delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
$deletedContentStates[] = $state;
$commands->appendChild($delete);
$collectionChanges++;
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
}
unset($serverModifications['deleted'][$id]);
}
$countOfPendingChanges = (count($serverModifications['added']) + count($serverModifications['changed']) + count($serverModifications['deleted']));
if ($countOfPendingChanges > 0) {
$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
} else {
$serverModifications = null;
}
if ($commands->hasChildNodes() === true) {
$collection->appendChild($commands);
}
$totalChanges += $collectionChanges;
// increase SyncKey if needed
if ((
// sent the clients updates?
!empty($clientModifications['added']) ||
!empty($clientModifications['changed']) ||
!empty($clientModifications['deleted'])
) || (
// sends the server updates to the client?
$commands->hasChildNodes() === true
) || (
// changed the pending data?
$collectionData->syncState->pendingdata != $serverModifications
)
) {
// then increase SyncKey
$collectionData->syncState->counter++;
}
- $syncKeyNode->appendChild($this->_outputDom->createTextNode($collectionData->syncState->counter));
+ $syncKeyElement->appendChild($this->_outputDom->createTextNode($collectionData->syncState->counter));
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData->syncState->counter);
}
if (isset($collectionData->syncState) && $collectionData->syncState instanceof Syncroton_Model_ISyncState &&
$collectionData->syncState->counter != $collectionData->syncKey) {
-
+
+ if ($this->_logger instanceof Zend_Log)
+ $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " update syncState for collection: " . $collectionData->collectionId);
+
// store pending data in sync state when needed
if(isset($countOfPendingChanges) && $countOfPendingChanges > 0) {
$collectionData->syncState->pendingdata = array(
'added' => (array)$serverModifications['added'],
'changed' => (array)$serverModifications['changed'],
'deleted' => (array)$serverModifications['deleted']
);
} else {
$collectionData->syncState->pendingdata = null;
}
if (!empty($clientModifications['added'])) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
$keepPreviousSyncKey = false;
} else {
$keepPreviousSyncKey = true;
}
$collectionData->syncState->lastsync = clone $this->_syncTimeStamp;
// increment sync timestamp by 1 second
$collectionData->syncState->lastsync->modify('+1 sec');
try {
$transactionId = Syncroton_Registry::getTransactionManager()->startTransaction(Syncroton_Registry::getDatabase());
// store new synckey
$this->_syncStateBackend->create($collectionData->syncState, $keepPreviousSyncKey);
// store contentstates for new entries added to client
- if (isset($newContentStates)) {
- foreach($newContentStates as $state) {
- #$state->creation_synckey = $collectionData->syncState->counter;
- $this->_contentStateBackend->create($state);
- }
+ foreach($newContentStates as $state) {
+ $this->_contentStateBackend->create($state);
}
// remove contentstates for entries to be deleted on client
- if (isset($deletedContentStates)) {
- foreach($deletedContentStates as $state) {
- $this->_contentStateBackend->delete($state);
- }
+ foreach($deletedContentStates as $state) {
+ $this->_contentStateBackend->delete($state);
}
Syncroton_Registry::getTransactionManager()->commitTransaction($transactionId);
} catch (Zend_Db_Statement_Exception $zdse) {
// something went wrong
// maybe another parallel request added a new synckey
// we must remove data added from client
if (!empty($clientModifications['added'])) {
foreach ($clientModifications['added'] as $added) {
$this->_contentStateBackend->delete($added['contentState']);
$dataController->deleteEntry($collectionData->collectionId, $added['serverId'], array());
}
}
Syncroton_Registry::getTransactionManager()->rollBack();
throw $zdse;
}
// store current filter type
try {
$folderState = $this->_folderBackend->getFolder($this->_device, $collectionData->collectionId);
$folderState->lastfiltertype = $collectionData->options['filterType'];
$this->_folderBackend->update($folderState);
} catch (Syncroton_Exception_NotFound $senf) {
// failed to get folderstate => should not happen but is also no problem in this state
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData->collectionId);
}
}
}
return $this->_outputDom;
}
}
diff --git a/lib/ext/Syncroton/Command/Wbxml.php b/lib/ext/Syncroton/Command/Wbxml.php
index cab5cc9..e927cc2 100644
--- a/lib/ext/Syncroton/Command/Wbxml.php
+++ b/lib/ext/Syncroton/Command/Wbxml.php
@@ -1,187 +1,222 @@
<?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>
*/
/**
* abstract class for all commands using wbxml encoded content
*
* @package Syncroton
* @subpackage Command
*/
abstract class Syncroton_Command_Wbxml implements Syncroton_Command_ICommand
{
/**
* informations about the currently device
*
* @var Syncroton_Model_Device
*/
protected $_device;
/**
* informations about the currently device
*
* @var Syncroton_Backend_IDevice
*/
protected $_deviceBackend;
/**
* informations about the currently device
*
* @var Syncroton_Backend_IFolder
*/
protected $_folderBackend;
/**
* @var Syncroton_Backend_ISyncState
*/
protected $_syncStateBackend;
/**
* @var Syncroton_Backend_IContent
*/
protected $_contentStateBackend;
/**
*
* @var Syncroton_Backend_IPolicy
*/
protected $_policyBackend;
/**
* the domDocument containing the xml response from the server
*
* @var DOMDocument
*/
protected $_outputDom;
/**
* the domDocucment containing the xml request from the client
*
* @var DOMDocument
*/
protected $_requestBody;
/**
* the default namespace
*
* @var string
*/
protected $_defaultNameSpace;
/**
* the main xml tag
*
* @var string
*/
protected $_documentElement;
/**
* @var array
*/
protected $_requestParameters;
/**
* @var Syncroton_Model_SyncState
*/
protected $_syncState;
protected $_skipValidatePolicyKey = false;
/**
* timestamp to use for all sync requests
*
* @var DateTime
*/
protected $_syncTimeStamp;
/**
* @var string
*/
protected $_transactionId;
/**
* @var string
*/
protected $_policyKey;
/**
* @var Zend_Log
*/
protected $_logger;
+ /**
+ * list of part streams
+ *
+ * @var array
+ */
+ protected $_parts = array();
+
+ /**
+ * list of headers
+ *
+ * @var array
+ */
+ protected $_headers = array();
+
/**
* the constructor
*
* @param mixed $requestBody
* @param Syncroton_Model_Device $device
* @param array $requestParameters
*/
public function __construct($requestBody, Syncroton_Model_IDevice $device, $requestParameters)
{
$this->_requestBody = $requestBody;
$this->_device = $device;
$this->_requestParameters = $requestParameters;
$this->_policyKey = $requestParameters['policyKey'];
$this->_deviceBackend = Syncroton_Registry::getDeviceBackend();
$this->_folderBackend = Syncroton_Registry::getFolderBackend();
$this->_syncStateBackend = Syncroton_Registry::getSyncStateBackend();
$this->_contentStateBackend = Syncroton_Registry::getContentStateBackend();
$this->_policyBackend = Syncroton_Registry::getPolicyBackend();
if (Syncroton_Registry::isRegistered('loggerBackend')) {
$this->_logger = Syncroton_Registry::get('loggerBackend');
}
$this->_syncTimeStamp = new DateTime(null, new DateTimeZone('UTC'));
+ // set default content type
+ $this->_headers['Content-Type'] = 'application/vnd.ms-sync.wbxml';
+
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " sync timestamp: " . $this->_syncTimeStamp->format('Y-m-d H:i:s'));
if (isset($this->_defaultNameSpace) && isset($this->_documentElement)) {
// Creates an instance of the DOMImplementation class
$imp = new DOMImplementation();
// Creates a DOMDocumentType instance
$dtd = $imp->createDocumentType('AirSync', "-//AIRSYNC//DTD AirSync//EN", "http://www.microsoft.com/");
// Creates a DOMDocument instance
$this->_outputDom = $imp->createDocument($this->_defaultNameSpace, $this->_documentElement, $dtd);
$this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Syncroton', 'uri:Syncroton');
$this->_outputDom->formatOutput = false;
$this->_outputDom->encoding = 'utf-8';
}
if ($this->_skipValidatePolicyKey != true) {
if (!empty($this->_device->policyId)) {
$policy = $this->_policyBackend->get($this->_device->policyId);
if($policy->policyKey != $this->_policyKey) {
$this->_outputDom->documentElement->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Status', 142));
$sepn = new Syncroton_Exception_ProvisioningNeeded();
$sepn->domDocument = $this->_outputDom;
throw $sepn;
}
// should we wipe the mobile phone?
if ($this->_device->remotewipe >= Syncroton_Command_Provision::REMOTEWIPE_REQUESTED) {
$this->_outputDom->documentElement->appendChild($this->_outputDom->createElementNS($this->_defaultNameSpace, 'Status', 140));
$sepn = new Syncroton_Exception_ProvisioningNeeded();
$sepn->domDocument = $this->_outputDom;
throw $sepn;
}
}
}
}
+
+ /**
+ * (non-PHPdoc)
+ * @see Syncroton_Command_ICommand::getHeaders()
+ */
+ public function getHeaders()
+ {
+ return $this->_headers;
+ }
+ /**
+ * return array of part streams
+ *
+ * @return array
+ */
+ public function getParts()
+ {
+ return $this->_parts;
+ }
}
diff --git a/lib/ext/Syncroton/Data/AData.php b/lib/ext/Syncroton/Data/AData.php
index 4e02ef5..1382f68 100644
--- a/lib/ext/Syncroton/Data/AData.php
+++ b/lib/ext/Syncroton/Data/AData.php
@@ -1,196 +1,235 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
*/
abstract class Syncroton_Data_AData implements Syncroton_Data_IData
{
- /**
- * used by unit tests only to simulated added folders
- */
- public static $folders = array();
+ const LONGID_DELIMITER = "\xe2\x87\x94"; # UTF8 ⇔
/**
* used by unit tests only to simulated added folders
*/
- public static $entries = array();
-
- /**
- * used by unit tests only to simulated added folders
- */
public static $changedEntries = array();
public function __construct(Syncroton_Model_IDevice $_device, DateTime $_timeStamp)
{
- $this->_device = $_device;
- $this->_timestamp = $_timeStamp;
-
+ $this->_device = $_device;
+ $this->_timestamp = $_timeStamp;
$this->_db = Syncroton_Registry::getDatabase();
$this->_tablePrefix = 'Syncroton_';
+ $this->_ownerId = '1234';
+ }
+
+ public function getFolder($id)
+ {
+ $select = $this->_db->select()
+ ->from($this->_tablePrefix . 'data_folder')
+ ->where('owner_id = ?', $this->_ownerId)
+ ->where('id = ?', $id);
- $this->_initData();
+ $stmt = $this->_db->query($select);
+ $folder = $stmt->fetch();
+ $stmt = null; # see https://bugs.php.net/bug.php?id=44081
+
+ if ($folder === false) {
+ throw new Syncroton_Exception_NotFound("folder $id not found");
+ }
+
+ return new Syncroton_Model_Folder(array(
+ 'serverId' => $folder['id'],
+ 'displayName' => $folder['name'],
+ 'type' => $folder['type'],
+ 'parentId' => !empty($folder['parent_id']) ? $folder['parent_id'] : null
+ ));
}
public function createFolder(Syncroton_Model_IFolder $folder)
{
- $folder->id = sha1(mt_rand(). microtime());
+ if (!in_array($folder->type, $this->_supportedFolderTypes)) {
+ throw new Syncroton_Exception_UnexpectedValue();
+ }
- // normaly generated on server backend
- $folder->serverId = sha1(mt_rand(). microtime());
-
- Syncroton_Data_AData::$folders[get_class($this)][$folder->serverId] = $folder;
-
- return Syncroton_Data_AData::$folders[get_class($this)][$folder->serverId];
+ $id = !empty($folder->serverId) ? $folder->serverId : sha1(mt_rand(). microtime());
+
+ $this->_db->insert($this->_tablePrefix . 'data_folder', array(
+ 'id' => $id,
+ 'type' => $folder->type,
+ 'name' => $folder->displayName,
+ 'owner_id' => $this->_ownerId,
+ 'parent_id' => $folder->parentId
+ ));
+
+ return $this->getFolder($id);
}
public function createEntry($_folderId, Syncroton_Model_IEntry $_entry)
{
$id = sha1(mt_rand(). microtime());
- #Syncroton_Data_AData::$entries[get_class($this)][$_folderId][$id] = $_entry;
-
$this->_db->insert($this->_tablePrefix . 'data', array(
'id' => $id,
- 'type' => get_class($this),
+ 'class' => get_class($_entry),
'folder_id' => $_folderId,
'data' => serialize($_entry)
));
return $id;
}
public function deleteEntry($_folderId, $_serverId, $_collectionData)
{
- #$folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId;
+ $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId;
$result = $this->_db->delete($this->_tablePrefix . 'data', array('id = ?' => $_serverId));
return (bool) $result;
-
- #unset(Syncroton_Data_AData::$entries[get_class($this)][$folderId][$_serverId]);
}
public function deleteFolder($_folderId)
{
$folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId;
-
- unset(Syncroton_Data_AData::$folders[get_class($this)][$folderId]);
- unset(Syncroton_Data_AData::$entries[get_class($this)][$folderId]);
+
+ $result = $this->_db->delete($this->_tablePrefix . 'data', array('folder_id = ?' => $folderId));
+ $result = $this->_db->delete($this->_tablePrefix . 'data_folder', array('id = ?' => $folderId));
+
+ return (bool) $result;
}
public function getAllFolders()
{
- return Syncroton_Data_AData::$folders[get_class($this)];
+ $select = $this->_db->select()
+ ->from($this->_tablePrefix . 'data_folder')
+ ->where('type IN (?)', $this->_supportedFolderTypes)
+ ->where('owner_id = ?', $this->_ownerId);
+
+ $stmt = $this->_db->query($select);
+ $folders = $stmt->fetchAll();
+ $stmt = null; # see https://bugs.php.net/bug.php?id=44081
+
+ $result = array();
+
+ foreach ((array) $folders as $folder) {
+ $result[$folder['id']] = new Syncroton_Model_Folder(array(
+ 'serverId' => $folder['id'],
+ 'displayName' => $folder['name'],
+ 'type' => $folder['type'],
+ 'parentId' => $folder['parent_id']
+ ));
+ }
+
+ return $result;
}
public function getChangedEntries($_folderId, DateTime $_startTimeStamp, DateTime $_endTimeStamp = NULL)
{
if (!isset(Syncroton_Data_AData::$changedEntries[get_class($this)])) {
return array();
} else {
return Syncroton_Data_AData::$changedEntries[get_class($this)];
}
}
/**
* @param Syncroton_Model_IFolder|string $_folderId
* @param string $_filter
* @return array
*/
public function getServerEntries($_folderId, $_filter)
{
$folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId;
$select = $this->_db->select()
->from($this->_tablePrefix . 'data', array('id'))
->where('folder_id = ?', $_folderId);
$ids = array();
$stmt = $this->_db->query($select);
while ($id = $stmt->fetchColumn()) {
$ids[] = $id;
}
return $ids;
}
public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
$allClientEntries = $contentBackend->getFolderState($this->_device, $folder);
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
$changedEntries = $this->getChangedEntries($folder->serverId, $syncState->lastsync);
return count($addedEntries) + count($deletedEntries) + count($changedEntries);
}
public function getFileReference($fileReference)
{
throw new Syncroton_Exception_NotFound('filereference not found');
}
/**
* (non-PHPdoc)
* @see Syncroton_Data_IData::getEntry()
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId)
{
$select = $this->_db->select()
->from($this->_tablePrefix . 'data', array('data'))
->where('id = ?', $serverId);
$stmt = $this->_db->query($select);
$entry = $stmt->fetchColumn();
if ($entry === false) {
throw new Syncroton_Exception_NotFound("entry $serverId not found in folder {$collection->collectionId}");
}
return unserialize($entry);
}
public function moveItem($_srcFolderId, $_serverId, $_dstFolderId)
{
$this->_db->update($this->_tablePrefix . 'data', array(
'folder_id' => $_dstFolderId,
), array(
'id = ?' => $_serverId
));
return $_serverId;
}
public function updateEntry($_folderId, $_serverId, Syncroton_Model_IEntry $_entry)
{
$this->_db->update($this->_tablePrefix . 'data', array(
- 'type' => get_class($this),
'folder_id' => $_folderId,
'data' => serialize($_entry)
), array(
'id = ?' => $_serverId
));
}
public function updateFolder(Syncroton_Model_IFolder $folder)
{
- Syncroton_Data_AData::$folders[get_class($this)][$folder->serverId] = $folder;
+ $this->_db->update($this->_tablePrefix . 'data_folder', array(
+ 'name' => $folder->displayName,
+ 'parent_id' => $folder->parentId
+ ), array(
+ 'id = ?' => $folder->serverId
+ ));
}
-
-
- abstract protected function _initData();
}
diff --git a/lib/ext/Syncroton/Data/Calendar.php b/lib/ext/Syncroton/Data/Calendar.php
index 9f24590..66c8d25 100644
--- a/lib/ext/Syncroton/Data/Calendar.php
+++ b/lib/ext/Syncroton/Data/Calendar.php
@@ -1,43 +1,34 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
*/
-
-class Syncroton_Data_Calendar extends Syncroton_Data_AData
+class Syncroton_Data_Calendar extends Syncroton_Data_AData implements Syncroton_Data_IDataCalendar
{
- protected function _initData()
+ protected $_supportedFolderTypes = array(
+ Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR,
+ Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED
+ );
+
+ /**
+ * set attendee status for meeting
+ *
+ * @param Syncroton_Model_MeetingResponse $request the meeting response
+ * @return string id of new calendar entry
+ */
+ public function setAttendeeStatus(Syncroton_Model_MeetingResponse $reponse)
{
- /**
- * used by unit tests only to simulated added folders
- */
- Syncroton_Data_AData::$folders[get_class($this)] = array(
- 'calendarFolderId' => new Syncroton_Model_Folder(array(
- 'id' => sha1(mt_rand(). microtime()),
- 'serverId' => 'calendarFolderId',
- 'parentId' => 0,
- 'displayName' => 'Default Contacts Folder',
- 'type' => Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR
- ))
- );
-
- #/**
- # * used by unit tests only to simulated added folders
- # */
- #Syncroton_Data_AData::$entries[get_class($this)] = array(
- # 'calendarFolderId' => array(
- # )
- #);
+ return $reponse->requestId;
}
}
diff --git a/lib/ext/Syncroton/Data/Contacts.php b/lib/ext/Syncroton/Data/Contacts.php
index 1f191c2..a9b8d58 100644
--- a/lib/ext/Syncroton/Data/Contacts.php
+++ b/lib/ext/Syncroton/Data/Contacts.php
@@ -1,202 +1,81 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
*/
class Syncroton_Data_Contacts extends Syncroton_Data_AData implements Syncroton_Data_IDataSearch
{
+ protected $_supportedFolderTypes = array(
+ Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT,
+ Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED
+ );
+
/**
* (non-PHPdoc)
* @see Syncroton_Data_IDataSearch::getSearchEntry()
*/
public function getSearchEntry($longId, $options)
{
- list($collectionId, $serverId) = explode('-', $longId, 2);
+ list($collectionId, $serverId) = explode(Syncroton_Data_AData::LONGID_DELIMITER, $longId, 2);
$contact = $this->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $collectionId)), $serverId);
return new Syncroton_Model_GAL(array(
'firstName' => $contact->firstName,
'lastName' => $contact->lastName,
'picture' => new Syncroton_Model_GALPicture(array('status' => 1, 'data' => 'abc'))
));
}
/**
* (non-PHPdoc)
* @see Syncroton_Data_IDataSearch::search()
*/
public function search(Syncroton_Model_StoreRequest $store)
{
$storeResponse = new Syncroton_Model_StoreResponse();
$serverIds = $this->getServerEntries('addressbookFolderId', Syncroton_Command_Sync::FILTER_NOTHING);
$total = 0;
$found = array();
foreach ($serverIds as $serverId) {
$contact = $this->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => 'addressbookFolderId')), $serverId);
if ($contact->firstName == $store->query) {
$total++;
if (count($found) == $store->options['range'][1]+1) {
continue;
}
$found[] = new Syncroton_Model_StoreResponseResult(array(
- 'longId' => 'addressbookFolderId-' . $serverId,
- 'properties' => $this->getSearchEntry('addressbookFolderId-' . $serverId, $store->options)
+ 'longId' => 'addressbookFolderId' . Syncroton_Data_AData::LONGID_DELIMITER . $serverId,
+ 'properties' => $this->getSearchEntry('addressbookFolderId' . Syncroton_Data_AData::LONGID_DELIMITER . $serverId, $store->options)
));
}
}
if (count($found) > 0) {
$storeResponse->result = $found;
$storeResponse->range = array(0, count($found) - 1);
$storeResponse->total = $total;
} else {
$storeResponse->total = $total;
}
return $storeResponse;
}
-
- protected function _initData()
- {
- /**
- * used by unit tests only to simulated added folders
- */
- if (!isset(Syncroton_Data_AData::$folders[get_class($this)])) {
- Syncroton_Data_AData::$folders[get_class($this)] = array(
- 'addressbookFolderId' => new Syncroton_Model_Folder(array(
- 'id' => sha1(mt_rand(). microtime()),
- 'serverId' => 'addressbookFolderId',
- 'parentId' => 0,
- 'displayName' => 'Default Contacts Folder',
- 'type' => Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT
- )),
- 'anotherAddressbookFolderId' => new Syncroton_Model_Folder(array(
- 'id' => sha1(mt_rand(). microtime()),
- 'serverId' => 'anotherAddressbookFolderId',
- 'parentId' => 0,
- 'displayName' => 'Another Contacts Folder',
- 'type' => Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED
- ))
- );
- }
-
- /**
- * used by unit tests only to simulated added folders
- */
- $entries = $this->getServerEntries('addressbookFolderId', 1);
-
- if (count($entries) == 0) {
- $testData = array(
- 'addressbookFolderId' => array(
- 'contact1' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact2' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact3' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact4' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact5' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact6' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact7' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact8' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact9' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact10' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- ))
- ),
- 'anotherAddressbookFolderId' => array(
- 'contact1' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact2' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact3' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact4' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact5' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact6' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact7' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact8' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- )),
- 'contact9' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Lars',
- 'lastName' => 'Kneschke'
- )),
- 'contact10' => new Syncroton_Model_Contact(array(
- 'firstName' => 'Cornelius',
- 'lastName' => 'Weiß'
- ))
- )
- );
-
- foreach ($testData['addressbookFolderId'] as $data) {
- $this->createEntry('addressbookFolderId', $data);
- }
- foreach ($testData['anotherAddressbookFolderId'] as $data) {
- $this->createEntry('anotherAddressbookFolderId', $data);
- }
- }
- }
}
diff --git a/lib/ext/Syncroton/Data/Email.php b/lib/ext/Syncroton/Data/Email.php
index 9c84aae..5005c97 100644
--- a/lib/ext/Syncroton/Data/Email.php
+++ b/lib/ext/Syncroton/Data/Email.php
@@ -1,144 +1,86 @@
<?php
/**
* Syncroton
*
* @package Syncroton
* @subpackage Data
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
*/
class Syncroton_Data_Email extends Syncroton_Data_AData implements Syncroton_Data_IDataEmail
{
- /**
- * used by unit tests only to simulated added folders
- */
- public static $entries = array();
+ protected $_supportedFolderTypes = array(
+ Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS,
+ Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS,
+ Syncroton_Command_FolderSync::FOLDERTYPE_INBOX,
+ Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED,
+ Syncroton_Command_FolderSync::FOLDERTYPE_OUTBOX,
+ Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL
+ );
/**
* (non-PHPdoc)
* @see Syncroton_Data_IDataEmail::forwardEmail()
*/
public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime)
{
if ($inputStream == 'triggerException') {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAILBOX_SERVER_OFFLINE);
}
// forward email
}
/**
* (non-PHPdoc)
* @see Syncroton_Data_AData::getFileReference()
*/
public function getFileReference($fileReference)
{
- list($messageId, $partId) = explode('-', $fileReference, 2);
+ list($messageId, $partId) = explode(Syncroton_Data_AData::LONGID_DELIMITER, $fileReference, 2);
// example code
return new Syncroton_Model_FileReference(array(
'contentType' => 'text/plain',
'data' => 'Lars'
));
}
/**
* (non-PHPdoc)
* @see Syncroton_Data_IDataEmail::replyEmail()
*/
public function replyEmail($source, $inputStream, $saveInSent, $replaceMime)
{
// forward email
}
/**
* (non-PHPdoc)
* @see Syncroton_Data_AData::updateEntry()
*/
public function updateEntry($_folderId, $_serverId, Syncroton_Model_IEntry $_entry)
{
// not used by email
}
/**
* (non-PHPdoc)
* @see Syncroton_Data_IDataEmail::sendEmail()
*/
public function sendEmail($inputStream, $saveInSent)
{
if ($inputStream == 'triggerException') {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAILBOX_SERVER_OFFLINE);
}
// send email
}
-
- protected function _initData()
- {
- /**
- * used by unit tests only to simulated added folders
- */
- Syncroton_Data_AData::$folders[get_class($this)] = array(
- 'emailInboxFolderId' => new Syncroton_Model_Folder(array(
- 'id' => sha1(mt_rand(). microtime()),
- 'serverId' => 'emailInboxFolderId',
- 'parentId' => 0,
- 'displayName' => 'Inbox',
- 'type' => Syncroton_Command_FolderSync::FOLDERTYPE_INBOX
- )),
- 'emailSentFolderId' => new Syncroton_Model_Folder(array(
- 'id' => sha1(mt_rand(). microtime()),
- 'serverId' => 'emailSentFolderId',
- 'parentId' => 0,
- 'displayName' => 'Sent',
- 'type' => Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL
- ))
- );
-
- /**
- * used by unit tests only to simulated added folders
- */
-
- $entries = $this->getServerEntries('emailInboxFolderId', 1);
-
- if (count($entries) == 0) {
- $testData = array(
- 'emailInboxFolderId' => array(
- 'email1' => new Syncroton_Model_Email(array(
- 'accountId' => 'FooBar',
- 'attachments' => array(
- new Syncroton_Model_EmailAttachment(array(
- 'fileReference' => '12345abcd',
- 'umAttOrder' => 1
- ))
- ),
- 'categories' => array('123', '456'),
- 'cc' => 'l.kneschke@metaways.de',
- 'dateReceived' => new DateTime('2012-03-21 14:00:00', new DateTimeZone('UTC')),
- 'from' => 'k.kneschke@metaways.de',
- 'subject' => 'Test Subject',
- 'to' => 'j.kneschke@metaways.de',
- 'read' => 1,
- 'body' => new Syncroton_Model_EmailBody(array(
- 'type' => Syncroton_Model_EmailBody::TYPE_PLAINTEXT,
- 'data' => 'Hello!',
- 'truncated' => true,
- 'estimatedDataSize' => 600
- ))
- )),
- )
- );
-
- foreach ($testData['emailInboxFolderId'] as $data) {
- $this->createEntry('emailInboxFolderId', $data);
- }
- }
- }
}
diff --git a/lib/ext/Syncroton/Data/IDataCalendar.php b/lib/ext/Syncroton/Data/IDataCalendar.php
new file mode 100644
index 0000000..2ea6831
--- /dev/null
+++ b/lib/ext/Syncroton/Data/IDataCalendar.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package 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>
+ */
+
+/**
+ * interface for extended calendar backend
+ *
+ * @package Model
+ */
+interface Syncroton_Data_IDataCalendar
+{
+ /**
+ * set attendee status for meeting
+ *
+ * @param Syncroton_Model_MeetingResponse $request the meeting response
+ * @return string id of new calendar entry
+ */
+ public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request);
+}
+
diff --git a/lib/ext/Syncroton/Data/IDataEmail.php b/lib/ext/Syncroton/Data/IDataEmail.php
index 7ecbb37..bca70b9 100644
--- a/lib/ext/Syncroton/Data/IDataEmail.php
+++ b/lib/ext/Syncroton/Data/IDataEmail.php
@@ -1,44 +1,44 @@
<?php
/**
* Syncroton
*
* @package Model
* @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>
*/
/**
- * intace for email backend
+ * interface for extended email backend
*
* @package Model
*/
interface Syncroton_Data_IDataEmail
{
/**
* send an email
*
* @param resource $inputStream
* @param boolean $saveInSent
*/
public function sendEmail($inputStream, $saveInSent);
/**
* forward an email
*
- * @param string|array $source is either a string(LongId) or an arrey with following properties collectionId, itemId and instanceId
+ * @param string|array $source is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
* @param string $inputStream
* @param string $saveInSent
*/
public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime);
/**
* reply to an email
*
- * @param string|array $source is either a string(LongId) or an arrey with following properties collectionId, itemId and instanceId
+ * @param string|array $source is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
* @param string $inputStream
* @param string $saveInSent
*/
public function replyEmail($source, $inputStream, $saveInSent, $replaceMime);
}
diff --git a/lib/ext/Syncroton/Data/Tasks.php b/lib/ext/Syncroton/Data/Tasks.php
index 30a80d2..a0c4756 100644
--- a/lib/ext/Syncroton/Data/Tasks.php
+++ b/lib/ext/Syncroton/Data/Tasks.php
@@ -1,43 +1,24 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
*/
class Syncroton_Data_Tasks extends Syncroton_Data_AData
{
- protected function _initData()
- {
- /**
- * used by unit tests only to simulated added folders
- */
- Syncroton_Data_AData::$folders[get_class($this)] = array(
- 'tasksFolderId' => new Syncroton_Model_Folder(array(
- 'id' => sha1(mt_rand(). microtime()),
- 'serverId' => 'tasksFolderId',
- 'parentId' => 0,
- 'displayName' => 'Default Tasks Folder',
- 'type' => Syncroton_Command_FolderSync::FOLDERTYPE_TASK
- ))
- );
-
- /**
- * used by unit tests only to simulated added folders
- */
- #Syncroton_Data_AData::$entries[get_class($this)] = array(
- # 'tasksFolderId' => array(
- # )
- #);
- }
+ protected $_supportedFolderTypes = array(
+ Syncroton_Command_FolderSync::FOLDERTYPE_TASK,
+ Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED
+ );
}
diff --git a/lib/ext/Syncroton/Model/AEntry.php b/lib/ext/Syncroton/Model/AEntry.php
index ed8c496..1a58827 100644
--- a/lib/ext/Syncroton/Model/AEntry.php
+++ b/lib/ext/Syncroton/Model/AEntry.php
@@ -1,330 +1,326 @@
<?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_AEntry implements Syncroton_Model_IEntry, IteratorAggregate, Countable
{
protected $_xmlBaseElement;
protected $_elements = array();
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);
}
}
/**
* (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 {
$element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
$this->_appendXMLElement($device, $element, $elementProperties, $value);
$domParrent->appendChild($element);
}
}
}
/**
* (non-PHPdoc)
* @see Countable::count()
*/
public function count()
{
return count($this->_elements);
}
/**
* (non-PHPdoc)
* @see IteratorAggregate::getIterator()
*/
public function getIterator()
{
return new ArrayIterator($this->_elements);
}
/**
* (non-PHPdoc)
* @see Syncroton_Model_IEntry::getProperties()
*/
public function getProperties()
{
$properties = array();
foreach($this->_properties as $namespace => $namespaceProperties) {
$properties = array_merge($properties, array_keys($namespaceProperties));
}
return $properties;
}
/**
* (non-PHPdoc)
* @see Syncroton_Model_IEntry::setFromArray()
*/
public function setFromArray(array $properties)
{
- $this->_elements = array();
-
foreach($properties as $key => $value) {
try {
$this->$key = $value; //echo __LINE__ . PHP_EOL;
} catch (InvalidArgumentException $iae) {
//ignore invalid properties
//echo __LINE__ . PHP_EOL; echo $iae->getMessage(); echo $iae->getTraceAsString();
}
}
}
/**
* 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());
}
- $this->_elements = array();
-
foreach (array_keys($this->_properties) as $namespace) {
if ($namespace == 'Internal') {
continue;
}
$this->_parseNamespace($namespace, $properties);
}
return;
}
/**
* add needed xml namespaces to DomDocument
*
* @param unknown_type $domParrent
*/
protected function _addXMLNamespaces(DOMElement $domParrent)
{
foreach($this->_properties as $namespace => $namespaceProperties) {
// don't add default namespace again
if($domParrent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) {
$domParrent->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:'.$namespace, 'uri:'.$namespace);
}
}
}
protected function _appendXMLElement(Syncroton_Model_IDevice $device, DOMElement $element, $elementProperties, $value)
{
if ($value instanceof Syncroton_Model_IEntry) {
$value->appendXML($element, $device);
} else {
if ($value instanceof DateTime) {
$value = $value->format($this->_dateTimeFormat);
} elseif (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {
if (is_resource($value)) {
rewind($value);
$value = stream_get_contents($value);
}
$value = base64_encode($value);
}
if ($elementProperties['type'] == 'byteArray') {
$element->setAttributeNS('uri:Syncroton', 'Syncroton:encoding', 'opaque');
// encode to base64; the wbxml encoder will base64_decode it again
// this way we can also transport data, which would break the xmlparser otherwise
$element->appendChild($element->ownerDocument->createCDATASection(base64_encode($value)));
} else {
// strip off any non printable control characters
if (!ctype_print($value)) {
$value = $this->_removeControlChars($value);
}
$element->appendChild($element->ownerDocument->createTextNode($value));
}
}
}
/**
* removed control chars from string which are not allowd in XML values
*
* @param string|array $_dirty
* @return string
*/
protected function _removeControlChars($dirty)
{
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', null, $dirty);
}
/**
*
* @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 (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");
}
$this->_elements[$name] = $value;
}
public function __isset($name)
{
return isset($this->_elements[$name]);
}
public function __unset($name)
{
unset($this->_elements[$name]);
}
}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Model/Email.php b/lib/ext/Syncroton/Model/Email.php
index 65cb674..35f2661 100644
--- a/lib/ext/Syncroton/Model/Email.php
+++ b/lib/ext/Syncroton/Model/Email.php
@@ -1,81 +1,88 @@
<?php
/**
* Syncroton
*
* @package 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>
*/
/**
* class to handle ActiveSync email
*
* @package Model
- * @property array attachments
- * @property string contentType
- * @property array flag
+ * @property array attachments
+ * @property string contentType
+ * @property array flag
* @property Syncroton_Model_EmailBody body
- * @property array cc
- * @property array to
- * @property int read
+ * @property array cc
+ * @property array to
+ * @property int lastVerbExecuted
+ * @property DateTime lastVerbExecutionTime
+ * @property int read
*/
class Syncroton_Model_Email extends Syncroton_Model_AEntry
{
+ const LASTVERB_UNKNOWN = 0;
+ const LASTVERB_REPLYTOSENDER = 1;
+ const LASTVERB_REPLYTOALL = 2;
+ const LASTVERB_FORWARD = 3;
+
protected $_xmlBaseElement = 'ApplicationData';
protected $_properties = array(
'AirSyncBase' => array(
'attachments' => array('type' => 'container', 'childElement' => 'attachment', 'class' => 'Syncroton_Model_EmailAttachment'),
'contentType' => array('type' => 'string'),
'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody'),
'nativeBodyType' => array('type' => 'number'),
),
'Email' => array(
'busyStatus' => array('type' => 'number'),
'categories' => array('type' => 'container', 'childElement' => 'category', 'supportedSince' => '14.0'),
'cc' => array('type' => 'string'),
'completeTime' => array('type' => 'datetime'),
'contentClass' => array('type' => 'string'),
'dateReceived' => array('type' => 'datetime'),
'disallowNewTimeProposal' => array('type' => 'number'),
'displayTo' => array('type' => 'string'),
'dTStamp' => array('type' => 'datetime'),
'endTime' => array('type' => 'datetime'),
'flag' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailFlag'),
'from' => array('type' => 'string'),
'globalObjId' => array('type' => 'string'),
'importance' => array('type' => 'number'),
'instanceType' => array('type' => 'number'),
'internetCPID' => array('type' => 'string'),
'location' => array('type' => 'string'),
'meetingRequest' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailMeetingRequest'),
'messageClass' => array('type' => 'string'),
'organizer' => array('type' => 'string'),
'read' => array('type' => 'number'),
'recurrences' => array('type' => 'container'),
'reminder' => array('type' => 'number'),
'replyTo' => array('type' => 'string'),
'responseRequested' => array('type' => 'number'),
'sensitivity' => array('type' => 'number'),
'startTime' => array('type' => 'datetime'),
'status' => array('type' => 'number'),
'subject' => array('type' => 'string'),
'threadTopic' => array('type' => 'string'),
'timeZone' => array('type' => 'timezone'),
'to' => array('type' => 'string'),
),
'Email2' => array(
'accountId' => array('type' => 'string', 'supportedSince' => '14.1'),
'conversationId' => array('type' => 'byteArray', 'supportedSince' => '14.0'),
'conversationIndex' => array('type' => 'byteArray', 'supportedSince' => '14.0'),
'lastVerbExecuted' => array('type' => 'number', 'supportedSince' => '14.0'),
'lastVerbExecutionTime' => array('type' => 'datetime', 'supportedSince' => '14.0'),
'meetingMessageType' => array('type' => 'number', 'supportedSince' => '14.1'),
'receivedAsBcc' => array('type' => 'number', 'supportedSince' => '14.0'),
'sender' => array('type' => 'string', 'supportedSince' => '14.0'),
'umCallerID' => array('type' => 'string', 'supportedSince' => '14.0'),
'umUserNotes' => array('type' => 'string', 'supportedSince' => '14.0'),
),
);
}
diff --git a/lib/ext/Syncroton/Model/FileReference.php b/lib/ext/Syncroton/Model/FileReference.php
index 3aa00cb..9f0bbad 100644
--- a/lib/ext/Syncroton/Model/FileReference.php
+++ b/lib/ext/Syncroton/Model/FileReference.php
@@ -1,43 +1,44 @@
<?php
/**
* Syncroton
*
* @package 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>
*/
/**
* class to handle ActiveSync event
*
* @package Model
* @property string ContentType
* @property string Data
*/
class Syncroton_Model_FileReference extends Syncroton_Model_AEntry
{
protected $_xmlBaseElement = 'ApplicationData';
protected $_properties = array(
'AirSyncBase' => array(
'contentType' => array('type' => 'string'),
),
'ItemOperations' => array(
'data' => array('type' => 'string', 'encoding' => 'base64'),
+ 'part' => array('type' => 'number')
)
);
/**
*
* @param SimpleXMLElement $xmlCollection
* @throws InvalidArgumentException
*/
public function setFromSimpleXMLElement(SimpleXMLElement $properties)
{
//do nothing
return;
}
}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Model/Folder.php b/lib/ext/Syncroton/Model/Folder.php
index 33412cf..dfabc34 100644
--- a/lib/ext/Syncroton/Model/Folder.php
+++ b/lib/ext/Syncroton/Model/Folder.php
@@ -1,37 +1,38 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
*/
class Syncroton_Model_Folder extends Syncroton_Model_AEntry implements Syncroton_Model_IFolder
{
protected $_xmlBaseElement = array('FolderUpdate', 'FolderCreate');
protected $_properties = array(
'FolderHierarchy' => array(
'parentId' => array('type' => 'string'),
'serverId' => array('type' => 'string'),
'displayName' => array('type' => 'string'),
'type' => array('type' => 'number')
),
'Internal' => array(
'id' => array('type' => 'string'),
'deviceId' => array('type' => 'string'),
+ 'ownerId' => array('type' => 'string'),
'class' => array('type' => 'string'),
'creationTime' => array('type' => 'datetime'),
'lastfiltertype' => array('type' => 'number')
),
);
}
diff --git a/lib/ext/Syncroton/Model/IDevice.php b/lib/ext/Syncroton/Model/IDevice.php
index b1177cb..e0c59ab 100644
--- a/lib/ext/Syncroton/Model/IDevice.php
+++ b/lib/ext/Syncroton/Model/IDevice.php
@@ -1,50 +1,51 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
* @property string id
* @property string deviceid
* @property string devicetype
* @property string policykey
* @property string policyId
* @property string ownerId
* @property string acsversion
* @property string pingfolder
* @property string pinglifetime
* @property string remotewipe
* @property string useragent
* @property string imei
* @property string model
* @property string friendlyname
* @property string os
* @property string oslanguage
* @property string phonenumber
* @property string pinglifetime
* @property string pingfolder
* @property string contactsfilter_id
* @property string calendarfilter_id
* @property string tasksfilter_id
* @property string emailfilter_id
+ * @property string lastsynccollection
*/
interface Syncroton_Model_IDevice
{
/**
* Returns major firmware version of this device
*
* @return int/string
*/
public function getMajorVersion();
}
diff --git a/lib/ext/Syncroton/Model/IFolder.php b/lib/ext/Syncroton/Model/IFolder.php
index 4ec3700..f27f523 100644
--- a/lib/ext/Syncroton/Model/IFolder.php
+++ b/lib/ext/Syncroton/Model/IFolder.php
@@ -1,30 +1,31 @@
<?php
/**
* Syncroton
*
* @package Model
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
*/
/**
* class to handle ActiveSync Sync command
*
* @package Model
* @property string id
* @property string deviceId
* @property string class
* @property string serverId
* @property string parentId
* @property string displayName
* @property string creationTime
* @property string lastfiltertype
+ * @property string type
*/
interface Syncroton_Model_IFolder
{
}
diff --git a/lib/ext/Syncroton/Model/MeetingResponse.php b/lib/ext/Syncroton/Model/MeetingResponse.php
new file mode 100644
index 0000000..4d1e171
--- /dev/null
+++ b/lib/ext/Syncroton/Model/MeetingResponse.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package 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>
+ */
+
+/**
+ * class to handle MeetingResponse request
+ *
+ * @package Model
+ * @property int userResponse
+ * @property string collectionId
+ * @property string calendarId
+ * @property string requestId
+ * @property string instanceId
+ * @property string longId
+ */
+
+class Syncroton_Model_MeetingResponse extends Syncroton_Model_AEntry
+{
+ protected $_xmlBaseElement = 'Request';
+
+ /**
+ * attendee status
+ */
+ const RESPONSE_ACCEPTED = 1;
+ const RESPONSE_TENTATIVE = 2;
+ const RESPONSE_DECLINED = 3;
+
+ protected $_properties = array(
+ 'MeetingResponse' => array(
+ 'userResponse' => array('type' => 'number'),
+ 'collectionId' => array('type' => 'string'),
+ 'calendarId' => array('type' => 'string'),
+ 'requestId' => array('type' => 'string'),
+ 'instanceId' => array('type' => 'datetime'),
+ ),
+ 'Search' => array(
+ 'longId' => array('type' => 'string')
+ )
+ );
+}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Model/SyncCollection.php b/lib/ext/Syncroton/Model/SyncCollection.php
index 736c582..220226b 100644
--- a/lib/ext/Syncroton/Model/SyncCollection.php
+++ b/lib/ext/Syncroton/Model/SyncCollection.php
@@ -1,272 +1,312 @@
<?php
/**
* Syncroton
*
* @package 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>
*/
/**
* class to handle ActiveSync Sync collection
*
* @package Model
* @property string class
* @property string collectionId
* @property bool deletesAsMoves
* @property bool getChanges
* @property string syncKey
* @property int windowSize
*/
-class Syncroton_Model_SyncCollection
+class Syncroton_Model_SyncCollection extends Syncroton_Model_AEntry
{
- protected $_collection = array();
+ protected $_elements = array(
+ 'syncState' => null,
+ 'folder' => null
+ );
protected $_xmlCollection;
+ protected $_xmlBaseElement = 'Collection';
+
public function __construct($properties = null)
{
if ($properties instanceof SimpleXMLElement) {
$this->setFromSimpleXMLElement($properties);
} elseif (is_array($properties)) {
$this->setFromArray($properties);
- }
+ }
+
+ if (!isset($this->_elements['options'])) {
+ $this->_elements['options'] = array();
+ }
+ if (!isset($this->_elements['options']['filterType'])) {
+ $this->_elements['options']['filterType'] = Syncroton_Command_Sync::FILTER_NOTHING;
+ }
+ if (!isset($this->_elements['options']['mimeSupport'])) {
+ $this->_elements['options']['mimeSupport'] = Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME;
+ }
+ if (!isset($this->_elements['options']['mimeTruncation'])) {
+ $this->_elements['options']['mimeTruncation'] = Syncroton_Command_Sync::TRUNCATE_NOTHING;
+ }
+ if (!isset($this->_elements['options']['bodyPreferences'])) {
+ $this->_elements['options']['bodyPreferences'] = array();
+ }
}
/**
* return XML element which holds all client Add commands
*
* @return SimpleXMLElement
*/
public function getClientAdds()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
throw new InvalidArgumentException('no collection xml element set');
}
return $this->_xmlCollection->Commands->Add;
}
/**
* return XML element which holds all client Change commands
*
* @return SimpleXMLElement
*/
public function getClientChanges()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
throw new InvalidArgumentException('no collection xml element set');
}
return $this->_xmlCollection->Commands->Change;
}
/**
* return XML element which holds all client Delete commands
*
* @return SimpleXMLElement
*/
public function getClientDeletes()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
throw new InvalidArgumentException('no collection xml element set');
}
return $this->_xmlCollection->Commands->Delete;
}
/**
* return XML element which holds all client Fetch commands
*
* @return SimpleXMLElement
*/
public function getClientFetches()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
throw new InvalidArgumentException('no collection xml element set');
}
return $this->_xmlCollection->Commands->Fetch;
}
/**
* check if client sent a Add command
*
* @throws InvalidArgumentException
* @return bool
*/
public function hasClientAdds()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
- throw new InvalidArgumentException('no collection xml element set');
+ return false;
}
return isset($this->_xmlCollection->Commands->Add);
}
/**
* check if client sent a Change command
*
* @throws InvalidArgumentException
* @return bool
*/
public function hasClientChanges()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
- throw new InvalidArgumentException('no collection xml element set');
+ return false;
}
return isset($this->_xmlCollection->Commands->Change);
}
/**
* check if client sent a Delete command
*
* @throws InvalidArgumentException
* @return bool
*/
public function hasClientDeletes()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
- throw new InvalidArgumentException('no collection xml element set');
+ return false;
}
return isset($this->_xmlCollection->Commands->Delete);
}
/**
* check if client sent a Fetch command
*
* @throws InvalidArgumentException
* @return bool
*/
public function hasClientFetches()
{
if (! $this->_xmlCollection instanceof SimpleXMLElement) {
- throw new InvalidArgumentException('no collection xml element set');
+ return false;
}
return isset($this->_xmlCollection->Commands->Fetch);
}
- public function setFromArray(array $properties)
- {
- $this->_collection = array('options' => array(
- 'filterType' => Syncroton_Command_Sync::FILTER_NOTHING,
- 'mimeSupport' => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME,
- 'mimeTruncation' => Syncroton_Command_Sync::TRUNCATE_NOTHING,
- '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;
- }
- }
- }
-
/**
+ * this functions does not only set from SimpleXMLElement but also does merge from SimpleXMLElement
+ * to support partial sync requests
*
- * @param SimpleXMLElement $xmlCollection
+ * @param SimpleXMLElement $properties
* @throws InvalidArgumentException
*/
- public function setFromSimpleXMLElement(SimpleXMLElement $xmlCollection)
+ public function setFromSimpleXMLElement(SimpleXMLElement $properties)
{
- if ($xmlCollection->getName() !== 'Collection') {
- throw new InvalidArgumentException('Unexpected element name: ' . $xmlCollection->getName());
+ if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) {
+ throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
}
- $this->_xmlCollection = $xmlCollection;
+ $this->_xmlCollection = $properties;
- $this->_collection = array(
- 'syncKey' => (int)$xmlCollection->SyncKey,
- 'collectionId' => (string)$xmlCollection->CollectionId,
- 'deletesAsMoves' => isset($xmlCollection->DeletesAsMoves) && (string)$xmlCollection->DeletesAsMoves === '0' ? false : true,
- 'conversationMode' => isset($xmlCollection->ConversationMode) && (string)$xmlCollection->ConversationMode === '0' ? false : true,
- 'getChanges' => isset($xmlCollection->GetChanges) && (string) $xmlCollection->GetChanges === '0' ? false : true,
- 'windowSize' => isset($xmlCollection->WindowSize) ? (int)$xmlCollection->WindowSize : 100,
- 'class' => isset($xmlCollection->Class) ? (string)$xmlCollection->Class : null,
- 'options' => array(
- 'filterType' => Syncroton_Command_Sync::FILTER_NOTHING,
- 'mimeSupport' => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME,
- 'mimeTruncation' => Syncroton_Command_Sync::TRUNCATE_NOTHING,
- 'bodyPreferences' => array()
- ),
-
- 'syncState' => null,
- 'folder' => null
- );
+ if (isset($properties->CollectionId)) {
+ $this->_elements['collectionId'] = (string)$properties->CollectionId;
+ }
+
+ if (isset($properties->SyncKey)) {
+ $this->_elements['syncKey'] = (int)$properties->SyncKey;
+ }
+
+ if (isset($properties->Class)) {
+ $this->_elements['class'] = (string)$properties->Class;
+ } elseif (!array_key_exists('class', $this->_elements)) {
+ $this->_elements['class'] = null;
+ }
- if (isset($xmlCollection->Supported)) {
- // @todo collected supported elements
+ if (isset($properties->WindowSize)) {
+ $this->_elements['windowSize'] = (string)$properties->WindowSize;
+ } elseif (!array_key_exists('windowSize', $this->_elements)) {
+ $this->_elements['windowSize'] = 100;
+ }
+
+ if (isset($properties->DeletesAsMoves)) {
+ if ((string)$properties->DeletesAsMoves === '0') {
+ $this->_elements['deletesAsMoves'] = false;
+ } else {
+ $this->_elements['deletesAsMoves'] = true;
+ }
+ } elseif (!array_key_exists('deletesAsMoves', $this->_elements)) {
+ $this->_elements['deletesAsMoves'] = true;
+ }
+
+ if (isset($properties->ConversationMode)) {
+ if ((string)$properties->ConversationMode === '0') {
+ $this->_elements['conversationMode'] = false;
+ } else {
+ $this->_elements['conversationMode'] = true;
+ }
+ } elseif (!array_key_exists('conversationMode', $this->_elements)) {
+ $this->_elements['conversationMode'] = true;
+ }
+
+ if (isset($properties->GetChanges)) {
+ if ((string)$properties->GetChanges === '0') {
+ $this->_elements['getChanges'] = false;
+ } else {
+ $this->_elements['getChanges'] = true;
+ }
+ } elseif (!array_key_exists('getChanges', $this->_elements)) {
+ $this->_elements['getChanges'] = true;
+ }
+
+ if (isset($properties->Supported)) {
+ // @todo collect supported elements
}
// process options
- if (isset($xmlCollection->Options)) {
+ if (isset($properties->Options)) {
+ $this->_elements['options'] = array();
+
// optional parameters
- if (isset($xmlCollection->Options->FilterType)) {
- $this->_collection['options']['filterType'] = (int)$xmlCollection->Options->FilterType;
+ if (isset($properties->Options->FilterType)) {
+ $this->_elements['options']['filterType'] = (int)$properties->Options->FilterType;
}
- if (isset($xmlCollection->Options->MIMESupport)) {
- $this->_collection['options']['mimeSupport'] = (int)$xmlCollection->Options->MIMESupport;
+ if (isset($properties->Options->MIMESupport)) {
+ $this->_elements['options']['mimeSupport'] = (int)$properties->Options->MIMESupport;
}
- if (isset($xmlCollection->Options->MIMETruncation)) {
- $this->_collection['options']['mimeTruncation'] = (int)$xmlCollection->Options->MIMETruncation;
+ if (isset($properties->Options->MIMETruncation)) {
+ $this->_elements['options']['mimeTruncation'] = (int)$properties->Options->MIMETruncation;
}
- if (isset($xmlCollection->Options->Class)) {
- $this->_collection['options']['class'] = (string)$xmlCollection->Options->Class;
+ if (isset($properties->Options->Class)) {
+ $this->_elements['options']['class'] = (string)$properties->Options->Class;
}
// try to fetch element from AirSyncBase:BodyPreference
- $airSyncBase = $xmlCollection->Options->children('uri:AirSyncBase');
+ $airSyncBase = $properties->Options->children('uri:AirSyncBase');
if (isset($airSyncBase->BodyPreference)) {
foreach ($airSyncBase->BodyPreference as $bodyPreference) {
$type = (int) $bodyPreference->Type;
- $this->_collection['options']['bodyPreferences'][$type] = array(
+ $this->_elements['options']['bodyPreferences'][$type] = array(
'type' => $type
);
// optional
if (isset($bodyPreference->TruncationSize)) {
- $this->_collection['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
+ $this->_elements['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
}
}
}
if (isset($airSyncBase->BodyPartPreference)) {
// process BodyPartPreference elements
}
}
}
+ public function toArray()
+ {
+ $result = array();
+
+ foreach (array('syncKey', 'collectionId', 'deletesAsMoves', 'conversationMode', 'getChanges', 'windowSize', 'class', 'options') as $key) {
+ if (isset($this->$key)) {
+ $result[$key] = $this->$key;
+ }
+ }
+
+ return $result;
+ }
+
public function &__get($name)
{
- if (array_key_exists($name, $this->_collection)) {
- return $this->_collection[$name];
+ if (array_key_exists($name, $this->_elements)) {
+ return $this->_elements[$name];
}
- //echo $name . PHP_EOL;
+ echo $name . PHP_EOL;
return null;
}
public function __set($name, $value)
{
- $this->_collection[$name] = $value;
- }
-
- public function __isset($name)
- {
- return isset($this->_collection[$name]);
- }
-
- public function __unset($name)
- {
- unset($this->_collection[$name]);
+ $this->_elements[$name] = $value;
}
}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Registry.php b/lib/ext/Syncroton/Registry.php
index 6903c5d..1495f8f 100644
--- a/lib/ext/Syncroton/Registry.php
+++ b/lib/ext/Syncroton/Registry.php
@@ -1,388 +1,422 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Syncroton
* @package Syncroton_Registry
* @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: Registry.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
*/
/**
* Generic storage class helps to manage global data.
*
* @category Syncroton
* @package Syncroton_Registry
* @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Syncroton_Registry extends ArrayObject
{
const CALENDAR_DATA_CLASS = 'calendar_data_class';
const CONTACTS_DATA_CLASS = 'contacts_data_class';
const EMAIL_DATA_CLASS = 'email_data_class';
const TASKS_DATA_CLASS = 'tasks_data_class';
const GAL_DATA_CLASS = 'gal_data_class';
const DEFAULT_POLICY = 'default_policy';
+ const PING_TIMEOUT = 'ping_timeout';
+ const QUIET_TIME = 'quiet_time';
const DATABASE = 'database';
const TRANSACTIONMANAGER = 'transactionmanager';
const CONTENTSTATEBACKEND = 'contentstatebackend';
const DEVICEBACKEND = 'devicebackend';
const FOLDERBACKEND = 'folderbackend';
const POLICYBACKEND = 'policybackend';
const SYNCSTATEBACKEND = 'syncstatebackend';
/**
* Class name of the singleton registry object.
* @var string
*/
private static $_registryClassName = 'Syncroton_Registry';
/**
* Registry object provides storage for shared objects.
* @var Syncroton_Registry
*/
private static $_registry = null;
/**
* Retrieves the default registry instance.
*
* @return Syncroton_Registry
*/
public static function getInstance()
{
if (self::$_registry === null) {
self::init();
}
return self::$_registry;
}
/**
* @return Zend_Db_Adapter_Abstract
*/
public static function getDatabase()
{
return self::get(self::DATABASE);
}
/**
* return transaction manager class
*
* @return Syncroton_TransactionManagerInterface
*/
public static function getTransactionManager()
{
if (!self::isRegistered(self::TRANSACTIONMANAGER)) {
self::set(self::TRANSACTIONMANAGER, Syncroton_TransactionManager::getInstance());
}
return self::get(self::TRANSACTIONMANAGER);
}
/**
* Set the default registry instance to a specified instance.
*
* @param Syncroton_Registry $registry An object instance of type Syncroton_Registry,
* or a subclass.
* @return void
* @throws Zend_Exception if registry is already initialized.
*/
public static function setInstance(Syncroton_Registry $registry)
{
if (self::$_registry !== null) {
require_once 'Zend/Exception.php';
throw new Zend_Exception('Registry is already initialized');
}
self::setClassName(get_class($registry));
self::$_registry = $registry;
}
/**
* Initialize the default registry instance.
*
* @return void
*/
protected static function init()
{
self::setInstance(new self::$_registryClassName());
}
/**
* Set the class name to use for the default registry instance.
* Does not affect the currently initialized instance, it only applies
* for the next time you instantiate.
*
* @param string $registryClassName
* @return void
* @throws Zend_Exception if the registry is initialized or if the
* class name is not valid.
*/
public static function setClassName($registryClassName = 'Syncroton_Registry')
{
if (self::$_registry !== null) {
require_once 'Zend/Exception.php';
throw new Zend_Exception('Registry is already initialized');
}
if (!is_string($registryClassName)) {
require_once 'Zend/Exception.php';
throw new Zend_Exception("Argument is not a class name");
}
/**
* @see Zend_Loader
*/
if (!class_exists($registryClassName)) {
require_once 'Zend/Loader.php';
Zend_Loader::loadClass($registryClassName);
}
self::$_registryClassName = $registryClassName;
}
/**
* Unset the default registry instance.
* Primarily used in tearDown() in unit tests.
* @returns void
*/
public static function _unsetInstance()
{
self::$_registry = null;
}
/**
* getter method, basically same as offsetGet().
*
* This method can be called from an object of type Syncroton_Registry, or it
* can be called statically. In the latter case, it uses the default
* static instance stored in the class.
*
* @param string $index - get the value associated with $index
* @return mixed
* @throws Zend_Exception if no entry is registerd for $index.
*/
public static function get($index)
{
$instance = self::getInstance();
if (!$instance->offsetExists($index)) {
require_once 'Zend/Exception.php';
throw new Zend_Exception("No entry is registered for key '$index'");
}
return $instance->offsetGet($index);
}
/**
* returns content state backend
*
* creates Syncroton_Backend_Content on the fly if not before via
* Syncroton_Registry::set(self::CONTENTSTATEBACKEND, $backend);
*
* @return Syncroton_Backend_IContent
*/
public static function getContentStateBackend()
{
if (!self::isRegistered(self::CONTENTSTATEBACKEND)) {
self::set(self::CONTENTSTATEBACKEND, new Syncroton_Backend_Content(self::getDatabase()));
}
return self::get(self::CONTENTSTATEBACKEND);
}
/**
* returns device backend
*
* creates Syncroton_Backend_Device on the fly if not before via
* Syncroton_Registry::set(self::DEVICEBACKEND, $backend);
*
* @return Syncroton_Backend_IDevice
*/
public static function getDeviceBackend()
{
if (!self::isRegistered(self::DEVICEBACKEND)) {
self::set(self::DEVICEBACKEND, new Syncroton_Backend_Device(self::getDatabase()));
}
return self::get(self::DEVICEBACKEND);
}
/**
* returns folder backend
*
* creates Syncroton_Backend_Folder on the fly if not before via
* Syncroton_Registry::set(self::FOLDERBACKEND, $backend);
*
* @return Syncroton_Backend_IFolder
*/
public static function getFolderBackend()
{
if (!self::isRegistered(self::FOLDERBACKEND)) {
self::set(self::FOLDERBACKEND, new Syncroton_Backend_Folder(self::getDatabase()));
}
return self::get(self::FOLDERBACKEND);
}
+ /**
+ * return ping timeout
+ *
+ * sleep "ping timeout" seconds between folder checks in Ping and Sync command
+ *
+ * @return int
+ */
+ public static function getPingTimeout()
+ {
+ if (!self::isRegistered(self::PING_TIMEOUT)) {
+ return 60;
+ }
+
+ return self::get(self::PING_TIMEOUT);
+ }
+
/**
* returns policy backend
*
* creates Syncroton_Backend_Policy on the fly if not set before via
* Syncroton_Registry::set(self::POLICYBACKEND, $backend);
*
* @return Syncroton_Backend_ISyncState
*/
public static function getPolicyBackend()
{
if (!self::isRegistered(self::POLICYBACKEND)) {
self::set(self::POLICYBACKEND, new Syncroton_Backend_Policy(self::getDatabase()));
}
return self::get(self::POLICYBACKEND);
}
+ /**
+ * return quiet time
+ *
+ * don't check folders if last sync was "quiet time" seconds ago
+ *
+ * @return int
+ */
+ public static function getQuietTime()
+ {
+ if (!self::isRegistered(self::QUIET_TIME)) {
+ return 180;
+ }
+
+ return self::get(self::QUIET_TIME);
+ }
+
/**
* returns syncstate backend
*
* creates Syncroton_Backend_SyncState on the fly if not before via
* Syncroton_Registry::set(self::SYNCSTATEBACKEND, $backend);
*
* @return Syncroton_Backend_ISyncState
*/
public static function getSyncStateBackend()
{
if (!self::isRegistered(self::SYNCSTATEBACKEND)) {
self::set(self::SYNCSTATEBACKEND, new Syncroton_Backend_SyncState(self::getDatabase()));
}
return self::get(self::SYNCSTATEBACKEND);
}
/**
* setter method, basically same as offsetSet().
*
* This method can be called from an object of type Syncroton_Registry, or it
* can be called statically. In the latter case, it uses the default
* static instance stored in the class.
*
* @param string $index The location in the ArrayObject in which to store
* the value.
* @param mixed $value The object to store in the ArrayObject.
* @return void
*/
public static function set($index, $value)
{
$instance = self::getInstance();
$instance->offsetSet($index, $value);
}
public static function setDatabase(Zend_Db_Adapter_Abstract $db)
{
self::set(self::DATABASE, $db);
}
public static function setCalendarDataClass($className)
{
if (!class_exists($className)) {
throw new InvalidArgumentException('invalid $_className provided');
}
self::set(self::CALENDAR_DATA_CLASS, $className);
}
public static function setContactsDataClass($className)
{
if (!class_exists($className)) {
throw new InvalidArgumentException('invalid $_className provided');
}
self::set(self::CONTACTS_DATA_CLASS, $className);
}
public static function setEmailDataClass($className)
{
if (!class_exists($className)) {
throw new InvalidArgumentException('invalid $_className provided');
}
self::set(self::EMAIL_DATA_CLASS, $className);
}
public static function setTasksDataClass($className)
{
if (!class_exists($className)) {
throw new InvalidArgumentException('invalid $_className provided');
}
self::set(self::TASKS_DATA_CLASS, $className);
}
public static function setGALDataClass($className)
{
if (!class_exists($className)) {
throw new InvalidArgumentException('invalid $_className provided');
}
self::set(self::GAL_DATA_CLASS, $className);
}
public static function setTransactionManager($manager)
{
self::set(self::TRANSACTIONMANAGER, $manager);
}
/**
* Returns TRUE if the $index is a named value in the registry,
* or FALSE if $index was not found in the registry.
*
* @param string $index
* @return boolean
*/
public static function isRegistered($index)
{
if (self::$_registry === null) {
return false;
}
return self::$_registry->offsetExists($index);
}
/**
* Constructs a parent ArrayObject with default
* ARRAY_AS_PROPS to allow acces as an object
*
* @param array $array data array
* @param integer $flags ArrayObject flags
*/
public function __construct($array = array(), $flags = parent::ARRAY_AS_PROPS)
{
parent::__construct($array, $flags);
}
/**
* @param string $index
* @returns mixed
*
* Workaround for http://bugs.php.net/bug.php?id=40442 (ZF-960).
*/
public function offsetExists($index)
{
return array_key_exists($index, $this);
}
}
diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php
index a9bc110..dc1064e 100644
--- a/lib/ext/Syncroton/Server.php
+++ b/lib/ext/Syncroton/Server.php
@@ -1,353 +1,400 @@
<?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;
+
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
- *
- */
+ * handle options request
+ */
protected function _handleOptions()
{
$command = new Syncroton_Command_Options();
- $command->getResponse();
+ $this->_sendHeaders($command->getHeaders());
}
+ protected function _sendHeaders(array $headers)
+ {
+ foreach ($headers as $name => $value) {
+ header($name . ': ' . $value);
+ }
+ }
+
+ /**
+ * handle post request
+ */
protected function _handlePost()
{
$requestParameters = $this->_getRequestParameters($this->_request);
if ($this->_logger instanceof Zend_Log)
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true));
$className = 'Syncroton_Command_' . $requestParameters['command'];
if(!class_exists($className)) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']);
header("HTTP/1.1 501 not implemented");
return;
}
// get user device
$device = $this->_getUserDevice($this->_userId, $requestParameters);
if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') {
// decode wbxml request
try {
$decoder = new Syncroton_Wbxml_Decoder($this->_body);
$requestBody = $decoder->decode();
if ($this->_logger instanceof Zend_Log) {
$requestBody->formatOutput = true;
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML());
}
} catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) {
$requestBody = NULL;
}
} else {
$requestBody = $this->_body;
}
- if (PHP_SAPI !== 'cli') {
- header("MS-Server-ActiveSync: 14.00.0536.000");
- }
+ header("MS-Server-ActiveSync: 14.00.0536.000");
try {
$command = new $className($requestBody, $device, $requestParameters);
$command->handle();
$response = $command->getResponse();
} catch (Syncroton_Exception_ProvisioningNeeded $sepn) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed");
if (version_compare($device->acsversion, '14.0', '>=')) {
$response = $sepn->domDocument;
} else {
// pre 14.0 method
header("HTTP/1.1 449 Retry after sending a PROVISION command");
return;
}
-
-
} catch (Exception $e) {
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage());
if ($this->_logger instanceof Zend_Log)
$this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
header("HTTP/1.1 500 Internal server error");
return;
}
if ($response instanceof DOMDocument) {
if ($this->_logger instanceof Zend_Log) {
$response->formatOutput = true;
$this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml response:\n" . $response->saveXML());
+ $response->formatOutput = false;
}
-
+
+ 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);
$encoder->encode($response);
-
- // avoid sending headers while running on cli (phpunit)
- if (PHP_SAPI !== 'cli') {
- header("Content-Type: application/vnd.ms-sync.wbxml");
+
+ 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);
+ }
+ }
}
- }
+ }
/**
* return request params
*
* @return array
*/
protected function _getRequestParameters(Zend_Controller_Request_Http $request)
{
- if (!isset($_GET['DeviceId'])) {
+ 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'
);
- // find the first $_GET parameter which has a key but no value
- // the key are the request parameters
- foreach ($_GET as $key => $value) {
- if (empty($value)) {
- $requestParameters = $key;
- break;
- }
- }
-
+ $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));
- $protocolVersion = $unpacked['protocolVersion'];
+ // 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) {
- $unpacked = unpack('H' . $length*2 . 'string', fread($stream, $length));
+ $toUnpack = fread($stream, $length);
+
+ $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack);
$deviceId = $unpacked['string'];
}
// unpack policyKey
$length = ord(fread($stream, 1));
if ($length > 0) {
$unpacked = unpack('Vstring', fread($stream, $length));
$policyKey = $unpacked['string'];
}
-
+
// unpack device type
$length = ord(fread($stream, 1));
if ($length > 0) {
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$deviceType = $unpacked['string'];
}
while (! feof($stream)) {
$tag = ord(fread($stream, 1));
$length = ord(fread($stream, 1));
-
+
switch ($tag) {
- case 0:
+ case self::PARAMETER_ATTACHMENTNAME:
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$attachmentName = $unpacked['string'];
break;
- case 1:
+ case self::PARAMETER_COLLECTIONID:
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$collectionId = $unpacked['string'];
break;
- case 3:
+ case self::PARAMETER_ITEMID:
$unpacked = unpack('A' . $length . 'string', fread($stream, $length));
$itemId = $unpacked['string'];
break;
- case 7:
+ 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
+ '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'),
+ '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'];
$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;
}
}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Wbxml/Abstract.php b/lib/ext/Syncroton/Wbxml/Abstract.php
index 6c86806..7fdd653 100644
--- a/lib/ext/Syncroton/Wbxml/Abstract.php
+++ b/lib/ext/Syncroton/Wbxml/Abstract.php
@@ -1,239 +1,241 @@
<?php
/**
* Syncroton
*
* @package Wbxml
* @subpackage Wbxml
* @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
* @copyright Copyright (c) 2008-2009 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Lars Kneschke <l.kneschke@metaways.de>
* @version $Id:Abstract.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $
*/
/**
* class documentation
*
* @package Wbxml
* @subpackage Wbxml
*/
abstract class Syncroton_Wbxml_Abstract
{
/**
* stream containing the wbxml encoded data
*
* @var resource
*/
protected $_stream;
/**
* the wbxml version
*
* @var string
*/
protected $_version;
/**
* the Document Public Identifier
*
* @var string
*/
protected $_dpi;
/**
* the current active dtd
*
* @var Syncroton_Wbxml_Dtd_Syncml_Abstract
*/
protected $_dtd;
/**
* the charSet used in the wbxml file
*
* @var string
*/
protected $_charSet;
/**
* currently active code page
*
* @var array
*/
protected $_codePage;
/**
* see section 5.5
*
*/
const DPI_WELLKNOWN = 'WELLKNOWN';
/**
* see section 5.5
*
*/
const DPI_STRINGTABLE = 'STRINGTABLE';
const SWITCH_PAGE = 0x00;
const END = 0x01;
const ENTITY = 0x02;
const STR_I = 0x03;
const LITERAL = 0x04;
const EXT_I_0 = 0x40;
const EXT_I_1 = 0x41;
const EXT_I_2 = 0x42;
const PI = 0x43;
const LITERAL_C = 0x44;
const EXT_T_0 = 0x80;
const EXT_T_1 = 0x81;
const EXT_T_2 = 0x82;
const STR_T = 0x83;
const LITERAL_A = 0x84;
const EXT_0 = 0xC0;
const EXT_1 = 0xC1;
const EXT_2 = 0xC2;
const OPAQUE = 0xC3;
const LITERAL_AC = 0xC4;
/**
* the real name for this DPI is "unknown"
* But Microsoft is using them for their ActiveSync stuff
* instead defining their own DPI like the sycnml creators did
*
*/
const DPI_1 = '-//AIRSYNC//DTD AirSync//EN';
/**
* return wellknown identifiers
*
* @param integer $_uInt
* @todo add well known identifiers from section 7.2
* @return string
*/
public function getDPI($_uInt = 0)
{
- if(($dpi = @constant('Syncroton_Wbxml_Abstract::DPI_' . $_uInt)) === NULL) {
+ if(!defined('Syncroton_Wbxml_Abstract::DPI_' . $_uInt)) {
throw new Syncroton_Wbxml_Exception('unknown wellknown identifier: ' . $_uInt);
}
-
+
+ $dpi = constant('Syncroton_Wbxml_Abstract::DPI_' . $_uInt);
+
return $dpi;
}
/**
* return multibyte integer
*
* @return integer
*/
protected function _getMultibyteUInt()
{
$uInt = 0;
do {
$byte = $this->_getByte();
$uInt <<= 7;
$uInt += ($byte & 127);
} while (($byte & 128) != 0);
return $uInt;
}
protected function _getByte()
{
$byte = fread($this->_stream, 1);
if($byte === false) {
throw new Syncroton_Wbxml_Exception("failed reading one byte");
}
return ord($byte);
}
protected function _getOpaque($_length)
{
$string = fread($this->_stream, $_length);
if($string === false) {
throw new Syncroton_Wbxml_Exception("failed reading opaque data");
}
return $string;
}
/**
* get a 0 terminated string
*
* @return string
*/
protected function _getTerminatedString()
{
$string = '';
while (($byte = $this->_getByte()) != 0) {
$string .= chr($byte);
}
return $string;
}
protected function _writeByte($_byte)
{
fwrite($this->_stream, chr($_byte));
}
protected function _writeMultibyteUInt($_integer)
{
$multibyte = NULL;
$remainder = $_integer;
do {
$byte = ($remainder & 127);
$remainder >>= 7;
if($multibyte === NULL) {
$multibyte = chr($byte);
} else {
$multibyte = chr($byte | 128) . $multibyte;
}
} while ($remainder != 0);
fwrite($this->_stream, $multibyte);
}
protected function _writeString($_string)
{
fwrite($this->_stream, $_string);
}
/**
* write opaque string to stream
*
* @param string|resource $_string
* @throws Syncroton_Wbxml_Exception
*/
protected function _writeOpaqueString($_string)
{
if (is_resource($_string)) {
$stream = $_string;
} else {
$stream = fopen("php://temp", 'r+');
fwrite($stream, $_string);
}
$length = ftell($stream);
rewind($stream);
$this->_writeByte(Syncroton_Wbxml_Abstract::OPAQUE);
$this->_writeMultibyteUInt($length);
$writenBytes = stream_copy_to_stream($stream, $this->_stream);
if($writenBytes !== $length) {
throw new Syncroton_Wbxml_Exception('blow');
}
fclose($stream);
}
protected function _writeTerminatedString($_string)
{
$this->_writeByte(Syncroton_Wbxml_Abstract::STR_I);
fwrite($this->_stream, $_string);
fwrite($this->_stream, chr(0));
}
}
\ No newline at end of file
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 03001b7..9316b7a 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -1,341 +1,345 @@
<?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> |
+--------------------------------------------------------------------------+
*/
/**
* Main application class (based on Roundcube Framework)
*/
class kolab_sync extends rcube
{
/**
* Application name
*
* @var string
*/
public $app_name = 'ActiveSync for Kolab'; // no double quotes inside
/**
* Current user
*
* @var rcube_user
*/
public $user;
const CHARSET = 'UTF-8';
- const VERSION = "2.0";
+ const VERSION = "2.1";
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync The one and only instance
*/
static function get_instance()
{
if (!self::$instance || !is_a(self::$instance, 'kolab_sync')) {
self::$instance = new kolab_sync();
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Initialization of class instance
*/
public function startup()
{
// Initialize Syncroton Logger
$debug_mode = $this->config->get('activesync_debug') ? kolab_sync_logger::DEBUG : kolab_sync_logger::WARN;
$this->logger = new kolab_sync_logger($debug_mode);
// Get list of plugins
// WARNING: We can use only plugins that are prepared for this
// e.g. are not using output or rcmail objects or
// doesn't throw errors when using them
$plugins = (array)$this->config->get('activesync_plugins', array('kolab_auth', 'kolab_folders'));
$required = array('libkolab', 'kolab_folders');
// Initialize/load plugins
$this->plugins = kolab_sync_plugin_api::get_instance();
$this->plugins->init($this, $this->task);
$this->plugins->load_plugins($plugins, $required);
}
/**
* Application execution (authentication and ActiveSync)
*/
public function run()
{
$this->plugins->exec_hook('startup', array('task' => 'login'));
// when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule
if (!isset($_SERVER['PHP_AUTH_USER'])) {
// "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..."
if (isset($_SERVER["REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6));
} elseif (isset($_SERVER["REDIRECT_REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
} elseif (isset($_SERVER["Authorization"])) {
$basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6));
} elseif (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6));
}
if (isset($basicAuthData) && !empty($basicAuthData)) {
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(":", $basicAuthData);
}
}
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
// Convert domain.tld\username into username@domain (?)
$username = explode("\\", $_SERVER['PHP_AUTH_USER']);
if (count($username) == 2) {
$_SERVER['PHP_AUTH_USER'] = $username[1];
if (!strpos($_SERVER['PHP_AUTH_USER'], '@') && !empty($username[0])) {
$_SERVER['PHP_AUTH_USER'] .= '@' . $username[0];
}
}
$userid = $this->authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
}
if (empty($userid)) {
header('WWW-Authenticate: Basic realm="' . $this->app_name .'"');
header('HTTP/1.1 401 Unauthorized');
exit;
}
// Save user password for Roundcube Framework
$this->password = $_SERVER['PHP_AUTH_PW'];
// Register Syncroton backends
Syncroton_Registry::set('loggerBackend', $this->logger);
Syncroton_Registry::set(Syncroton_Registry::DATABASE, new kolab_sync_db);
Syncroton_Registry::set(Syncroton_Registry::TRANSACTIONMANAGER, kolab_sync_transaction_manager::getInstance());
Syncroton_Registry::set(Syncroton_Registry::DEVICEBACKEND, new kolab_sync_backend_device);
Syncroton_Registry::set(Syncroton_Registry::FOLDERBACKEND, new kolab_sync_backend_folder);
Syncroton_Registry::set(Syncroton_Registry::SYNCSTATEBACKEND, new kolab_sync_backend_state);
Syncroton_Registry::set(Syncroton_Registry::CONTENTSTATEBACKEND, new kolab_sync_backend_content);
- Syncroton_Registry::set(Syncroton_Registry::POLICYBACKEND, new kolab_sync_backend_policy);
+ Syncroton_Registry::set(Syncroton_Registry::POLICYBACKEND, new kolab_sync_backend_policy);
Syncroton_Registry::setContactsDataClass('kolab_sync_data_contacts');
Syncroton_Registry::setCalendarDataClass('kolab_sync_data_calendar');
Syncroton_Registry::setEmailDataClass('kolab_sync_data_email');
Syncroton_Registry::setTasksDataClass('kolab_sync_data_tasks');
Syncroton_Registry::setGALDataClass('kolab_sync_data_gal');
+ // Configuration
+ Syncroton_Registry::set(Syncroton_Registry::PING_TIMEOUT, $this->config->get('activesync_ping_timeout', 60));
+ Syncroton_Registry::set(Syncroton_Registry::QUIET_TIME, $this->config->get('activesync_quiet_time', 180));
+
// Run Syncroton
$syncroton = new Syncroton_Server($userid);
$syncroton->handle();
}
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @param int User ID
*/
public function authenticate($username, $password)
{
$auth = $this->plugins->exec_hook('authenticate', array(
'host' => $this->select_host($username),
'user' => $username,
'pass' => $password,
'valid' => true,
));
// Authenticate - get Roundcube user ID
if ($auth['valid'] && !$auth['abort']
&& ($userid = $this->login($auth['user'], $auth['pass'], $auth['host']))) {
return $userid;
}
$this->plugins->exec_hook('login_failed', array(
'host' => $auth['host'],
'user' => $auth['user'],
));
}
/**
* Storage host selection
*/
private function select_host($username)
{
// Get IMAP host
$host = $this->config->get('default_host');
if (is_array($host)) {
list($user, $domain) = explode('@', $username);
// try to select host by mail domain
if (!empty($domain)) {
foreach ($host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is not found
if (is_array($host)) {
list($key, $val) = each($default_host);
$host = is_numeric($key) ? $val : $key;
}
}
return rcube_utils::parse_host($host);
}
/**
* Authenticates a user in IMAP and returns Roundcube user ID.
*/
private function login($username, $password, $host)
{
if (empty($username)) {
return null;
}
$login_lc = $this->config->get('login_lc');
$default_port = $this->config->get('default_port', 143);
// parse $host
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port'])) {
$port = $a_host['port'];
}
else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) {
$port = 993;
}
}
if (!$port) {
$port = $default_port;
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username
if ($login_lc) {
if ($login_lc == 2 || $login_lc === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// Here we need IDNA ASCII
// Only rcube_contacts class is using domain names in Unicode
$host = rcube_utils::idn_to_ascii($host);
$username = rcube_utils::idn_to_ascii($username);
// user already registered?
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
// authenticate user in IMAP
$storage = $this->get_storage();
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
return null;
}
// No user in database, but IMAP auth works
if (!is_object($user)) {
if ($this->config->get('auto_create_user')) {
// create a new user record
$user = rcube_user::create($username, $host);
if (!$user) {
self::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to create a user record",
), true, false);
return null;
}
}
else {
self::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Access denied for new user $username. 'auto_create_user' is disabled",
), true, false);
return null;
}
}
// overwrite config with user preferences
$this->user = $user;
$this->config->set_user_prefs((array)$this->user->get_prefs());
$this->set_storage_prop();
setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8');
// force reloading of mailboxes list/data
//$storage->clear_cache('mailboxes', true);
return $user->ID;
}
/**
* Function to be executed in script shutdown
*/
public function shutdown()
{
parent::shutdown();
// write performance stats to logs/console
if ($this->config->get('devel_mode')) {
if (function_exists('memory_get_usage'))
$mem = sprintf('%.1f', memory_get_usage() / 1048576);
if (function_exists('memory_get_peak_usage'))
$mem .= '/' . sprintf('%.1f', memory_get_peak_usage() / 1048576);
$query = $_SERVER['QUERY_STRING'];
$log = $query . ($mem ? ($query ? ' ' : '') . "[$mem]" : '');
if (defined('KOLAB_SYNC_START'))
self::print_timer(KOLAB_SYNC_START, $log);
else
self::console($log);
}
}
}
diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php
index 7cf2ae2..a0c828b 100644
--- a/lib/kolab_sync_data_calendar.php
+++ b/lib/kolab_sync_data_calendar.php
@@ -1,554 +1,565 @@
<?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> |
+--------------------------------------------------------------------------+
*/
/**
* Calendar (Events) data class for Syncroton
*/
-class kolab_sync_data_calendar extends kolab_sync_data
+class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data_IDataCalendar
{
/**
* Mapping from ActiveSync Calendar namespace fields
*/
protected $mapping = array(
'allDayEvent' => 'allday',
//'attendees' => 'attendees',
'body' => 'description',
//'bodyTruncated' => 'bodytruncated',
'busyStatus' => 'free_busy',
//'categories' => 'categories',
'dtStamp' => 'changed',
'endTime' => 'end',
//'exceptions' => 'exceptions',
'location' => 'location',
//'meetingStatus' => 'meetingstatus',
//'organizerEmail' => 'organizeremail',
//'organizerName' => 'organizername',
//'recurrence' => 'recurrence',
//'reminder' => 'reminder',
//'responseRequested' => 'responserequested',
//'responseType' => 'responsetype',
'sensitivity' => 'sensitivity',
'startTime' => 'start',
'subject' => 'title',
//'timezone' => 'timezone',
'uID' => 'uid',
);
/**
* Kolab object type
*
* @var string
*/
protected $modelName = 'event';
/**
* Type of the default folder
*
* @var int
*/
protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR;
/**
* Default container for new entries
*
* @var string
*/
protected $defaultFolder = 'Calendar';
/**
* Type of user created folders
*
* @var int
*/
protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED;
/**
* attendee status
*/
const ATTENDEE_STATUS_UNKNOWN = 0;
const ATTENDEE_STATUS_TENTATIVE = 2;
const ATTENDEE_STATUS_ACCEPTED = 3;
const ATTENDEE_STATUS_DECLINED = 4;
const ATTENDEE_STATUS_NOTRESPONDED = 5;
/**
* attendee types
*/
const ATTENDEE_TYPE_REQUIRED = 1;
const ATTENDEE_TYPE_OPTIONAL = 2;
const ATTENDEE_TYPE_RESOURCE = 3;
/**
* busy status constants
*/
const BUSY_STATUS_FREE = 0;
const BUSY_STATUS_TENTATIVE = 1;
const BUSY_STATUS_BUSY = 2;
const BUSY_STATUS_OUTOFOFFICE = 3;
/**
* Sensitivity values
*/
const SENSITIVITY_NORMAL = 0;
const SENSITIVITY_PERSONAL = 1;
const SENSITIVITY_PRIVATE = 2;
const SENSITIVITY_CONFIDENTIAL = 3;
/**
* Mapping of attendee status
*
* @var array
*/
protected $attendeeStatusMap = array(
'UNKNOWN' => self::ATTENDEE_STATUS_UNKNOWN,
'TENTATIVE' => self::ATTENDEE_STATUS_TENTATIVE,
'ACCEPTED' => self::ATTENDEE_STATUS_ACCEPTED,
'DECLINED' => self::ATTENDEE_STATUS_DECLINED,
'DELEGATED' => self::ATTENDEE_STATUS_UNKNOWN,
'NEEDS-ACTION' => self::ATTENDEE_STATUS_UNKNOWN,
//self::ATTENDEE_STATUS_NOTRESPONDED,
);
/**
* Mapping of attendee type
*
* NOTE: recurrences need extra handling!
* @var array
*/
protected $attendeeTypeMap = array(
'REQ-PARTICIPANT' => self::ATTENDEE_TYPE_REQUIRED,
'OPT-PARTICIPANT' => self::ATTENDEE_TYPE_OPTIONAL,
// 'NON-PARTICIPANT' => self::ATTENDEE_TYPE_RESOURCE,
// 'CHAIR' => self::ATTENDEE_TYPE_RESOURCE,
);
/**
* Mapping of busy status
*
* @var array
*/
protected $busyStatusMap = array(
'free' => self::BUSY_STATUS_FREE,
'tentative' => self::BUSY_STATUS_TENTATIVE,
'busy' => self::BUSY_STATUS_BUSY,
'outofoffice' => self::BUSY_STATUS_OUTOFOFFICE,
);
/**
* mapping of sensitivity
*
* @var array
*/
protected $sensitivityMap = array(
'public' => self::SENSITIVITY_PERSONAL,
'private' => self::SENSITIVITY_PRIVATE,
'confidential' => self::SENSITIVITY_CONFIDENTIAL,
);
/**
* Appends contact data to xml element
*
* @param Syncroton_Model_SyncCollection $collection Collection data
* @param string $serverId Local entry identifier
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId)
{
$event = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId);
$config = $this->getFolderConfig($event['_mailbox']);
$result = array();
// Timezone
// Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows
// only one timezone per-event. We'll use timezone of the start date
if ($event['start'] instanceof DateTime) {
$timezone = $event['start']->getTimezone();
if ($timezone && ($tz_name = $timezone->getName()) != 'UTC') {
$tzc = kolab_sync_timezone_converter::getInstance();
if ($tz_name = $tzc->encodeTimezone($tz_name)) {
$result['timezone'] = $tz_name;
}
}
}
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
$value = $this->getKolabDataItem($event, $name);
switch ($name) {
case 'changed':
case 'end':
case 'start':
// For all-day events Kolab uses different times
// At least Android doesn't display such event as all-day event
if ($value) {
$date = clone $value;
if ($event['allday']) {
if ($name == 'start') {
$date->setTime(0, 0, 0);
}
else if ($name == 'end') {
$date->setTime(0, 0, 0);
$date->modify('+1 day');
}
}
$value = self::date_from_kolab($date);
}
break;
case 'sensitivity':
$value = intval($this->sensitivityMap[$value]);
break;
case 'free_busy':
$value = $this->busyStatusMap[$value];
break;
case 'description':
$value = $this->setBody($value);
break;
}
if (empty($value) || is_array($value)) {
continue;
}
$result[$key] = $value;
}
// Event reminder time
if ($config['ALARMS'] && ($minutes = $this->from_kolab_alarm($event['alarms']))) {
$result['reminder'] = $minutes;
}
// Categories, Roundcube Calendar plugin supports only one category at a time
if (!empty($event['categories'])) {
$result['categories'] = (array) $event['categories'];
}
// Organizer
if (!empty($event['attendees'])) {
foreach ($event['attendees'] as $idx => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
if ($name = $attendee['name']) {
$result['organizerName'] = $name;
}
if ($email = $attendee['email']) {
$result['organizerEmail'] = $email;
}
unset($event['attendees'][$idx]);
break;
}
}
}
// Attendees
if (!empty($event['attendees'])) {
$result['attendees'] = array();
foreach ($event['attendees'] as $idx => $attendee) {
$att = array();
if ($attendee['name']) {
$att['name'] = $name;
}
if ($attendee['email']) {
$att['email'] = $email;
}
if ($this->asversion >= 12) {
$type = isset($attendee['role']) ? $this->attendeeTypeMap[$attendee['role']] : null;
$status = isset($attendee['status']) ? $this->attendeeStatusMap[$attende['status']] : null;
$att['attendeeType'] = $type ? $type : self::ATTENDEE_TYPE_REQUIRED;
$att['attendeeStatus'] = $status ? $status : self::ATTENDEE_STATUS_UNKNOWN;
}
$result['attendees'][] = new Syncroton_Model_EventAttendee($att);
}
/*
// set own status
if (($ownAttendee = Calendar_Model_Attender::getOwnAttender($event->attendee)) !== null
&& ($busyType = array_search($ownAttendee->status, $this->_busyStatusMapping)) !== false
) {
$result['BusyStatus'] = $busyType;
}
*/
}
// Event meeting status
$result['meetingStatus'] = intval(!empty($result['attendees']));
// Recurrence
$result['recurrence'] = $this->recurrence_from_kolab($event);
// Recurrence exceptions
$result['exceptions'] = $this->exceptions_from_kolab($event, $result['startTime']);
return new Syncroton_Model_Event($result);
}
/**
* convert contact from xml to libkolab array
*
* @param Syncroton_Model_IEntry $data Contact to convert
* @param string $folderid Folder identifier
* @param array $entry Existing entry
*
* @return array
*/
public function toKolab(Syncroton_Model_IEntry $data, $folderid, $entry = null)
{
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$event = !empty($entry) ? $entry : array();
$config = $this->getFolderConfig($foldername);
$event['allday'] = 0;
// Timezone
if (isset($data->timezone)) {
$tzc = kolab_sync_timezone_converter::getInstance();
$expected = kolab_format::$timezone->getName();
if (!empty($event['start']) && ($event['start'] instanceof DateTime)) {
$expected = $event['start']->getTimezone()->getName();
}
$timezone = $tzc->getTimezone($data->timezone, $expected);
try {
$timezone = new DateTimeZone($timezone);
}
catch (Exception $e) {
$timezone = null;
}
}
if (empty($timezone)) {
$timezone = new DateTimeZone('UTC');
}
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
$value = $data->$key;
switch ($name) {
case 'changed':
$value = null;
break;
case 'end':
case 'start':
if ($timezone && $value) {
$value->setTimezone($timezone);
}
// In ActiveSync all-day event ends on 00:00:00 next day
if ($value && $data->allDayEvent && $name == 'end') {
$value->modify('-1 second');
}
break;
case 'sensitivity':
$map = array_flip($this->sensitivityMap);
$value = $map[$value];
break;
case 'free_busy':
$map = array_flip($this->busyStatusMap);
$value = $map[$value];
break;
case 'description':
$value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT);
// If description isn't specified keep old description
if ($value === null) {
continue 2;
}
break;
case 'uid':
// If UID is too long, use auto-generated UID (#1034)
// It's because UID is used as ServerId which cannot be longer than 64 chars
if (strlen($value) > 64) {
$value = null;
}
break;
}
$this->setKolabDataItem($event, $name, $value);
}
// Try to fix allday events from Android
// It doesn't set all-day flag but the period is a whole day
if (!$event['allday'] && $event['end'] && $event['start']) {
$interval = @date_diff($event['start'], $event['end']);
if ($interval && $interval->format('%y%m%d%h%i%s') == '001000') {
$event['allday'] = 1;
$event['end'] = clone $event['start'];
}
}
// Reminder
// @TODO: should alarms be used when importing event from phone?
if ($config['ALARMS']) {
$event['alarms'] = $this->to_kolab_alarm($data->reminder, $event);
}
$event['attendees'] = array();
$event['categories'] = array();
// Categories
if (isset($data->categories)) {
foreach ($data->categories as $category) {
$event['categories'][] = $category;
}
}
// Organizer
$name = $data->organizerName;
$email = $data->organizerEmail;
if ($name || $email) {
$event['attendees'][] = array(
'role' => 'ORGANIZER',
'name' => $name,
'email' => $email,
);
}
// Attendees
if (isset($data->attendees)) {
foreach ($data->attendees as $attendee) {
$role = false;
if (isset($attendee->attendeeType)) {
$role = array_search($attendee->attendeeType, $this->attendeeTypeMap);
}
if ($role === false) {
$role = array_search(self::ATTENDEE_TYPE_REQUIRED, $this->attendeeTypeMap);
}
// AttendeeStatus send only on repsonse (?)
$event['attendees'][] = array(
'role' => $role,
'name' => $attendee->name,
'email' => $attendee->email,
);
}
}
// recurrence
$event['recurrence'] = $this->recurrence_to_kolab($data->recurrence, $timezone);
// recurrence exceptions
$event['recurrence']['EXDATE'] = $this->exceptions_to_kolab($data->exceptions, $timezone);
return $event;
}
+ /**
+ * Set attendee status for meeting
+ *
+ * @param Syncroton_Model_MeetingResponse $request The meeting response
+ *
+ * @return string ID of new calendar entry
+ */
+ public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request)
+ {
+ // @TODO: not implemented
+ throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingMeeting::MEETING_ERROR);
+ }
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
$filter = array();
switch ($filter_type) {
case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
$mod = '-2 weeks';
break;
case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
$mod = '-1 month';
break;
case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK:
$mod = '-3 months';
break;
case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK:
$mod = '-6 months';
break;
}
if (!empty($mod)) {
$dt = new DateTime('now', new DateTimeZone('UTC'));
$dt->modify($mod);
$filter[] = array('dtend', '>', $dt);
}
return $filter;
}
-
/**
* Converts libkolab alarms string into number of minutes
*/
protected function from_kolab_alarm($value)
{
// e.g. '-15M:DISPLAY'
// Ignore EMAIL alarms
if (preg_match('/^-([0-9]+)([WDHMS]):(DISPLAY|AUDIO)$/', $value, $matches)) {
$value = intval($matches[1]);
switch ($matches[2]) {
case 'S': $value = 1; break;
case 'H': $value *= 60; break;
case 'D': $value *= 24 * 60; break;
case 'W': $value *= 7 * 24 * 60; break;
}
return $value;
}
}
/**
* Converts ActiveSync libkolab alarms string into number of minutes
*/
protected function to_kolab_alarm($value, $event)
{
// Get alarm type from old event object if exists
if (!empty($event['alarms']) && preg_match('/:(.*)$/', $event['alarms'], $matches)) {
$type = $matches[1];
}
if ($value) {
return sprintf('-%dM:%s', $value, $type ? $type : 'DISPLAY');
}
if ($type == 'DISPLAY' || $type == 'AUDIO') {
return null;
}
return $event['alarms'];
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Mar 19, 9:08 AM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
458001
Default Alt Text
(258 KB)

Event Timeline