diff --git a/plugins/kolab_2fa/kolab_2fa.php b/plugins/kolab_2fa/kolab_2fa.php index f1794588..f3f85197 100644 --- a/plugins/kolab_2fa/kolab_2fa.php +++ b/plugins/kolab_2fa/kolab_2fa.php @@ -1,774 +1,774 @@ * * Copyright (C) 2015, Kolab Systems AG * * 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 . */ class kolab_2fa extends rcube_plugin { public $task = '(login|settings)'; protected $login_verified = null; protected $login_factors = array(); protected $drivers = array(); protected $storage; /** * Plugin init */ public function init() { $this->load_config(); $this->add_hook('startup', array($this, 'startup')); } /** * Startup hook */ public function startup($args) { $rcmail = rcmail::get_instance(); // register library namespace to autoloader $loader = include(INSTALL_PATH . 'vendor/autoload.php'); $loader->set('Kolab2FA', array($this->home . '/lib')); if ($args['task'] === 'login' && $this->api->output) { $this->add_texts('localization/', false); $this->add_hook('authenticate', array($this, 'authenticate')); // process 2nd factor auth step after regular login if ($args['action'] === 'plugin.kolab-2fa-login' /* || !empty($_SESSION['kolab_2fa_factors']) */) { return $this->login_verify($args); } } else if ($args['task'] === 'settings') { $this->add_texts('localization/', !$this->api->output->ajax_call); $this->add_hook('settings_actions', array($this, 'settings_actions')); $this->register_action('plugin.kolab-2fa', array($this, 'settings_view')); $this->register_action('plugin.kolab-2fa-data', array($this, 'settings_data')); $this->register_action('plugin.kolab-2fa-save', array($this, 'settings_save')); $this->register_action('plugin.kolab-2fa-verify', array($this, 'settings_verify')); } return $args; } /** * Handler for 'authenticate' plugin hook. * * ATTENTION: needs to be called *after* kolab_auth::authenticate() */ public function authenticate($args) { // nothing to be done for me if ($args['abort'] || $this->login_verified !== null) { return $args; } $rcmail = rcmail::get_instance(); // parse $host URL $a_host = parse_url($args['host']); $hostname = $_SESSION['hostname'] = $a_host['host'] ?: $args['host']; // 1. find user record (and its prefs) before IMAP login if ($user = rcube_user::query($args['user'], $hostname)) { $rcmail->config->set_user_prefs($user->get_prefs()); } // 2a. let plugins provide the list of active authentication factors $lookup = $rcmail->plugins->exec_hook('kolab_2fa_lookup', array( 'user' => $args['user'], 'host' => $hostname, 'factors' => $rcmail->config->get('kolab_2fa_factors'), 'check' => $rcmail->config->get('kolab_2fa_check', true), )); if (isset($lookup['factors'])) { $factors = (array)$lookup['factors']; } // 2b. check storage if this user has 2FA enabled else if ($lookup['check'] !== false && ($storage = $this->get_storage($args['user']))) { $factors = (array)$storage->enumerate(); } if (count($factors) > 0) { $args['abort'] = true; $factors = array_unique($factors); // 3. flag session for 2nd factor verification $_SESSION['kolab_2fa_time'] = time(); $_SESSION['kolab_2fa_nonce'] = bin2hex(openssl_random_pseudo_bytes(32)); $_SESSION['kolab_2fa_factors'] = $factors; $_SESSION['username'] = $args['user']; $_SESSION['host'] = $args['host']; $_SESSION['password'] = $rcmail->encrypt($args['pass']); // 4. render to 2nd auth step $this->login_step($factors); } return $args; } /** * Handler for the additional login step requesting the 2FA verification code */ public function login_step($factors) { // replace handler for login form $this->login_factors = array_values($factors); $this->api->output->add_handler('loginform', array($this, 'auth_form')); // focus the code input field on load $this->api->output->add_script('$("input.kolab2facode").first().select();', 'docready'); $this->api->output->send('login'); } /** * Process the 2nd factor code verification form submission */ public function login_verify($args) { $rcmail = rcmail::get_instance(); $time = $_SESSION['kolab_2fa_time']; $nonce = $_SESSION['kolab_2fa_nonce']; $factors = (array)$_SESSION['kolab_2fa_factors']; $this->login_verified = false; $expired = $time < time() - $rcmail->config->get('kolab_2fa_timeout', 120); if (!empty($factors) && !empty($nonce) && !$expired) { // TODO: check signature // try to verify each configured factor foreach ($factors as $factor) { list($method) = explode(':', $factor, 2); // verify the submitted code $code = rcube_utils::get_input_value("_${nonce}_${method}", rcube_utils::INPUT_POST); $this->login_verified = $this->verify_factor_auth($factor, $code); // accept first successful method if ($this->login_verified) { break; } } } if ($this->login_verified) { // restore POST data from session $_POST['_user'] = $_SESSION['username']; $_POST['_host'] = $_SESSION['host']; $_POST['_pass'] = $rcmail->decrypt($_SESSION['password']); } // proceed with regular login ... $args['action'] = 'login'; // session data will be reset in index.php thus additional // auth attempts with intercepted data will be rejected // $rcmail->kill_session(); // we can't display any custom error messages on failed login // but that's actually desired to expose as little information as possible return $args; } /** * Helper method to verify the given method/code tuple */ protected function verify_factor_auth($method, $code) { if (strlen($code) && ($driver = $this->get_driver($method))) { // set properties from login $driver->username = $_SESSION['username']; try { // verify the submitted code return $driver->verify($code, $_SESSION['kolab_2fa_time']); } catch (Exception $e) { rcube::raise_error($e, true, false); } } return false; } /** * Render 2nd factor authentication form in place of the regular login form */ public function auth_form($attrib = array()) { $form_name = !empty($attrib['form']) ? $attrib['form'] : 'form'; $nonce = $_SESSION['kolab_2fa_nonce']; $methods = array_unique(array_map(function($factor) { list($method, $id) = explode(':', $factor); return $method; }, $this->login_factors )); // forward these values as the regular login screen would submit them $input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login')); $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'plugin.kolab-2fa-login')); $input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_POST))); $input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST))); // create HTML table with two cols $table = new html_table(array('cols' => 2)); $required = count($methods) > 1 ? null : 'required'; // render input for each configured auth method foreach ($methods as $i => $method) { if ($row++ > 0) { $table->add(array('colspan' => 2, 'class' => 'title hint', 'style' => 'text-align:center'), $this->gettext('or')); } $field_id = "rcmlogin2fa$method"; $input_code = new html_inputfield(array( 'name' => "_${nonce}_${method}", 'class' => 'kolab2facode', 'id' => $field_id, 'required' => $required, 'autocomplete' => 'off', 'data-icon' => 'key' // for Elastic ) + $attrib); $table->add('title', html::label($field_id, html::quote($this->gettext($method)))); $table->add('input', $input_code->show('')); } $out = $input_task->show(); $out .= $input_action->show(); $out .= $input_tzone->show(); $out .= $input_url->show(); $out .= $table->show(); // add submit button if (rcube_utils::get_boolean($attrib['submit'])) { $out .= html::p('formbuttons', html::tag('button', array( 'type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction save', ), $this->gettext('continue')) ); } // surround html output with a form tag if (empty($attrib['form'])) { $out = $this->api->output->form_tag(array('name' => $form_name, 'method' => 'post'), $out); } return $out; } /** * Load driver class for the given authentication factor * * @param string $factor Factor identifier (:) * @return Kolab2FA\Driver\Base */ public function get_driver($factor) { list($method) = explode(':', $factor, 2); $rcmail = rcmail::get_instance(); if ($this->drivers[$factor]) { return $this->drivers[$factor]; } $config = $rcmail->config->get('kolab_2fa_' . $method, array()); // use product name as "issuer"" if (empty($config['issuer'])) { $config['issuer'] = $rcmail->config->get('product_name'); } try { // TODO: use external auth service if configured $driver = \Kolab2FA\Driver\Base::factory($factor, $config); // attach storage $driver->storage = $this->get_storage(); if ($rcmail->user->ID) { $driver->username = $rcmail->get_user_name(); } $this->drivers[$factor] = $driver; return $driver; } catch (Exception $e) { $error = strval($e); } rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => $error), true, false); return false; } /** * Getter for a storage instance singleton */ public function get_storage($for = null) { if (!isset($this->storage) || (!empty($for) && $this->storage->username !== $for)) { $rcmail = rcmail::get_instance(); try { $this->storage = \Kolab2FA\Storage\Base::factory( $rcmail->config->get('kolab_2fa_storage', 'roundcube'), $rcmail->config->get('kolab_2fa_storage_config', array()) ); $this->storage->set_username($for); $this->storage->set_logger(new \Kolab2FA\Log\RcubeLogger()); // set user properties from active session if (!empty($_SESSION['kolab_dn'])) { $this->storage->userdn = $_SESSION['kolab_dn']; } } catch (Exception $e) { $this->storage = false; rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => $error), true, false); } } return $this->storage; } /** * Handler for 'settings_actions' hook */ public function settings_actions($args) { // register as settings action $args['actions'][] = array( 'action' => 'plugin.kolab-2fa', 'class' => 'twofactorauth', 'label' => 'settingslist', 'title' => 'settingstitle', 'domain' => 'kolab_2fa', ); return $args; } /** * Handler for settings/plugin.kolab-2fa requests */ public function settings_view() { $this->register_handler('plugin.settingsform', array($this, 'settings_form')); $this->register_handler('plugin.settingslist', array($this, 'settings_list')); $this->register_handler('plugin.factoradder', array($this, 'settings_factoradder')); $this->register_handler('plugin.highsecuritydialog', array($this, 'settings_highsecuritydialog')); $this->include_script('kolab2fa.js'); - $this->include_stylesheet($this->local_skin_path() . '/kolab2fa.css', true); + $this->include_stylesheet($this->local_skin_path() . '/kolab2fa.css'); if ($this->check_secure_mode()) { $this->api->output->set_env('session_secured', $_SESSION['kolab_2fa_secure_mode']); } $this->api->output->add_label('save','cancel'); $this->api->output->set_pagetitle($this->gettext('settingstitle')); $this->api->output->send('kolab_2fa.config'); } /** * Render the menu to add another authentication factor */ public function settings_factoradder($attrib) { $rcmail = rcmail::get_instance(); $attrib['id'] = 'kolab2fa-add'; $select = new html_select($attrib); $select->add($this->gettext('addfactor') . '...', ''); foreach ((array)$rcmail->config->get('kolab_2fa_drivers', array()) as $method) { $select->add($this->gettext($method), $method); } return $select->show(); } /** * Render a list of active factor this user has configured */ public function settings_list($attrib = array()) { $attrib['id'] = 'kolab2fa-factors'; $table = new html_table(array('cols' => 3)); $table->add_header('name', $this->gettext('factor')); $table->add_header('created', $this->gettext('created')); $table->add_header('actions', ''); return $table->show($attrib); } /** * Render the settings form template object */ public function settings_form($attrib = array()) { $rcmail = rcmail::get_instance(); $storage = $this->get_storage($rcmail->get_user_name()); $factors = $storage ? (array)$storage->enumerate() : array(); $drivers = (array)$rcmail->config->get('kolab_2fa_drivers', array()); $env_methods = array(); foreach ($drivers as $j => $method) { $out .= $this->settings_factor($method, $attrib); $env_methods[$method] = array( 'name' => $this->gettext($method), 'active' => 0, ); } $me = $this; $factors = array_combine( $factors, array_map(function($id) use ($me, &$env_methods) { $props = array('id' => $id); if ($driver = $me->get_driver($id)) { $props += $this->format_props($driver->props()); $props['method'] = $driver->method; $props['name'] = $me->gettext($driver->method); $env_methods[$driver->method]['active']++; } return $props; }, $factors) ); $this->api->output->set_env('kolab_2fa_methods', $env_methods); $this->api->output->set_env('kolab_2fa_factors', !empty($factors) ? $factors : null); return html::div(array('id' => 'kolab2fapropform'), $out); } /** * Render the settings UI for the given method/driver */ protected function settings_factor($method, $attrib) { $out = ''; $rcmail = rcmail::get_instance(); $attrib += array('class' => 'propform'); if ($driver = $this->get_driver($method)) { $table = new html_table(array('cols' => 2, 'class' => $attrib['class'])); foreach ($driver->props() as $field => $prop) { if (!$prop['editable']) { continue; } switch ($prop['type']) { case 'boolean': case 'checkbox': $input = new html_checkbox(array('value' => '1')); break; case 'enum': case 'select': $input = new html_select(array('disabled' => $prop['readonly'])); $input->add(array_map(array($this, 'gettext'), $prop['options']), $prop['options']); break; default: $input = new html_inputfield(array('size' => $prop['size'] ?: 30, 'disabled' => !$prop['editable'])); } $explain_label = $field . 'explain' . $method; $explain_html = $rcmail->text_exists($explain_label, 'kolab_2fa') ? html::div('explain form-text', $this->gettext($explain_label)) : ''; $field_id = 'rcmk2fa' . $method . $field; $table->add('title', html::label($field_id, $this->gettext($field))); $table->add(null, $input->show('', array('id' => $field_id, 'name' => "_prop[$field]")) . $explain_html); } // add row for displaying the QR code if (method_exists($driver, 'get_provisioning_uri')) { $table->add('title', $this->gettext('qrcode')); $table->add(null, html::div('explain form-text', $this->gettext("qrcodeexplain$method")) . html::tag('img', array('src' => 'data:image/gif;base64,R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'class' => 'qrcode', 'rel' => $method)) ); // add row for testing the factor $field_id = 'rcmk2faverify' . $method; $table->add('title', html::label($field_id, $this->gettext('verifycode'))); $table->add(null, html::tag('input', array('type' => 'text', 'name' => '_verify_code', 'id' => $field_id, 'class' => 'k2fa-verify', 'size' => 20, 'required' => true)) . html::div('explain form-text', $this->gettext("verifycodeexplain$method")) ); } $input_id = new html_hiddenfield(array('name' => '_prop[id]', 'value' => '')); $out .= html::tag('form', array( 'method' => 'post', 'action' => '#', 'id' => 'kolab2fa-prop-' . $method, 'style' => 'display:none', 'class' => 'propform', ), html::tag('fieldset', array(), html::tag('legend', array(), $this->gettext($method)) . html::div('factorprop', $table->show()) . $input_id->show() ) ); } return $out; } /** * Render the high-security-dialog content */ public function settings_highsecuritydialog($attrib = array()) { $attrib += array('id' => 'kolab2fa-highsecuritydialog'); $field_id = 'rcmk2facode'; $input = new html_inputfield(array('name' => '_code', 'id' => $field_id, 'class' => 'verifycode', 'size' => 20)); $label = html::label(array('for' => $field_id, 'class' => 'col-form-label col-sm-4'), '$name'); return html::div($attrib, html::div('explain form-text', $this->gettext('highsecuritydialog')) . html::div('propform row form-group', $label . html::div('col-sm-8', $input->show(''))) ); } /** * Handler for settings/plugin.kolab-2fa-save requests */ public function settings_save() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); $rcmail = rcmail::get_instance(); $storage = $this->get_storage($rcmail->get_user_name()); $success = false; $errors = 0; $save_data = array(); if ($driver = $this->get_driver($method)) { if ($data === false) { if ($this->check_secure_mode()) { // remove method from active factors and clear stored settings $success = $driver->clear(); } else { $errors++; } } else { // verify the submitted code before saving $verify_code = rcube_utils::get_input_value('_verify_code', rcube_utils::INPUT_POST); $timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST)); if (!empty($verify_code)) { if (!$driver->verify($verify_code, $timestamp)) { $this->api->output->command('plugin.verify_response', array( 'id' => $driver->id, 'method' => $driver->method, 'success' => false, 'message' => str_replace('$method', $this->gettext($driver->method), $this->gettext('codeverificationfailed')) )); $this->api->output->send(); } } foreach ($data as $prop => $value) { if (!$driver->set($prop, $value)) { $errors++; } } $driver->set('active', true); } // commit changes to the user properties if (!$errors) { if ($success = $driver->commit()) { $save_data = $data !== false ? $this->format_props($driver->props()) : array(); } else { $errors++; } } } if ($success) { $this->api->output->show_message($data === false ? $this->gettext('factorremovesuccess') : $this->gettext('factorsavesuccess'), 'confirmation'); $this->api->output->command('plugin.save_success', array( 'method' => $method, 'active' => $data !== false, 'id' => $driver->id) + $save_data); } else if ($errors) { $this->api->output->show_message($this->gettext('factorsaveerror'), 'error'); $this->api->output->command('plugin.reset_form', $method); } $this->api->output->send(); } /** * Handler for settings/plugin.kolab-2fa-data requests */ public function settings_data() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); if ($driver = $this->get_driver($method)) { $data = array('method' => $method, 'id' => $driver->id); foreach ($driver->props(true) as $field => $prop) { $data[$field] = $prop['text'] ?: $prop['value']; } // generate QR code for provisioning URI if (method_exists($driver, 'get_provisioning_uri')) { try { $uri = $driver->get_provisioning_uri(); $qr = new Endroid\QrCode\QrCode(); $qr->setText($uri) ->setSize(240) ->setPadding(10) ->setErrorCorrection('high') ->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0)) ->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0)); $data['qrcode'] = base64_encode($qr->get()); } catch (Exception $e) { rcube::raise_error($e, true, false); } } $this->api->output->command('plugin.render_data', $data); } $this->api->output->send(); } /** * Handler for settings/plugin.kolab-2fa-verify requests */ public function settings_verify() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); $timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST)); $success = false; if ($driver = $this->get_driver($method)) { $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); if (is_array($data)) { foreach ($data as $key => $value) { if ($value !== '******') { $driver->$key = $value; } } } $success = $driver->verify(rcube_utils::get_input_value('_code', rcube_utils::INPUT_POST), $timestamp); $method = $driver->method; } // put session into high-security mode if ($success && !empty($_POST['_session'])) { $_SESSION['kolab_2fa_secure_mode'] = time(); } $this->api->output->command('plugin.verify_response', array( 'method' => $method, 'success' => $success, 'message' => str_replace('$method', $this->gettext($method), $this->gettext($success ? 'codeverificationpassed' : 'codeverificationfailed')) )); $this->api->output->send(); } /** * */ protected function format_props($props) { $rcmail = rcmail::get_instance(); $values = array(); foreach ($props as $key => $prop) { switch ($prop['type']) { case 'datetime': $value = $rcmail->format_date($prop['value']); break; default: $value = $prop['value']; } $values[$key] = $value; } return $values; } /** * */ protected function check_secure_mode() { $valid = ($_SESSION['kolab_2fa_secure_mode'] && $_SESSION['kolab_2fa_secure_mode'] > time() - 180); return $valid; } } \ No newline at end of file diff --git a/plugins/kolab_activesync/kolab_activesync_ui.php b/plugins/kolab_activesync/kolab_activesync_ui.php index 4959e4a9..636f97c6 100644 --- a/plugins/kolab_activesync/kolab_activesync_ui.php +++ b/plugins/kolab_activesync/kolab_activesync_ui.php @@ -1,299 +1,299 @@ * @author Aleksander Machniak * * Copyright (C) 2011-2013, Kolab Systems AG * * 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 . */ class kolab_activesync_ui { private $rc; private $plugin; public $device = array(); const SETUP_URL = 'http://docs.kolab.org/client-configuration'; public function __construct($plugin) { $this->plugin = $plugin; $this->rc = rcube::get_instance(); $skin_path = $this->plugin->local_skin_path() . '/'; $this->skin_path = 'plugins/kolab_activesync/' . $skin_path; - $this->plugin->include_stylesheet($skin_path . 'config.css', true); + $this->plugin->include_stylesheet($skin_path . 'config.css'); } public function device_list($attrib = array()) { $attrib += array('id' => 'devices-list'); $devices = $this->plugin->list_devices(); $table = new html_table(); foreach ($devices as $id => $device) { $name = $device['ALIAS'] ? $device['ALIAS'] : $id; $table->add_row(array('id' => 'rcmrow' . $id)); $table->add(null, html::span('devicealias', rcube::Q($name)) . ' ' . html::span('devicetype secondary', rcube::Q($device['TYPE']))); } $this->rc->output->add_gui_object('devicelist', $attrib['id']); $this->rc->output->set_env('devicecount', count($devices)); $this->rc->output->include_script('list.js'); return $table->show($attrib); } public function device_config_form($attrib = array()) { $table = new html_table(array('cols' => 2)); $field_id = 'config-device-alias'; $input = new html_inputfield(array('name' => 'devicealias', 'id' => $field_id, 'size' => 40)); $table->add('title', html::label($field_id, $this->plugin->gettext('devicealias'))); $table->add(null, $input->show($this->device['ALIAS'] ? $this->device['ALIAS'] : $this->device['_id'])); // read-only device information $info = $this->plugin->device_info($this->device['ID']); if (!empty($info)) { foreach ($info as $key => $value) { if ($value) { $table->add('title', html::label(null, rcube::Q($this->plugin->gettext($key)))); $table->add(null, rcube::Q($value)); } } } if ($attrib['form']) { $this->rc->output->add_gui_object('editform', $attrib['form']); } return $table->show($attrib); } public function folder_subscriptions($attrib = array()) { if (!$attrib['id']) { $attrib['id'] = 'foldersubscriptions'; } // group folders by type (show only known types) $folder_groups = array('mail' => array(), 'contact' => array(), 'event' => array(), 'task' => array(), 'note' => array()); $folder_types = kolab_storage::folders_typedata(); $use_fieldsets = rcube_utils::get_boolean($attrib['use-fieldsets']); $imei = $this->device['_id']; $subscribed = array(); if ($imei) { $folder_meta = $this->plugin->folder_meta(); } foreach ($this->plugin->list_folders() as $folder) { if ($folder_types[$folder]) { list($type, ) = explode('.', $folder_types[$folder]); } else { $type = 'mail'; } if (is_array($folder_groups[$type])) { $folder_groups[$type][] = $folder; if (!empty($folder_meta) && ($meta = $folder_meta[$folder]) && $meta['FOLDER'] && $meta['FOLDER'][$imei]['S'] ) { $subscribed[$folder] = intval($meta['FOLDER'][$imei]['S']); } } } // build block for every folder type foreach ($folder_groups as $type => $group) { if (empty($group)) { continue; } $attrib['type'] = $type; $table = $this->folder_subscriptions_block($group, $attrib, $subscribed); $label = $this->plugin->gettext($type); if ($use_fieldsets) { $html .= html::tag('fieldset', 'subscriptionblock', html::tag('legend', $type, $label) . $table); } else { $html .= html::div('subscriptionblock', html::tag('h3', $type, $label) . $table); } } $this->rc->output->add_gui_object('subscriptionslist', $attrib['id']); return html::div($attrib, $html); } public function folder_subscriptions_block($a_folders, $attrib, $subscribed) { $alarms = ($attrib['type'] == 'event' || $attrib['type'] == 'task'); $table = new html_table(array('cellspacing' => 0)); $table->add_header(array( 'class' => 'subscription checkbox-cell', 'title' => $this->plugin->gettext('synchronize'), 'tabindex' => 0 ), $attrib['syncicon'] ? html::img(array('src' => $this->skin_path . $attrib['syncicon'])) : $this->plugin->gettext('synchronize') ); if ($alarms) { $table->add_header(array( 'class' => 'alarm checkbox-cell', 'title' => $this->plugin->gettext('withalarms'), 'tabindex' => 0 ), $attrib['alarmicon'] ? html::img(array('src' => $this->skin_path . $attrib['alarmicon'])) : $this->plugin->gettext('withalarms') ); } $table->add_header('foldername', $this->plugin->gettext('folder')); $checkbox_sync = new html_checkbox(array('name' => 'subscribed[]', 'class' => 'subscription')); $checkbox_alarm = new html_checkbox(array('name' => 'alarm[]', 'class' => 'alarm')); $names = array(); foreach ($a_folders as $folder) { $foldername = $origname = preg_replace('/^INBOX »\s+/', '', kolab_storage::object_prettyname($folder)); // find folder prefix to truncate (the same code as in kolab_addressbook plugin) for ($i = count($names)-1; $i >= 0; $i--) { if (strpos($foldername, $names[$i].' » ') === 0) { $length = strlen($names[$i].' » '); $prefix = substr($foldername, 0, $length); $count = count(explode(' » ', $prefix)); $foldername = str_repeat('  ', $count-1) . '» ' . substr($foldername, $length); break; } } $folder_id = 'rcmf' . rcube_utils::html_identifier($folder); $names[] = $origname; $classes = array('mailbox'); if ($folder_class = $this->rc->folder_classname($folder)) { $foldername = html::quote($this->rc->gettext($folder_class)); $classes[] = $folder_class; } $table->add_row(); $table->add('subscription checkbox-cell', $checkbox_sync->show( !empty($subscribed[$folder]) ? $folder : null, array('value' => $folder, 'id' => $folder_id))); if ($alarms) { $table->add('alarm checkbox-cell', $checkbox_alarm->show( intval($subscribed[$folder]) > 1 ? $folder : null, array('value' => $folder, 'id' => $folder_id.'_alarm'))); } $table->add(join(' ', $classes), html::label($folder_id, $foldername)); } return $table->show(); } public function folder_options_table($folder_name, $devices, $type) { $alarms = $type == 'event' || $type == 'task'; $meta = $this->plugin->folder_meta(); $folder_data = (array) ($meta[$folder_name] ? $meta[$folder_name]['FOLDER'] : null); $table = new html_table(array('cellspacing' => 0, 'id' => 'folder-sync-options', 'class' => 'records-table')); // table header $table->add_header(array('class' => 'device'), $this->plugin->gettext('devicealias')); $table->add_header(array('class' => 'subscription'), $this->plugin->gettext('synchronize')); if ($alarms) { $table->add_header(array('class' => 'alarm'), $this->plugin->gettext('withalarms')); } // table records foreach ($devices as $id => $device) { $info = $this->plugin->device_info($device['ID']); $name = $id; $title = ''; $checkbox = new html_checkbox(array('name' => "_subscriptions[$id]", 'value' => 1, 'onchange' => 'return activesync_object.update_sync_data(this)')); if (!empty($info)) { $_name = trim($info['friendlyname'] . ' ' . $info['os']); $title = $info['useragent']; if ($_name) { $name .= " ($_name)"; } } $table->add_row(); $table->add(array('class' => 'device', 'title' => $title), $name); $table->add('subscription checkbox-cell', $checkbox->show(!empty($folder_data[$id]['S']) ? 1 : 0)); if ($alarms) { $checkbox_alarm = new html_checkbox(array('name' => "_alarms[$id]", 'value' => 1, 'onchange' => 'return activesync_object.update_sync_data(this)')); $table->add('alarm checkbox-cell', $checkbox_alarm->show($folder_data[$id]['S'] > 1 ? 1 : 0)); } } return $table->show(); } /** * Displays initial page (when no devices are registered) */ function init_message() { $this->plugin->load_config(); $this->rc->output->add_handlers(array( 'initmessage' => array($this, 'init_message_content') )); $this->rc->output->send('kolab_activesync.configempty'); } /** * Handler for initmessage template object */ function init_message_content() { $url = $this->rc->config->get('activesync_setup_url', self::SETUP_URL); $vars = array('url' => $url); $msg = $this->plugin->gettext(array('name' => 'nodevices', 'vars' => $vars)); return $msg; } } diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php index 0454e8b0..726b66cf 100644 --- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php +++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php @@ -1,363 +1,363 @@ * * Copyright (C) 2012, Kolab Systems AG * * 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 . */ class kolab_addressbook_ui { private $plugin; private $rc; /** * Class constructor * * @param kolab_addressbook $plugin Plugin object */ public function __construct($plugin) { $this->rc = rcube::get_instance(); $this->plugin = $plugin; $this->init_ui(); } /** * Adds folders management functionality to Addressbook UI */ private function init_ui() { if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action) && $this->rc->action != 'show') { return; } // Include script $this->plugin->include_script('kolab_addressbook.js'); if (empty($this->rc->action)) { // Include stylesheet (for directorylist) - $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css', true); + $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); // include kolab folderlist widget if available if (in_array('libkolab', $this->plugin->api->loaded_plugins())) { $this->plugin->api->include_script('libkolab/js/folderlist.js'); } $this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.search_addon', false, false)); // Add actions on address books $options = array('book-create', 'book-edit', 'book-delete', 'book-remove'); $idx = 0; if ($dav_url = $this->rc->config->get('kolab_addressbook_carddav_url')) { $options[] = 'book-showurl'; $this->rc->output->set_env('kolab_addressbook_carddav_url', true); // set CardDAV URI for specified ldap addressbook if ($ldap_abook = $this->rc->config->get('kolab_addressbook_carddav_ldap')) { $dav_ldap_url = strtr($dav_url, array( '%h' => $_SERVER['HTTP_HOST'], '%u' => urlencode($this->rc->get_user_name()), '%i' => 'ldap-directory', '%n' => '', )); $this->rc->output->set_env('kolab_addressbook_carddav_ldap', $ldap_abook); $this->rc->output->set_env('kolab_addressbook_carddav_ldap_url', $dav_ldap_url); } } foreach ($options as $command) { $content = html::tag('li', $idx ? null : array('class' => 'separator_above'), $this->plugin->api->output->button(array( 'label' => 'kolab_addressbook.'.str_replace('-', '', $command), 'domain' => $this->ID, 'class' => str_replace('-', ' ', $command) . ' disabled', 'classact' => str_replace('-', ' ', $command) . ' active', 'command' => $command, 'type' => 'link' ))); $this->plugin->api->add_content($content, 'groupoptions'); $idx++; } // Link to Settings/Folders $content = html::tag('li', array('class' => 'separator_above'), $this->plugin->api->output->button(array( 'label' => 'managefolders', 'type' => 'link', 'class' => 'folders disabled', 'classact' => 'folders active', 'command' => 'folders', 'task' => 'settings', ))); $this->plugin->api->add_content($content, 'groupoptions'); $this->rc->output->add_label('kolab_addressbook.bookdeleteconfirm', 'kolab_addressbook.bookdeleting', 'kolab_addressbook.bookshowurl', 'kolab_addressbook.carddavurldescription', 'kolab_addressbook.bookedit', 'kolab_addressbook.bookdelete', 'kolab_addressbook.bookshowurl', 'kolab_addressbook.findaddressbooks', 'kolab_addressbook.searchterms', 'kolab_addressbook.foldersearchform', 'kolab_addressbook.listsearchresults', 'kolab_addressbook.nraddressbooksfound', 'kolab_addressbook.noaddressbooksfound', 'kolab_addressbook.foldersubscribe', 'resetsearch' ); if ($this->plugin->bonnie_api) { $this->rc->output->set_env('kolab_audit_trail', true); $this->plugin->api->include_script('libkolab/js/audittrail.js'); $this->rc->output->add_label( 'kolab_addressbook.showhistory', 'kolab_addressbook.objectchangelog', 'kolab_addressbook.objectdiff', 'kolab_addressbook.objectdiffnotavailable', 'kolab_addressbook.objectchangelognotavailable', 'kolab_addressbook.revisionrestoreconfirm' ); $this->plugin->add_hook('render_page', array($this, 'render_audittrail_page')); $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); } } // include stylesheet for audit trail else if ($this->rc->action == 'show' && $this->plugin->bonnie_api) { $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css', true); $this->rc->output->add_label('kolab_addressbook.showhistory'); } // book create/edit form else { $this->rc->output->add_label('kolab_addressbook.nobooknamewarning', 'kolab_addressbook.booksaving'); } } /** * Handler for address book create/edit action */ public function book_edit() { $this->rc->output->add_handler('bookdetails', array($this, 'book_form')); $this->rc->output->send('kolab_addressbook.bookedit'); } /** * Handler for 'bookdetails' object returning form content for book create/edit * * @param array $attr Object attributes * * @return string HTML output */ public function book_form($attrib) { $action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC)); $folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true)); // UTF8 $hidden_fields[] = array('name' => '_source', 'value' => $folder); $folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP'); $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); if ($this->rc->action == 'plugin.book-save') { // save error $name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_GPC, true)); // UTF8 $old = trim(rcube_utils::get_input_value('_oldname', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP $path_imap = trim(rcube_utils::get_input_value('_parent', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP $hidden_fields[] = array('name' => '_oldname', 'value' => $old); $folder = $old; } else if ($action == 'edit') { $path_imap = explode($delim, $folder); $name = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP'); $path_imap = implode($path_imap, $delim); } else { // create $path_imap = $folder; $name = ''; $folder = ''; } // Store old name, get folder options if (strlen($folder)) { $hidden_fields[] = array('name' => '_oldname', 'value' => $folder); $options = $storage->folder_info($folder); } $form = array(); // General tab $form['props'] = array( 'name' => $this->rc->gettext('properties'), ); if (!empty($options) && ($options['norename'] || $options['protected'])) { $foldername = rcube::Q(str_replace($delim, ' » ', kolab_storage::object_name($folder))); } else { $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30)); $foldername = $foldername->show($name); } $form['props']['fieldsets']['location'] = array( 'name' => $this->rc->gettext('location'), 'content' => array( 'name' => array( 'label' => $this->plugin->gettext('bookname'), 'value' => $foldername, ), ), ); if (!empty($options) && ($options['norename'] || $options['protected'])) { // prevent user from moving folder $hidden_fields[] = array('name' => '_parent', 'value' => $path_imap); } else { $prop = array('name' => '_parent', 'id' => '_parent'); $select = kolab_storage::folder_selector('contact', $prop, $folder); $form['props']['fieldsets']['location']['content']['parent'] = array( 'label' => $this->plugin->gettext('parentbook'), 'value' => $select->show(strlen($folder) ? $path_imap : ''), ); } // Allow plugins to modify address book form content (e.g. with ACL form) $plugin = $this->rc->plugins->exec_hook('addressbook_form', array('form' => $form, 'options' => $options, 'name' => $folder)); $form = $plugin['form']; // Set form tags and hidden fields list($form_start, $form_end) = $this->get_form_tags($attrib, 'plugin.book-save', null, $hidden_fields); unset($attrib['form']); // return the complete edit form as table $out = "$form_start\n"; // Create form output foreach ($form as $idx => $tab) { if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) { $content = ''; foreach ($tab['fieldsets'] as $fieldset) { $subcontent = $this->get_form_part($fieldset); if ($subcontent) { $content .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($fieldset['name'])) . $subcontent) ."\n"; } } } else { $content = $this->get_form_part($tab); } if ($content) { if ($idx != 'props') { $out .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) ."\n"; } else { $out .= $content ."\n"; } } } $out .= "\n$form_end"; return $out; } /** * */ public function render_audittrail_page($p) { // append audit trail UI elements to contact page if ($p['template'] === 'addressbook' && !$p['kolab-audittrail']) { $this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.audittrail', false, false)); $p['kolab-audittrail'] = true; } return $p; } private function get_form_part($form) { $content = ''; if (is_array($form['content']) && !empty($form['content'])) { $table = new html_table(array('cols' => 2, 'class' => 'propform')); foreach ($form['content'] as $col => $colprop) { $colprop['id'] = '_'.$col; $label = !empty($colprop['label']) ? $colprop['label'] : $this->rc->gettext($col); $table->add('title', sprintf('', $colprop['id'], rcube::Q($label))); $table->add(null, $colprop['value']); } $content = $table->show(); } else { $content = $form['content']; } return $content; } private function get_form_tags($attrib, $action, $id = null, $hidden = null) { $form_start = $form_end = ''; $request_key = $action . (isset($id) ? '.'.$id : ''); $form_start = $this->rc->output->request_form(array( 'name' => 'form', 'method' => 'post', 'task' => $this->rc->task, 'action' => $action, 'request' => $request_key, 'noclose' => true, ) + $attrib); if (is_array($hidden)) { foreach ($hidden as $field) { $hiddenfield = new html_hiddenfield($field); $form_start .= $hiddenfield->show(); } } $form_end = !strlen($attrib['form']) ? '' : ''; $EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form'; $this->rc->output->add_gui_object('editform', $EDIT_FORM); return array($form_start, $form_end); } } diff --git a/plugins/kolab_delegation/kolab_delegation.php b/plugins/kolab_delegation/kolab_delegation.php index 306564aa..0752d0cf 100644 --- a/plugins/kolab_delegation/kolab_delegation.php +++ b/plugins/kolab_delegation/kolab_delegation.php @@ -1,576 +1,576 @@ * @author Thomas Bruederli * * Copyright (C) 2011-2012, Kolab Systems AG * * 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 . */ class kolab_delegation extends rcube_plugin { public $task = 'login|mail|settings|calendar|tasks'; private $rc; private $engine; /** * Plugin initialization. */ public function init() { $this->rc = rcube::get_instance(); $this->require_plugin('libkolab'); $this->require_plugin('kolab_auth'); // on-login delegation initialization $this->add_hook('login_after', array($this, 'login_hook')); // on-check-recent delegation support $this->add_hook('check_recent', array($this, 'check_recent_hook')); // on-message-send delegation support $this->add_hook('message_before_send', array($this, 'message_before_send')); // delegation support in Calendar and Tasklist plugins $this->add_hook('message_load', array($this, 'message_load')); $this->add_hook('calendar_user_emails', array($this, 'calendar_user_emails')); $this->add_hook('calendar_list_filter', array($this, 'calendar_list_filter')); $this->add_hook('calendar_load_itip', array($this, 'calendar_load_itip')); $this->add_hook('tasklist_list_filter', array($this, 'tasklist_list_filter')); // delegation support in kolab_auth plugin $this->add_hook('kolab_auth_emails', array($this, 'kolab_auth_emails')); if ($this->rc->task == 'settings') { // delegation management interface $this->register_action('plugin.delegation', array($this, 'controller_ui')); $this->register_action('plugin.delegation-delete', array($this, 'controller_action')); $this->register_action('plugin.delegation-save', array($this, 'controller_action')); $this->register_action('plugin.delegation-autocomplete', array($this, 'controller_action')); $this->add_hook('settings_actions', array($this, 'settings_actions')); if ($this->rc->output->type == 'html' && ($this->rc->action == 'plugin.delegation' || empty($_REQUEST['_framed'])) ) { $this->add_texts('localization/', array('deleteconfirm', 'savingdata', 'yes', 'no')); if ($this->rc->action == 'plugin.delegation') { $this->include_script('kolab_delegation.js'); } $this->skin_path = $this->local_skin_path(); - $this->include_stylesheet($this->skin_path . '/style.css', true); + $this->include_stylesheet($this->skin_path . '/style.css'); } } // Calendar/Tasklist plugin UI bindings else if (($this->rc->task == 'calendar' || $this->rc->task == 'tasks') && empty($_REQUEST['_framed']) ) { if ($this->rc->output->type == 'html') { $this->calendar_ui(); } } } /** * Adds Delegation section in Settings */ function settings_actions($args) { $args['actions'][] = array( 'action' => 'plugin.delegation', 'class' => 'delegation', 'label' => 'tabtitle', 'domain' => 'kolab_delegation', 'title' => 'delegationtitle', ); return $args; } /** * Engine object getter */ private function engine() { if (!$this->engine) { require_once $this->home . '/kolab_delegation_engine.php'; $this->load_config(); $this->engine = new kolab_delegation_engine(); } return $this->engine; } /** * On-login action */ public function login_hook($args) { // Manage (create) identities for delegator's email addresses // and subscribe to delegator's folders. Also remove identities // after delegation is removed $engine = $this->engine(); $engine->delegation_init(); return $args; } /** * Check-recent action */ public function check_recent_hook($args) { // Checking for new messages shall be extended to Inbox folders of all // delegators if 'check_all_folders' is set to false. if ($this->rc->task != 'mail') { return $args; } if (!empty($args['all'])) { return $args; } if (empty($_SESSION['delegators'])) { return $args; } $storage = $this->rc->get_storage(); $other_ns = $storage->get_namespace('other'); $folders = $storage->list_folders_subscribed('', '*', 'mail'); foreach (array_keys($_SESSION['delegators']) as $uid) { foreach ($other_ns as $ns) { $folder = $ns[0] . $uid; if (in_array($folder, $folders) && !in_array($folder, $args['folders'])) { $args['folders'][] = $folder; } } } return $args; } /** * Mail send action */ public function message_before_send($args) { // Checking headers of email being send, we'll add // Sender: header if mail is send on behalf of someone else if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_delivery_filter($args); } return $args; } /** * E-mail message loading action */ public function message_load($args) { // This is a place where we detect delegate context // So we can handle event invitations on behalf of delegator // @TODO: should we do this only in delegators' folders? // skip invalid messages or Kolab objects (for better performance) if (empty($args['object']->headers) || $args['object']->headers->get('x-kolab-type', false)) { return $args; } $engine = $this->engine(); $context = $engine->delegator_context_from_message($args['object']); if ($context) { $this->rc->output->set_env('delegator_context', $context); $this->include_script('kolab_delegation.js'); } return $args; } /** * calendar::get_user_emails() handler */ public function calendar_user_emails($args) { // In delegator context we'll use delegator's addresses // instead of current user addresses if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_emails_filter($args); } return $args; } /** * calendar_driver::list_calendars() handler */ public function calendar_list_filter($args) { // In delegator context we'll use delegator's folders // instead of current user folders if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_folder_filter($args, 'calendars'); } return $args; } /** * tasklist_driver::get_lists() handler */ public function tasklist_list_filter($args) { // In delegator context we'll use delegator's folders // instead of current user folders if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_folder_filter($args, 'tasklists'); } return $args; } /** * calendar::load_itip() handler */ public function calendar_load_itip($args) { // In delegator context we'll use delegator's address/name // for invitation responses if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $engine->delegator_identity_filter($args); } return $args; } /** * Delegation support in Calendar/Tasks plugin UI */ public function calendar_ui() { // Initialize handling of delegators' identities in event form if (!empty($_SESSION['delegators'])) { $engine = $this->engine(); $this->rc->output->set_env('namespace', $engine->namespace_js()); $this->rc->output->set_env('delegators', $engine->list_delegators_js()); $this->include_script('kolab_delegation.js'); } } /** * Delegation support in kolab_auth plugin */ public function kolab_auth_emails($args) { // Add delegators addresses to address selector in user identity form if (!empty($_SESSION['delegators'])) { // @TODO: Consider not adding all delegator addresses to the list. // Instead add only address of currently edited identity foreach ($_SESSION['delegators'] as $emails) { $args['emails'] = array_merge($args['emails'], $emails); } $args['emails'] = array_unique($args['emails']); sort($args['emails']); } return $args; } /** * Delegation UI handler */ public function controller_ui() { // main interface (delegates list) if (empty($_REQUEST['_framed'])) { $this->register_handler('plugin.delegatelist', array($this, 'delegate_list')); $this->rc->output->include_script('list.js'); $this->rc->output->send('kolab_delegation.settings'); } // delegate frame else { $this->register_handler('plugin.delegateform', array($this, 'delegate_form')); $this->register_handler('plugin.delegatefolders', array($this, 'delegate_folders')); $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15)); $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length')); $this->rc->output->add_label('autocompletechars', 'autocompletemore'); $this->rc->output->send('kolab_delegation.editform'); } } /** * Delegation action handler */ public function controller_action() { $this->add_texts('localization/'); $engine = $this->engine(); // Delegate delete if ($this->rc->action == 'plugin.delegation-delete') { $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC); $error = $engine->delegate_delete($id, (bool) rcube_utils::get_input_value('acl', rcube_utils::INPUT_GPC)); if (!$error) { $this->rc->output->show_message($this->gettext('deletesuccess'), 'confirmation'); $this->rc->output->command('plugin.delegate_save_complete', array('deleted' => $id)); } else { $this->rc->output->show_message($this->gettext($error), 'error'); } } // Delegate add/update else if ($this->rc->action == 'plugin.delegation-save') { $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC); $acl = rcube_utils::get_input_value('folders', rcube_utils::INPUT_GPC); // update if ($id) { $delegate = $engine->delegate_get($id); $error = $engine->delegate_acl_update($delegate['uid'], $acl); if (!$error) { $this->rc->output->show_message($this->gettext('updatesuccess'), 'confirmation'); $this->rc->output->command('plugin.delegate_save_complete', array('updated' => $id)); } else { $this->rc->output->show_message($this->gettext($error), 'error'); } } // new else { $login = rcube_utils::get_input_value('newid', rcube_utils::INPUT_GPC); $delegate = $engine->delegate_get_by_name($login); $error = $engine->delegate_add($delegate, $acl); if (!$error) { $this->rc->output->show_message($this->gettext('createsuccess'), 'confirmation'); $this->rc->output->command('plugin.delegate_save_complete', array( 'created' => $delegate['ID'], 'name' => $delegate['name'], )); } else { $this->rc->output->show_message($this->gettext($error), 'error'); } } } // Delegate autocompletion else if ($this->rc->action == 'plugin.delegation-autocomplete') { $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true); $reqid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC); $users = $engine->list_users($search); $this->rc->output->command('ksearch_query_results', $users, $search, $reqid); } $this->rc->output->send(); } /** * Template object of delegates list */ public function delegate_list($attrib = array()) { $attrib += array('id' => 'delegate-list'); $engine = $this->engine(); $list = $engine->list_delegates(); $table = new html_table(); // sort delegates list asort($list, SORT_LOCALE_STRING); foreach ($list as $id => $delegate) { $table->add_row(array('id' => 'rcmrow' . $id)); $table->add(null, rcube::Q($delegate)); } $this->rc->output->add_gui_object('delegatelist', $attrib['id']); $this->rc->output->set_env('delegatecount', count($list)); return $table->show($attrib); } /** * Template object of delegate form */ public function delegate_form($attrib = array()) { $engine = $this->engine(); $table = new html_table(array('cols' => 2)); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $field_id = 'delegate'; if ($id) { $delegate = $engine->delegate_get($id); } if ($delegate) { $input = new html_hiddenfield(array('name' => $field_id, 'id' => $field_id, 'size' => 40)); $input = rcube::Q($delegate['name']) . $input->show($id); $this->rc->output->set_env('active_delegate', $id); $this->rc->output->command('parent.enable_command','delegate-delete', true); } else { $input = new html_inputfield(array('name' => $field_id, 'id' => $field_id, 'size' => 40)); $input = $input->show(); } $table->add('title', html::label($field_id, $this->gettext('delegate'))); $table->add(null, $input); if ($attrib['form']) { $this->rc->output->add_gui_object('editform', $attrib['form']); } return $table->show($attrib); } /** * Template object of folders list */ public function delegate_folders($attrib = array()) { if (!$attrib['id']) { $attrib['id'] = 'delegatefolders'; } $engine = $this->engine(); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); if ($id) { $delegate = $engine->delegate_get($id); } $folder_data = $engine->list_folders($delegate['uid']); $use_fieldsets = rcube_utils::get_boolean($attrib['use-fieldsets']); $rights = array(); $folder_groups = array(); foreach ($folder_data as $folder_name => $folder) { $folder_groups[$folder['type']][] = $folder_name; $rights[$folder_name] = $folder['rights']; } // build block for every folder type foreach ($folder_groups as $type => $group) { if (empty($group)) { continue; } $attrib['type'] = $type; $table = $this->delegate_folders_block($group, $attrib, $rights); $label = $this->gettext($type); if ($use_fieldsets) { $html .= html::tag('fieldset', 'foldersblock', html::tag('legend', $type, $label) . $table); } else { $html .= html::div('foldersblock', html::tag('h3', $type, $label) . $table); } } $this->rc->output->add_gui_object('folderslist', $attrib['id']); return html::div($attrib, $html); } /** * List of folders in specified group */ private function delegate_folders_block($a_folders, $attrib, $rights) { $path = 'plugins/kolab_delegation/' . $this->skin_path . '/'; $read_ico = $attrib['readicon'] ? html::img(array('src' => $path . $attrib['readicon'], 'title' => $this->gettext('read'))) : ''; $write_ico = $attrib['writeicon'] ? html::img(array('src' => $path . $attrib['writeicon'], 'title' => $this->gettext('write'))) : ''; $table = new html_table(array('cellspacing' => 0)); $table->add_header(array('class' => 'read checkbox-cell', 'title' => $this->gettext('read'), 'tabindex' => 0), $read_ico); $table->add_header(array('class' => 'write checkbox-cell', 'title' => $this->gettext('write'), 'tabindex' => 0), $write_ico); $table->add_header('foldername', $this->rc->gettext('folder')); $checkbox_read = new html_checkbox(array('name' => 'read[]', 'class' => 'read')); $checkbox_write = new html_checkbox(array('name' => 'write[]', 'class' => 'write')); $names = array(); foreach ($a_folders as $folder) { $foldername = $origname = preg_replace('/^INBOX »\s+/', '', kolab_storage::object_prettyname($folder)); // find folder prefix to truncate (the same code as in kolab_addressbook plugin) for ($i = count($names)-1; $i >= 0; $i--) { if (strpos($foldername, $names[$i].' » ') === 0) { $length = strlen($names[$i].' » '); $prefix = substr($foldername, 0, $length); $count = count(explode(' » ', $prefix)); $foldername = str_repeat('  ', $count-1) . '» ' . substr($foldername, $length); break; } } $folder_id = 'rcmf' . rcube_utils::html_identifier($folder); $names[] = $origname; $classes = array('mailbox'); if ($folder_class = $this->rc->folder_classname($folder)) { $foldername = html::quote($this->rc->gettext($folder_class)); $classes[] = $folder_class; } $table->add_row(); $table->add('read checkbox-cell', $checkbox_read->show( $rights[$folder] >= kolab_delegation_engine::ACL_READ ? $folder : null, array('value' => $folder))); $table->add('write checkbox-cell', $checkbox_write->show( $rights[$folder] >= kolab_delegation_engine::ACL_WRITE ? $folder : null, array('value' => $folder, 'id' => $folder_id))); $table->add(join(' ', $classes), html::label($folder_id, $foldername)); } return $table->show(); } } diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php index a3152915..cee2df9c 100644 --- a/plugins/libkolab/libkolab.php +++ b/plugins/libkolab/libkolab.php @@ -1,345 +1,345 @@ * * Copyright (C) 2012-2015, Kolab Systems AG * * 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 . */ class libkolab extends rcube_plugin { static $http_requests = array(); static $bonnie_api = false; /** * Required startup method of a Roundcube plugin */ public function init() { // load local config $this->load_config(); // extend include path to load bundled lib classes $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path'); set_include_path($include_path); $this->add_hook('storage_init', array($this, 'storage_init')); $this->add_hook('storage_connect', array($this, 'storage_connect')); $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders')); $rcmail = rcube::get_instance(); try { kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT')); } catch (Exception $e) { rcube::raise_error($e, true); kolab_format::$timezone = new DateTimeZone('GMT'); } $this->add_texts('localization/', false); if ($rcmail->output->type == 'html') { - $this->include_stylesheet($this->local_skin_path() . '/libkolab.css', true); + $this->include_stylesheet($this->local_skin_path() . '/libkolab.css'); } // embed scripts and templates for email message audit trail if ($rcmail->task == 'mail' && self::get_bonnie_api()) { if ($rcmail->output->type == 'html') { $this->add_hook('render_page', array($this, 'bonnie_render_page')); $this->include_script('js/audittrail.js'); // add 'Show history' item to message menu $this->api->add_content(html::tag('li', null, $this->api->output->button(array( 'command' => 'kolab-mail-history', 'label' => 'libkolab.showhistory', 'type' => 'link', 'classact' => 'icon history active', 'class' => 'icon history', 'innerclass' => 'icon history', ))), 'messagemenu'); } $this->register_action('plugin.message-changelog', array($this, 'message_changelog')); } } /** * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers */ function storage_init($p) { $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID'); return $p; } /** * Hook into IMAP connection to replace client identity */ function storage_connect($p) { $client_name = 'Roundcube/Kolab'; if (empty($p['ident'])) { $p['ident'] = array( 'name' => $client_name, 'version' => RCUBE_VERSION, /* 'php' => PHP_VERSION, 'os' => PHP_OS, 'command' => $_SERVER['REQUEST_URI'], */ ); } else { $p['ident']['name'] = $client_name; } return $p; } /** * Getter for a singleton instance of the Bonnie API * * @return mixed kolab_bonnie_api instance if configured, false otherwise */ public static function get_bonnie_api() { // get configuration for the Bonnie API if (!self::$bonnie_api && ($bonnie_config = rcube::get_instance()->config->get('kolab_bonnie_api', false))) { self::$bonnie_api = new kolab_bonnie_api($bonnie_config); } return self::$bonnie_api; } /** * Hook to append the message history dialog template to the mail view */ function bonnie_render_page($p) { if (($p['template'] === 'mail' || $p['template'] === 'message') && !$p['kolab-audittrail']) { // append a template for the audit trail dialog $this->api->output->add_footer( html::div(array('id' => 'mailmessagehistory', 'class' => 'uidialog', 'aria-hidden' => 'true', 'style' => 'display:none'), self::object_changelog_table(array('class' => 'records-table changelog-table')) ) ); $this->api->output->set_env('kolab_audit_trail', true); $p['kolab-audittrail'] = true; } return $p; } /** * Handler for message audit trail changelog requests */ public function message_changelog() { if (!self::$bonnie_api) { return false; } $rcmail = rcube::get_instance(); $msguid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST, true); $mailbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $result = $msguid && $mailbox ? self::$bonnie_api->changelog('mail', null, $mailbox, $msguid) : null; if (is_array($result)) { if (is_array($result['changes'])) { $dtformat = $rcmail->config->get('date_format') . ' ' . $rcmail->config->get('time_format'); array_walk($result['changes'], function(&$change) use ($dtformat, $rcmail) { if ($change['date']) { $dt = rcube_utils::anytodatetime($change['date']); if ($dt instanceof DateTime) { $change['date'] = $rcmail->format_date($dt, $dtformat); } } }); } $this->api->output->command('plugin.message_render_changelog', $result['changes']); } else { $this->api->output->command('plugin.message_render_changelog', false); } $this->api->output->send(); } /** * Wrapper function to load and initalize the HTTP_Request2 Object * * @param string|Net_Url2 Request URL * @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT') * @param array Configuration for this Request instance, that will be merged * with default configuration * * @return HTTP_Request2 Request object */ public static function http_request($url = '', $method = 'GET', $config = array()) { $rcube = rcube::get_instance(); $http_config = (array) $rcube->config->get('kolab_http_request'); // deprecated configuration options if (empty($http_config)) { foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) { $value = $rcube->config->get('kolab_' . $option, true); if (is_bool($value)) { $http_config[$option] = $value; } } } if (!empty($config)) { $http_config = array_merge($http_config, $config); } // force CURL adapter, this allows to handle correctly // compressed responses with SplObserver registered (kolab_files) (#4507) $http_config['adapter'] = 'HTTP_Request2_Adapter_Curl'; $key = md5(serialize($http_config)); if (!($request = self::$http_requests[$key])) { // load HTTP_Request2 require_once 'HTTP/Request2.php'; try { $request = new HTTP_Request2(); $request->setConfig($http_config); } catch (Exception $e) { rcube::raise_error($e, true, true); } // proxy User-Agent string $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']); self::$http_requests[$key] = $request; } // cleanup try { $request->setBody(''); $request->setUrl($url); $request->setMethod($method); } catch (Exception $e) { rcube::raise_error($e, true, true); } return $request; } /** * Table oultine for object changelog display */ public static function object_changelog_table($attrib = array()) { $rcube = rcube::get_instance(); $attrib += array('domain' => 'libkolab'); $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0)); $table->add_header('diff', ''); $table->add_header('revision', $rcube->gettext('revision', $attrib['domain'])); $table->add_header('date', $rcube->gettext('date', $attrib['domain'])); $table->add_header('user', $rcube->gettext('user', $attrib['domain'])); $table->add_header('operation', $rcube->gettext('operation', $attrib['domain'])); $table->add_header('actions', ' '); $rcube->output->add_label( 'libkolab.showrevision', 'libkolab.actionreceive', 'libkolab.actionappend', 'libkolab.actionmove', 'libkolab.actiondelete', 'libkolab.actionread', 'libkolab.actionflagset', 'libkolab.actionflagclear', 'libkolab.objectchangelog', 'libkolab.objectchangelognotavailable', 'close' ); return $table->show($attrib); } /** * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill */ public static function html_diff($from, $to, $is_html = null) { // auto-detect text/html format if ($is_html === null) { $from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '') > 0); $to_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '') > 0); $is_html = $from_html || $to_html; // ensure both parts are of the same format if ($is_html && !$from_html) { $converter = new rcube_text2html($from, false, array('wrap' => true)); $from = $converter->get_html(); } if ($is_html && !$to_html) { $converter = new rcube_text2html($to, false, array('wrap' => true)); $to = $converter->get_html(); } } // compute diff from HTML if ($is_html) { include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php'; include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php'; include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php'; // replace data: urls with a transparent image to avoid memory problems $from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from); $to = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to); $diff = new Caxy\HtmlDiff\HtmlDiff($from, $to); $diffhtml = $diff->build(); // remove empty inserts (from tables) return preg_replace('!\s*!Uims', '', $diffhtml); } else { include_once __dir__ . '/vendor/finediff.php'; $diff = new FineDiff($from, $to, FineDiff::$wordGranularity); return $diff->renderDiffToHTML(); } } /** * Return a date() format string to render identifiers for recurrence instances * * @param array Hash array with event properties * @return string Format string */ public static function recurrence_id_format($event) { return $event['allday'] ? 'Ymd' : 'Ymd\THis'; } }