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;
+ }
}