diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..fdc17eb --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,47 @@ +in([__DIR__]) + ->exclude([ + 'lib/ext', + 'lib/plugins', + 'vendor', + ]) + ->ignoreDotFiles(false) + ->name('*.php.dist'); + +return (new Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PHP74Migration' => true, + '@PHP74Migration:risky' => true, + '@PHP80Migration' => true, + '@PSR1' => true, + '@PSR12' => true, + + 'concat_space' => [ + 'spacing' => 'one', + ], + + 'declare_strict_types' => false, + 'phpdoc_add_missing_param_annotation' => false, + 'use_arrow_functions' => false, + 'void_return' => false, + + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + ], + + // TODO - risky + 'no_unset_on_property' => false, + 'random_api_migration' => false, + 'strict_param' => false, + ]) + ->setFinder($finder) + ->setCacheFile(sys_get_temp_dir() . '/php-cs-fixer.' . md5(__DIR__) . '.cache'); diff --git a/composer.json-dist b/composer.json-dist index 18f8bdf..0f3030a 100644 --- a/composer.json-dist +++ b/composer.json-dist @@ -1,28 +1,29 @@ { "name": "kolab/syncroton", "description": "The ActiveSync Service for Kolab", "license": "AGPL-3.0+", "require": { "php": ">=7.2.0", "kolab/net_ldap3": "dev-master", "pear/pear-core-minimal": "~1.10.1", "pear/net_socket": "~1.2.1", "pear/auth_sasl": "~1.1.0", "pear/http_request2": "~2.5.0", "pear/net_idna2": "~0.2.0", "pear/mail_mime": "~1.10.0", "pear/net_smtp": "~1.7.1", "pear/net_ldap2": "~2.2.0", "pear/net_sieve": "~1.4.0", "roundcube/rtf-html-php": "~2.1", "sabre/vobject": "~4.5.1", "zf1s/zend-controller": "~1.12.20", "zf1s/zend-json": "~1.12.20", "zf1s/zend-log": "~1.12.20" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", "guzzlehttp/guzzle": "^7.3.0", "phpstan/phpstan": "^1.2", "phpunit/phpunit": "^8 || ^9" } } diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist index c010e3a..a3d0bcd 100644 --- a/config/config.inc.php.dist +++ b/config/config.inc.php.dist @@ -1,123 +1,123 @@ 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', ); */ $config['activesync_gal_fieldmap'] = null; // List of device types that will sync the LDAP addressbook(s) as a normal folder. // For devices that do not support GAL searching, e.g. Outlook. // Note: To make the LDAP addressbook sources working we need two additional // fields ('uid' and 'changed') specified in the fieldmap array // of the LDAP configuration ('ldap_public' option). For example: // 'uid' => 'nsuniqueid', // 'changed' => 'modifytimestamp', // Examples: // array('windowsoutlook') # enable for Oultook only // true # enable for all $config['activesync_gal_sync'] = false; // GAL cache. As reading all contacts from LDAP may be slow, caching is recommended. $config['activesync_gal_cache'] = 'db'; // TTL of GAL cache entries. Technically this causes that synchronized // contacts will not be updated (queried) often than the specified interval. $config['activesync_gal_cache_ttl'] = '1d'; // List of Roundcube plugins // WARNING: Not all plugins used in Roundcube can be listed here -$config['activesync_plugins'] = array( +$config['activesync_plugins'] = [ 'libcalendaring', - 'libkolab' -); + 'libkolab', +]; // Defines for how many seconds we'll sleep between every // action for detecting changes in folders. Default: 60 $config['activesync_ping_timeout'] = 60; // Defines maximum Ping interval in seconds. Default: 900 (15 minutes) $config['activesync_ping_interval'] = 900; // We start detecting changes n seconds since the last sync of a folder // Default: 180 $config['activesync_quiet_time'] = 180; // Defines maximum number of folders in a single Sync/Ping request. Default: 100. $config['activesync_max_folders'] = 100; // When a device is reqistered, by default a set of folders are // subscribed for syncronization, i.e. INBOX and personal folders with // defined folder type: // mail.drafts, mail.wastebasket, mail.sentitems, mail.outbox, // event, event.default, // contact, contact.default, // task, task.default // This default set can be extended by adding following values: // 1 - all subscribed folders in personal namespace // 2 - all folders in personal namespace // 4 - all subscribed folders in other users namespace // 8 - all folders in other users namespace // 16 - all subscribed folders in shared namespace // 32 - all folders in shared namespace $config['activesync_init_subscriptions'] = 21; // Defines blacklist of devices (device type strings) that do not support folder hierarchies. // When set to an array folder hierarchies are used on all devices not listed here. // When set to null an old whitelist approach will be used where we do opposite // action and enable folder hierarchies only on device types known to support it. -$config['activesync_multifolder_blacklist'] = array(); +$config['activesync_multifolder_blacklist'] = []; // Blacklist overwrites for specified object type. If set to an array // it will have a precedence over 'activesync_multifolder_blacklist' list only for that type. // Note: Outlook does not support multiple folders for contacts, // in that case use $config['activesync_multifolder_blacklist_contact'] = array('windowsoutlook'); $config['activesync_multifolder_blacklist_mail'] = null; $config['activesync_multifolder_blacklist_event'] = null; -$config['activesync_multifolder_blacklist_contact'] = array('windowsoutlook'); +$config['activesync_multifolder_blacklist_contact'] = ['windowsoutlook']; $config['activesync_multifolder_blacklist_note'] = null; $config['activesync_multifolder_blacklist_task'] = null; // Enables adding sender name in the From: header of send email // when a device uses email address only (e.g. iOS devices) $config['activesync_fix_from'] = false; diff --git a/index.php b/index.php index 3cc82d9..120d96d 100644 --- a/index.php +++ b/index.php @@ -1,31 +1,31 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ // environment initialization require_once './lib/init.php'; $KSYNC = kolab_sync::get_instance(); $KSYNC->run(); diff --git a/lib/init.php b/lib/init.php index f729aaa..df48d94 100644 --- a/lib/init.php +++ b/lib/init.php @@ -1,69 +1,69 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ define('KOLAB_SYNC_START', microtime(true)); // Roundcube Framework constants define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__) . '/../') . '/'); define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/plugins/'); // Define include path $include_path = RCUBE_INSTALL_PATH . 'lib' . PATH_SEPARATOR; $include_path .= RCUBE_INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR; $include_path .= ini_get('include_path'); set_include_path($include_path); // include composer autoloader (if available) if (@file_exists(RCUBE_INSTALL_PATH . 'vendor/autoload.php')) { require RCUBE_INSTALL_PATH . 'vendor/autoload.php'; } // include global functions from Roundcube Framework require_once 'Roundcube/bootstrap.php'; // Register main autoloader spl_autoload_register('kolab_sync_autoload'); // Autoloader for Syncroton //require_once 'Zend/Loader/Autoloader.php'; //$autoloader = Zend_Loader_Autoloader::getInstance(); //$autoloader->setFallbackAutoloader(true); /** * Use PHP5 autoload for dynamic class loading */ function kolab_sync_autoload($classname) { // Syncroton, replacement for Zend autoloader $filename = str_replace('_', DIRECTORY_SEPARATOR, $classname); if ($fp = @fopen("$filename.php", 'r', true)) { fclose($fp); include_once "$filename.php"; return true; } return false; } diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php index a709f58..b4543ce 100644 --- a/lib/kolab_sync.php +++ b/lib/kolab_sync.php @@ -1,559 +1,556 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Main application class (based on Roundcube Framework) */ class kolab_sync extends rcube { /** @var string Application name */ public $app_name = 'ActiveSync for Kolab'; // no double quotes inside /** @var string|null Request user name */ public $username; /** @var string|null Request user password */ public $password; public $task; protected $per_user_log_dir; protected $log_dir; protected $logger; - const CHARSET = 'UTF-8'; - const VERSION = "2.4.2"; + public const CHARSET = 'UTF-8'; + public const VERSION = "2.4.2"; /** * This implements the 'singleton' design pattern * * @param int $mode Unused * @param string $env Unused * * @return kolab_sync The one and only instance */ - static function get_instance($mode = 0, $env = '') + public static function get_instance($mode = 0, $env = '') { 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); $this->log_dir = $this->config->get('log_dir'); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects and // do not throw errors when using them - $plugins = (array)$this->config->get('activesync_plugins', array('kolab_auth')); - $plugins = array_unique(array_merge($plugins, array('libkolab', 'libcalendaring'))); + $plugins = (array)$this->config->get('activesync_plugins', ['kolab_auth']); + $plugins = array_unique(array_merge($plugins, ['libkolab', 'libcalendaring'])); // Initialize/load plugins $this->plugins = kolab_sync_plugin_api::get_instance(); $this->plugins->init($this, $this->task); // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->plugins->load_plugin($plugin, true); } } /** * Application execution (authentication and ActiveSync) */ public function run() { // 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); + [$_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]; } } // Set log directory per-user $this->set_log_dir($_SERVER['PHP_AUTH_USER']); // Authenticate the user $userid = $this->authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); } if (empty($userid)) { - header('WWW-Authenticate: Basic realm="' . $this->app_name .'"'); + header('WWW-Authenticate: Basic realm="' . $this->app_name . '"'); header('HTTP/1.1 401 Unauthorized'); exit; } - $this->plugins->exec_hook('ready', array('task' => 'syncroton')); + $this->plugins->exec_hook('ready', ['task' => 'syncroton']); // Set log directory per-user (again, in case the username changed above) $this->set_log_dir(); // Save user password for Roundcube Framework $this->password = $_SERVER['PHP_AUTH_PW']; // Register Syncroton backends/callbacks - Syncroton_Registry::set(Syncroton_Registry::LOGGERBACKEND, $this->logger); - Syncroton_Registry::set(Syncroton_Registry::DATABASE, $this->get_dbh()); - 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::SLEEP_CALLBACK, array($this, 'sleep')); + Syncroton_Registry::set(Syncroton_Registry::LOGGERBACKEND, $this->logger); + Syncroton_Registry::set(Syncroton_Registry::DATABASE, $this->get_dbh()); + 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::SLEEP_CALLBACK, [$this, 'sleep']); Syncroton_Registry::setContactsDataClass('kolab_sync_data_contacts'); Syncroton_Registry::setCalendarDataClass('kolab_sync_data_calendar'); Syncroton_Registry::setEmailDataClass('kolab_sync_data_email'); Syncroton_Registry::setNotesDataClass('kolab_sync_data_notes'); Syncroton_Registry::setTasksDataClass('kolab_sync_data_tasks'); Syncroton_Registry::setGALDataClass('kolab_sync_data_gal'); // Configuration - Syncroton_Registry::set(Syncroton_Registry::PING_TIMEOUT, (int) $this->config->get('activesync_ping_timeout', 60)); - Syncroton_Registry::set(Syncroton_Registry::PING_INTERVAL, (int) $this->config->get('activesync_ping_interval', 15 * 60)); - Syncroton_Registry::set(Syncroton_Registry::QUIET_TIME, (int) $this->config->get('activesync_quiet_time', 3 * 60)); + Syncroton_Registry::set(Syncroton_Registry::PING_TIMEOUT, (int) $this->config->get('activesync_ping_timeout', 60)); + Syncroton_Registry::set(Syncroton_Registry::PING_INTERVAL, (int) $this->config->get('activesync_ping_interval', 15 * 60)); + Syncroton_Registry::set(Syncroton_Registry::QUIET_TIME, (int) $this->config->get('activesync_quiet_time', 3 * 60)); Syncroton_Registry::set(Syncroton_Registry::MAX_COLLECTIONS, (int) $this->config->get('activesync_max_folders', 100)); // Run Syncroton $syncroton = new Syncroton_Server($userid); $syncroton->handle(); } /** * Authenticates a user * * @param string $username User name * @param string $password User password * * @return null|int User ID */ public function authenticate($username, $password) { // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->get_cache_shared('activesync_auth'); $host = $this->select_host($username); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { - $auth = $this->plugins->exec_hook('authenticate', array( + $auth = $this->plugins->exec_hook('authenticate', [ 'host' => $host, 'user' => $username, 'pass' => $password, - )); + ]); if (!$auth['abort'] && $cache) { - $cache->set($cache_key, array( + $cache->set($cache_key, [ 'user' => $auth['user'], 'host' => $auth['host'], - )); + ]); } // LDAP server failure... send 503 error if (!empty($auth['kolab_ldap_error'])) { self::server_error(); } // Close LDAP connection from kolab_auth plugin if (class_exists('kolab_auth', false)) { kolab_auth::ldap_close(); } - } - else { + } else { $auth['pass'] = $password; } $err = null; // Authenticate - get Roundcube user ID if (empty($auth['abort']) && ($userid = $this->login($auth['user'], $auth['pass'], $auth['host'], $err))) { // set real username $this->username = $auth['user']; return $userid; } if ($err) { $err_str = $this->get_storage()->get_error_str(); } if (class_exists('kolab_auth', false)) { kolab_auth::log_login_error($auth['user'], !empty($err_str) ? $err_str : $err); } - $this->plugins->exec_hook('login_failed', array( + $this->plugins->exec_hook('login_failed', [ 'host' => $auth['host'], 'user' => $auth['user'], - )); + ]); // IMAP server failure... send 503 error if ($err == rcube_imap_generic::ERROR_BAD) { self::server_error(); } return null; } /** * Storage host selection */ private function select_host($username) { // Get IMAP host $host = $this->config->get('imap_host', $this->config->get('default_host')); if (is_array($host)) { - list($user, $domain) = explode('@', $username); + [$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) { + } elseif (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)) { $key = key($host); $host = is_numeric($key) ? $host[$key] : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ private function login($username, $password, $host, &$error = null) { 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); $port = null; $ssl = null; if (!empty($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; + $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], ['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)) { + } elseif ($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, '@')) { + } elseif (strpos($username, '@')) { // lowercase domain name - list($local, $domain) = explode('@', $username); + [$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)) { $error = $storage->get_error_code(); 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( + self::raise_error([ 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", - ), true, false); + ], true, false); return null; } - } - else { - self::raise_error(array( + } else { + self::raise_error([ 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", - ), true, false); + ], 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(); // required by rcube_utils::parse_host() later $_SESSION['storage_host'] = $host; setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); // force reloading of mailboxes list/data //$storage->clear_cache('mailboxes', true); return $user->ID; } /** * Initializes and returns the storage backend object */ public static function storage() { $class = 'kolab_sync_storage'; $self = self::get_instance(); if (($name = $self->config->get('activesync_storage')) && $name != 'kolab') { $class .= '_' . strtolower($name); } return $class::get_instance(); } /** * Set logging directory per-user */ protected function set_log_dir($username = null) { if (empty($username)) { $username = $this->username; } if (empty($username)) { return; } $this->logger->set_username($username); $user_debug = (bool) $this->config->get('per_user_logging'); if (!$user_debug) { return; } $log_dir = $this->log_dir . DIRECTORY_SEPARATOR . $username; // No automatically creating any log directories if (!is_dir($log_dir)) { $this->logger->set_log_dir(null); return; } $deviceId = null; if (!empty($_GET['DeviceId'])) { $deviceId = $_GET['DeviceId']; - } - else if ( + } elseif ( !empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], '&') == false && ($query = base64_decode($_SERVER['QUERY_STRING'])) && strlen($query) > 8 ) { // unpack the first 5 bytes, the last one is a length of the device id $unpacked = unpack('Cversion/Ccommand/vlocale/Clength', substr($query, 0, 5)); // unpack the deviceId, with some input sanity checks if ( !empty($unpacked['version']) && !empty($unpacked['length']) && $unpacked['version'] >= 121 && ($length = $unpacked['length']) > 0 && $length <= 32 ) { $unpacked = unpack("H" . ($length * 2) . "string", $query, 5); $deviceId = $unpacked['string']; } } if (!empty($deviceId)) { $dev_dir = $log_dir . DIRECTORY_SEPARATOR . $deviceId; if (is_dir($dev_dir) || mkdir($dev_dir, 0770)) { $log_dir = $dev_dir; } } $this->per_user_log_dir = $log_dir; $this->logger->set_log_dir($log_dir); $this->config->set('log_dir', $log_dir); } /** * Get the per-user log directory */ public function get_user_log_dir() { return $this->per_user_log_dir; } /** * Send HTTP 503 response. * We send it on LDAP/IMAP server error instead of 401 (Unauth), * so devices will not ask for new password. */ public static function server_error() { if (php_sapi_name() == 'cli') { throw new Exception("LDAP/IMAP error on authentication"); } header("HTTP/1.1 503 Service Temporarily Unavailable"); header("Retry-After: 120"); exit; } /** * Function to be executed in script shutdown */ public function shutdown() { parent::shutdown(); // cache garbage collector $this->gc_run(); // write performance stats to logs/console if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) { // we have to disable per_user_logging to make sure stats end up in the main console log $this->config->set('per_user_logging', false); $this->config->set('log_dir', $this->log_dir); // make sure logged numbers use unified format setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); $mem = ''; - if (function_exists('memory_get_usage')) + if (function_exists('memory_get_usage')) { $mem = round(memory_get_usage() / 1048576, 1); - if (function_exists('memory_get_peak_usage')) + } + if (function_exists('memory_get_peak_usage')) { $mem .= '/' . round(memory_get_peak_usage() / 1048576, 1); + } $query = $_SERVER['QUERY_STRING'] ?? ''; $log = $query . ($mem ? ($query ? ' ' : '') . "[$mem]" : ''); - if (defined('KOLAB_SYNC_START')) + if (defined('KOLAB_SYNC_START')) { self::print_timer(KOLAB_SYNC_START, $log); - else + } else { self::console($log); + } } } /** * When you're going to sleep the script execution for a longer time * it is good to close all external connections (sql, memcache, SMTP, IMAP). * * No action is required on wake up, all connections will be * re-established automatically. */ public function sleep() { parent::sleep(); // We'll have LDAP addressbooks here if using activesync_gal_sync if ($this->config->get('activesync_gal_sync')) { foreach (kolab_sync_data_gal::$address_books as $book) { $book->close(); } - kolab_sync_data_gal::$address_books = array(); + kolab_sync_data_gal::$address_books = []; } // Reset internal cache of the storage class self::storage()->reset(); } } diff --git a/lib/kolab_sync_backend_common.php b/lib/kolab_sync_backend_common.php index 36c16c2..75a4793 100644 --- a/lib/kolab_sync_backend_common.php +++ b/lib/kolab_sync_backend_common.php @@ -1,281 +1,282 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Parent backend class for kolab backends */ class kolab_sync_backend_common implements Syncroton_Backend_IBackend { /** * Table name * * @var string */ protected $table_name; /** * Model interface name * * @var string */ protected $interface_name; /** * Backend interface name * * @var string */ protected $class_name; /** * SQL Database engine * * @var rcube_db */ protected $db; /** * Internal cache (in-memory) * * @var array */ - protected $cache = array(); + protected $cache = []; /** * Constructor */ - function __construct() + public function __construct() { $this->db = rcube::get_instance()->get_dbh(); if (empty($this->class_name)) { $this->class_name = str_replace('Model_I', 'Model_', $this->interface_name); } } /** * Creates new Syncroton object in database * * @param object $object Object * * @return object Object * @throws InvalidArgumentException|Syncroton_Exception_DeadlockDetected|Exception */ public function create($object) { if (! $object instanceof $this->interface_name) { throw new InvalidArgumentException('$object must be instance of ' . $this->interface_name); } $data = $this->object_to_array($object); - $cols = array(); + $cols = []; - $data['id'] = $object->id = sha1(mt_rand(). microtime()); + $data['id'] = $object->id = sha1(mt_rand() . microtime()); foreach (array_keys($data) as $key) { $cols[] = $this->db->quote_identifier($key); } - $result = $this->db->query('INSERT INTO `' . $this->table_name . '`' . ' (' . implode(', ', $cols) . ')' + $result = $this->db->query( + 'INSERT INTO `' . $this->table_name . '`' . ' (' . implode(', ', $cols) . ')' . ' VALUES(' . implode(', ', array_fill(0, count($cols), '?')) . ')', array_values($data) ); if ($err = $this->db->is_error($result)) { $err = "Failed to save instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { throw new Syncroton_Exception_DeadlockDetected($err); } else { throw new Exception($err); } } return $object; } /** * Returns Syncroton data object * * @param string $id * * @throws Syncroton_Exception_NotFound * @return object */ public function get($id) { $id = $id instanceof $this->interface_name ? $id->id : $id; if ($id) { - $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE `id` = ?', array($id)); + $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE `id` = ?', [$id]); $data = $this->db->fetch_assoc($select); } if (empty($data)) { throw new Syncroton_Exception_NotFound('Object not found'); } return $this->get_object($data); } /** * Deletes Syncroton data object * * @param string|object $id Object or identifier * * @return bool True on success, False on failure * @throws Syncroton_Exception_DeadlockDetected|Exception */ public function delete($id) { $id = $id instanceof $this->interface_name ? $id->id : $id; if (!$id) { return false; } - $result = $this->db->query('DELETE FROM `' . $this->table_name .'` WHERE `id` = ?', array($id)); + $result = $this->db->query('DELETE FROM `' . $this->table_name . '` WHERE `id` = ?', [$id]); if ($err = $this->db->is_error($result)) { $err = "Failed to delete instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { throw new Syncroton_Exception_DeadlockDetected($err); } else { throw new Exception($err); } } return (bool) $this->db->affected_rows($result); } /** * Updates Syncroton data object * * @param object $object * * @return object Object * @throws InvalidArgumentException|Syncroton_Exception_DeadlockDetected|Exception */ public function update($object) { if (! $object instanceof $this->interface_name) { throw new InvalidArgumentException('$object must be instanace of ' . $this->interface_name); } $data = $this->object_to_array($object); - $set = array(); + $set = []; foreach (array_keys($data) as $key) { $set[] = $this->db->quote_identifier($key) . ' = ?'; } $result = $this->db->query('UPDATE `' . $this->table_name . '` SET ' . implode(', ', $set) . ' WHERE `id` = ' . $this->db->quote($object->id), array_values($data)); if ($err = $this->db->is_error($result)) { $err = "Failed to update instance of {$this->interface_name}: {$err}"; if ($this->db->error_info()[0] == '40001') { throw new Syncroton_Exception_DeadlockDetected($err); } else { throw new Exception($err); } } return $object; } /** * Returns list of user accounts * * @param Syncroton_Model_Device $device The current device * * @return array List of Syncroton_Model_Account objects */ public function userAccounts($device) { // this method is overwritten by kolab_sync_backend class return []; } /** * Convert array into model object */ protected function get_object($data) { foreach ($data as $key => $value) { unset($data[$key]); if (!empty($value) && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) { // 2012-08-12 07:43:26 $value = new DateTime($value, new DateTimeZone('utc')); } $data[$this->to_camelcase($key, false)] = $value; } return new $this->class_name($data); } /** * Converts model object into array */ protected function object_to_array($object) { - $data = array(); + $data = []; foreach ($object as $key => $value) { if ($value instanceof DateTime) { $value = $value->format('Y-m-d H:i:s'); } elseif (is_object($value) && isset($value->id)) { $value = $value->id; } $data[$this->from_camelcase($key)] = $value; } return $data; } /** * Convert property name from camel-case to lower-case-with-underscore */ protected function from_camelcase($string) { $string = lcfirst($string); return preg_replace_callback('/([A-Z])/', function ($string) { return '_' . strtolower($string[0]); }, $string); } /** * Convert property name from lower-case-with-underscore to camel-case */ protected function to_camelcase($string, $ucFirst = true) { if ($ucFirst) { $string = ucfirst($string); } return preg_replace_callback('/_([a-z])/', function ($string) { return strtoupper($string[1]); }, $string); } } diff --git a/lib/kolab_sync_backend_content.php b/lib/kolab_sync_backend_content.php index 0944613..cebe133 100644 --- a/lib/kolab_sync_backend_content.php +++ b/lib/kolab_sync_backend_content.php @@ -1,137 +1,137 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for content storage */ class kolab_sync_backend_content extends kolab_sync_backend_common implements Syncroton_Backend_IContent { protected $table_name = 'syncroton_content'; protected $interface_name = 'Syncroton_Model_IContent'; /** * mark state as deleted. The state gets removed finally, * when the synckey gets validated during next sync. * * @param Syncroton_Model_IContent|string $id */ public function delete($id) { $id = $id instanceof Syncroton_Model_IContent ? $id->id : $id; - $result = $this->db->query("UPDATE `{$this->table_name}` SET `is_deleted` = 1 WHERE `id` = ?", array($id)); + $result = $this->db->query("UPDATE `{$this->table_name}` SET `is_deleted` = 1 WHERE `id` = ?", [$id]); if ($result = (bool) $this->db->affected_rows($result)) { unset($this->cache['content_folderstate']); } return $result; } /** * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId * @param string $_contentId * * @return Syncroton_Model_IContent */ public function getContentState($_deviceId, $_folderId, $_contentId) { $deviceId = $_deviceId instanceof Syncroton_Model_IDevice ? $_deviceId->id : $_deviceId; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($deviceId); $where[] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folderId); $where[] = $this->db->quote_identifier('contentid') . ' = ' . $this->db->quote($_contentId); $where[] = $this->db->quote_identifier('is_deleted') . ' = 0'; $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); $state = $this->db->fetch_assoc($select); if (empty($state)) { throw new Syncroton_Exception_NotFound('Content not found'); } return $this->get_object($state); } /** * get array of ids which got send to the client for a given class * * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId * @param int $syncKey * * @return array */ public function getFolderState($_deviceId, $_folderId, $syncKey = null) { $deviceId = $_deviceId instanceof Syncroton_Model_IDevice ? $_deviceId->id : $_deviceId; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; $cachekey = $deviceId . ':' . $folderId . ':' . ($syncKey ?: '*'); // in Sync request we call this function twice in case when // folder state changed - use cache to skip at least one SELECT query if (isset($this->cache['content_folderstate'][$cachekey])) { return $this->cache['content_folderstate'][$cachekey]; } $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($deviceId); $where[] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folderId); $where[] = $this->db->quote_identifier('is_deleted') . ' = 0'; if ($syncKey) { $where[] = $this->db->quote_identifier('creation_synckey') . ' < ' . $this->db->quote($syncKey + 1); } $select = $this->db->query("SELECT `contentid` FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); - $result = array(); + $result = []; while ($state = $this->db->fetch_assoc($select)) { $result[] = $state['contentid']; } return $this->cache['content_folderstate'][$cachekey] = $result; } /** * reset list of stored id * * @param Syncroton_Model_IDevice|string $_deviceId * @param Syncroton_Model_IFolder|string $_folderId */ public function resetState($_deviceId, $_folderId) { $deviceId = $_deviceId instanceof Syncroton_Model_IDevice ? $_deviceId->id : $_deviceId; $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->id : $_folderId; unset($this->cache['content_folderstate']); $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($deviceId); $where[] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folderId); $this->db->query("DELETE FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); } } diff --git a/lib/kolab_sync_backend_device.php b/lib/kolab_sync_backend_device.php index e3a93cc..9c4101c 100644 --- a/lib/kolab_sync_backend_device.php +++ b/lib/kolab_sync_backend_device.php @@ -1,323 +1,320 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for device storage */ class kolab_sync_backend_device extends kolab_sync_backend_common implements Syncroton_Backend_IDevice { protected $table_name = 'syncroton_device'; protected $interface_name = 'Syncroton_Model_IDevice'; /** * Kolab Sync storage backend * * @var kolab_sync_storage */ protected $backend; /** * Constructor */ public function __construct() { parent::__construct(); $this->backend = kolab_sync::storage(); } /** * Create (register) a new device * * @param Syncroton_Model_IDevice $device Device object * * @return Syncroton_Model_IDevice Device object */ public function create($device) { $device = parent::create($device); // Create device entry in kolab backend - $created = $this->backend->device_create(array( + $created = $this->backend->device_create([ 'ID' => $device->id, 'TYPE' => $device->devicetype, 'ALIAS' => $device->friendlyname, - ), $device->deviceid); + ], $device->deviceid); if (!$created) { throw new Syncroton_Exception_NotFound('Device creation failed'); } return $device; } /** * Delete a device * * @param Syncroton_Model_IDevice $device Device object * * @return bool True on success, False on failure */ public function delete($device) { // Update IMAP annotation $this->backend->device_delete($device->deviceid); return parent::delete($device); } /** * Return device for a given user * * @param string $ownerid User identifier * @param string $deviceid Device identifier * * @throws Syncroton_Exception_NotFound * @return Syncroton_Model_Device Device object */ public function getUserDevice($ownerid, $deviceid) { $where[] = $this->db->quote_identifier('deviceid') . ' = ' . $this->db->quote($deviceid); $where[] = $this->db->quote_identifier('owner_id') . ' = ' . $this->db->quote($ownerid); $select = $this->db->query('SELECT * FROM ' . $this->table_name . ' WHERE ' . implode(' AND ', $where)); $device = $this->db->fetch_assoc($select); if (empty($device)) { throw new Syncroton_Exception_NotFound('Device not found'); } $device = $this->get_object($device); // Make sure device exists (could be deleted by the user) $dev = $this->backend->device_get($deviceid); if (empty($dev)) { // Remove the device (and related cached data) from database $this->delete($device); throw new Syncroton_Exception_NotFound('Device not found'); } return $device; } /** * Returns list of user accounts * * @param Syncroton_Model_Device $device The device * * @return array List of Syncroton_Model_Account objects */ public function userAccounts($device) { $engine = kolab_sync::get_instance(); $identities = $engine->user->list_identities(); $email = $engine->get_user_email(); - $addresses = array(); + $addresses = []; $displayname = null; // read email addresses and display name (default ident comes first) foreach ((array)$identities as $ident) { if ($ident['name'] && !isset($displayname)) { $displayname = $ident['name']; } $addresses[] = $ident['email']; } if (empty($displayname) && empty($email) && empty($addresses)) { - return array(); + return []; } - $account = new Syncroton_Model_Account; + $account = new Syncroton_Model_Account(); if ($email) { - $addresses = array_diff($addresses, array($email)); + $addresses = array_diff($addresses, [$email]); } $account->userDisplayName = $displayname; $account->primaryAddress = $email; $account->addresses = array_unique($addresses); - return array($account); + return [$account]; } /** * Returns OOF information * * @param array $request Oof/Get request data * * @return Syncroton_Model_Oof|null Response object or NULL if OOF is not supported * @throws Syncroton_Exception_Status */ public function getOOF($request) { $vacation_engine = $this->vacation_engine(); if (!$vacation_engine) { return null; } $vacation = $vacation_engine->get_vacation(); if (!$vacation['enabled']) { $status = Syncroton_Model_Oof::STATUS_DISABLED; $vacation['start'] = $vacation['end'] = null; - } - else if ($vacation['start'] || $vacation['end']) { + } elseif ($vacation['start'] || $vacation['end']) { // in Activesync both or none time are required if (!$vacation['start'] && $vacation['end']) { $vacation['start'] = new DateTime('1970-01-01', new DateTimeZone('UTC')); } if (!$vacation['end'] && $vacation['start']) { $vacation['end'] = new DateTime('2100-01-01', new DateTimeZone('UTC')); } // convert timezone to UTC if ($vacation['start']) { $vacation['start']->setTimezone(new DateTimeZone('UTC')); } if ($vacation['end']) { $vacation['end']->setTimezone(new DateTimeZone('UTC')); } $status = Syncroton_Model_Oof::STATUS_TIME_BASED; - } - else { + } else { $status = Syncroton_Model_Oof::STATUS_GLOBAL; } $message = null; if ($vacation['message']) { - $message = array(); + $message = []; // convert message format, Roundcube supports plain text only if ($request['bodyType'] == 'HTML') { $text2html = new rcube_text2html($vacation['message']); $vacation['message'] = $text2html->get_html(); } - foreach (array('Internal', 'ExternalKnown', 'ExternalUnknown') as $type) { - $message[] = new Syncroton_Model_OofMessage(array( + foreach (['Internal', 'ExternalKnown', 'ExternalUnknown'] as $type) { + $message[] = new Syncroton_Model_OofMessage([ "appliesTo$type" => true, 'enabled' => 1, 'bodyType' => 'Text', 'replyMessage' => rcube_charset::clean($vacation['message']), - )); + ]); } } - return new Syncroton_Model_Oof(array( + return new Syncroton_Model_Oof([ 'oofState' => $status, 'startTime' => $vacation['start'], 'endTime' => $vacation['end'], 'oofMessage' => $message, - )); + ]); } /** * Sets OOF information * * @param Syncroton_Model_Oof $request Request object * * @throws Syncroton_Exception_Status */ public function setOOF($request) { $vacation_engine = $this->vacation_engine(); if (!$vacation_engine) { return; } $vacation = $vacation_engine->get_vacation(); // enable out-of-office if (!empty($request->oofState)) { if ($request->oofState == Syncroton_Model_Oof::STATUS_TIME_BASED) { $vacation['start'] = $request->startTime; $vacation['end'] = $request->endTime; if (empty($vacation['start']) || empty($vacation['end'])) { throw new Syncroton_Exception_Status_Settings(Syncroton_Exception_Status_Settings::INVALID_ARGUMENTS); } - } - else { + } else { $vacation['start'] = $vacation['end'] = null; } foreach ($request->oofMessage as $msg) { if ($msg->enabled && ($message = $msg->replyMessage)) { $message_type = $msg->bodyType; // convert message format, Roundcube supports plain text only if ($message_type == 'HTML') { $html2text = new rcube_html2text($message, false, true); $message = $html2text->get_text(); } break; } } if (empty($message)) { throw new Syncroton_Exception_Status_Settings(Syncroton_Exception_Status_Settings::INVALID_ARGUMENTS); } $vacation['message'] = $message; $vacation['subject'] = null; $vacation['enabled'] = true; $vacation_engine->set_vacation($vacation); } // disable out-of-office - else if (isset($request->oofState)) { + elseif (isset($request->oofState)) { if ($vacation['enabled']) { $vacation['enabled'] = false; $vacation_engine->set_vacation($vacation); } } } /** * Load managesieve plugin and return vacation engine class */ private function vacation_engine() { $engine = kolab_sync::get_instance(); $engine->plugins->load_plugin('managesieve', true, false); if (class_exists('managesieve')) { $plugin = $engine->plugins->get_plugin('managesieve'); $vacation = $plugin->get_engine('vacation'); // @phpstan-ignore-line if ($vacation->connect($engine->username, $engine->password)) { throw new Exception("Connection to managesieve server failed"); } return $vacation; } } } diff --git a/lib/kolab_sync_backend_folder.php b/lib/kolab_sync_backend_folder.php index d5a25de..7df726e 100644 --- a/lib/kolab_sync_backend_folder.php +++ b/lib/kolab_sync_backend_folder.php @@ -1,191 +1,190 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for the folder state storage */ class kolab_sync_backend_folder extends kolab_sync_backend_common implements Syncroton_Backend_IFolder { protected $table_name = 'syncroton_folder'; protected $interface_name = 'Syncroton_Model_IFolder'; /** * Delete all stored folder ids for a given device * * @param Syncroton_Model_Device|string $deviceid Device object or identifier */ public function resetState($deviceid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $this->db->query('DELETE FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); } /** * Get array of ids which got send to the client for a given class * * @param Syncroton_Model_Device|string $deviceid Device object or identifier * @param string $class Class name * * @return array List of object identifiers */ public function getFolderState($deviceid, $class) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where[] = $this->db->quote_identifier('class') . ' = ' . $this->db->quote($class); + $where[] = $this->db->quote_identifier('class') . ' = ' . $this->db->quote($class); - $select = $this->db->query('SELECT * FROM `' . $this->table_name .'` WHERE ' . implode(' AND ', $where)); - $result = array(); + $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); + $result = []; while ($folder = $this->db->fetch_assoc($select)) { $result[$folder['folderid']] = $this->get_object($folder); } return $result; } /** * Get folder * * @param Syncroton_Model_Device|string $deviceid Device object or identifier * @param string $folderid Folder identifier * * @return Syncroton_Model_IFolder Folder object */ public function getFolder($deviceid, $folderid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where[] = $this->db->quote_identifier('folderid') . ' = ' . $this->db->quote($folderid); + $where[] = $this->db->quote_identifier('folderid') . ' = ' . $this->db->quote($folderid); $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); $folder = $this->db->fetch_assoc($select); if (empty($folder)) { throw new Syncroton_Exception_NotFound('Folder not found'); } return $this->get_object($folder); } /** * Find out if the folder hierarchy changed since the last FolderSync * * @param Syncroton_Model_Device $device Device object * * @return bool True if folders hierarchy changed, False otherwise */ public function hasHierarchyChanges($device) { $timestamp = new DateTime('now', new DateTimeZone('utc')); $client_crc = ''; $server_crc = ''; - $client_folders = array(); - $server_folders = array(); - $folder_classes = array( + $client_folders = []; + $server_folders = []; + $folder_classes = [ Syncroton_Data_Factory::CLASS_CALENDAR, Syncroton_Data_Factory::CLASS_CONTACTS, Syncroton_Data_Factory::CLASS_EMAIL, Syncroton_Data_Factory::CLASS_NOTES, - Syncroton_Data_Factory::CLASS_TASKS - ); + Syncroton_Data_Factory::CLASS_TASKS, + ]; // Reset imap cache so we work with up-to-date folders list rcube::get_instance()->get_storage()->clear_cache('mailboxes', true); foreach ($folder_classes as $class) { try { // retrieve all folders available in data backend $dataController = Syncroton_Data_Factory::factory($class, $device, $timestamp); $server_folders = array_merge($server_folders, $dataController->getAllFolders()); - } - catch (Exception $e) { + } catch (Exception $e) { rcube::raise_error($e, true, false); // This is server error, returning True might cause infinite sync loops return false; } } // retrieve all folders sent to the client $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE `device_id` = ?", $device->id); while ($folder = $this->db->fetch_assoc($select)) { $client_folders[$folder['folderid']] = $this->get_object($folder); } ksort($client_folders); ksort($server_folders); foreach ($client_folders as $folder) { $client_crc .= '^' . $folder->serverId . ':' . $folder->displayName . ':' . $folder->parentId; } foreach ($server_folders as $folder) { $server_crc .= '^' . $folder->serverId . ':' . $folder->displayName . ':' . $folder->parentId; } return $client_crc !== $server_crc; } /** * (non-PHPdoc) * @see kolab_sync_backend_common::from_camelcase() */ protected function from_camelcase($string) { switch ($string) { case 'displayName': case 'parentId': return strtolower($string); case 'serverId': return 'folderid'; default: return parent::from_camelcase($string); } } /** * (non-PHPdoc) * @see kolab_sync_backend_common::to_camelcase() */ protected function to_camelcase($string, $ucFirst = true) { switch ($string) { case 'displayname': return 'displayName'; case 'parentid': return 'parentId'; case 'folderid': return 'serverId'; default: return parent::to_camelcase($string, $ucFirst); } } } diff --git a/lib/kolab_sync_backend_policy.php b/lib/kolab_sync_backend_policy.php index d7b8f09..40b97ab 100644 --- a/lib/kolab_sync_backend_policy.php +++ b/lib/kolab_sync_backend_policy.php @@ -1,33 +1,33 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * sql backend class for policy storage */ class kolab_sync_backend_policy extends kolab_sync_backend_common //implements Syncroton_Backend_IPolicy { protected $table_name = 'syncroton_policy'; protected $interface_name = 'Syncroton_Model_IPolicy'; } diff --git a/lib/kolab_sync_backend_state.php b/lib/kolab_sync_backend_state.php index 6130529..96da005 100644 --- a/lib/kolab_sync_backend_state.php +++ b/lib/kolab_sync_backend_state.php @@ -1,228 +1,227 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for the folder state storage */ class kolab_sync_backend_state extends kolab_sync_backend_common implements Syncroton_Backend_ISyncState { protected $table_name = 'syncroton_synckey'; protected $interface_name = 'Syncroton_Model_ISyncState'; /** * Create new sync state of a folder * * @param Syncroton_Model_ISyncState $object State object * @param bool $keep_previous_state Don't remove other states * * @return Syncroton_Model_SyncState */ public function create($object, $keep_previous_state = true) { $object = parent::create($object); if ($keep_previous_state !== true) { // remove all other synckeys $this->_deleteOtherStates($object); } return $object; } /** * Deletes states other than specified one */ protected function _deleteOtherStates(Syncroton_Model_ISyncState $state) { // remove all other synckeys - $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($state->deviceId); - $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($state->type); - $where[] = $this->db->quote_identifier('counter') . ' <> ' . $this->db->quote($state->counter); + $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($state->deviceId); + $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($state->type); + $where[] = $this->db->quote_identifier('counter') . ' <> ' . $this->db->quote($state->counter); $this->db->query("DELETE FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); } /** * @see kolab_sync_backend_common::object_to_array() */ protected function object_to_array($object) { $data = parent::object_to_array($object); if (is_array($object->pendingdata)) { $data['pendingdata'] = gzdeflate(json_encode($object->pendingdata)); } return $data; } /** * @see kolab_sync_backend_common::get_object() */ protected function get_object($data) { $object = parent::get_object($data); if ($object->pendingdata) { $inflated = gzinflate($object->pendingdata); // Inflation may fail for backward compatiblity $data = $inflated ? $inflated : $object->pendingdata; $object->pendingdata = json_decode($data, true); } return $object; } /** * Returns the latest sync state * * @param Syncroton_Model_IDevice|string $deviceid Device object or identifier * @param Syncroton_Model_IFolder|string $folderid Folder object or identifier * * @return Syncroton_Model_SyncState */ public function getSyncState($deviceid, $folderid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); + $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $select = $this->db->limitquery("SELECT * FROM `{$this->table_name}` WHERE " . implode(' AND ', $where) . " ORDER BY `counter` DESC", 0, 1); $state = $this->db->fetch_assoc($select); if (empty($state)) { throw new Syncroton_Exception_NotFound('SyncState not found'); } return $this->get_object($state); } /** * Delete all stored synckeys of given type * * @param Syncroton_Model_IDevice|string $deviceid Device object or identifier * @param Syncroton_Model_IFolder|string $folderid Folder object or identifier */ public function resetState($deviceid, $folderid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); + $where[] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $this->db->query("DELETE FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); } /** * Validates specified sync state by checking for existance of newer keys * * @param Syncroton_Model_IDevice|string $deviceid Device object or identifier * @param Syncroton_Model_IFolder|string $folderid Folder object or identifier * @param int $sync_key State key * * @return Syncroton_Model_SyncState|false */ public function validate($deviceid, $folderid, $sync_key) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; - $states = array(); + $states = []; // get sync data // we'll get all records, thanks to this we'll be able to // skip _deleteOtherStates() call below (one DELETE query less) $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where['type'] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); + $where['type'] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); while ($row = $this->db->fetch_assoc($select)) { $states[$row['counter']] = $this->get_object($row); } // last state not found if (empty($states) || empty($states[$sync_key])) { return false; } $state = $states[$sync_key]; $next = max(array_keys($states)); - $where = array(); - $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where['folder_id'] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folder_id); + $where = []; + $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); + $where['folder_id'] = $this->db->quote_identifier('folder_id') . ' = ' . $this->db->quote($folder_id); $where['is_deleted'] = $this->db->quote_identifier('is_deleted') . ' = 1'; // found more recent synckey => the last sync response was not received by the client if ($next > $sync_key) { // We store the clientIdMap with the "next" sync state, so we need to copy it back. $state->clientIdMap = $states[$next]->clientIdMap; - } - else { + } else { // finally delete all entries marked for removal in syncroton_content table $retryCounter = 0; while (true) { $result = $this->db->query("DELETE FROM `syncroton_content` WHERE " . implode(' AND ', $where)); if ($this->db->is_error($result)) { $retryCounter++; // Retry on deadlock if ($this->db->error_info()[0] != '40001' || $retryCounter > 60) { throw new Exception('Failed to delete entries in sync_key check'); } } else { break; } //Give the other transactions some time before we try again sleep(1); } } // remove all other synckeys if (count($states) > 1) { $this->_deleteOtherStates($state); } return $state; } public function haveNext($deviceid, $folderid, $sync_key) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $folder_id = $folderid instanceof Syncroton_Model_IFolder ? $folderid->id : $folderid; $where['device_id'] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); - $where['type'] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); - $where['counter'] = $this->db->quote_identifier('counter') . ' > ' . $this->db->quote($sync_key); + $where['type'] = $this->db->quote_identifier('type') . ' = ' . $this->db->quote($folder_id); + $where['counter'] = $this->db->quote_identifier('counter') . ' > ' . $this->db->quote($sync_key); $select = $this->db->query("SELECT id FROM `{$this->table_name}` WHERE " . implode(' AND ', $where)); return $this->db->num_rows($select) > 0; } } diff --git a/lib/kolab_sync_body_converter.php b/lib/kolab_sync_body_converter.php index b7545c5..c8e1bef 100644 --- a/lib/kolab_sync_body_converter.php +++ b/lib/kolab_sync_body_converter.php @@ -1,157 +1,157 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Utility class for data convertion between ActiveSync Body formats */ class kolab_sync_body_converter { protected $text; protected $type; /** * Constructor * * @param string $data Data string * @param int $type Data type. One of Syncroton_Model_EmailBody constants. */ public function __construct($data, $type) { $this->text = $data; $this->type = $type; } /** * Converter * * @param int $type Result data type (to which the string will be converted). * One of Syncroton_Model_EmailBody constants. * * @return string Body value */ public function convert($type) { if (empty($this->text) || empty($type) || $type == $this->type) { return $this->text; } // ActiveSync types: TYPE_PLAINTEXT, TYPE_HTML, TYPE_RTF, TYPE_MIME switch ($this->type) { - case Syncroton_Model_EmailBody::TYPE_PLAINTEXT: - return $this->convert_plain($type); - case Syncroton_Model_EmailBody::TYPE_HTML: - return $this->convert_html($type); - case Syncroton_Model_EmailBody::TYPE_RTF: - return $this->convert_rtf($type); - default: - return $this->text; + case Syncroton_Model_EmailBody::TYPE_PLAINTEXT: + return $this->convert_plain($type); + case Syncroton_Model_EmailBody::TYPE_HTML: + return $this->convert_html($type); + case Syncroton_Model_EmailBody::TYPE_RTF: + return $this->convert_rtf($type); + default: + return $this->text; } } /** * Text/plain converter * * @param int $type Result data type (to which the string will be converted). * One of Syncroton_Model_EmailBody constants. * * @return string Body value */ protected function convert_plain($type) { $data = $this->text; switch ($type) { - case Syncroton_Model_EmailBody::TYPE_HTML: - return '
' . htmlspecialchars($data, ENT_COMPAT, kolab_sync::CHARSET) . '
'; - case Syncroton_Model_EmailBody::TYPE_RTF: - // @TODO - return ''; + case Syncroton_Model_EmailBody::TYPE_HTML: + return '
' . htmlspecialchars($data, ENT_COMPAT, kolab_sync::CHARSET) . '
'; + case Syncroton_Model_EmailBody::TYPE_RTF: + // @TODO + return ''; } return $data; } /** * HTML converter * * @param int $type Result data type (to which the string will be converted). * One of Syncroton_Model_EmailBody constants. * * @return string Body value */ protected function convert_html($type) { switch ($type) { - case Syncroton_Model_EmailBody::TYPE_PLAINTEXT: - $txt = new rcube_html2text($this->text, false, true); - return $txt->get_text(); - case Syncroton_Model_EmailBody::TYPE_RTF: - // @TODO - return ''; - case Syncroton_Model_EmailBody::TYPE_MIME: - return ''; + case Syncroton_Model_EmailBody::TYPE_PLAINTEXT: + $txt = new rcube_html2text($this->text, false, true); + return $txt->get_text(); + case Syncroton_Model_EmailBody::TYPE_RTF: + // @TODO + return ''; + case Syncroton_Model_EmailBody::TYPE_MIME: + return ''; } return $this->text; } /** * RTF converter * * @param int $type Result data type (to which the string will be converted). * One of Syncroton_Model_EmailBody constants. * * @return string Body value */ protected function convert_rtf($type) { switch ($type) { - case Syncroton_Model_EmailBody::TYPE_PLAINTEXT: - try { - $document = new RtfHtmlPhp\Document($this->text); - $formatter = new RtfHtmlPhp\Html\HtmlFormatter(RCUBE_CHARSET); - $txt = new rcube_html2text($formatter->format($document), false, true); - return $txt->get_text(); - } catch (Exception $e) { - $logger = Syncroton_Registry::get('loggerBackend'); - $logger->warn("Failed to convert RTF content"); - return ''; - } - case Syncroton_Model_EmailBody::TYPE_HTML: - try { - $document = new RtfHtmlPhp\Document($this->text); - $formatter = new RtfHtmlPhp\Html\HtmlFormatter(RCUBE_CHARSET); - return $formatter->format($document); - } catch (Exception $e) { - $logger = Syncroton_Registry::get('loggerBackend'); - $logger->warn("Failed to convert RTF content"); - return ''; - } + case Syncroton_Model_EmailBody::TYPE_PLAINTEXT: + try { + $document = new RtfHtmlPhp\Document($this->text); + $formatter = new RtfHtmlPhp\Html\HtmlFormatter(RCUBE_CHARSET); + $txt = new rcube_html2text($formatter->format($document), false, true); + return $txt->get_text(); + } catch (Exception $e) { + $logger = Syncroton_Registry::get('loggerBackend'); + $logger->warn("Failed to convert RTF content"); + return ''; + } + case Syncroton_Model_EmailBody::TYPE_HTML: + try { + $document = new RtfHtmlPhp\Document($this->text); + $formatter = new RtfHtmlPhp\Html\HtmlFormatter(RCUBE_CHARSET); + return $formatter->format($document); + } catch (Exception $e) { + $logger = Syncroton_Registry::get('loggerBackend'); + $logger->warn("Failed to convert RTF content"); + return ''; + } } return $this->text; } } diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php index b507568..199702b 100644 --- a/lib/kolab_sync_data.php +++ b/lib/kolab_sync_data.php @@ -1,1579 +1,1567 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Base class for Syncroton data backends */ abstract class kolab_sync_data implements Syncroton_Data_IData { /** * ActiveSync protocol version * * @var float */ protected $asversion = 0; /** * The storage backend * * @var kolab_sync_storage */ protected $backend; /** * information about the current device * * @var Syncroton_Model_IDevice */ protected $device; /** * timestamp to use for all sync requests * * @var DateTime */ protected $syncTimeStamp; /** * name of model to use * * @var string */ protected $modelName; /** * type of the default folder * * @var int */ protected $defaultFolderType; /** * default container for new entries * * @var string */ protected $defaultFolder; /** * default root folder * * @var string */ protected $defaultRootFolder; /** * type of user created folders * * @var int */ protected $folderType; /** * Internal cache for storage folders list * * @var array */ protected $folders = []; - /** - * Logger instance. - * - * @var kolab_sync_logger - */ + /** + * Logger instance. + * + * @var kolab_sync_logger + */ protected $logger; /** * Timezone * * @var string */ protected $timezone; /** * List of device types with multiple folders support * * @var array */ - protected $ext_devices = array( + protected $ext_devices = [ 'iphone', 'ipad', 'thundertine', 'windowsphone', 'wp', 'wp8', 'playbook', - ); + ]; protected $lastsync_folder = null; protected $lastsync_time = null; - const RESULT_OBJECT = 0; - const RESULT_UID = 1; - const RESULT_COUNT = 2; + public const RESULT_OBJECT = 0; + public const RESULT_UID = 1; + public const RESULT_COUNT = 2; /** * Recurrence types */ - const RECUR_TYPE_DAILY = 0; // Recurs daily. - const RECUR_TYPE_WEEKLY = 1; // Recurs weekly - const RECUR_TYPE_MONTHLY = 2; // Recurs monthly - const RECUR_TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day - const RECUR_TYPE_YEARLY = 5; // Recurs yearly - const RECUR_TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day + public const RECUR_TYPE_DAILY = 0; // Recurs daily. + public const RECUR_TYPE_WEEKLY = 1; // Recurs weekly + public const RECUR_TYPE_MONTHLY = 2; // Recurs monthly + public const RECUR_TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day + public const RECUR_TYPE_YEARLY = 5; // Recurs yearly + public const RECUR_TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day /** * Day of week constants */ - const RECUR_DOW_SUNDAY = 1; - const RECUR_DOW_MONDAY = 2; - const RECUR_DOW_TUESDAY = 4; - const RECUR_DOW_WEDNESDAY = 8; - const RECUR_DOW_THURSDAY = 16; - const RECUR_DOW_FRIDAY = 32; - const RECUR_DOW_SATURDAY = 64; - const RECUR_DOW_LAST = 127; // The last day of the month. Used as a special value in monthly or yearly recurrences. + public const RECUR_DOW_SUNDAY = 1; + public const RECUR_DOW_MONDAY = 2; + public const RECUR_DOW_TUESDAY = 4; + public const RECUR_DOW_WEDNESDAY = 8; + public const RECUR_DOW_THURSDAY = 16; + public const RECUR_DOW_FRIDAY = 32; + public const RECUR_DOW_SATURDAY = 64; + public const RECUR_DOW_LAST = 127; // The last day of the month. Used as a special value in monthly or yearly recurrences. /** * Mapping of recurrence types * * @var array */ - protected $recurTypeMap = array( + protected $recurTypeMap = [ self::RECUR_TYPE_DAILY => 'DAILY', self::RECUR_TYPE_WEEKLY => 'WEEKLY', self::RECUR_TYPE_MONTHLY => 'MONTHLY', self::RECUR_TYPE_MONTHLY_DAYN => 'MONTHLY', self::RECUR_TYPE_YEARLY => 'YEARLY', self::RECUR_TYPE_YEARLY_DAYN => 'YEARLY', - ); + ]; /** * Mapping of weekdays * NOTE: ActiveSync uses a bitmask * * @var array */ - protected $recurDayMap = array( + protected $recurDayMap = [ 'SU' => self::RECUR_DOW_SUNDAY, 'MO' => self::RECUR_DOW_MONDAY, 'TU' => self::RECUR_DOW_TUESDAY, 'WE' => self::RECUR_DOW_WEDNESDAY, 'TH' => self::RECUR_DOW_THURSDAY, 'FR' => self::RECUR_DOW_FRIDAY, 'SA' => self::RECUR_DOW_SATURDAY, - ); + ]; /** * the constructor * * @param Syncroton_Model_IDevice $device * @param DateTime $syncTimeStamp */ public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp) { $this->backend = kolab_sync::storage(); $this->device = $device; $this->asversion = floatval($device->acsversion); $this->syncTimeStamp = $this->backend->syncTimeStamp = $syncTimeStamp; $this->logger = Syncroton_Registry::get(Syncroton_Registry::LOGGERBACKEND); $this->defaultRootFolder = $this->defaultFolder . '::Syncroton'; // set internal timezone of kolab_format to user timezone try { $this->timezone = rcube::get_instance()->config->get('timezone', 'GMT'); kolab_format::$timezone = new DateTimeZone($this->timezone); - } - catch (Exception $e) { + } catch (Exception $e) { //rcube::raise_error($e, true); $this->timezone = 'GMT'; kolab_format::$timezone = new DateTimeZone('GMT'); } } /** * return list of supported folders for this backend * * @return array */ public function getAllFolders() { - $list = array(); + $list = []; // device supports multiple folders ? if ($this->isMultiFolder()) { // get the folders the user has access to $list = $this->listFolders(); - } - else if ($default = $this->getDefaultFolder()) { - $list = array($default['serverId'] => $default); + } elseif ($default = $this->getDefaultFolder()) { + $list = [$default['serverId'] => $default]; } // getAllFolders() is called only in FolderSync // throw Syncroton_Exception_Status_FolderSync exception if (!is_array($list)) { throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR); } foreach ($list as $idx => $folder) { $list[$idx] = new Syncroton_Model_Folder($folder); } return $list; } /** * Retrieve folders which were modified since last sync * * @param DateTime $startTimeStamp * @param DateTime $endTimeStamp * * @return array List of folders */ public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp) { // FIXME/TODO: Can we get mtime of a DAV folder? // Without this, we have a problem if folder ID does not change on rename - return array(); + return []; } /** * Returns true if the device supports multiple folders or it was configured so */ protected function isMultiFolder() { $config = rcube::get_instance()->config; $blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName); if (!is_array($blacklist)) { $blacklist = $config->get('activesync_multifolder_blacklist'); } if (is_array($blacklist)) { return !$this->deviceTypeFilter($blacklist); } return in_array_nocase($this->device->devicetype, $this->ext_devices); } /** * Returns default folder for current class type. */ protected function getDefaultFolder() { // Check if there's any folder configured for sync $folders = $this->listFolders(); if (empty($folders)) { return $folders; } foreach ($folders as $folder) { if ($folder['type'] == $this->defaultFolderType) { $default = $folder; break; } } // Return first on the list if there's no default if (empty($default)) { $default = array_first($folders); // make sure the type is default here $default['type'] = $this->defaultFolderType; } // Remember real folder ID and set ID/name to root folder $default['realid'] = $default['serverId']; $default['serverId'] = $this->defaultRootFolder; $default['displayName'] = $this->defaultFolder; return $default; } /** * Creates a folder */ public function createFolder(Syncroton_Model_IFolder $folder) { $result = $this->backend->folder_create($folder->displayName, $folder->type, $this->device->deviceid, $folder->parentId); if ($result) { $folder->serverId = $result; return $folder; } // Note: Looks like Outlook 2013 ignores any errors on FolderCreate command throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::UNKNOWN_ERROR); } /** * Updates a folder */ public function updateFolder(Syncroton_Model_IFolder $folder) { $result = $this->backend->folder_rename($folder->serverId, $this->device->deviceid, $folder->displayName, $folder->parentId); if ($result) { return $folder; } // @TODO: throw exception } /** * Deletes a folder */ public function deleteFolder($folder) { if ($folder instanceof Syncroton_Model_IFolder) { $folder = $folder->serverId; } // @TODO: throw exception return $this->backend->folder_delete($folder, $this->device->deviceid); } /** * Empty folder (remove all entries and optionally subfolders) * * @param string $folderid Folder identifier * @param array $options Options */ public function emptyFolderContents($folderid, $options) { // ActiveSync spec.: Clients use EmptyFolderContents to empty the Deleted Items folder. // The client can clear out all items in the Deleted Items folder when the user runs out of storage quota // (indicated by the return of an MailboxQuotaExceeded (113) status code from the server. // FIXME: Does that mean we don't need this to work on any other folder? // TODO: Respond with MailboxQuotaExceeded status. Where exactly? foreach ($this->extractFolders($folderid) as $folderid) { if (!$this->backend->folder_empty($folderid, $this->device->deviceid, !empty($options['deleteSubFolders']))) { throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR); } } } /** * Moves object into another location (folder) * * @param string $srcFolderId Source folder identifier * @param string $serverId Object identifier * @param string $dstFolderId Destination folder identifier * * @throws Syncroton_Exception_Status * @return string New object identifier */ public function moveItem($srcFolderId, $serverId, $dstFolderId) { // TODO: Optimize, we just need to find the folder ID and UID, we do not need to "fetch" it. $item = $this->getObject($srcFolderId, $serverId); if (!$item) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } $uid = $this->backend->moveItem($item['folderId'], $this->device->deviceid, $this->modelName, $item['uid'], $dstFolderId); return $this->serverId($uid, $dstFolderId); } /** * Add entry * * @param string $folderId Folder identifier * @param Syncroton_Model_IEntry $entry Entry object * * @return string ID of the created entry */ public function createEntry($folderId, Syncroton_Model_IEntry $entry) { $entry = $this->toKolab($entry, $folderId); if ($folderId == $this->defaultRootFolder) { $default = $this->getDefaultFolder(); if (!is_array($default)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } - $folderId = isset($default['realid']) ? $default['realid'] : $default['serverId']; + $folderId = $default['realid'] ?? $default['serverId']; } $uid = $this->backend->createItem($folderId, $this->device->deviceid, $this->modelName, $entry); if (empty($uid)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } return $this->serverId($uid, $folderId); } /** * update existing entry * * @param string $folderId * @param string $serverId * @param Syncroton_Model_IEntry $entry * * @return string ID of the updated entry */ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) { $oldEntry = $this->getObject($folderId, $serverId); if (empty($oldEntry)) { throw new Syncroton_Exception_NotFound('entry not found'); } $entry = $this->toKolab($entry, $folderId, $oldEntry); $uid = $this->backend->updateItem($oldEntry['folderId'], $this->device->deviceid, $this->modelName, $oldEntry['uid'], $entry); if (empty($uid)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } return $this->serverId($uid, $oldEntry['folderId']); } /** * Delete entry * * @param string $folderId * @param string $serverId * @param ?Syncroton_Model_SyncCollection $collectionData */ public function deleteEntry($folderId, $serverId, $collectionData = null) { // TODO: Optimize, we just need to find the folder ID and UID, we do not need to "fetch" it. $object = $this->getObject($folderId, $serverId); if ($object) { $deleted = $this->backend->deleteItem($object['folderId'], $this->device->deviceid, $this->modelName, $object['uid']); if (!$deleted) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } } } /** * Get attachment data from the server. * * @param string $fileReference * * @return Syncroton_Model_FileReference */ public function getFileReference($fileReference) { // to be implemented by Email data class throw new Syncroton_Exception_NotFound('File references not supported'); } /** * Search for existing entries * * @param string $folderid Folder identifier * @param array $filter Search filter * @param int $result_type Type of the result (see RESULT_* constants) * * @return array|int Search result as count or array of uids/objects */ - protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID) + protected function searchEntries($folderid, $filter = [], $result_type = self::RESULT_UID) { $result = $result_type == self::RESULT_COUNT ? 0 : []; $ts = time(); $force = $this->lastsync_folder != $folderid || $this->lastsync_time <= $ts - Syncroton_Registry::getPingTimeout(); $found = false; foreach ($this->extractFolders($folderid) as $fid) { $search = $this->backend->searchEntries($fid, $this->device->deviceid, $this->modelName, $filter, $result_type, $force); $found = true; switch ($result_type) { - case self::RESULT_COUNT: - $result += $search; - break; + case self::RESULT_COUNT: + $result += $search; + break; - case self::RESULT_UID: - foreach ($search as $idx => $uid) { - $search[$idx] = $this->serverId($uid, $fid); - } + case self::RESULT_UID: + foreach ($search as $idx => $uid) { + $search[$idx] = $this->serverId($uid, $fid); + } - $result = array_unique(array_merge($result, $search)); - break; + $result = array_unique(array_merge($result, $search)); + break; } } if (!$found) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $this->lastsync_folder = $folderid; $this->lastsync_time = $ts; return $result; } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { // overwrite by child class according to specified type - return array(); + return []; } /** * get all entries changed between two dates * * @param string $folderId * @param DateTime $start * @param DateTime $end * @param int $filter_type * * @return array */ public function getChangedEntries($folderId, DateTime $start, DateTime $end = null, $filter_type = null) { $filter = $this->filter($filter_type); - $filter[] = array('changed', '>', $start); + $filter[] = ['changed', '>', $start]; if ($end) { - $filter[] = array('changed', '<=', $end); + $filter[] = ['changed', '<=', $end]; } return $this->searchEntries($folderId, $filter, self::RESULT_UID); } /** * Get count of entries changed between two dates * * @param string $folderId * @param DateTime $start * @param DateTime $end * @param int $filter_type * * @return int */ public function getChangedEntriesCount($folderId, DateTime $start, DateTime $end = null, $filter_type = null) { $filter = $this->filter($filter_type); - $filter[] = array('changed', '>', $start); + $filter[] = ['changed', '>', $start]; if ($end) { - $filter[] = array('changed', '<=', $end); + $filter[] = ['changed', '<=', $end]; } return $this->searchEntries($folderId, $filter, self::RESULT_COUNT); } /** * get id's of all entries available on the server * * @param string $folder_id * @param string $filter_type * * @return array */ public function getServerEntries($folder_id, $filter_type) { $filter = $this->filter($filter_type); $result = $this->searchEntries($folder_id, $filter, self::RESULT_UID); return $result; } /** * get count of all entries available on the server * * @param string $folder_id * @param string $filter_type * * @return int */ public function getServerEntriesCount($folder_id, $filter_type) { $filter = $this->filter($filter_type); $result = $this->searchEntries($folder_id, $filter, self::RESULT_COUNT); return $result; } /** * Returns number of changed objects in the backend folder * * @param Syncroton_Backend_IContent $contentBackend * @param Syncroton_Model_IFolder $folder * @param Syncroton_Model_ISyncState $syncState * * @return int */ public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { // @phpstan-ignore-next-line $allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter); $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype); $changedEntries = $this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype); $addedEntries = array_diff($allServerEntries, $allClientEntries); $deletedEntries = array_diff($allClientEntries, $allServerEntries); return count($addedEntries) + count($deletedEntries) + $changedEntries; } /** * Returns true if any data got modified in the backend folder * * @param Syncroton_Backend_IContent $contentBackend * @param Syncroton_Model_IFolder $folder * @param Syncroton_Model_ISyncState $syncState * * @return bool */ public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { try { if ($this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype)) { return true; } // @phpstan-ignore-next-line $allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter); // @TODO: Consider looping over all folders here, not in getServerEntries() and // getChangedEntriesCount(). This way we could break the loop and not check all folders // or at least skip redundant cache sync of the same folder $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype); $addedEntries = array_diff($allServerEntries, $allClientEntries); $deletedEntries = array_diff($allClientEntries, $allServerEntries); return count($addedEntries) > 0 || count($deletedEntries) > 0; - } - catch (Exception $e) { + } catch (Exception $e) { // return "no changes" if something failed return false; } } /** * Fetches the entry from the backend */ protected function getObject($folderid, $entryid) { foreach ($this->extractFolders($folderid) as $fid) { $crc = null; $uid = $entryid; // See self::serverId() for full explanation // Use (slower) UID prefix matching... if (preg_match('/^CRC([0-9A-Fa-f]{8})(.+)$/', $uid, $matches)) { $crc = $matches[1]; $uid = $matches[2]; if (strlen($entryid) >= 64) { $objects = $this->backend->getItemsByUidPrefix($fid, $this->device->deviceid, $this->modelName, $uid); foreach ($objects as $object) { if (($object['uid'] === $uid || strpos($object['uid'], $uid) === 0) && $crc == $this->objectCRC($object['uid'], $fid) ) { $object['folderId'] = $fid; return $object; } } continue; } } // Or (faster) strict UID matching... $object = $this->backend->getItem($fid, $this->device->deviceid, $this->modelName, $uid); if (!empty($object) && ($crc === null || $crc == $this->objectCRC($object['uid'], $fid))) { $object['folderId'] = $fid; return $object; } } } /** * Returns internal folder IDs * * @param string $folderid Folder identifier * * @return array List of folder identifiers */ protected function extractFolders($folderid) { if ($folderid instanceof Syncroton_Model_IFolder) { $folderid = $folderid->serverId; } if ($folderid === $this->defaultRootFolder) { $folders = $this->listFolders(); if (!is_array($folders)) { throw new Syncroton_Exception_NotFound('Folder not found'); } $folders = array_keys($folders); - } - else { - $folders = array($folderid); + } else { + $folders = [$folderid]; } return $folders; } /** * List of all IMAP folders (or subtree) * * @param string $parentid Parent folder identifier * * @return array List of folder identifiers */ protected function listFolders($parentid = null) { if (empty($this->folders)) { $this->folders = $this->backend->folders_list( - $this->device->deviceid, $this->modelName, $this->isMultiFolder()); + $this->device->deviceid, + $this->modelName, + $this->isMultiFolder() + ); } if ($parentid === null || !is_array($this->folders)) { return $this->folders; } $folders = []; $parents = [$parentid]; foreach ($this->folders as $folder_id => $folder) { if ($folder['parentId'] && in_array($folder['parentId'], $parents)) { $folders[$folder_id] = $folder; $parents[] = $folder_id; } } return $folders; } /** * Returns ActiveSync settings of specified folder * * @param string $folderid Folder identifier * * @return array Folder settings */ protected function getFolderConfig($folderid) { if ($folderid == $this->defaultRootFolder) { $default = $this->getDefaultFolder(); if (!is_array($default)) { return []; } - $folderid = isset($default['realid']) ? $default['realid'] : $default['serverId']; + $folderid = $default['realid'] ?? $default['serverId']; } return $this->backend->getFolderConfig($folderid, $this->device->deviceid, $this->modelName); } /** * Convert contact from xml to kolab format * * @param mixed $data Contact data * @param string $folderId Folder identifier * @param array $entry Old Contact data for merge * * @return array */ - abstract function toKolab($data, $folderId, $entry = null); + abstract public function toKolab($data, $folderId, $entry = null); /** * Extracts data from kolab data array */ protected function getKolabDataItem($data, $name) { $name_items = explode('.', $name); $count = count($name_items); // multi-level array (e.g. address, phone) if ($count == 3) { $name = $name_items[0]; $type = $name_items[1]; $key_name = $name_items[2]; if (!empty($data[$name]) && is_array($data[$name])) { foreach ($data[$name] as $element) { if ($element['type'] == $type) { return $element[$key_name]; } } } return null; } // custom properties if ($count == 2 && $name_items[0] == 'x-custom') { $value = null; if (!empty($data['x-custom']) && is_array($data['x-custom'])) { foreach ($data['x-custom'] as $val) { if (is_array($val) && $val[0] == $name_items[1]) { $value = $val[1]; break; } } } return $value; } $name_items = explode(':', $name); $name = $name_items[0]; if (empty($data[$name])) { return null; } // simple array (e.g. email) if (count($name_items) == 2) { return $data[$name][$name_items[1]]; } return $data[$name]; } /** * Saves data in kolab data array */ protected function setKolabDataItem(&$data, $name, $value) { if (empty($value)) { return $this->unsetKolabDataItem($data, $name); } $name_items = explode('.', $name); $count = count($name_items); // multi-level array (e.g. address, phone) if ($count == 3) { $name = $name_items[0]; $type = $name_items[1]; $key_name = $name_items[2]; if (!isset($data[$name])) { - $data[$name] = array(); + $data[$name] = []; } foreach ($data[$name] as $idx => $element) { if ($element['type'] == $type) { $found = $idx; break; } } if (!isset($found)) { $data[$name] = array_values($data[$name]); $found = count($data[$name]); - $data[$name][$found] = array('type' => $type); + $data[$name][$found] = ['type' => $type]; } $data[$name][$found][$key_name] = $value; return; } // custom properties if ($count == 2 && $name_items[0] == 'x-custom') { - $data['x-custom'] = isset($data['x-custom']) ? ((array) $data['x-custom']) : array(); + $data['x-custom'] = isset($data['x-custom']) ? ((array) $data['x-custom']) : []; foreach ($data['x-custom'] as $idx => $val) { if (is_array($val) && $val[0] == $name_items[1]) { $data['x-custom'][$idx][1] = $value; return; } } - $data['x-custom'][] = array($name_items[1], $value); + $data['x-custom'][] = [$name_items[1], $value]; return; } $name_items = explode(':', $name); $name = $name_items[0]; // simple array (e.g. email) if (count($name_items) == 2) { $data[$name][$name_items[1]] = $value; return; } $data[$name] = $value; } /** * Unsets data item in kolab data array */ protected function unsetKolabDataItem(&$data, $name) { $name_items = explode('.', $name); $count = count($name_items); // multi-level array (e.g. address, phone) if ($count == 3) { $name = $name_items[0]; $type = $name_items[1]; $key_name = $name_items[2]; if (!isset($data[$name])) { return; } foreach ($data[$name] as $idx => $element) { if ($element['type'] == $type) { $found = $idx; break; } } if (!isset($found)) { return; } unset($data[$name][$found][$key_name]); // if there's only one element and it's 'type', remove it if (count($data[$name][$found]) == 1 && isset($data[$name][$found]['type'])) { unset($data[$name][$found]['type']); } if (empty($data[$name][$found])) { unset($data[$name][$found]); } if (empty($data[$name])) { unset($data[$name]); } return; } // custom properties if ($count == 2 && $name_items[0] == 'x-custom') { foreach ((array) $data['x-custom'] as $idx => $val) { if (is_array($val) && $val[0] == $name_items[1]) { unset($data['x-custom'][$idx]); } } } $name_items = explode(':', $name); $name = $name_items[0]; // simple array (e.g. email) if (count($name_items) == 2) { unset($data[$name][$name_items[1]]); if (empty($data[$name])) { unset($data[$name]); } return; } unset($data[$name]); } /** * Setter for Body attribute according to client version * * @param string $value Body * @param array $params Body parameters * * @reurn Syncroton_Model_EmailBody Body element */ - protected function setBody($value, $params = array()) + protected function setBody($value, $params = []) { if (empty($value) && empty($params)) { return; } // Old protocol version doesn't support AirSyncBase:Body, it's eg. WindowsCE if ($this->asversion < 12) { return; } if (!empty($value)) { // cast to string to workaround issue described in Bug #1635 $params['data'] = (string) $value; } if (!isset($params['type'])) { $params['type'] = Syncroton_Model_EmailBody::TYPE_PLAINTEXT; } return new Syncroton_Model_EmailBody($params); } /** * Getter for Body attribute value according to client version * * @param mixed $body Body element * @param int $type Result data type (to which the body will be converted, if specified). * One or array of Syncroton_Model_EmailBody constants. * * @return string|null Body value */ protected function getBody($body, $type = null) { $data = null; if ($body && $body->data) { $data = $body->data; } if (!$data || empty($type)) { return null; } $type = (array) $type; // Convert to specified type if (!in_array($body->type, $type)) { $converter = new kolab_sync_body_converter($data, $body->type); $data = $converter->convert($type[0]); } return $data; } /** * Converts text (plain or html) into ActiveSync Body element. * Takes bodyPreferences into account and detects if the text is plain or html. */ protected function body_from_kolab($body, $collection) { if (empty($body)) { return; } $opts = $collection->options; $prefs = $opts['bodyPreferences']; $html_type = Syncroton_Command_Sync::BODY_TYPE_HTML; $type = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT; - $params = array(); + $params = []; // HTML? check for opening and closing or tags - $is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, '') > 0; + $is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, '') > 0; // here we assume that all devices support plain text if ($is_html) { // device supports HTML... if (!empty($prefs[$html_type])) { $type = $html_type; } // ...else convert to plain text else { $txt = new rcube_html2text($body, false, true); $body = $txt->get_text(); } } // strip out any non utf-8 characters $body = rcube_charset::clean($body); $real_length = $body_length = strlen($body); // truncate the body if needed if (isset($prefs[$type]['truncationSize']) && ($truncateAt = $prefs[$type]['truncationSize']) && $body_length > $truncateAt) { $body = mb_strcut($body, 0, $truncateAt); $body_length = strlen($body); $params['truncated'] = 1; $params['estimatedDataSize'] = $real_length; } $params['type'] = $type; return $this->setBody($body, $params); } /** * Converts PHP DateTime, date (YYYY-MM-DD) or unixtimestamp into PHP DateTime in UTC * * @param DateTime|int|string $date Unix timestamp, date (YYYY-MM-DD) or PHP DateTime object * * @return DateTime|null Datetime object */ protected static function date_from_kolab($date) { if (!empty($date)) { if (is_numeric($date)) { $date = new DateTime('@' . $date); - } - else if (is_string($date)) { + } elseif (is_string($date)) { $date = new DateTime($date, new DateTimeZone('UTC')); - } - else if ($date instanceof DateTime) { + } elseif ($date instanceof DateTime) { $date = clone $date; $tz = $date->getTimezone(); $tz_name = $tz->getName(); // convert to UTC if needed if ($tz_name != 'UTC') { $utc = new DateTimeZone('UTC'); // safe dateonly object conversion to UTC // note: _dateonly flag is set by libkolab e.g. for birthdays if (!empty($date->_dateonly)) { // avoid time change $date = new DateTime($date->format('Y-m-d'), $utc); // set time to noon to avoid timezone troubles $date->setTime(12, 0, 0); - } - else { + } else { $date->setTimezone($utc); } } - } - else { + } else { return null; // invalid input } return $date; } return null; } /** * Convert Kolab event/task recurrence into ActiveSync */ protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event') { if (empty($data['recurrence']) || !empty($data['recurrence_date']) || empty($data['recurrence']['FREQ'])) { return; } - $recurrence = array(); + $recurrence = []; $r = $data['recurrence']; // required fields switch($r['FREQ']) { - case 'DAILY': - $recurrence['type'] = self::RECUR_TYPE_DAILY; - break; - - case 'WEEKLY': - $day = $r['BYDAY'] ?? 0; - if (!$day && (!empty($data['_start']) || !empty($data['start']))) { - $days = ['', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA','SU']; - $start = $data['_start'] ?? $data['start']; - $day = $days[$start->format('N')]; - } + case 'DAILY': + $recurrence['type'] = self::RECUR_TYPE_DAILY; + break; - $recurrence['type'] = self::RECUR_TYPE_WEEKLY; - $recurrence['dayOfWeek'] = $this->day2bitmask($day); - break; - - case 'MONTHLY': - if (!empty($r['BYMONTHDAY'])) { - // @TODO: ActiveSync doesn't support multi-valued month days, - // should we replicate the recurrence element for each day of month? - [$month_day, ] = explode(',', $r['BYMONTHDAY']); - $recurrence['type'] = self::RECUR_TYPE_MONTHLY; - $recurrence['dayOfMonth'] = $month_day; - } - else if (!empty($r['BYDAY'])) { - $week = (int) substr($r['BYDAY'], 0, -2); - $week = ($week == -1) ? 5 : $week; - $day = substr($r['BYDAY'], -2); - $recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN; - $recurrence['weekOfMonth'] = $week; - $recurrence['dayOfWeek'] = $this->day2bitmask($day); - } else { - return; - } - break; - - case 'YEARLY': - // @TODO: ActiveSync doesn't support multi-valued months, - // should we replicate the recurrence element for each month? - [$month, ] = explode(',', $r['BYMONTH']); - - if (!empty($r['BYDAY'])) { - $week = (int) substr($r['BYDAY'], 0, -2); - $week = ($week == -1) ? 5 : $week; - $day = substr($r['BYDAY'], -2); - $recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN; - $recurrence['weekOfMonth'] = $week; + case 'WEEKLY': + $day = $r['BYDAY'] ?? 0; + if (!$day && (!empty($data['_start']) || !empty($data['start']))) { + $days = ['', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA','SU']; + $start = $data['_start'] ?? $data['start']; + $day = $days[$start->format('N')]; + } + + $recurrence['type'] = self::RECUR_TYPE_WEEKLY; $recurrence['dayOfWeek'] = $this->day2bitmask($day); - $recurrence['monthOfYear'] = $month; - } - else if (!empty($r['BYMONTHDAY'])) { - // @TODO: ActiveSync doesn't support multi-valued month days, - // should we replicate the recurrence element for each day of month? - [$month_day, ] = explode(',', $r['BYMONTHDAY']); - $recurrence['type'] = self::RECUR_TYPE_YEARLY; - $recurrence['dayOfMonth'] = $month_day; - $recurrence['monthOfYear'] = $month; - } - else { - $recurrence['type'] = self::RECUR_TYPE_YEARLY; - $recurrence['monthOfYear'] = $month; - } - break; + break; + + case 'MONTHLY': + if (!empty($r['BYMONTHDAY'])) { + // @TODO: ActiveSync doesn't support multi-valued month days, + // should we replicate the recurrence element for each day of month? + [$month_day, ] = explode(',', $r['BYMONTHDAY']); + $recurrence['type'] = self::RECUR_TYPE_MONTHLY; + $recurrence['dayOfMonth'] = $month_day; + } elseif (!empty($r['BYDAY'])) { + $week = (int) substr($r['BYDAY'], 0, -2); + $week = ($week == -1) ? 5 : $week; + $day = substr($r['BYDAY'], -2); + $recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN; + $recurrence['weekOfMonth'] = $week; + $recurrence['dayOfWeek'] = $this->day2bitmask($day); + } else { + return; + } + break; + + case 'YEARLY': + // @TODO: ActiveSync doesn't support multi-valued months, + // should we replicate the recurrence element for each month? + [$month, ] = explode(',', $r['BYMONTH']); + + if (!empty($r['BYDAY'])) { + $week = (int) substr($r['BYDAY'], 0, -2); + $week = ($week == -1) ? 5 : $week; + $day = substr($r['BYDAY'], -2); + $recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN; + $recurrence['weekOfMonth'] = $week; + $recurrence['dayOfWeek'] = $this->day2bitmask($day); + $recurrence['monthOfYear'] = $month; + } elseif (!empty($r['BYMONTHDAY'])) { + // @TODO: ActiveSync doesn't support multi-valued month days, + // should we replicate the recurrence element for each day of month? + [$month_day, ] = explode(',', $r['BYMONTHDAY']); + $recurrence['type'] = self::RECUR_TYPE_YEARLY; + $recurrence['dayOfMonth'] = $month_day; + $recurrence['monthOfYear'] = $month; + } else { + $recurrence['type'] = self::RECUR_TYPE_YEARLY; + $recurrence['monthOfYear'] = $month; + } + break; } // Skip all empty values (T2519) if ($recurrence['type'] != self::RECUR_TYPE_DAILY) { $recurrence = array_filter($recurrence); } // required field $recurrence['interval'] = $r['INTERVAL'] ?: 1; if (!empty($r['UNTIL'])) { $recurrence['until'] = self::date_from_kolab($r['UNTIL']); - } - else if (!empty($r['COUNT'])) { + } elseif (!empty($r['COUNT'])) { $recurrence['occurrences'] = $r['COUNT']; } $class = 'Syncroton_Model_' . $type . 'Recurrence'; $result['recurrence'] = new $class($recurrence); // Tasks do not support exceptions if ($type == 'Event') { $result['exceptions'] = $this->exceptions_from_kolab($collection, $data); } } /** * Convert ActiveSync event/task recurrence into Kolab */ protected function recurrence_to_kolab($data, $folderid, $timezone = null) { if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence) && !($data->recurrence instanceof Syncroton_Model_TaskRecurrence) ) { return; } if (!isset($data->recurrence->type)) { return; } $recurrence = $data->recurrence; $type = $recurrence->type; switch ($type) { - case self::RECUR_TYPE_DAILY: - break; + case self::RECUR_TYPE_DAILY: + break; - case self::RECUR_TYPE_WEEKLY: - $rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek); - break; + case self::RECUR_TYPE_WEEKLY: + $rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek); + break; - case self::RECUR_TYPE_MONTHLY: - $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth; - break; + case self::RECUR_TYPE_MONTHLY: + $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth; + break; - case self::RECUR_TYPE_MONTHLY_DAYN: - $week = $recurrence->weekOfMonth; - $day = $recurrence->dayOfWeek; - $byDay = $week == 5 ? -1 : $week; - $byDay .= $this->bitmask2day($day); + case self::RECUR_TYPE_MONTHLY_DAYN: + $week = $recurrence->weekOfMonth; + $day = $recurrence->dayOfWeek; + $byDay = $week == 5 ? -1 : $week; + $byDay .= $this->bitmask2day($day); - $rrule['BYDAY'] = $byDay; - break; + $rrule['BYDAY'] = $byDay; + break; - case self::RECUR_TYPE_YEARLY: - $rrule['BYMONTH'] = $recurrence->monthOfYear; - $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth; - break; + case self::RECUR_TYPE_YEARLY: + $rrule['BYMONTH'] = $recurrence->monthOfYear; + $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth; + break; - case self::RECUR_TYPE_YEARLY_DAYN: - $rrule['BYMONTH'] = $recurrence->monthOfYear; + case self::RECUR_TYPE_YEARLY_DAYN: + $rrule['BYMONTH'] = $recurrence->monthOfYear; - $week = $recurrence->weekOfMonth; - $day = $recurrence->dayOfWeek; - $byDay = $week == 5 ? -1 : $week; - $byDay .= $this->bitmask2day($day); + $week = $recurrence->weekOfMonth; + $day = $recurrence->dayOfWeek; + $byDay = $week == 5 ? -1 : $week; + $byDay .= $this->bitmask2day($day); - $rrule['BYDAY'] = $byDay; - break; + $rrule['BYDAY'] = $byDay; + break; } $rrule['FREQ'] = $this->recurTypeMap[$type]; - $rrule['INTERVAL'] = isset($recurrence->interval) ? $recurrence->interval : 1; + $rrule['INTERVAL'] = $recurrence->interval ?? 1; if (isset($recurrence->until)) { if ($timezone) { $recurrence->until->setTimezone($timezone); } $rrule['UNTIL'] = $recurrence->until; - } - else if (!empty($recurrence->occurrences)) { + } elseif (!empty($recurrence->occurrences)) { $rrule['COUNT'] = $recurrence->occurrences; } // recurrence exceptions (not supported by Tasks) if ($data instanceof Syncroton_Model_Event) { $this->exceptions_to_kolab($data, $rrule, $folderid, $timezone); } return $rrule; } /** * Convert Kolab event recurrence exceptions into ActiveSync */ protected function exceptions_from_kolab($collection, $data) { if (empty($data['recurrence']['EXCEPTIONS']) && empty($data['recurrence']['EXDATE'])) { return null; } - $ex_list = array(); + $ex_list = []; // exceptions (modified occurences) if (!empty($data['recurrence']['EXCEPTIONS'])) { foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) { $exception['_mailbox'] = $data['_mailbox']; $ex = $this->getEntry($collection, $exception, true); // @phpstan-ignore-line $date = clone ($exception['recurrence_date'] ?: $ex['startTime']); $ex['exceptionStartTime'] = self::set_exception_time($date, $data['_start'] ?? null); // remove fields not supported by Syncroton_Model_EventException unset($ex['uID']); // @TODO: 'thisandfuture=true' is not supported in Activesync // we'd need to slit the event into two separate events $ex_list[] = new Syncroton_Model_EventException($ex); } } // exdate (deleted occurences) if (!empty($data['recurrence']['EXDATE'])) { foreach ((array)$data['recurrence']['EXDATE'] as $exception) { if (!($exception instanceof DateTime)) { continue; } - $ex = array( + $ex = [ 'deleted' => 1, 'exceptionStartTime' => self::set_exception_time($exception, $data['_start'] ?? null), - ); + ]; $ex_list[] = new Syncroton_Model_EventException($ex); } } return $ex_list; } /** * Convert ActiveSync event recurrence exceptions into Kolab */ protected function exceptions_to_kolab($data, &$rrule, $folderid, $timezone = null) { - $rrule['EXDATE'] = array(); - $rrule['EXCEPTIONS'] = array(); + $rrule['EXDATE'] = []; + $rrule['EXCEPTIONS'] = []; // handle exceptions from recurrence if (!empty($data->exceptions)) { foreach ($data->exceptions as $exception) { $date = clone $exception->exceptionStartTime; if ($timezone) { $date->setTimezone($timezone); } if ($exception->deleted) { $date->setTime(0, 0, 0); $rrule['EXDATE'][] = $date; - } - else { + } else { $ex = $this->toKolab($exception, $folderid, null, $timezone); // @phpstan-ignore-line $ex['recurrence_date'] = $date; if (!empty($data->allDayEvent)) { $ex['allday'] = 1; } $rrule['EXCEPTIONS'][] = $ex; } } } if (empty($rrule['EXDATE'])) { unset($rrule['EXDATE']); } if (empty($rrule['EXCEPTIONS'])) { unset($rrule['EXCEPTIONS']); } } /** * Sets ExceptionStartTime according to occurrence date and event start time */ protected static function set_exception_time($exception_date, $event_start) { if ($exception_date && $event_start) { $hour = $event_start->format('H'); $minute = $event_start->format('i'); $second = $event_start->format('s'); $exception_date->setTime($hour, $minute, $second); $exception_date->_dateonly = false; return self::date_from_kolab($exception_date); } } /** * Converts string of days (TU,TH) to bitmask used by ActiveSync * * @param string $days * * @return int */ protected function day2bitmask($days) { $days = explode(',', $days); $result = 0; foreach ($days as $day) { if ($day) { $result = $result + ($this->recurDayMap[$day] ?? 0); } } return $result; } /** * Convert bitmask used by ActiveSync to string of days (TU,TH) * * @param int $days * * @return string */ protected function bitmask2day($days) { - $days_arr = array(); + $days_arr = []; for ($bitmask = 1; $bitmask <= self::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) { $dayMatch = $days & $bitmask; if ($dayMatch === $bitmask) { $days_arr[] = array_search($bitmask, $this->recurDayMap); } } $result = implode(',', $days_arr); return $result; } /** * Check if current device type string matches any of options */ protected function deviceTypeFilter($options) { foreach ($options as $option) { if ($option[0] == '/') { if (preg_match($option, $this->device->devicetype)) { return true; } - } - else if (stripos($this->device->devicetype, $option) !== false) { + } elseif (stripos($this->device->devicetype, $option) !== false) { return true; } } return false; } /** * Returns all email addresses of the current user */ protected function user_emails() { $user_emails = kolab_sync::get_instance()->user->list_emails(); - $user_emails = array_map(function($v) { return $v['email']; }, $user_emails); + $user_emails = array_map(function ($v) { return $v['email']; }, $user_emails); return $user_emails; } /** * Generate CRC-based ServerId from object UID */ protected function serverId($uid, $folder) { // When ActiveSync communicates with the client, it refers to objects with a ServerId // We can't use object UID for ServerId because: // - ServerId is limited to 64 chars, // - there can be multiple calendars with a copy of the same event. // // The solution is to; Take the original UID, and regardless of its length, execute the following: // - Hash the UID concatenated with the Folder ID using CRC32b, // - Prefix the UID with 'CRC' and the hash string, // - Tryncate the result to 64 characters. // // Searching for the server-side copy of the object now follows the logic; // - If the ServerId is prefixed with 'CRC', strip off the first 11 characters // and we search for the UID using the remainder; // - if the UID is shorter than 53 characters, it'll be the complete UID, // - if the UID is longer than 53 characters, it'll be the truncated UID, // and we search for a wildcard match of * // When multiple copies of the same event are found, the same CRC32b hash can be used // on the events metadata (i.e. the copy's UID and Folder ID), and compared with the CRC from the ServerId. // ServerId is max. 64 characters, below we generate a string of max. 64 chars // Note: crc32b is always 8 characters return 'CRC' . $this->objectCRC($uid, $folder) . substr($uid, 0, 53); } /** * Calculate checksum on object UID and folder UID */ protected function objectCRC($uid, $folder) { if (!is_object($folder)) { $folder = $this->backend->getFolder($folder, $this->device->deviceid, $this->modelName); } $folder_uid = $folder->get_uid(); return strtoupper(hash('crc32b', $folder_uid . $uid)); // always 8 chars } } diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php index 81dd9ce..12e660e 100644 --- a/lib/kolab_sync_data_calendar.php +++ b/lib/kolab_sync_data_calendar.php @@ -1,1355 +1,1346 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Calendar (Events) data class for Syncroton */ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data_IDataCalendar { /** * Mapping from ActiveSync Calendar namespace fields */ - protected $mapping = array( + protected $mapping = [ 'allDayEvent' => 'allday', 'startTime' => 'start', // keep it before endTime here //'attendees' => 'attendees', 'body' => 'description', //'bodyTruncated' => 'bodytruncated', 'busyStatus' => 'free_busy', //'categories' => 'categories', 'dtStamp' => 'changed', 'endTime' => 'end', //'exceptions' => 'exceptions', 'location' => 'location', //'meetingStatus' => 'meetingstatus', //'organizerEmail' => 'organizeremail', //'organizerName' => 'organizername', //'recurrence' => 'recurrence', //'reminder' => 'reminder', //'responseRequested' => 'responserequested', //'responseType' => 'responsetype', 'sensitivity' => 'sensitivity', 'subject' => 'title', //'timezone' => 'timezone', 'uID' => 'uid', - ); + ]; /** * Kolab object type * * @var string */ protected $modelName = 'event'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'Calendar'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED; /** * attendee status */ - 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; + public const ATTENDEE_STATUS_UNKNOWN = 0; + public const ATTENDEE_STATUS_TENTATIVE = 2; + public const ATTENDEE_STATUS_ACCEPTED = 3; + public const ATTENDEE_STATUS_DECLINED = 4; + public const ATTENDEE_STATUS_NOTRESPONDED = 5; /** * attendee types */ - const ATTENDEE_TYPE_REQUIRED = 1; - const ATTENDEE_TYPE_OPTIONAL = 2; - const ATTENDEE_TYPE_RESOURCE = 3; + public const ATTENDEE_TYPE_REQUIRED = 1; + public const ATTENDEE_TYPE_OPTIONAL = 2; + public 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; + public const BUSY_STATUS_FREE = 0; + public const BUSY_STATUS_TENTATIVE = 1; + public const BUSY_STATUS_BUSY = 2; + public const BUSY_STATUS_OUTOFOFFICE = 3; /** * Sensitivity values */ - const SENSITIVITY_NORMAL = 0; - const SENSITIVITY_PERSONAL = 1; - const SENSITIVITY_PRIVATE = 2; - const SENSITIVITY_CONFIDENTIAL = 3; + public const SENSITIVITY_NORMAL = 0; + public const SENSITIVITY_PERSONAL = 1; + public const SENSITIVITY_PRIVATE = 2; + public const SENSITIVITY_CONFIDENTIAL = 3; /** * Internal iTip states */ - const ITIP_ACCEPTED = 'ACCEPTED'; - const ITIP_DECLINED = 'DECLINED'; - const ITIP_TENTATIVE = 'TENTATIVE'; - const ITIP_CANCELLED = 'CANCELLED'; + public const ITIP_ACCEPTED = 'ACCEPTED'; + public const ITIP_DECLINED = 'DECLINED'; + public const ITIP_TENTATIVE = 'TENTATIVE'; + public const ITIP_CANCELLED = 'CANCELLED'; - const KEY_DTSTAMP = 'x-custom.X-ACTIVESYNC-DTSTAMP'; - const KEY_REPLYTIME = 'x-custom.X-ACTIVESYNC-REPLYTIME'; + public const KEY_DTSTAMP = 'x-custom.X-ACTIVESYNC-DTSTAMP'; + public const KEY_REPLYTIME = 'x-custom.X-ACTIVESYNC-REPLYTIME'; /** * Mapping of attendee status * * @var array */ - protected $attendeeStatusMap = array( + protected $attendeeStatusMap = [ 'UNKNOWN' => self::ATTENDEE_STATUS_UNKNOWN, 'TENTATIVE' => self::ATTENDEE_STATUS_TENTATIVE, 'ACCEPTED' => self::ATTENDEE_STATUS_ACCEPTED, 'DECLINED' => self::ATTENDEE_STATUS_DECLINED, 'DELEGATED' => self::ATTENDEE_STATUS_UNKNOWN, 'NEEDS-ACTION' => self::ATTENDEE_STATUS_NOTRESPONDED, - ); + ]; /** * Mapping of attendee type * * NOTE: recurrences need extra handling! * @var array */ - protected $attendeeTypeMap = array( + protected $attendeeTypeMap = [ 'REQ-PARTICIPANT' => self::ATTENDEE_TYPE_REQUIRED, 'OPT-PARTICIPANT' => self::ATTENDEE_TYPE_OPTIONAL, // 'NON-PARTICIPANT' => self::ATTENDEE_TYPE_RESOURCE, // 'CHAIR' => self::ATTENDEE_TYPE_RESOURCE, - ); + ]; /** * Mapping of busy status * * @var array */ - protected $busyStatusMap = array( + protected $busyStatusMap = [ 'free' => self::BUSY_STATUS_FREE, 'tentative' => self::BUSY_STATUS_TENTATIVE, 'busy' => self::BUSY_STATUS_BUSY, 'outofoffice' => self::BUSY_STATUS_OUTOFOFFICE, - ); + ]; /** * mapping of sensitivity * * @var array */ - protected $sensitivityMap = array( + protected $sensitivityMap = [ 'public' => self::SENSITIVITY_PERSONAL, 'private' => self::SENSITIVITY_PRIVATE, 'confidential' => self::SENSITIVITY_CONFIDENTIAL, - ); + ]; /** * Appends contact data to xml element * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * @param bool $as_array Return entry as array * * @return array|Syncroton_Model_Event Event object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false) { $event = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId); $config = $this->getFolderConfig($event['folderId']); - $result = array(); + $result = []; $is_outlook = stripos($this->device->devicetype, 'outlook') !== false; $is_android = stripos($this->device->devicetype, 'android') !== false; // Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows // only one timezone per-event. We'll use timezone of the start date $result['timezone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']); // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getKolabDataItem($event, $name); switch ($name) { - case 'changed': - case 'end': - case 'start': - // For all-day events Kolab uses different times - // At least Android doesn't display such event as all-day event - if ($value && is_a($value, 'DateTime')) { - $date = clone $value; - if (!empty($event['allday'])) { - // need this for self::date_from_kolab() - $date->_dateonly = false; // @phpstan-ignore-line + case 'changed': + case 'end': + case 'start': + // For all-day events Kolab uses different times + // At least Android doesn't display such event as all-day event + if ($value && is_a($value, 'DateTime')) { + $date = clone $value; + if (!empty($event['allday'])) { + // need this for self::date_from_kolab() + $date->_dateonly = false; // @phpstan-ignore-line + + if ($name == 'start') { + $date->setTime(0, 0, 0); + } elseif ($name == 'end') { + $date->setTime(0, 0, 0); + $date->modify('+1 day'); + } + } + // set this date for use in recurrence exceptions handling if ($name == 'start') { - $date->setTime(0, 0, 0); - } - else if ($name == 'end') { - $date->setTime(0, 0, 0); - $date->modify('+1 day'); + $event['_start'] = $date; } - } - // set this date for use in recurrence exceptions handling - if ($name == 'start') { - $event['_start'] = $date; + $value = self::date_from_kolab($date); } - $value = self::date_from_kolab($date); - } - - break; + break; - case 'sensitivity': - if (!empty($value)) { - $value = intval($this->sensitivityMap[$value]); - } - break; + case 'sensitivity': + if (!empty($value)) { + $value = intval($this->sensitivityMap[$value]); + } + break; - case 'free_busy': - if (!empty($value)) { - $value = $this->busyStatusMap[$value]; - } - break; + case 'free_busy': + if (!empty($value)) { + $value = $this->busyStatusMap[$value]; + } + break; - case 'description': - $value = $this->body_from_kolab($value, $collection); - break; + case 'description': + $value = $this->body_from_kolab($value, $collection); + break; } // Ignore empty values (but not integer 0) if ((empty($value) || is_array($value)) && $value !== 0) { continue; } $result[$key] = $value; } // Event reminder time if (!empty($config['ALARMS'])) { $result['reminder'] = $this->from_kolab_alarm($event); } - $result['categories'] = array(); - $result['attendees'] = array(); + $result['categories'] = []; + $result['attendees'] = []; // Categories, Roundcube Calendar plugin supports only one category at a time if (!empty($event['categories'])) { $result['categories'] = (array) $event['categories']; } // Organizer if (!empty($event['attendees'])) { foreach ($event['attendees'] as $idx => $attendee) { if ($attendee['role'] == 'ORGANIZER') { if (!empty($attendee['name'])) { $result['organizerName'] = $attendee['name']; } if (!empty($attendee['email'])) { $result['organizerEmail'] = $attendee['email']; } unset($event['attendees'][$idx]); break; } } } $resp_type = self::ATTENDEE_STATUS_UNKNOWN; $user_rsvp = false; // Attendees if (!empty($event['attendees'])) { $user_emails = $this->user_emails(); foreach ($event['attendees'] as $idx => $attendee) { if (empty($attendee['email'])) { // In Activesync email is required continue; } $email = $attendee['email']; $att = [ 'email' => $email, 'name' => !empty($attendee['name']) ? $attendee['name'] : $email, ]; - $type = isset($attendee['role']) ? $this->attendeeTypeMap[$attendee['role']] : null; + $type = isset($attendee['role']) ? $this->attendeeTypeMap[$attendee['role']] : null; $status = isset($attendee['status']) ? $this->attendeeStatusMap[$attendee['status']] : null; if ($this->asversion >= 12) { if (isset($attendee['cutype']) && strtolower($attendee['cutype']) == 'resource') { $att['attendeeType'] = self::ATTENDEE_TYPE_RESOURCE; } else { $att['attendeeType'] = $type ?: self::ATTENDEE_TYPE_REQUIRED; } $att['attendeeStatus'] = $status ?: self::ATTENDEE_STATUS_UNKNOWN; } if (in_array_nocase($email, $user_emails)) { $user_rsvp = !empty($attendee['rsvp']); $resp_type = $status ?: self::ATTENDEE_STATUS_UNKNOWN; // Synchronize the attendee status to the event status to get the same behaviour as outlook. - if (($is_outlook || $is_android )&& isset($attendee['status'])) { + if (($is_outlook || $is_android) && isset($attendee['status'])) { if ($attendee['status'] == 'ACCEPTED') { $result['busyStatus'] = self::BUSY_STATUS_BUSY; } if ($attendee['status'] == 'TENTATIVE') { $result['busyStatus'] = self::BUSY_STATUS_TENTATIVE; } } } $result['attendees'][] = new Syncroton_Model_EventAttendee($att); } } // Event meeting status $this->meeting_status_from_kolab($event, $result); // Recurrence (and exceptions) $this->recurrence_from_kolab($collection, $event, $result); // RSVP status $result['responseRequested'] = $result['meetingStatus'] == 3 && $user_rsvp ? 1 : 0; $result['responseType'] = $result['meetingStatus'] == 3 ? $resp_type : null; // Appointment Reply Time (without it Outlook displays e.g. "Accepted on None") if ($resp_type != self::ATTENDEE_STATUS_UNKNOWN) { if ($reply_time = $this->getKolabDataItem($event, self::KEY_REPLYTIME)) { $result['appointmentReplyTime'] = new DateTime($reply_time, new DateTimeZone('UTC')); } elseif (!empty($event['changed'])) { $reply_time = clone $event['changed']; $reply_time->setTimezone(new DateTimeZone('UTC')); $result['appointmentReplyTime'] = $reply_time; } } return $as_array ? $result : new Syncroton_Model_Event($result); } /** * Convert an event from xml to libkolab array * * @param Syncroton_Model_Event|Syncroton_Model_EventException $data Event or event exception to convert * @param string $folderid Folder identifier * @param array $entry Existing entry * @param DateTimeZone $timezone Timezone of the event * * @return array */ public function toKolab($data, $folderid, $entry = null, $timezone = null) { if (empty($entry) && !empty($data->uID)) { // If we don't have an existing event (not a modification) we nevertheless check for conflicts. // This is necessary so we don't overwrite the server-side copy in case the client did not have it available // when generating an Add command. try { $entry = $this->getObject($folderid, $data->uID); if ($entry) { $this->logger->debug('Found and existing event for UID: ' . $data->uID); } } catch (Exception $e) { // uID is not available on exceptions, so we guard for that and silently ignore. } } $config = $this->getFolderConfig($entry ? $entry['folderId'] : $folderid); $event = !empty($entry) ? $entry : []; $is_exception = $data instanceof Syncroton_Model_EventException; $dummy_tz = str_repeat('A', 230) . '=='; $is_outlook = stripos($this->device->devicetype, 'outlook') !== false; $is_android = stripos($this->device->devicetype, 'android') !== false; // check data validity (of a new event) if (empty($event)) { $this->check_event($data); } if (!empty($event['start']) && ($event['start'] instanceof DateTime)) { $old_timezone = $event['start']->getTimezone(); } // Timezone if (!$timezone && isset($data->timezone) && $data->timezone != $dummy_tz) { $tzc = kolab_sync_timezone_converter::getInstance(); $expected = !empty($old_timezone) ? $old_timezone : kolab_format::$timezone; try { $timezone = $tzc->getTimezone($data->timezone, $expected->getName()); $timezone = new DateTimeZone($timezone); - } - catch (Exception $e) { + } catch (Exception $e) { $this->logger->warn('Failed to convert the timezone information. UID: ' . $event['uid'] . 'Timezone: ' . $data->timezone); $timezone = null; } } if (empty($timezone)) { $timezone = !empty($old_timezone) ? $old_timezone : new DateTimeZone('UTC'); } $event['allday'] = 0; // Calendar namespace fields foreach ($this->mapping as $key => $name) { // skip UID field, unsupported in event exceptions // we need to do this here, because the next line (data getter) will throw an exception if ($is_exception && $key == 'uID') { continue; } $value = $data->$key; // Skip ghosted (unset) properties, (but make sure 'changed' timestamp is reset) if ($value === null && $name != 'changed') { continue; } switch ($name) { - case 'changed': - $value = null; - break; + case 'changed': + $value = null; + break; - case 'end': - case 'start': - if ($timezone && $value) { - $value->setTimezone($timezone); - } + case 'end': + case 'start': + if ($timezone && $value) { + $value->setTimezone($timezone); + } - if ($value && $data->allDayEvent) { - $value->_dateonly = true; + if ($value && $data->allDayEvent) { + $value->_dateonly = true; - // In ActiveSync all-day event ends on 00:00:00 next day - // In Kolab we just ignore the time spec. - if ($name == 'end') { - $diff = date_diff($event['start'], $value); - $value = clone $event['start']; + // In ActiveSync all-day event ends on 00:00:00 next day + // In Kolab we just ignore the time spec. + if ($name == 'end') { + $diff = date_diff($event['start'], $value); + $value = clone $event['start']; - if ($diff->days > 1) { - $value->add(new DateInterval('P' . ($diff->days - 1) . 'D')); + if ($diff->days > 1) { + $value->add(new DateInterval('P' . ($diff->days - 1) . 'D')); + } } } - } - break; + break; - case 'sensitivity': - $map = array_flip($this->sensitivityMap); - $value = isset($map[$value]) ? $map[$value] : null; - break; + case 'sensitivity': + $map = array_flip($this->sensitivityMap); + $value = $map[$value] ?? null; + break; - case 'free_busy': - // Outlook sets the busy state to the attendance state, and we don't want to change the event state based on that. - // Outlook doesn't have the concept of an event state, so we just ignore this. - if ($is_outlook || $is_android) { - continue 2; - } - $map = array_flip($this->busyStatusMap); - $value = isset($map[$value]) ? $map[$value] : null; - break; + case 'free_busy': + // Outlook sets the busy state to the attendance state, and we don't want to change the event state based on that. + // Outlook doesn't have the concept of an event state, so we just ignore this. + if ($is_outlook || $is_android) { + continue 2; + } + $map = array_flip($this->busyStatusMap); + $value = $map[$value] ?? null; + break; - case 'description': - $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT); - // If description isn't specified keep old description - if ($value === null) { - continue 2; - } - break; + case 'description': + $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT); + // If description isn't specified keep old description + if ($value === null) { + continue 2; + } + break; } $this->setKolabDataItem($event, $name, $value); } // Try to fix allday events from Android // It doesn't set all-day flag but the period is a whole day if (empty($event['allday']) && !empty($event['end']) && !empty($event['start'])) { $interval = @date_diff($event['start'], $event['end']); if ($interval->format('%y%m%d%h%i%s') === '001000') { $event['allday'] = 1; $event['end'] = clone $event['start']; } } // Reminder // @TODO: should alarms be used when importing event from phone? if (!empty($config['ALARMS'])) { $event['valarms'] = $this->to_kolab_alarm($data->reminder, $event); } - $attendees = array(); - $categories = array(); + $attendees = []; + $categories = []; // Categories if (isset($data->categories)) { foreach ($data->categories as $category) { $categories[] = $category; } } // Organizer if (!$is_exception) { // Organizer specified if ($organizer_email = $data->organizerEmail) { - $attendees[] = array( + $attendees[] = [ 'role' => 'ORGANIZER', 'name' => $data->organizerName, 'email' => $organizer_email, - ); - } else if (!empty($event['attendees'])) { + ]; + } elseif (!empty($event['attendees'])) { // Organizer not specified, use one from the original event if that's an update foreach ($event['attendees'] as $idx => $attendee) { if (!empty($attendee['email']) && !empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') { $organizer_email = $attendee['email']; - $attendees[] = array( + $attendees[] = [ 'role' => 'ORGANIZER', 'name' => $attendee['name'] ?? '', 'email' => $organizer_email, - ); + ]; } } } } // Attendees // Whenever Outlook sends dummy timezone it is an event where the user is an attendee. // In these cases Attendees element is bogus: contains invalid status and does not // contain all attendees. We have to ignore it. if ($is_outlook && !$is_exception && $data->timezone === $dummy_tz) { $this->logger->debug('Dummy outlook update detected, ignoring attendee changes.'); $attendees = $entry['attendees']; - } - else if (isset($data->attendees)) { + } elseif (isset($data->attendees)) { foreach ($data->attendees as $attendee) { if (!empty($organizer_email) && $attendee->email && !strcasecmp($attendee->email, $organizer_email)) { // skip the organizer continue; } $role = false; if (isset($attendee->attendeeType)) { $role = array_search($attendee->attendeeType, $this->attendeeTypeMap); } if ($role === false) { $role = array_search(self::ATTENDEE_TYPE_REQUIRED, $this->attendeeTypeMap); } - $_attendee = array( + $_attendee = [ 'role' => $role, 'name' => $attendee->name != $attendee->email ? $attendee->name : '', 'email' => $attendee->email, - ); + ]; if (isset($attendee->attendeeType) && $attendee->attendeeType == self::ATTENDEE_TYPE_RESOURCE) { $_attendee['cutype'] = 'RESOURCE'; } if (isset($attendee->attendeeStatus)) { $_attendee['status'] = $attendee->attendeeStatus ? array_search($attendee->attendeeStatus, $this->attendeeStatusMap) : null; if (!$_attendee['status']) { $_attendee['status'] = 'NEEDS-ACTION'; $_attendee['rsvp'] = true; } - } - else if (!empty($event['attendees']) && !empty($attendee->email)) { + } elseif (!empty($event['attendees']) && !empty($attendee->email)) { // copy the old attendee status foreach ($event['attendees'] as $old_attendee) { if ($old_attendee['email'] == $_attendee['email'] && isset($old_attendee['status'])) { $_attendee['status'] = $old_attendee['status']; $_attendee['rsvp'] = $old_attendee['rsvp']; break; } } } $attendees[] = $_attendee; } } // Outlook does not send the correct attendee status when changing between accepted and tentative, but it toggles the busyStatus. if ($is_outlook || $is_android) { $status = null; if ($data->busyStatus == self::BUSY_STATUS_BUSY) { $status = "ACCEPTED"; - } else if ($data->busyStatus == self::BUSY_STATUS_TENTATIVE) { + } elseif ($data->busyStatus == self::BUSY_STATUS_TENTATIVE) { $status = "TENTATIVE"; } if ($status) { $this->logger->debug("Updating our attendee status based on the busy status to {$status}."); $emails = $this->user_emails(); $this->find_and_update_attendee_status($attendees, $status, $emails); } } if (!$is_exception) { // Make sure the event has the organizer set if (!$organizer_email && ($identity = kolab_sync::get_instance()->user->get_identity())) { - $attendees[] = array( + $attendees[] = [ 'role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], - ); + ]; } // recurrence (and exceptions) $event['recurrence'] = $this->recurrence_to_kolab($data, $folderid, $timezone); } $event['attendees'] = $attendees; $event['categories'] = $categories; - $event['exceptions'] = isset($event['recurrence']['EXCEPTIONS']) ? $event['recurrence']['EXCEPTIONS'] : array(); + $event['exceptions'] = $event['recurrence']['EXCEPTIONS'] ?? []; // Bump SEQUENCE number on update (Outlook only). // It's been confirmed that any change of the event that has attendees specified // bumps SEQUENCE number of the event (we can see this in sent iTips). // Unfortunately Outlook also sends an update when no SEQUENCE bump // is needed, e.g. when updating attendee status. // We try our best to bump the SEQUENCE only when expected // @phpstan-ignore-next-line if (!empty($entry) && !$is_exception && !empty($data->attendees) && $data->timezone != $dummy_tz) { if ($last_update = $this->getKolabDataItem($event, self::KEY_DTSTAMP)) { $last_update = new DateTime($last_update); } if (!empty($data->dtStamp) && $data->dtStamp != $last_update) { if ($this->has_significant_changes($event, $entry)) { $event['sequence']++; $this->logger->debug('Found significant changes in the updated event. Bumping SEQUENCE to ' . $event['sequence']); } } } // Because we use last event modification time above, we make sure // the event modification time is not (re)set by the server, // we use the original Outlook's timestamp. if ($is_outlook && !empty($data->dtStamp)) { $this->setKolabDataItem($event, self::KEY_DTSTAMP, $data->dtStamp->format(DateTime::ATOM)); } // This prevents kolab_format code to bump the sequence when not needed if (!isset($event['sequence'])) { $event['sequence'] = 0; } return $event; } /** * Set attendee status for meeting * * @param Syncroton_Model_MeetingResponse $request The meeting response * * @return string ID of new calendar entry */ public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request) { - $status_map = array( + $status_map = [ 1 => 'ACCEPTED', 2 => 'TENTATIVE', 3 => 'DECLINED', - ); + ]; $status = $status_map[$request->userResponse] ?? null; if (empty($status)) { throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR); } // extract event from the invitation - list($event, $existing) = $this->get_event_from_invitation($request); -/* - switch ($status) { - case 'ACCEPTED': $event['free_busy'] = 'busy'; break; - case 'TENTATIVE': $event['free_busy'] = 'tentative'; break; - case 'DECLINED': $event['free_busy'] = 'free'; break; - } -*/ + [$event, $existing] = $this->get_event_from_invitation($request); + /* + switch ($status) { + case 'ACCEPTED': $event['free_busy'] = 'busy'; break; + case 'TENTATIVE': $event['free_busy'] = 'tentative'; break; + case 'DECLINED': $event['free_busy'] = 'free'; break; + } + */ // Store response timestamp for further use $reply_time = new DateTime('now', new DateTimeZone('UTC')); $this->setKolabDataItem($event, self::KEY_REPLYTIME, $reply_time->format('Ymd\THis\Z')); // Update/Save the event if (empty($existing)) { $folderId = $this->save_event($event, $status); // Create SyncState for the new event, so it is not synced twice if ($folderId) { try { $syncBackend = Syncroton_Registry::getSyncStateBackend(); $folderBackend = Syncroton_Registry::getFolderBackend(); $contentBackend = Syncroton_Registry::getContentStateBackend(); $syncFolder = $folderBackend->getFolder($this->device->id, $folderId); $syncState = $syncBackend->getSyncState($this->device->id, $syncFolder->id); - $contentBackend->create(new Syncroton_Model_Content(array( + $contentBackend->create(new Syncroton_Model_Content([ 'device_id' => $this->device->id, 'folder_id' => $syncFolder->id, 'contentid' => $this->serverId($event['uid'], $folderId), 'creation_time' => $syncState->lastsync, 'creation_synckey' => $syncState->counter, - ))); - } - catch (Exception $e) { + ])); + } catch (Exception $e) { // ignore } } - } - else { + } else { $folderId = $this->update_event($event, $existing, $status, $request->instanceId); } if (!$folderId) { throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR); } // TODO: ActiveSync version >= 16, send the iTip response. if (isset($request->sendResponse)) { // SendResponse can contain Body to use as email body (can be empty) // TODO: Activesync >= 16.1 proposedStartTime and proposedEndTime. } // FIXME: We should not return an UID when status=DECLINED // as it's expected by the specification. Server // should delete an event in such a case, but we // keep the event copy with appropriate attendee status instead. return $this->serverId($event['uid'], $folderId); } /** * Process an event from an iTip message - update the event in the recipient's calendar * * @param array $event Event data from the iTip * * @return string|null Attendee status from the iTip (self::ITIP_* constant value) */ public function processItipReply($event) { // FIXME: This does not prevent from spoofing, i.e. an iTip message // could be sent by anyone impersonating an organizer or attendee // FIXME: This will not work with Kolab delegation, as we do look // for the event instance in personal folders only (for now) // We also do not use SENT-BY,DELEGATED-TO,DELEGATED-FROM here at all. // FIXME: This is potential performance problem - we update an event // whenever we sync an email message. User can have multiple AC clients // or many iTip messages in INBOX. Should we remember which email was // already processed? // FIXME: Should we check SEQUENCE or something else to prevent // overwriting the attendee status with outdated status (on REPLY)? // Here we're handling CANCEL message, find the event (or occurrence) and remove it if ($event['_method'] == 'CANCEL') { // TODO: Performance: When we're going to delete the event we don't have to fetch it, // we just need to find that it exists and in which folder. if ($existing = $this->find_event_by_uid($event['uid'])) { // Note: Normally we'd just set the event status to canceled, but // ActiveSync clients do not understand that, we have to delete it if (!empty($event['recurrence_date'])) { // A single recurring event occurrence $rec_day = $event['recurrence_date']->format('Ymd'); // Remove the matching RDATE entry if (!empty($existing['recurrence']['RDATE'])) { foreach ($existing['recurrence']['RDATE'] as $j => $rdate) { if ($rdate->format('Ymd') == $rec_day) { unset($existing['recurrence']['RDATE'][$j]); break; } } } // Check EXDATE list, maybe already cancelled if (!empty($existing['recurrence']['EXDATE'])) { foreach ($existing['recurrence']['EXDATE'] as $j => $exdate) { if ($exdate->format('Ymd') == $rec_day) { return self::ITIP_CANCELLED; // skip update } } } else { $existing['recurrence']['EXDATE'] = []; } if (!isset($existing['exceptions'])) { $existing['exceptions'] = []; } if (!empty($existing['exceptions'])) { foreach ($existing['exceptions'] as $i => $exception) { if (libcalendaring::is_recurrence_exception($event, $exception)) { unset($existing['exceptions'][$i]); } } } // Add an exception to the master event $existing['recurrence']['EXDATE'][] = $event['recurrence_date']; // TODO: Handle errors $this->save_event($existing, null); - } - else { + } else { $folder = $this->backend->getFolder($existing['folderId'], $this->device->deviceid, $this->modelName); if ($folder && $folder->valid) { // TODO: Handle errors $folder->delete($event['uid']); } } } return self::ITIP_CANCELLED; } // Here we're handling REPLY message if (empty($event['attendees']) || $event['_method'] != 'REPLY') { return null; } $attendeeStatus = null; $attendeeEmail = null; // Get the attendee/status foreach ($event['attendees'] as $attendee) { if (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') { if (!empty($attendee['email']) && !empty($attendee['status'])) { // Per iTip spec. there should be only one (non-organizer) attendee here // FIXME: Verify is it realy the case with e.g. Kolab webmail, If not, we should // probably use the message sender from the From: header $attendeeStatus = strtoupper($attendee['status']); $attendeeEmail = $attendee['email']; break; } } } // Find the event (or occurrence) and update it if ($attendeeStatus && ($existing = $this->find_event_by_uid($event['uid']))) { // TODO: We should probably check the SEQUENCE to not reset status to an outdated value if (!empty($event['recurrence_date'])) { // A single recurring event occurrence // Find the exception entry, it should exist, if not ignore if (!empty($existing['exceptions'])) { foreach ($existing['exceptions'] as $i => $exception) { if (!empty($exception['attendees']) && libcalendaring::is_recurrence_exception($event, $exception)) { $attendees = &$existing['exceptions'][$i]['attendees']; break; } } } - } - else if (!empty($existing['attendees'])) { + } elseif (!empty($existing['attendees'])) { $attendees = &$existing['attendees']; } if (isset($attendees)) { $found = $this->find_and_update_attendee_status($attendees, $attendeeStatus, [$attendeeEmail], $changed); if ($found && $changed) { // TODO: error handling $this->save_event($existing, null); } } } return $attendeeStatus; } /** * Get an event from the invitation email or calendar folder */ protected function get_event_from_invitation(Syncroton_Model_MeetingResponse $request) { // Limitation: LongId might be used instead of RequestId, this is not supported if ($request->requestId) { $mail_class = new kolab_sync_data_email($this->device, $this->syncTimeStamp); // Event from an invitation email if ($event = $mail_class->get_invitation_event($request->requestId)) { // find the event in calendar $existing = $this->find_event_by_uid($event['uid']); - return array($event, $existing); + return [$event, $existing]; } // Event from calendar folder if ($event = $this->getObject($request->collectionId, $request->requestId)) { - return array($event, $event); + return [$event, $event]; } throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); } throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR); } /** * Find the Kolab event in any (of subscribed personal calendars) folder */ protected function find_event_by_uid($uid) { if (empty($uid)) { return; } // TODO: should we check every existing event folder even if not subscribed for sync? if ($folders = $this->listFolders()) { foreach ($folders as $_folder) { $folder = $this->backend->getFolder($_folder['serverId'], $this->device->deviceid, $this->modelName); if ($folder && $folder->get_namespace() == 'personal' && ($result = $this->backend->getItem($_folder['serverId'], $this->device->deviceid, $this->modelName, $uid)) ) { $result['folderId'] = $_folder['serverId']; return $result; } } } } /** * Wrapper to update an event object */ protected function update_event($event, $old, $status, $instanceId = null) { // TODO: instanceId - DateTime - of the exception to be processed, if not set process all occurrences if ($instanceId) { throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); } // A single recurring event occurrence if (!empty($event['recurrence_date'])) { $event['recurrence'] = []; if ($status) { $this->update_attendee_status($event, $status); $status = null; } if (!isset($old['exceptions'])) { $old['exceptions'] = []; } $existing = false; foreach ($old['exceptions'] as $i => $exception) { if (libcalendaring::is_recurrence_exception($event, $exception)) { $old['exceptions'][$i] = $event; $existing = true; } } // TODO: In case organizer first cancelled an occurrence and then invited // an attendee to the same date, and attendee accepts, we should remove EXDATE entry. // FIXME: We have to check with ActiveSync clients whether it is better // to have an exception with DECLINED attendee status, or an EXDATE entry if (!$existing) { $old['exceptions'][] = $event; } } // A main event update - else if (isset($event['sequence']) && $event['sequence'] > $old['sequence']) { + elseif (isset($event['sequence']) && $event['sequence'] > $old['sequence']) { // FIXME: Can we be smarter here? Should we update everything? What about e.g. new attendees? // And do we need to check the sequence? $props = ['start', 'end', 'title', 'description', 'location', 'free_busy']; foreach ($props as $prop) { if (isset($event[$prop])) { $old[$prop] = $event[$prop]; } } // Copy new custom properties if (!empty($event['x-custom'])) { foreach ($event['x-custom'] as $key => $val) { $old['x-custom'][$key] = $val; } } } // Updating an existing event is most-likely a response // to an iTip request with bumped SEQUENCE $old['sequence'] = ($old['sequence'] ?? 0) + 1; // Update the event return $this->save_event($old, $status); } /** * Save the Kolab event (create if not exist) * If an event does not exist it will be created in the default folder */ protected function save_event(&$event, $status = null) { $first = null; $default = null; if (!isset($event['folderId'])) { // Find the folder to which we'll save the event if ($folders = $this->listFolders()) { foreach ($folders as $_folder) { $folder = $this->backend->getFolder($_folder['serverId'], $this->device->deviceid, $this->modelName); if ($folder && $folder->get_namespace() == 'personal') { if ($_folder['type'] == 8) { $default = $_folder['serverId']; break; } if (!$first) { $first = $_folder['serverId']; } } } } // TODO: what if the user has no subscribed event folders for this device // should we use any existing event folder even if not subscribed for sync? } if ($status) { $this->update_attendee_status($event, $status); } // TODO: Free/busy trigger? $old_uid = isset($event['folderId']) ? $event['uid'] : null; $folder_id = $event['folderId'] ?? ($default ?? $first); $folder = $this->backend->getFolder($folder_id, $this->device->deviceid, $this->modelName); if (!empty($folder) && $folder->valid && $folder->save($event, $this->modelName, $old_uid)) { return $folder_id; } return false; } /** * Update the attendee status of the user matching $emails */ protected function find_and_update_attendee_status(&$attendees, $status, $emails, &$changed = false) { $found = false; foreach ((array) $attendees as $i => $attendee) { if (!empty($attendee['email']) && (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') && in_array_nocase($attendee['email'], $emails) ) { $changed = $changed || ($status != ($attendee['status'] ?? '')); $attendees[$i]['status'] = $status; $attendees[$i]['rsvp'] = false; $this->logger->debug('Updating existing attendee: ' . $attendee['email'] . ' status: ' . $status); $found = true; } } return $found; } /** * Update the attendee status of the user */ protected function update_attendee_status(&$event, $status) { $emails = $this->user_emails(); if (!$this->find_and_update_attendee_status($event['attendees'], $status, $emails)) { $this->logger->debug('Adding new attendee ' . $emails[0] . ' status: ' . $status); // Add the user to the attendees list - $event['attendees'][] = array( + $event['attendees'][] = [ 'role' => 'OPT-PARTICIPANT', 'name' => '', 'email' => $emails[0], 'status' => $status, 'rsvp' => false, - ); + ]; } } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { - $filter = array(array('type', '=', $this->modelName)); + $filter = [['type', '=', $this->modelName]]; switch ($filter_type) { - case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK: - $mod = '-2 weeks'; - break; - case Syncroton_Command_Sync::FILTER_1_MONTH_BACK: - $mod = '-1 month'; - break; - case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK: - $mod = '-3 months'; - break; - case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK: - $mod = '-6 months'; - break; + 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); + $filter[] = ['dtend', '>', $dt]; } return $filter; } /** * Set MeetingStatus according to event data */ protected function meeting_status_from_kolab($event, &$result) { // 0 - The event is an appointment, which has no attendees. // 1 - The event is a meeting and the user is the meeting organizer. // 3 - This event is a meeting, and the user is not the meeting organizer. // 5 - The meeting has been canceled and the user was the meeting organizer. // 7 - The meeting has been canceled. The user was not the meeting organizer. $status = 0; if (!empty($event['attendees'])) { // Find out if the user is an organizer // TODO: Delegation/aliases support $user_emails = $this->user_emails(); $is_organizer = false; if ($event['organizer'] && $event['organizer']['email']) { $is_organizer = in_array_nocase($event['organizer']['email'], $user_emails); } if ($event['status'] == 'CANCELLED') { $status = $is_organizer ? 5 : 7; - } - else { + } else { $status = $is_organizer ? 1 : 3; } } $result['meetingStatus'] = $status; } /** * Converts libkolab alarms spec. into a number of minutes */ protected function from_kolab_alarm($event) { if (isset($event['valarms'])) { foreach ($event['valarms'] as $alarm) { - if (in_array($alarm['action'], array('DISPLAY', 'AUDIO'))) { + if (in_array($alarm['action'], ['DISPLAY', 'AUDIO'])) { $value = $alarm['trigger']; break; } } } if (!empty($value) && $value instanceof DateTime) { if (!empty($event['start']) && ($interval = $event['start']->diff($value))) { if ($interval->invert && !$interval->m && !$interval->y) { - return intval(round($interval->s/60) + $interval->i + $interval->h * 60 + $interval->d * 60 * 24); + return intval(round($interval->s / 60) + $interval->i + $interval->h * 60 + $interval->d * 60 * 24); } } - } - else if (!empty($value) && preg_match('/^([-+]*)[PT]*([0-9]+)([WDHMS])$/', $value, $matches)) { + } elseif (!empty($value) && preg_match('/^([-+]*)[PT]*([0-9]+)([WDHMS])$/', $value, $matches)) { $value = intval($matches[2]); if ($value && $matches[1] != '-') { return null; } switch ($matches[3]) { - case 'S': $value = intval(round($value/60)); break; - case 'H': $value *= 60; break; - case 'D': $value *= 24 * 60; break; - case 'W': $value *= 7 * 24 * 60; break; + case 'S': $value = intval(round($value / 60)); + break; + case 'H': $value *= 60; + break; + case 'D': $value *= 24 * 60; + break; + case 'W': $value *= 7 * 24 * 60; + break; } return $value; } } /** * Converts ActiveSync reminder into libkolab alarms spec. */ protected function to_kolab_alarm($value, $event) { if ($value === null || $value === '') { - return isset($event['valarms']) ? (array) $event['valarms'] : array(); + return isset($event['valarms']) ? (array) $event['valarms'] : []; } - $valarms = array(); - $unsupported = array(); + $valarms = []; + $unsupported = []; if (!empty($event['valarms'])) { foreach ($event['valarms'] as $alarm) { - if (empty($current) && in_array($alarm['action'], array('DISPLAY', 'AUDIO'))) { + if (empty($current) && in_array($alarm['action'], ['DISPLAY', 'AUDIO'])) { $current = $alarm; - } - else { + } else { $unsupported[] = $alarm; } } } - $valarms[] = array( + $valarms[] = [ 'action' => !empty($current['action']) ? $current['action'] : 'DISPLAY', 'description' => !empty($current['description']) ? $current['description'] : '', 'trigger' => sprintf('-PT%dM', $value), - ); + ]; if (!empty($unsupported)) { $valarms = array_merge($valarms, $unsupported); } return $valarms; } /** * Sanity checks on event input * * @param Syncroton_Model_IEntry &$entry Entry object * * @throws Syncroton_Exception_Status_Sync */ protected function check_event(Syncroton_Model_IEntry &$entry) { // https://msdn.microsoft.com/en-us/library/jj194434(v=exchg.80).aspx $now = new DateTime('now'); $rounded = new DateTime('now'); $min = (int) $rounded->format('i'); $add = $min > 30 ? (60 - $min) : (30 - $min); $rounded->add(new DateInterval('PT' . $add . 'M')); if (empty($entry->startTime) && empty($entry->endTime)) { // use current time rounded to 30 minutes $end = clone $rounded; $end->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M')); $entry->startTime = $rounded; $entry->endTime = $end; - } - else if (empty($entry->startTime)) { + } elseif (empty($entry->startTime)) { if ($entry->endTime < $now || $entry->endTime < $rounded) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM); } $entry->startTime = $rounded; - } - else if (empty($entry->endTime)) { + } elseif (empty($entry->endTime)) { if ($entry->startTime < $now) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM); } $rounded->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M')); $entry->endTime = $rounded; } } /** * Check if the new event version has any significant changes */ protected function has_significant_changes($event, $old) { // Calendar namespace fields - foreach (array('allday', 'start', 'end', 'location', 'recurrence') as $key) { - if ((isset($event[$key]) ? $event[$key] : null) != (isset($old[$key]) ? $old[$key] : null)) { + foreach (['allday', 'start', 'end', 'location', 'recurrence'] as $key) { + if (($event[$key] ?? null) != ($old[$key] ?? null)) { // Comparing recurrence is tricky as there can be differences in default // value handling. Let's try to handle most common cases if ($key == 'recurrence' && $this->fixed_recurrence($event) == $this->fixed_recurrence($old)) { continue; } return true; } } if (count($event['attendees']) != count($old['attendees'])) { return true; } foreach ($event['attendees'] as $idx => $attendee) { $old_attendee = $old['attendees'][$idx]; if ($old_attendee['email'] != $attendee['email'] || ($attendee['role'] != 'ORGANIZER' && $attendee['status'] != $old_attendee['status'] && $attendee['status'] == 'NEEDS-ACTION') ) { return true; } } return false; } /** * Unify recurrence spec. for comparison */ protected function fixed_recurrence($event) { $rec = (array) $event['recurrence']; // Add BYDAY if not exists if ($rec['FREQ'] == 'WEEKLY' && empty($rec['BYDAY'])) { - $days = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; $day = $event['start']->format('w'); $rec['BYDAY'] = $days[$day]; } if (!$rec['INTERVAL']) { $rec['INTERVAL'] = 1; } ksort($rec); return $rec; } } diff --git a/lib/kolab_sync_data_contacts.php b/lib/kolab_sync_data_contacts.php index d538395..8da47c6 100644 --- a/lib/kolab_sync_data_contacts.php +++ b/lib/kolab_sync_data_contacts.php @@ -1,642 +1,640 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * COntacts data class for Syncroton */ class kolab_sync_data_contacts extends kolab_sync_data { /** * Mapping from ActiveSync Contacts namespace fields */ - protected $mapping = array( + protected $mapping = [ 'anniversary' => 'anniversary', 'assistantName' => 'assistant:0', //'assistantPhoneNumber' => 'assistantphonenumber', 'birthday' => 'birthday', 'body' => 'notes', 'businessAddressCity' => 'address.work.locality', 'businessAddressCountry' => 'address.work.country', 'businessAddressPostalCode' => 'address.work.code', 'businessAddressState' => 'address.work.region', 'businessAddressStreet' => 'address.work.street', 'businessFaxNumber' => 'phone.workfax.number', 'businessPhoneNumber' => 'phone.work.number', 'carPhoneNumber' => 'phone.car.number', //'categories' => 'categories', 'children' => 'children', 'companyName' => 'organization', 'department' => 'department', //'email1Address' => 'email:0', //'email2Address' => 'email:1', //'email3Address' => 'email:2', //'fileAs' => 'fileas', //@TODO: ? 'firstName' => 'firstname', //'home2PhoneNumber' => 'home2phonenumber', 'homeAddressCity' => 'address.home.locality', 'homeAddressCountry' => 'address.home.country', 'homeAddressPostalCode' => 'address.home.code', 'homeAddressState' => 'address.home.region', 'homeAddressStreet' => 'address.home.street', 'homeFaxNumber' => 'phone.homefax.number', 'homePhoneNumber' => 'phone.home.number', 'jobTitle' => 'jobtitle', 'lastName' => 'surname', 'middleName' => 'middlename', 'mobilePhoneNumber' => 'phone.mobile.number', //'officeLocation' => 'officelocation', 'otherAddressCity' => 'address.office.locality', 'otherAddressCountry' => 'address.office.country', 'otherAddressPostalCode' => 'address.office.code', 'otherAddressState' => 'address.office.region', 'otherAddressStreet' => 'address.office.street', 'pagerNumber' => 'phone.pager.number', 'picture' => 'photo', //'radioPhoneNumber' => 'radiophonenumber', //'rtf' => 'rtf', 'spouse' => 'spouse', 'suffix' => 'suffix', 'title' => 'prefix', 'webPage' => 'website.homepage.url', //'yomiCompanyName' => 'yomicompanyname', //'yomiFirstName' => 'yomifirstname', //'yomiLastName' => 'yomilastname', // Mapping from ActiveSync Contacts2 namespace fields //'accountName' => 'accountname', //'companyMainPhone' => 'companymainphone', //'customerId' => 'customerid', //'governmentId' => 'governmentid', 'iMAddress' => 'im:0', 'iMAddress2' => 'im:1', 'iMAddress3' => 'im:2', 'managerName' => 'manager:0', //'mMS' => 'mms', 'nickName' => 'nickname', - ); + ]; /** * Kolab object type * * @var string */ protected $modelName = 'contact'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'Contacts'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; /** * Identifier of special Global Address List folder * * @var string */ protected $galFolder = 'GAL'; /** * Name of special Global Address List folder * * @var string */ protected $galFolderName = 'Global Address Book'; protected $galPrefix = 'GAL:'; protected $galSources; protected $galResult; protected $galCache; /** * Creates model object * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * * @return array|Syncroton_Model_Contact Contact object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId) { $data = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId); - $result = array(); + $result = []; if (empty($data)) { throw new Syncroton_Exception_NotFound("Contact $serverId not found"); } // Contacts namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getKolabDataItem($data, $name); switch ($name) { - case 'photo': - if ($value) { - // ActiveSync limits photo size to 48KB (of base64 encoded string) - if (strlen($value) * 1.33 > 48 * 1024) { - continue 2; + case 'photo': + if ($value) { + // ActiveSync limits photo size to 48KB (of base64 encoded string) + if (strlen($value) * 1.33 > 48 * 1024) { + continue 2; + } } - } - break; + break; - case 'birthday': - case 'anniversary': - $value = self::date_from_kolab($value); - break; + case 'birthday': + case 'anniversary': + $value = self::date_from_kolab($value); + break; - case 'notes': - $value = $this->body_from_kolab($value, $collection); - break; + case 'notes': + $value = $this->body_from_kolab($value, $collection); + break; } if (empty($value) || is_array($value)) { continue; } $result[$key] = $value; } // email address(es): email1Address, email2Address, email3Address - for ($x=0; $x<3; $x++) { + for ($x = 0; $x < 3; $x++) { if (!empty($data['email'][$x])) { $email = $data['email'][$x]; if (is_array($email)) { $email = $email['address']; } if ($email) { - $result['email' . ($x+1) . 'Address'] = $email; + $result['email' . ($x + 1) . 'Address'] = $email; } } } return new Syncroton_Model_Contact($result); } /** * convert contact from xml to libkolab array * * @param Syncroton_Model_Contact $data Contact to convert * @param string $folderId Folder identifier * @param array $entry Existing entry * * @return array Kolab object array */ public function toKolab($data, $folderId, $entry = null) { - $contact = !empty($entry) ? $entry : array(); + $contact = !empty($entry) ? $entry : []; // Contacts namespace fields foreach ($this->mapping as $key => $name) { $value = $data->$key; switch ($name) { - case 'address.work.street': - if (strtolower($this->device->devicetype) == 'palm') { - // palm pre sends the whole address in the tag - $value = null; - } - break; + case 'address.work.street': + if (strtolower($this->device->devicetype) == 'palm') { + // palm pre sends the whole address in the tag + $value = null; + } + break; - case 'website.homepage.url': - // remove facebook urls - if (preg_match('/^fb:\/\//', $value)) { - $value = null; - } - break; + case 'website.homepage.url': + // remove facebook urls + if (preg_match('/^fb:\/\//', $value)) { + $value = null; + } + break; - case 'notes': - $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT); - // If note isn't specified keep old note - if ($value === null) { - continue 2; - } - break; + case 'notes': + $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT); + // If note isn't specified keep old note + if ($value === null) { + continue 2; + } + break; - case 'photo': - // If photo isn't specified keep old photo - if ($value === null) { - continue 2; - } - break; - - case 'birthday': - case 'anniversary': - if ($value) { - // convert date to string format, so libkolab will store - // it with no time and timezone what could be incorrectly re-calculated (#2555) - $value = $value->format('Y-m-d'); - } - break; + case 'photo': + // If photo isn't specified keep old photo + if ($value === null) { + continue 2; + } + break; + + case 'birthday': + case 'anniversary': + if ($value) { + // convert date to string format, so libkolab will store + // it with no time and timezone what could be incorrectly re-calculated (#2555) + $value = $value->format('Y-m-d'); + } + break; } $this->setKolabDataItem($contact, $name, $value); } // email address(es): email1Address, email2Address, email3Address - $emails = array(); - for ($x=0; $x<3; $x++) { - $key = 'email' . ($x+1) . 'Address'; + $emails = []; + for ($x = 0; $x < 3; $x++) { + $key = 'email' . ($x + 1) . 'Address'; if ($value = $data->$key) { // Android sends email address as: Lars Kneschke if (preg_match('/(.*)<(.+@[^@]+)>/', $value, $matches)) { $value = trim($matches[2]); } // sanitize email address, it can contain broken (non-unicode) characters (#3287) $value = rcube_charset::clean($value); // try to find address type, at least we can do this if // address wasn't changed $type = ''; foreach ((array)$contact['email'] as $email) { if ($email['address'] == $value) { $type = $email['type']; } } - $emails[] = array('address' => $value, 'type' => $type); + $emails[] = ['address' => $value, 'type' => $type]; } } $contact['email'] = $emails; return $contact; } /** * Return list of supported folders for this backend * * @return array */ public function getAllFolders() { $list = parent::getAllFolders(); if ($this->isMultiFolder() && $this->hasGAL()) { - $list[$this->galFolder] = new Syncroton_Model_Folder(array( + $list[$this->galFolder] = new Syncroton_Model_Folder([ 'displayName' => $this->galFolderName, // @TODO: localization? 'serverId' => $this->galFolder, 'parentId' => 0, 'type' => 14, - )); + ]); } return $list; } /** * Updates a folder */ public function updateFolder(Syncroton_Model_IFolder $folder) { if ($folder->serverId === $this->galFolder && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Updating GAL folder is not possible"); } return parent::updateFolder($folder); } /** * Deletes a folder */ public function deleteFolder($folder) { if ($folder instanceof Syncroton_Model_IFolder) { $folder = $folder->serverId; } if ($folder === $this->galFolder && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Deleting GAL folder is not possible"); } return parent::deleteFolder($folder); } /** * Empty folder (remove all entries and optionally subfolders) * * @param string $folderid Folder identifier * @param array $options Options */ public function emptyFolderContents($folderid, $options) { if ($folderid === $this->galFolder && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Emptying GAL folder is not possible"); } return parent::emptyFolderContents($folderid, $options); } /** * Moves object into another location (folder) * * @param string $srcFolderId Source folder identifier * @param string $serverId Object identifier * @param string $dstFolderId Destination folder identifier * * @throws Syncroton_Exception_Status * @return string New object identifier */ public function moveItem($srcFolderId, $serverId, $dstFolderId) { if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Moving GAL entries is not possible"); } if ($srcFolderId === $this->galFolder && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Moving/Deleting GAL entries is not possible"); } if ($dstFolderId === $this->galFolder && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Creating GAL entries is not possible"); } return parent::moveItem($srcFolderId, $serverId, $dstFolderId); } /** * Add entry * * @param string $folderId Folder identifier * @param Syncroton_Model_IEntry $entry Entry object * * @return string ID of the created entry */ public function createEntry($folderId, Syncroton_Model_IEntry $entry) { if ($folderId === $this->galFolder && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Creating GAL entries is not possible"); } return parent::createEntry($folderId, $entry); } /** * update existing entry * * @param string $folderId * @param string $serverId * @param Syncroton_Model_IEntry $entry * * @return string ID of the updated entry */ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) { if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Updating GAL entries is not possible"); } return parent::updateEntry($folderId, $serverId, $entry); } /** * Delete an entry * * @param string $folderId * @param string $serverId * @param ?Syncroton_Model_SyncCollection $collectionData */ public function deleteEntry($folderId, $serverId, $collectionData = null) { if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) { throw new Syncroton_Exception_AccessDenied("Deleting GAL entries is not possible"); } return parent::deleteEntry($folderId, $serverId, $collectionData); } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { // specify object type, contact folders in Kolab might // contain also ditribution-list objects, we'll skip them - return array(array('type', '=', $this->modelName)); + return [['type', '=', $this->modelName]]; } /** * Check if GAL synchronization is enabled for current device */ protected function hasGAL() { return count($this->getGALSources()); } /** * Search for existing entries * * @param string $folderid Folder identifier * @param array $filter Search filter * @param int $result_type Type of the result (see RESULT_* constants) * * @return array|int Search result as count or array of uids/objects */ - protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID) + protected function searchEntries($folderid, $filter = [], $result_type = self::RESULT_UID) { // GAL Folder exists, return result from LDAP only if ($folderid === $this->galFolder && $this->hasGAL()) { return $this->searchGALEntries($filter, $result_type); } $result = parent::searchEntries($folderid, $filter, $result_type); // Merge results from LDAP if ($this->hasGAL() && !$this->isMultiFolder()) { $gal_result = $this->searchGALEntries($filter, $result_type); if ($result_type == self::RESULT_COUNT) { $result += $gal_result; - } - else { + } else { $result = array_merge($result, $gal_result); } } return $result; } /** * Fetches the entry from the backend */ protected function getObject($folderid, $entryid) { if (strpos($entryid, $this->galPrefix) === 0 && $this->hasGAL()) { return $this->getGALEntry($entryid); } return parent::getObject($folderid, $entryid); } /** * Search for existing LDAP entries * * @param array $filter Search filter * @param int $result_type Type of the result (see RESULT_* constants) * * @return array|int Search result as count or array of uids/objects */ protected function searchGALEntries($filter, $result_type) { // For GAL we don't check for changes. // When something changed a new UID will be generated so the update // will be done as delete + create foreach ($filter as $f) { if ($f[0] == 'changed') { - return $result_type == self::RESULT_COUNT ? 0 : array(); + return $result_type == self::RESULT_COUNT ? 0 : []; } } if ($this->galCache && ($result = $this->galCache->get('index')) !== null) { $result = explode("\n", $result); return $result_type == self::RESULT_COUNT ? count($result) : $result; } - $result = array(); + $result = []; foreach ($this->getGALSources() as $source) { if ($book = kolab_sync_data_gal::get_address_book($source['id'])) { $book->reset(); $book->set_page(1); $book->set_pagesize(10000); $set = $book->list_records(); foreach ($set as $contact) { $result[] = $this->createGALEntryUID($contact, $source['id']); } } } if ($this->galCache) { $this->galCache->set('index', implode("\n", $result)); } return $result_type == self::RESULT_COUNT ? count($result) : $result; } /** * Return specified LDAP entry * * @param string $serverId Entry identifier * * @return array|null Contact data */ protected function getGALEntry($serverId) { - list($source, $timestamp, $uid) = $this->resolveGALEntryUID($serverId); + [$source, $timestamp, $uid] = $this->resolveGALEntryUID($serverId); if ($source && $uid && ($book = kolab_sync_data_gal::get_address_book($source))) { $book->reset(); - $set = $book->search('uid', array($uid), rcube_addressbook::SEARCH_STRICT, true, true); + $set = $book->search('uid', [$uid], rcube_addressbook::SEARCH_STRICT, true, true); $result = $set->first(); if ($result['uid'] == $uid && $result['changed'] == $timestamp) { // As in kolab_sync_data_gal we use only one email address if (empty($result['email'])) { $emails = $book->get_col_values('email', $result, true); - $result['email'] = array($emails[0]); + $result['email'] = [$emails[0]]; } return $result; } } return null; } /** * Return LDAP address books list * * @return array Address books array */ protected function getGALSources() { if ($this->galSources === null) { $rcube = rcube::get_instance(); $gal_sync = $rcube->config->get('activesync_gal_sync'); $enabled = false; if ($gal_sync === true) { $enabled = true; - } - else if (is_array($gal_sync)) { + } elseif (is_array($gal_sync)) { $enabled = $this->deviceTypeFilter($gal_sync); } - $this->galSources = $enabled ? kolab_sync_data_gal::get_address_sources() : array(); + $this->galSources = $enabled ? kolab_sync_data_gal::get_address_sources() : []; if ($cache_type = $rcube->config->get('activesync_gal_cache', 'db')) { $cache_ttl = $rcube->config->get('activesync_gal_cache_ttl', '1d'); $this->galCache = $rcube->get_cache('activesync_gal', $cache_type, $cache_ttl, false); // expunge cache every now and then if (rand(0, 10) === 0) { $this->galCache->expunge(); } } } return $this->galSources; } /** * Builds contact identifier from contact data and source id */ protected function createGALEntryUID($contact, $source_id) { return $this->galPrefix . sprintf('%s:%s:%s', rcube_ldap::dn_encode($source_id), $contact['changed'], $contact['uid']); } /** * Extracts contact identification data from contact identifier */ protected function resolveGALEntryUID($uid) { if (strpos($uid, $this->galPrefix) === 0) { $items = explode(':', substr($uid, strlen($this->galPrefix))); $items[0] = rcube_ldap::dn_decode($items[0]); return $items; // source, timestamp, uid } - return array(); + return []; } } diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php index 5f5a0f6..8893072 100644 --- a/lib/kolab_sync_data_email.php +++ b/lib/kolab_sync_data_email.php @@ -1,1584 +1,1565 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Email data class for Syncroton */ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_IDataSearch { - const MAX_SEARCH_RESULT = 200; + public const MAX_SEARCH_RESULT = 200; /** * Mapping from ActiveSync Email namespace fields */ - protected $mapping = array( + protected $mapping = [ 'cc' => 'cc', //'contentClass' => 'contentclass', 'dateReceived' => 'internaldate', //'displayTo' => 'displayto', //? //'flag' => 'flag', 'from' => 'from', //'importance' => 'importance', 'internetCPID' => 'charset', //'messageClass' => 'messageclass', 'replyTo' => 'replyto', //'read' => 'read', 'subject' => 'subject', //'threadTopic' => 'threadtopic', 'to' => 'to', - ); + ]; - static $memory_accumulated = 0; + public static $memory_accumulated = 0; /** * Special folder type/name map * * @var array */ - protected $folder_types = array( + protected $folder_types = [ 2 => 'Inbox', 3 => 'Drafts', 4 => 'Deleted Items', 5 => 'Sent Items', 6 => 'Outbox', - ); + ]; /** * Kolab object type * * @var string */ protected $modelName = 'mail'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_INBOX; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'INBOX'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED; protected $storage; /** * the constructor * * @param Syncroton_Model_IDevice $device * @param DateTime $syncTimeStamp */ public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp) { parent::__construct($device, $syncTimeStamp); $this->storage = rcube::get_instance()->get_storage(); // Outlook 2013 support multi-folder $this->ext_devices[] = 'windowsoutlook15'; } /** * Encode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3 * * @param array $data An array with the data to encode * * @return string the encoded globalObjId */ public static function encodeGlobalObjId(array $data): string { $classid = "040000008200e00074c5b7101a82e008"; if (!empty($data['data'])) { $payload = $data['data']; } else { $uid = $data['uid']; $payload = "vCal-Uid\1\0\0\0{$uid}\0"; } $packed = pack( "H32nCCPx8Va*", $classid, $data['year'] ?? 0, $data['month'] ?? 0, $data['day'] ?? 0, $data['now'] ?? 0, strlen($payload), $payload ); return base64_encode($packed); } /** * Decode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3 * * @param string $globalObjId The encoded globalObjId * * @return array An array with the decoded data */ public static function decodeGlobalObjId(string $globalObjId): array { $unpackString = 'H32classid/nyear/Cmonth/Cday/Pnow/x8/Vbytecount/a*data'; $decoded = unpack($unpackString, base64_decode($globalObjId)); $decoded['uid'] = substr($decoded['data'], strlen("vCal-Uid\1\0\0\0"), -1); return $decoded; } /** * Creates model object * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * * @return Syncroton_Model_Email Email object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId) { $message = $this->getObject($serverId); // error (message doesn't exist?) if (empty($message)) { throw new Syncroton_Exception_NotFound("Message $serverId not found"); } $headers = $message->headers; // rcube_message_header $this->storage->set_folder($message->folder); $this->logger->debug(sprintf("Processing message %s (size: %.2f MB)", $serverId, $headers->size / 1024 / 1024)); // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = null; switch ($name) { - case 'internaldate': - $value = self::date_from_kolab(rcube_utils::strtotime($headers->internaldate)); - break; + case 'internaldate': + $value = self::date_from_kolab(rcube_utils::strtotime($headers->internaldate)); + break; - case 'cc': - case 'to': - case 'replyto': - case 'from': - $addresses = rcube_mime::decode_address_list($headers->$name, null, true, $headers->charset); + case 'cc': + case 'to': + case 'replyto': + case 'from': + $addresses = rcube_mime::decode_address_list($headers->$name, null, true, $headers->charset); - foreach ($addresses as $idx => $part) { - // @FIXME: set name + address or address only? - $addresses[$idx] = format_email_recipient($part['mailto'], $part['name']); - } + foreach ($addresses as $idx => $part) { + // @FIXME: set name + address or address only? + $addresses[$idx] = format_email_recipient($part['mailto'], $part['name']); + } - $value = implode(',', $addresses); - break; + $value = implode(',', $addresses); + break; - case 'subject': - $value = $headers->get('subject'); - break; + case 'subject': + $value = $headers->get('subject'); + break; - case 'charset': - $value = self::charset_to_cp($headers->charset); - break; + case 'charset': + $value = self::charset_to_cp($headers->charset); + break; } if (empty($value) || is_array($value)) { continue; } if (is_string($value)) { $value = rcube_charset::clean($value); } $result[$key] = $value; } -// $result['ConversationId'] = 'FF68022058BD485996BE15F6F6D99320'; -// $result['ConversationIndex'] = 'CA2CFA8A23'; + // $result['ConversationId'] = 'FF68022058BD485996BE15F6F6D99320'; + // $result['ConversationIndex'] = 'CA2CFA8A23'; // Read flag $result['read'] = intval(!empty($headers->flags['SEEN'])); // Flagged message if (!empty($headers->flags['FLAGGED'])) { // Use FollowUp flag which is used in Android when message is marked with a star - $result['flag'] = new Syncroton_Model_EmailFlag(array( + $result['flag'] = new Syncroton_Model_EmailFlag([ 'flagType' => 'FollowUp', 'status' => Syncroton_Model_EmailFlag::STATUS_ACTIVE, - )); + ]); } else { $result['flag'] = new Syncroton_Model_EmailFlag(); } // Importance/Priority if ($headers->priority) { if ($headers->priority < 3) { $result['importance'] = 2; // High - } - else if ($headers->priority > 3) { - $result['importance'] = 0; // Low + } elseif ($headers->priority > 3) { + $result['importance'] = 0; // Low } } // get truncation and body type $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT; $truncateAt = null; $opts = $collection->options; $prefs = $opts['bodyPreferences']; if ($opts['mimeSupport'] == Syncroton_Command_Sync::MIMESUPPORT_SEND_MIME) { $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_MIME; if (isset($prefs[Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize'])) { $truncateAt = $prefs[Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize']; - } - else if (isset($opts['mimeTruncation']) && $opts['mimeTruncation'] < Syncroton_Command_Sync::TRUNCATE_NOTHING) { + } elseif (isset($opts['mimeTruncation']) && $opts['mimeTruncation'] < Syncroton_Command_Sync::TRUNCATE_NOTHING) { switch ($opts['mimeTruncation']) { - case Syncroton_Command_Sync::TRUNCATE_ALL: - $truncateAt = 0; - break; - case Syncroton_Command_Sync::TRUNCATE_4096: - $truncateAt = 4096; - break; - case Syncroton_Command_Sync::TRUNCATE_5120: - $truncateAt = 5120; - break; - case Syncroton_Command_Sync::TRUNCATE_7168: - $truncateAt = 7168; - break; - case Syncroton_Command_Sync::TRUNCATE_10240: - $truncateAt = 10240; - break; - case Syncroton_Command_Sync::TRUNCATE_20480: - $truncateAt = 20480; - break; - case Syncroton_Command_Sync::TRUNCATE_51200: - $truncateAt = 51200; - break; - case Syncroton_Command_Sync::TRUNCATE_102400: - $truncateAt = 102400; - break; + case Syncroton_Command_Sync::TRUNCATE_ALL: + $truncateAt = 0; + break; + case Syncroton_Command_Sync::TRUNCATE_4096: + $truncateAt = 4096; + break; + case Syncroton_Command_Sync::TRUNCATE_5120: + $truncateAt = 5120; + break; + case Syncroton_Command_Sync::TRUNCATE_7168: + $truncateAt = 7168; + break; + case Syncroton_Command_Sync::TRUNCATE_10240: + $truncateAt = 10240; + break; + case Syncroton_Command_Sync::TRUNCATE_20480: + $truncateAt = 20480; + break; + case Syncroton_Command_Sync::TRUNCATE_51200: + $truncateAt = 51200; + break; + case Syncroton_Command_Sync::TRUNCATE_102400: + $truncateAt = 102400; + break; } } - } - else { + } else { // The spec is not very clear, but it looks that if MimeSupport is not set // we can't add Syncroton_Command_Sync::BODY_TYPE_MIME to the supported types // list below (Bug #1688) - $types = array( + $types = [ Syncroton_Command_Sync::BODY_TYPE_HTML, Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT, - ); + ]; // @TODO: if client can support both HTML and TEXT use one of // them which is better according to the real message body type foreach ($types as $type) { if (!empty($prefs[$type])) { if (!empty($prefs[$type]['truncationSize'])) { $truncateAt = $prefs[$type]['truncationSize']; } - $preview = (int) (isset($prefs[$type]['preview']) ? $prefs[$type]['preview'] : 0); + $preview = (int) ($prefs[$type]['preview'] ?? 0); $airSyncBaseType = $type; break; } } } - $body_params = array('type' => $airSyncBaseType); + $body_params = ['type' => $airSyncBaseType]; // Message body // In Sync examples there's one in which bodyPreferences is not defined // in such case Truncated=1 and there's no body sent to the client // only it's estimated size $isTruncated = 0; if (empty($prefs)) { $messageBody = ''; $real_length = $headers->size; $truncateAt = 0; $body_length = 0; $isTruncated = 1; - } - else if ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) { + } elseif ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) { // Check if we have enough memory to handle the message $messageBody = $this->message_mem_check($message, $headers->size); static::$memory_accumulated += $headers->size; if (empty($messageBody)) { $messageBody = $this->storage->get_raw_body($message->uid); } // make the source safe (Bug #2715, #2757) $messageBody = kolab_sync_message::recode_message($messageBody); // strip out any non utf-8 characters $messageBody = rcube_charset::clean($messageBody); $real_length = $body_length = strlen($messageBody); - } - else { + } else { $messageBody = $this->getMessageBody($message, $airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_HTML); // strip out any non utf-8 characters $messageBody = rcube_charset::clean($messageBody); $real_length = $body_length = strlen($messageBody); } // add Preview element to the Body result if (!empty($preview) && $body_length) { $body_params['preview'] = $this->getPreview($messageBody, $airSyncBaseType, $preview); } // truncate the body if needed if ($truncateAt && $body_length > $truncateAt) { $messageBody = mb_strcut($messageBody, 0, $truncateAt); $body_length = strlen($messageBody); $isTruncated = 1; } if ($isTruncated) { $body_params['truncated'] = 1; $body_params['estimatedDataSize'] = $real_length; } // add Body element to the result $result['body'] = $this->setBody($messageBody, $body_params); // original body type // @TODO: get this value from getMessageBody() $result['nativeBodyType'] = $message->has_html_part() ? 2 : 1; // Message class $result['messageClass'] = 'IPM.Note'; $result['contentClass'] = 'urn:content-classes:message'; if ($headers->ctype == 'multipart/signed' && !empty($message->parts[1]) && $message->parts[1]->mimetype == 'application/pkcs7-signature' ) { $result['messageClass'] = 'IPM.Note.SMIME.MultipartSigned'; - } - else if ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') { + } elseif ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') { $result['messageClass'] = 'IPM.Note.SMIME'; - } - else if ($event = $this->get_invitation_event_from_message($message)) { + } elseif ($event = $this->get_invitation_event_from_message($message)) { // Note: Depending on MessageClass a client will display a proper set of buttons // Either Accept/Maybe/Decline (REQUEST), or "Remove from Calendar" (CANCEL) or none (REPLY). $result['messageClass'] = 'IPM.Schedule.Meeting.Request'; $result['contentClass'] = 'urn:content-classes:calendarmessage'; - $meeting = array(); + $meeting = []; $meeting['allDayEvent'] = $event['allday'] ?? null ? 1 : 0; $meeting['startTime'] = self::date_from_kolab($event['start']); $meeting['dtStamp'] = self::date_from_kolab($event['dtstamp'] ?? null); $meeting['endTime'] = self::date_from_kolab($event['end'] ?? null); $meeting['location'] = $event['location'] ?? null; $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_NORMAL; if (!empty($event['recurrence_date'])) { $meeting['recurrenceId'] = self::date_from_kolab($event['recurrence_date']); if (!empty($event['status']) && $event['status'] == 'CANCELLED') { $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_EXCEPTION; } else { $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_SINGLE; } - } else if (!empty($event['recurrence'])) { + } elseif (!empty($event['recurrence'])) { $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_MASTER; // TODO: MeetingRequest recurrence is different that the one in Calendar // $this->recurrence_from_kolab($collection, $event, $meeting); } // Organizer if (!empty($event['attendees'])) { foreach ($event['attendees'] as $attendee) { if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER' && !empty($attendee['email'])) { $meeting['organizer'] = $attendee['email']; break; } } } // Current time as a number of 100-nanosecond units since 1601-01-01 $fileTime = ($event['start']->getTimestamp() + 11644473600) * 10000000; // Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows // only one timezone per-event. We'll use timezone of the start date $meeting['timeZone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']); $meeting['globalObjId'] = self::encodeGlobalObjId([ 'uid' => $event['uid'], 'year' => intval($event['start']->format('Y')), 'month' => intval($event['start']->format('n')), 'day' => intval($event['start']->format('j')), 'now' => $fileTime, ]); if ($event['_method'] == 'REQUEST') { $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_REQUEST; // Some clients (iOS) without this flag do not send the invitation reply to the organizer. // Note: Microsoft says "the value of the ResponseRequested element comes from the PARTSTAT // parameter value of "NEEDS-ACTION" in the request". I think it is safe to do this for all requests. // Note: This does not have impact on the existence of Accept/Decline buttons in the client. $meeting['responseRequested'] = 1; } else { // REPLY or CANCEL $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_NORMAL; $itip_processing = kolab_sync::get_instance()->config->get('activesync_itip_processing'); $attendeeStatus = null; if ($itip_processing && empty($headers->flags['SEEN'])) { // Optionally process the message and update the event in recipient's calendar // Warning: Only for development purposes, for now it's better to use wallace $calendar_class = new kolab_sync_data_calendar($this->device, $this->syncTimeStamp); $attendeeStatus = $calendar_class->processItipReply($event); - } - else if ($event['_method'] == 'CANCEL') { + } elseif ($event['_method'] == 'CANCEL') { $attendeeStatus = kolab_sync_data_calendar::ITIP_CANCELLED; - } - else if (!empty($event['attendees'])) { + } elseif (!empty($event['attendees'])) { // Get the attendee/status in the REPLY foreach ($event['attendees'] as $attendee) { if (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') { if (!empty($attendee['email']) && !empty($attendee['status'])) { // Per iTip spec. there should be only one (non-organizer) attendee here // FIXME: Verify is it realy the case with e.g. Kolab webmail, If not, we should // probably use the message sender from the From: header $attendeeStatus = strtoupper($attendee['status']); break; } } } } switch ($attendeeStatus) { - case kolab_sync_data_calendar::ITIP_CANCELLED: - $result['messageClass'] = 'IPM.Schedule.Meeting.Canceled'; - break; - case kolab_sync_data_calendar::ITIP_DECLINED: - $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Neg'; - break; - case kolab_sync_data_calendar::ITIP_TENTATIVE: - $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Tent'; - break; - case kolab_sync_data_calendar::ITIP_ACCEPTED: - $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Pos'; - break; - default: - $skipRequest = true; + case kolab_sync_data_calendar::ITIP_CANCELLED: + $result['messageClass'] = 'IPM.Schedule.Meeting.Canceled'; + break; + case kolab_sync_data_calendar::ITIP_DECLINED: + $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Neg'; + break; + case kolab_sync_data_calendar::ITIP_TENTATIVE: + $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Tent'; + break; + case kolab_sync_data_calendar::ITIP_ACCEPTED: + $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Pos'; + break; + default: + $skipRequest = true; } } // New time proposals aren't supported by Kolab. // This disables the UI elements related to this on the client side $meeting['disallowNewTimeProposal'] = 1; if (empty($skipRequest)) { $result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting); } } // Categories (Tags) $result['categories'] = $message->headers->others['categories'] ?? []; $is_ios = preg_match('/(iphone|ipad)/i', $this->device->devicetype); // attachments $attachments = array_merge($message->attachments, $message->inline_parts); if (!empty($attachments)) { - $result['attachments'] = array(); + $result['attachments'] = []; foreach ($attachments as $attachment) { - $att = array(); + $att = []; if ($is_ios && !empty($event) && $attachment->mime_id == $event['_mime_id']) { continue; } $filename = rcube_charset::clean($attachment->filename); if (empty($filename) && $attachment->mimetype == 'text/html') { $filename = 'HTML Part'; } $att['displayName'] = $filename; $att['fileReference'] = $serverId . '::' . $attachment->mime_id; $att['method'] = 1; $att['estimatedDataSize'] = $attachment->size; if (!empty($attachment->content_id)) { $att['contentId'] = rcube_charset::clean($attachment->content_id); } if (!empty($attachment->content_location)) { $att['contentLocation'] = rcube_charset::clean($attachment->content_location); } if (in_array($attachment, $message->inline_parts)) { $att['isInline'] = 1; } $result['attachments'][] = new Syncroton_Model_EmailAttachment($att); } } return new Syncroton_Model_Email($result); } /** * Returns properties of a message for Search response * * @param string $longId Message identifier * @param array $options Search options * * @return Syncroton_Model_Email Email object */ public function getSearchEntry($longId, $options) { - $collection = new Syncroton_Model_SyncCollection(array( + $collection = new Syncroton_Model_SyncCollection([ 'options' => $options, - )); + ]); return $this->getEntry($collection, $longId); } /** * convert email from xml to libkolab array * * @param Syncroton_Model_Email $data Email to convert * @param string $folderid Folder identifier * @param array $entry Existing entry * * @return array */ public function toKolab($data, $folderid, $entry = null) { // does nothing => you can't add emails via ActiveSync return []; } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { - $filter = array(); + $filter = []; switch ($filter_type) { - case Syncroton_Command_Sync::FILTER_1_DAY_BACK: - $mod = '-1 day'; - break; - case Syncroton_Command_Sync::FILTER_3_DAYS_BACK: - $mod = '-3 days'; - break; - case Syncroton_Command_Sync::FILTER_1_WEEK_BACK: - $mod = '-1 week'; - break; - case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK: - $mod = '-2 weeks'; - break; - case Syncroton_Command_Sync::FILTER_1_MONTH_BACK: - $mod = '-1 month'; - break; + case Syncroton_Command_Sync::FILTER_1_DAY_BACK: + $mod = '-1 day'; + break; + case Syncroton_Command_Sync::FILTER_3_DAYS_BACK: + $mod = '-3 days'; + break; + case Syncroton_Command_Sync::FILTER_1_WEEK_BACK: + $mod = '-1 week'; + break; + case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK: + $mod = '-2 weeks'; + break; + case Syncroton_Command_Sync::FILTER_1_MONTH_BACK: + $mod = '-1 month'; + break; } if (!empty($mod)) { $dt = new DateTime('now', new DateTimeZone('UTC')); $dt->modify($mod); // RFC3501: IMAP SEARCH $filter[] = 'SINCE ' . $dt->format('d-M-Y'); } return $filter; } /** * Return list of supported folders for this backend * * @return array */ public function getAllFolders() { $list = $this->listFolders(); if (!is_array($list)) { throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR); } // device doesn't support multiple folders if (!$this->isMultiFolder()) { // We'll return max. one folder of supported type - $result = array(); + $result = []; $types = $this->folder_types; foreach ($list as $idx => $folder) { $type = $folder['type'] == 12 ? 2 : $folder['type']; // unknown to Inbox if ($folder_id = $types[$type]) { - $result[$folder_id] = array( + $result[$folder_id] = [ 'displayName' => $folder_id, 'serverId' => $folder_id, 'parentId' => 0, 'type' => $type, - ); + ]; } } $list = $result; } foreach ($list as $idx => $folder) { $list[$idx] = new Syncroton_Model_Folder($folder); } return $list; } /** * Return list of folders for specified folder ID * * @param string $folder_id Folder identifier * * @return array Folder identifiers list */ protected function extractFolders($folder_id) { $list = $this->listFolders(); - $result = array(); + $result = []; if (!is_array($list)) { throw new Syncroton_Exception_NotFound('Folder not found'); } // device supports multiple folders? if ($this->isMultiFolder()) { if ($list[$folder_id]) { $result[] = $folder_id; } - } - else if ($type = array_search($folder_id, $this->folder_types)) { + } elseif ($type = array_search($folder_id, $this->folder_types)) { foreach ($list as $id => $folder) { if ($folder['type'] == $type || ($folder_id == 'Inbox' && $folder['type'] == 12)) { $result[] = $id; } } } if (empty($result)) { throw new Syncroton_Exception_NotFound('Folder not found'); } return $result; } /** * Moves object into another location (folder) * * @param string $srcFolderId Source folder identifier * @param string $serverId Object identifier * @param string $dstFolderId Destination folder identifier * * @throws Syncroton_Exception_Status * @return string New object identifier */ public function moveItem($srcFolderId, $serverId, $dstFolderId) { $msg = $this->parseMessageId($serverId); $dest = $this->extractFolders($dstFolderId); $dest_id = array_shift($dest); if (empty($msg)) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } $uid = $this->backend->moveItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], $dest_id); return $uid ? $this->serverId($uid, $dest_id) : null; } /** * add entry from xml data * * @param string $folderId Folder identifier * @param Syncroton_Model_IEntry $entry Entry * * @return string ID of the created entry */ public function createEntry($folderId, Syncroton_Model_IEntry $entry) { $params = ['flags' => [!empty($entry->read) ? 'SEEN' : 'UNSEEN']]; $uid = $this->backend->createItem($folderId, $this->device->deviceid, $this->modelName, $entry->body->data, $params); if (!$uid) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } return $this->serverId($uid, $folderId); } /** * Update existing message * * @param string $folderId Folder identifier * @param string $serverId Entry identifier * @param Syncroton_Model_IEntry $entry Entry */ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) { $msg = $this->parseMessageId($serverId); if (empty($msg)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } $params = ['flags' => []]; if (isset($entry->categories)) { $params['categories'] = $entry->categories; } // Read status change if (isset($entry->read)) { $params['flags'][] = !empty($entry->read) ? 'SEEN' : 'UNSEEN'; } // Flag change if (isset($entry->flag)) { if (empty($entry->flag) || empty($entry->flag->flagType)) { $params['flags'][] = 'UNFLAGGED'; - } - else if (preg_match('/follow\s*up/i', $entry->flag->flagType)) { + } elseif (preg_match('/follow\s*up/i', $entry->flag->flagType)) { $params['flags'][] = 'FLAGGED'; } } $this->backend->updateItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], null, $params); return $serverId; } /** * Delete an email (or move to Trash) * * @param string $folderId * @param string $serverId * @param ?Syncroton_Model_SyncCollection $collection */ public function deleteEntry($folderId, $serverId, $collection = null) { $trash = kolab_sync::get_instance()->config->get('trash_mbox'); $msg = $this->parseMessageId($serverId); if (empty($msg)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } // Note: If DeletesAsMoves is not specified in the request, its default is 1 (true). $moveToTrash = !isset($collection->deletesAsMoves) || !empty($collection->deletesAsMoves); $deleted = $this->backend->deleteItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], $moveToTrash); if (!$deleted) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } } /** * Send an email * * @param mixed $message MIME message * @param boolean $saveInSent Enables saving the sent message in Sent folder * * @throws Syncroton_Exception_Status */ public function sendEmail($message, $saveInSent) { if (!($message instanceof kolab_sync_message)) { $message = new kolab_sync_message($message); } $sent = $message->send($smtp_error); if (!$sent) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAIL_SUBMISSION_FAILED); } // Save sent message in Sent folder if ($saveInSent) { $sent_folder = kolab_sync::get_instance()->config->get('sent_mbox'); if (strlen($sent_folder) && $this->storage->folder_exists($sent_folder)) { - return $this->storage->save_message($sent_folder, $message->source(), '', false, array('SEEN')); + return $this->storage->save_message($sent_folder, $message->source(), '', false, ['SEEN']); } } } /** * Forward an email * * @param array|string $itemId A string LongId or an array with following properties: * collectionId, itemId and instanceId * @param resource|string $body MIME message * @param boolean $saveInSent Enables saving the sent message in Sent folder * @param boolean $replaceMime If enabled, original message would be appended * * @throws Syncroton_Exception_Status */ public function forwardEmail($itemId, $body, $saveInSent, $replaceMime) { /* @TODO: The SmartForward command can be applied to a meeting. When SmartForward is applied to a recurring meeting, the InstanceId element (section 2.2.3.83.2) specifies the ID of a particular occurrence in the recurring meeting. If SmartForward is applied to a recurring meeting and the InstanceId element is absent, the server SHOULD forward the entire recurring meeting. If the value of the InstanceId element is invalid, the server responds with Status element (section 2.2.3.162.15) value 104, as specified in section 2.2.4. When the SmartForward command is used for an appointment, the original message is included by the server as an attachment to the outgoing message. When the SmartForward command is used for a normal message or a meeting, the behavior of the SmartForward command is the same as that of the SmartReply command (section 2.2.2.18). */ $msg = $this->parseMessageId($itemId); $message = $this->getObject($itemId); if (empty($message)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND); } // Parse message $sync_msg = new kolab_sync_message($body); // forward original message as attachment if (!$replaceMime) { $this->storage->set_folder($message->folder); $attachment = $this->storage->get_raw_body($msg['uid']); if (empty($attachment)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND); } - $sync_msg->add_attachment($attachment, array( + $sync_msg->add_attachment($attachment, [ 'encoding' => '8bit', 'content_type' => 'message/rfc822', 'disposition' => 'inline', //'name' => 'message.eml', - )); + ]); } // Send message $this->sendEmail($sync_msg, $saveInSent); // Set FORWARDED flag on the replied message if (empty($message->headers->flags['FORWARDED'])) { $params = ['flags' => ['FORWARDED']]; $this->backend->updateItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], null, $params); } } /** * Reply to an email * * @param array|string $itemId A string LongId or an array with following properties: * collectionId, itemId and instanceId * @param resource|string $body MIME message * @param boolean $saveInSent Enables saving the sent message in Sent folder * @param boolean $replaceMime If enabled, original message would be appended * * @throws Syncroton_Exception_Status */ public function replyEmail($itemId, $body, $saveInSent, $replaceMime) { $msg = $this->parseMessageId($itemId); $message = $this->getObject($itemId); if (empty($message)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND); } $sync_msg = new kolab_sync_message($body); $headers = $sync_msg->headers(); // Add References header if (empty($headers['References'])) { $sync_msg->set_header('References', trim($message->headers->references . ' ' . $message->headers->messageID)); } // Get original message body if (!$replaceMime) { // @TODO: here we're assuming that reply message is in text/plain format // So, original message will be converted to plain text if needed $message_body = $this->getMessageBody($message, false); // Quote original message body $message_body = self::wrap_and_quote(trim($message_body), 72); // Join bodies $sync_msg->append("\n" . ltrim($message_body)); } // Send message $this->sendEmail($sync_msg, $saveInSent); // Set ANSWERED flag on the replied message if (empty($message->headers->flags['ANSWERED'])) { $params = ['flags' => ['ANSWERED']]; $this->backend->updateItem($msg['folderId'], $this->device->deviceid, $this->modelName, $msg['uid'], null, $params); } } /** * ActiveSync Search handler * * @param Syncroton_Model_StoreRequest $store Search query * * @return Syncroton_Model_StoreResponse Complete Search response */ public function search(Syncroton_Model_StoreRequest $store) { - list($folders, $search_str) = $this->parse_search_query($store); + [$folders, $search_str] = $this->parse_search_query($store); if (empty($search_str)) { throw new Exception('Empty/invalid search request'); } if (!is_array($folders)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } - $result = array(); + $result = []; // @TODO: caching with Options->RebuildResults support foreach ($folders as $folderid) { $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); if ($foldername === null) { continue; } -// $this->storage->set_folder($foldername); -// $this->storage->folder_sync($foldername); + // $this->storage->set_folder($foldername); + // $this->storage->folder_sync($foldername); $search = $this->storage->search_once($foldername, $search_str); if (!($search instanceof rcube_result_index)) { continue; } $uids = $search->get(); foreach ($uids as $idx => $uid) { - $uids[$idx] = new Syncroton_Model_StoreResponseResult(array( + $uids[$idx] = new Syncroton_Model_StoreResponseResult([ 'longId' => $this->serverId($uid, $folderid), 'collectionId' => $folderid, 'class' => 'Email', - )); + ]); } $result = array_merge($result, $uids); // We don't want to search all folders if we've got already a lot messages if (count($result) >= self::MAX_SEARCH_RESULT) { break; } } $result = array_values($result); $response = new Syncroton_Model_StoreResponse(); // Calculate requested range $start = (int) $store->options['range'][0]; $limit = (int) $store->options['range'][1] + 1; $total = count($result); $response->total = $total; // Get requested chunk of data set if ($total) { if ($start > $total) { $start = $total; } if ($limit > $total) { - $limit = max($start+1, $total); + $limit = max($start + 1, $total); } if ($start > 0 || $limit < $total) { - $result = array_slice($result, $start, $limit-$start); + $result = array_slice($result, $start, $limit - $start); } - $response->range = array($start, $start + count($result) - 1); + $response->range = [$start, $start + count($result) - 1]; } // Build result array, convert to ActiveSync format foreach ($result as $idx => $rec) { $rec->properties = $this->getSearchEntry($rec->longId, $store->options); $response->result[] = $rec; unset($result[$idx]); } return $response; } /** * Converts ActiveSync search parameters into IMAP search string */ protected function parse_search_query($store) { $options = $store->options; $query = $store->query; $search_str = ''; - $folders = array(); + $folders = []; if (empty($query) || !is_array($query)) { - return array(); + return []; } if (!empty($query['and']['collections'])) { foreach ($query['and']['collections'] as $collection) { $folders = array_merge($folders, $this->extractFolders($collection)); } } if (!empty($query['and']['greaterThan']) && !empty($query['and']['greaterThan']['dateReceived']) && !empty($query['and']['greaterThan']['value']) ) { $search_str .= ' SINCE ' . $query['and']['greaterThan']['value']->format('d-M-Y'); } if (!empty($query['and']['lessThan']) && !empty($query['and']['lessThan']['dateReceived']) && !empty($query['and']['lessThan']['value']) ) { $search_str .= ' BEFORE ' . $query['and']['lessThan']['value']->format('d-M-Y'); } if (isset($query['and']['freeText']) && strlen($query['and']['freeText'])) { // @FIXME: Should we use TEXT/BODY search? ActiveSync protocol specification says "indexed fields" $search = $query['and']['freeText']; - $search_keys = array('SUBJECT', 'TO', 'FROM', 'CC'); - $search_str .= str_repeat(' OR', count($search_keys)-1); + $search_keys = ['SUBJECT', 'TO', 'FROM', 'CC']; + $search_str .= str_repeat(' OR', count($search_keys) - 1); foreach ($search_keys as $key) { $search_str .= sprintf(" %s {%d}\r\n%s", $key, strlen($search), $search); } } if (!strlen($search_str)) { - return array(); + return []; } $search_str = 'ALL UNDELETED ' . trim($search_str); // @TODO: DeepTraversal if (empty($folders)) { $folders = $this->listFolders(); if (!is_array($folders)) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $folders = array_keys($folders); } - return array($folders, $search_str); + return [$folders, $search_str]; } /** * Fetches the entry from the backend */ protected function getObject($entryid, $dummy = null) { $message = $this->parseMessageId($entryid); if (empty($message)) { // @TODO: exception? return null; } return $this->backend->getItem($message['folderId'], $this->device->deviceid, $this->modelName, $message['uid']); } /** * Get attachment data from the server. * * @param string $fileReference * * @return Syncroton_Model_FileReference */ public function getFileReference($fileReference) { - list($folderid, $uid, $part_id) = explode('::', $fileReference); + [$folderid, $uid, $part_id] = explode('::', $fileReference); $message = $this->getObject($fileReference); if (!$message) { throw new Syncroton_Exception_NotFound('Message not found'); } $part = $message->mime_parts[$part_id]; $body = $message->get_part_body($part_id); - return new Syncroton_Model_FileReference(array( + return new Syncroton_Model_FileReference([ 'contentType' => $part->mimetype, 'data' => $body, - )); + ]); } /** * Parses entry ID to get folder name and UID of the message */ protected function parseMessageId($entryid) { // replyEmail/forwardEmail if (is_array($entryid)) { $entryid = $entryid['itemId']; } if (!is_string($entryid) || !strpos($entryid, '::')) { return; } // Note: the id might be in a form of ::[::] - list($folderid, $uid) = explode('::', $entryid); + [$folderid, $uid] = explode('::', $entryid); return [ 'uid' => $uid, 'folderId' => $folderid, ]; } /** * Creates entry ID of the message */ protected function serverId($uid, $folderid) { return $folderid . '::' . $uid; } /** * Returns body of the message in specified format * * @param rcube_message $message * @param bool $html */ protected function getMessageBody($message, $html = false) { if (!is_array($message->parts) && empty($message->body)) { return ''; } if (!empty($message->parts)) { foreach ($message->parts as $part) { // skip no-content and attachment parts (#1488557) if ($part->type != 'content' || !$part->size || $message->is_attachment($part)) { continue; } return $this->getMessagePartBody($message, $part, $html); } } return $this->getMessagePartBody($message, $message, $html); } /** * Returns body of the message part in specified format * * @param rcube_message $message * @param rcube_message_part $part * @param bool $html */ protected function getMessagePartBody($message, $part, $html = false) { if (empty($part->size) || empty($part->mime_id)) { // TODO: Throw an exception? return ''; } // Check if we have enough memory to handle the message in it $body = $this->message_mem_check($message, $part->size, false); if ($body !== false) { $body = $message->get_part_body($part->mime_id, true); } // message is cached but not exists, or other error if ($body === false) { return ''; } $ctype_secondary = !empty($part->ctype_secondary) ? $part->ctype_secondary : null; if ($html) { if ($ctype_secondary == 'html') { // charset was converted to UTF-8 in rcube_storage::get_message_part(), // change/add charset specification in HTML accordingly - $meta = ''; + $meta = ''; // remove old meta tag and add the new one, making sure // that it is placed in the head $body = preg_replace('/]+charset=[a-z0-9-_]+[^>]*>/Ui', '', $body); - $body = preg_replace('/(]*>)/Ui', '\\1'.$meta, $body, -1, $rcount); + $body = preg_replace('/(]*>)/Ui', '\\1' . $meta, $body, -1, $rcount); if (!$rcount) { $body = '' . $meta . '' . $body; } - } - else if ($ctype_secondary == 'enriched') { + } elseif ($ctype_secondary == 'enriched') { $body = rcube_enriched::to_html($body); - } - else { + } else { // Roundcube >= 1.2 if (class_exists('rcube_text2html')) { $flowed = isset($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed'; $delsp = isset($part->ctype_parameters['delsp']) && $part->ctype_parameters['delsp'] == 'yes'; - $options = array('flowed' => $flowed, 'wrap' => false, 'delsp' => $delsp); + $options = ['flowed' => $flowed, 'wrap' => false, 'delsp' => $delsp]; $text2html = new rcube_text2html($body, false, $options); $body = '' . $text2html->get_html() . ''; - } - else { + } else { $body = '
' . $body . '
'; } } - } - else { + } else { if ($ctype_secondary == 'enriched') { $body = rcube_enriched::to_html($body); $part->ctype_secondary = 'html'; } if ($ctype_secondary == 'html') { $txt = new rcube_html2text($body, false, true); $body = $txt->get_text(); - } - else { + } else { if ($ctype_secondary == 'plain' && !empty($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed' ) { $body = rcube_mime::unfold_flowed($body); } } } return $body; } /** * Converts and truncates message body for use in * * @return string Truncated plain text message */ protected function getPreview($body, $type, $size) { if ($type == Syncroton_Command_Sync::BODY_TYPE_HTML) { $txt = new rcube_html2text($body, false, true); $body = $txt->get_text(); } // size limit defined in ActiveSync protocol if ($size > 255) { $size = 255; } return mb_strcut(trim($body), 0, $size); } public static function charset_to_cp($charset) { // @TODO: ????? // The body is converted to utf-8 in get_part_body(), what about headers? return 65001; // UTF-8 /* $aliases = array( 'asmo708' => 708, 'shiftjis' => 932, 'gb2312' => 936, 'ksc56011987' => 949, 'big5' => 950, 'utf16' => 1200, 'utf16le' => 1200, 'unicodefffe' => 1201, 'utf16be' => 1201, 'johab' => 1361, 'macintosh' => 10000, 'macjapanese' => 10001, 'macchinesetrad' => 10002, 'mackorean' => 10003, 'macarabic' => 10004, 'machebrew' => 10005, 'macgreek' => 10006, 'maccyrillic' => 10007, 'macchinesesimp' => 10008, 'macromanian' => 10010, 'macukrainian' => 10017, 'macthai' => 10021, 'macce' => 10029, 'macicelandic' => 10079, 'macturkish' => 10081, 'maccroatian' => 10082, 'utf32' => 12000, 'utf32be' => 12001, 'chinesecns' => 20000, 'chineseeten' => 20002, 'ia5' => 20105, 'ia5german' => 20106, 'ia5swedish' => 20107, 'ia5norwegian' => 20108, 'usascii' => 20127, 'ibm273' => 20273, 'ibm277' => 20277, 'ibm278' => 20278, 'ibm280' => 20280, 'ibm284' => 20284, 'ibm285' => 20285, 'ibm290' => 20290, 'ibm297' => 20297, 'ibm420' => 20420, 'ibm423' => 20423, 'ibm424' => 20424, 'ebcdickoreanextended' => 20833, 'ibmthai' => 20838, 'koi8r' => 20866, 'ibm871' => 20871, 'ibm880' => 20880, 'ibm905' => 20905, 'ibm00924' => 20924, 'cp1025' => 21025, 'koi8u' => 21866, 'iso88591' => 28591, 'iso88592' => 28592, 'iso88593' => 28593, 'iso88594' => 28594, 'iso88595' => 28595, 'iso88596' => 28596, 'iso88597' => 28597, 'iso88598' => 28598, 'iso88599' => 28599, 'iso885913' => 28603, 'iso885915' => 28605, 'xeuropa' => 29001, 'iso88598i' => 38598, 'iso2022jp' => 50220, 'csiso2022jp' => 50221, 'iso2022jp' => 50222, 'iso2022kr' => 50225, 'eucjp' => 51932, 'euccn' => 51936, 'euckr' => 51949, 'hzgb2312' => 52936, 'gb18030' => 54936, 'isciide' => 57002, 'isciibe' => 57003, 'isciita' => 57004, 'isciite' => 57005, 'isciias' => 57006, 'isciior' => 57007, 'isciika' => 57008, 'isciima' => 57009, 'isciigu' => 57010, 'isciipa' => 57011, 'utf7' => 65000, 'utf8' => 65001, ); $charset = strtolower($charset); $charset = preg_replace(array('/^x-/', '/[^a-z0-9]/'), '', $charset); if (isset($aliases[$charset])) { return $aliases[$charset]; } if (preg_match('/^(ibm|dos|cp|windows|win)[0-9]+/', $charset, $m)) { return substr($charset, strlen($m[1]) + 1); } */ } /** * Wrap text to a given number of characters per line * but respect the mail quotation of replies messages (>). * Finally add another quotation level by prepending the lines * with > * * @param string $text Text to wrap * @param int $length The line width * * @return string The wrapped text */ protected static function wrap_and_quote($text, $length = 72) { // Function stolen from Roundcube ;) // Rebuild the message body with a maximum of $max chars, while keeping quoted message. $max = min(77, $length + 8); $lines = preg_split('/\r?\n/', trim($text)); $out = ''; foreach ($lines as $line) { // don't wrap already quoted lines if (isset($line[0]) && $line[0] == '>') { $line = '>' . rtrim($line); - } - else if (mb_strlen($line) > $max) { + } elseif (mb_strlen($line) > $max) { $newline = ''; foreach (explode("\n", rcube_mime::wordwrap($line, $length - 2)) as $l) { if (strlen($l)) { $newline .= '> ' . $l . "\n"; - } - else { + } else { $newline .= ">\n"; } } $line = rtrim($newline); - } - else { + } else { $line = '> ' . $line; } // Append the line $out .= $line . "\n"; } return $out; } /** * Returns calendar event data from the iTip invitation attached to a mail message */ public function get_invitation_event_from_message($message) { // Parse the message and find iTip attachments $libcal = libcalendaring::get_instance(); - $libcal->mail_message_load(array('object' => $message)); + $libcal->mail_message_load(['object' => $message]); $ical_objects = $libcal->get_mail_ical_objects(); // Skip methods we do not support here if (!in_array($ical_objects->method, ['REQUEST', 'CANCEL', 'REPLY'])) { return null; } // We support only one event in the iTip foreach ($ical_objects as $mime_id => $event) { if ($event['_type'] == 'event') { $event['_method'] = $ical_objects->method; $event['_mime_id'] = $ical_objects->mime_id; return $event; } } return null; } /** * Returns calendar event data from the iTip invitation attached to a mail message */ public function get_invitation_event($messageId) { // Get the mail message object if ($message = $this->getObject($messageId)) { return $this->get_invitation_event_from_message($message); } return null; } private function mem_check($need) { $mem_limit = (int) parse_bytes(ini_get('memory_limit')); $memory = static::$memory_accumulated; // @phpstan-ignore-next-line return ($mem_limit > 0 && $memory + $need > $mem_limit) ? false : true; } /** * Checks if the message can be processed, depending on its size and * memory_limit, otherwise throws an exception or returns fake body. */ protected function message_mem_check($message, $size, $result = null) { static $memory_rised; // @FIXME: we need up to 5x more memory than the body // Note: Biggest memory multiplication happens in recode_message() // and the Syncroton engine (which also does not support passing bodies // as streams). It also happens when parsing the plain/html text body // in getMessagePartBody() though the footprint there is probably lower. if (!$this->mem_check($size * 5)) { // If we already rised the memory we throw an exception, so the message // will be synchronized in the next run (then we might have enough memory) if ($memory_rised) { - throw new Syncroton_Exception_MemoryExhausted; + throw new Syncroton_Exception_MemoryExhausted(); } $memory_rised = true; $memory_max = 512; // maximum in MB $memory_limit = round(parse_bytes(ini_get('memory_limit')) / 1024 / 1024); // current limit (in MB) $memory_add = round($size * 5 / 1024 / 1024); // how much we need (in MB) $memory_needed = min($memory_limit + $memory_add, $memory_max) . "M"; if ($memory_limit < $memory_max) { $this->logger->debug("Setting memory_limit=$memory_needed"); if (ini_set('memory_limit', $memory_needed) !== false) { // Memory has been rised, check again // @phpstan-ignore-next-line if ($this->mem_check($size * 5)) { return; } } } $this->logger->warn("Not enough memory. Using fake email body."); if ($result !== null) { return $result; } // Let's return a fake message. If we return an empty body Outlook // will not list the message at all. This way user can do something // with the message (flag, delete, move) and see the reason why it's fake // and importantly see its subject, sender, etc. // TODO: Localization? $msg = "This message is too large for ActiveSync."; // $msg .= "See https://kb.kolabenterprise.com/documentation/some-place for more information."; // Get original message headers $headers = $this->storage->get_raw_headers($message->uid); // Build a fake message with original headers, but changed body return kolab_sync_message::fake_message($headers, $msg); } } } diff --git a/lib/kolab_sync_data_gal.php b/lib/kolab_sync_data_gal.php index 385d99e..a798696 100644 --- a/lib/kolab_sync_data_gal.php +++ b/lib/kolab_sync_data_gal.php @@ -1,395 +1,399 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * GAL (Global Address List) data backend for Syncroton */ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDataSearch { - const MAX_SEARCH_RESULT = 100; + public const MAX_SEARCH_RESULT = 100; /** * LDAP search result * * @var array */ - protected $result = array(); + protected $result = []; /** * LDAP address books list * * @var array */ - public static $address_books = array(); + public static $address_books = []; /** * Mapping from ActiveSync Contacts namespace fields */ - protected $mapping = array( + protected $mapping = [ 'alias' => 'nickname', 'company' => 'organization', 'displayName' => 'name', 'emailAddress' => 'email', 'firstName' => 'firstname', 'lastName' => 'surname', 'mobilePhone' => 'phone.mobile', 'office' => 'office', 'picture' => 'photo', 'phone' => 'phone', 'title' => 'jobtitle', - ); + ]; /** * Kolab object type * * @var string */ protected $modelName = 'contact'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'Contacts'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; /** * the constructor * * @param Syncroton_Model_IDevice $device * @param DateTime $syncTimeStamp */ public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp) { parent::__construct($device, $syncTimeStamp); // Use configured fields mapping $rcube = rcube::get_instance(); $fieldmap = (array) $rcube->config->get('activesync_gal_fieldmap'); if (!empty($fieldmap)) { $fieldmap = array_intersect_key($fieldmap, array_keys($this->mapping)); $this->mapping = array_merge($this->mapping, $fieldmap); } } /** * Not used but required by parent class */ public function toKolab($data, $folderId, $entry = null) { return []; } /** * Not used but required by parent class * * @return array|Syncroton_Model_Contact Contact object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId) { throw new Syncroton_Exception_NotFound("getEntry() on GAL is not implemented"); } /** * Returns properties of a contact for Search response * * @param array $data Contact data * @param array $options Search options * * @return Syncroton_Model_GAL Contact (GAL) object */ public function getSearchEntry($data, $options) { - $result = array(); + $result = []; // Contacts namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getLDAPDataItem($data, $name); if (empty($value) || is_array($value)) { continue; } switch ($name) { - case 'photo': - // @TODO: MaxPictures option - // ActiveSync limits photo size of GAL contact to 100KB - $maxsize = 102400; - if (!empty($options['picture']['maxSize'])) { - $maxsize = min($maxsize, $options['picture']['maxSize']); - } - - if (strlen($value) > $maxsize) { - continue 2; - } - - $value = new Syncroton_Model_GALPicture(array( - 'data' => $value, // binary - 'status' => Syncroton_Model_GALPicture::STATUS_SUCCESS, - )); - - break; + case 'photo': + // @TODO: MaxPictures option + // ActiveSync limits photo size of GAL contact to 100KB + $maxsize = 102400; + if (!empty($options['picture']['maxSize'])) { + $maxsize = min($maxsize, $options['picture']['maxSize']); + } + + if (strlen($value) > $maxsize) { + continue 2; + } + + $value = new Syncroton_Model_GALPicture([ + 'data' => $value, // binary + 'status' => Syncroton_Model_GALPicture::STATUS_SUCCESS, + ]); + + break; } $result[$key] = $value; } return new Syncroton_Model_GAL($result); } /** * ActiveSync Search handler * * @param Syncroton_Model_StoreRequest $store Search query parameters * * @return Syncroton_Model_StoreResponse Complete Search response * @throws Exception */ public function search(Syncroton_Model_StoreRequest $store) { $options = $store->options; $query = $store->query; if (empty($query) || !is_string($query)) { throw new Exception('Empty/invalid search request'); } - $records = array(); + $records = []; $rcube = rcube::get_instance(); // @TODO: caching with Options->RebuildResults support $books = self::get_address_sources(); $mode = 2; // use prefix mode $fields = $rcube->config->get('contactlist_fields'); if (empty($fields)) { $fields = '*'; } foreach ($books as $idx => $book) { $book = self::get_address_book($idx); if (!$book) { continue; } $book->set_page(1); $book->set_pagesize(self::MAX_SEARCH_RESULT); $result = $book->search($fields, $query, $mode, true, true, 'email'); if (!$result->count) { continue; } // get records $result = $book->list_records(); foreach ($result as $row) { $row['sourceid'] = $idx; // make sure 'email' item is there, convert all email:* into one $row['email'] = $book->get_col_values('email', $row, true); $key = $this->contact_key($row); unset($row['_raw_attrib']); // save some memory, @TODO: do this in rcube_ldap $records[$key] = $row; } // We don't want to search all sources if we've got already a lot of contacts if (count($records) >= self::MAX_SEARCH_RESULT) { break; } } // sort the records ksort($records, SORT_LOCALE_STRING); $records = array_values($records); $response = new Syncroton_Model_StoreResponse(); // Calculate requested range $start = (int) $options['range'][0]; $limit = (int) $options['range'][1] + 1; $total = count($records); $response->total = $total; // Get requested chunk of data set if ($total) { if ($start > $total) { $start = $total; } if ($limit > $total) { - $limit = max($start+1, $total); + $limit = max($start + 1, $total); } if ($start > 0 || $limit < $total) { - $records = array_slice($records, $start, $limit-$start); + $records = array_slice($records, $start, $limit - $start); } - $response->range = array($start, $start + count($records) - 1); + $response->range = [$start, $start + count($records) - 1]; } // Build result array, convert to ActiveSync format foreach ($records as $idx => $rec) { - $response->result[] = new Syncroton_Model_StoreResponseResult(array( + $response->result[] = new Syncroton_Model_StoreResponseResult([ 'longId' => $rec['ID'], 'properties' => $this->getSearchEntry($rec, $options), - )); + ]); unset($records[$idx]); } return $response; } /** * Return instance of the internal address book class * * @param string $id Address book identifier * * @return rcube_addressbook|null Address book object */ public static function get_address_book($id) { $config = rcube::get_instance()->config; $ldap_config = (array) $config->get('ldap_public'); // use existing instance if (isset(self::$address_books[$id]) && (self::$address_books[$id] instanceof rcube_addressbook)) { $book = self::$address_books[$id]; - } - else if ($id && $ldap_config[$id]) { - $book = new rcube_ldap($ldap_config[$id], $config->get('ldap_debug'), - $config->mail_domain($_SESSION['storage_host'])); + } elseif ($id && $ldap_config[$id]) { + $book = new rcube_ldap( + $ldap_config[$id], + $config->get('ldap_debug'), + $config->mail_domain($_SESSION['storage_host']) + ); } if (empty($book)) { - rcube::raise_error(array( + rcube::raise_error( + [ 'code' => 700, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Addressbook source ($id) not found!"), - true, false); + 'message' => "Addressbook source ($id) not found!"], + true, + false + ); return null; } -/* - // set configured sort order - if ($sort_col = $this->config->get('addressbook_sort_col')) - $book->set_sort_order($sort_col); -*/ + /* + // set configured sort order + if ($sort_col = $this->config->get('addressbook_sort_col')) + $book->set_sort_order($sort_col); + */ // add to the 'books' array for shutdown function self::$address_books[$id] = $book; return $book; } /** * Return LDAP address books list * * @return array Address books array */ public static function get_address_sources() { $config = rcube::get_instance()->config; $ldap_config = (array) $config->get('ldap_public'); $async_books = $config->get('activesync_addressbooks'); if ($async_books === null) { $async_books = (array) $config->get('autocomplete_addressbooks'); } - $list = array(); + $list = []; foreach ((array)$async_books as $id) { $prop = $ldap_config[$id]; if (!empty($prop) && is_array($prop)) { - $list[$id] = array( + $list[$id] = [ 'id' => $id, 'name' => $prop['name'], - ); + ]; } } return $list; } /** * Creates contact key for sorting by */ protected function contact_key($row) { $key = $row['name'] . ':' . $row['sourceid']; // add email to a key to not skip contacts with the same name if (!empty($row['email'])) { if (is_array($row['email'])) { $key .= ':' . implode(':', $row['email']); - } - else { + } else { $key .= ':' . $row['email']; } } return $key; } /** * Extracts data from Roundcube LDAP data array */ protected function getLDAPDataItem($data, $name) { - list($name, $index) = explode(':', $name); + [$name, $index] = explode(':', $name); $name = str_replace('.', ':', $name); if (isset($data[$name])) { if ($index) { return is_array($data[$name]) ? $data[$name][$index] : null; } return is_array($data[$name]) ? array_shift($data[$name]) : $data[$name]; } return null; } } diff --git a/lib/kolab_sync_data_notes.php b/lib/kolab_sync_data_notes.php index dbaa4b9..7db6e8c 100644 --- a/lib/kolab_sync_data_notes.php +++ b/lib/kolab_sync_data_notes.php @@ -1,148 +1,148 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Notes data class for Syncroton */ class kolab_sync_data_notes extends kolab_sync_data { /** * Mapping from ActiveSync Calendar namespace fields */ - protected $mapping = array( + protected $mapping = [ 'body' => 'description', 'categories' => 'categories', 'lastModifiedDate' => 'changed', //'messageClass' => 'messageClass', 'subject' => 'title', - ); + ]; /** * Kolab object type * * @var string */ protected $modelName = 'note'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_NOTE; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'Notes'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED; /** * Appends note data to xml element * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * @param boolean $as_array Return entry as an array * * @return array|Syncroton_Model_Note|array Note object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false) { $note = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId); - $result = array(); + $result = []; // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getKolabDataItem($note, $name); switch ($name) { - case 'changed': - $value = self::date_from_kolab($value); - break; + case 'changed': + $value = self::date_from_kolab($value); + break; - case 'description': - $value = $this->body_from_kolab($value, $collection); - break; + case 'description': + $value = $this->body_from_kolab($value, $collection); + break; } if (empty($value) || is_array($value)) { continue; } $result[$key] = $value; } $result['messageClass'] = 'IPM.StickyNote'; return $as_array ? $result : new Syncroton_Model_Note($result); } /** * convert note from xml to libkolab array * * @param Syncroton_Model_Note $data Note to convert * @param string $folderid Folder identifier * @param array $entry Existing entry * * @return array */ public function toKolab($data, $folderid, $entry = null) { - $note = !empty($entry) ? $entry : array(); + $note = !empty($entry) ? $entry : []; // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = $data->$key; switch ($name) { - case 'description': - $supported_body_types = array( - Syncroton_Model_EmailBody::TYPE_HTML, - Syncroton_Model_EmailBody::TYPE_PLAINTEXT, - ); - $value = $this->getBody($value, $supported_body_types); - - // If description isn't specified keep old description - if ($value === null) { - continue 2; - } - break; + case 'description': + $supported_body_types = [ + Syncroton_Model_EmailBody::TYPE_HTML, + Syncroton_Model_EmailBody::TYPE_PLAINTEXT, + ]; + $value = $this->getBody($value, $supported_body_types); + + // If description isn't specified keep old description + if ($value === null) { + continue 2; + } + break; } $this->setKolabDataItem($note, $name, $value); } return $note; } } diff --git a/lib/kolab_sync_data_tasks.php b/lib/kolab_sync_data_tasks.php index 20850e6..e3c1a40 100644 --- a/lib/kolab_sync_data_tasks.php +++ b/lib/kolab_sync_data_tasks.php @@ -1,325 +1,324 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Tasks data class for Syncroton */ class kolab_sync_data_tasks extends kolab_sync_data { /** * Mapping from ActiveSync Calendar namespace fields */ - protected $mapping = array( + protected $mapping = [ 'body' => 'description', 'categories' => 'categories', //'complete' => 'complete', // handled separately 'dateCompleted' => 'changed', 'dueDate' => 'due', 'importance' => 'priority', //'recurrence' => 'recurrence', //'reminderSet' => 'reminderset', //'reminderTime' => 'remindertime', 'sensitivity' => 'sensitivity', 'startDate' => 'start', 'subject' => 'title', 'utcDueDate' => 'due', 'utcStartDate' => 'start', - ); + ]; /** * Sensitivity values */ - const SENSITIVITY_NORMAL = 0; - const SENSITIVITY_PERSONAL = 1; - const SENSITIVITY_PRIVATE = 2; - const SENSITIVITY_CONFIDENTIAL = 3; + public const SENSITIVITY_NORMAL = 0; + public const SENSITIVITY_PERSONAL = 1; + public const SENSITIVITY_PRIVATE = 2; + public const SENSITIVITY_CONFIDENTIAL = 3; /** * mapping of sensitivity * * @var array */ - protected $sensitivityMap = array( + protected $sensitivityMap = [ 'public' => self::SENSITIVITY_PERSONAL, 'private' => self::SENSITIVITY_PRIVATE, 'confidential' => self::SENSITIVITY_CONFIDENTIAL, - ); + ]; /** * Kolab object type * * @var string */ protected $modelName = 'task'; /** * Type of the default folder * * @var int */ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_TASK; /** * Default container for new entries * * @var string */ protected $defaultFolder = 'Tasks'; /** * Type of user created folders * * @var int */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED; /** * Appends contact data to xml element * * @param Syncroton_Model_SyncCollection $collection Collection data * @param string $serverId Local entry identifier * @param boolean $as_array Return entry as an array * * @return array|Syncroton_Model_Task|array Task object */ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false) { $task = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId); - $result = array(); + $result = []; // Completion status (required) $result['complete'] = intval(($task['status'] ?? null) == 'COMPLETED' || ($task['complete'] ?? null) == 100); // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getKolabDataItem($task, $name); switch ($name) { - case 'due': - case 'start': - if (preg_match('/^UTC/i', $key)) { - $value = self::date_from_kolab($value); - } - break; - - case 'changed': - $value = $result['complete'] ? self::date_from_kolab($value) : null; - break; - - case 'description': - $value = $this->body_from_kolab($value, $collection); - break; - - case 'sensitivity': - if (!empty($value)) { - $value = intval($this->sensitivityMap[$value]); - } - break; - - case 'priority': - $value = $this->prio_to_importance($value); - break; + case 'due': + case 'start': + if (preg_match('/^UTC/i', $key)) { + $value = self::date_from_kolab($value); + } + break; + + case 'changed': + $value = $result['complete'] ? self::date_from_kolab($value) : null; + break; + + case 'description': + $value = $this->body_from_kolab($value, $collection); + break; + + case 'sensitivity': + if (!empty($value)) { + $value = intval($this->sensitivityMap[$value]); + } + break; + + case 'priority': + $value = $this->prio_to_importance($value); + break; } if (empty($value) || is_array($value)) { continue; } $result[$key] = $value; } // Recurrence $this->recurrence_from_kolab($collection, $task, $result, 'Task'); return $as_array ? $result : new Syncroton_Model_Task($result); } /** * Apply a timezone matching the utc offset. */ private static function applyTimezone($value, $utcValue) { $tzc = kolab_sync_timezone_converter::getInstance(); $tz = $tzc->getOffsetTimezone($value, $utcValue); if ($tz) { //Setting the timezone will change the time, so we set it on the utc variant instead to end up with the same time in the new timezone. $value = $utcValue; $value->setTimezone($tz); } return $value; } /** * convert contact from xml to libkolab array * * @param Syncroton_Model_Task $data Contact to convert * @param string $folderid Folder identifier * @param array $entry Existing entry * * @return array */ public function toKolab($data, $folderid, $entry = null) { - $task = !empty($entry) ? $entry : array(); + $task = !empty($entry) ? $entry : []; $task['allday'] = 0; // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = $data->$key; switch ($name) { - case 'due': - case 'start': - // We expect to always get regular and utc variants, so we only need to take one into account. - if ($key == 'utcStartDate' || $key == 'utcDueDate') { - continue 2; - } - if ($value) { - if ($name =='due' && $data->utcDueDate) { - $value = self::applyTimezone($value, $data->utcDueDate); + case 'due': + case 'start': + // We expect to always get regular and utc variants, so we only need to take one into account. + if ($key == 'utcStartDate' || $key == 'utcDueDate') { + continue 2; + } + if ($value) { + if ($name == 'due' && $data->utcDueDate) { + $value = self::applyTimezone($value, $data->utcDueDate); + } + if ($name == 'start' && $data->utcStartDate) { + $value = self::applyTimezone($value, $data->utcStartDate); + } } - if ($name =='start' && $data->utcStartDate) { - $value = self::applyTimezone($value, $data->utcStartDate); + break; + + case 'sensitivity': + $map = array_flip($this->sensitivityMap); + $value = $map[$value ?? 'none'] ?? self::SENSITIVITY_NORMAL; + 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 'sensitivity': - $map = array_flip($this->sensitivityMap); - $value = $map[$value ?? 'none'] ?? self::SENSITIVITY_NORMAL; - 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 'priority': - $value = $this->importance_to_prio($value); - break; + break; + + case 'priority': + $value = $this->importance_to_prio($value); + break; } $this->setKolabDataItem($task, $name, $value); } if (!empty($data->complete)) { $task['status'] = 'COMPLETED'; $task['complete'] = 100; - } - else if (isset($data->complete)) { + } elseif (isset($data->complete)) { if ((!empty($task['status']) && $task['status'] == 'COMPLETED') || (!empty($task['complete']) && $task['complete'] == 100) ) { $task['status'] = ''; $task['complete'] = 0; } } // recurrence $task['recurrence'] = $this->recurrence_to_kolab($data, $folderid, null); return $task; } /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type * * @return array Filter query */ protected function filter($filter_type = 0) { - $filter = array(array('type', '=', $this->modelName)); + $filter = [['type', '=', $this->modelName]]; if ($filter_type == Syncroton_Command_Sync::FILTER_INCOMPLETE) { - $filter[] = array('tags', '!~', 'x-complete'); + $filter[] = ['tags', '!~', 'x-complete']; } return $filter; } /** * Convert Kolab priority into ActiveSync importance value */ protected function prio_to_importance($value) { // ActiveSync has only 3 levels of importance: // 0 - Low, 1 - Normal, 2 - High // but Kolab uses ten levels: // 0 - unknown and 1-9 where 1 is the highest // Use mapping from http://msdn.microsoft.com/en-us/library/ee159635.aspx if ($value === null) { return; } switch ($value) { - case 1: - case 2: - case 3: - case 4: - return 2; - case 5: - return 1; - case 6: - case 7: - case 8: - case 9: - return 0; + case 1: + case 2: + case 3: + case 4: + return 2; + case 5: + return 1; + case 6: + case 7: + case 8: + case 9: + return 0; } return; } /** * Convert ActiveSync importance into Kolab priority value */ protected function importance_to_prio($value) { // Use mapping from http://msdn.microsoft.com/en-us/library/ee159635.aspx if ($value === null) { return; } switch ($value) { - case 0: - return 9; - case 1: - return 5; - case 2: - return 1; + case 0: + return 9; + case 1: + return 5; + case 2: + return 1; } return; } } diff --git a/lib/kolab_sync_logger.php b/lib/kolab_sync_logger.php index 7b31525..07db63c 100644 --- a/lib/kolab_sync_logger.php +++ b/lib/kolab_sync_logger.php @@ -1,181 +1,180 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Class for logging messages into log file(s) */ class kolab_sync_logger extends Zend_Log { public $mode; protected $logfile; protected $format; protected $log_dir; protected $username; /** * Constructor */ - function __construct($mode = null) + public function __construct($mode = null) { $rcube = rcube::get_instance(); $this->mode = intval($mode); $this->logfile = $rcube->config->get('activesync_log_file'); $this->format = $rcube->config->get('log_date_format', 'd-M-Y H:i:s O'); $this->log_dir = $rcube->config->get('log_dir'); $r = new ReflectionClass($this); $this->_priorities = $r->getConstants(); } public function __call($method, $params) { $method = strtoupper($method); if ($this->_priorities[$method] <= $this->mode) { $this->log(array_shift($params), $method); } } /** * Check whether debug logging is enabled * * @return bool */ public function hasDebug() { // This is what we check in self::log() below return !empty($this->log_dir) && $this->mode >= self::NOTICE; } /** * Message logger * * @param string $message Log message * @param int|string $method Message severity */ public function log($message, $method, $extras = null) { if (is_numeric($method)) { $mode = $method; $method = array_search($method, $this->_priorities); - } - else { + } else { $mode = $this->_priorities[$method]; } // Don't log messages with lower prio than the configured one if ($mode > $this->mode) { return; } // Don't log debug messages if it's disabled e.g. by per_user_logging if (empty($this->log_dir) && $mode >= self::NOTICE) { return; } $rcube = rcube::get_instance(); $log_dir = $this->log_dir ?: $rcube->config->get('log_dir'); $logfile = $this->logfile; // if log_file is configured all logs will go to it // otherwise use separate file for info/debug and warning/error if (!$logfile) { switch ($mode) { - case self::DEBUG: - case self::INFO: - case self::NOTICE: - $file = 'console'; - break; - default: - $file = 'errors'; - break; + case self::DEBUG: + case self::INFO: + case self::NOTICE: + $file = 'console'; + break; + default: + $file = 'errors'; + break; } $logfile = $log_dir . DIRECTORY_SEPARATOR . $file; if (version_compare(version_parse(RCUBE_VERSION), '1.4.0') >= 0) { $logfile .= $rcube->config->get('log_file_ext', '.log'); } - } - else if ($logfile[0] != '/') { + } elseif ($logfile[0] != '/') { $logfile = $log_dir . DIRECTORY_SEPARATOR . $logfile; } if (!is_string($message)) { $message = var_export($message, true); } // add user/request information to the log if ($mode <= self::WARN) { - $device = array(); - $params = array('cmd' => 'Cmd', 'device' => 'DeviceId', 'type' => 'DeviceType'); + $device = []; + $params = ['cmd' => 'Cmd', 'device' => 'DeviceId', 'type' => 'DeviceType']; if (!empty($this->username)) { $device['user'] = $this->username; } foreach ($params as $key => $val) { if ($val = $_GET[$val]) { $device[$key] = $val; } } if (!empty($device)) { $message = @json_encode($device) . ' ' . $message; } } $date = rcube_utils::date_format($this->format); $logline = sprintf("[%s]: [%s] %s\n", $date, $method, $message); if ($fp = @fopen($logfile, 'a')) { fwrite($fp, $logline); fflush($fp); fclose($fp); return; } if ($mode <= self::WARN) { // send error to PHPs error handler if write to file didn't succeed trigger_error($message, E_USER_WARNING); } } /** * Set current user name to add into error log */ public function set_username($username) { $this->username = $username; } /** * Set log directory */ public function set_log_dir($dir) { $this->log_dir = $dir; } } diff --git a/lib/kolab_sync_message.php b/lib/kolab_sync_message.php index 2057b75..2ada9de 100644 --- a/lib/kolab_sync_message.php +++ b/lib/kolab_sync_message.php @@ -1,493 +1,499 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_sync_message { - protected $headers = array(); + protected $headers = []; protected $body; protected $ctype; - protected $ctype_params = array(); + protected $ctype_params = []; /** * Constructor * * @param string|resource $source MIME message source */ - function __construct($source) + public function __construct($source) { $this->parse_mime($source); } /** * Returns message headers * * @return array Message headers */ public function headers() { return $this->headers; } public function source() { - $headers = array(); + $headers = []; // Build the message back foreach ($this->headers as $header => $header_value) { $headers[$header] = $header . ': ' . $header_value; } return trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($this->body); // @TODO: work with file streams } /** * Appends text at the end of the message body * * @todo: HTML support * * @param string $text Text to append * @param string $charset Text charset */ public function append($text, $charset = null) { if ($this->ctype == 'text/plain') { // decode body $body = $this->decode($this->body, $this->headers['Content-Transfer-Encoding']); $body = rcube_charset::convert($body, $this->ctype_params['charset'], $charset); // append text $body .= $text; // encode and save $body = rcube_charset::convert($body, $charset, $this->ctype_params['charset']); $this->body = $this->encode($body, $this->headers['Content-Transfer-Encoding']); } } /** * Adds attachment to the message * * @param string $body Attachment body (not encoded) * @param array $params Attachment parameters (Mail_mimePart format) */ - public function add_attachment($body, $params = array()) + public function add_attachment($body, $params = []) { // convert the message into multipart/mixed if ($this->ctype != 'multipart/mixed') { $boundary = '_' . md5(rand() . microtime()); $this->body = "--$boundary\r\n" - ."Content-Type: " . $this->headers['Content-Type']."\r\n" - ."Content-Transfer-Encoding: " . $this->headers['Content-Transfer-Encoding']."\r\n" - ."\r\n" . trim($this->body) . "\r\n" - ."--$boundary\r\n"; + . "Content-Type: " . $this->headers['Content-Type'] . "\r\n" + . "Content-Transfer-Encoding: " . $this->headers['Content-Transfer-Encoding'] . "\r\n" + . "\r\n" . trim($this->body) . "\r\n" + . "--$boundary\r\n"; $this->ctype = 'multipart/mixed'; - $this->ctype_params = array('boundary' => $boundary); + $this->ctype_params = ['boundary' => $boundary]; unset($this->headers['Content-Transfer-Encoding']); $this->save_content_type($this->ctype, $this->ctype_params); } // make sure MIME-Version header is set, it's required by some servers if (empty($this->headers['MIME-Version'])) { $this->headers['MIME-Version'] = '1.0'; } $boundary = $this->ctype_params['boundary']; $part = new Mail_mimePart($body, $params); $body = $part->encode(); foreach ($body['headers'] as $name => $value) { $body['headers'][$name] = $name . ': ' . $value; } $this->body = rtrim($this->body); $this->body = preg_replace('/--$/', '', $this->body); // add the attachment to the end of the message $this->body .= "\r\n" - .implode("\r\n", $body['headers']) . "\r\n\r\n" - .$body['body'] . "\r\n--$boundary--\r\n"; + . implode("\r\n", $body['headers']) . "\r\n\r\n" + . $body['body'] . "\r\n--$boundary--\r\n"; } /** * Sets the value of specified message header * * @param string $name Header name * @param string $value Header value */ public function set_header($name, $value) { $name = self::normalize_header_name($name); if ($name != 'Content-Type') { $this->headers[$name] = $value; } } /** * Send the given message using the configured method. * * @param array $smtp_error SMTP error array (reference) * @param array $smtp_opts SMTP options (e.g. DSN request) * * @return bool Send status. */ public function send(&$smtp_error = null, $smtp_opts = null) { $rcube = kolab_sync::get_instance(); $headers = $this->headers; $mailto = $headers['To']; $headers['User-Agent'] = $rcube->app_name . ' ' . kolab_sync::VERSION; if ($agent = $rcube->config->get('useragent')) { $headers['User-Agent'] .= '/' . $agent; } if (empty($headers['From'])) { $headers['From'] = $this->get_identity(); } // make sure there's sender name in From: - else if ($rcube->config->get('activesync_fix_from') + elseif ($rcube->config->get('activesync_fix_from') && preg_match('/^?$/', trim($headers['From']), $m) ) { $identities = kolab_sync::get_instance()->user->list_identities(); $email = $m[1]; foreach ((array) $identities as $ident) { if ($ident['email'] == $email) { if ($ident['name']) { $headers['From'] = format_email_recipient($email, $ident['name']); } break; } } } if (empty($headers['Message-ID'])) { $headers['Message-ID'] = $rcube->gen_message_id(); } // remove empty headers $headers = array_filter($headers); $smtp_headers = $headers; // generate list of recipients - $recipients = array(); + $recipients = []; - if (!empty($headers['To'])) + if (!empty($headers['To'])) { $recipients[] = $headers['To']; - if (!empty($headers['Cc'])) + } + if (!empty($headers['Cc'])) { $recipients[] = $headers['Cc']; - if (!empty($headers['Bcc'])) + } + if (!empty($headers['Bcc'])) { $recipients[] = $headers['Bcc']; + } if (empty($headers['To']) && empty($headers['Cc'])) { $headers['To'] = 'undisclosed-recipients:;'; } // remove Bcc header unset($smtp_headers['Bcc']); // send message if (!is_object($rcube->smtp)) { $rcube->smtp_init(true); } $sent = $rcube->smtp->send_mail($headers['From'], $recipients, $smtp_headers, $this->body, $smtp_opts); $smtp_response = $rcube->smtp->get_response(); $smtp_error = $rcube->smtp->get_error(); // log error if (!$sent) { - rcube::raise_error(array('code' => 800, 'type' => 'smtp', + rcube::raise_error(['code' => 800, 'type' => 'smtp', 'line' => __LINE__, 'file' => __FILE__, - 'message' => "SMTP error: ".join("\n", $smtp_response)), true, false); + 'message' => "SMTP error: " . implode("\n", $smtp_response)], true, false); } if ($sent) { - $rcube->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $this->body)); + $rcube->plugins->exec_hook('message_sent', ['headers' => $headers, 'body' => $this->body]); // remove MDN headers after sending unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']); if ($rcube->config->get('smtp_log')) { // get all recipient addresses $mailto = rcube_mime::decode_address_list(implode(',', $recipients), null, false, null, true); - rcube::write_log('sendmail', sprintf("User %s [%s]; Message %s for %s; %s", - $rcube->get_user_name(), - rcube_utils::remote_addr(), - $headers['Message-ID'], - implode(', ', $mailto), - !empty($smtp_response) ? implode('; ', $smtp_response) : '') + rcube::write_log( + 'sendmail', + sprintf( + "User %s [%s]; Message %s for %s; %s", + $rcube->get_user_name(), + rcube_utils::remote_addr(), + $headers['Message-ID'], + implode(', ', $mailto), + !empty($smtp_response) ? implode('; ', $smtp_response) : '' + ) ); } } $this->headers = $headers; return $sent; } /** * Parses the message source and fixes 8bit data for ActiveSync. * This way any not UTF8 characters will be encoded before * sending to the device. * * @param string $message Message source * * @return string Fixed message source */ public static function recode_message($message) { // @TODO: work with stream, to workaround memory issues with big messages if (is_resource($message)) { $message = stream_get_contents($message); } - list($headers, $message) = array_pad(preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY), 2, ''); + [$headers, $message] = array_pad(preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY), 2, ''); $hdrs = self::parse_headers($headers); // multipart message if (preg_match('/boundary="?([a-z0-9-\'\(\)+_\,\.\/:=\? ]+)"?/i', $hdrs['Content-Type'] ?? '', $matches)) { $boundary = '--' . $matches[1]; $message = explode($boundary, $message); - for ($x=1, $parts = count($message) - 1; $x<$parts; $x++) { + for ($x = 1, $parts = count($message) - 1; $x < $parts; $x++) { $message[$x] = "\r\n" . self::recode_message(ltrim($message[$x])); } - return $headers . "\r\n\r\n" . implode($boundary , $message); + return $headers . "\r\n\r\n" . implode($boundary, $message); } // single part $enc = !empty($hdrs['Content-Transfer-Encoding']) ? strtolower($hdrs['Content-Transfer-Encoding']) : null; // do nothing if already encoded if ($enc != 'quoted-printable' && $enc != 'base64') { // recode body if any non-printable-ascii characters found if (preg_match('/[^\x20-\x7E\x0A\x0D\x09]/', $message)) { $hdrs['Content-Transfer-Encoding'] = 'base64'; foreach ($hdrs as $header => $header_value) { $hdrs[$header] = $header . ': ' . $header_value; } $headers = trim(implode("\r\n", $hdrs)); $message = rtrim(chunk_split(base64_encode(rtrim($message)), 76, "\r\n")) . "\r\n"; } } return $headers . "\r\n\r\n" . $message; } /** * Creates a fake plain text message source with predefined headers and body * * @param string $headers Message headers * @param string $body Plain text body * * @return string Message source */ public static function fake_message($headers, $body = '') { $hdrs = self::parse_headers($headers); $result = ''; $hdrs['Content-Type'] = 'text/plain; charset=UTF-8'; $hdrs['Content-Transfer-Encoding'] = 'quoted-printable'; foreach ($hdrs as $header => $header_value) { $result .= $header . ': ' . $header_value . "\r\n"; } return $result . "\r\n" . self::encode($body, 'quoted-printable'); } /** * MIME message parser * * @param string|resource $message MIME message source */ protected function parse_mime($message) { // @TODO: work with stream, to workaround memory issues with big messages if (is_resource($message)) { $message = stream_get_contents($message); } - list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY); + [$headers, $message] = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY); $headers = self::parse_headers($headers); // parse Content-Type header $ctype_parts = preg_split('/[; ]+/', $headers['Content-Type']); $this->ctype = strtolower(array_shift($ctype_parts)); foreach ($ctype_parts as $part) { if (preg_match('/^([a-z-_]+)\s*=\s*(.+)$/i', trim($part), $m)) { $this->ctype_params[strtolower($m[1])] = trim($m[2], '"'); } } if (!empty($headers['Content-Transfer-Encoding'])) { $headers['Content-Transfer-Encoding'] = strtolower($headers['Content-Transfer-Encoding']); } $this->headers = $headers; $this->body = $message; } /** * Parse message source with headers */ protected static function parse_headers($headers) { // Parse headers $headers = str_replace("\r\n", "\n", $headers); $headers = explode("\n", trim($headers)); $ln = 0; - $lines = array(); + $lines = []; foreach ($headers as $line) { if (ord($line[0]) <= 32) { $lines[$ln] .= (empty($lines[$ln]) ? '' : "\r\n") . $line; - } - else { + } else { $lines[++$ln] = trim($line); } } // Unify char-case of header names - $headers = array(); + $headers = []; foreach ($lines as $line) { if (strpos($line, ':') !== false) { - list($field, $string) = explode(':', $line, 2); + [$field, $string] = explode(':', $line, 2); if ($field = self::normalize_header_name($field)) { $headers[$field] = trim($string); } } } return $headers; } /** * Normalize (fix) header names */ protected static function normalize_header_name($name) { - $headers_map = array( + $headers_map = [ 'subject' => 'Subject', 'from' => 'From', 'to' => 'To', 'cc' => 'Cc', 'bcc' => 'Bcc', 'message-id' => 'Message-ID', 'references' => 'References', 'content-type' => 'Content-Type', 'content-transfer-encoding' => 'Content-Transfer-Encoding', - ); + ]; $name_lc = strtolower($name); - return isset($headers_map[$name_lc]) ? $headers_map[$name_lc] : $name; + return $headers_map[$name_lc] ?? $name; } /** * Encodes message/part body * * @param string $body Message/part body * @param string $encoding Content encoding * * @return string Encoded body */ protected static function encode($body, $encoding) { switch ($encoding) { - case 'base64': - $body = base64_encode($body); - $body = chunk_split($body, 76, "\r\n"); - break; - case 'quoted-printable': - $body = quoted_printable_encode($body); - break; + case 'base64': + $body = base64_encode($body); + $body = chunk_split($body, 76, "\r\n"); + break; + case 'quoted-printable': + $body = quoted_printable_encode($body); + break; } return $body; } /** * Decodes message/part body * * @param string $body Message/part body * @param string $encoding Content encoding * * @return string Decoded body */ protected function decode($body, $encoding) { $body = str_replace("\r\n", "\n", $body); switch ($encoding) { - case 'base64': - $body = base64_decode($body); - break; - case 'quoted-printable': - $body = quoted_printable_decode($body); - break; + case 'base64': + $body = base64_decode($body); + break; + case 'quoted-printable': + $body = quoted_printable_decode($body); + break; } return $body; } /** * Returns email address string from default identity of the current user */ protected function get_identity() { $user = kolab_sync::get_instance()->user; if ($identity = $user->get_identity()) { return format_email_recipient(format_email($identity['email']), $identity['name']); } } - protected function save_content_type($ctype, $params = array()) + protected function save_content_type($ctype, $params = []) { $this->ctype = $ctype; $this->ctype_params = $params; $this->headers['Content-Type'] = $ctype; if (!empty($params)) { foreach ($params as $name => $value) { $this->headers['Content-Type'] .= sprintf('; %s="%s"', $name, $value); } } } } diff --git a/lib/kolab_sync_plugin_api.php b/lib/kolab_sync_plugin_api.php index c9089de..1dcf620 100644 --- a/lib/kolab_sync_plugin_api.php +++ b/lib/kolab_sync_plugin_api.php @@ -1,103 +1,103 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * The plugin loader and global API * * @package PluginAPI */ class kolab_sync_plugin_api extends rcube_plugin_api { /** * This implements the 'singleton' design pattern * * @return rcube_plugin_api The one and only instance if this class */ - static function get_instance() + public static function get_instance() { if (!self::$instance) { self::$instance = new kolab_sync_plugin_api(); } return self::$instance; } /** * Initialize plugin engine * * This has to be done after rcmail::load_gui() or rcmail::json_init() * was called because plugins need to have access to rcmail->output * * @param rcube $app Instance of the rcube base class * @param string $task Current application task (used for conditional plugin loading) */ public function init($app, $task = '') { $this->task = $task; } /** * Register a handler function for template objects * * @param string $name Object name * @param string $owner Plugin name that registers this action * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') */ public function register_handler($name, $owner, $callback) { // empty } /** * Register this plugin to be responsible for a specific task * * @param string $task Task name (only characters [a-z0-9_.-] are allowed) * @param string $owner Plugin name that registers this action */ public function register_task($task, $owner) { $this->tasks[$task] = $owner; } /** * Include a plugin script file in the current HTML page * * @param string $fn Path to script */ public function include_script($fn) { //empty } /** * Include a plugin stylesheet in the current HTML page * * @param string $fn Path to stylesheet */ public function include_stylesheet($fn) { //empty } } diff --git a/lib/kolab_sync_storage.php b/lib/kolab_sync_storage.php index 0e3813d..3e0c8c7 100644 --- a/lib/kolab_sync_storage.php +++ b/lib/kolab_sync_storage.php @@ -1,2042 +1,2064 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Storage handling class with basic Kolab support (everything stored in IMAP) */ class kolab_sync_storage { - const INIT_SUB_PERSONAL = 1; // all subscribed folders in personal namespace - const INIT_ALL_PERSONAL = 2; // all folders in personal namespace - const INIT_SUB_OTHER = 4; // all subscribed folders in other users namespace - const INIT_ALL_OTHER = 8; // all folders in other users namespace - const INIT_SUB_SHARED = 16; // all subscribed folders in shared namespace - const INIT_ALL_SHARED = 32; // all folders in shared namespace - - const MODEL_CALENDAR = 'event'; - const MODEL_CONTACTS = 'contact'; - const MODEL_EMAIL = 'mail'; - const MODEL_NOTES = 'note'; - const MODEL_TASKS = 'task'; - - const ROOT_MAILBOX = 'INBOX'; - const ASYNC_KEY = '/private/vendor/kolab/activesync'; - const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid'; - - const CTYPE_KEY = '/shared/vendor/kolab/folder-type'; - const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type'; + public const INIT_SUB_PERSONAL = 1; // all subscribed folders in personal namespace + public const INIT_ALL_PERSONAL = 2; // all folders in personal namespace + public const INIT_SUB_OTHER = 4; // all subscribed folders in other users namespace + public const INIT_ALL_OTHER = 8; // all folders in other users namespace + public const INIT_SUB_SHARED = 16; // all subscribed folders in shared namespace + public const INIT_ALL_SHARED = 32; // all folders in shared namespace + + public const MODEL_CALENDAR = 'event'; + public const MODEL_CONTACTS = 'contact'; + public const MODEL_EMAIL = 'mail'; + public const MODEL_NOTES = 'note'; + public const MODEL_TASKS = 'task'; + + public const ROOT_MAILBOX = 'INBOX'; + public const ASYNC_KEY = '/private/vendor/kolab/activesync'; + public const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid'; + + public const CTYPE_KEY = '/shared/vendor/kolab/folder-type'; + public const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type'; public $syncTimeStamp; protected $storage; protected $folder_meta; protected $folder_uids; protected $folders = []; protected $modseq = []; protected $root_meta; protected $relations = []; protected $relationSupport = true; protected $tag_rts = []; protected static $instance; protected static $types = [ 1 => '', 2 => 'mail.inbox', 3 => 'mail.drafts', 4 => 'mail.wastebasket', 5 => 'mail.sentitems', 6 => 'mail.outbox', 7 => 'task.default', 8 => 'event.default', 9 => 'contact.default', 10 => 'note.default', 11 => 'journal.default', 12 => 'mail', 13 => 'event', 14 => 'contact', 15 => 'task', 16 => 'journal', 17 => 'note', ]; /** * This implements the 'singleton' design pattern * * @return kolab_sync_storage The one and only instance */ public static function get_instance() { if (!self::$instance) { - self::$instance = new kolab_sync_storage; + self::$instance = new kolab_sync_storage(); self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->storage = kolab_sync::get_instance()->get_storage(); // set additional header used by libkolab $this->storage->set_options([ // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, ]); // Disable paging $this->storage->set_pagesize(999999); } /** * Clear internal cache state */ public function reset() { $this->folders = []; } /** * List known devices * * @return array Device list as hash array */ public function devices_list() { if ($this->root_meta === null) { // @TODO: consider server annotation instead of INBOX if ($meta = $this->storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) { $this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]); - } - else { + } else { $this->root_meta = []; } } if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) { return $this->root_meta['DEVICE']; } return []; } /** * Get list of folders available for sync * * @param string $deviceid Device identifier * @param string $type Folder type * @param bool $flat_mode Enables flat-list mode * * @return array|bool List of mailbox folders, False on backend failure */ public function folders_list($deviceid, $type, $flat_mode = false) { // get all folders of specified type $folders = kolab_storage::list_folders('', '*', $type, false, $typedata); // get folders activesync config $folderdata = $this->folder_meta(); if (!is_array($folders) || !is_array($folderdata)) { return false; } $folders_list = []; // check if folders are "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } // force numeric folder name to be a string (T1283) $folder = (string) $folder; if (!empty($type) && !in_array($folder, $folders)) { continue; } // Activesync folder identifier (serverId) $folder_type = !empty($typedata[$folder]) ? $typedata[$folder] : 'mail'; $folder_id = $this->folder_id($folder, $folder_type); $folders_list[$folder_id] = $this->folder_data($folder, $folder_type); } if ($flat_mode) { $folders_list = $this->folders_list_flat($folders_list, $type, $typedata); } return $folders_list; } /** * Converts list of folders to a "flat" list */ protected function folders_list_flat($folders, $type, $typedata) { $delim = $this->storage->get_hierarchy_delimiter(); foreach ($folders as $idx => $folder) { if ($folder['parentId']) { // for non-mail folders we make the list completely flat if ($type != self::MODEL_EMAIL) { $display_name = kolab_storage::object_name($folder['imap_name']); $display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET); $folders[$idx]['parentId'] = 0; $folders[$idx]['displayName'] = $display_name; } // for mail folders we modify only folders with non-existing parents - else if (!isset($folders[$folder['parentId']])) { + elseif (!isset($folders[$folder['parentId']])) { $items = explode($delim, $folder['imap_name']); $parent = 0; // find existing parent while (count($items) > 0) { array_pop($items); $parent_name = implode($delim, $items); $parent_type = !empty($typedata[$parent_name]) ? $typedata[$parent_name] : 'mail'; $parent_id = $this->folder_id($parent_name, $parent_type); if (isset($folders[$parent_id])) { $parent = $parent_id; break; } } if (!$parent) { $display_name = kolab_storage::object_name($folder['imap_name']); $display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET); - } - else { + } else { $parent_name = isset($parent_id) ? $folders[$parent_id]['imap_name'] : ''; $display_name = substr($folder['imap_name'], strlen($parent_name) + 1); $display_name = rcube_charset::convert($display_name, 'UTF7-IMAP'); $display_name = str_replace($delim, ' » ', $display_name); } $folders[$idx]['parentId'] = $parent; $folders[$idx]['displayName'] = $display_name; } } } return $folders; } /** * Getter for folder metadata * * @return array|bool Hash array with meta data for each folder, False on backend failure */ protected function folder_meta() { if (!isset($this->folder_meta)) { // get folders activesync config $folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY); if (!is_array($folderdata)) { return $this->folder_meta = false; } $this->folder_meta = []; foreach ($folderdata as $folder => $meta) { if (isset($meta[self::ASYNC_KEY])) { if ($metadata = $this->unserialize_metadata($meta[self::ASYNC_KEY])) { $this->folder_meta[$folder] = $metadata; } } } } return $this->folder_meta; } /** * Creates folder and subscribes to the device * * @param string $name Folder name (UTF8) * @param int $type Folder (ActiveSync) type * @param string $deviceid Device identifier * @param ?string $parentid Parent folder id identifier * * @return string|false New folder identifier on success, False on failure */ public function folder_create($name, $type, $deviceid, $parentid = null) { $parent = null; $name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP'); if ($parentid) { $parent = $this->folder_id2name($parentid, $deviceid); if ($parent === null) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND); } } if ($parent !== null) { $delim = $this->storage->get_hierarchy_delimiter(); $name = $parent . $delim . $name; } if ($this->storage->folder_exists($name)) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS); } $type = self::type_activesync2kolab($type); $created = kolab_storage::folder_create($name, $type, true); if ($created) { // Set ActiveSync subscription flag $this->folder_set($name, $deviceid, 1); return $this->folder_id($name, $type); } // Special case when client tries to create a subfolder of INBOX // which is not possible on Cyrus-IMAP (T2223) if ($parent === 'INBOX' && stripos($this->last_error(), 'invalid') !== false) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER); } return false; } /** * Renames a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $new_name New folder name (UTF8) * @param ?string $parentid Folder parent identifier * * @return bool True on success, False on failure */ public function folder_rename($folderid, $deviceid, $new_name, $parentid) { $old_name = $this->folder_id2name($folderid, $deviceid); if ($parentid) { $parent = $this->folder_id2name($parentid, $deviceid); } $name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP'); if (isset($parent)) { $delim = $this->storage->get_hierarchy_delimiter(); $name = $parent . $delim . $name; } // Rename/move IMAP folder if ($name === $old_name) { return true; } $this->folder_meta = null; // TODO: folder type change? $type = kolab_storage::folder_type($old_name); // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { return $this->storage->rename_folder($old_name, $name); - } - else { + } else { return kolab_storage::folder_rename($old_name, $name); } } /** * Deletes folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * * @return bool True on success, False otherwise */ public function folder_delete($folderid, $deviceid) { $name = $this->folder_id2name($folderid, $deviceid); $type = kolab_storage::folder_type($name); unset($this->folder_meta[$name]); // don't use kolab_storage for deleting mail folders if (preg_match('/^mail/', $type)) { return $this->storage->delete_folder($name); } return kolab_storage::folder_delete($name); } /** * Deletes contents of a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param bool $recursive Apply to the folder and its subfolders * * @return bool True on success, False otherwise */ public function folder_empty($folderid, $deviceid, $recursive = false) { $foldername = $this->folder_id2name($folderid, $deviceid); // Remove all entries if (!$this->storage->clear_folder($foldername)) { return false; } // Remove subfolders if ($recursive) { $delim = $this->storage->get_hierarchy_delimiter(); $folderdata = $this->folder_meta(); if (!is_array($folderdata)) { return false; } foreach ($folderdata as $subfolder => $meta) { if (!empty($meta['FOLDER'][$deviceid]['S']) && strpos((string) $subfolder, $foldername . $delim)) { if (!$this->storage->clear_folder((string) $subfolder)) { return false; } } } } return true; } /** * Sets ActiveSync subscription flag on a folder * * @param string $name Folder name (UTF7-IMAP) * @param string $deviceid Device identifier * @param int $flag Flag value (0|1|2) * * @return bool True on success, False on failure */ protected function folder_set($name, $deviceid, $flag) { if (empty($deviceid)) { return false; } // get folders activesync config $metadata = $this->folder_meta(); if (!is_array($metadata)) { return false; } - $metadata = isset($metadata[$name]) ? $metadata[$name] : []; + $metadata = $metadata[$name] ?? []; - if ($flag) { + if ($flag) { if (empty($metadata)) { $metadata = []; } if (empty($metadata['FOLDER'])) { $metadata['FOLDER'] = []; } if (empty($metadata['FOLDER'][$deviceid])) { $metadata['FOLDER'][$deviceid] = []; } // Z-Push uses: // 1 - synchronize, no alarms // 2 - synchronize with alarms $metadata['FOLDER'][$deviceid]['S'] = $flag; - } - else { + } else { unset($metadata['FOLDER'][$deviceid]['S']); if (empty($metadata['FOLDER'][$deviceid])) { unset($metadata['FOLDER'][$deviceid]); } if (empty($metadata['FOLDER'])) { unset($metadata['FOLDER']); } if (empty($metadata)) { $metadata = null; } } // Return if nothing's been changed - if (!self::data_array_diff(isset($this->folder_meta[$name]) ? $this->folder_meta[$name] : null, $metadata)) { + if (!self::data_array_diff($this->folder_meta[$name] ?? null, $metadata)) { return true; } $this->folder_meta[$name] = $metadata; return $this->storage->set_metadata($name, [self::ASYNC_KEY => $this->serialize_metadata($metadata)]); } /** * Returns device metadata * * @param string $id Device ID * * @return array|null Device metadata */ public function device_get($id) { $devices_list = $this->devices_list(); return $devices_list[$id] ?? null; } /** * Registers new device on server * * @param array $device Device data * @param string $id Device ID * * @return bool True on success, False on failure */ public function device_create($device, $id) { // Fill local cache $this->devices_list(); // Some devices create dummy devices with name "validate" (#1109) // This device entry is used in two initial requests, but later // the device registers a real name. We can remove this dummy entry // on new device creation $this->device_delete('validate'); // Old Kolab_ZPush device parameters // MODE: -1 | 0 | 1 (not set | flatmode | foldermode) // TYPE: device type string // ALIAS: user-friendly device name // Syncroton (kolab_sync_backend_device) uses // ID: internal identifier in syncroton database // TYPE: device type string // ALIAS: user-friendly device name $metadata = $this->root_meta; $metadata['DEVICE'][$id] = $device; $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)]; $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); if ($result) { // Update local cache $this->root_meta['DEVICE'][$id] = $device; // subscribe default set of folders $this->device_init_subscriptions($id); } return $result; } /** * Device update. * * @param array $device Device data * @param string $id Device ID * * @return bool True on success, False on failure */ public function device_update($device, $id) { $devices_list = $this->devices_list(); $old_device = $devices_list[$id]; if (!$old_device) { return false; } // Do nothing if nothing is changed if (!self::data_array_diff($old_device, $device)) { return true; } $device = array_merge($old_device, $device); $metadata = $this->root_meta; $metadata['DEVICE'][$id] = $device; $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)]; $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); if ($result) { // Update local cache $this->root_meta['DEVICE'][$id] = $device; } return $result; } /** * Device delete. * * @param string $id Device ID * * @return bool True on success, False on failure */ public function device_delete($id) { $device = $this->device_get($id); if (!$device) { return false; } unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]); if (empty($this->root_meta['DEVICE'])) { unset($this->root_meta['DEVICE']); } if (empty($this->root_meta['FOLDER'])) { unset($this->root_meta['FOLDER']); } $metadata = $this->serialize_metadata($this->root_meta); $metadata = [self::ASYNC_KEY => $metadata]; // update meta data $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); if ($result) { // remove device annotation for every folder foreach ($this->folder_meta() as $folder => $meta) { // skip root folder (already handled above) - if ($folder == self::ROOT_MAILBOX) + if ($folder == self::ROOT_MAILBOX) { continue; + } if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) { unset($meta['FOLDER'][$id]); if (empty($meta['FOLDER'])) { unset($this->folder_meta[$folder]['FOLDER']); unset($meta['FOLDER']); } if (empty($meta)) { unset($this->folder_meta[$folder]); $meta = null; } $metadata = [self::ASYNC_KEY => $this->serialize_metadata($meta)]; $res = $this->storage->set_metadata($folder, $metadata); if ($res && $meta) { $this->folder_meta[$folder] = $meta; } } } } return $result; } /** * Creates an item in a folder. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param string|array $data Object data (string for email, array for other types) * @param array $params Additional parameters (e.g. mail flags) * * @return string|null Item UID on success or null on failure */ public function createItem($folderid, $deviceid, $type, $data, $params = []) { if ($type == self::MODEL_EMAIL) { $foldername = $this->folder_id2name($folderid, $deviceid); $uid = $this->storage->save_message($foldername, $data, '', false, $params['flags'] ?? []); if (!$uid) { // $this->logger->error("Error while storing the message " . $this->storage->get_error_str()); } return $uid; } $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES); // convert categories into tags, save them after creating an object if ($useTags && !empty($data['categories'])) { $tags = $data['categories']; unset($data['categories']); } $folder = $this->getFolder($folderid, $deviceid, $type); // Set User-Agent for saved objects $app = kolab_sync::get_instance(); $app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION); if ($folder && $folder->valid && $folder->save($data)) { if (!empty($tags) && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES)) { $this->setCategories($data['uid'], $tags); } return $data['uid']; } return null; } /** * Deletes an item from a folder by UID. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param string $uid Requested object UID * @param bool $moveToTrash Move to trash, instead of delete (for mail messages only) * * @return bool True on success, False on failure */ public function deleteItem($folderid, $deviceid, $type, $uid, $moveToTrash = false) { if ($type == self::MODEL_EMAIL) { $foldername = $this->folder_id2name($folderid, $deviceid); $trash = kolab_sync::get_instance()->config->get('trash_mbox'); // move message to the Trash folder if ($moveToTrash && strlen($trash) && $trash != $foldername && $this->storage->folder_exists($trash)) { return $this->storage->move_message($uid, $trash, $foldername); } // delete the message // According to the ActiveSync spec. "If the DeletesAsMoves element is set to false, // the deletion is PERMANENT.", therefore we delete the message, and not flag as deleted. // FIXME: We could consider acting according to the 'flag_for_deletion' setting. // Don't forget about 'read_when_deleted' setting then. // $this->storage->set_flag($uid, 'DELETED', $foldername); // $this->storage->set_flag($uid, 'SEEN', $foldername); return $this->storage->delete_message($uid, $foldername); } $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES); $folder = $this->getFolder($folderid, $deviceid, $type); if (!$folder || !$folder->valid) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } if ($folder->delete($uid)) { if ($useTags) { $this->setCategories($uid, []); } return true; } return false; } /** * Updates an item in a folder. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param string $uid Object UID * @param string|array $data Object data (string for email, array for other types) * @param array $params Additional parameters (e.g. mail flags) * * @return string|null Item UID on success or null on failure */ public function updateItem($folderid, $deviceid, $type, $uid, $data, $params = []) { if ($type == self::MODEL_EMAIL) { $foldername = $this->folder_id2name($folderid, $deviceid); // Note: We do not support a message body update, as it's not needed foreach (($params['flags'] ?? []) as $flag) { $this->storage->set_flag($uid, $flag, $foldername); } // Categories (Tags) change if (isset($params['categories']) && $this->relationSupport) { $message = new rcube_message($uid, $foldername); if (empty($message->headers)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } $this->setCategories($message, $params['categories']); } return $uid; } $folder = $this->getFolder($folderid, $deviceid, $type); $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES); // convert categories into tags, save them after updating an object if ($useTags && array_key_exists('categories', $data)) { $tags = (array) $data['categories']; unset($data['categories']); } // Set User-Agent for saved objects $app = kolab_sync::get_instance(); $app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION); if ($folder && $folder->valid && $folder->save($data, $type, $uid)) { if (isset($tags)) { $this->setCategories($uid, $tags); } return $uid; } return null; } /** * Returns list of categories assigned to an object * * @param object|string $object UID or rcube_message object * @param array $categories Addition tag names to merge with * * @return array List of categories */ public function getCategories($object, $categories = []) { if (is_object($object)) { // support only messages with message-id if (!($msg_id = $object->headers->get('message-id', false))) { return []; } $config = kolab_storage_config::get_instance(); $delta = Syncroton_Registry::getPingTimeout(); $folder = $object->folder; $uid = $object->uid; // get tag objects raleted to specified message-id $tags = $config->get_tags($msg_id); foreach ($tags as $idx => $tag) { // resolve members if it wasn't done recently $force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta; $members = $config->resolve_members($tag, $force); if (empty($members[$folder]) || !in_array($uid, $members[$folder])) { unset($tags[$idx]); } if ($force) { $this->tag_rts[$tag['uid']] = time(); } } // make sure current folder is set correctly again $this->storage->set_folder($folder); } else { $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($object); } - $tags = array_filter(array_map(function($v) { return $v['name']; }, $tags)); + $tags = array_filter(array_map(function ($v) { return $v['name']; }, $tags)); // merge result with old categories if (!empty($categories)) { $tags = array_unique(array_merge($tags, (array) $categories)); } return $tags; } /** * Gets kolab_storage_folder object from Activesync folder ID. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * * @return ?kolab_storage_folder */ public function getFolder($folderid, $deviceid, $type) { $unique_key = "$folderid:$deviceid:$type"; if (array_key_exists($unique_key, $this->folders)) { return $this->folders[$unique_key]; } $foldername = $this->folder_id2name($folderid, $deviceid); return $this->folders[$unique_key] = kolab_storage::get_folder($foldername, $type); } /** * Gets Activesync preferences for a folder. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * * @return array Folder preferences */ public function getFolderConfig($folderid, $deviceid, $type) { $foldername = $this->folder_id2name($folderid, $deviceid); $metadata = $this->folder_meta(); $config = []; if (!empty($metadata[$foldername]['FOLDER'][$deviceid])) { $config = $metadata[$foldername]['FOLDER'][$deviceid]; } return [ 'ALARMS' => ($config['S'] ?? 0) == 2, ]; } /** * Gets an item from a folder by UID. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param string $uid Requested object UID * * @return array|rcube_message|null Object properties */ public function getItem($folderid, $deviceid, $type, $uid) { if ($type == self::MODEL_EMAIL) { $foldername = $this->folder_id2name($folderid, $deviceid); $message = new rcube_message($uid, $foldername); if (!empty($message->headers)) { if ($this->relationSupport) { - $message->headers->others['categories'] = $this->getCategories($message); + $message->headers->others['categories'] = $this->getCategories($message); } return $message; } return null; } $folder = $this->getFolder($folderid, $deviceid, $type); if (!$folder || !$folder->valid) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $result = $folder->get_object($uid); if ($result === false) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES); if ($useTags) { $result['categories'] = $this->getCategories($uid, $result['categories'] ?? []); } return $result; } /** * Gets items matching UID by prefix. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param string $uid Requested object UID prefix * * @return array|iterable List of objects */ public function getItemsByUidPrefix($folderid, $deviceid, $type, $uid) { $folder = $this->getFolder($folderid, $deviceid, $type); if (!$folder || !$folder->valid) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $result = $folder->select([['uid', '~*', $uid]]); if ($result === null) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } return $result; } /** * Move an item from one folder to another. * * @param string $srcFolderId Source folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param string $uid Object UID * @param string $dstFolderId Destination folder identifier * * @return string New object UID * @throws Syncroton_Exception_Status */ public function moveItem($srcFolderId, $deviceid, $type, $uid, $dstFolderId) { if ($type === self::MODEL_EMAIL) { $src_name = $this->folder_id2name($srcFolderId, $deviceid); $dst_name = $this->folder_id2name($dstFolderId, $deviceid); if ($dst_name === null) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION); } if ($src_name === null) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } if (!$this->storage->move_message($uid, $dst_name, $src_name)) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION); } // Use COPYUID feature (RFC2359) to get the new UID of the copied message if (empty($this->storage->conn->data['COPYUID'])) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } return $this->storage->conn->data['COPYUID'][1]; } $srcFolder = $this->getFolder($srcFolderId, $deviceid, $type); $dstFolder = $this->getFolder($dstFolderId, $deviceid, $type); if (!$srcFolder || !$dstFolder) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION); } if (!$srcFolder->move($uid, $dstFolder)) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } return $uid; } /** * Set categories to an object * * @param object|string $object UID or rcube_message object * @param array $categories List of Category names */ public function setCategories($object, $categories) { if (!is_object($object)) { $config = kolab_storage_config::get_instance(); $config->save_tags($object, $categories); return; } $config = kolab_storage_config::get_instance(); $delta = Syncroton_Registry::getPingTimeout(); $uri = kolab_storage_config::get_message_uri($object->headers, $object->folder); // for all tag objects... foreach ($config->get_tags() as $relation) { // resolve members if it wasn't done recently $uid = $relation['uid']; $force = empty($this->tag_rts[$uid]) || $this->tag_rts[$uid] <= time() - $delta; if ($force) { $config->resolve_members($relation, $force); $this->tag_rts[$relation['uid']] = time(); } $selected = !empty($categories) && in_array($relation['name'], $categories); $found = !empty($relation['members']) && in_array($uri, $relation['members']); $update = false; // remove member from the relation if ($found && !$selected) { $relation['members'] = array_diff($relation['members'], (array) $uri); $update = true; } // add member to the relation - else if (!$found && $selected) { + elseif (!$found && $selected) { $relation['members'][] = $uri; $update = true; } if ($update) { $config->save($relation, 'relation'); } $categories = array_diff($categories, (array) $relation['name']); } // create new relations if (!empty($categories)) { foreach ($categories as $tag) { $relation = [ 'name' => $tag, 'members' => (array) $uri, 'category' => 'tag', ]; $config->save($relation, 'relation'); } } // make sure current folder is set correctly again $this->storage->set_folder($object->folder); } /** * Search for existing objects in a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param array $filter Filter * @param int $result_type Type of the result (see kolab_sync_data::RESULT_* constants) * @param bool $force Force IMAP folder cache synchronization * * @return array|int Search result as count or array of uids */ public function searchEntries($folderid, $deviceid, $type, $filter, $result_type, $force) { if ($type != self::MODEL_EMAIL) { return $this->searchKolabEntries($folderid, $deviceid, $type, $filter, $result_type, $force); } $filter_str = 'ALL UNDELETED'; // convert filter into one IMAP search string foreach ($filter as $idx => $filter_item) { if (is_array($filter_item)) { // This is a request for changes since last time // we'll use HIGHESTMODSEQ value from the last Sync if ($filter_item[0] == 'changed' && $filter_item[1] == '>') { $modseq_lasttime = $filter_item[2]; $modseq_data = []; $modseq = (array) $this->modseq_get($deviceid, $folderid, $modseq_lasttime); } - } - else { + } else { $filter_str .= ' ' . $filter_item; } } // get members of modified relations if ($this->relationSupport) { $changed_msgs = $this->getChangesByRelations($folderid, $deviceid, $type, $filter); } $result = $result_type == kolab_sync_data::RESULT_COUNT ? 0 : []; $foldername = $this->folder_id2name($folderid, $deviceid); if ($foldername === null) { return $result; } $this->storage->set_folder($foldername); // Synchronize folder (if it wasn't synced in this request already) if ($force) { $this->storage->folder_sync($foldername); } // We're in "get changes" mode if (isset($modseq_data)) { $folder_data = $this->storage->folder_data($foldername); $modified = false; // If previous HIGHESTMODSEQ doesn't exist we can't get changes // We can only get folder's HIGHESTMODSEQ value and store it for the next try // Skip search if HIGHESTMODSEQ didn't change if (!empty($folder_data['HIGHESTMODSEQ'])) { $modseq_data[$foldername] = $folder_data['HIGHESTMODSEQ']; - $modseq_old = isset($modseq[$foldername]) ? $modseq[$foldername] : null; + $modseq_old = $modseq[$foldername] ?? null; if ($modseq_data[$foldername] != $modseq_old) { $modseq_update = true; if (!empty($modseq) && $modseq_old) { $modified = true; $filter_str .= " MODSEQ " . ($modseq_old + 1); } } } - } - else { + } else { $modified = true; } // We could use messages cache by replacing search() with index() // in some cases. This however is possible only if user has skip_deleted=true, // in his Roundcube preferences, otherwise we'd make often cache re-initialization, // because Roundcube message cache can work only with one skip_deleted // setting at a time. We'd also need to make sure folder_sync() was called // before (see above). // // if ($filter_str == 'ALL UNDELETED') // $search = $this->storage->index($foldername, null, null, true, true); // else if ($modified) { $search = $this->storage->search_once($foldername, $filter_str); if (!($search instanceof rcube_result_index) || $search->is_error()) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } switch ($result_type) { - case kolab_sync_data::RESULT_COUNT: - $result = $search->count(); - break; + case kolab_sync_data::RESULT_COUNT: + $result = $search->count(); + break; - case kolab_sync_data::RESULT_UID: - $result = $search->get(); - break; + case kolab_sync_data::RESULT_UID: + $result = $search->get(); + break; } } // handle relation changes if (!empty($changed_msgs)) { $members = $this->findRelationMembersInFolder($foldername, $changed_msgs, $filter); switch ($result_type) { - case kolab_sync_data::RESULT_COUNT: - $result += count($members); - break; + case kolab_sync_data::RESULT_COUNT: + $result += count($members); + break; - case kolab_sync_data::RESULT_UID: - $result = array_values(array_unique(array_merge($result, $members))); - break; + case kolab_sync_data::RESULT_UID: + $result = array_values(array_unique(array_merge($result, $members))); + break; } } if (!empty($modseq_update) && !empty($modseq_data)) { $this->modseq_set($deviceid, $folderid, $this->syncTimeStamp, $modseq_data); // if previous modseq information does not exist save current set as it, // we would at least be able to detect changes since now if (empty($result) && empty($modseq)) { $this->modseq_set($deviceid, $folderid, $modseq_lasttime ?? 0, $modseq_data); } } return $result; } /** * Search for existing objects in a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * @param array $filter Filter * @param int $result_type Type of the result (see kolab_sync_data::RESULT_* constants) * @param bool $force Force IMAP folder cache synchronization * * @return array|int Search result as count or array of uids */ protected function searchKolabEntries($folderid, $deviceid, $type, $filter, $result_type, $force) { // there's a PHP Warning from kolab_storage if $filter isn't an array if (empty($filter)) { $filter = []; } elseif ($this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES)) { $changed_objects = $this->getChangesByRelations($folderid, $deviceid, $type, $filter); } $folder = $this->getFolder($folderid, $deviceid, $type); if (!$folder || !$folder->valid) { throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } $error = false; switch ($result_type) { - case kolab_sync_data::RESULT_COUNT: - $count = $folder->count($filter); + case kolab_sync_data::RESULT_COUNT: + $count = $folder->count($filter); - if ($count === null) { - throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); - } + if ($count === null) { + throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); + } - $result = (int) $count; - break; + $result = (int) $count; + break; - case kolab_sync_data::RESULT_UID: - default: - $uids = $folder->get_uids($filter); + case kolab_sync_data::RESULT_UID: + default: + $uids = $folder->get_uids($filter); - if (!is_array($uids)) { - throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); - } + if (!is_array($uids)) { + throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); + } - $result = $uids; - break; + $result = $uids; + break; } // handle tag modifications if (!empty($changed_objects)) { // build new filter // search objects mathing current filter, // relations may contain members of many types, we need to // search them by UID in all requested folders to get // only these with requested type (and that really exist // in specified folders) $tag_filter = [['uid', '=', $changed_objects]]; foreach ($filter as $f) { if ($f[0] != 'changed') { $tag_filter[] = $f; } } switch ($result_type) { - case kolab_sync_data::RESULT_COUNT: - // Note: this way we're potentally counting the same objects twice - // I'm not sure if this is a problem, we most likely do not - // need a precise result here - $count = $folder->count($tag_filter); - if ($count !== null) { - $result += (int) $count; - } + case kolab_sync_data::RESULT_COUNT: + // Note: this way we're potentally counting the same objects twice + // I'm not sure if this is a problem, we most likely do not + // need a precise result here + $count = $folder->count($tag_filter); + if ($count !== null) { + $result += (int) $count; + } - break; + break; - case kolab_sync_data::RESULT_UID: - default: - $uids = $folder->get_uids($tag_filter); - if (is_array($uids) && !empty($uids)) { - $result = array_unique(array_merge($result, $uids)); - } + case kolab_sync_data::RESULT_UID: + default: + $uids = $folder->get_uids($tag_filter); + if (is_array($uids) && !empty($uids)) { + $result = array_unique(array_merge($result, $uids)); + } - break; + break; } } return $result; } /** * Find members (messages) in specified folder */ protected function findRelationMembersInFolder($foldername, $members, $filter) { foreach ($members as $member) { // IMAP URI members if ($url = kolab_storage_config::parse_member_url($member)) { $result[$url['folder']][$url['uid']] = $url['params']; } } // convert filter into one IMAP search string $filter_str = 'ALL UNDELETED'; foreach ($filter as $filter_item) { if (is_string($filter_item)) { $filter_str .= ' ' . $filter_item; } } $found = []; // first find messages by UID if (!empty($result[$foldername])) { $index = $this->storage->search_once($foldername, 'UID ' . rcube_imap_generic::compressMessageSet(array_keys($result[$foldername]))); $found = $index->get(); // remove found messages from the $result if (!empty($found)) { $result[$foldername] = array_diff_key($result[$foldername], array_flip($found)); if (empty($result[$foldername])) { unset($result[$foldername]); } // now apply the current filter to the found messages $index = $this->storage->search_once($foldername, $filter_str . ' UID ' . rcube_imap_generic::compressMessageSet($found)); $found = $index->get(); } } // search by message parameters if (!empty($result)) { // @TODO: do this search in chunks (for e.g. 25 messages)? $search = ''; $search_count = 0; foreach ($result as $data) { foreach ($data as $p) { $search_params = []; $search_count++; foreach ($p as $key => $val) { $key = strtoupper($key); // don't search by subject, we don't want false-positives if ($key != 'SUBJECT') { $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val); } } $search .= ' (' . implode(' ', $search_params) . ')'; } } - $search_str = str_repeat(' OR', $search_count-1) . $search; + $search_str = str_repeat(' OR', $search_count - 1) . $search; // search messages in current folder $search = $this->storage->search_once($foldername, $search_str); $uids = $search->get(); if (!empty($uids)) { // add UIDs into the result $found = array_unique(array_merge($found, $uids)); } } return $found; } /** * Detect changes of relation (tag) objects data and assigned objects * Returns relation member identifiers */ protected function getChangesByRelations($folderid, $deviceid, $type, $filter) { // get period filter, create new objects filter foreach ($filter as $f) { if ($f[0] == 'changed' && $f[1] == '>') { $since = $f[2]; } } // this is not search for changes, do nothing if (empty($since)) { return; } // get relations state from the last sync $last_state = (array) $this->relations_state_get($deviceid, $folderid, $since); // get current relations state $config = kolab_storage_config::get_instance(); $default = true; $filter = [ ['type', '=', 'relation'], - ['category', '=', 'tag'] + ['category', '=', 'tag'], ]; $relations = $config->get_objects($filter, $default, 100); $result = []; $changed = false; // compare states, get members of changed relations foreach ($relations as $relation) { $rel_id = $relation['uid']; if ($relation['changed']) { $relation['changed']->setTimezone(new DateTimeZone('UTC')); } // last state unknown... if (empty($last_state[$rel_id])) { // ...get all members if (!empty($relation['members'])) { $changed = true; $result = array_merge($result, $relation['members']); } } // last state known, changed tag name... - else if ($last_state[$rel_id]['name'] != $relation['name']) { + elseif ($last_state[$rel_id]['name'] != $relation['name']) { // ...get all (old and new) members $members_old = explode("\n", $last_state[$rel_id]['members']); $changed = true; $members = array_unique(array_merge($relation['members'], $members_old)); $result = array_merge($result, $members); } // last state known, any other change change... - else if ($last_state[$rel_id]['changed'] < $relation['changed']->format('U')) { + elseif ($last_state[$rel_id]['changed'] < $relation['changed']->format('U')) { // ...find new and removed members $members_old = explode("\n", $last_state[$rel_id]['members']); $new = array_diff($relation['members'], $members_old); $removed = array_diff($members_old, $relation['members']); if (!empty($new) || !empty($removed)) { $changed = true; $result = array_merge($result, $new, $removed); } } unset($last_state[$rel_id]); } // get members of deleted relations if (!empty($last_state)) { $changed = true; foreach ($last_state as $relation) { $members = explode("\n", $relation['members']); $result = array_merge($result, $members); } } // save current state if ($changed) { $data = []; foreach ($relations as $relation) { $data[$relation['uid']] = [ 'name' => $relation['name'], 'changed' => $relation['changed']->format('U'), 'members' => implode("\n", (array)$relation['members']), ]; } $now = new DateTime('now', new DateTimeZone('UTC')); $this->relations_state_set($deviceid, $folderid, $now, $data); } // in mail mode return only message URIs if ($type == self::MODEL_EMAIL) { // lambda function to skip email members - $filter_func = function($value) { + $filter_func = function ($value) { return strpos($value, 'imap://') === 0; }; $result = array_filter(array_unique($result), $filter_func); } // otherwise return only object UIDs else { // lambda function to skip email members - $filter_func = function($value) { + $filter_func = function ($value) { return strpos($value, 'urn:uuid:') === 0; }; // lambda function to parse member URI - $member_func = function($value) { + $member_func = function ($value) { if (strpos($value, 'urn:uuid:') === 0) { $value = substr($value, 9); } return $value; }; $result = array_map($member_func, array_filter(array_unique($result), $filter_func)); } return $result; } /** * Subscribe default set of folders on device registration */ protected function device_init_subscriptions($deviceid) { // INBOX always exists $this->folder_set('INBOX', $deviceid, 1); $supported_types = [ 'mail.drafts', 'mail.wastebasket', 'mail.sentitems', 'mail.outbox', 'event.default', 'contact.default', 'note.default', 'task.default', 'event', 'contact', 'note', 'task', 'event.confidential', 'event.private', 'task.confidential', 'task.private', ]; $rcube = rcube::get_instance(); $config = $rcube->config; $mode = (int) $config->get('activesync_init_subscriptions'); $folders = []; // Subscribe to default folders $foldertypes = kolab_storage::folders_typedata(); if (!empty($foldertypes)) { $_foldertypes = array_intersect($foldertypes, $supported_types); // get default folders foreach ($_foldertypes as $folder => $type) { // only personal folders if ($this->storage->folder_namespace($folder) == 'personal') { $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; $this->folder_set($folder, $deviceid, $flag); $folders[] = $folder; } } } // we're in default mode, exit if (!$mode) { return; } // below we support additionally all mail folders $supported_types[] = 'mail'; $supported_types[] = 'mail.junkemail'; // get configured special folders $special_folders = []; $map = [ 'drafts' => 'mail.drafts', 'junk' => 'mail.junkemail', 'sent' => 'mail.sentitems', 'trash' => 'mail.wastebasket', ]; foreach ($map as $folder => $type) { if ($folder = $config->get($folder . '_mbox')) { $special_folders[$folder] = $type; } } // get folders list(s) if (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) { $all_folders = $this->storage->list_folders(); if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) { $subscribed_folders = $this->storage->list_folders_subscribed(); } - } - else { + } else { $all_folders = $this->storage->list_folders_subscribed(); } foreach ($all_folders as $folder) { // folder already subscribed if (in_array($folder, $folders)) { continue; } $type = ($foldertypes[$folder] ?? null) ?: 'mail'; if ($type == 'mail' && isset($special_folders[$folder])) { $type = $special_folders[$folder]; } if (!in_array($type, $supported_types)) { continue; } $ns = strtoupper($this->storage->folder_namespace($folder)); // subscribe the folder according to configured mode // and folder namespace/subscription status if (($mode & constant("self::INIT_ALL_{$ns}")) || (($mode & constant("self::INIT_SUB_{$ns}")) && (!isset($subscribed_folders) || in_array($folder, $subscribed_folders))) ) { $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; $this->folder_set($folder, $deviceid, $flag); } } } /** * Helper method to decode saved IMAP metadata */ protected function unserialize_metadata($str) { if (!empty($str)) { $data = json_decode($str, true); return $data; } return null; } /** * Helper method to encode IMAP metadata for saving */ protected function serialize_metadata($data) { if (!empty($data) && is_array($data)) { $data = json_encode($data); return $data; } return null; } /** * Returns Kolab folder type for specified ActiveSync type ID */ protected static function type_activesync2kolab($type) { if (!empty(self::$types[$type])) { return self::$types[$type]; } return ''; } /** * Returns ActiveSync folder type for specified Kolab type */ protected static function type_kolab2activesync($type) { $type = preg_replace('/\.(confidential|private)$/i', '', $type); if ($key = array_search($type, self::$types)) { return $key; } return key(self::$types); } /** * Returns folder data in Syncroton format */ protected function folder_data($folder, $type) { // Folder name parameters $delim = $this->storage->get_hierarchy_delimiter(); $items = explode($delim, $folder); $name = array_pop($items); // Folder UID $folder_id = $this->folder_id($folder, $type); // Folder type if (strcasecmp($folder, 'INBOX') === 0) { // INBOX is always inbox, prevent from issues related with a change of // folder type annotation (it can be initially unset). $type = 2; - } - else { + } else { $type = self::type_kolab2activesync($type); // fix type, if there's no type annotation it's detected as UNKNOWN we'll use 'mail' (12) if ($type == 1) { $type = 12; } } // Syncroton folder data array return [ 'serverId' => $folder_id, 'parentId' => count($items) ? $this->folder_id(implode($delim, $items), $type) : 0, 'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET), 'type' => $type, // for internal use 'imap_name' => $folder, ]; } /** * Builds folder ID based on folder name */ protected function folder_id($name, $type = null) { // ActiveSync expects folder identifiers to be max.64 characters // So we can't use just folder name $name = (string) $name; if ($name === '') { return null; } if (isset($this->folder_uids[$name])) { return $this->folder_uids[$name]; } -/* - @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves. - There's one inconvenience of this solution: folder name/type change - would be handled in ActiveSync as delete + create. + /* + @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves. + There's one inconvenience of this solution: folder name/type change + would be handled in ActiveSync as delete + create. - // get folders unique identifier - $folderdata = $this->storage->get_metadata($name, self::UID_KEY); + // get folders unique identifier + $folderdata = $this->storage->get_metadata($name, self::UID_KEY); - if ($folderdata && !empty($folderdata[$name])) { - $uid = $folderdata[$name][self::UID_KEY]; - return $this->folder_uids[$name] = $uid; - } -*/ + if ($folderdata && !empty($folderdata[$name])) { + $uid = $folderdata[$name][self::UID_KEY]; + return $this->folder_uids[$name] = $uid; + } + */ if (strcasecmp($name, 'INBOX') === 0) { // INBOX is always inbox, prevent from issues related with a change of // folder type annotation (it can be initially unset). $type = 'mail.inbox'; - } - else { + } else { if ($type === null) { $type = kolab_storage::folder_type($name); } if ($type != null) { $type = preg_replace('/\.(confidential|private)$/i', '', $type); } } // Add type to folder UID hash, so type change can be detected by Syncroton $uid = $name . '!!' . $type; $uid = md5($uid); return $this->folder_uids[$name] = $uid; } /** * Returns IMAP folder name * * @param string $id Folder identifier * @param string $deviceid Device dentifier * * @return string|null Folder name (UTF7-IMAP) */ public function folder_id2name($id, $deviceid) { // check in cache first if (!empty($this->folder_uids)) { if (($name = array_search($id, $this->folder_uids)) !== false) { return $name; } } -/* - @TODO: see folder_id() + /* + @TODO: see folder_id() - // get folders unique identifier - $folderdata = $this->storage->get_metadata('*', self::UID_KEY); + // get folders unique identifier + $folderdata = $this->storage->get_metadata('*', self::UID_KEY); - foreach ((array)$folderdata as $folder => $data) { - if (!empty($data[self::UID_KEY])) { - $uid = $data[self::UID_KEY]; - $this->folder_uids[$folder] = $uid; - if ($uid == $id) { - $name = $folder; + foreach ((array)$folderdata as $folder => $data) { + if (!empty($data[self::UID_KEY])) { + $uid = $data[self::UID_KEY]; + $this->folder_uids[$folder] = $uid; + if ($uid == $id) { + $name = $folder; + } + } } - } - } -*/ + */ // get all folders of specified type $folderdata = $this->folder_meta(); if (!is_array($folderdata) || empty($id)) { return null; } $name = null; // check if folders are "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } if ($uid = $this->folder_id($folder)) { $this->folder_uids[$folder] = $uid; } if ($uid === $id) { $name = $folder; } } return $name; } /** * Save MODSEQ value for a folder */ protected function modseq_set($deviceid, $folderid, $synctime, $data) { $synctime = $synctime->format('Y-m-d H:i:s'); $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); $old_data = $this->modseq[$folderid][$synctime] ?? null; if (empty($old_data)) { $this->modseq[$folderid][$synctime] = $data; $data = json_encode($data); $db->set_option('ignore_key_errors', true); - $db->query("INSERT INTO `syncroton_modseq` (`device_id`, `folder_id`, `synctime`, `data`)" - ." VALUES (?, ?, ?, ?)", - $deviceid, $folderid, $synctime, $data); + $db->query( + "INSERT INTO `syncroton_modseq` (`device_id`, `folder_id`, `synctime`, `data`)" + . " VALUES (?, ?, ?, ?)", + $deviceid, + $folderid, + $synctime, + $data + ); $db->set_option('ignore_key_errors', false); } } /** * Get stored MODSEQ value for a folder */ protected function modseq_get($deviceid, $folderid, $synctime) { $synctime = $synctime->format('Y-m-d H:i:s'); if (empty($this->modseq[$folderid][$synctime])) { $this->modseq[$folderid] = []; $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); - $db->limitquery("SELECT `data`, `synctime` FROM `syncroton_modseq`" - ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?" - ." ORDER BY `synctime` DESC", - 0, 1, $deviceid, $folderid, $synctime); + $db->limitquery( + "SELECT `data`, `synctime` FROM `syncroton_modseq`" + . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?" + . " ORDER BY `synctime` DESC", + 0, + 1, + $deviceid, + $folderid, + $synctime + ); if ($row = $db->fetch_assoc()) { $synctime = $row['synctime']; // @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format $this->modseq[$folderid][$synctime] = json_decode($row['data'], true); } // Cleanup: remove all records except the current one - $db->query("DELETE FROM `syncroton_modseq`" - ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?", - $deviceid, $folderid, $synctime); + $db->query( + "DELETE FROM `syncroton_modseq`" + . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?", + $deviceid, + $folderid, + $synctime + ); } return $this->modseq[$folderid][$synctime] ?? null; } /** * Set state of relation objects at specified point in time */ public function relations_state_set($deviceid, $folderid, $synctime, $relations) { $synctime = $synctime->format('Y-m-d H:i:s'); $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); $old_data = $this->relations[$folderid][$synctime] ?? null; if (empty($old_data)) { $this->relations[$folderid][$synctime] = $relations; $data = rcube_charset::clean(json_encode($relations)); $db->set_option('ignore_key_errors', true); - $db->query("INSERT INTO `syncroton_relations_state`" - ." (`device_id`, `folder_id`, `synctime`, `data`)" - ." VALUES (?, ?, ?, ?)", - $deviceid, $folderid, $synctime, $data); + $db->query( + "INSERT INTO `syncroton_relations_state`" + . " (`device_id`, `folder_id`, `synctime`, `data`)" + . " VALUES (?, ?, ?, ?)", + $deviceid, + $folderid, + $synctime, + $data + ); $db->set_option('ignore_key_errors', false); } } /** * Get state of relation objects at specified point in time */ protected function relations_state_get($deviceid, $folderid, $synctime) { $synctime = $synctime->format('Y-m-d H:i:s'); if (empty($this->relations[$folderid][$synctime])) { $this->relations[$folderid] = []; $rcube = rcube::get_instance(); $db = $rcube->get_dbh(); - $db->limitquery("SELECT `data`, `synctime` FROM `syncroton_relations_state`" - ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?" - ." ORDER BY `synctime` DESC", - 0, 1, $deviceid, $folderid, $synctime); + $db->limitquery( + "SELECT `data`, `synctime` FROM `syncroton_relations_state`" + . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?" + . " ORDER BY `synctime` DESC", + 0, + 1, + $deviceid, + $folderid, + $synctime + ); if ($row = $db->fetch_assoc()) { $synctime = $row['synctime']; // @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format $this->relations[$folderid][$synctime] = json_decode($row['data'], true); } // Cleanup: remove all records except the current one - $db->query("DELETE FROM `syncroton_relations_state`" - ." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?", - $deviceid, $folderid, $synctime); + $db->query( + "DELETE FROM `syncroton_relations_state`" + . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?", + $deviceid, + $folderid, + $synctime + ); } return $this->relations[$folderid][$synctime] ?? null; } /** * Return last storage error */ public static function last_error() { return kolab_storage::$last_error; } /** * Compares two arrays * * @param array $array1 * @param array $array2 * * @return bool True if arrays differs, False otherwise */ protected static function data_array_diff($array1, $array2) { if (!is_array($array1) || !is_array($array2)) { return $array1 != $array2; } if (count($array1) != count($array2)) { return true; } foreach ($array1 as $key => $val) { if (!array_key_exists($key, $array2)) { return true; } if ($val !== $array2[$key]) { return true; } } return false; } } diff --git a/lib/kolab_sync_storage_kolab4.php b/lib/kolab_sync_storage_kolab4.php index 6ec4e13..b15b22a 100644 --- a/lib/kolab_sync_storage_kolab4.php +++ b/lib/kolab_sync_storage_kolab4.php @@ -1,566 +1,563 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Storage handling class with Kolab 4 support (IMAP + CalDAV + CardDAV) */ class kolab_sync_storage_kolab4 extends kolab_sync_storage { protected $davStorage = null; protected $relationSupport = false; /** * This implements the 'singleton' design pattern * * @return kolab_sync_storage_kolab4 The one and only instance */ public static function get_instance() { if (!self::$instance) { self::$instance = new kolab_sync_storage_kolab4(); self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $sync = kolab_sync::get_instance(); if ($sync->username === null || $sync->password === null) { throw new Exception("Unsupported storage handler use!"); } $url = $sync->config->get('activesync_dav_server', 'http://localhost'); if (strpos($url, '://') === false) { $url = 'http://' . $url; } // Inject user+password to the URL, there's no other way to pass it to the DAV client $url = str_replace('://', '://' . rawurlencode($sync->username) . ':' . rawurlencode($sync->password) . '@', $url); $this->davStorage = new kolab_storage_dav($url); // DAV $this->storage = $sync->get_storage(); // IMAP // set additional header used by libkolab $this->storage->set_options([ 'skip_deleted' => true, 'threading' => false, ]); // Disable paging $this->storage->set_pagesize(999999); } /** * Get list of folders available for sync * * @param string $deviceid Device identifier * @param string $type Folder (class) type * @param bool $flat_mode Enables flat-list mode * * @return array|bool List of mailbox folders, False on backend failure */ public function folders_list($deviceid, $type, $flat_mode = false) { $list = []; // get mail folders subscribed for sync if ($type === self::MODEL_EMAIL) { $folderdata = $this->folder_meta(); if (!is_array($folderdata)) { return false; } $special_folders = $this->storage->get_special_folders(true); $type_map = [ 'drafts' => 3, 'trash' => 4, 'sent' => 5, ]; // Get the folders "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } // Force numeric folder name to be a string (T1283) $folder = (string) $folder; // Activesync folder properties $folder_data = $this->folder_data($folder, 'mail'); // Set proper type for special folders if (($type = array_search($folder, $special_folders)) && isset($type_map[$type])) { $folder_data['type'] = $type_map[$type]; } $list[$folder_data['serverId']] = $folder_data; } - } - else if (in_array($type, [self::MODEL_CONTACTS, self::MODEL_CALENDAR, self::MODEL_TASKS])) { + } elseif (in_array($type, [self::MODEL_CONTACTS, self::MODEL_CALENDAR, self::MODEL_TASKS])) { if (!empty($this->folders)) { foreach ($this->folders as $unique_key => $folder) { if (strpos($unique_key, "DAV:$type:") === 0) { $folder_data = $this->folder_data($folder, $type); $list[$folder_data['serverId']] = $folder_data; } } } // TODO: For now all DAV folders are subscribed if (empty($list)) { foreach ($this->davStorage->get_folders($type) as $folder) { $folder_data = $this->folder_data($folder, $type); $list[$folder_data['serverId']] = $folder_data; // Store all folder objects in internal cache, otherwise // Any access to the folder (or list) will invoke excessive DAV requests $unique_key = $folder_data['serverId'] . ":$deviceid:$type"; $this->folders[$unique_key] = $folder; } } } -/* - // TODO - if ($flat_mode) { - $list = $this->folders_list_flat($list, $type, $typedata); - } -*/ + /* + // TODO + if ($flat_mode) { + $list = $this->folders_list_flat($list, $type, $typedata); + } + */ return $list; } /** * Creates folder and subscribes to the device * * @param string $name Folder name (UTF8) * @param int $type Folder (ActiveSync) type * @param string $deviceid Device identifier * @param ?string $parentid Parent folder identifier * * @return string|false New folder identifier on success, False on failure */ public function folder_create($name, $type, $deviceid, $parentid = null) { // Mail folder if ($type <= 6 || $type == 12) { $parent = null; $name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP'); if ($parentid) { $parent = $this->folder_id2name($parentid, $deviceid); if ($parent === null) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND); } } if ($parent !== null) { $delim = $this->storage->get_hierarchy_delimiter(); $name = $parent . $delim . $name; } if ($this->storage->folder_exists($name)) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS); } // TODO: Support setting folder types? $created = $this->storage->create_folder($name, true); if ($created) { // Set ActiveSync subscription flag $this->folder_set($name, $deviceid, 1); return $this->folder_id($name, 'mail'); } // Special case when client tries to create a subfolder of INBOX // which is not possible on Cyrus-IMAP (T2223) if ($parent == 'INBOX' && stripos($this->last_error(), 'invalid') !== false) { throw new Syncroton_Exception('', Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER); } return false; - } - else if ($type == 8 || $type == 13 || $type == 7 || $type == 15 || $type == 9 || $type == 14) { + } elseif ($type == 8 || $type == 13 || $type == 7 || $type == 15 || $type == 9 || $type == 14) { // DAV folder $type = preg_replace('|\..*|', '', self::type_activesync2kolab($type)); // TODO: Folder hierarchy support // Check if folder exists foreach ($this->davStorage->get_folders($type) as $folder) { if ($folder->get_name() == $name) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS); } } $props = ['name' => $name, 'type' => $type]; if ($id = $this->davStorage->folder_update($props)) { return "DAV:{$type}:{$id}"; } return false; } throw new \Exception("Not implemented"); } /** * Renames a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $new_name New folder name (UTF8) * @param ?string $parentid Folder parent identifier * * @return bool True on success, False on failure */ public function folder_rename($folderid, $deviceid, $new_name, $parentid) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { [, $type, $id] = explode(':', $folderid); $props = [ 'id' => $id, 'name' => $new_name, 'type' => $type, ]; // TODO: Folder hierarchy support return $this->davStorage->folder_update($props) !== false; } // Mail folder $old_name = $this->folder_id2name($folderid, $deviceid); if ($parentid) { $parent = $this->folder_id2name($parentid, $deviceid); } $name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP'); if (isset($parent)) { $delim = $this->storage->get_hierarchy_delimiter(); $name = $parent . $delim . $name; } if ($name === $old_name) { return true; } $this->folder_meta = null; return $this->storage->rename_folder($old_name, $name); } /** * Deletes folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * * @return bool True on success, False otherwise */ public function folder_delete($folderid, $deviceid) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { [, $type, $id] = explode(':', $folderid); return $this->davStorage->folder_delete($id, $type) !== false; } // Mail folder $name = $this->folder_id2name($folderid, $deviceid); unset($this->folder_meta[$name]); return $this->storage->delete_folder($name); } /** * Deletes contents of a folder * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param bool $recursive Apply to the folder and its subfolders * * @return bool True on success, False otherwise */ public function folder_empty($folderid, $deviceid, $recursive = false) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { [, $type, $id] = explode(':', $folderid); if ($folder = $this->davStorage->get_folder($id, $type)) { return $folder->delete_all(); } // TODO: $recursive=true return false; } // Mail folder return parent::folder_empty($folderid, $deviceid, $recursive); } /** * Returns folder data in Syncroton format */ protected function folder_data($folder, $type) { // Mail folders if (strpos($type, 'mail') === 0) { return parent::folder_data($folder, $type); } // DAV folders return [ 'serverId' => "DAV:{$type}:{$folder->id}", 'parentId' => 0, // TODO: Folder hierarchy 'displayName' => $folder->get_name(), 'type' => $this->type_kolab2activesync($type), ]; } /** * Builds folder ID based on folder name * * @param string $name Folder name (UTF7-IMAP) * @param string $type Kolab folder type * * @return string|null Folder identifier (up to 64 characters) */ protected function folder_id($name, $type = null) { if (!$type) { $type = 'mail'; } // ActiveSync expects folder identifiers to be max.64 characters // So we can't use just folder name $name = (string) $name; if ($name === '') { return null; } if (strpos($type, 'mail') !== 0) { throw new Exception("Unsupported folder_id() call on a DAV folder"); } if (isset($this->folder_uids[$name])) { return $this->folder_uids[$name]; } -/* - @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves. - There's one inconvenience of this solution: folder name/type change - would be handled in ActiveSync as delete + create. - @TODO: Consider using MAILBOXID (RFC8474) that Cyrus v3 supports - - // get folders unique identifier - $folderdata = $this->storage->get_metadata($name, self::UID_KEY); - - if ($folderdata && !empty($folderdata[$name])) { - $uid = $folderdata[$name][self::UID_KEY]; - return $this->folder_uids[$name] = $uid; - } -*/ + /* + @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves. + There's one inconvenience of this solution: folder name/type change + would be handled in ActiveSync as delete + create. + @TODO: Consider using MAILBOXID (RFC8474) that Cyrus v3 supports + + // get folders unique identifier + $folderdata = $this->storage->get_metadata($name, self::UID_KEY); + + if ($folderdata && !empty($folderdata[$name])) { + $uid = $folderdata[$name][self::UID_KEY]; + return $this->folder_uids[$name] = $uid; + } + */ if (strcasecmp($name, 'INBOX') === 0) { // INBOX is always inbox, prevent from issues related with a change of // folder type annotation (it can be initially unset). $type = 'mail.inbox'; } // Add type to folder UID hash, so type change can be detected by Syncroton $uid = $name . '!!' . $type; $uid = md5($uid); return $this->folder_uids[$name] = $uid; } /** * Returns IMAP folder name * * @param string $id Folder identifier * @param string $deviceid Device dentifier * * @return null|string Folder name (UTF7-IMAP) */ public function folder_id2name($id, $deviceid) { // TODO: This method should become protected and be used for mail folders only if (strpos($id, 'DAV:') === 0) { throw new Exception("Unsupported folder_id2name() call on a DAV folder"); } // check in cache first if (!empty($this->folder_uids)) { if (($name = array_search($id, $this->folder_uids)) !== false) { return $name; } } // get all folders of specified type $folderdata = $this->folder_meta(); if (!is_array($folderdata) || empty($id)) { return null; } // check if folders are "subscribed" for activesync foreach ($folderdata as $folder => $meta) { if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid]) || empty($meta['FOLDER'][$deviceid]['S']) ) { continue; } if ($uid = $this->folder_id($folder, 'mail')) { $this->folder_uids[$folder] = $uid; } if ($uid === $id) { $name = $folder; } } return $name ?? null; } /** * Gets kolab_storage_folder object from Activesync folder ID. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * * @return ?kolab_storage_folder */ public function getFolder($folderid, $deviceid, $type) { if (strpos($folderid, 'DAV:') !== 0) { throw new Exception("Unsupported getFolder() call on a mail folder"); } $unique_key = "$folderid:$deviceid:$type"; if (array_key_exists($unique_key, $this->folders)) { return $this->folders[$unique_key]; } [, $type, $id] = explode(':', $folderid); return $this->folders[$unique_key] = $this->davStorage->get_folder($id, $type); } /** * Gets Activesync preferences for a folder. * * @param string $folderid Folder identifier * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * * @return array Folder preferences */ public function getFolderConfig($folderid, $deviceid, $type) { // TODO: Get "alarms" from the DAV folder props, or implement // a storage for folder properties return [ 'ALARMS' => true, ]; } /** * Return last storage error */ public static function last_error() { // TODO return null; } /** * Subscribe default set of folders on device registration */ protected function device_init_subscriptions($deviceid) { $config = rcube::get_instance()->config; $mode = (int) $config->get('activesync_init_subscriptions'); $subscribed_folders = null; // Special folders only if (!$mode) { $all_folders = $this->storage->get_special_folders(true); // We do not subscribe to the Spam folder by default, same as the old Kolab driver does unset($all_folders['junk']); $all_folders = array_unique(array_merge(['INBOX'], array_values($all_folders))); } // other modes elseif (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) { $all_folders = $this->storage->list_folders(); if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) { $subscribed_folders = $this->storage->list_folders_subscribed(); } - } - else { + } else { $all_folders = $this->storage->list_folders_subscribed(); } foreach ($all_folders as $folder) { $ns = strtoupper($this->storage->folder_namespace($folder)); // subscribe the folder according to configured mode // and folder namespace/subscription status if (!$mode || ($mode & constant("self::INIT_ALL_{$ns}")) || (($mode & constant("self::INIT_SUB_{$ns}")) && ($subscribed_folders === null || in_array($folder, $subscribed_folders))) ) { $this->folder_set($folder, $deviceid, 1); } } // TODO: Subscribe personal DAV folders, for now we assume all are subscribed // TODO: Subscribe shared DAV folders } } diff --git a/lib/kolab_sync_timezone_converter.php b/lib/kolab_sync_timezone_converter.php index 608c1d8..f2743f3 100644 --- a/lib/kolab_sync_timezone_converter.php +++ b/lib/kolab_sync_timezone_converter.php @@ -1,665 +1,656 @@ | | Copyright (C) 2008-2012, Metaways Infosystems GmbH | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Jonas Fischer | +--------------------------------------------------------------------------+ */ /** * Activesync timezone converter */ class kolab_sync_timezone_converter { /** * holds the instance of the singleton * * @var ?kolab_sync_timezone_converter */ private static $_instance; - protected $_startDate = array(); + protected $_startDate = []; /** * If set then the timezone guessing results will be cached. * This is strongly recommended for performance reasons. * * @var rcube_cache */ protected $cache = null; /** * array of offsets known by ActiceSync clients, but unknown by php * @var array */ - protected $_knownTimezones = array( - '0AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' => array( - 'Pacific/Kwajalein' => 'MHT' - ) - ); + protected $_knownTimezones = [ + '0AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' => [ + 'Pacific/Kwajalein' => 'MHT', + ], + ]; - protected $_legacyTimezones = array( + protected $_legacyTimezones = [ // This is an outdated timezone that outlook keeps sending because of an outdate timezone database on windows - 'Lv///0kAcgBhAG4AIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkABAADABcAOwA7AOcDAAAAAEkAcgBhAG4AIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAwAEAAAAAAAAAAAAxP///w==' => array( - 'Asia/Tehran' => '+0330' - ) - ); + 'Lv///0kAcgBhAG4AIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkABAADABcAOwA7AOcDAAAAAEkAcgBhAG4AIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAwAEAAAAAAAAAAAAxP///w==' => [ + 'Asia/Tehran' => '+0330', + ], + ]; /** * don't use the constructor. Use the singleton. * * @param $_logger */ private function __construct() { } /** * don't clone. Use the singleton. */ private function __clone() { } /** * the singleton pattern * * @return kolab_sync_timezone_converter */ public static function getInstance() { - if (self::$_instance === NULL) { + if (self::$_instance === null) { self::$_instance = new kolab_sync_timezone_converter(); } return self::$_instance; } /** * Returns a timezone with an offset matching the time difference * of $dt from $referenceDt. * * If set and matching the offset, kolab_format::$timezone is preferred. * * @param DateTime $dt The date time value for which we * calculate the offset. * @param DateTime $referenceDt The reference value, for instance in UTC. * * @return DateTimeZone|null */ public function getOffsetTimezone($dt, $referenceDt) { $interval = $referenceDt->diff($dt); $tz = new DateTimeZone($interval->format('%R%H%I')); //e.g. +0200 $utcOffset = $tz->getOffset($dt); //Prefer the configured timezone if it matches the offset. if (kolab_format::$timezone) { if (kolab_format::$timezone->getOffset($dt) == $utcOffset) { return kolab_format::$timezone; } } //Look for any timezone with a matching offset. foreach (DateTimeZone::listIdentifiers() as $timezoneIdentifier) { $timezone = new DateTimeZone($timezoneIdentifier); if ($timezone->getOffset($dt) == $utcOffset) { return $timezone; } } return null; } /** * Returns a list of timezones that match to the {@param $_offsets} * * If {@see $_expectedTimezone} is set then the method will terminate as soon * as the expected timezone has matched and the expected timezone will be the * first entry to the returned array. * * @param string|array $_offsets * * @return array */ public function getListOfTimezones($_offsets) { if (is_string($_offsets) && isset($this->_knownTimezones[$_offsets])) { $timezones = $this->_knownTimezones[$_offsets]; - } - elseif (is_string($_offsets) && isset($this->_legacyTimezones[$_offsets])) { + } elseif (is_string($_offsets) && isset($this->_legacyTimezones[$_offsets])) { $timezones = $this->_legacyTimezones[$_offsets]; - } - else { + } else { if (is_string($_offsets)) { // unpack timezone info to array $_offsets = $this->_unpackTimezoneInfo($_offsets); } if (!$this->_validateOffsets($_offsets)) { - return array(); + return []; } $this->_setDefaultStartDateIfEmpty($_offsets); - $timezones = array(); + $timezones = []; foreach (DateTimeZone::listIdentifiers() as $timezoneIdentifier) { $timezone = new DateTimeZone($timezoneIdentifier); if (false !== ($matchingTransition = $this->_checkTimezone($timezone, $_offsets))) { $timezones[$timezoneIdentifier] = $matchingTransition['abbr']; } } } return $timezones; } /** * Returns PHP timezone that matches to the {@param $_offsets} * * If {@see $_expectedTimezone} is set then the method will return this timezone if it matches. * * @param string|array $_offsets Activesync timezone definition * @param string $_expectedTimezone Expected timezone name * * @return string Expected timezone name */ public function getTimezone($_offsets, $_expectedTimezone = null) { $timezones = $this->getListOfTimezones($_offsets); if ($_expectedTimezone && isset($timezones[$_expectedTimezone])) { return $_expectedTimezone; - } - else { + } else { return key($timezones); } } /** * Return packed string for given {@param $_timezone} * * @param string $_timezone Timezone identifier * @param string|int $_startDate Start date * * @return string Packed timezone offsets */ public function encodeTimezone($_timezone, $_startDate = null) { foreach ($this->_knownTimezones as $packedString => $knownTimezone) { if (array_key_exists($_timezone, $knownTimezone)) { return $packedString; } } $offsets = $this->getOffsetsForTimezone($_timezone, $_startDate); return $this->_packTimezoneInfo($offsets); } /** * Returns an encoded timezone representation from $date * * @param DateTime $date The date with the timezone to encode * * @return string|null Timezone name */ public static function encodeTimezoneFromDate($date) { if ($date instanceof DateTime) { $timezone = $date->getTimezone(); if (($tz_name = $timezone->getName()) != 'UTC') { $tzc = self::getInstance(); if ($tz_name = $tzc->encodeTimezone($tz_name, $date->format('Y-m-d'))) { return $tz_name; } } } return null; } /** * Get offsets for given timezone * * @param string $_timezone Timezone identifier * @param string|int $_startDate Start date * * @return array|null Timezone offsets */ public function getOffsetsForTimezone($_timezone, $_startDate = null) { $this->_setStartDate($_startDate); $offsets = $this->_getOffsetsTemplate(); try { $timezone = new DateTimeZone($_timezone); - } - catch (Exception $e) { + } catch (Exception $e) { return null; } - list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']); + [$standardTransition, $daylightTransition] = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']); if ($standardTransition) { - $offsets['bias'] = $standardTransition['offset']/60*-1; + $offsets['bias'] = $standardTransition['offset'] / 60 * -1; if ($daylightTransition) { $offsets = $this->_generateOffsetsForTransition($offsets, $standardTransition, 'standard', $timezone); $offsets = $this->_generateOffsetsForTransition($offsets, $daylightTransition, 'daylight', $timezone); //@todo how do we get the standardBias (is usually 0)? //$offsets['standardBias'] = ... - $offsets['daylightBias'] = ($daylightTransition['offset'] - $standardTransition['offset'])/60*-1; + $offsets['daylightBias'] = ($daylightTransition['offset'] - $standardTransition['offset']) / 60 * -1; $offsets['standardHour'] -= $offsets['daylightBias'] / 60; $offsets['daylightHour'] += $offsets['daylightBias'] / 60; } } return $offsets; } /** * Get offsets for timezone transition * * @param array $_offsets Timezone offsets * @param array $_transition Timezone transition information * @param string $_type Transition type: 'standard' or 'daylight' * @param DateTimeZone $_timezone Timezone of the transition * * @return array */ protected function _generateOffsetsForTransition(array $_offsets, array $_transition, $_type, $_timezone) { $transitionDate = new DateTime($_transition['time'], $_timezone); if ($_transition['offset']) { $transitionDate->modify($_transition['offset'] . ' seconds'); } $_offsets[$_type . 'Month'] = (int) $transitionDate->format('n'); $_offsets[$_type . 'DayOfWeek'] = (int) $transitionDate->format('w'); $_offsets[$_type . 'Minute'] = (int) $transitionDate->format('i'); $_offsets[$_type . 'Hour'] = (int) $transitionDate->format('G'); - for ($i=5; $i>0; $i--) { + for ($i = 5; $i > 0; $i--) { if ($this->_isNthOcurrenceOfWeekdayInMonth($transitionDate, $i)) { $_offsets[$_type . 'Week'] = $i; break; }; } return $_offsets; } /** * Test if the weekday of the given {@param $_timestamp} is the {@param $_occurence}th occurence of this weekday within its month. * * @param DateTime $_datetime * @param int $_occurence [1 to 5, where 5 indicates the final occurrence during the month if that day of the week does not occur 5 times] * * @return bool */ protected function _isNthOcurrenceOfWeekdayInMonth($_datetime, $_occurence) { if ($_occurence <= 1) { return true; } $orig = $_datetime->format('n'); if ($_occurence == 5) { $modified = clone($_datetime); $modified->modify('1 week'); $mod = $modified->format('n'); // modified date is a next month return $mod > $orig || ($mod == 1 && $orig == 12); } $modified = clone($_datetime); $modified->modify(sprintf('-%d weeks', $_occurence - 1)); $mod = $modified->format('n'); if ($mod != $orig) { return false; } $modified = clone($_datetime); $modified->modify(sprintf('-%d weeks', $_occurence)); $mod = $modified->format('n'); // modified month is earlier than original return $mod < $orig || ($mod == 12 && $orig == 1); } /** * Check if the given {@param $_standardTransition} and {@param $_daylightTransition} * match to the object property {@see $_offsets} * * @param array $_standardTransition * @param array $_daylightTransition * @param array $_offsets * @param DateTimeZone $tz * * @return bool */ protected function _checkTransition($_standardTransition, $_daylightTransition, $_offsets, $tz) { if (empty($_standardTransition) || empty($_offsets)) { return false; } $standardOffset = ($_offsets['bias'] + $_offsets['standardBias']) * 60 * -1; // check each condition in a single if statement and break the chain when one condition is not met - for performance reasons if ($standardOffset == $_standardTransition['offset']) { if (empty($_offsets['daylightMonth']) && (empty($_daylightTransition) || empty($_daylightTransition['isdst']))) { // No DST return true; } $daylightOffset = ($_offsets['bias'] + $_offsets['daylightBias']) * 60 * -1; // the milestone is sending a positive value for daylightBias while it should send a negative value - $daylightOffsetMilestone = ($_offsets['bias'] + ($_offsets['daylightBias'] * -1) ) * 60 * -1; + $daylightOffsetMilestone = ($_offsets['bias'] + ($_offsets['daylightBias'] * -1)) * 60 * -1; if ( !empty($_daylightTransition) && ($daylightOffset == $_daylightTransition['offset'] || $daylightOffsetMilestone == $_daylightTransition['offset']) ) { // date-time input here contains UTC timezone specifier (+0000), // we have to convert the date to the requested timezone afterwards. $standardDate = new DateTime($_standardTransition['time']); $daylightDate = new DateTime($_daylightTransition['time']); $standardDate->setTimezone($tz); $daylightDate->setTimezone($tz); if ($standardDate->format('n') == $_offsets['standardMonth'] && $daylightDate->format('n') == $_offsets['daylightMonth'] && $standardDate->format('w') == $_offsets['standardDayOfWeek'] && $daylightDate->format('w') == $_offsets['daylightDayOfWeek'] ) { return $this->_isNthOcurrenceOfWeekdayInMonth($daylightDate, $_offsets['daylightWeek']) && $this->_isNthOcurrenceOfWeekdayInMonth($standardDate, $_offsets['standardWeek']); } } } return false; } /** * decode timezone info from activesync * * @param string $_packedTimezoneInfo the packed timezone info * @return array */ protected function _unpackTimezoneInfo($_packedTimezoneInfo) { $timezoneUnpackString = 'lbias/a64standardName/vstandardYear/vstandardMonth/vstandardDayOfWeek/vstandardWeek/vstandardHour/vstandardMinute/vstandardSecond/vstandardMilliseconds/lstandardBias' . '/a64daylightName/vdaylightYear/vdaylightMonth/vdaylightDayOfWeek/vdaylightWeek/vdaylightHour/vdaylightMinute/vdaylightSecond/vdaylightMilliseconds/ldaylightBias'; $timezoneInfo = unpack($timezoneUnpackString, base64_decode($_packedTimezoneInfo)); if ($timezoneInfo['standardHour'] == 23 && $timezoneInfo['standardMilliseconds'] == 999 && $timezoneInfo['standardMinute'] == 59 && $timezoneInfo['standardSecond'] == 59 ) { $timezoneInfo['standardHour'] = 24; $timezoneInfo['standardMinute'] = 0; $timezoneInfo['standardSecond'] = 0; $timezoneInfo['standardMilliseconds'] = 0; } return $timezoneInfo; } /** * Encode timezone info to activesync * * @param array $_timezoneInfo * * @return string|null */ protected function _packTimezoneInfo($_timezoneInfo) { if (!is_array($_timezoneInfo)) { return null; } // According to e.g. https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime, // 24 is not allowed in the Hour field, and consequently Outlook can't deal with it. // This is the same workaround that Outlook applies. if ($_timezoneInfo['standardHour'] == 24) { $_timezoneInfo['standardHour'] = 23; $_timezoneInfo['standardMinute'] = 59; $_timezoneInfo['standardSecond'] = 59; $_timezoneInfo['standardMilliseconds'] = 999; } $packed = pack( "la64vvvvvvvvla64vvvvvvvvl", $_timezoneInfo['bias'], $_timezoneInfo['standardName'], $_timezoneInfo['standardYear'], $_timezoneInfo['standardMonth'], $_timezoneInfo['standardDayOfWeek'], $_timezoneInfo['standardWeek'], $_timezoneInfo['standardHour'], $_timezoneInfo['standardMinute'], $_timezoneInfo['standardSecond'], $_timezoneInfo['standardMilliseconds'], $_timezoneInfo['standardBias'], $_timezoneInfo['daylightName'], $_timezoneInfo['daylightYear'], $_timezoneInfo['daylightMonth'], $_timezoneInfo['daylightDayOfWeek'], $_timezoneInfo['daylightWeek'], $_timezoneInfo['daylightHour'], $_timezoneInfo['daylightMinute'], $_timezoneInfo['daylightSecond'], $_timezoneInfo['daylightMilliseconds'], $_timezoneInfo['daylightBias'] ); return base64_encode($packed); } /** * Returns complete offsets array with all fields empty * * Used e.g. when reverse-generating ActiveSync Timezone Offset Information * based on a given Timezone, {@see getOffsetsForTimezone} * * @return array */ protected function _getOffsetsTemplate() { - return array( + return [ 'bias' => 0, 'standardName' => '', 'standardYear' => 0, 'standardMonth' => 0, 'standardDayOfWeek' => 0, 'standardWeek' => 0, 'standardHour' => 0, 'standardMinute' => 0, 'standardSecond' => 0, 'standardMilliseconds' => 0, 'standardBias' => 0, 'daylightName' => '', 'daylightYear' => 0, 'daylightMonth' => 0, 'daylightDayOfWeek' => 0, 'daylightWeek' => 0, 'daylightHour' => 0, 'daylightMinute' => 0, 'daylightSecond' => 0, 'daylightMilliseconds' => 0, - 'daylightBias' => 0 - ); + 'daylightBias' => 0, + ]; } /** * Validate and set offsets * * @param array $value * * @return bool Validation result */ protected function _validateOffsets($value) { // validate $value if ((!empty($value['standardMonth']) || !empty($value['standardWeek']) || !empty($value['daylightMonth']) || !empty($value['daylightWeek'])) && (empty($value['standardMonth']) || empty($value['standardWeek']) || empty($value['daylightMonth']) || empty($value['daylightWeek'])) ) { // It is not possible not set standard offsets without setting daylight offsets and vice versa return false; } return true; } /** * Parse and set object property {@see $_startDate} * * @param mixed $_startDate * @return void */ protected function _setStartDate($_startDate) { if (empty($_startDate)) { $this->_setDefaultStartDateIfEmpty(); return; } - $startDateParsed = array(); + $startDateParsed = []; if (is_string($_startDate)) { $startDateParsed['string'] = $_startDate; $startDateParsed['ts'] = strtotime($_startDate); - } - else if (is_int($_startDate)) { + } elseif (is_int($_startDate)) { $startDateParsed['ts'] = $_startDate; $startDateParsed['string'] = date('Y-m-d', $_startDate); - } - else { + } else { $this->_setDefaultStartDateIfEmpty(); return; } $startDateParsed['object'] = new DateTime($startDateParsed['string']); $startDateParsed = array_merge($startDateParsed, getdate($startDateParsed['ts'])); $this->_startDate = $startDateParsed; } /** * Set default value for object property {@see $_startdate} if it is not set yet. * Tries to guess the correct startDate depending on object property {@see $_offsets} and * falls back to current date. * * @param array $_offsets [offsets may be avaluated for a given start year] * @return void */ protected function _setDefaultStartDateIfEmpty($_offsets = null) { if (!empty($this->_startDate)) { return; } if (!empty($_offsets['standardYear'])) { - $this->_setStartDate($_offsets['standardYear'].'-01-01'); - } - else { + $this->_setStartDate($_offsets['standardYear'] . '-01-01'); + } else { $this->_setStartDate(time()); } } /** * Check if the given {@param $_timezone} matches the {@see $_offsets} * and also evaluate the daylight saving time transitions for this timezone if necessary. * * @param DateTimeZone $timezone * @param array $offsets * * @return array|bool */ protected function _checkTimezone(DateTimeZone $timezone, $offsets) { - list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']); + [$standardTransition, $daylightTransition] = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']); if ($this->_checkTransition($standardTransition, $daylightTransition, $offsets, $timezone)) { return $standardTransition; } return false; } /** * Returns the standard and daylight transitions for the given {@param $_timezone} * and {@param $_year}. * * @param DateTimeZone $_timezone * @param int $_year * * @return array */ protected function _getTransitionsForTimezoneAndYear(DateTimeZone $_timezone, $_year) { $standardTransition = null; $daylightTransition = null; $start = mktime(0, 0, 0, 12, 1, $_year - 1); $end = mktime(24, 0, 0, 12, 31, $_year); $transitions = $_timezone->getTransitions($start, $end); if ($transitions === false) { - return array(); + return []; } foreach ($transitions as $index => $transition) { if (date('Y', $transition['ts']) == $_year) { - if (isset($transitions[$index+1]) && date('Y', $transitions[$index]['ts']) == date('Y', $transitions[$index+1]['ts'])) { - $daylightTransition = $transition['isdst'] ? $transition : $transitions[$index+1]; - $standardTransition = $transition['isdst'] ? $transitions[$index+1] : $transition; - } - else { + if (isset($transitions[$index + 1]) && date('Y', $transitions[$index]['ts']) == date('Y', $transitions[$index + 1]['ts'])) { + $daylightTransition = $transition['isdst'] ? $transition : $transitions[$index + 1]; + $standardTransition = $transition['isdst'] ? $transitions[$index + 1] : $transition; + } else { $daylightTransition = $transition['isdst'] ? $transition : null; $standardTransition = $transition['isdst'] ? null : $transition; } break; - } - else if ($index == count($transitions) -1) { + } elseif ($index == count($transitions) - 1) { $standardTransition = $transition; } } - return array($standardTransition, $daylightTransition); + return [$standardTransition, $daylightTransition]; } } diff --git a/lib/kolab_sync_transaction_manager.php b/lib/kolab_sync_transaction_manager.php index 453cb1c..b7f0a33 100644 --- a/lib/kolab_sync_transaction_manager.php +++ b/lib/kolab_sync_transaction_manager.php @@ -1,180 +1,178 @@ | | Copyright (C) 2008-2012, Metaways Infosystems GmbH | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Cornelius Weiss | +--------------------------------------------------------------------------+ */ /** * Transaction Manger for Syncroton * * This is the central class, all transactions within Syncroton must be handled with. * For each supported transactionable (backend) this class start a real transaction on * the first startTransaction request. * * Transactions of all transactionable will be commited at once when all requested transactions * are being commited using this class. * * Transactions of all transactionable will be roll back when one rollBack is requested * using this class. */ class kolab_sync_transaction_manager implements Syncroton_TransactionManagerInterface { /** * @var array holds all transactionables with open transactions */ - protected $_openTransactionables = array(); + protected $_openTransactionables = []; /** * @var array list of all open (not commited) transactions */ - protected $_openTransactions = array(); + protected $_openTransactions = []; /** * @var ?self */ private static $_instance; /** * @var Zend_Log */ protected $_logger; /** * don't clone. Use the singleton. */ private function __clone() { } /** * constructor */ private function __construct() { if (Syncroton_Registry::isRegistered('loggerBackend')) { $this->_logger = Syncroton_Registry::get('loggerBackend'); } } /** * @return self */ public static function getInstance() { - if (self::$_instance === NULL) { - self::$_instance = new kolab_sync_transaction_manager; + if (self::$_instance === null) { + self::$_instance = new kolab_sync_transaction_manager(); } return self::$_instance; } /** * starts a transaction * * @param mixed $_transactionable * * @return string Transaction Id * @throws Exception */ public function startTransaction($_transactionable) { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " startTransaction request"); } if (! in_array($_transactionable, $this->_openTransactionables)) { if ($_transactionable instanceof rcube_db) { $_transactionable->startTransaction(); - } - else { + } else { $this->rollBack(); throw new Syncroton_Exception_UnexpectedValue('Unsupported transactionable!'); } array_push($this->_openTransactionables, $_transactionable); } - $transactionId = sha1(mt_rand(). microtime()); + $transactionId = sha1(mt_rand() . microtime()); array_push($this->_openTransactions, $transactionId); return $transactionId; } /** * commits a transaction * * @param string $_transactionId * @return void */ public function commitTransaction($_transactionId) { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " commitTransaction request for $_transactionId"); } $transactionIdx = array_search($_transactionId, $this->_openTransactions); if ($transactionIdx !== false) { unset($this->_openTransactions[$transactionIdx]); } $numOpenTransactions = count($this->_openTransactions); if ($numOpenTransactions === 0) { foreach ($this->_openTransactionables as $transactionable) { if ($transactionable instanceof rcube_db) { $transactionable->endTransaction(); } } - $this->_openTransactionables = array(); - $this->_openTransactions = array(); - } - else { + $this->_openTransactionables = []; + $this->_openTransactions = []; + } else { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " commiting defered, as there are still $numOpenTransactions in the queue"); } } } /** * perform rollBack on all transactionables with open transactions * * @return void */ public function rollBack() { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " rollBack request, rollBack all transactionables"); } foreach ($this->_openTransactionables as $transactionable) { if ($transactionable instanceof rcube_db) { $transactionable->rollbackTransaction(); } } - $this->_openTransactionables = array(); - $this->_openTransactions = array(); + $this->_openTransactionables = []; + $this->_openTransactions = []; } } diff --git a/tests/Sync/FoldersTest.php b/tests/Sync/FoldersTest.php index a69cd90..5457154 100644 --- a/tests/Sync/FoldersTest.php +++ b/tests/Sync/FoldersTest.php @@ -1,389 +1,389 @@ deleteTestFolder('Test Folder', 'mail'); $this->deleteTestFolder('Test Folder New', 'mail'); $this->deleteTestFolder('Test Contacts Folder', 'contact'); $this->deleteTestFolder('Test Contacts New', 'contact'); $request = << - - - 0 - - EOF; + + + + 0 + + EOF; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // Note: We're expecting activesync_init_subscriptions=0 here. if ($this->isStorageDriver('kolab4')) { $folders = [ ['Calendar', Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR], ['Contacts', Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT], ['INBOX', Syncroton_Command_FolderSync::FOLDERTYPE_INBOX], ['Drafts', Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS], ['Sent', Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL], ['Trash', Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS], ['Tasks', Syncroton_Command_FolderSync::FOLDERTYPE_TASK], ]; } else { $folders = [ ['Calendar', Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR], ['Contacts', Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT], ['INBOX', Syncroton_Command_FolderSync::FOLDERTYPE_INBOX], ['Drafts', Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS], ['Sent', Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL], ['Trash', Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS], ['Notes', Syncroton_Command_FolderSync::FOLDERTYPE_NOTE], ['Tasks', Syncroton_Command_FolderSync::FOLDERTYPE_TASK], ]; } $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(strval(count($folders)), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); foreach ($folders as $idx => $folder) { $this->assertSame($folder[0], $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item($idx)->nodeValue); $this->assertSame((string) $folder[1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item($idx)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item($idx)->nodeValue); } // Test with multi-folder support enabled self::$deviceType = 'iphone'; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); if ($this->isStorageDriver('kolab4')) { $folders = [ ['Calendar', Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED], // Note: Kolab 4 with Cyrus DAV uses Addressbook, but Kolab 3 with iRony would use 'Contacts' ['/^(Contacts|Addressbook)$/', Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED], ['INBOX', Syncroton_Command_FolderSync::FOLDERTYPE_INBOX], ['Drafts', Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS], ['Sent', Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL], ['Trash', Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS], // Note: For now Kolab 4 uses the same Calendar folder for calendar and tasks - ['/^(Tasks|Calendar)$/', Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED] + ['/^(Tasks|Calendar)$/', Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED], ]; } $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(strval(count($folders)), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); foreach ($folders as $idx => $folder) { $displayName = $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item($idx)->nodeValue; if (str_starts_with($folder[0], '/')) { $this->assertMatchesRegularExpression($folder[0], $displayName); } else { $this->assertSame($folder[0], $displayName); } $this->assertSame((string) $folder[1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item($idx)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item($idx)->nodeValue); $idx++; } // After we switched to multi-folder supported mode we expect next FolderSync // to delete the old "collective" folders $request = << - - - 1 - - EOF; + + + + 1 + + EOF; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $deleted = $this->isStorageDriver('kolab4') ? 3 : 4; // No Notes folder in Kolab4 $syncKey = 2; $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame(strval($syncKey), $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(strval($deleted), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); $this->assertSame($deleted, $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete")->length); return $syncKey; } /** * Test FolderCreate command * * @depends testFolderSync */ public function testFolderCreate($syncKey) { // Multi-folder mode self::$deviceType = 'iphone'; // Create a mail folder $folderName1 = 'Test Folder'; $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED; $request = << - - - {$syncKey} - 0 - {$folderName1} - {$folderType} - - EOF; + + + + {$syncKey} + 0 + {$folderName1} + {$folderType} + + EOF; $response = $this->request($request, 'FolderCreate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderCreate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:FolderCreate/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("//ns:FolderCreate/ns:ServerId")->count()); $folder1 = $xpath->query("//ns:FolderCreate/ns:ServerId")->item(0)->nodeValue; // Note: After FolderCreate there are no changes in the following FolderSync expected // Create a contacts folder $folderName2 = 'Test Contacts Folder'; $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; $request = << - - - {$syncKey} - 0 - {$folderName2} - {$folderType} - - EOF; + + + + {$syncKey} + 0 + {$folderName2} + {$folderType} + + EOF; $response = $this->request($request, 'FolderCreate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderCreate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:FolderCreate/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("//ns:FolderCreate/ns:ServerId")->count()); $folder2 = $xpath->query("//ns:FolderCreate/ns:ServerId")->item(0)->nodeValue; // Note: After FolderCreate there are no changes in the following FolderSync expected // TODO: Test folder with a parent return [ 'SyncKey' => $syncKey, 'folders' => [ $folder1, $folder2, - ] + ], ]; } /** * Test FolderUpdate command * * @depends testFolderCreate */ public function testFolderUpdate($params) { // Multi-folder mode self::$deviceType = 'iphone'; // Test renaming a mail folder $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED; $request = << - - - {$params['SyncKey']} - {$params['folders'][0]} - - Test Folder New - {$folderType} - - EOF; + + + + {$params['SyncKey']} + {$params['folders'][0]} + + Test Folder New + {$folderType} + + EOF; $response = $this->request($request, 'FolderUpdate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderUpdate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderUpdate/ns:SyncKey")->item(0)->nodeValue); // Test FolderSync after folder update, get the new folder id (for delete test) $request = << - - - {$params['SyncKey']} - - EOF; + + + + {$params['SyncKey']} + + EOF; $response = $this->request($request, 'FolderSync'); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // Note we expect Add+Delete here, instead of Update (but this could change in the future) $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("//ns:FolderSync/ns:Changes/ns:Add")->length); $this->assertSame(1, $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete")->length); $this->assertSame($params['folders'][0], $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete/ns:ServerId")->item(0)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item(0)->nodeValue); $this->assertSame('Test Folder New', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item(0)->nodeValue); $this->assertSame(strval($folderType), $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item(0)->nodeValue); $params['folders'][0] = $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ServerId")->item(0)->nodeValue; // Test renaming a contacts folder $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; $request = << - - - {$params['SyncKey']} - {$params['folders'][1]} - - Test Contacts New - {$folderType} - - EOF; + + + + {$params['SyncKey']} + {$params['folders'][1]} + + Test Contacts New + {$folderType} + + EOF; $response = $this->request($request, 'FolderUpdate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderUpdate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderUpdate/ns:SyncKey")->item(0)->nodeValue); // Test FolderSync after folder update, get the new folder id (for delete test) $request = << - - - {$params['SyncKey']} - - EOF; + + + + {$params['SyncKey']} + + EOF; $response = $this->request($request, 'FolderSync'); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); if ($this->isStorageDriver('kolab4')) { // Note we expect Update here, not Add+Delete, folder ID does not change $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); $this->assertSame($params['folders'][1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Update/ns:ServerId")->item(0)->nodeValue); $this->assertSame('Test Contacts New', $xpath->query("//ns:FolderSync/ns:Changes/ns:Update/ns:DisplayName")->item(0)->nodeValue); $this->assertSame(strval($folderType), $xpath->query("//ns:FolderSync/ns:Changes/ns:Update/ns:Type")->item(0)->nodeValue); } else { // Note we expect Add+Delete here, instead of Update (but this could change in the future) $this->assertSame('2', $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); $this->assertSame($params['folders'][1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete/ns:ServerId")->item(0)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item(0)->nodeValue); $this->assertSame('Test Contacts New', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item(0)->nodeValue); $this->assertSame(strval($folderType), $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item(0)->nodeValue); $params['folders'][1] = $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ServerId")->item(0)->nodeValue; } // TODO: Test folder with a parent change // TODO: Assert the folder name has changed in the storage // TODO: Test Sync after a DAV folder rename made in another client return $params; } /** * Test FolderDelete command * * @depends testFolderUpdate */ public function testFolderDelete($params) { // Multi-folder mode self::$deviceType = 'iphone'; // Delete mail folder $request = << - - - {$params['SyncKey']} - {$params['folders'][0]} - - EOF; + + + + {$params['SyncKey']} + {$params['folders'][0]} + + EOF; $response = $this->request($request, 'FolderDelete'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderDelete/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderDelete/ns:SyncKey")->item(0)->nodeValue); // Note: After FolderDelete there are no changes in the following FolderSync expected // Delete contacts folder $request = << - - - {$params['SyncKey']} - {$params['folders'][1]} - - EOF; + + + + {$params['SyncKey']} + {$params['folders'][1]} + + EOF; $response = $this->request($request, 'FolderDelete'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderDelete/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderDelete/ns:SyncKey")->item(0)->nodeValue); // Note: After FolderDelete there are no changes in the following FolderSync expected // TODO: Assert the folders no longer exist } } diff --git a/tests/Sync/ItemOperationsTest.php b/tests/Sync/ItemOperationsTest.php index 12374e6..6ce673b 100644 --- a/tests/Sync/ItemOperationsTest.php +++ b/tests/Sync/ItemOperationsTest.php @@ -1,81 +1,81 @@ registerDevice(); // TODO: Test invalid folder ID $collectionId = 'AAAAAAAAAAAA'; $request = << - - - - {$collectionId} - - - - EOF; + + + + + {$collectionId} + + + + EOF; $response = $this->request($request, 'ItemOperations'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:ItemOperations/ns:Status")->item(0)->nodeValue); $this->assertSame( strval(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR), $xpath->query("//ns:ItemOperations/ns:Response/ns:EmptyFolderContents/ns:Status")->item(0)->nodeValue ); $this->assertSame( $collectionId, $xpath->query("//ns:ItemOperations/ns:Response/ns:EmptyFolderContents/AirSync:CollectionId")->item(0)->nodeValue ); // Test Trash folder $collectionId = array_search('Trash', $this->folders); $this->assertIsString($collectionId); $request = << - - - - {$collectionId} - - - - EOF; + + + + + {$collectionId} + + + + EOF; $response = $this->request($request, 'ItemOperations'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:ItemOperations/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:ItemOperations/ns:Response/ns:EmptyFolderContents/ns:Status")->item(0)->nodeValue); $this->assertSame($collectionId, $xpath->query("//ns:ItemOperations/ns:Response/ns:EmptyFolderContents/AirSync:CollectionId")->item(0)->nodeValue); // TODO: Test DAV folder // TODO: Test a folder with subfolders // TODO: Test non-empty folder and assert that all objects are gone $this->markTestIncomplete(); } /** * Test ItemOperations::Fetch request */ public function testFetch() { $this->markTestIncomplete(); } } diff --git a/tests/Sync/MeetingResponseTest.php b/tests/Sync/MeetingResponseTest.php index 54e945d..ef5bb26 100644 --- a/tests/Sync/MeetingResponseTest.php +++ b/tests/Sync/MeetingResponseTest.php @@ -1,131 +1,131 @@ emptyTestFolder($davFolder = 'Calendar', 'event'); $this->emptyTestFolder('INBOX', 'mail'); $this->registerDevice(); // Do the initial INBOX sync $folderId = '38b950ebd62cd9a66929c89615d0fc04'; // INBOX $syncKey = 0; $request = << - - - - - {$syncKey} - {$folderId} - - - - EOF; + + + + + + {$syncKey} + {$folderId} + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:SyncKey")->item(0)->nodeValue); // Append an invitation email, and sync it $sync = \kolab_sync::get_instance(); $replace = [ '$from' => 'test.test@domain.tld', '$to' => $sync->config->get('activesync_test_username'), ]; $this->appendMail('INBOX', 'mail.itip1', $replace); $request = << - - - - - {$syncKey} - {$folderId} - 1 - 1 - 1 - - 0 - 1 - - 2 - 51200 - 0 - - - - - - EOF; + + + + + + {$syncKey} + {$folderId} + 1 + 1 + 1 + + 0 + 1 + + 2 + 51200 + 0 + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("ns:SyncKey", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Add", $root)->count()); $serverId = $xpath->query("ns:Commands/ns:Add/ns:ServerId", $root)->item(0)->nodeValue; $root = $xpath->query("ns:Commands/ns:Add/ns:ApplicationData", $root)->item(0); $this->assertSame('You\'ve been invited to "Test"', $xpath->query("Email:Subject", $root)->item(0)->nodeValue); $this->assertSame('Organizer ', $xpath->query("Email:From", $root)->item(0)->nodeValue); $this->assertSame($replace['$to'], $xpath->query("Email:To", $root)->item(0)->nodeValue); $this->assertSame('0', $xpath->query("Email:Read", $root)->item(0)->nodeValue); $this->assertSame('IPM.Schedule.Meeting.Request', $xpath->query("Email:MessageClass", $root)->item(0)->nodeValue); $this->assertSame('urn:content-classes:calendarmessage', $xpath->query("Email:ContentClass", $root)->item(0)->nodeValue); $root = $xpath->query("Email:MeetingRequest", $root)->item(0); $this->assertSame('0', $xpath->query("Email:AllDayEvent", $root)->item(0)->nodeValue); $this->assertSame('2023-12-07T13:00:00.000Z', $xpath->query("Email:StartTime", $root)->item(0)->nodeValue); $this->assertSame('2023-12-07T13:30:00.000Z', $xpath->query("Email:EndTime", $root)->item(0)->nodeValue); $this->assertSame('test.test@domain.tld', $xpath->query("Email:Organizer", $root)->item(0)->nodeValue); $this->assertSame('1', $xpath->query("Email:ResponseRequested", $root)->item(0)->nodeValue); $this->assertSame('1', $xpath->query("Email:DisallowNewTimeProposal", $root)->item(0)->nodeValue); // Accept the invitation $request = << - - - - 1 - {$folderId} - {$serverId} - - - EOF; + + + + + 1 + {$folderId} + {$serverId} + + + EOF; $response = $this->request($request, 'MeetingResponse'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $xpath->registerNamespace('MeetingResponse', 'uri:MeetingResponse'); $root = $xpath->query("//MeetingResponse:MeetingResponse/MeetingResponse:Result")->item(0); $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame($serverId, $xpath->query("ns:RequestId", $root)->item(0)->nodeValue); $this->assertStringMatchesFormat("CRC%s", $xpath->query("ns:CalendarId", $root)->item(0)->nodeValue); } } diff --git a/tests/Sync/MoveItemsTest.php b/tests/Sync/MoveItemsTest.php index bf99e80..865d756 100644 --- a/tests/Sync/MoveItemsTest.php +++ b/tests/Sync/MoveItemsTest.php @@ -1,334 +1,334 @@ emptyTestFolder('INBOX', 'mail'); $this->emptyTestFolder('Trash', 'mail'); $uid = $this->appendMail('INBOX', 'mail.sync1'); $this->registerDevice(); $inbox = array_search('INBOX', $this->folders); $trash = array_search('Trash', $this->folders); // Initial sync $request = << - - - - - 0 - {$inbox} - - - 0 - {$trash} - - - - EOF; + + + + + + 0 + {$inbox} + + + 0 + {$trash} + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); // Sync mail from INBOX and Trash $request = << - - - - - 1 - {$inbox} - 1 - - - 1 - {$trash} - 1 - - - - EOF; + + + + + + 1 + {$inbox} + 1 + + + 1 + {$trash} + 1 + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame(1, $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->count()); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); $this->assertSame(1, $xpath->query("ns:Commands/ns:Add", $root)->count()); $root = $xpath->query("ns:Commands/ns:Add", $root)->item(0); $this->assertSame('test sync', $xpath->query("ns:ApplicationData/Email:Subject", $root)->item(0)->nodeValue); // Move the message to $trash $request = << - - - - {$inbox}::{$uid} - {$inbox} - {$trash} - - - EOF; + + + + + {$inbox}::{$uid} + {$inbox} + {$trash} + + + EOF; $response = $this->request($request, 'MoveItems'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $xpath->registerNamespace('Move', 'uri:Move'); $root = $xpath->query("//Move:MoveItems/Move:Response")->item(0); $this->assertSame('3', $xpath->query("Move:Status", $root)->item(0)->nodeValue); $this->assertSame("{$inbox}::{$uid}", $xpath->query("Move:SrcMsgId", $root)->item(0)->nodeValue); $serverId = $xpath->query("Move:DstMsgId", $root)->item(0)->nodeValue; // Sync mail from INBOX and Trash $request = << - - - - - 2 - {$inbox} - 1 - - - 1 - {$trash} - 1 - - - - EOF; + + + + + + 2 + {$inbox} + 1 + + + 1 + {$trash} + 1 + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); // INBOX $this->assertSame($inbox, $xpath->query("ns:CollectionId", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Delete", $root)->count()); $this->assertSame("$inbox::$uid", $xpath->query("ns:Commands/ns:Delete/ns:ServerId", $root)->item(0)->nodeValue); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(1); // Trash $this->assertSame($trash, $xpath->query("ns:CollectionId", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Add", $root)->count()); $this->assertSame('test sync', $xpath->query("ns:Commands/ns:Add/ns:ApplicationData/Email:Subject", $root)->item(0)->nodeValue); } /** * Test moving a contact */ public function testMoveContact() { // Test with multi-folder support enabled self::$deviceType = 'iphone'; $davFolder = $this->isStorageDriver('kolab') ? 'Contacts' : 'Addressbook'; $this->emptyTestFolder($davFolder, 'contact'); $this->deleteTestFolder($folderName = 'Test Contacts Folder', 'contact'); $this->appendObject($davFolder, 'contact.vcard1', 'contact'); $this->registerDevice(); $srcFolderId = array_search($davFolder, $this->folders); // Create a contacts folder $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; $request = << - - - 1 - 0 - {$folderName} - {$folderType} - - EOF; + + + + 1 + 0 + {$folderName} + {$folderType} + + EOF; $response = $this->request($request, 'FolderCreate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $dstFolderId = $xpath->query("//ns:FolderCreate/ns:ServerId")->item(0)->nodeValue; // Sync both folders $request = << - - - - - Contacts - 0 - {$srcFolderId} - - - Contacts - 0 - {$dstFolderId} - - - - EOF; + + + + + + Contacts + 0 + {$srcFolderId} + + + Contacts + 0 + {$dstFolderId} + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $request = << - - - - - Contacts - 1 - {$srcFolderId} - - - - - 1 - 5120 - - 1 - - - - Contacts - 1 - {$dstFolderId} - - - - - 1 - 5120 - - 1 - - - - - EOF; + + + + + + Contacts + 1 + {$srcFolderId} + + + + + 1 + 5120 + + 1 + + + + Contacts + 1 + {$dstFolderId} + + + + + 1 + 5120 + + 1 + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); $this->assertSame($srcFolderId, $xpath->query("ns:CollectionId", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Add", $root)->count()); $this->assertSame('Jane', $xpath->query("ns:Commands/ns:Add/ns:ApplicationData/Contacts:FirstName", $root)->item(0)->nodeValue); $srcMsgId = $xpath->query("ns:Commands/ns:Add/ns:ServerId", $root)->item(0)->nodeValue; // Move the message to the other folder $request = << - - - - {$srcMsgId} - {$srcFolderId} - {$dstFolderId} - - - EOF; + + + + + {$srcMsgId} + {$srcFolderId} + {$dstFolderId} + + + EOF; $response = $this->request($request, 'MoveItems'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $xpath->registerNamespace('Move', 'uri:Move'); $root = $xpath->query("//Move:MoveItems/Move:Response")->item(0); $this->assertSame('3', $xpath->query("Move:Status", $root)->item(0)->nodeValue); $this->assertSame($srcMsgId, $xpath->query("Move:SrcMsgId", $root)->item(0)->nodeValue); $dstMsgId = $xpath->query("Move:DstMsgId", $root)->item(0)->nodeValue; // Sync the folders again $request = << - - - - - Contacts - 2 - {$srcFolderId} - - - - - 1 - 5120 - - 1 - - - - Contacts - 1 - {$dstFolderId} - - - - - 1 - 5120 - - 1 - - - - - EOF; + + + + + + Contacts + 2 + {$srcFolderId} + + + + + 1 + 5120 + + 1 + + + + Contacts + 1 + {$dstFolderId} + + + + + 1 + 5120 + + 1 + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); // src folder $this->assertSame($srcFolderId, $xpath->query("ns:CollectionId", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Delete", $root)->count()); $this->assertSame($srcMsgId, $xpath->query("ns:Commands/ns:Delete/ns:ServerId", $root)->item(0)->nodeValue); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(1); // dst folder $this->assertSame($dstFolderId, $xpath->query("ns:CollectionId", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Add", $root)->count()); $this->assertSame('Jane', $xpath->query("ns:Commands/ns:Add/ns:ApplicationData/Contacts:FirstName", $root)->item(0)->nodeValue); $this->assertSame($dstMsgId, $xpath->query("ns:Commands/ns:Add/ns:ServerId", $root)->item(0)->nodeValue); $this->deleteTestFolder($folderName, 'contact'); } } diff --git a/tests/Sync/ProvisionTest.php b/tests/Sync/ProvisionTest.php index b4355a7..454907e 100644 --- a/tests/Sync/ProvisionTest.php +++ b/tests/Sync/ProvisionTest.php @@ -1,45 +1,45 @@ - - - - - moto e(6) plus - 000000000000000 - pokerp_reteu_64 - Android 9.58-8 - Polish (Poland) - - - - - - MS-EAS-Provisioning-WBXML - - - - EOF; + + + + + + moto e(6) plus + 000000000000000 + pokerp_reteu_64 + Android 9.58-8 + Polish (Poland) + + + + + + MS-EAS-Provisioning-WBXML + + + + EOF; $response = $this->request($request, 'Provision'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:Provision/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:Provision/Settings:DeviceInformation/Settings:Status")->item(0)->nodeValue); $this->assertSame('2', $xpath->query("//ns:Provision/ns:Policies/ns:Policy/ns:Status")->item(0)->nodeValue); // TODO: Assert the properties have been set } } diff --git a/tests/Sync/SettingsTest.php b/tests/Sync/SettingsTest.php index c2af198..4ded4d8 100644 --- a/tests/Sync/SettingsTest.php +++ b/tests/Sync/SettingsTest.php @@ -1,80 +1,80 @@ - - - - - - - EOF; + + + + + + + + EOF; $response = $this->request($request, 'Settings'); $this->assertEquals(200, $response->getStatusCode()); $dom = self::fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:Settings/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:Settings/ns:UserInformation/ns:Status")->item(0)->nodeValue); $this->assertSame( self::$username, $xpath->query("//ns:Settings/ns:UserInformation/ns:Get/ns:Accounts/ns:Account/ns:EmailAddresses/ns:PrimarySmtpAddress")->item(0)->nodeValue ); } /** * Test Settings command */ public function testSettingsDeviceInfomation() { // Test device info update $request = << - - - - - moto plus - 111111111 - fn - Android 10 - English - - - - - EOF; + + + + + + moto plus + 111111111 + fn + Android 10 + English + + + + + EOF; $response = $this->request($request, 'Settings'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:Settings/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:Settings/ns:DeviceInformation/ns:Set/ns:Status")->item(0)->nodeValue); // TODO: Assert the properties have been set } /** * Test Settings command regarding OOF */ public function testSettingsOOF() { // TODO: Test OOF settings $this->markTestIncomplete(); } } diff --git a/tests/Sync/Sync/CalendarTest.php b/tests/Sync/Sync/CalendarTest.php index 2323397..f11a3a3 100644 --- a/tests/Sync/Sync/CalendarTest.php +++ b/tests/Sync/Sync/CalendarTest.php @@ -1,75 +1,75 @@ emptyTestFolder($davFolder = 'Calendar', 'event'); $this->registerDevice(); // Test empty folder $folderId = 'Calendar::Syncroton'; $syncKey = 0; $request = << - - - - - Calendar - {$syncKey} - {$folderId} - - - - - - EOF; + + + + + + Calendar + {$syncKey} + {$folderId} + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); // Append two event objects and sync them $this->appendObject($davFolder, 'event.ics1', 'event'); $this->appendObject($davFolder, 'event.ics2', 'event'); $request = str_replace("0", "{$syncKey}", $request); $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(2, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); $root .= "/ns:Commands/ns:Add"; $this->assertStringMatchesFormat("CRC%s", $xpath->query("{$root}/ns:ServerId")->item(0)->nodeValue); $this->assertSame('20240715T170000Z', $xpath->query("{$root}/ns:ApplicationData/Calendar:StartTime")->item(0)->nodeValue); $this->assertSame('Meeting', $xpath->query("{$root}/ns:ApplicationData/Calendar:Subject")->item(0)->nodeValue); $this->assertSame('20240714T170000Z', $xpath->query("{$root}/ns:ApplicationData/Calendar:StartTime")->item(1)->nodeValue); $this->assertSame('Party', $xpath->query("{$root}/ns:ApplicationData/Calendar:Subject")->item(1)->nodeValue); return $syncKey; } } diff --git a/tests/Sync/Sync/ContactsTest.php b/tests/Sync/Sync/ContactsTest.php index aebbebb..5de304f 100644 --- a/tests/Sync/Sync/ContactsTest.php +++ b/tests/Sync/Sync/ContactsTest.php @@ -1,250 +1,250 @@ isStorageDriver('kolab') ? 'Contacts' : 'Addressbook'; $this->emptyTestFolder($davFolder, 'contact'); $this->deleteTestFolder('Test Contacts Folder', 'contact'); // from other test files $this->registerDevice(); // Test empty contacts folder $folderId = 'Contacts::Syncroton'; $syncKey = 0; $request = << - - - - - Contacts - {$syncKey} - {$folderId} - - - - - 1 - 5120 - - 1 - - - - - EOF; + + + + + + Contacts + {$syncKey} + {$folderId} + + + + + 1 + 5120 + + 1 + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(0, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); // Append two contact objects and sync them // TODO: Test a folder with contact groups inside $this->appendObject($davFolder, 'contact.vcard1', 'contact'); $this->appendObject($davFolder, 'contact.vcard2', 'contact'); $request = str_replace("0", "{$syncKey}", $request); $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(2, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); $root .= "/ns:Commands/ns:Add"; $this->assertStringMatchesFormat("CRC%s", $xpath->query("{$root}/ns:ServerId")->item(0)->nodeValue); $this->assertSame('Jack', $xpath->query("{$root}/ns:ApplicationData/Contacts:FirstName")->item(0)->nodeValue); $this->assertSame('Strong', $xpath->query("{$root}/ns:ApplicationData/Contacts:LastName")->item(0)->nodeValue); $this->assertSame('Jane', $xpath->query("{$root}/ns:ApplicationData/Contacts:FirstName")->item(1)->nodeValue); $this->assertSame('Doe', $xpath->query("{$root}/ns:ApplicationData/Contacts:LastName")->item(1)->nodeValue); return $syncKey; } /** * Test adding objects from client * * @depends testSync */ public function testAddFromClient($syncKey) { $request = << - - - - - Contacts - {$syncKey} - Contacts::Syncroton - - - - - 1 - 5120 - - 1 - - - - 42 - - Lars - - - - - - - EOF; + + + + + + Contacts + {$syncKey} + Contacts::Syncroton + + + + + 1 + 5120 + + 1 + + + + 42 + + Lars + + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("ns:SyncKey", $root)->item(0)->nodeValue); $root = $xpath->query("ns:Responses/ns:Add", $root)->item(0); $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame('42', $xpath->query("ns:ClientId", $root)->item(0)->nodeValue); $serverId = $xpath->query("ns:ServerId", $root)->item(0)->nodeValue; $this->assertStringMatchesFormat("CRC%s", $serverId); // TODO: Test the content on the server return [$syncKey, $serverId]; } /** * Test updating objects from client * * @depends testAddFromClient */ public function testChangeFromClient($params) { $request = << - - - - - Contacts - {$params[0]} - Contacts::Syncroton - - - - - 1 - 5120 - - 1 - - - - {$params[1]} - - First - Last - - - - - - - EOF; + + + + + + Contacts + {$params[0]} + Contacts::Syncroton + + + + + 1 + 5120 + + 1 + + + + {$params[1]} + + First + Last + + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame(strval(++$params[0]), $xpath->query("ns:SyncKey", $root)->item(0)->nodeValue); $this->assertSame(0, $xpath->query("ns:Responses", $root)->length); // TODO: Assert updated content on the server return $params; } /** * Test deleting objects from client * * @depends testChangeFromClient */ public function testDeleteFromClient($params) { $request = << - - - - - Contacts - {$params[0]} - Contacts::Syncroton - - - - - 1 - 5120 - - 1 - - - - {$params[1]} - - - - - - EOF; + + + + + + Contacts + {$params[0]} + Contacts::Syncroton + + + + + 1 + 5120 + + 1 + + + + {$params[1]} + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame(strval(++$params[0]), $xpath->query("ns:SyncKey", $root)->item(0)->nodeValue); $this->assertSame(0, $xpath->query("ns:Responses", $root)->length); // TODO: Assert deleted contact on the server } } diff --git a/tests/Sync/Sync/EmailTest.php b/tests/Sync/Sync/EmailTest.php index a45cfc3..77f306e 100644 --- a/tests/Sync/Sync/EmailTest.php +++ b/tests/Sync/Sync/EmailTest.php @@ -1,179 +1,179 @@ emptyTestFolder('INBOX', 'mail'); $this->registerDevice(); // Test invalid collection identifier $request = << - - - - - 0 - 1111111111 - - - - EOF; + + + + + + 0 + 1111111111 + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('12', $xpath->query("//ns:Sync/ns:Status")->item(0)->nodeValue); // Test INBOX $folderId = '38b950ebd62cd9a66929c89615d0fc04'; $syncKey = 0; $request = << - - - - - {$syncKey} - {$folderId} - - - - EOF; + + + + + + {$syncKey} + {$folderId} + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:SyncKey")->item(0)->nodeValue); $this->assertSame('Email', $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:Class")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:CollectionId")->item(0)->nodeValue); // Test listing mail in INBOX, use WindowSize=1 // Append two mail messages $this->appendMail('INBOX', 'mail.sync1'); $this->appendMail('INBOX', 'mail.sync2'); $request = << - - - - - {$syncKey} - {$folderId} - 1 - 1 - 1 - - 0 - 1 - - 2 - 51200 - 0 - - - - - - EOF; + + + + + + {$syncKey} + {$folderId} + 1 + 1 + 1 + + 0 + 1 + + 2 + 51200 + 0 + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); // Note: We assume messages are in IMAP default order, it may change in future $root .= "/ns:Commands/ns:Add"; $this->assertStringMatchesFormat("{$folderId}::%d", $xpath->query("{$root}/ns:ServerId")->item(0)->nodeValue); $this->assertSame('test sync', $xpath->query("{$root}/ns:ApplicationData/Email:Subject")->item(0)->nodeValue); // List the rest of the mail $request = << - - - - - {$syncKey} - {$folderId} - 1 - 1 - - 0 - 1 - - 2 - 51200 - 0 - - - - - - EOF; + + + + + + {$syncKey} + {$folderId} + 1 + 1 + + 0 + 1 + + 2 + 51200 + 0 + + + + + + EOF; $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $root = "//ns:Sync/ns:Collections/ns:Collection"; $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); // Note: We assume messages are in IMAP default order, it may change in future $root .= "/ns:Commands/ns:Add"; $this->assertStringMatchesFormat("{$folderId}::%d", $xpath->query("{$root}/ns:ServerId")->item(0)->nodeValue); $this->assertSame('sync test with attachment', $xpath->query("{$root}/ns:ApplicationData/Email:Subject")->item(0)->nodeValue); return $syncKey; } /** * Test updating message properties from client * * @depends testSync */ public function testChangeFromClient($syncKey) { $this->markTestIncomplete(); } /** * Test deleting messages from client * * @depends testChangeFromClient */ public function testDeleteFromClient($syncKey) { $this->markTestIncomplete(); } } diff --git a/tests/SyncTestCase.php b/tests/SyncTestCase.php index 82e2743..d0f1dd9 100644 --- a/tests/SyncTestCase.php +++ b/tests/SyncTestCase.php @@ -1,362 +1,362 @@ markTestSkipped('Not setup'); } self::$deviceType = null; } /** * {@inheritDoc} */ public static function setUpBeforeClass(): void { $sync = \kolab_sync::get_instance(); $config = $sync->config; $db = $sync->get_dbh(); self::$username = $config->get('activesync_test_username'); self::$password = $config->get('activesync_test_password'); if (empty(self::$username)) { return; } self::$deviceId = 'test' . time(); $db->query('DELETE FROM syncroton_device'); $db->query('DELETE FROM syncroton_synckey'); $db->query('DELETE FROM syncroton_folder'); $db->query('DELETE FROM syncroton_data'); $db->query('DELETE FROM syncroton_data_folder'); $db->query('DELETE FROM syncroton_modseq'); $db->query('DELETE FROM syncroton_content'); self::$client = new \GuzzleHttp\Client([ 'http_errors' => false, 'base_uri' => 'http://localhost:8000', 'verify' => false, 'auth' => [self::$username, self::$password], 'connect_timeout' => 10, 'timeout' => 10, 'headers' => [ 'Content-Type' => 'application/xml; charset=utf-8', 'Depth' => '1', - ] + ], ]); // TODO: execute: php -S localhost:8000 } /** * {@inheritDoc} */ public static function tearDownAfterClass(): void { if (self::$deviceId) { $sync = \kolab_sync::get_instance(); if (self::$authenticated || $sync->authenticate(self::$username, self::$password)) { $sync->password = self::$password; $storage = $sync->storage(); $storage->device_delete(self::$deviceId); } $db = $sync->get_dbh(); $db->query('DELETE FROM syncroton_device'); $db->query('DELETE FROM syncroton_synckey'); $db->query('DELETE FROM syncroton_folder'); } } /** * Append an email message to the IMAP folder */ protected function appendMail($folder, $filename, $replace = []) { $imap = $this->getImapStorage(); $source = __DIR__ . '/src/' . $filename; if (!file_exists($source)) { exit("File does not exist: {$source}"); } $is_file = true; if (!empty($replace)) { $is_file = false; $source = file_get_contents($source); foreach ($replace as $token => $value) { $source = str_replace($token, $value, $source); } } $uid = $imap->save_message($folder, $source, '', $is_file); if ($uid === false) { exit("Failed to append mail into {$folder}"); } return $uid; } /** * Append an DAV object to a DAV/IMAP folder */ protected function appendObject($foldername, $filename, $type) { $path = __DIR__ . '/src/' . $filename; if (!file_exists($path)) { exit("File does not exist: {$path}"); } $content = file_get_contents($path); $uid = preg_match('/UID:(?:urn:uuid:)?([a-z0-9-]+)/', $content, $m) ? $m[1] : null; if (empty($uid)) { exit("Filed to find UID in {$path}"); } if ($this->isStorageDriver('kolab')) { $imap = $this->getImapStorage(); if ($imap->folder_exists($foldername)) { // TODO exit("Not implemented for Kolab v3 storage driver"); } return; } $dav = $this->getDavStorage(); foreach ($dav->get_folders($type) as $folder) { if ($folder->get_name() === $foldername) { $dav_type = $folder->get_dav_type(); $location = $folder->object_location($uid); if ($folder->dav->create($location, $content, $dav_type) !== false) { return; } } } exit("Failed to append object into {$foldername}"); } /** * Delete a folder */ protected function deleteTestFolder($name, $type) { // Deleting IMAP folders if ($type == 'mail' || $this->isStorageDriver('kolab')) { $imap = $this->getImapStorage(); if ($imap->folder_exists($name)) { $imap->delete_folder($name); } return; } // Deleting DAV folders $dav = $this->getDavStorage(); foreach ($dav->get_folders($type) as $folder) { if ($folder->get_name() === $name) { $dav->folder_delete($folder->id, $type); } } } /** * Remove all objects from a folder */ protected function emptyTestFolder($name, $type) { // Deleting in IMAP folders if ($type == 'mail' || $this->isStorageDriver('kolab')) { $imap = $this->getImapStorage(); $imap->delete_message('*', $name); return; } // Deleting in DAV folders $dav = $this->getDavStorage(); foreach ($dav->get_folders($type) as $folder) { if ($folder->get_name() === $name) { $folder->delete_all(); } } } /** * Convert WBXML binary content into XML */ protected function fromWbxml($binary) { $stream = fopen('php://memory', 'r+'); fwrite($stream, $binary); rewind($stream); $decoder = new \Syncroton_Wbxml_Decoder($stream); return $decoder->decode(); } /** * Initialize DAV storage */ protected function getDavStorage() { $sync = \kolab_sync::get_instance(); $url = $sync->config->get('activesync_dav_server', 'http://localhost'); if (strpos($url, '://') === false) { $url = 'http://' . $url; } // Inject user+password to the URL, there's no other way to pass it to the DAV client $url = str_replace('://', '://' . rawurlencode(self::$username) . ':' . rawurlencode(self::$password) . '@', $url); // Make sure user is authenticated $this->getImapStorage(); if (!empty($sync->user)) { // required e.g. for DAV client cache use \rcube::get_instance()->user = $sync->user; } return new \kolab_storage_dav($url); } /** * Initialize IMAP storage */ protected function getImapStorage() { $sync = \kolab_sync::get_instance(); if (!self::$authenticated) { if ($sync->authenticate(self::$username, self::$password)) { self::$authenticated = true; $sync->password = self::$password; } } return $sync->get_storage(); } /** * Check the configured activesync_storage driver */ protected function isStorageDriver($name) { return $name === \kolab_sync::get_instance()->config->get('activesync_storage', 'kolab'); } /** * Make a HTTP request to the ActiveSync server */ protected function request($body, $cmd, $type = 'POST') { $username = self::$username; $deviceId = self::$deviceId; $deviceType = self::$deviceType ?: 'WindowsOutlook15'; $body = $this->toWbxml($body); return self::$client->request( $type, "?Cmd={$cmd}&User={$username}&DeviceId={$deviceId}&DeviceType={$deviceType}", [ 'headers' => [ 'Content-Type' => 'application/vnd.ms-sync.wbxml', - 'MS-ASProtocolVersion' => '14.0' + 'MS-ASProtocolVersion' => '14.0', ], 'body' => $body, ] ); } /** * Register the device for tests, some commands do not work until device/folders are registered */ protected function registerDevice() { // Execute initial FolderSync, it is required before executing some commands $request = << - - - 0 - - EOF; + + + + 0 + + EOF; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); foreach ($xpath->query("//ns:FolderSync/ns:Changes/ns:Add") as $idx => $folder) { $serverId = $folder->getElementsByTagName('ServerId')->item(0)->nodeValue; $displayName = $folder->getElementsByTagName('DisplayName')->item(0)->nodeValue; $this->folders[$serverId] = $displayName; } } /** * Convert XML into WBXML binary content */ protected function toWbxml($xml) { $outputStream = fopen('php://temp', 'r+'); $encoder = new \Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $dom = new \DOMDocument(); $dom->loadXML($xml); $encoder->encode($dom); rewind($outputStream); return stream_get_contents($outputStream); } /** * Get XPath from a DOM */ protected function xpath($dom) { $xpath = new \DOMXpath($dom); $xpath->registerNamespace("ns", $dom->documentElement->namespaceURI); $xpath->registerNamespace("AirSync", "uri:AirSync"); $xpath->registerNamespace("Calendar", "uri:Calendar"); $xpath->registerNamespace("Contacts", "uri:Contacts"); $xpath->registerNamespace("Email", "uri:Email"); $xpath->registerNamespace("Email2", "uri:Email2"); $xpath->registerNamespace("Settings", "uri:Settings"); $xpath->registerNamespace("Tasks", "uri:Tasks"); return $xpath; } } diff --git a/tests/Unit/BodyConverterTest.php b/tests/Unit/BodyConverterTest.php index f3afe40..61506cd 100644 --- a/tests/Unit/BodyConverterTest.php +++ b/tests/Unit/BodyConverterTest.php @@ -1,41 +1,41 @@ ', ''), - array('
a
', 'a'), - array('title', ''), - ); + return [ + ['', ''], + ['
', ''], + ['
a
', 'a'], + ['title', ''], + ]; } /** * @dataProvider data_html_to_text */ - function test_html_to_text($html, $text) + public function test_html_to_text($html, $text) { $converter = new kolab_sync_body_converter($html, Syncroton_Model_EmailBody::TYPE_HTML); $output = $converter->convert(Syncroton_Model_EmailBody::TYPE_PLAINTEXT); $this->assertEquals(trim($text), trim($output)); } /** * Test RTF convertion to HTML and Plain text */ - function test_rtf_to_text() + public function test_rtf_to_text() { $rtf = file_get_contents(__DIR__ . '/../src/sample.rtf'); $converter = new kolab_sync_body_converter($rtf, Syncroton_Model_EmailBody::TYPE_RTF); $output = $converter->convert(Syncroton_Model_EmailBody::TYPE_PLAINTEXT); $this->assertStringContainsString('This is text', trim($output)); $output = $converter->convert(Syncroton_Model_EmailBody::TYPE_HTML); $this->assertStringContainsString('anchor', trim($output)); } } diff --git a/tests/Unit/DataCalendarTest.php b/tests/Unit/DataCalendarTest.php index 65bd820..a96e8a8 100644 --- a/tests/Unit/DataCalendarTest.php +++ b/tests/Unit/DataCalendarTest.php @@ -1,126 +1,126 @@ from_kolab_alarm(array()); + $result = $obj->from_kolab_alarm([]); $this->assertSame(null, $result); - $event = array('valarms' => array(array( + $event = ['valarms' => [[ 'action' => 'DISPLAY', 'trigger' => 'PT5M', - ))); + ]]]; $result = $obj->from_kolab_alarm($event); $this->assertSame(null, $result); - $event = array('valarms' => array(array( + $event = ['valarms' => [[ 'action' => 'DISPLAY', 'trigger' => '-PT5M', - ))); + ]]]; $result = $obj->from_kolab_alarm($event); $this->assertSame(5, $result); - $event = array('valarms' => array(array( + $event = ['valarms' => [[ 'action' => 'DISPLAY', 'trigger' => 'PT0M', - ))); + ]]]; $result = $obj->from_kolab_alarm($event); $this->assertSame(0, $result); - $event = array('valarms' => array(array( + $event = ['valarms' => [[ 'action' => 'DISPLAY', 'trigger' => '-PT0M', - ))); + ]]]; $result = $obj->from_kolab_alarm($event); $this->assertSame(0, $result); - $event = array('valarms' => array(array( + $event = ['valarms' => [[ 'action' => 'DISPLAY', 'trigger' => 'PT0S', - ))); + ]]]; $result = $obj->from_kolab_alarm($event); $this->assertSame(0, $result); // alarms on specified DateTime (T2420) - $event = array( + $event = [ // no start datetime defined - 'valarms' => array( - array( + 'valarms' => [ + [ 'action' => 'DISPLAY', 'trigger' => new DateTime('now + 1 hour'), - ), - ), - ); + ], + ], + ]; $result = $obj->from_kolab_alarm($event); $this->assertSame(null, $result); - $event = array( + $event = [ 'start' => new DateTime('now + 10 minutes'), - 'valarms' => array( - array( + 'valarms' => [ + [ 'action' => 'DISPLAY', 'trigger' => new DateTime('now + 1 hour'), - ), - ), - ); + ], + ], + ]; $result = $obj->from_kolab_alarm($event); $this->assertSame(null, $result); - $event = array( + $event = [ 'start' => new DateTime('now + 60 minutes'), - 'valarms' => array( - array( + 'valarms' => [ + [ 'action' => 'DISPLAY', 'trigger' => new DateTime('now + 50 minutes'), - ), - ), - ); + ], + ], + ]; $result = $obj->from_kolab_alarm($event); $this->assertSame(10, $result); } /** * Test for kolab_sync_data_calendar::to_kolab_alarm() */ - function test_to_kolab_alarm() + public function test_to_kolab_alarm() { - $obj = new kolab_sync_data_calendar_test; + $obj = new kolab_sync_data_calendar_test(); - $result = $obj->to_kolab_alarm(null, array()); - $this->assertSame(array(), $result); + $result = $obj->to_kolab_alarm(null, []); + $this->assertSame([], $result); - $result = $obj->to_kolab_alarm(0, array()); + $result = $obj->to_kolab_alarm(0, []); $this->assertSame('-PT0M', $result[0]['trigger']); - $result = $obj->to_kolab_alarm(15, array()); + $result = $obj->to_kolab_alarm(15, []); $this->assertSame('-PT15M', $result[0]['trigger']); $this->assertSame('DISPLAY', $result[0]['action']); } } /** * kolab_sync_data_calendar wrapper, so we can test protected methods too */ class kolab_sync_data_calendar_test extends kolab_sync_data_calendar { - function __construct() + public function __construct() { } public function from_kolab_alarm($value) { return parent::from_kolab_alarm($value); } public function to_kolab_alarm($value, $event) { return parent::to_kolab_alarm($value, $event); } } diff --git a/tests/Unit/DataEmailTest.php b/tests/Unit/DataEmailTest.php index b724d5e..c63bb36 100644 --- a/tests/Unit/DataEmailTest.php +++ b/tests/Unit/DataEmailTest.php @@ -1,37 +1,37 @@ assertSame(51, $output['bytecount']); $this->assertSame('{81412D3C-2A24-4E9D-B20E-11F7BBE92799}', $output['uid']); $encoded = kolab_sync_data_email::encodeGlobalObjId($output); $this->assertSame($encoded, $input); $input = 'BAAAAIIA4AB0xbcQGoLgCAfUCRDgQMnBJoXEAQAAAAAAAAAAEAAAAAvw7UtuTulOnjnjhns3jvM='; $output = kolab_sync_data_email::decodeGlobalObjId($input); $this->assertSame(16, $output['bytecount']); $this->assertSame(2004, $output['year']); $this->assertSame(9, $output['month']); $this->assertSame(16, $output['day']); $this->assertSame(127373090979660000, $output['now']); // This is how the "now" value is interpreted // $winSecs = (int)($output['now'] / 10000000); // convert microseconds to seconds // $unixTimestamp = ($winSecs - 11644473600); // subtract 1.1.1600 - 1.1.1970 difference in seconds // print(date(DateTime::RFC822, $unixTimestamp)); $encoded = kolab_sync_data_email::encodeGlobalObjId($output); $this->assertSame($encoded, $input); } } diff --git a/tests/Unit/DataTasksTest.php b/tests/Unit/DataTasksTest.php index 1c97d98..85c4819 100644 --- a/tests/Unit/DataTasksTest.php +++ b/tests/Unit/DataTasksTest.php @@ -1,78 +1,78 @@ prio_to_importance($input); $this->assertEquals($output, $result); } /** * Test for kolab_sync_data_tasks::importance_to_prio() * @dataProvider data_importance() */ - function test_importance_to_prio($input, $output) + public function test_importance_to_prio($input, $output) { - $data = new kolab_sync_data_tasks_test; + $data = new kolab_sync_data_tasks_test(); $result = $data->importance_to_prio($input); $this->assertEquals($output, $result); } } /** * kolab_sync_data_tasks wrapper, so we can test preotected methods too */ class kolab_sync_data_tasks_test extends kolab_sync_data_tasks { - function __construct() + public function __construct() { } public function prio_to_importance($value) { return parent::prio_to_importance($value); } public function importance_to_prio($value) { return parent::importance_to_prio($value); } } diff --git a/tests/Unit/DataTest.php b/tests/Unit/DataTest.php index a22a40e..bf6bad2 100644 --- a/tests/Unit/DataTest.php +++ b/tests/Unit/DataTest.php @@ -1,152 +1,152 @@ 0 1 20101128T225959Z '; $xml = new SimpleXMLElement($xml); $event = new Syncroton_Model_Event($xml->ApplicationData); - $data = new kolab_sync_data_test; + $data = new kolab_sync_data_test(); $result = $data->recurrence_to_kolab($event); $this->assertEquals('DAILY', $result['FREQ']); $this->assertEquals(1, $result['INTERVAL']); $this->assertEquals('20101128T225959Z', $result['UNTIL']->format("Ymd\THis\Z")); $xml = ' 1 1 8 '; $xml = new SimpleXMLElement($xml); $event = new Syncroton_Model_Event($xml->ApplicationData); $result = $data->recurrence_to_kolab($event, null); $this->assertEquals('WEEKLY', $result['FREQ']); $this->assertEquals(1, $result['INTERVAL']); $this->assertEquals('WE', $result['BYDAY']); } /** * Test for kolab_sync_data::recurrence_from_kolab() */ - function test_recurrence_from_kolab() + public function test_recurrence_from_kolab() { $event = [ 'uid' => '52A09F6151F020312D99779F86838CF5-93BC4FC398A3FD52', '_type' => 'event', 'priority' => 0, 'attendees' => [ [ 'rsvp' => false, 'email' => 'bartek.machniak@nestle.kolab.ch', 'role' => 'ORGANIZER', 'status' => 'ACCEPTED', ], [ 'name' => 'Machniak, Aleksander', 'status' => 'ACCEPTED', 'cutype' => 'INDIVIDUAL', 'rsvp' => false, 'email' => 'aleksander.machniak@nestle.kolab.ch', ], ], 'created' => new DateTime('2023-10-20 09:49:26.000000'), 'changed' => new DateTime('2023-10-20 09:49:34.000000'), 'description' => 'description', 'end' => new DateTime('2023-10-20 14:30:00.000000'), 'start' => new DateTime('2023-10-20 14:00:00.000000'), 'location' => '', 'organizer' => [ 'rsvp' => false, 'email' => 'bartek.machniak@nestle.kolab.ch', 'role' => 'ORGANIZER', 'status' => 'ACCEPTED', ], 'sequence' => 0, 'status' => 'CONFIRMED', 'free_busy' => 'busy', 'allday' => false, 'recurrence' => [ 'FREQ' => 'WEEKLY', 'INTERVAL' => '1', ], ]; - $data = new kolab_sync_data_test; + $data = new kolab_sync_data_test(); $data->recurrence_from_kolab(null, $event, $result); $this->assertEquals($data::RECUR_TYPE_WEEKLY, $result['recurrence']->type); $this->assertEquals($data::RECUR_DOW_FRIDAY, $result['recurrence']->dayOfWeek); $this->assertEquals(1, $result['recurrence']->interval); $event['recurrence'] = [ 'FREQ' => 'MONTHLY', 'BYMONTHDAY' => '2,15', 'INTERVAL' => '5', 'COUNT' => 3, ]; $data->recurrence_from_kolab(null, $event, $result); $this->assertEquals($data::RECUR_TYPE_MONTHLY, $result['recurrence']->type); $this->assertEquals(2, $result['recurrence']->dayOfMonth); $this->assertEquals(5, $result['recurrence']->interval); $this->assertEquals(3, $result['recurrence']->occurrences); // TODO: More cases } } /** * kolab_sync_data wrapper, so we can test preotected methods too */ class kolab_sync_data_test extends kolab_sync_data { - function __construct() + public function __construct() { } public function recurrence_to_kolab($data, $dummy1 = null, $dummy2 = null) { return parent::recurrence_to_kolab($data, null); } public function recurrence_from_kolab($collection, $data, &$result, $type = 'Event') { return parent::recurrence_from_kolab($collection, $data, $result, $type); } - function toKolab($data, $folderId, $entry = null, $timezone = null) + public function toKolab($data, $folderId, $entry = null, $timezone = null) { return []; } - function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false) + public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false) { throw new Syncroton_Exception_NotFound("Not implemented"); } } diff --git a/tests/Unit/MessageTest.php b/tests/Unit/MessageTest.php index bed5423..164abb4 100644 --- a/tests/Unit/MessageTest.php +++ b/tests/Unit/MessageTest.php @@ -1,146 +1,146 @@ headers(); $this->assertArrayHasKey('MIME-Version', $headers); $this->assertCount(8, $headers); $this->assertEquals('kolab@domain.tld', $headers['To']); // test set_header() $message->set_header('to', 'test@domain.tld'); $headers = $message->headers(); $this->assertCount(8, $headers); $this->assertEquals('test@domain.tld', $headers['To']); } /** * Test message parsing */ - function test_source() + public function test_source() { $source = file_get_contents(TESTS_DIR . '/src/mail.plain'); $message = new kolab_sync_message($source); $result = $message->source(); $this->assertEquals($source, str_replace("\r\n", "\n", $result)); } /** * Test adding attachments to the message */ - function test_attachment() + public function test_attachment() { $source = file_get_contents(TESTS_DIR . '/src/mail.plain'); $mixed = file_get_contents(TESTS_DIR . '/src/mail.plain.mixed'); $mixed2 = file_get_contents(TESTS_DIR . '/src/mail.mixed'); // test adding attachment to text/plain message $message = new kolab_sync_message($source); - $message->add_attachment('aaa', array( + $message->add_attachment('aaa', [ 'content_type' => 'text/plain', 'encoding' => '8bit', - )); + ]); $result = $message->source(); $result = str_replace("\r\n", "\n", $result); if (preg_match('/boundary="([^"]+)"/', $result, $m)) { $mixed = str_replace('BOUNDARY', $m[1], $mixed); } $this->assertEquals($mixed, $result); // test adding attachment to multipart/mixed message $message = new kolab_sync_message($mixed); - $message->add_attachment('aaa', array( + $message->add_attachment('aaa', [ 'content_type' => 'text/plain', 'encoding' => 'base64', - )); + ]); $result = $message->source(); $result = str_replace("\r\n", "\n", $result); if (preg_match('/boundary="([^"]+)"/', $result, $m)) { $mixed2 = str_replace('BOUNDARY', $m[1], $mixed2); } $this->assertEquals($mixed2, $result); } /** * Test appending a text to the message */ - function test_append() + public function test_append() { // test appending text to text/plain message $source = file_get_contents(TESTS_DIR . '/src/mail.plain'); $append = file_get_contents(TESTS_DIR . '/src/mail.plain.append'); $message = new kolab_sync_message($source); $message->append('a'); $result = $message->source(); $result = str_replace("\r\n", "\n", $result); $this->assertEquals($append, $result); } /** * Test recoding the message */ - function test_recode_message_1() + public function test_recode_message_1() { $source = file_get_contents(TESTS_DIR . '/src/mail.recode1'); $result = file_get_contents(TESTS_DIR . '/src/mail.recode1.out'); $message = kolab_sync_message::recode_message($source); $this->assertEquals($result, $message); } /** * Test recoding the message */ - function test_recode_message_2() + public function test_recode_message_2() { $source = file_get_contents(TESTS_DIR . '/src/mail.recode2'); $result = file_get_contents(TESTS_DIR . '/src/mail.recode2.out'); $message = kolab_sync_message::recode_message($source); $this->assertEquals($result, $message); } /** * Test recoding the message */ - function test_recode_message_3() + public function test_recode_message_3() { $source = file_get_contents(TESTS_DIR . '/src/mail.recode3'); $result = file_get_contents(TESTS_DIR . '/src/mail.recode3.out'); $message = kolab_sync_message::recode_message($source); $this->assertEquals($result, $message); } /** * Test recoding the message */ - function test_recode_message_4() + public function test_recode_message_4() { $source = file_get_contents(TESTS_DIR . '/src/mail.recode4'); $result = file_get_contents(TESTS_DIR . '/src/mail.recode4.out'); $message = kolab_sync_message::recode_message($source); $this->assertEquals($result, $message); } } diff --git a/tests/Unit/TimezoneConverterTest.php b/tests/Unit/TimezoneConverterTest.php index 44cfc34..0dcea31 100644 --- a/tests/Unit/TimezoneConverterTest.php +++ b/tests/Unit/TimezoneConverterTest.php @@ -1,174 +1,174 @@ getListOfTimezones('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAEAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAEAAAAAAAAAxP///w=='); $this->assertTrue(is_array($output)); $converter = kolab_sync_timezone_converter::getInstance(); $output = $converter->getListOfTimezones('xP///0MAZQBuAHQAcgBhAGwAIABFAHUAcgBvAHAAZQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAEMAZQBuAHQAcgBhAGwAIABFAHUAcgBvAHAAZQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w=='); $this->assertTrue(is_array($output)); $this->assertTrue(isset($output['Europe/Warsaw'])); $converter = kolab_sync_timezone_converter::getInstance(); $output = $converter->getListOfTimezones('4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w=='); $this->assertTrue(is_array($output)); $this->assertTrue(isset($output['America/Los_Angeles'])); $converter = kolab_sync_timezone_converter::getInstance(); $output = $converter->getListOfTimezones('Lv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='); $this->assertTrue(is_array($output)); $this->assertTrue(isset($output['Asia/Tehran'])); // As seen in outlook $converter = kolab_sync_timezone_converter::getInstance(); $output = $converter->getListOfTimezones('Lv///0kAcgBhAG4AIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkABAADABcAOwA7AOcDAAAAAEkAcgBhAG4AIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAwAEAAAAAAAAAAAAxP///w=='); $this->assertTrue(is_array($output)); $this->assertTrue(isset($output['Asia/Tehran'])); } - function data_get_timezone() + public function data_get_timezone() { return [ ['UTC'], ['Europe/Warsaw'], ['Europe/Zurich'], ['America/Los_Angeles'], ['Asia/Tehran'], ]; } /** * @dataProvider data_get_timezone */ - function test_get_timezone($tzName) + public function test_get_timezone($tzName) { date_default_timezone_set('America/Los_Angeles'); $converter = kolab_sync_timezone_converter::getInstance(); $datetime = '2017-01-01T12:00:00Z'; $offsets = $converter->getOffsetsForTimezone($tzName, $datetime); $output = $converter->getTimezone($offsets, $tzName); $this->assertSame($tzName, $output); } - function test_get_offsets_for_timezone() + public function test_get_offsets_for_timezone() { date_default_timezone_set('America/Los_Angeles'); $converter = kolab_sync_timezone_converter::getInstance(); $datetime = '2017-01-01T12:00:00Z'; $output = $converter->getOffsetsForTimezone('UTC', $datetime); $this->assertSame($output['bias'], 0); $this->assertSame($output['standardBias'], 0); $this->assertSame($output['daylightBias'], 0); $this->assertSame($output['standardMonth'], 0); $this->assertSame($output['daylightMonth'], 0); $output = $converter->getOffsetsForTimezone('Europe/Warsaw', $datetime); $this->assertSame($output['standardBias'], 0); $this->assertSame($output['standardMonth'], 10); $this->assertSame($output['standardWeek'], 5); $this->assertSame($output['standardHour'], 3); $this->assertSame($output['daylightBias'], -60); $this->assertSame($output['daylightMonth'], 3); $this->assertSame($output['daylightWeek'], 5); $this->assertSame($output['daylightHour'], 2); $output = $converter->getOffsetsForTimezone('America/Los_Angeles', $datetime); $this->assertSame($output['bias'], 480); $this->assertSame($output['standardBias'], 0); $this->assertSame($output['standardMonth'], 11); $this->assertSame($output['standardWeek'], 1); $this->assertSame($output['standardHour'], 2); $this->assertSame($output['daylightBias'], -60); $this->assertSame($output['daylightMonth'], 3); $this->assertSame($output['daylightWeek'], 2); $this->assertSame($output['daylightHour'], 2); $output = $converter->getOffsetsForTimezone('Atlantic/Azores', $datetime); $this->assertSame($output['bias'], 60); $this->assertSame($output['standardBias'], 0); $this->assertSame($output['standardMonth'], 10); $this->assertSame($output['standardWeek'], 5); $this->assertSame($output['standardHour'], 1); $this->assertSame($output['daylightBias'], -60); $this->assertSame($output['daylightMonth'], 3); $this->assertSame($output['daylightWeek'], 5); $this->assertSame($output['daylightHour'], 0); //Check before dst change $output = $converter->getOffsetsForTimezone('Asia/Tehran', $datetime); $this->assertSame($output['bias'], -210); $this->assertSame($output['standardBias'], 0); $this->assertSame($output['standardMonth'], 9); $this->assertSame($output['standardWeek'], 3); $this->assertSame($output['standardDayOfWeek'], 4); $this->assertSame($output['standardHour'], 24); $this->assertSame($output['daylightBias'], -60); $this->assertSame($output['daylightMonth'], 3); $this->assertSame($output['daylightWeek'], 4); $this->assertSame($output['daylightDayOfWeek'], 3); $this->assertSame($output['daylightHour'], 0); //Check after dst change $output = $converter->getOffsetsForTimezone('Asia/Tehran', '2023-01-01T12:00:00Z'); $this->assertSame($output['bias'], -210); $this->assertSame($output['standardBias'], 0); $this->assertSame($output['standardMonth'], 0); $this->assertSame($output['standardWeek'], 0); $this->assertSame($output['standardDayOfWeek'], 0); $this->assertSame($output['standardHour'], 0); $this->assertSame($output['daylightBias'], 0); $this->assertSame($output['daylightMonth'], 0); $this->assertSame($output['daylightWeek'], 0); $this->assertSame($output['daylightDayOfWeek'], 0); $this->assertSame($output['daylightHour'], 0); } - function data_timezone_conversion() + public function data_timezone_conversion() { - return array( + return [ //Pre dst change - array('Asia/Tehran', 'Lv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAgADABcAOwA7AOcDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAQAEAAAAAAAAAAAAxP///w==', '2021-07-01T12:00:00Z'), + ['Asia/Tehran', 'Lv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAgADABcAOwA7AOcDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAQAEAAAAAAAAAAAAxP///w==', '2021-07-01T12:00:00Z'], //Post dst change - array('Asia/Tehran', 'Lv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', '2023-04-01T12:00:00Z'), - array('Pacific/Pago_Pago', 'lAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', '2021-07-01T12:00:00Z'), - array('Europe/Warsaw', 'xP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w==', '2021-07-01T12:00:00Z'), - ); + ['Asia/Tehran', 'Lv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', '2023-04-01T12:00:00Z'], + ['Pacific/Pago_Pago', 'lAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', '2021-07-01T12:00:00Z'], + ['Europe/Warsaw', 'xP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w==', '2021-07-01T12:00:00Z'], + ]; } /** * @dataProvider data_timezone_conversion */ - function test_timezone_conversion($tz, $expected, $datetime) + public function test_timezone_conversion($tz, $expected, $datetime) { $converter = kolab_sync_timezone_converter::getInstance(); $output = $converter->encodeTimezone($tz, $datetime); $this->assertSame($expected, $output); $output = $converter->getListOfTimezones($output); $this->assertTrue(is_array($output)); $this->assertTrue(isset($output[$tz])); } } diff --git a/tests/Unit/WbxmlTest.php b/tests/Unit/WbxmlTest.php index c10398c..2369ab2 100644 --- a/tests/Unit/WbxmlTest.php +++ b/tests/Unit/WbxmlTest.php @@ -1,910 +1,909 @@ loadXML($lastSyncCollection['lastXML']); // // // try { // $decoder = new Syncroton_Wbxml_Decoder($dom); // $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; // } // //TODO validate output //} public function testEncode() { $outputStream = fopen("php://temp", 'r+'); - + $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $xml = << - - - - - 2 - tasksId - - - clientId2 - - task2 - 0 - 2020-11-04T00:00:00.000Z - 2020-11-03T23:00:00.000Z - - - - clientId3 - - task3 - 0 - 2020-11-04T00:00:00.000Z - 2020-11-03T23:00:00.000Z - - - - - - 16 - - EOF; + + + + + + 2 + tasksId + + + clientId2 + + task2 + 0 + 2020-11-04T00:00:00.000Z + 2020-11-03T23:00:00.000Z + + + + clientId3 + + task3 + 0 + 2020-11-04T00:00:00.000Z + 2020-11-03T23:00:00.000Z + + + + + + 16 + + EOF; $dom = new DOMDocument(); $dom->loadXML($xml); - + $encoder->encode($dom); rewind($outputStream); $output = stream_get_contents($outputStream); // print("----"); // print(var_export($output, true)); // print("----"); $this->assertEquals( base64_decode('AwFqAEVcT0sDMgABUgN0YXNrc0lkAAFWR0wDY2xpZW50SWQyAAFdAAlgA3Rhc2syAAFKAzAAAUwDMjAyMC0xMS0wNFQwMDowMDowMC4wMDBaAAFNAzIwMjAtMTEtMDNUMjM6MDA6MDAuMDAwWgABAQEAAEdMA2NsaWVudElkMwABXQAJYAN0YXNrMwABSgMwAAFMAzIwMjAtMTEtMDRUMDA6MDA6MDAuMDAwWgABTQMyMDIwLTExLTAzVDIzOjAwOjAwLjAwMFoAAQEBAQEBAABVAzE2AAEB'), $output ); } public function testEncodeFolderSync() { $outputStream = fopen("php://temp", 'r+'); - + $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $xml = << - - - 1 - 1 - - 18 - - 2685b302b79f58d2753199545e3cb8be - 0 - Test2 - 13 - - - 9770b083c68e8584f396d15a116d6608 - 0 - DavidCalendar - 13 - - - 0f66388806743c514b8063bf0dc87486 - 0 - SergeyCalendar - 13 - - - cca1b81c734abbcd669bea90d23e08ae - 0 - Calendar - 8 - - - ab1ddb4ef8e8f8fcc2c9f5a7f9062452 - 0 - PubCal - 13 - - - d98bd8721371544ed095841ead941893 - 0 - (david) Test2 - 13 - - - 9e7b9656ef61d4af2fb2fdcabe600079 - 0 - (david) DavidCalendar - 13 - - - 384cf2d877c39a622fdc2a16898052e2 - 0 - (david) Calendar - 13 - - - Contacts::Syncroton - 0 - Contacts - 9 - - - 1bb8c55fe84d52c6968db2571f7dc124 - 0 - Archive - 12 - - - b51abe73e9e98fe200a4afe409050502 - 38b950ebd62cd9a66929c89615d0fc04 - Spam - 12 - - - cf529c792fc87d1f207435b3921bb02e - 0 - Sent - 5 - - - 715ed9ea29b8a5377a69c1f758037c65 - 0 - Spam - 12 - - - db0d959a3aeb21757f8849a830947a7a - 0 - Trash - 4 - - - 5ac9ec2e1a9d99e2e10cabe4abf26729 - 0 - Drafts - 3 - - - 38b950ebd62cd9a66929c89615d0fc04 - 0 - INBOX - 2 - - - fc56f4c7ffe0aefa622db9f8d9186c4a - 0 - Notes - 10 - - - 90335880f65deff6e521acea2b71a773 - 0 - Tasks - 7 - - - - EOF; + + + + 1 + 1 + + 18 + + 2685b302b79f58d2753199545e3cb8be + 0 + Test2 + 13 + + + 9770b083c68e8584f396d15a116d6608 + 0 + DavidCalendar + 13 + + + 0f66388806743c514b8063bf0dc87486 + 0 + SergeyCalendar + 13 + + + cca1b81c734abbcd669bea90d23e08ae + 0 + Calendar + 8 + + + ab1ddb4ef8e8f8fcc2c9f5a7f9062452 + 0 + PubCal + 13 + + + d98bd8721371544ed095841ead941893 + 0 + (david) Test2 + 13 + + + 9e7b9656ef61d4af2fb2fdcabe600079 + 0 + (david) DavidCalendar + 13 + + + 384cf2d877c39a622fdc2a16898052e2 + 0 + (david) Calendar + 13 + + + Contacts::Syncroton + 0 + Contacts + 9 + + + 1bb8c55fe84d52c6968db2571f7dc124 + 0 + Archive + 12 + + + b51abe73e9e98fe200a4afe409050502 + 38b950ebd62cd9a66929c89615d0fc04 + Spam + 12 + + + cf529c792fc87d1f207435b3921bb02e + 0 + Sent + 5 + + + 715ed9ea29b8a5377a69c1f758037c65 + 0 + Spam + 12 + + + db0d959a3aeb21757f8849a830947a7a + 0 + Trash + 4 + + + 5ac9ec2e1a9d99e2e10cabe4abf26729 + 0 + Drafts + 3 + + + 38b950ebd62cd9a66929c89615d0fc04 + 0 + INBOX + 2 + + + fc56f4c7ffe0aefa622db9f8d9186c4a + 0 + Notes + 10 + + + 90335880f65deff6e521acea2b71a773 + 0 + Tasks + 7 + + + + EOF; $dom = new DOMDocument(); $dom->loadXML($xml); - + $encoder->encode($dom); rewind($outputStream); $output = stream_get_contents($outputStream); // print("----"); // print(var_export(base64_encode($output), true)); // print("----"); $this->assertEquals( base64_decode('AwFqAAAHVkwDMQABUgMxAAFOVwMxOAABT0gDMjY4NWIzMDJiNzlmNThkMjc1MzE5OTU0NWUzY2I4YmUAAUkDMAABRwNUZXN0MgABSgMxMwABAU9IAzk3NzBiMDgzYzY4ZTg1ODRmMzk2ZDE1YTExNmQ2NjA4AAFJAzAAAUcDRGF2aWRDYWxlbmRhcgABSgMxMwABAU9IAzBmNjYzODg4MDY3NDNjNTE0YjgwNjNiZjBkYzg3NDg2AAFJAzAAAUcDU2VyZ2V5Q2FsZW5kYXIAAUoDMTMAAQFPSANjY2ExYjgxYzczNGFiYmNkNjY5YmVhOTBkMjNlMDhhZQABSQMwAAFHA0NhbGVuZGFyAAFKAzgAAQFPSANhYjFkZGI0ZWY4ZThmOGZjYzJjOWY1YTdmOTA2MjQ1MgABSQMwAAFHA1B1YkNhbAABSgMxMwABAU9IA2Q5OGJkODcyMTM3MTU0NGVkMDk1ODQxZWFkOTQxODkzAAFJAzAAAUcDKGRhdmlkKSBUZXN0MgABSgMxMwABAU9IAzllN2I5NjU2ZWY2MWQ0YWYyZmIyZmRjYWJlNjAwMDc5AAFJAzAAAUcDKGRhdmlkKSBEYXZpZENhbGVuZGFyAAFKAzEzAAEBT0gDMzg0Y2YyZDg3N2MzOWE2MjJmZGMyYTE2ODk4MDUyZTIAAUkDMAABRwMoZGF2aWQpIENhbGVuZGFyAAFKAzEzAAEBT0gDQ29udGFjdHM6OlN5bmNyb3RvbgABSQMwAAFHA0NvbnRhY3RzAAFKAzkAAQFPSAMxYmI4YzU1ZmU4NGQ1MmM2OTY4ZGIyNTcxZjdkYzEyNAABSQMwAAFHA0FyY2hpdmUAAUoDMTIAAQFPSANiNTFhYmU3M2U5ZTk4ZmUyMDBhNGFmZTQwOTA1MDUwMgABSQMzOGI5NTBlYmQ2MmNkOWE2NjkyOWM4OTYxNWQwZmMwNAABRwNTcGFtAAFKAzEyAAEBT0gDY2Y1MjljNzkyZmM4N2QxZjIwNzQzNWIzOTIxYmIwMmUAAUkDMAABRwNTZW50AAFKAzUAAQFPSAM3MTVlZDllYTI5YjhhNTM3N2E2OWMxZjc1ODAzN2M2NQABSQMwAAFHA1NwYW0AAUoDMTIAAQFPSANkYjBkOTU5YTNhZWIyMTc1N2Y4ODQ5YTgzMDk0N2E3YQABSQMwAAFHA1RyYXNoAAFKAzQAAQFPSAM1YWM5ZWMyZTFhOWQ5OWUyZTEwY2FiZTRhYmYyNjcyOQABSQMwAAFHA0RyYWZ0cwABSgMzAAEBT0gDMzhiOTUwZWJkNjJjZDlhNjY5MjljODk2MTVkMGZjMDQAAUkDMAABRwNJTkJPWAABSgMyAAEBT0gDZmM1NmY0YzdmZmUwYWVmYTYyMmRiOWY4ZDkxODZjNGEAAUkDMAABRwNOb3RlcwABSgMxMAABAU9IAzkwMzM1ODgwZjY1ZGVmZjZlNTIxYWNlYTJiNzFhNzczAAFJAzAAAUcDVGFza3MAAUoDNwABAQEB'), $output ); } public function testEncodeCalendar() { $outputStream = fopen("php://temp", 'r+'); - + $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $xml = << - - - - - 0 - 38b950ebd62cd9a66929c89615d0fc04 - 0 - 0 - 512 - - 0 - 2 - 8 - - 4 - 1 - - - - - 0 - cca1b81c734abbcd669bea90d23e08ae - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - Contacts::Syncroton - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - db0d959a3aeb21757f8849a830947a7a - 0 - 0 - 512 - - 0 - 2 - 8 - - 4 - 1 - - - - - 0 - cf529c792fc87d1f207435b3921bb02e - 0 - 0 - 512 - - 0 - 2 - 8 - - 4 - 1 - - - - - 0 - 90335880f65deff6e521acea2b71a773 - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - 1bb8c55fe84d52c6968db2571f7dc124 - 0 - 0 - 512 - - 0 - 2 - 8 - - 4 - 1 - - - - - 0 - 715ed9ea29b8a5377a69c1f758037c65 - 0 - 0 - 512 - - 0 - 2 - 8 - - 4 - 1 - - - - - 0 - b51abe73e9e98fe200a4afe409050502 - 0 - 0 - 512 - - 0 - 2 - 8 - - 4 - 1 - - - - - 0 - 0f66388806743c514b8063bf0dc87486 - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - 2685b302b79f58d2753199545e3cb8be - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - 384cf2d877c39a622fdc2a16898052e2 - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - 9770b083c68e8584f396d15a116d6608 - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - 9e7b9656ef61d4af2fb2fdcabe600079 - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - ab1ddb4ef8e8f8fcc2c9f5a7f9062452 - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 0 - d98bd8721371544ed095841ead941893 - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 512 - - 0 - - 1 - 1 - - - - - 16 - - EOF; + + + + + + 0 + 38b950ebd62cd9a66929c89615d0fc04 + 0 + 0 + 512 + + 0 + 2 + 8 + + 4 + 1 + + + + + 0 + cca1b81c734abbcd669bea90d23e08ae + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + Contacts::Syncroton + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + db0d959a3aeb21757f8849a830947a7a + 0 + 0 + 512 + + 0 + 2 + 8 + + 4 + 1 + + + + + 0 + cf529c792fc87d1f207435b3921bb02e + 0 + 0 + 512 + + 0 + 2 + 8 + + 4 + 1 + + + + + 0 + 90335880f65deff6e521acea2b71a773 + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + 1bb8c55fe84d52c6968db2571f7dc124 + 0 + 0 + 512 + + 0 + 2 + 8 + + 4 + 1 + + + + + 0 + 715ed9ea29b8a5377a69c1f758037c65 + 0 + 0 + 512 + + 0 + 2 + 8 + + 4 + 1 + + + + + 0 + b51abe73e9e98fe200a4afe409050502 + 0 + 0 + 512 + + 0 + 2 + 8 + + 4 + 1 + + + + + 0 + 0f66388806743c514b8063bf0dc87486 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + 2685b302b79f58d2753199545e3cb8be + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + 384cf2d877c39a622fdc2a16898052e2 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + 9770b083c68e8584f396d15a116d6608 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + 9e7b9656ef61d4af2fb2fdcabe600079 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + ab1ddb4ef8e8f8fcc2c9f5a7f9062452 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 0 + d98bd8721371544ed095841ead941893 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 512 + + 0 + + 1 + 1 + + + + + 16 + + EOF; $dom = new DOMDocument(); $dom->loadXML($xml); - + $encoder->encode($dom); rewind($outputStream); $output = stream_get_contents($outputStream); // print("----"); // print(var_export(base64_encode($output), true)); // print("----"); - + $this->assertEquals( base64_decode('AwFqAEVcT0sDMAABUgMzOGI5NTBlYmQ2MmNkOWE2NjkyOWM4OTYxNWQwZmMwNAABXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAFiAzIAAWMDOAABABFFRgM0AAFIAzEAAQEBAQAAT0sDMAABUgNjY2ExYjgxYzczNGFiYmNkNjY5YmVhOTBkMjNlMDhhZQABYAAEEQ4lDSgFJyYXEhsGJBQHGDQzAQAAXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAEAEUVGAzEAAUgDMQABAQEBAABPSwMwAAFSA0NvbnRhY3RzOjpTeW5jcm90b24AAV4DMAABUwMwAAFVAzUxMgABV1gDMAABABFFRgMxAAFIAzEAAQEBAQAAT0sDMAABUgNkYjBkOTU5YTNhZWIyMTc1N2Y4ODQ5YTgzMDk0N2E3YQABXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAFiAzIAAWMDOAABABFFRgM0AAFIAzEAAQEBAQAAT0sDMAABUgNjZjUyOWM3OTJmYzg3ZDFmMjA3NDM1YjM5MjFiYjAyZQABXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAFiAzIAAWMDOAABABFFRgM0AAFIAzEAAQEBAQAAT0sDMAABUgM5MDMzNTg4MGY2NWRlZmY2ZTUyMWFjZWEyYjcxYTc3MwABXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAEAEUVGAzEAAUgDMQABAQEBAABPSwMwAAFSAzFiYjhjNTVmZTg0ZDUyYzY5NjhkYjI1NzFmN2RjMTI0AAFeAzAAAVMDMAABVQM1MTIAAVdYAzAAAWIDMgABYwM4AAEAEUVGAzQAAUgDMQABAQEBAABPSwMwAAFSAzcxNWVkOWVhMjliOGE1Mzc3YTY5YzFmNzU4MDM3YzY1AAFeAzAAAVMDMAABVQM1MTIAAVdYAzAAAWIDMgABYwM4AAEAEUVGAzQAAUgDMQABAQEBAABPSwMwAAFSA2I1MWFiZTczZTllOThmZTIwMGE0YWZlNDA5MDUwNTAyAAFeAzAAAVMDMAABVQM1MTIAAVdYAzAAAWIDMgABYwM4AAEAEUVGAzQAAUgDMQABAQEBAABPSwMwAAFSAzBmNjYzODg4MDY3NDNjNTE0YjgwNjNiZjBkYzg3NDg2AAFgAAQRDiUNKAUnJhcSGwYkFAcYNDMBAABeAzAAAVMDMAABVQM1MTIAAVdYAzAAAQARRUYDMQABSAMxAAEBAQEAAE9LAzAAAVIDMjY4NWIzMDJiNzlmNThkMjc1MzE5OTU0NWUzY2I4YmUAAWAABBEOJQ0oBScmFxIbBiQUBxg0MwEAAF4DMAABUwMwAAFVAzUxMgABV1gDMAABABFFRgMxAAFIAzEAAQEBAQAAT0sDMAABUgMzODRjZjJkODc3YzM5YTYyMmZkYzJhMTY4OTgwNTJlMgABYAAEEQ4lDSgFJyYXEhsGJBQHGDQzAQAAXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAEAEUVGAzEAAUgDMQABAQEBAABPSwMwAAFSAzk3NzBiMDgzYzY4ZTg1ODRmMzk2ZDE1YTExNmQ2NjA4AAFgAAQRDiUNKAUnJhcSGwYkFAcYNDMBAABeAzAAAVMDMAABVQM1MTIAAVdYAzAAAQARRUYDMQABSAMxAAEBAQEAAE9LAzAAAVIDOWU3Yjk2NTZlZjYxZDRhZjJmYjJmZGNhYmU2MDAwNzkAAWAABBEOJQ0oBScmFxIbBiQUBxg0MwEAAF4DMAABUwMwAAFVAzUxMgABV1gDMAABABFFRgMxAAFIAzEAAQEBAQAAT0sDMAABUgNhYjFkZGI0ZWY4ZThmOGZjYzJjOWY1YTdmOTA2MjQ1MgABYAAEEQ4lDSgFJyYXEhsGJBQHGDQzAQAAXgMwAAFTAzAAAVUDNTEyAAFXWAMwAAEAEUVGAzEAAUgDMQABAQEBAABPSwMwAAFSA2Q5OGJkODcyMTM3MTU0NGVkMDk1ODQxZWFkOTQxODkzAAFgAAQRDiUNKAUnJhcSGwYkFAcYNDMBAABeAzAAAVMDMAABVQM1MTIAAVdYAzAAAQARRUYDMQABSAMxAAEBAQEBAABVAzE2AAEB'), $output ); } public function testEncodeEmail() { $outputStream = fopen("php://temp", 'r+'); - + $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $xml = << - - - - - Email - 2 - 38b950ebd62cd9a66929c89615d0fc04 - 1 - - - - 38b950ebd62cd9a66929c89615d0fc04::1 - - 2023-05-06T14:51:40.000Z - "Mollekopf, Christian" <christian@example.ch> - 65001 - Foobar 1 - christian@example.ch - 0 - - - 4 - Return-Path: <christian@example.ch> - Received: from imapb010.mykolab.com ([unix socket]) - by imapb010.mykolab.com (Cyrus 2.5.10-49-g2e214b4-Kolab-2.5.10-8.1.el7.kolab_14) with LMTPA; - Wed, 09 Aug 2017 18:37:01 +0200 - X-Sieve: CMU Sieve 2.4 - Received: from int-mx002.mykolab.com (unknown [10.9.13.2]) - by imapb010.mykolab.com (Postfix) with ESMTPS id 0A93910A25047 - for <christian@example.ch>; Wed, 9 Aug 2017 18:37:01 +0200 (CEST) - Received: from int-subm002.mykolab.com (unknown [10.9.37.2]) - by int-mx002.mykolab.com (Postfix) with ESMTPS id EC06AF6E - for <christian@example.ch>; Wed, 9 Aug 2017 18:37:00 +0200 (CEST) - MIME-Version: 1.0 - Content-Type: multipart/mixed; - boundary="=_291b8e96564265636432c6d494e02322" - Date: Sat, 06 May 2023 14:41:40 - From: "Mollekopf, Christian" <christian@example.ch> - To: christian@example.ch - Subject: Foobar 1 - Message-ID: <foobar1@example.org> - - --=_291b8e96564265636432c6d494e02322 - Content-Type: multipart/alternative; - boundary="=_ceff0fd19756f45ed1295ee2069ff8e0" - - --=_ceff0fd19756f45ed1295ee2069ff8e0 - Content-Transfer-Encoding: 7bit - Content-Type: text/plain; charset=US-ASCII - - sdlkjsdjf - --=_ceff0fd19756f45ed1295ee2069ff8e0 - Content-Transfer-Encoding: quoted-printable - Content-Type: text/html; charset=UTF-8 - - <html><head><meta http-equiv=3D"Content-Type" content=3D"text/html; charset= - =3DUTF-8" /></head><body style=3D'font-size: 10pt; font-family: Verdana,Gen= - eva,sans-serif'> - <p>sdlkjsdjf</p> - - </body></html> - - --=_ceff0fd19756f45ed1295ee2069ff8e0-- - - --=_291b8e96564265636432c6d494e02322 - Content-Transfer-Encoding: base64 - Content-Type: text/plain; - name=xorg.conf - Content-Disposition: attachment; - filename=xorg.conf; - size=211 - - U2VjdGlvbiAiRGV2aWNlIgogICAgSWRlbnRpZmllciAgICAgIkRldmljZTAiCiAgICBEcml2ZXIg - ICAgIEJvYXJkTmFtZSAgICAgICJOVlMgNDIwME0iCiAgICBPcHRpb24gIk5vTG9nbyIgInRydWUi - CiAgICBPcHRpb24gIlVzZUVESUQiICJ0cnVlIgpFbmRTZWN0aW9uCg== - --=_291b8e96564265636432c6d494e02322-- - - 2 - IPM.Note - urn:content-classes:message - - - xorg.conf - 38b950ebd62cd9a66929c89615d0fc04::5::2 - 1 - 35100212 - - - - - - - - - EOF; + + + + + + Email + 2 + 38b950ebd62cd9a66929c89615d0fc04 + 1 + + + + 38b950ebd62cd9a66929c89615d0fc04::1 + + 2023-05-06T14:51:40.000Z + "Mollekopf, Christian" <christian@example.ch> + 65001 + Foobar 1 + christian@example.ch + 0 + + + 4 + Return-Path: <christian@example.ch> + Received: from imapb010.mykolab.com ([unix socket]) + by imapb010.mykolab.com (Cyrus 2.5.10-49-g2e214b4-Kolab-2.5.10-8.1.el7.kolab_14) with LMTPA; + Wed, 09 Aug 2017 18:37:01 +0200 + X-Sieve: CMU Sieve 2.4 + Received: from int-mx002.mykolab.com (unknown [10.9.13.2]) + by imapb010.mykolab.com (Postfix) with ESMTPS id 0A93910A25047 + for <christian@example.ch>; Wed, 9 Aug 2017 18:37:01 +0200 (CEST) + Received: from int-subm002.mykolab.com (unknown [10.9.37.2]) + by int-mx002.mykolab.com (Postfix) with ESMTPS id EC06AF6E + for <christian@example.ch>; Wed, 9 Aug 2017 18:37:00 +0200 (CEST) + MIME-Version: 1.0 + Content-Type: multipart/mixed; + boundary="=_291b8e96564265636432c6d494e02322" + Date: Sat, 06 May 2023 14:41:40 + From: "Mollekopf, Christian" <christian@example.ch> + To: christian@example.ch + Subject: Foobar 1 + Message-ID: <foobar1@example.org> + + --=_291b8e96564265636432c6d494e02322 + Content-Type: multipart/alternative; + boundary="=_ceff0fd19756f45ed1295ee2069ff8e0" + + --=_ceff0fd19756f45ed1295ee2069ff8e0 + Content-Transfer-Encoding: 7bit + Content-Type: text/plain; charset=US-ASCII + + sdlkjsdjf + --=_ceff0fd19756f45ed1295ee2069ff8e0 + Content-Transfer-Encoding: quoted-printable + Content-Type: text/html; charset=UTF-8 + + <html><head><meta http-equiv=3D"Content-Type" content=3D"text/html; charset= + =3DUTF-8" /></head><body style=3D'font-size: 10pt; font-family: Verdana,Gen= + eva,sans-serif'> + <p>sdlkjsdjf</p> + + </body></html> + + --=_ceff0fd19756f45ed1295ee2069ff8e0-- + + --=_291b8e96564265636432c6d494e02322 + Content-Transfer-Encoding: base64 + Content-Type: text/plain; + name=xorg.conf + Content-Disposition: attachment; + filename=xorg.conf; + size=211 + + U2VjdGlvbiAiRGV2aWNlIgogICAgSWRlbnRpZmllciAgICAgIkRldmljZTAiCiAgICBEcml2ZXIg + ICAgIEJvYXJkTmFtZSAgICAgICJOVlMgNDIwME0iCiAgICBPcHRpb24gIk5vTG9nbyIgInRydWUi + CiAgICBPcHRpb24gIlVzZUVESUQiICJ0cnVlIgpFbmRTZWN0aW9uCg== + --=_291b8e96564265636432c6d494e02322-- + + 2 + IPM.Note + urn:content-classes:message + + + xorg.conf + 38b950ebd62cd9a66929c89615d0fc04::5::2 + 1 + 35100212 + + + + + + + + + EOF; $dom = new DOMDocument(); $dom->loadXML($xml); - + $encoder->encode($dom); rewind($outputStream); $output = stream_get_contents($outputStream); // print("----"); // print(var_export(base64_encode($output), true)); // print("----"); - + $this->assertEquals( base64_decode('AwFqAEVcT1ADRW1haWwAAUsDMgABUgMzOGI5NTBlYmQ2MmNkOWE2NjkyOWM4OTYxNWQwZmMwNAABTgMxAAEUVkdNAzM4Yjk1MGViZDYyY2Q5YTY2OTI5Yzg5NjE1ZDBmYzA0OjoxAAFdAAJPAzIwMjMtMDUtMDZUMTQ6NTE6NDAuMDAwWgABWAMiTW9sbGVrb3BmLCBDaHJpc3RpYW4iIDxjaHJpc3RpYW5AZXhhbXBsZS5jaD4AAXkDNjUwMDEAAVQDRm9vYmFyIDEAAVYDY2hyaXN0aWFuQGV4YW1wbGUuY2gAAVUDMAABOgARSkYDNAABSwNSZXR1cm4tUGF0aDogPGNocmlzdGlhbkBleGFtcGxlLmNoPg0KUmVjZWl2ZWQ6IGZyb20gaW1hcGIwMTAubXlrb2xhYi5jb20gKFt1bml4IHNvY2tldF0pDQogICAgICAgIGJ5IGltYXBiMDEwLm15a29sYWIuY29tIChDeXJ1cyAyLjUuMTAtNDktZzJlMjE0YjQtS29sYWItMi41LjEwLTguMS5lbDcua29sYWJfMTQpIHdpdGggTE1UUEE7DQogICAgICAgIFdlZCwgMDkgQXVnIDIwMTcgMTg6Mzc6MDEgKzAyMDANClgtU2lldmU6IENNVSBTaWV2ZSAyLjQNClJlY2VpdmVkOiBmcm9tIGludC1teDAwMi5teWtvbGFiLmNvbSAodW5rbm93biBbMTAuOS4xMy4yXSkNCiAgICAgICAgYnkgaW1hcGIwMTAubXlrb2xhYi5jb20gKFBvc3RmaXgpIHdpdGggRVNNVFBTIGlkIDBBOTM5MTBBMjUwNDcNCiAgICAgICAgZm9yIDxjaHJpc3RpYW5AZXhhbXBsZS5jaD47IFdlZCwgIDkgQXVnIDIwMTcgMTg6Mzc6MDEgKzAyMDAgKENFU1QpDQpSZWNlaXZlZDogZnJvbSBpbnQtc3VibTAwMi5teWtvbGFiLmNvbSAodW5rbm93biBbMTAuOS4zNy4yXSkNCiAgICAgICAgYnkgaW50LW14MDAyLm15a29sYWIuY29tIChQb3N0Zml4KSB3aXRoIEVTTVRQUyBpZCBFQzA2QUY2RQ0KICAgICAgICBmb3IgPGNocmlzdGlhbkBleGFtcGxlLmNoPjsgV2VkLCAgOSBBdWcgMjAxNyAxODozNzowMCArMDIwMCAoQ0VTVCkNCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9taXhlZDsNCmJvdW5kYXJ5PSI9XzI5MWI4ZTk2NTY0MjY1NjM2NDMyYzZkNDk0ZTAyMzIyIg0KRGF0ZTogU2F0LCAwNiBNYXkgMjAyMyAxNDo0MTo0MCANCkZyb206ICJNb2xsZWtvcGYsIENocmlzdGlhbiIgPGNocmlzdGlhbkBleGFtcGxlLmNoPg0KVG86IGNocmlzdGlhbkBleGFtcGxlLmNoDQpTdWJqZWN0OiBGb29iYXIgMQ0KTWVzc2FnZS1JRDogPGZvb2JhcjFAZXhhbXBsZS5vcmc+DQoNCi0tPV8yOTFiOGU5NjU2NDI2NTYzNjQzMmM2ZDQ5NGUwMjMyMg0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7DQpib3VuZGFyeT0iPV9jZWZmMGZkMTk3NTZmNDVlZDEyOTVlZTIwNjlmZjhlMCINCg0KLS09X2NlZmYwZmQxOTc1NmY0NWVkMTI5NWVlMjA2OWZmOGUwDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9VVMtQVNDSUkNCg0Kc2Rsa2pzZGpmDQotLT1fY2VmZjBmZDE5NzU2ZjQ1ZWQxMjk1ZWUyMDY5ZmY4ZTANCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCkNvbnRlbnQtVHlwZTogdGV4dC9odG1sOyBjaGFyc2V0PVVURi04DQoNCjxodG1sPjxoZWFkPjxtZXRhIGh0dHAtZXF1aXY9M0QiQ29udGVudC1UeXBlIiBjb250ZW50PTNEInRleHQvaHRtbDsgY2hhcnNldD0NCj0zRFVURi04IiAvPjwvaGVhZD48Ym9keSBzdHlsZT0zRCdmb250LXNpemU6IDEwcHQ7IGZvbnQtZmFtaWx5OiBWZXJkYW5hLEdlbj0NCmV2YSxzYW5zLXNlcmlmJz4NCjxwPnNkbGtqc2RqZjwvcD4NCg0KPC9ib2R5PjwvaHRtbD4NCg0KLS09X2NlZmYwZmQxOTc1NmY0NWVkMTI5NWVlMjA2OWZmOGUwLS0NCg0KLS09XzI5MWI4ZTk2NTY0MjY1NjM2NDMyYzZkNDk0ZTAyMzIyDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBiYXNlNjQNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsNCm5hbWU9eG9yZy5jb25mDQpDb250ZW50LURpc3Bvc2l0aW9uOiBhdHRhY2htZW50Ow0KZmlsZW5hbWU9eG9yZy5jb25mOw0Kc2l6ZT0yMTENCg0KVTJWamRHbHZiaUFpUkdWMmFXTmxJZ29nSUNBZ1NXUmxiblJwWm1sbGNpQWdJQ0FnSWtSbGRtbGpaVEFpQ2lBZ0lDQkVjbWwyWlhJZw0KSUNBZ0lFSnZZWEprVG1GdFpTQWdJQ0FnSUNKT1ZsTWdOREl3TUUwaUNpQWdJQ0JQY0hScGIyNGdJazV2VEc5bmJ5SWdJblJ5ZFdVaQ0KQ2lBZ0lDQlBjSFJwYjI0Z0lsVnpaVVZFU1VRaUlDSjBjblZsSWdwRmJtUlRaV04wYVc5dUNnPT0NCi0tPV8yOTFiOGU5NjU2NDI2NTYzNjQzMmM2ZDQ5NGUwMjMyMi0tAAEBVgMyAAEAAlMDSVBNLk5vdGUAAXwDdXJuOmNvbnRlbnQtY2xhc3NlczptZXNzYWdlAAEAEU5PUAN4b3JnLmNvbmYAAVEDMzhiOTUwZWJkNjJjZDlhNjY5MjljODk2MTVkMGZjMDQ6OjU6OjIAAVIDMQABTAMzNTEwMDIxMgABAQEBAQEBAQE='), $output ); } public function testEncodeEmailPerformanceTest() { $outputStream = fopen("php://temp", 'r+'); - + $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $attachment = str_repeat("ICAgIEJvYXJkTmFtZSAgICAgICJOVlMgNDIwME0iCiAgICBPcHRpb24gIk5vTG9nbyIgInRydWUi \n", 100000); $xml = << - - - - - Email - 2 - 38b950ebd62cd9a66929c89615d0fc04 - 1 - - - - 38b950ebd62cd9a66929c89615d0fc04::1 - - 2023-05-06T14:51:40.000Z - "Mollekopf, Christian" <christian@example.ch> - 65001 - Foobar 1 - christian@example.ch - 0 - - - 4 - Return-Path: <christian@example.ch> - Received: from imapb010.mykolab.com ([unix socket]) - by imapb010.mykolab.com (Cyrus 2.5.10-49-g2e214b4-Kolab-2.5.10-8.1.el7.kolab_14) with LMTPA; - Wed, 09 Aug 2017 18:37:01 +0200 - X-Sieve: CMU Sieve 2.4 - Received: from int-mx002.mykolab.com (unknown [10.9.13.2]) - by imapb010.mykolab.com (Postfix) with ESMTPS id 0A93910A25047 - for <christian@example.ch>; Wed, 9 Aug 2017 18:37:01 +0200 (CEST) - Received: from int-subm002.mykolab.com (unknown [10.9.37.2]) - by int-mx002.mykolab.com (Postfix) with ESMTPS id EC06AF6E - for <christian@example.ch>; Wed, 9 Aug 2017 18:37:00 +0200 (CEST) - MIME-Version: 1.0 - Content-Type: multipart/mixed; - boundary="=_291b8e96564265636432c6d494e02322" - Date: Sat, 06 May 2023 14:41:40 - From: "Mollekopf, Christian" <christian@example.ch> - To: christian@example.ch - Subject: Foobar 1 - Message-ID: <foobar1@example.org> - - --=_291b8e96564265636432c6d494e02322 - Content-Type: multipart/alternative; - boundary="=_ceff0fd19756f45ed1295ee2069ff8e0" - - --=_ceff0fd19756f45ed1295ee2069ff8e0 - Content-Transfer-Encoding: 7bit - Content-Type: text/plain; charset=US-ASCII - - sdlkjsdjf - --=_ceff0fd19756f45ed1295ee2069ff8e0 - Content-Transfer-Encoding: quoted-printable - Content-Type: text/html; charset=UTF-8 - - <html><head><meta http-equiv=3D"Content-Type" content=3D"text/html; charset= - =3DUTF-8" /></head><body style=3D'font-size: 10pt; font-family: Verdana,Gen= - eva,sans-serif'> - <p>sdlkjsdjf</p> - - </body></html> - - --=_ceff0fd19756f45ed1295ee2069ff8e0-- - - --=_291b8e96564265636432c6d494e02322 - Content-Transfer-Encoding: base64 - Content-Type: text/plain; - name=xorg.conf - Content-Disposition: attachment; - filename=xorg.conf; - size=211 - - U2VjdGlvbiAiRGV2aWNlIgogICAgSWRlbnRpZmllciAgICAgIkRldmljZTAiCiAgICBEcml2ZXIg - {$attachment} - CiAgICBPcHRpb24gIlVzZUVESUQiICJ0cnVlIgpFbmRTZWN0aW9uCg== - --=_291b8e96564265636432c6d494e02322-- - - 2 - IPM.Note - urn:content-classes:message - - - xorg.conf - 38b950ebd62cd9a66929c89615d0fc04::5::2 - 1 - 35100212 - - - - - - - - - EOF; + + + + + + Email + 2 + 38b950ebd62cd9a66929c89615d0fc04 + 1 + + + + 38b950ebd62cd9a66929c89615d0fc04::1 + + 2023-05-06T14:51:40.000Z + "Mollekopf, Christian" <christian@example.ch> + 65001 + Foobar 1 + christian@example.ch + 0 + + + 4 + Return-Path: <christian@example.ch> + Received: from imapb010.mykolab.com ([unix socket]) + by imapb010.mykolab.com (Cyrus 2.5.10-49-g2e214b4-Kolab-2.5.10-8.1.el7.kolab_14) with LMTPA; + Wed, 09 Aug 2017 18:37:01 +0200 + X-Sieve: CMU Sieve 2.4 + Received: from int-mx002.mykolab.com (unknown [10.9.13.2]) + by imapb010.mykolab.com (Postfix) with ESMTPS id 0A93910A25047 + for <christian@example.ch>; Wed, 9 Aug 2017 18:37:01 +0200 (CEST) + Received: from int-subm002.mykolab.com (unknown [10.9.37.2]) + by int-mx002.mykolab.com (Postfix) with ESMTPS id EC06AF6E + for <christian@example.ch>; Wed, 9 Aug 2017 18:37:00 +0200 (CEST) + MIME-Version: 1.0 + Content-Type: multipart/mixed; + boundary="=_291b8e96564265636432c6d494e02322" + Date: Sat, 06 May 2023 14:41:40 + From: "Mollekopf, Christian" <christian@example.ch> + To: christian@example.ch + Subject: Foobar 1 + Message-ID: <foobar1@example.org> + + --=_291b8e96564265636432c6d494e02322 + Content-Type: multipart/alternative; + boundary="=_ceff0fd19756f45ed1295ee2069ff8e0" + + --=_ceff0fd19756f45ed1295ee2069ff8e0 + Content-Transfer-Encoding: 7bit + Content-Type: text/plain; charset=US-ASCII + + sdlkjsdjf + --=_ceff0fd19756f45ed1295ee2069ff8e0 + Content-Transfer-Encoding: quoted-printable + Content-Type: text/html; charset=UTF-8 + + <html><head><meta http-equiv=3D"Content-Type" content=3D"text/html; charset= + =3DUTF-8" /></head><body style=3D'font-size: 10pt; font-family: Verdana,Gen= + eva,sans-serif'> + <p>sdlkjsdjf</p> + + </body></html> + + --=_ceff0fd19756f45ed1295ee2069ff8e0-- + + --=_291b8e96564265636432c6d494e02322 + Content-Transfer-Encoding: base64 + Content-Type: text/plain; + name=xorg.conf + Content-Disposition: attachment; + filename=xorg.conf; + size=211 + + U2VjdGlvbiAiRGV2aWNlIgogICAgSWRlbnRpZmllciAgICAgIkRldmljZTAiCiAgICBEcml2ZXIg + {$attachment} + CiAgICBPcHRpb24gIlVzZUVESUQiICJ0cnVlIgpFbmRTZWN0aW9uCg== + --=_291b8e96564265636432c6d494e02322-- + + 2 + IPM.Note + urn:content-classes:message + + + xorg.conf + 38b950ebd62cd9a66929c89615d0fc04::5::2 + 1 + 35100212 + + + + + + + + + EOF; $dom = new DOMDocument(); $dom->loadXML($xml); $start = microtime(true); $encoder->encode($dom); $end = microtime(true); $this->assertTrue($end - $start < 0.05); } public function testDecoder() { $inputStream = fopen("php://memory", 'r+'); $input = "\x03\x01j\x00\x00\x07VR\x030\x00\x01\x01"; fwrite($inputStream, $input); rewind($inputStream); $decoder = new Syncroton_Wbxml_Decoder($inputStream); $dom = $decoder->decode(); $xml = $dom->saveXML(); $expected = '' . '' . '0'; $this->assertSame($expected, str_replace("\n", '', $xml)); } } -