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

<?php

/**
 * Kolab notes module
 *
 * Adds simple notes management features to the web client
 *
 * @version @package_version@
 * @author Thomas Bruederli <bruederli@kolabsys.com>
 *
 * Copyright (C) 2014-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_notes extends rcube_plugin
{
    public $task = '?(?!login|logout).*';
    public $allowed_prefs = array('kolab_notes_sort_col');
    public $rc;

    private $ui;
    private $lists;
    private $folders;
    private $cache = array();
    private $message_notes = array();
    private $bonnie_api = false;

    /**
     * Required startup method of a Roundcube plugin
     */
    public function init()
    {
        $this->require_plugin('libkolab');

        $this->rc = rcube::get_instance();

        // proceed initialization in startup hook
        $this->add_hook('startup', array($this, 'startup'));
    }

    /**
     * Startup hook
     */
    public function startup($args)
    {
        // the notes module can be enabled/disabled by the kolab_auth plugin
        if ($this->rc->config->get('kolab_notes_disabled', false) || !$this->rc->config->get('kolab_notes_enabled', true)) {
            return;
        }

        $this->register_task('notes');

        // load plugin configuration
        $this->load_config();

        // load localizations
        $this->add_texts('localization/', $args['task'] == 'notes' && (!$args['action'] || $args['action'] == 'dialog-ui'));
        $this->rc->load_language($_SESSION['language'], array('notes.notes' => $this->gettext('navtitle')));  // add label for task title

        if ($args['task'] == 'notes') {
            $this->add_hook('storage_init', array($this, 'storage_init'));

            // register task actions
            $this->register_action('index', array($this, 'notes_view'));
            $this->register_action('fetch', array($this, 'notes_fetch'));
            $this->register_action('get',   array($this, 'note_record'));
            $this->register_action('action', array($this, 'note_action'));
            $this->register_action('list',  array($this, 'list_action'));
            $this->register_action('dialog-ui', array($this, 'dialog_view'));
            $this->register_action('print', array($this, 'print_note'));

            if (!$this->rc->output->ajax_call && in_array($args['action'], array('dialog-ui', 'list'))) {
                $this->load_ui();
            }
        }
        else if ($args['task'] == 'mail') {
            $this->add_hook('storage_init', array($this, 'storage_init'));
            $this->add_hook('message_compose', array($this, 'mail_message_compose'));

            if (in_array($args['action'], array('show', 'preview', 'print'))) {
                $this->add_hook('message_load', array($this, 'mail_message_load'));
                $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
            }

            // add 'Append note' item to message menu
            if ($this->api->output->type == 'html' && ($_REQUEST['_rel'] ?? null) != 'note') {
                $this->api->add_content(html::tag('li', array('role' => 'menuitem'),
                    $this->api->output->button(array(
                      'command'  => 'append-kolab-note',
                      'label'    => 'kolab_notes.appendnote',
                      'type'     => 'link',
                      'classact' => 'icon appendnote active',
                      'class'    => 'icon appendnote disabled',
                      'innerclass' => 'icon note',
                    ))),
                    'messagemenu');

                $this->api->output->add_label('kolab_notes.appendnote', 'kolab_notes.editnote', 'kolab_notes.deletenotesconfirm', 'kolab_notes.entertitle', 'save', 'delete', 'cancel', 'close');
                $this->include_script('notes_mail.js');
            }
        }

        if (!$this->rc->output->ajax_call && empty($this->rc->output->env['framed'])) {
            $this->load_ui();
        }

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

        // notes use fully encoded identifiers
        kolab_storage::$encode_ids = true;
    }

    /**
     * Hook into IMAP FETCH HEADER.FIELDS command and request MESSAGE-ID
     */
    public function storage_init($p)
    {
        $p['fetch_headers'] = trim($p['fetch_headers'] . ' MESSAGE-ID');
        return $p;
    }

    /**
     * Load and initialize UI class
     */
    private function load_ui()
    {
        if (!$this->ui) {
            require_once($this->home . '/kolab_notes_ui.php');
            $this->ui = new kolab_notes_ui($this);
            $this->ui->init();
        }
    }

    /**
     * Read available calendars for the current user and store them internally
     */
    private function _read_lists($force = false)
    {
        // already read sources
        if (isset($this->lists) && !$force)
            return $this->lists;

        // get all folders that have type "task"
        $folders = kolab_storage::sort_folders(kolab_storage::get_folders('note'));
        $this->lists = $this->folders = array();

        // find default folder
        $default_index = 0;
        foreach ($folders as $i => $folder) {
            if ($folder->default)
                $default_index = $i;
        }

        // put default folder on top of the list
        if ($default_index > 0) {
            $default_folder = $folders[$default_index];
            unset($folders[$default_index]);
            array_unshift($folders, $default_folder);
        }

        foreach ($folders as $folder) {
            $item = $this->folder_props($folder);
            $this->lists[$item['id']] = $item;
            $this->folders[$item['id']] = $folder;
            $this->folders[$folder->name] = $folder;
        }
    }

    /**
     * Get a list of available folders from this source
     */
    public function get_lists(&$tree = null)
    {
        $this->_read_lists();

        // attempt to create a default folder for this user
        if (empty($this->lists)) {
            $folder = array('name' => 'Notes', 'type' => 'note', 'default' => true, 'subscribed' => true);
            if (kolab_storage::folder_update($folder)) {
                $this->_read_lists(true);
            }
        }

        $folders = array();
        foreach ($this->lists as $id => $list) {
            if (!empty($this->folders[$id])) {
                $folders[] = $this->folders[$id];
            }
        }

        // include virtual folders for a full folder tree
        if (!is_null($tree)) {
            $folders = kolab_storage::folder_hierarchy($folders, $tree);
        }

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

        $lists = array();
        foreach ($folders as $folder) {
            $list_id = $folder->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());
            }

            $fullname = $folder->get_name();
            $listname = $folder->get_foldername();

            // special handling for virtual folders
            if ($folder instanceof kolab_storage_folder_user) {
                $lists[$list_id] = array(
                    'id'       => $list_id,
                    'name'     => $fullname,
                    'listname' => $listname,
                    'title'    => $folder->get_title(),
                    'virtual'  => true,
                    'editable' => false,
                    'rights'   => 'l',
                    'group'    => 'other virtual',
                    'class'    => 'user',
                    'parent'   => $parent_id,
                );
            }
            else if (!empty($folder->virtual)) {
                $lists[$list_id] = array(
                    'id'       => $list_id,
                    'name'     => $fullname,
                    'listname' => $listname,
                    'virtual'  => true,
                    'editable' => false,
                    'rights'   => 'l',
                    'group'    => $folder->get_namespace(),
                    'parent'   => $parent_id,
                );
            }
            else {
                if (!$this->lists[$list_id]) {
                    $this->lists[$list_id] = $this->folder_props($folder);
                    $this->folders[$list_id] = $folder;
                }
                $this->lists[$list_id]['parent'] = $parent_id;
                $lists[$list_id] = $this->lists[$list_id];
            }
        }

        return $lists;
    }

    /**
     * Search for shared or otherwise not listed folders the user has access
     *
     * @param string Search string
     * @param string Section/source to search
     * @return array List of notes folders
     */
    protected function search_lists($query, $source)
    {
        if (!kolab_storage::setup()) {
            return array();
        }

        $this->search_more_results = false;
        $this->lists = $this->folders = array();

        // find unsubscribed IMAP folders that have "event" type
        if ($source == 'folders') {
            foreach ((array)kolab_storage::search_folders('note', $query, array('other')) as $folder) {
                $this->folders[$folder->id] = $folder;
                $this->lists[$folder->id] = $this->folder_props($folder);
            }
        }
        // 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 note folders shared by this user
                foreach (kolab_storage::list_user_folders($user, 'note', false) as $foldername) {
                    $folders[] = new kolab_storage_folder($foldername, 'note');
                }

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

                    foreach ($folders as $folder) {
                        $this->folders[$folder->id] = $folder;
                        $this->lists[$folder->id] = $this->folder_props($folder);
                        $count++;
                    }
                }

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

        }

        return $this->get_lists();
    }

    /**
     * Derive list properties from the given kolab_storage_folder object
     */
    protected function folder_props($folder)
    {
        if ($folder->get_namespace() == 'personal') {
            $norename = false;
            $editable = true;
            $rights = 'lrswikxtea';
            $alarms = true;
        }
        else {
            $alarms = false;
            $rights = 'lr';
            $editable = false;
            if (($myrights = $folder->get_myrights()) && !PEAR::isError($myrights)) {
                $rights = $myrights;
                if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
                    $editable = strpos($rights, 'i');
            }
            $info = $folder->get_folder_info();
            $norename = $readonly || $info['norename'] || $info['protected'];
        }

        $list_id = $folder->id;
        return array(
            'id' => $list_id,
            'name' => $folder->get_name(),
            'listname' => $folder->get_foldername(),
            'editname' => $folder->get_foldername(),
            'editable' => $editable,
            'rights'   => $rights,
            'norename' => $norename,
            'parentfolder' => $folder->get_parent(),
            'subscribed' => (bool)$folder->is_subscribed(),
            'default'  => $folder->default,
            'group'    => $folder->default ? 'default' : $folder->get_namespace(),
            'class'    => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
        );
    }

    /**
     * Get the kolab_calendar instance for the given calendar ID
     *
     * @param string List identifier (encoded imap folder name)
     * @return object kolab_storage_folder Object nor null if list doesn't exist
     */
    public function get_folder($id)
    {
        // create list and folder instance if necesary
        if (!$this->lists[$id]) {
            $folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
            if ($folder->type) {
                $this->folders[$id] = $folder;
                $this->lists[$id] = $this->folder_props($folder);
            }
        }

        return $this->folders[$id];
    }

    /*******  UI functions  ********/

    /**
     * Render main view of the tasklist task
     */
    public function notes_view()
    {
        $this->ui->init();
        $this->ui->init_templates();
        $this->rc->output->set_pagetitle($this->gettext('navtitle'));
        $this->rc->output->send('kolab_notes.notes');
    }

    /**
     * Deliver a rediced UI for inline (dialog)
     */
    public function dialog_view()
    {
        // resolve message reference
        if ($msgref = rcube_utils::get_input_value('_msg', rcube_utils::INPUT_GPC, true)) {
            $storage = $this->rc->get_storage();
            list($uid, $folder) = explode('-', $msgref, 2);
            if ($message = $storage->get_message_headers($msgref)) {
                $this->rc->output->set_env('kolab_notes_template', array(
                    '_from_mail' => true,
                    'title' => $message->get('subject'),
                    'links' => array(kolab_storage_config::get_message_reference(
                        kolab_storage_config::get_message_uri($message, $folder),
                        'note'
                    )),
                ));
            }
        }

        $this->ui->init_templates();
        $this->rc->output->send('kolab_notes.dialogview');
    }

    /**
     * Handler to retrieve note records for the given list and/or search query
     */
    public function notes_fetch()
    {
        $search = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GPC, true);
        $list   = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);

        $data = $this->notes_data($this->list_notes($list, $search), $tags);

        $this->rc->output->command('plugin.data_ready', array(
                'list'   => $list,
                'search' => $search,
                'data'   => $data,
                'tags'   => array_values($tags)
        ));
    }

    /**
     * Convert the given note records for delivery to the client
     */
    protected function notes_data($records, &$tags)
    {
        $config = kolab_storage_config::get_instance();
        $tags   = $config->apply_tags($records);
        $config->apply_links($records);

        foreach ($records as $i => $rec) {
            unset($records[$i]['description']);
            $this->_client_encode($records[$i]);
        }

        return $records;
    }

    /**
     * Read note records for the given list from the storage backend
     */
    protected function list_notes($list_id, $search = null)
    {
        $results = array();

        // query Kolab storage
        $query = array();

        // full text search (only works with cache enabled)
        if (strlen($search)) {
            $words = array_filter(rcube_utils::normalize_string(mb_strtolower($search), true));
            foreach ($words as $word) {
                if (strlen($word) > 2) {  // only words > 3 chars are stored in DB
                    $query[] = array('words', '~', $word);
                }
            }
        }

        $this->_read_lists();
        if ($folder = $this->get_folder($list_id)) {
            foreach ($folder->select($query, empty($query)) as $record) {
                // post-filter search results
                if (strlen($search)) {
                    $matches = 0;
                    $desc = $this->is_html($record) ? strip_tags($record['description']) : ($record['description'] ?? '');
                    $contents = mb_strtolower($record['title'] . $desc);

                    foreach ($words as $word) {
                        if (mb_strpos($contents, $word) !== false) {
                            $matches++;
                        }
                    }

                    // skip records not matching all search words
                    if ($matches < count($words)) {
                        continue;
                    }
                }
                $record['list'] = $list_id;
                $results[] = $record;
            }
        }

        return $results;
    }

    /**
     * Handler for delivering a full note record to the client
     */
    public function note_record()
    {
        $data = $this->get_note(array(
            'uid'  => rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
            'list' => rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC),
        ));

        // encode for client use
        if (is_array($data)) {
            $this->_client_encode($data);
        }

        $this->rc->output->command('plugin.render_note', $data);
    }

    /**
     * Get the full note record identified by the given UID + Lolder identifier
     */
    public function get_note($note)
    {
        if (is_array($note)) {
            $uid = $note['uid'] ?: $note['id'];
            $list_id = $note['list'];
        }
        else {
            $uid = $note;
        }

        // deliver from in-memory cache
        $key = $list_id . ':' . $uid;
        if (!empty($this->cache[$key])) {
            return $this->cache[$key];
        }

        $result = false;

        $this->_read_lists();
        if ($list_id) {
            if ($folder = $this->get_folder($list_id)) {
                $result = $folder->get_object($uid);
            }
        }
        // iterate over all calendar folders and search for the event ID
        else {
            foreach ($this->folders as $list_id => $folder) {
                if ($result = $folder->get_object($uid)) {
                    $result['list'] = $list_id;
                    break;
                }
            }
        }

        if ($result) {
            // get note tags
            $result['tags'] = $this->get_tags($result['uid']);
            // get note links
            $result['links'] = $this->get_links($result['uid']);
        }

        return $result;
    }

    /**
     * Helper method to encode the given note record for use in the client
     */
    private function _client_encode(&$note)
    {
        foreach ($note as $key => $prop) {
            if ($key[0] == '_' || $key == 'x-custom') {
                unset($note[$key]);
            }
        }

        foreach (array('created','changed') as $key) {
            if (is_object($note[$key]) && $note[$key] instanceof DateTime) {
                $note[$key.'_'] = $note[$key]->format('U');
                $note[$key] = $this->rc->format_date($note[$key]);
            }
        }

        // clean HTML contents
        if (!empty($note['description']) && $this->is_html($note)) {
            $note['html'] = $this->_wash_html($note['description']);
        }

        // convert link URIs references into structs
        if (array_key_exists('links', $note)) {
            foreach ((array)$note['links'] as $i => $link) {
                if (strpos($link, 'imap://') === 0 && ($msgref = kolab_storage_config::get_message_reference($link, 'note'))) {
                    $note['links'][$i] = $msgref;
                }
            }
        }

        return $note;
    }

    /**
     * Handler for client-initiated actions on a single note record
     */
    public function note_action()
    {
        $action = rcube_utils::get_input_value('_do', rcube_utils::INPUT_POST);
        $note   = rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST, true);

        $success = $silent = $refresh = false;
        switch ($action) {
            case 'new':
            case 'edit':
                if ($success = $this->save_note($note)) {
                    $refresh = $this->get_note($note);
                }
                break;

            case 'move':
                $uids = explode(',', $note['uid']);
                foreach ($uids as $uid) {
                    $note['uid'] = $uid;
                    if (!($success = $this->move_note($note, $note['to']))) {
                        $refresh = $this->get_note($note);
                        break;
                    }
                }
                break;

            case 'delete':
                $uids = explode(',', $note['uid']);
                foreach ($uids as $uid) {
                    $note['uid'] = $uid;
                    if (!($success = $this->delete_note($note))) {
                        $refresh = $this->get_note($note);
                        break;
                    }
                }
                break;

            case 'changelog':
                $data = $this->get_changelog($note);
                if (is_array($data) && !empty($data)) {
                    $rcmail = $this->rc;
                    $dtformat = $rcmail->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                    array_walk($data, function(&$change) use ($lib, $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('plugin.note_render_changelog', $data);
                }
                else {
                    $this->rc->output->command('plugin.note_render_changelog', false);
                }
                $silent = true;
                break;

            case 'diff':
                $silent = true;
                $data = $this->get_diff($note, $note['rev1'], $note['rev2']);
                if (is_array($data)) {
                    $this->rc->output->command('plugin.note_show_diff', $data);
                }
                else {
                    $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
                }
                break;

            case 'show':
                if ($rec = $this->get_revison($note, $note['rev'])) {
                    $this->rc->output->command('plugin.note_show_revision', $this->_client_encode($rec));
                }
                else {
                    $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
                }
                $silent = true;
                break;

            case 'restore':
                if ($this->restore_revision($note, $note['rev'])) {
                    $refresh = $this->get_note($note);
                    $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $note['rev']))), 'confirmation');
                    $this->rc->output->command('plugin.close_history_dialog');
                }
                else {
                    $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
                }
                $silent = true;
                break;
        }

        // show confirmation/error message
        if ($success) {
            $this->rc->output->show_message('successfullysaved', 'confirmation');
        }
        else if (!$silent) {
            $this->rc->output->show_message('errorsaving', 'error');
        }

        // unlock client
        $this->rc->output->command('plugin.unlock_saving');

        if ($refresh) {
            $this->rc->output->command('plugin.update_note', $this->_client_encode($refresh));
        }
    }

    /**
     * Update an note record with the given data
     *
     * @param array Hash array with note properties (id, list)
     * @return boolean True on success, False on error
     */
    private function save_note(&$note)
    {
        $this->_read_lists();

        $list_id = $note['list'];
        if (!$list_id || !($folder = $this->get_folder($list_id)))
            return false;

        // moved from another folder
        if (!empty($note['_fromlist']) && ($fromfolder = $this->get_folder($note['_fromlist']))) {
            if (!$fromfolder->move($note['uid'], $folder->name))
                return false;

            unset($note['_fromlist']);
        }

        // load previous version of this record to merge
        $old = null;
        if (!empty($note['uid'])) {
            $old = $folder->get_object($note['uid']);
            if (!$old || PEAR::isError($old))
                return false;

            // merge existing properties if the update isn't complete
            if (!isset($note['title']) || !isset($note['description']))
                $note += $old;
        }

        // generate new note object from input
        $object = $this->_write_preprocess($note, $old);

        // email links and tags are handled separately
        $links = $object['links'] ?? null;
        $tags  = $object['tags'] ?? null;

        unset($object['links']);
        unset($object['tags']);

        $saved = $folder->save($object, 'note', $note['uid']);

        if (!$saved) {
            rcube::raise_error(array(
                'code' => 600, 'type' => 'php',
                'file' => __FILE__, 'line' => __LINE__,
                'message' => "Error saving note object to Kolab server"),
                true, false);
            $saved = false;
        }
        else {
            // save links in configuration.relation object
            $this->save_links($object['uid'], $links);
            // save tags in configuration.relation object
            $this->save_tags($object['uid'], $tags);

            $note         = $object;
            $note['list'] = $list_id;
            $note['tags'] = (array) $tags;

            // cache this in memory for later read
            $key = $list_id . ':' . $note['uid'];
            $this->cache[$key] = $note;
        }

        return $saved;
    }

    /**
     * Move the given note to another folder
     */
    function move_note($note, $list_id)
    {
        $this->_read_lists();

        $tofolder   = $this->get_folder($list_id);
        $fromfolder = $this->get_folder($note['list']);

        if ($fromfolder && $tofolder) {
            return $fromfolder->move($note['uid'], $tofolder->name);
        }

        return false;
    }

    /**
     * Remove a single note record from the backend
     *
     * @param array   Hash array with note properties (id, list)
     * @param boolean Remove record irreversible (mark as deleted otherwise)
     * @return boolean True on success, False on error
     */
    public function delete_note($note, $force = true)
    {
        $this->_read_lists();

        $list_id = $note['list'];
        if (!$list_id || !($folder = $this->get_folder($list_id))) {
            return false;
        }

        $status = $folder->delete($note['uid'], $force);

        if ($status) {
            $this->save_links($note['uid'], null);
            $this->save_tags($note['uid'], null);
        }

        return $status;
    }

    /**
     * Render the template for printing with placeholders
     */
    public function print_note()
    {
        $uid  = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
        $list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GET);

        $this->note = $this->get_note(array('uid' => $uid, 'list' => $list));

        // encode for client use
        if (is_array($this->note)) {
            $this->_client_encode($this->note);
        }

        $this->rc->output->set_pagetitle($this->note['title']);
        $this->rc->output->add_handlers(array(
                'noteheader' => array($this, 'print_note_header'),
                'notebody'   => array($this, 'print_note_body'),
        ));

        $this->include_script('notes.js');

        $this->rc->output->send('kolab_notes.print');
    }

    public function print_note_header()
    {
        $tags = array_map(array('rcube', 'Q'), (array) $this->note['tags']);
        $tags = implode(' ', $tags);

        return html::tag('h1', array('id' => 'notetitle'), rcube::Q($this->note['title']))
            . html::div(array('id' => 'notetags', 'class' => 'tagline'), $tags)
            . html::div('dates',
                html::label(null, rcube::Q($this->gettext('created')))
                . html::span(array('id' => 'notecreated'), rcube::Q($this->note['created']))
                . html::label(null, rcube::Q($this->gettext('changed')))
                . html::span(array('id' => 'notechanged'), rcube::Q($this->note['changed']))
            );
    }

    public function print_note_body()
    {
        return isset($this->note['html']) ? $this->note['html'] : rcube::Q($this->note['description']);
    }

    /**
     * Provide a list of revisions for the given object
     *
     * @param array  $note Hash array with note properties
     * @return array List of changes, each as a hash array
     */
    public function get_changelog($note)
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);

        $result = $uid && $mailbox ? $this->bonnie_api->changelog('note', $uid, $mailbox, $msguid) : null;
        if (is_array($result) && $result['uid'] == $uid) {
            return $result['changes'];
        }

        return false;
    }

    /**
     * Return full data of a specific revision of a note record
     *
     * @param mixed  $note UID string or hash array with note properties
     * @param mixed  $rev Revision number
     *
     * @return array Note object as hash array
     */
    public function get_revison($note, $rev)
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);

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

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

        return false;
    }

    /**
     * Get a list of property changes beteen two revisions of a note object
     *
     * @param array  $$note Hash array with note properties
     * @param mixed  $rev   Revisions: "from:to"
     *
     * @return array List of property changes, each as a hash array
     */
    public function get_diff($note, $rev1, $rev2)
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);

        // call Bonnie API
        $result = $this->bonnie_api->diff('note', $uid, $rev1, $rev2, $mailbox, $msguid);
        if (is_array($result) && $result['uid'] == $uid) {
            $result['rev1'] = $rev1;
            $result['rev2'] = $rev2;

            // convert some properties, similar to self::_client_encode()
            $keymap = array(
                'summary'  => 'title',
                'lastmodified-date' => 'changed',
            );

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

                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_);
                    }
                }

                // compute a nice diff of note contents
                if ($change['property'] == 'description') {
                    $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
                    if (!empty($change['diff_'])) {
                        unset($change['old'], $change['new']);
                        $change['diff_'] = preg_replace(array('!^.*<body[^>]*>!Uims','!</body>.*$!Uims'), '', $change['diff_']);
                        $change['diff_'] = preg_replace("!</(p|li|span)>\n!", '</\\1>', $change['diff_']);
                    }
                }
            });

            return $result;
        }

        return false;
    }

    /**
     * Command the backend to restore a certain revision of a note.
     * This shall replace the current object with an older version.
     *
     * @param array  $note Hash array with note properties (id, list)
     * @param mixed  $rev Revision number
     *
     * @return boolean True on success, False on failure
     */
    public function restore_revision($note, $rev)
    {
        if (empty($this->bonnie_api)) {
            return false;
        }

        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);

        $folder = $this->get_folder($note['list']);
        $success = false;

        if ($folder && ($raw_msg = $this->bonnie_api->rawdata('note', $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();
            }
        }

        return $success;
    }

    /**
     * Helper method to resolved the given note identifier into uid and mailbox
     *
     * @return array (uid,mailbox,msguid) tuple
     */
    private function _resolve_note_identity($note)
    {
        $mailbox = $msguid = null;

        if (!is_array($note)) {
            $note = $this->get_note($note);
        }

        if (is_array($note)) {
            $uid = $note['uid'] ?: $note['id'];
            $list = $note['list'];
        }
        else {
            return array(null, $mailbox, $msguid);
        }

        if ($folder = $this->get_folder($list)) {
            $mailbox = $folder->get_mailbox_id();

            // get object from storage in order to get the real object uid an msguid
            if ($rec = $folder->get_object($uid)) {
                $msguid = $rec['_msguid'];
                $uid = $rec['uid'];
            }
        }

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


    /**
     * Handler for client requests to list (aka folder) actions
     */
    public function list_action()
    {
        $action  = rcube_utils::get_input_value('_do', rcube_utils::INPUT_GPC);
        $list    = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC, true);
        $success = $update_cmd = false;

        if (empty($action)) {
            $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
        }

        switch ($action) {
            case 'form-new':
            case 'form-edit':
                $this->_read_lists();
                $this->ui->list_editform($action, $this->lists[$list['id']], $this->folders[$list['id']]);
                exit;

            case 'new':
                $list['type'] = 'note';
                $list['subscribed'] = true;
                $folder = kolab_storage::folder_update($list);

                if ($folder === false) {
                    $save_error = $this->gettext(kolab_storage::$last_error);
                }
                else {
                    $success = true;
                    $update_cmd = 'plugin.update_list';
                    $list['id'] = kolab_storage::folder_id($folder);
                    $list['_reload'] = true;
                }
                break;

            case 'edit':
                $this->_read_lists();
                $oldparent = $this->lists[$list['id']]['parentfolder'];
                $newfolder = kolab_storage::folder_update($list);

                if ($newfolder === false) {
                    $save_error = $this->gettext(kolab_storage::$last_error);
                }
                else {
                    $success = true;
                    $update_cmd = 'plugin.update_list';
                    $list['newid'] = kolab_storage::folder_id($newfolder);
                    $list['_reload'] = $list['parent'] != $oldparent;

                    // compose the new display name
                    $delim            = $this->rc->get_storage()->get_hierarchy_delimiter();
                    $path_imap        = explode($delim, $newfolder);
                    $list['name']     = kolab_storage::object_name($newfolder);
                    $list['editname'] = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
                    $list['listname'] = $list['editname'];
                }
                break;

            case 'delete':
                $this->_read_lists();
                $folder = $this->get_folder($list['id']);
                if ($folder && kolab_storage::folder_delete($folder->name)) {
                    $success = true;
                    $update_cmd = 'plugin.destroy_list';
                }
                else {
                    $save_error = $this->gettext(kolab_storage::$last_error);
                }
                break;

            case 'search':
                $this->load_ui();
                $results = array();
                foreach ((array)$this->search_lists(rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC)) as $id => $prop) {
                    $editname = $prop['editname'];
                    unset($prop['editname']);  // force full name to be displayed

                    // let the UI generate HTML and CSS representation for this calendar
                    $html = $this->ui->folder_list_item($id, $prop, $jsenv, true);
                    $prop += (array)$jsenv[$id];
                    $prop['editname'] = $editname;
                    $prop['html'] = $html;

                    $results[] = $prop;
                }
                // report more results available
                if ($this->driver->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));
                return;

            case 'subscribe':
                $success = false;
                if ($list['id'] && ($folder = $this->get_folder($list['id']))) {
                    if (isset($list['permanent']))
                        $success |= $folder->subscribe(intval($list['permanent']));
                    if (isset($list['active']))
                        $success |= $folder->activate(intval($list['active']));

                    // apply to child folders, too
                    if ($list['recursive']) {
                        foreach ((array)kolab_storage::list_folders($folder->name, '*', 'node') as $subfolder) {
                            if (isset($list['permanent']))
                                ($list['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
                            if (isset($list['active']))
                                ($list['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
                        }
                    }
                }
                break;
        }

        $this->rc->output->command('plugin.unlock_saving');

        if ($success) {
            $this->rc->output->show_message('successfullysaved', 'confirmation');

            if ($update_cmd) {
                $this->rc->output->command($update_cmd, $list);
            }
        }
        else {
            $error_msg = $this->gettext('errorsaving') . ($save_error ? ': ' . $save_error :'');
            $this->rc->output->show_message($error_msg, 'error');
        }
    }

    /**
     * Hook to add note attachments to message compose if the according parameter is present.
     * This completes the 'send note by mail' feature.
     */
    public function mail_message_compose($args)
    {
        if (!empty($args['param']['with_notes'])) {
            $uids = explode(',', $args['param']['with_notes']);
            $list = $args['param']['notes_list'];

            foreach ($uids as $uid) {
                if ($note = $this->get_note(array('uid' => $uid, 'list' => $list))) {
                    $data = $this->note2message($note);
                    $args['attachments'][] = array(
                        'name'     => abbreviate_string($note['title'], 50, ''),
                        'mimetype' => 'message/rfc822',
                        'data'     => $data,
                        'size'     => strlen($data),
                    );

                    if (empty($args['param']['subject'])) {
                        $args['param']['subject'] = $note['title'];
                    }
                }
            }

            unset($args['param']['with_notes'], $args['param']['notes_list']);
        }

        return $args;
    }

    /**
     * Lookup backend storage and find notes associated with the given message
     */
    public function mail_message_load($p)
    {
        if (empty($p['object']->headers->others['x-kolab-type'])) {
            $this->message_notes = $this->get_message_notes($p['object']->headers, $p['object']->folder);
        }
    }

    /**
     * Handler for 'messagebody_html' hook
     */
    public function mail_messagebody_html($args)
    {
        $html = '';
        foreach ($this->message_notes as $note) {
            $html .= html::a(array(
                'href' => $this->rc->url(array('task' => 'notes', '_list' => $note['list'], '_id' => $note['uid'])),
                'class' => 'kolabnotesref',
                'rel' => $note['uid'] . '@' . $note['list'],
                'target' => '_blank',
            ), rcube::Q($note['title']));
        }

        // prepend note links to message body
        if ($html) {
            $this->load_ui();
            $args['content'] = html::div('kolabmessagenotes boxinformation', $html) . $args['content'];
        }

        return $args;
    }

    /**
     * Determine whether the given note is HTML formatted
     */
    private function is_html($note)
    {
        // check for opening and closing <html> or <body> tags
        return !empty($note['description'])
            && preg_match('/<(html|body)(\s+[a-z]|>)/', $note['description'], $m)
            && strpos($note['description'], '</' . $m[1] . '>') > 0;
    }

    /**
     * Build an RFC 822 message from the given note
     */
    private function note2message($note)
    {
        $message = new Mail_mime("\r\n");

        $message->setParam('text_encoding', '8bit');
        $message->setParam('html_encoding', 'quoted-printable');
        $message->setParam('head_encoding', 'quoted-printable');
        $message->setParam('head_charset', RCUBE_CHARSET);
        $message->setParam('html_charset', RCUBE_CHARSET);
        $message->setParam('text_charset', RCUBE_CHARSET);

        $message->headers(array(
            'Subject' => $note['title'],
            'Date' => $note['changed']->format('r'),
        ));

        if ($this->is_html($note)) {
            $message->setHTMLBody($note['description']);

            // add a plain text version of the note content as an alternative part.
            $h2t = new rcube_html2text($note['description'], false, true, 0, RCUBE_CHARSET);
            $plain_part = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 72), "\r\n", false, RCUBE_CHARSET);
            $plain_part = trim(wordwrap($plain_part, 998, "\r\n", true));

            // make sure all line endings are CRLF
            $plain_part = preg_replace('/\r?\n/', "\r\n", $plain_part);

            $message->setTXTBody($plain_part);
        }
        else {
            $message->setTXTBody($note['description']);
        }

        return $message->getMessage();
    }

    private function save_links($uid, $links)
    {
        $config = kolab_storage_config::get_instance();
        return $config->save_object_links($uid, (array) $links);
    }

    /**
     * Find messages assigned to specified note
     */
    private function get_links($uid)
    {
        $config = kolab_storage_config::get_instance();
        return $config->get_object_links($uid);
    }

    /**
     * Get note tags
     */
    private function get_tags($uid)
    {
        $config = kolab_storage_config::get_instance();
        $tags   = $config->get_tags($uid);
        $tags   = array_map(function($v) { return $v['name']; }, $tags);

        return $tags;
    }

    /**
     * Find notes assigned to specified message
     */
    private function get_message_notes($message, $folder)
    {
        $config = kolab_storage_config::get_instance();
        $result = $config->get_message_relations($message, $folder, 'note');

        foreach ($result as $idx => $note) {
            $result[$idx]['list'] = kolab_storage::folder_id($note['_mailbox']);
        }

        return $result;
    }

    /**
     * Update note tags
     */
    private function save_tags($uid, $tags)
    {
        $config = kolab_storage_config::get_instance();
        $config->save_tags($uid, $tags);
    }

    /**
     * Process the given note data (submitted by the client) before saving it
     */
    private function _write_preprocess($note, $old = array())
    {
        $object = $note;

        // TODO: handle attachments

        // convert link references into simple URIs
        if (array_key_exists('links', $note)) {
            $object['links'] = array_map(function($link){ return is_array($link) ? $link['uri'] : strval($link); }, $note['links']);
        }
        else {
            if ($old) {
                $object['links'] = $old['links'] ?? null;
            }
        }

        // clean up HTML content
        $object['description'] = $this->_wash_html($note['description']);
        $is_html = true;

        // try to be smart and convert to plain-text if no real formatting is detected
        if (preg_match('!<body><(?:p|pre)>(.*)</(?:p|pre)></body>!Uims', $object['description'], $m)) {
            if (!preg_match('!<(a|b|i|strong|em|p|span|div|pre|li|img)(\s+[a-z]|>)!im', $m[1], $n)
                || ($n[1] != 'img' && !strpos($m[1], '</'.$n[1].'>'))
            ) {
                // $converter = new rcube_html2text($m[1], false, true, 0);
                // $object['description'] = rtrim($converter->get_text());
                $object['description'] = html_entity_decode(preg_replace('!<br(\s+/)>!', "\n", $m[1]));
                $is_html = false;
            }
        }

        // Add proper HTML header, otherwise Kontact renders it as plain text
        if ($is_html) {
            $object['description'] = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">'."\n" .
                str_replace('<head>', '<head><meta name="qrichtext" content="1" />', $object['description']);
        }

        // copy meta data (starting with _) from old object
        foreach ((array)$old as $key => $val) {
            if (!isset($object[$key]) && $key[0] == '_')
                $object[$key] = $val;
        }

        // make list of categories unique
        if (!empty($object['tags'])) {
            $object['tags'] = array_unique(array_filter($object['tags']));
        }

        unset($object['list'], $object['tempid'], $object['created'], $object['changed'], $object['created_'], $object['changed_']);
        return $object;
    }

    /**
     * Sanity checks/cleanups HTML content
     */
    private function _wash_html($html)
    {
        // Add header with charset spec., washtml cannot work without that
        $html = '<html><head>'
            . '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
            . '</head><body>' . $html . '</body></html>';

        // clean HTML with washtml by Frederic Motte
        $wash_opts = array(
            'show_washed'   => false,
            'allow_remote'  => 1,
            'charset'       => RCUBE_CHARSET,
            'html_elements' => array('html', 'head', 'meta', 'body', 'link'),
            'html_attribs'  => array('rel', 'type', 'name', 'http-equiv'),
        );

        // initialize HTML washer
        $washer = new rcube_washtml($wash_opts);

        $washer->add_callback('form', array($this, '_washtml_callback'));
        $washer->add_callback('a',    array($this, '_washtml_callback'));

        // Remove non-UTF8 characters
        $html = rcube_charset::clean($html);

        $html = $washer->wash($html);

        // remove unwanted comments (produced by washtml)
        $html = preg_replace('/<!--[^>]+-->/', '', $html);

        return $html;
    }

    /**
     * Callback function for washtml cleaning class
     */
    public function _washtml_callback($tagname, $attrib, $content, $washtml)
    {
        switch ($tagname) {
        case 'form':
            $out = html::div('form', $content);
            break;

        case 'a':
            // strip temporary link tags from plain-text markup
            $attrib = html::parse_attrib_string($attrib);
            if (!empty($attrib['class']) && strpos($attrib['class'], 'x-templink') !== false) {
                // remove link entirely
                if (strpos($attrib['href'], html_entity_decode($content)) !== false) {
                    $out = $content;
                    break;
                }
                $attrib['class'] = trim(str_replace('x-templink', '', $attrib['class']));
            }
            $out = html::a($attrib, $content);
            break;

        default:
            $out = '';
        }

        return $out;
    }

}
Back to Directory File Manager