diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist index 1b9b011..daf1d39 100644 --- a/config/config.inc.php.dist +++ b/config/config.inc.php.dist @@ -1,123 +1,120 @@ 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(); // 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'] = 0; // 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'] = null; // 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'] = null; $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/lib/kolab_sync.php b/lib/kolab_sync.php index d1c8c1d..9d48ca5 100644 --- a/lib/kolab_sync.php +++ b/lib/kolab_sync.php @@ -1,509 +1,494 @@ | | | | 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 { /** * Application name * * @var string */ public $app_name = 'ActiveSync for Kolab'; // no double quotes inside /** * Current user * * @var rcube_user */ public $user; public $username; public $password; const CHARSET = 'UTF-8'; const VERSION = "2.3.12"; /** * 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 = '') { if (!self::$instance || !is_a(self::$instance, 'kolab_sync')) { self::$instance = new kolab_sync(); self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Initialization of class instance */ public function startup() { // Initialize Syncroton Logger $debug_mode = $this->config->get('activesync_debug') ? kolab_sync_logger::DEBUG : kolab_sync_logger::WARN; $this->logger = new kolab_sync_logger($debug_mode); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array)$this->config->get('activesync_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('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); } } 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]; } } // 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('HTTP/1.1 401 Unauthorized'); exit; } $this->plugins->exec_hook('ready', array('task' => 'syncroton')); // Set log directory per-user $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::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::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 * * @param 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( 'host' => $host, 'user' => $username, 'pass' => $password, )); if (!$auth['abort'] && $cache) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error']) { self::server_error(); } // Close LDAP connection from kolab_auth plugin if (class_exists('kolab_auth', false)) { if (method_exists('kolab_auth', 'ldap_close')) { kolab_auth::ldap_close(); } } } else { $auth['pass'] = $password; } // Authenticate - get Roundcube user ID if (!$auth['abort'] && ($userid = $this->login($auth['user'], $auth['pass'], $auth['host'], $err))) { // set real username $this->username = $auth['user']; return $userid; } else if ($err) { $err_str = $this->get_storage()->get_error_str(); } kolab_auth::log_login_error($auth['user'], $err_str ?: $err); $this->plugins->exec_hook('login_failed', array( 'host' => $auth['host'], 'user' => $auth['user'], )); // IMAP server failure... send 503 error if ($err == rcube_imap_generic::ERROR_BAD) { self::server_error(); } } /** * Storage host selection */ private function select_host($username) { // Get IMAP host $host = $this->config->get('default_host'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ private function login($username, $password, $host, &$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); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP $storage = $this->get_storage(); if (!$storage->connect($host, $username, $password, $port, $ssl)) { $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( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { self::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->config->set_user_prefs((array)$this->user->get_prefs()); $this->set_storage_prop(); // 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; } /** * Set logging directory per-user */ protected function set_log_dir() { if (empty($this->username)) { return; } $this->logger->set_username($this->username); - $user_debug = $this->config->get('per_user_logging'); - $user_log = $user_debug || $this->config->get('activesync_user_log'); + $user_debug = (bool) $this->config->get('per_user_logging'); - if (!$user_log) { + if (!$user_debug) { return; } $log_dir = $this->config->get('log_dir'); $log_dir .= DIRECTORY_SEPARATOR . $this->username; - // No automatically creating any log directories. + // No automatically creating any log directories if (!is_dir($log_dir)) { - return; - } - - // No dir, no glory - if (!mkdir($log_dir, 0770)) { + $this->logger->set_log_dir(null); return; } if (!empty($_GET['DeviceId'])) { - $log_dir .= DIRECTORY_SEPARATOR . $_GET['DeviceId']; - } + $dev_dir = $log_dir . DIRECTORY_SEPARATOR . $_GET['DeviceId']; - if (!is_dir($log_dir)) { - if (!mkdir($log_dir, 0770)) { - return; + if (is_dir($dev_dir) || mkdir($dev_dir, 0770)) { + $log_dir = $dev_dir; } } - if ($user_debug) { - $this->per_user_log_dir = $log_dir; - } - else { - $this->config->set('log_dir', $log_dir); - } - - // re-set PHP error logging - if (($this->config->get('debug_level') & 1) && $this->config->get('log_driver') != 'syslog') { - ini_set('error_log', $log_dir . '/errors'); - } + $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 - */ + * 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() { 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')) { // make sure logged numbers use unified format setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); if (function_exists('memory_get_usage')) $mem = sprintf('%.1f', memory_get_usage() / 1048576); if (function_exists('memory_get_peak_usage')) $mem .= '/' . sprintf('%.1f', memory_get_peak_usage() / 1048576); $query = $_SERVER['QUERY_STRING']; $log = $query . ($mem ? ($query ? ' ' : '') . "[$mem]" : ''); if (defined('KOLAB_SYNC_START')) self::print_timer(KOLAB_SYNC_START, $log); else self::console($log); } } /** * 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(); } } } diff --git a/lib/kolab_sync_logger.php b/lib/kolab_sync_logger.php index 83e5311..1d7fd84 100644 --- a/lib/kolab_sync_logger.php +++ b/lib/kolab_sync_logger.php @@ -1,147 +1,170 @@ | +--------------------------------------------------------------------------+ | 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) { - $this->mode = intval($mode); + $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); } } /** * Message logger * * @param string $message Log message * @param int|string $method Message severity */ public function log($message, $method, $extras = null) { - $rcube = rcube::get_instance(); - $logfile = $rcube->config->get('activesync_log_file'); - $format = $rcube->config->get('log_date_format', 'd-M-Y H:i:s O'); - $log_dir = $rcube->get_user_log_dir() ?: $rcube->config->get('log_dir'); - if (is_numeric($method)) { $mode = $method; $method = array_search($method, $this->_priorities); } else { $mode = $this->_priorities[$method]; } + // Don't log messages with lower prio than the configured one if ($mode > $this->mode) { return; } + // Don't log debug messages if it's disabled e.g. by per_user_logging + if (empty($this->log_dir) && $mode >= self::NOTICE) { + return; + } + + $rcube = rcube::get_instance(); + $log_dir = $this->log_dir ?: $rcube->config->get('log_dir'); + $logfile = $this->logfile; + // if log_file is configured all logs will go to it // otherwise use separate file for info/debug and warning/error if (!$logfile) { switch ($mode) { case self::DEBUG: case self::INFO: case self::NOTICE: $file = 'console'; break; default: $file = 'errors'; break; } $logfile = $log_dir . DIRECTORY_SEPARATOR . $file; if (version_compare(version_parse(RCUBE_VERSION), '1.4.0') >= 0) { $logfile .= $rcube->config->get('log_file_ext', '.log'); } } else if ($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'); 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($format); + $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; + } }