diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php index 369bf26e..f2d30520 100644 --- a/plugins/kolab_folders/kolab_folders.php +++ b/plugins/kolab_folders/kolab_folders.php @@ -1,853 +1,850 @@ * * Copyright (C) 2011-2017, 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_folders extends rcube_plugin { public $task = '?(?!login).*'; public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy'); public $subtypes = array( 'mail' => array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail'), 'event' => array('default'), 'task' => array('default'), 'journal' => array('default'), 'note' => array('default'), 'contact' => array('default'), 'configuration' => array('default'), 'file' => array('default'), 'freebusy' => array('default'), ); public $act_types = array('event', 'task'); private $rc; private static $instance; private $expire_annotation = '/shared/vendor/cmu/cyrus-imapd/expire'; private $is_processing = false; /** * Plugin initialization. */ function init() { self::$instance = $this; $this->rc = rcube::get_instance(); // load required plugin $this->require_plugin('libkolab'); // Folder listing hooks $this->add_hook('storage_folders', array($this, 'mailboxes_list')); // Folder manager hooks $this->add_hook('folder_form', array($this, 'folder_form')); $this->add_hook('folder_update', array($this, 'folder_save')); $this->add_hook('folder_create', array($this, 'folder_save')); $this->add_hook('folder_delete', array($this, 'folder_save')); $this->add_hook('folder_rename', array($this, 'folder_save')); $this->add_hook('folders_list', array($this, 'folders_list')); // Special folders setting $this->add_hook('preferences_save', array($this, 'prefs_save')); // ACL plugin hooks $this->add_hook('acl_rights_simple', array($this, 'acl_rights_simple')); $this->add_hook('acl_rights_supported', array($this, 'acl_rights_supported')); // Resolving other user folder names $this->add_hook('render_mailboxlist', array($this, 'render_folderlist')); $this->add_hook('render_folder_selector', array($this, 'render_folderlist')); $this->add_hook('folders_list', array($this, 'render_folderlist')); } /** * Handler for mailboxes_list hook. Enables type-aware lists filtering. */ function mailboxes_list($args) { // infinite loop prevention if ($this->is_processing) { return $args; } if (!$this->metadata_support()) { return $args; } $this->is_processing = true; // get folders $folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB', $folderdata); $this->is_processing = false; if (!is_array($folders)) { return $args; } // Create default folders if ($args['root'] == '' && $args['name'] = '*') { $this->create_default_folders($folders, $args['filter'], $folderdata, $args['mode'] == 'LSUB'); } $args['folders'] = $folders; return $args; } /** * Handler for folders_list hook. Add css classes to folder rows. */ function folders_list($args) { if (!$this->metadata_support()) { return $args; } // load translations $this->add_texts('localization/', false); // Add javascript script to the client $this->include_script('kolab_folders.js'); $this->add_label('folderctype'); foreach ($this->types as $type) { $this->add_label('foldertype' . $type); } $skip_namespace = $this->rc->config->get('kolab_skip_namespace'); $skip_roots = array(); if (!empty($skip_namespace)) { $storage = $this->rc->get_storage(); foreach ((array)$skip_namespace as $ns) { foreach((array)$storage->get_namespace($ns) as $root) { $skip_roots[] = rtrim($root[0], $root[1]); } } } $this->rc->output->set_env('skip_roots', $skip_roots); $this->rc->output->set_env('foldertypes', $this->types); // get folders types $folderdata = kolab_storage::folders_typedata(); if (!is_array($folderdata)) { return $args; } // Add type-based style for table rows // See kolab_folders::folder_class_name() if (!empty($args['table'])) { $table = $args['table']; for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) { $attrib = $table->get_row_attribs($i); $folder = $attrib['foldername']; // UTF7-IMAP $type = $folderdata[$folder]; if (!$type) { $type = 'mail'; } $class_name = self::folder_class_name($type); $attrib['class'] = trim($attrib['class'] . ' ' . $class_name); $table->set_row_attribs($attrib, $i); } } // Add type-based class for list items if (!empty($args['list']) && is_array($args['list'])) { foreach ($args['list'] as $k => $item) { $folder = $item['folder_imap']; // UTF7-IMAP $type = $folderdata[$folder] ?? null; if (!$type) { $type = 'mail'; } $class_name = self::folder_class_name($type); $args['list'][$k]['class'] = trim($item['class'] . ' ' . $class_name); } } return $args; } /** * Handler for folder info/edit form (folder_form hook). * Adds folder type selector. */ function folder_form($args) { if (!$this->metadata_support()) { return $args; } // load translations $this->add_texts('localization/', false); // INBOX folder is of type mail.inbox and this cannot be changed if ($args['name'] == 'INBOX') { $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array( 'label' => $this->gettext('folderctype'), 'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')), ); $this->add_expire_input($args['form'], 'INBOX'); return $args; } if ($args['options']['is_root']) { return $args; } $mbox = strlen($args['name']) ? $args['name'] : $args['parent_name']; if (isset($_POST['_ctype'])) { $new_ctype = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST)); $new_subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST)); } // Get type of the folder or the parent if (strlen($mbox)) { list($ctype, $subtype) = $this->get_folder_type($mbox); if (strlen($args['parent_name']) && $subtype == 'default') $subtype = ''; // there can be only one } if (!$ctype) { $ctype = 'mail'; } $storage = $this->rc->get_storage(); // Don't allow changing type of shared folder, according to ACL if (strlen($mbox)) { $options = $storage->folder_info($mbox); if ($options['namespace'] != 'personal' && !in_array('a', (array)$options['rights'])) { if (in_array($ctype, $this->types)) { $value = $this->gettext('foldertype'.$ctype); } else { $value = $ctype; } if ($subtype) { $value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')'; } $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array( 'label' => $this->gettext('folderctype'), 'value' => $value, ); return $args; } } // Add javascript script to the client $this->include_script('kolab_folders.js'); // build type SELECT fields $type_select = new html_select(array('name' => '_ctype', 'id' => '_folderctype', 'onchange' => "\$('[name=\"_expire\"]').attr('disabled', \$(this).val() != 'mail')" )); $sub_select = new html_select(array('name' => '_subtype', 'id' => '_subtype')); $sub_select->add('', ''); foreach ($this->types as $type) { $type_select->add($this->gettext('foldertype'.$type), $type); } // add non-supported type if (!in_array($ctype, $this->types)) { $type_select->add($ctype, $ctype); } $sub_types = array(); foreach ($this->subtypes as $ftype => $subtypes) { $sub_types[$ftype] = array_combine($subtypes, array_map(array($this, 'gettext'), $subtypes)); // fill options for the current folder type if ($ftype == $ctype || (isset($new_ctype) && $ftype == $new_ctype)) { $sub_select->add(array_values($sub_types[$ftype]), $subtypes); } } $args['form']['props']['fieldsets']['settings']['content']['folderctype'] = array( 'label' => $this->gettext('folderctype'), 'value' => html::div('input-group', $type_select->show(isset($new_ctype) ? $new_ctype : $ctype) . $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype) ), ); $this->rc->output->set_env('kolab_folder_subtypes', $sub_types); $this->rc->output->set_env('kolab_folder_subtype', isset($new_subtype) ? $new_subtype : $subtype); $this->add_expire_input($args['form'], $args['name'], $ctype); return $args; } /** * Handler for folder update/create action (folder_update/folder_create hook). */ function folder_save($args) { // Folder actions from folders list if (empty($args['record'])) { return $args; } // Folder create/update with form $ctype = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST)); $subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST)); $mbox = $args['record']['name']; $old_mbox = $args['record']['oldname'] ?? null; - $subscribe = $args['record']['subscribe']; + $subscribe = $args['record']['subscribe'] ?? true; if (empty($ctype)) { return $args; } // load translations $this->add_texts('localization/', false); // Skip folder creation/rename in core // @TODO: Maybe we should provide folder_create_after and folder_update_after hooks? // Using create_mailbox/rename_mailbox here looks bad $args['abort'] = true; // There can be only one default folder of specified type if ($subtype == 'default') { $default = $this->get_default_folder($ctype); if ($default !== null && $old_mbox != $default) { $args['result'] = false; $args['message'] = $this->gettext('defaultfolderexists'); return $args; } } // Subtype sanity-checks else if ($subtype && (!($subtypes = $this->subtypes[$ctype]) || !in_array($subtype, $subtypes))) { $subtype = ''; } $ctype .= $subtype ? '.'.$subtype : ''; $storage = $this->rc->get_storage(); // Create folder if (!strlen($old_mbox)) { - // By default don't subscribe to non-mail folders - if ($subscribe) - $subscribe = (bool) preg_match('/^mail/', $ctype); - $result = $storage->create_folder($mbox, $subscribe); + // Set folder type if ($result) { $this->set_folder_type($mbox, $ctype); } } // Rename folder else { if ($old_mbox != $mbox) { $result = $storage->rename_folder($old_mbox, $mbox); } else { $result = true; } if ($result) { list($oldtype, $oldsubtype) = $this->get_folder_type($mbox); $oldtype .= $oldsubtype ? '.'.$oldsubtype : ''; if ($ctype != $oldtype) { $this->set_folder_type($mbox, $ctype); } } } // Set messages expiration in days if ($result && isset($_POST['_expire'])) { $expire = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST)); $expire = intval($expire) && preg_match('/^mail/', $ctype) ? intval($expire) : null; $storage->set_metadata($mbox, array($this->expire_annotation => $expire)); } $args['record']['class'] = self::folder_class_name($ctype); $args['record']['subscribe'] = $subscribe; $args['result'] = $result; return $args; } /** * Handler for user preferences save (preferences_save hook) * * @param array $args Hash array with hook parameters * * @return array Hash array with modified hook parameters */ public function prefs_save($args) { if ($args['section'] != 'folders') { return $args; } $dont_override = (array) $this->rc->config->get('dont_override', array()); // map config option name to kolab folder type annotation $opts = array( 'drafts_mbox' => 'mail.drafts', 'sent_mbox' => 'mail.sentitems', 'junk_mbox' => 'mail.junkemail', 'trash_mbox' => 'mail.wastebasket', ); // check if any of special folders has been changed foreach ($opts as $opt_name => $type) { $new = $args['prefs'][$opt_name]; $old = $this->rc->config->get($opt_name); if (!strlen($new) || $new === $old || in_array($opt_name, $dont_override)) { unset($opts[$opt_name]); } } if (empty($opts)) { return $args; } $folderdata = kolab_storage::folders_typedata(); if (!is_array($folderdata)) { return $args; } foreach ($opts as $opt_name => $type) { $foldername = $args['prefs'][$opt_name]; // get all folders of specified type $folders = array_intersect($folderdata, array($type)); // folder already annotated with specified type if (!empty($folders[$foldername])) { continue; } // set type to the new folder $this->set_folder_type($foldername, $type); // unset old folder(s) type annotation list($maintype, $subtype) = explode('.', $type); foreach (array_keys($folders) as $folder) { $this->set_folder_type($folder, $maintype); } } return $args; } /** * Handler for ACL permissions listing (acl_rights_simple hook) * * This shall combine the write and delete permissions into one item for * groupware folders as updating groupware objects is an insert + delete operation. * * @param array $args Hash array with hook parameters * * @return array Hash array with modified hook parameters */ public function acl_rights_simple($args) { if ($args['folder']) { list($type,) = $this->get_folder_type($args['folder']); // we're dealing with a groupware folder here... if ($type && $type !== 'mail') { if ($args['rights']['write'] && $args['rights']['delete']) { $write_perms = $args['rights']['write'] . $args['rights']['delete']; $rw_perms = $write_perms . $args['rights']['read']; $args['rights']['write'] = $write_perms; $args['rights']['other'] = preg_replace("/[$rw_perms]/", '', $args['rights']['other']); // add localized labels and titles for the altered items $args['labels'] = array( 'other' => $this->rc->gettext('shortacla','acl'), ); $args['titles'] = array( 'other' => $this->rc->gettext('longaclother','acl'), ); } } } return $args; } /** * Handler for ACL permissions listing (acl_rights_supported hook) * * @param array $args Hash array with hook parameters * * @return array Hash array with modified hook parameters */ public function acl_rights_supported($args) { if ($args['folder']) { list($type,) = $this->get_folder_type($args['folder']); // we're dealing with a groupware folder here... if ($type && $type !== 'mail') { // remove some irrelevant (for groupware objects) rights $args['rights'] = str_split(preg_replace('/[p]/', '', join('', $args['rights']))); } } return $args; } /** * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2 * * @return boolean */ function metadata_support() { $storage = $this->rc->get_storage(); return $storage->get_capability('METADATA') || $storage->get_capability('ANNOTATEMORE') || $storage->get_capability('ANNOTATEMORE2'); } /** * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2 * * @param string $folder Folder name * * @return array Folder content-type */ function get_folder_type($folder) { return explode('.', (string)kolab_storage::folder_type($folder)); } /** * Sets folder content-type. * * @param string $folder Folder name * @param string $type Content type * * @return boolean True on success */ function set_folder_type($folder, $type = 'mail') { return kolab_storage::set_folder_type($folder, $type); } /** * Returns the name of default folder * * @param string $type Folder type * * @return string Folder name */ function get_default_folder($type) { $folderdata = kolab_storage::folders_typedata(); if (!is_array($folderdata)) { return null; } // get all folders of specified type $folderdata = array_intersect($folderdata, array($type.'.default')); return key($folderdata); } /** * Returns CSS class name for specified folder type * * @param string $type Folder type * * @return string Class name */ static function folder_class_name($type) { if ($type && strpos($type, '.')) { list($ctype, $subtype) = explode('.', $type); return 'type-' . $ctype . ' subtype-' . $subtype; } return 'type-' . ($type ? $type : 'mail'); } /** * Creates default folders if they doesn't exist */ private function create_default_folders(&$folders, $filter, $folderdata = null, $lsub = false) { $storage = $this->rc->get_storage(); $namespace = $storage->get_namespace(); $defaults = array(); $prefix = ''; // Find personal namespace prefix if (is_array($namespace['personal']) && count($namespace['personal']) == 1) { $prefix = $namespace['personal'][0][0]; } $this->load_config(); // get configured defaults foreach ($this->types as $type) { foreach ((array)$this->subtypes[$type] as $subtype) { $opt_name = 'kolab_folders_' . $type . '_' . $subtype; if ($folder = $this->rc->config->get($opt_name)) { // convert configuration value to UTF7-IMAP charset $folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP'); // and namespace prefix if needed if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') { $folder = $prefix . $folder; } $defaults[$type . '.' . $subtype] = $folder; } } } if (empty($defaults)) { return; } if ($folderdata === null) { $folderdata = kolab_storage::folders_typedata(); } if (!is_array($folderdata)) { return; } // find default folders foreach ($defaults as $type => $foldername) { // get all folders of specified type $_folders = array_intersect($folderdata, array($type)); // default folder found if (!empty($_folders)) { continue; } list($type1, $type2) = explode('.', $type); $activate = in_array($type1, $this->act_types); $exists = false; $result = false; // check if folder exists if (!empty($folderdata[$foldername]) || $foldername == 'INBOX') { $exists = true; } else if ((!$filter || $filter == $type1) && in_array($foldername, $folders)) { // this assumes also that subscribed folder exists $exists = true; } else { $exists = $storage->folder_exists($foldername); } // create folder if (!$exists) { $exists = $storage->create_folder($foldername); } // set type + subscribe + activate if ($exists) { if ($result = kolab_storage::set_folder_type($foldername, $type)) { // check if folder is subscribed if ((!$filter || $filter == $type1) && $lsub && in_array($foldername, $folders)) { // already subscribed $subscribed = true; } else { $subscribed = $storage->subscribe($foldername); } // activate folder if ($activate) { kolab_storage::folder_activate($foldername, true); } } } // add new folder to the result if ($result && (!$filter || $filter == $type1) && (!$lsub || $subscribed)) { $folders[] = $foldername; } } } /** * Static getter for default folder of the given type * * @param string $type Folder type * * @return string Folder name */ public static function default_folder($type) { return self::$instance->get_default_folder($type); } /** * Get /shared/vendor/cmu/cyrus-imapd/expire value * * @param string $folder IMAP folder name * * @return int|false The annotation value or False if not supported */ private function get_expire_annotation($folder) { $storage = $this->rc->get_storage(); if ($storage->get_vendor() != 'cyrus') { return false; } if (!strlen($folder)) { return 0; } $value = $storage->get_metadata($folder, $this->expire_annotation); if (is_array($value)) { return !empty($value[$folder]) ? intval($value[$folder][$this->expire_annotation] ?? 0) : 0; } return false; } /** * Add expiration time input to the form if supported */ private function add_expire_input(&$form, $folder, $type = null) { if (($expire = $this->get_expire_annotation($folder)) !== false) { $post = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST)); $is_mail = empty($type) || preg_match('/^mail/i', $type); $label = $this->gettext('xdays'); $input = new html_inputfield(array( 'id' => '_kolabexpire', 'name' => '_expire', 'size' => 3, 'disabled' => !$is_mail )); if ($post && $is_mail) { $expire = (int) $post; } if (strpos($label, '$') === 0) { $label = str_replace('$x', '', $label); $html = $input->show($expire ?: '') . html::span('input-group-append', html::span('input-group-text', rcube::Q($label))); } else { $label = str_replace('$x', '', $label); $html = html::span('input-group-prepend', html::span('input-group-text', rcube::Q($label))) . $input->show($expire ?: ''); } $form['props']['fieldsets']['settings']['content']['kolabexpire'] = array( 'label' => $this->gettext('folderexpire'), 'value' => html::div('input-group', $html), ); } } /** * Handler for various folders list widgets (hooks) * * @param array $args Hash array with hook parameters * * @return array Hash array with modified hook parameters */ public function render_folderlist($args) { $storage = $this->rc->get_storage(); $ns_other = $storage->get_namespace('other'); $is_fl = $this->rc->plugins->is_processing('folders_list'); foreach ((array) $ns_other as $root) { $delim = $root[1]; $prefix = rtrim($root[0], $delim); $length = strlen($prefix); if (!$length) { continue; } // folders_list hook mode if ($is_fl) { foreach ((array) $args['list'] as $folder_name => $folder) { if (strpos($folder_name, $root[0]) === 0 && !substr_count($folder_name, $root[1], $length+1)) { if ($name = kolab_storage::folder_id2user(substr($folder_name, $length+1), true)) { $old = $args['list'][$folder_name]['display']; $content = $args['list'][$folder_name]['content']; $name = rcube::Q($name); $content = str_replace(">$old<", ">$name<", $content); $args['list'][$folder_name]['display'] = $name; $args['list'][$folder_name]['content'] = $content; } } } // TODO: Re-sort the list } // render_* hooks mode else if (!empty($args['list'][$prefix]) && !empty($args['list'][$prefix]['folders'])) { $map = array(); foreach ($args['list'][$prefix]['folders'] as $folder_name => $folder) { if ($name = kolab_storage::folder_id2user($folder_name, true)) { $args['list'][$prefix]['folders'][$folder_name]['name'] = $name; } $map[$folder_name] = $name ?: $args['list'][$prefix]['folders'][$folder_name]['name']; } // Re-sort the list uasort($map, 'strcoll'); $args['list'][$prefix]['folders'] = array_replace($map, $args['list'][$prefix]['folders']); } } return $args; } }