Viewing File: /usr/local/cpanel/base/3rdparty/roundcube/plugins/kolab_addressbook/kolab_addressbook.php

<?php

/**
 * Kolab address book
 *
 * Sample plugin to add a new address book source with data from Kolab storage
 * It provides also a possibilities to manage contact folders
 * (create/rename/delete/acl) directly in Addressbook UI.
 *
 * @version @package_version@
 * @author Thomas Bruederli <bruederli@kolabsys.com>
 * @author Aleksander Machniak <machniak@kolabsys.com>
 *
 * Copyright (C) 2011-2015, Kolab Systems AG <contact@kolabsys.com>
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

class kolab_addressbook extends rcube_plugin
{
    public $task = '?(?!logout).*';

    public $driver;
    public $bonnie_api = false;

    private $sources;
    private $rc;
    private $ui;
    private $recurrent = false;

    const GLOBAL_FIRST = 0;
    const PERSONAL_FIRST = 1;
    const GLOBAL_ONLY = 2;
    const PERSONAL_ONLY = 3;

    /**
     * Startup method of a Roundcube plugin
     */
    public function init()
    {
        $this->rc = rcube::get_instance();

        // load required plugin
        $this->require_plugin('libkolab');

        $this->load_config();

        $driver       = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
        $driver_class = "{$driver}_contacts_driver";

        require_once dirname(__FILE__) . "/drivers/{$driver}/{$driver}_contacts_driver.php";
        require_once dirname(__FILE__) . "/drivers/{$driver}/{$driver}_contacts.php";

        $this->driver = new $driver_class($this);

        // register hooks
        $this->add_hook('addressbooks_list', array($this, 'address_sources'));
        $this->add_hook('addressbook_get', array($this, 'get_address_book'));
        $this->add_hook('config_get', array($this, 'config_get'));

        if ($this->rc->task == 'addressbook') {
            $this->add_texts('localization');

            if ($this->driver instanceof kolab_contacts_driver) {
                $this->add_hook('contact_form', array($this, 'contact_form'));
                $this->add_hook('contact_photo', array($this, 'contact_photo'));
            }

            $this->add_hook('template_object_directorylist', array($this, 'directorylist_html'));

            // Plugin actions
            $this->register_action('plugin.book', array($this, 'book_actions'));
            $this->register_action('plugin.book-save', array($this, 'book_save'));
            $this->register_action('plugin.book-search', array($this, 'book_search'));
            $this->register_action('plugin.book-subscribe', array($this, 'book_subscribe'));

            $this->register_action('plugin.contact-changelog', array($this, 'contact_changelog'));
            $this->register_action('plugin.contact-diff', array($this, 'contact_diff'));
            $this->register_action('plugin.contact-restore', array($this, 'contact_restore'));

            // get configuration for the Bonnie API
            $this->bonnie_api = libkolab::get_bonnie_api();

            // Load UI elements
            if ($this->api->output->type == 'html') {
                require_once $this->home . '/lib/kolab_addressbook_ui.php';
                $this->ui = new kolab_addressbook_ui($this);

                if ($this->bonnie_api) {
                    $this->add_button(array(
                        'command'    => 'contact-history-dialog',
                        'class'      => 'history contact-history disabled',
                        'classact'   => 'history contact-history active',
                        'innerclass' => 'icon inner',
                        'label'      => 'kolab_addressbook.showhistory',
                        'type'       => 'link-menuitem'
                    ), 'contactmenu');
                }
            }
        }
        else if ($this->rc->task == 'settings') {
            $this->add_texts('localization');
            $this->add_hook('preferences_list', array($this, 'prefs_list'));
            $this->add_hook('preferences_save', array($this, 'prefs_save'));
        }

        if ($this->driver instanceof kolab_contacts_driver) {
            $this->add_hook('folder_delete', array($this, 'prefs_folder_delete'));
            $this->add_hook('folder_rename', array($this, 'prefs_folder_rename'));
            $this->add_hook('folder_update', array($this, 'prefs_folder_update'));
        }
    }

    /**
     * Handler for the addressbooks_list hook.
     *
     * This will add all instances of available Kolab-based address books
     * to the list of address sources of Roundcube.
     * This will also hide some addressbooks according to kolab_addressbook_prio setting.
     *
     * @param array $p Hash array with hook parameters
     *
     * @return array Hash array with modified hook parameters
     */
    public function address_sources($p)
    {
        $abook_prio = $this->addressbook_prio();

        // Disable all global address books
        // Assumes that all non-kolab_addressbook sources are global
        if ($abook_prio == self::PERSONAL_ONLY) {
            $p['sources'] = array();
        }

        $sources = array();
        foreach ($this->_list_sources() as $abook_id => $abook) {
            // register this address source
            $sources[$abook_id] = $this->driver->abook_prop($abook_id, $abook);

            // flag folders with 'i' right as writeable
            if ($this->rc->action == 'add' && strpos($abook->rights, 'i') !== false) {
                $sources[$abook_id]['readonly'] = false;
            }
        }

        // Add personal address sources to the list
        if ($abook_prio == self::PERSONAL_FIRST) {
            // $p['sources'] = array_merge($sources, $p['sources']);
            // Don't use array_merge(), because if you have folders name
            // that resolve to numeric identifier it will break output array keys
            foreach ($p['sources'] as $idx => $value) {
                $sources[$idx] = $value;
            }
            $p['sources'] = $sources;
        }
        else {
            // $p['sources'] = array_merge($p['sources'], $sources);
            foreach ($sources as $idx => $value) {
                $p['sources'][$idx] = $value;
            }
        }

        return $p;
    }

    /**
     *
     */
    public function directorylist_html($args)
    {
        $out     = '';
        $spec    = '';
        $kolab   = '';
        $jsdata  = [];
        $sources = (array) $this->rc->get_address_sources();

        // list all non-kolab sources first (also exclude hidden sources), special folders will go last
        foreach ($sources  as $j => $source) {
            $id = strval(strlen($source['id']) ? $source['id'] : $j);
            if (!empty($source['kolab']) || !empty($source['hidden'])) {
                continue;
            }

            // Roundcube >= 1.5, Collected Recipients and Trusted Senders sources will be listed at the end
            if ((defined('rcube_addressbook::TYPE_RECIPIENT') && $source['id'] == (string) rcube_addressbook::TYPE_RECIPIENT)
                || (defined('rcube_addressbook::TYPE_TRUSTED_SENDER') && $source['id'] == (string) rcube_addressbook::TYPE_TRUSTED_SENDER)
            ) {
                $spec .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
            }
            else {
                $out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
            }
        }

        // render a hierarchical list of kolab contact folders
        // TODO: Move this to the drivers
        if ($this->driver instanceof kolab_contacts_driver) {
            $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
            kolab_storage::folder_hierarchy($folders, $tree);
            if ($tree && !empty($tree->children)) {
                $kolab .= $this->folder_tree_html($tree, $sources, $jsdata);
            }
        }
        else {
            $filter = function($source) { return !empty($source['kolab']) && empty($source['hidden']); };
            foreach (array_filter($sources, $filter) as $j => $source) {
                $id = strval(strlen($source['id']) ? $source['id'] : $j);
                $kolab .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
            }
        }

        $out .= $kolab . $spec;

        $this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return isset($src['type']) && $src['type'] == 'group'; }));
        $this->rc->output->set_env('address_sources', array_filter($jsdata, function($src){ return !isset($src['type']) || $src['type'] != 'group'; }));

        $args['content'] = html::tag('ul', $args, $out, html::$common_attrib);
        return $args;
    }

    /**
     * Return html for a structured list <ul> for the folder tree
     */
    protected function folder_tree_html($node, $data, &$jsdata)
    {
        $out = '';
        foreach ($node->children as $folder) {
            $id = $folder->id;
            $source = $data[$id];
            $is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false;

            if (!empty($folder->virtual)) {
                $source = $this->driver->abook_prop($folder->id, $folder);
            }
            else if (empty($source)) {
                $this->sources[$id] = new kolab_contacts($folder->name);
                $source = $this->driver->abook_prop($id, $this->sources[$id]);
            }

            $content = $this->addressbook_list_item($id, $source, $jsdata);

            if (!empty($folder->children)) {
                $child_html = $this->folder_tree_html($folder, $data, $jsdata);

                // copy group items...
                if (preg_match('!<ul[^>]*>(.*)</ul>\n*$!Ums', $content, $m)) {
                    $child_html = $m[1] . $child_html;
                    $content = substr($content, 0, -strlen($m[0]));
                }
                // ... and re-create the subtree
                if (!empty($child_html)) {
                    $content .= html::tag('ul', array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), $child_html);
                }
            }

            $out .= $content . '</li>';
        }

        return $out;
    }

    /**
     *
     */
    protected function addressbook_list_item($id, $source, &$jsdata, $search_mode = false)
    {
        $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);

        if (empty($source['virtual'])) {
            $jsdata[$id] = $source;
            $jsdata[$id]['name'] = html_entity_decode($source['name'], ENT_NOQUOTES, RCUBE_CHARSET);
        }

        // set class name(s)
        $classes = array('addressbook');
        if (!empty($source['group']))
            $classes[] = $source['group'];
        if ($current === $id)
            $classes[] = 'selected';
        if (!empty($source['readonly']))
            $classes[] = 'readonly';
        if (!empty($source['virtual']))
            $classes[] = 'virtual';
        if (!empty($source['class_name']))
            $classes[] = $source['class_name'];

        $name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id);
        $label_id = 'kabt:' . $id;
        $inner = (!empty($source['virtual']) ?
            html::a(array('tabindex' => '0'), $name) :
            html::a(array(
                    'href' => $this->rc->url(array('_source' => $id)),
                    'rel' => $source['id'],
                    'id' => $label_id,
                    'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
                ), $name)
        );

        if ($this->driver instanceof kolab_contacts_driver && isset($source['subscribed'])) {
            $inner .= html::span(array(
                'class' => 'subscribed',
                'title' => $this->gettext('foldersubscribe'),
                'role' => 'checkbox',
                'aria-checked' => $source['subscribed'] ? 'true' : 'false',
            ), '');
        }

        // don't wrap in <li> but add a checkbox for search results listing
        if ($search_mode) {
            $jsdata[$id]['group'] = join(' ', $classes);

            if (!$source['virtual']) {
                $inner .= html::tag('input', array(
                    'type' => 'checkbox',
                    'name' => '_source[]',
                    'value' => $id,
                    'checked' => false,
                    'aria-labelledby' => $label_id,
                ));
            }
            return html::div(null, $inner);
        }

        $out = html::tag('li', array(
                'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
                'class' => join(' ', $classes),
                'noclose' => true,
            ),
            html::div(!empty($source['subscribed']) ? 'subscribed' : null, $inner)
        );

        $groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id);
        if ($source['groups']) {
            if (function_exists('rcmail_contact_groups')) {
                $groupdata = rcmail_contact_groups($groupdata);
            }
            else {
                // Roundcube >= 1.5
                $groupdata = rcmail_action_contacts_index::contact_groups($groupdata);
            }
        }

        $jsdata = $groupdata['jsdata'];
        $out .= $groupdata['out'];

        return $out;
    }

    /**
     * Sets autocomplete_addressbooks option according to
     * kolab_addressbook_prio setting extending list of address sources
     * to be used for autocompletion.
     */
    public function config_get($args)
    {
        if ($args['name'] != 'autocomplete_addressbooks' || $this->recurrent) {
            return $args;
        }

        $abook_prio = $this->addressbook_prio();

        // Get the original setting, use temp flag to prevent from an infinite recursion
        $this->recurrent = true;
        $sources = $this->rc->config->get('autocomplete_addressbooks');
        $this->recurrent = false;

        // Disable all global address books
        // Assumes that all non-kolab_addressbook sources are global
        if ($abook_prio == self::PERSONAL_ONLY) {
            $sources = array();
        }

        if (!is_array($sources)) {
            $sources = array();
        }

        $kolab_sources = array();
        foreach (array_keys($this->_list_sources()) as $abook_id) {
            if (!in_array($abook_id, $sources))
                $kolab_sources[] = $abook_id;
        }

        // Add personal address sources to the list
        if (!empty($kolab_sources)) {
            if ($abook_prio == self::PERSONAL_FIRST) {
                $sources = array_merge($kolab_sources, $sources);
            }
            else {
                $sources = array_merge($sources, $kolab_sources);
            }
        }

        $args['result'] = $sources;

        return $args;
    }

    /**
     * Getter for the rcube_addressbook instance
     *
     * @param array $p Hash array with hook parameters
     *
     * @return array Hash array with modified hook parameters
     */
    public function get_address_book($p)
    {
        if ($p['id']) {
            if ($source = $this->driver->get_address_book($p['id'])) {
                $p['instance'] = $source;

                // flag source as writeable if 'i' right is given
                if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) {
                    $p['instance']->readonly = false;
                }
                else if ($this->rc->action == 'delete' && strpos($p['instance']->rights, 't') !== false) {
                    $p['instance']->readonly = false;
                }
            }
        }

        return $p;
    }

    /**
     * List addressbook sources list
     */
    private function _list_sources()
    {
        // already read sources
        if (isset($this->sources)) {
            return $this->sources;
        }

        $this->sources = [];

        $abook_prio = $this->addressbook_prio();

        // Personal address source(s) disabled?
        if ($abook_prio == kolab_addressbook::GLOBAL_ONLY) {
            return $this->sources;
        }

        $folders = $this->driver->list_folders();

        // get all folders that have "contact" type
        foreach ($folders as $id => $source) {
            $this->sources[$id] = $source;
        }

        return $this->sources;
    }

    /**
     * Plugin hook called before rendering the contact form or detail view
     *
     * @param array $p Hash array with hook parameters
     *
     * @return array Hash array with modified hook parameters
     */
    public function contact_form($p)
    {
        // none of our business
        if (empty($GLOBALS['CONTACTS']) || !($GLOBALS['CONTACTS'] instanceof kolab_contacts)) {
            return $p;
        }

        // extend the list of contact fields to be displayed in the 'personal' section
        if (is_array($p['form']['personal'])) {
            $p['form']['personal']['content']['profession']    = array('size' => 40);
            $p['form']['personal']['content']['children']      = array('size' => 40);
            $p['form']['personal']['content']['freebusyurl']   = array('size' => 40);
            $p['form']['personal']['content']['pgppublickey']  = array('size' => 70);
            $p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);

            // re-order fields according to the coltypes list
            $p['form']['contact']['content']  = $this->_sort_form_fields($p['form']['contact']['content'], $GLOBALS['CONTACTS']);
            $p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content'], $GLOBALS['CONTACTS']);

            /* define a separate section 'settings'
            $p['form']['settings'] = array(
                'name'    => $this->gettext('settings'),
                'content' => array(
                    'freebusyurl'  => array('size' => 40, 'visible' => true),
                    'pgppublickey' => array('size' => 70, 'visible' => true),
                    'pkcs7publickey' => array('size' => 70, 'visible' => false),
                )
            );
            */
        }

        if ($this->bonnie_api && $this->rc->action == 'show' && empty($p['record']['rev'])) {
            $this->rc->output->set_env('kolab_audit_trail', true);
        }

        return $p;
    }

    /**
     * Plugin hook for the contact photo image
     */
    public function contact_photo($p)
    {
        // add photo data from old revision inline as data url
        if (!empty($p['record']['rev']) && !empty($p['data'])) {
            $p['url'] = 'data:image/gif;base64,' . base64_encode($p['data']);
        }

        return $p;
    }

    /**
     * Handler for contact audit trail changelog requests
     */
    public function contact_changelog()
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
        $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);

        list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);

        $result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null;
        if (is_array($result) && $result['uid'] == $uid) {
            if (is_array($result['changes'])) {
                $rcmail = $this->rc;
                $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                array_walk($result['changes'], function(&$change) use ($rcmail, $dtformat) {
                  if ($change['date']) {
                      $dt = rcube_utils::anytodatetime($change['date']);
                      if ($dt instanceof DateTime) {
                          $change['date'] = $rcmail->format_date($dt, $dtformat);
                      }
                  }
                });
            }
            $this->rc->output->command('contact_render_changelog', $result['changes']);
        }
        else {
            $this->rc->output->command('contact_render_changelog', false);
        }

        $this->rc->output->send();
    }

    /**
     * Handler for audit trail diff view requests
     */
    public function contact_diff()
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
        $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
        $rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST);
        $rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST);

        list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);

        $result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid);
        if (is_array($result) && $result['uid'] == $uid) {
            $result['rev1'] = $rev1;
            $result['rev2'] = $rev2;
            $result['cid'] = $contact;

            // convert some properties, similar to kolab_contacts::_to_rcube_contact()
            $keymap = array(
                'lastmodified-date' => 'changed',
                'additional' => 'middlename',
                'fn' => 'name',
                'tel' => 'phone',
                'url' => 'website',
                'bday' => 'birthday',
                'note' => 'notes',
                'role' => 'profession',
                'title' => 'jobtitle',
            );

            $propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number');
            $date_format = $this->rc->config->get('date_format', 'Y-m-d');

            // map kolab object properties to keys and values the client expects
            array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) {
                if (array_key_exists($change['property'], $keymap)) {
                    $change['property'] = $keymap[$change['property']];
                }

                // format date-time values
                if ($change['property'] == 'created' || $change['property'] == 'changed') {
                    if ($old_ = rcube_utils::anytodatetime($change['old'])) {
                        $change['old_'] = $this->rc->format_date($old_);
                    }
                    if ($new_ = rcube_utils::anytodatetime($change['new'])) {
                        $change['new_'] = $this->rc->format_date($new_);
                    }
                }
                // format dates
                else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') {
                    if ($old_ = rcube_utils::anytodatetime($change['old'])) {
                        $change['old_'] = $this->rc->format_date($old_, $date_format);
                    }
                    if ($new_ = rcube_utils::anytodatetime($change['new'])) {
                        $change['new_'] = $this->rc->format_date($new_, $date_format);
                    }
                }
                // convert email, website, phone values
                else if (array_key_exists($change['property'], $propmap)) {
                    $propname = $propmap[$change['property']];
                    foreach (array('old','new') as $k) {
                        $k_ = $k . '_';
                        if (!empty($change[$k])) {
                            $change[$k_] = html::quote($change[$k][$propname] ?: '--');
                            if ($change[$k]['type']) {
                                $change[$k_] .= '&nbsp;' . html::span('subtype', $this->get_type_label($change[$k]['type']));
                            }
                            $change['ishtml'] = true;
                        }
                    }
                }
                // serialize address structs
                if ($change['property'] == 'address') {
                    foreach (array('old','new') as $k) {
                        $k_ = $k . '_';
                        $change[$k]['zipcode'] = $change[$k]['code'];
                        $template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}');
                        $composite = array();
                        foreach ($change[$k] as $p => $val) {
                            if (strlen($val))
                                $composite['{'.$p.'}'] = $val;
                        }
                        $change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
                        if ($change[$k]['type']) {
                            $change[$k_] .= html::div('subtype', $this->get_type_label($change[$k]['type']));
                        }
                        $change['ishtml'] = true;
                    }

                    $change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true);
                }
                // localize gender values
                else if ($change['property'] == 'gender') {
                    if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']);
                    if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']);
                }
                // translate 'key' entries in individual properties
                else if ($change['property'] == 'key') {
                    $p = $change['old'] ?: $change['new'];
                    $t = $p['type'];
                    $change['property'] = $t . 'publickey';
                    $change['old'] = $change['old'] ? $change['old']['key'] : '';
                    $change['new'] = $change['new'] ? $change['new']['key'] : '';
                }
                // compute a nice diff of notes
                else if ($change['property'] == 'notes') {
                    $change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false);
                }
            });

            $this->rc->output->command('contact_show_diff', $result);
        }
        else {
            $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
        }

        $this->rc->output->send();
    }

    /**
     * Handler for audit trail revision restore requests
     */
    public function contact_restore()
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        $success = false;
        $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
        $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
        $rev = rcube_utils::get_input_value('rev', rcube_utils::INPUT_POST);

        list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source, $folder);

        if ($folder && ($raw_msg = $this->bonnie_api->rawdata('contact', $uid, $rev, $mailbox))) {
            $imap = $this->rc->get_storage();

            // insert $raw_msg as new message
            if ($imap->save_message($folder->name, $raw_msg, null, false)) {
                $success = true;

                // delete old revision from imap and cache
                $imap->delete_message($msguid, $folder->name);
                $folder->cache->set($msguid, false);
                $this->cache = array();
            }
        }

        if ($success) {
            $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rev))), 'confirmation');
            $this->rc->output->command('close_contact_history_dialog', $contact);
        }
        else {
            $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
        }

        $this->rc->output->send();
    }

    /**
     * Get a previous revision of the given contact record from the Bonnie API
     */
    public function get_revision($cid, $source, $rev)
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($cid, $source);

        // call Bonnie API
        $result = $this->bonnie_api->get('contact', $uid, $rev, $mailbox, $msguid);
        if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
            $format = kolab_format::factory('contact');
            $format->load($result['xml']);
            $rec = $format->to_array();

            if ($format->is_valid()) {
                $rec['rev'] = $result['rev'];
                return $rec;
            }
        }

        return false;
    }

    /**
     * Helper method to resolved the given contact identifier into uid and mailbox
     *
     * @return array (uid,mailbox,msguid) tuple
     */
    private function _resolve_contact_identity($id, $abook, &$folder = null)
    {
        $mailbox = $msguid = null;

        $source = $this->get_address_book(array('id' => $abook));
        if ($source['instance']) {
            $uid = $source['instance']->id2uid($id);
            $list = kolab_storage::id_decode($abook);
        }
        else {
            return array(null, $mailbox, $msguid);
        }

        // get resolve message UID and mailbox identifier
        if ($folder = kolab_storage::get_folder($list)) {
            $mailbox = $folder->get_mailbox_id();
            $msguid = $folder->cache->uid2msguid($uid);
        }

        return array($uid, $mailbox, $msguid);
    }

    /**
     *
     */
    private function _sort_form_fields($contents, $source)
    {
        $block = [];

        foreach (array_keys($source->coltypes) as $col) {
            if (isset($contents[$col])) {
                $block[$col] = $contents[$col];
            }
        }

        return $block;
    }

    /**
     * Handler for user preferences form (preferences_list hook)
     *
     * @param array $args Hash array with hook parameters
     *
     * @return array Hash array with modified hook parameters
     */
    public function prefs_list($args)
    {
        if ($args['section'] != 'addressbook') {
            return $args;
        }

        $ldap_public = $this->rc->config->get('ldap_public');

        // Hide option if there's no global addressbook
        if (empty($ldap_public)) {
            return $args;
        }

        // Check that configuration is not disabled
        $dont_override = (array) $this->rc->config->get('dont_override', array());
        $prio          = $this->addressbook_prio();

        if (!in_array('kolab_addressbook_prio', $dont_override)) {
            // Load localization
            $this->add_texts('localization');

            $field_id = '_kolab_addressbook_prio';
            $select   = new html_select(array('name' => $field_id, 'id' => $field_id));

            $select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
            $select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
            $select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
            $select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);

            $args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
                'title' => html::label($field_id, rcube::Q($this->gettext('addressbookprio'))),
                'content' => $select->show($prio),
            );
        }

        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'] != 'addressbook') {
            return $args;
        }

        // Check that configuration is not disabled
        $dont_override = (array) $this->rc->config->get('dont_override', array());
        $key           = 'kolab_addressbook_prio';

        if (!in_array('kolab_addressbook_prio', $dont_override) || !isset($_POST['_'.$key])) {
            $args['prefs'][$key] = (int) rcube_utils::get_input_value('_'.$key, rcube_utils::INPUT_POST);
        }

        return $args;
    }


    /**
     * Handler for plugin actions
     */
    public function book_actions()
    {
        $action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));

        if ($action == 'create') {
            $this->ui->book_edit();
        }
        else if ($action == 'edit') {
            $this->ui->book_edit();
        }
        else if ($action == 'delete') {
            $this->book_delete();
        }
    }

    /**
     * Handler for address book create/edit form submit
     */
    public function book_save()
    {
        $this->driver->folder_save();
        $this->rc->output->send('iframe');
    }

    /**
     *
     */
    public function book_search()
    {
        $results = [];
        $query   = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
        $source  = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);

        kolab_storage::$encode_ids = true;
        $search_more_results = false;
        $this->sources = array();
        $this->folders = array();

        // find unsubscribed IMAP folders that have "event" type
        if ($source == 'folders') {
            foreach ((array)kolab_storage::search_folders('contact', $query, array('other')) as $folder) {
                $this->folders[$folder->id] = $folder;
                $this->sources[$folder->id] = new kolab_contacts($folder->name);
            }
        }
        // search other user's namespace via LDAP
        else if ($source == 'users') {
            $limit = $this->rc->config->get('autocomplete_max', 15) * 2;  // we have slightly more space, so display twice the number
            foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
                $folders = array();
                // search for contact folders shared by this user
                foreach (kolab_storage::list_user_folders($user, 'contact', false) as $foldername) {
                    $folders[] = new kolab_storage_folder($foldername, 'contact');
                }

                if (count($folders)) {
                    $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
                    $this->folders[$userfolder->id] = $userfolder;
                    $this->sources[$userfolder->id] = $userfolder;

                    foreach ($folders as $folder) {
                        $this->folders[$folder->id] = $folder;
                        $this->sources[$folder->id] = new kolab_contacts($folder->name);;
                        $count++;
                    }
                }

                if ($count >= $limit) {
                    $search_more_results = true;
                    break;
                }
            }
        }

        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();

        // build results list
        foreach ($this->sources as $id => $source) {
            $folder = $this->folders[$id];
            $imap_path = explode($delim, $folder->name);

            // find parent
            do {
              array_pop($imap_path);
              $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
            }
            while (count($imap_path) > 1 && !$this->folders[$parent_id]);

            // restore "real" parent ID
            if ($parent_id && !$this->folders[$parent_id]) {
                $parent_id = kolab_storage::folder_id($folder->get_parent());
            }

            $prop = $this->driver->abook_prop($id, $source);
            $prop['parent'] = $parent_id;

            $html = $this->addressbook_list_item($id, $prop, $jsdata, true);
            unset($prop['group']);
            $prop += (array)$jsdata[$id];
            $prop['html'] = $html;

            $results[] = $prop;
        }

        // report more results available
        if ($search_more_results) {
            $this->rc->output->show_message('autocompletemore', 'notice');
        }

        $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
    }

    /**
     *
     */
    public function book_subscribe()
    {
        $success = false;
        $id = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);

        if ($id && ($folder = kolab_storage::get_folder(kolab_storage::id_decode($id)))) {
            if (isset($_POST['_permanent']))
                $success |= $folder->subscribe(intval($_POST['_permanent']));
            if (isset($_POST['_active']))
                $success |= $folder->activate(intval($_POST['_active']));

            // list groups for this address book
            if (!empty($_POST['_groups'])) {
                $abook = new kolab_contacts($folder->name);
                foreach ((array)$abook->list_groups() as $prop) {
                    $prop['source'] = $id;
                    $prop['id'] = $prop['ID'];
                    unset($prop['ID']);
                    $this->rc->output->command('insert_contact_group', $prop);
                }
            }
        }

        if ($success) {
            $this->rc->output->show_message('successfullysaved', 'confirmation');
        }
        else {
            $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
        }

        $this->rc->output->send();
    }


    /**
     * Handler for address book delete action (AJAX)
     */
    private function book_delete()
    {
        $source = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true));

        if ($source && ($result = $this->driver->folder_delete($source))) {
            $storage   = $this->rc->get_storage();
            $delimiter = $storage->get_hierarchy_delimiter();

            $this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
            $this->rc->output->set_env('pagecount', 0);
            $this->rc->output->command('set_rowcount', $this->rc->gettext('nocontactsfound'));
            $this->rc->output->command('set_env', 'delimiter', $delimiter);
            $this->rc->output->command('list_contacts_clear');
            $this->rc->output->command('book_delete_done', $source);
        }
        else {
            $this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
        }

        $this->rc->output->send();
    }

    /**
     * Returns value of kolab_addressbook_prio setting
     */
    private function addressbook_prio()
    {
        $abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');

        // Make sure any global addressbooks are defined
        if ($abook_prio == 0 || $abook_prio == 2) {
            $ldap_public = $this->rc->config->get('ldap_public');

            if (empty($ldap_public)) {
                $abook_prio = 1;
            }
        }

        return $abook_prio;
    }

    /**
     * Hook for (contact) folder deletion
     */
    function prefs_folder_delete($args)
    {
        // ignore...
        if ($args['abort'] && !$args['result']) {
            return $args;
        }

        $this->_contact_folder_rename($args['name'], false);
    }

    /**
     * Hook for (contact) folder renaming
     */
    function prefs_folder_rename($args)
    {
        // ignore...
        if ($args['abort'] && !$args['result']) {
            return $args;
        }

        $this->_contact_folder_rename($args['oldname'], $args['newname']);
    }

    /**
     * Hook for (contact) folder updates. Forward to folder_rename handler if name was changed
     */
    function prefs_folder_update($args)
    {
        // ignore...
        if ($args['abort'] && !$args['result']) {
            return $args;
        }

        if ($args['record']['name'] != $args['record']['oldname']) {
            $this->_contact_folder_rename($args['record']['oldname'], $args['record']['name']);
        }
    }

    /**
     * Apply folder renaming or deletion to the registered birthday calendar address books
     */
    private function _contact_folder_rename($oldname, $newname = false)
    {
        $update = false;
        $delimiter = $this->rc->get_storage()->get_hierarchy_delimiter();
        $bday_addressbooks = (array) $this->rc->config->get('calendar_birthday_adressbooks', []);

        foreach ($bday_addressbooks as $i => $id) {
            $folder_name = kolab_storage::id_decode($id);
            if ($oldname === $folder_name || strpos($folder_name, $oldname.$delimiter) === 0) {
                if ($newname) {  // rename
                    $new_folder = $newname . substr($folder_name, strlen($oldname));
                    $bday_addressbooks[$i] = kolab_storage::id_encode($new_folder);
                }
                else {  // delete
                    unset($bday_addressbooks[$i]);
                }
                $update = true;
            }
        }

        if ($update) {
            $this->rc->user->save_prefs(['calendar_birthday_adressbooks' => $bday_addressbooks]);
        }
    }

    /**
     * Get a localization label for specified field type
     */
    private function get_type_label($type)
    {
        // Roundcube < 1.5
        if (function_exists('rcmail_get_type_label')) {
            return rcmail_get_type_label($type);
        }

        // Roundcube >= 1.5
        return rcmail_action_contacts_index::get_type_label($type);
    }
}
Back to Directory File Manager