Page MenuHomePhorge

kolab_notes.php
No OneTemporary

Authored By
Unknown
Size
25 KB
Referenced Files
None
Subscribers
None

kolab_notes.php

<?php
/**
* Kolab notes module
*
* Adds simple notes management features to the web client
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2014-2025, Apheleia IT <contact@apheleia-it.ch>
*
* 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 = ['kolab_notes_sort_col'];
public $rc;
public $driver;
private $ui;
private $message_notes = [];
private $note;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$this->rc = rcube::get_instance();
// proceed initialization in startup hook
$this->add_hook('startup', [$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();
$driver = $this->rc->config->get('kolab_notes_driver') ?: 'kolab';
$driver_class = "{$driver}_notes_driver";
require_once __DIR__ . "/drivers/{$driver}/{$driver_class}.php";
$this->driver = new $driver_class($this);
// load localizations
$this->add_texts('localization/', $args['task'] == 'notes' && (!$args['action'] || $args['action'] == 'dialog-ui'));
$this->rc->load_language($_SESSION['language'], ['notes.notes' => $this->gettext('navtitle')]); // add label for task title
if ($args['task'] == 'notes') {
$this->add_hook('storage_init', [$this, 'storage_init']);
// register task actions
$this->register_action('index', [$this, 'notes_view']);
$this->register_action('fetch', [$this, 'notes_fetch']);
$this->register_action('get', [$this, 'note_record']);
$this->register_action('action', [$this, 'note_action']);
$this->register_action('list', [$this, 'list_action']);
$this->register_action('dialog-ui', [$this, 'dialog_view']);
$this->register_action('print', [$this, 'print_note']);
if (!$this->rc->output->ajax_call && in_array($args['action'], ['dialog-ui', 'list'])) {
$this->load_ui();
}
} elseif ($args['task'] == 'mail') {
$this->add_hook('storage_init', [$this, 'storage_init']);
$this->add_hook('message_compose', [$this, 'mail_message_compose']);
if (in_array($args['action'], ['show', 'preview', 'print'])) {
$this->add_hook('message_load', [$this, 'mail_message_load']);
$this->add_hook('template_object_messagebody', [$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',
['role' => 'menuitem'],
$this->api->output->button([
'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();
}
}
/**
* 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();
}
}
/******* 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();
[$uid, $folder] = explode('-', $msgref, 2);
if ($message = $storage->get_message_headers($msgref)) {
$message->folder = $folder;
$this->rc->output->set_env('kolab_notes_template', [
'_from_mail' => true,
'title' => $message->get('subject'),
'links' => [$this->driver->get_message_reference($message)],
]);
}
}
$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->driver->list_notes($list, $search);
$tags = [];
foreach ($data as $i => $note) {
unset($data[$i]['description']);
$tags = array_merge($tags, $note['tags'] ?? []);
$this->_client_encode($data[$i]);
}
$this->rc->output->command('plugin.data_ready', [
'list' => $list,
'search' => $search,
'data' => $data,
'tags' => array_values(array_unique($tags)),
]);
}
/**
* Handler for delivering a full note record to the client
*/
public function note_record()
{
$data = $this->driver->get_note([
'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);
}
/**
* 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 (['created', 'changed'] as $key) {
if (!empty($note[$key]) && $note[$key] instanceof DateTime) {
$note[$key . '_'] = $note[$key]->format('U');
$note[$key] = $this->rc->format_date($note[$key]);
}
}
// clean HTML contents
if ($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 = $this->driver->get_message_reference($link))) {
$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':
$moved = true;
if (!empty($note['_fromlist'])) {
$list_id = $note['list'];
$note['list'] = $note['_fromlist'];
unset($note['_fromlist']);
$moved = $this->driver->move_note($note, $list_id);
$note['list'] = $list_id;
}
if ($moved) {
if ($success = $this->driver->save_note($note)) {
$refresh = $this->driver->get_note($note);
}
}
break;
case 'move':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->driver->move_note($note, $note['to']))) {
$refresh = $this->driver->get_note($note);
break;
}
}
break;
case 'delete':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->driver->delete_note($note))) {
$refresh = $this->driver->get_note($note);
break;
}
}
break;
case 'changelog':
$data = $this->driver->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 ($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->driver->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->driver->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->driver->restore_revision($note, $note['rev'])) {
$refresh = $this->driver->get_note($note);
$this->rc->output->command('display_message', $this->gettext(['name' => 'objectrestoresuccess', 'vars' => ['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');
} elseif (!$silent) {
$last_error = $this->driver->last_error();
$error_msg = $this->gettext('errorsaving') . (!empty($last_error) ? ': ' . $last_error : '');
$this->rc->output->show_message($error_msg, 'error');
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
$this->rc->output->command('plugin.update_note', $this->_client_encode($refresh));
}
}
/**
* 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->driver->get_note(['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([
'noteheader' => [$this, 'print_note_header'],
'notebody' => [$this, 'print_note_body'],
]);
$this->include_script('notes.js');
$this->rc->output->send('kolab_notes.print');
}
public function print_note_header()
{
$tags = array_map(['rcube', 'Q'], (array) $this->note['tags']);
$tags = implode(' ', $tags);
return html::tag('h1', ['id' => 'notetitle'], rcube::Q($this->note['title']))
. html::div(['id' => 'notetags', 'class' => 'tagline'], $tags)
. html::div(
'dates',
html::label(null, rcube::Q($this->gettext('created')))
. html::span(['id' => 'notecreated'], rcube::Q($this->note['created']))
. html::label(null, rcube::Q($this->gettext('changed')))
. html::span(['id' => 'notechanged'], rcube::Q($this->note['changed']))
);
}
public function print_note_body()
{
return $this->note['html'] ?? rcube::Q($this->note['description']);
}
/**
* 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;
$jsenv = [];
if (empty($action)) {
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
}
switch ($action) {
case 'form-new':
case 'form-edit':
$this->ui->list_editform($action, $list['id']);
exit;
case 'new':
$list['subscribed'] = true;
if ($success = $this->driver->list_create($list)) {
$update_cmd = 'plugin.update_list';
$list['_reload'] = true;
}
break;
case 'edit':
$oldid = $list['id'];
$oldparent = $this->driver->list_get($list['id'])['parent'] ?? null;
if ($success = $this->driver->list_update($list)) {
$update_cmd = 'plugin.update_list';
$list['_reload'] = $list['parent'] != $oldparent;
if ($oldid != $list['id']) {
$list['newid'] = $list['id'];
$list['id'] = $oldid;
}
}
break;
case 'delete':
if ($success = $this->driver->list_delete($list)) {
$update_cmd = 'plugin.destroy_list';
}
break;
case 'search':
$this->load_ui();
$q = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
$results = [];
foreach ((array)$this->driver->search_lists($q, $source) 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 folder
$html = $this->ui->folder_list_item($id, $prop, $jsenv, true);
$prop += $jsenv[$id] ?? []; // @phpstan-ignore-line
$prop['editname'] = $editname;
$prop['html'] = $html;
$results[] = $prop;
}
if ($this->driver->has_more) {
$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 = $this->driver->list_subscribe($list);
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 {
$last_error = $this->driver->last_error();
$error_msg = $this->gettext('errorsaving') . (!empty($last_error) ? ': ' . $last_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->driver->get_note(['uid' => $uid, 'list' => $list])) {
$data = $this->note2message($note);
$args['attachments'][] = [
'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'])) {
$p['object']->headers->folder = $p['object']->folder;
$this->message_notes = $this->driver->get_message_notes($p['object']->headers);
}
}
/**
* Handler for 'messagebody_html' hook
*/
public function mail_messagebody_html($args)
{
$html = '';
foreach ($this->message_notes as $note) {
$html .= html::a([
'href' => $this->rc->url(['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
*/
public static 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([
'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();
}
/**
* Sanity checks/cleanups HTML content
*/
public 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 = [
'show_washed' => false,
'allow_remote' => 1,
'charset' => RCUBE_CHARSET,
'html_elements' => ['html', 'head', 'meta', 'body', 'link'],
'html_attribs' => ['rel', 'type', 'name', 'http-equiv'],
];
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
$washer->add_callback('form', [$this, '_washtml_callback']);
$washer->add_callback('a', [$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;
}
}

File Metadata

Mime Type
text/x-php
Expires
Sat, Apr 4, 8:51 AM (2 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
23/dd/09edc1b714bd00fdd7df07273ce8
Default Alt Text
kolab_notes.php (25 KB)

Event Timeline