Page MenuHomePhorge

No OneTemporary

Size
58 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 86039ab2..ed50af7e 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -1,1166 +1,1166 @@
<?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, 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();
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$this->require_plugin('libkolab');
$this->rc = rcube::get_instance();
$this->register_task('notes');
// load plugin configuration
$this->load_config();
// 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('notes_disabled', false) || !$this->rc->config->get('notes_enabled', true)) {
return;
}
// 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'));
}
else if ($args['task'] == 'mail') {
$this->add_hook('storage_init', array($this, 'storage_init'));
$this->add_hook('message_load', array($this, 'mail_message_load'));
$this->add_hook('message_compose', array($this, 'mail_message_compose'));
$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'] != 'note') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'append-kolab-note',
'label' => 'kolab_notes.appendnote',
'type' => 'link',
'classact' => 'icon appendnote active',
'class' => 'icon appendnote',
'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 && (!$this->rc->output->env['framed'] || in_array($args['action'], array('folder-acl','dialog-ui')))) {
$this->load_ui();
}
// 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()
{
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);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
foreach ($folders as $folder) {
$item = $this->folder_props($folder, $delim);
$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_owner(),
'virtual' => true,
'editable' => false,
'group' => 'other virtual',
'class' => 'user',
'parent' => $parent_id,
);
}
else if ($folder->virtual) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => kolab_storage::object_name($fullname),
'listname' => $listname,
'virtual' => true,
'editable' => false,
'group' => $folder->get_namespace(),
'parent' => $parent_id,
);
}
else {
if (!$this->lists[$list_id]) {
$this->lists[$list_id] = $this->folder_props($folder, $delim);
$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();
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// 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, $delim);
}
}
// 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 tasks 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, $delim, array());
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
$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, $delim)
{
if ($folder->get_namespace() == 'personal') {
$norename = false;
$readonly = false;
$alarms = true;
}
else {
$alarms = false;
$readonly = true;
if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$readonly = false;
}
$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' => !$readonly,
'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, $this->rc->get_storage()->get_hierarchy_delimiter());
}
}
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_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($this->get_message_reference($this->get_message_uri($message, $folder))),
));
}
}
$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_INPUT_GPC, true);
$list = rcube_utils::get_input_value('_list', RCUBE_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)
{
$tags = array();
foreach ($records as $i => $rec) {
unset($records[$i]['description']);
$this->_client_encode($records[$i]);
foreach ((array)$rec['categories'] as $tag) {
$tags[] = $tag;
}
}
$tags = array_unique($tags);
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) as $record) {
// post-filter search results
if (strlen($search)) {
$matches = 0;
$contents = mb_strtolower(
$record['title'] .
($this->is_html($record) ? strip_tags($record['description']) : $record['description']) .
join(' ', (array)$record['categories'])
);
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_INPUT_GPC),
'list' => rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC),
));
// encode for client use
if (is_array($data)) {
$this->_client_encode($data, true);
}
$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 ($this->cache[$key]) {
return $this->cache[$key];
}
$this->_read_lists();
if ($list_id) {
if ($folder = $this->get_folder($list_id)) {
return $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;
return $result;
}
}
}
return false;
}
/**
* Helper method to encode the given note record for use in the client
*/
private function _client_encode(&$note, $resolve = false)
{
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']);
}
// resolve message links
if (is_array($note['links'])) {
$me = $this;
$note['links'] = array_map(function($link) use ($me, $resolve){
return $me->get_message_reference($link, $resolve) ?: array('uri' => $link);
}, $note['links']);
}
return $note;
}
/**
* Handler for client-initiated actions on a single note record
*/
public function note_action()
{
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_POST);
$note = rcube_utils::get_input_value('_data', RCUBE_INPUT_POST, true);
$success = false;
switch ($action) {
case 'new':
$temp_id = $rec['tempid'];
case 'edit':
if ($success = $this->save_note($note)) {
$refresh = $this->get_note($note);
$refresh['tempid'] = $temp_id;
}
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;
}
// show confirmation/error message
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
$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 ($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
if ($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);
$saved = $folder->save($object, 'note', $note['uid']);
if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving note object to Kolab server"),
true, false);
$saved = false;
}
else {
$note = $object;
$note['list'] = $list_id;
// 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;
return $folder->delete($note['uid'], $force);
}
/**
* Handler for client requests to list (aka folder) actions
*/
public function list_action()
{
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_GPC);
$list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC, true);
$success = $update_cmd = false;
if (empty($action)) {
$action = rcube_utils::get_input_value('action', RCUBE_INPUT_GPC);
}
switch ($action) {
case 'form-new':
case 'form-edit':
$this->_read_lists();
echo $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'] = str_repeat('&nbsp;&nbsp;&nbsp;', count($path_imap)) . '&raquo; ' . $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_INPUT_GPC), rcube_utils::get_input_value('source', RCUBE_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);
+ $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', 'info');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', RCUBE_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']));
}
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'];
$attachments = array();
foreach ($uids as $uid) {
if ($note = $this->get_note(array('uid' => $uid, 'list' => $list))) {
$args['attachments'][] = array(
'name' => abbreviate_string($note['title'], 50, ''),
'mimetype' => 'message/rfc822',
'data' => $this->note2message($note),
);
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 tagged with the given message-ID
*/
public function mail_message_load($p)
{
$this->message = $p['object'];
$this->message_notes = array();
// TODO: only query for notes if message was flagged with $KolabNotes ?
$message_id = trim($p['object']->headers->messageID, '<> ');
if ($message_id && $p['object']->uid) {
$query = array(array('tags','=','ref:' . $message_id));
foreach (kolab_storage::select($query, 'note') as $record) {
$record['list'] = kolab_storage::folder_id($record['_mailbox']);
$this->message_notes[] = $record;
}
}
}
/**
* 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',
), Q($note['title']));
}
// prepend note links to message body
if ($html) {
$this->load_ui();
$args['content'] = html::div('kolabmessagenotes', $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 (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();
}
/**
* Build a URI representing the given message reference
*/
private function get_message_uri($headers, $folder)
{
return sprintf('imap://%s/%s?message-id=%s&subject=%s',
$headers->folder ?: $folder,
$headers->uid,
urlencode($headers->messageID),
urlencode($headers->get('subject'))
);
}
/**
* Extract message reference components from the given URI
*/
private function parse_message_uri($uri)
{
$url = parse_url($uri);
if ($url['scheme'] == 'imap') {
parse_str($url['query'], $param);
$linkref = array(
'uri' => $uri,
'message_id' => $param['message-id'] ?: urldecode($url['fragment']),
'subject' => $param['subject'],
);
$path = explode('/', $url['host'] . $url['path']);
$linkref['msguid'] = array_pop($path);
$linkref['folder'] = join('/', $path);
return $linkref;
}
return false;
}
/**
* Resolve the email message reference from the given URI
*/
public function get_message_reference($uri, $resolve = false)
{
if ($linkref = $this->parse_message_uri($uri)) {
// fetch message subject
if ($resolve || empty($linkref['subject'])) {
$imap = $this->rc->get_storage();
if (!($message = $imap->get_message_headers($linrkef['msguid'], $linkref['folder']))) {
// try to find message using the message_id fragment
$index = $imap->search_once($imap->list_folders_subscribed('', '*', 'mail', null, true), 'HEADER MESSAGE-ID ' . $linkref['message_id']);
if ($index->count()) {
$uid = reset($index->get());
$folder = $index->get_parameters('MAILBOX');
if ($message = $imap->get_message_headers($uid, $folder)) {
// replace metadata
$linkref['subject'] = $message->get('subject');
$linkref['msguid'] = $message->uid;
$linkref['folder'] = $message->folder;
$linkref['uri'] = $this->get_message_uri($message, $folder);
}
}
}
}
$linkref['mailurl'] = $this->rc->url(array(
'task' => 'mail',
'action' => 'show',
'mbox' => $linkref['folder'],
'uid' => $linkref['msguid'],
'rel' => 'note',
));
}
return $linkref;
}
/**
* 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 {
$object['links'] = $old['links'];
}
// 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)(\s+[a-z]|>)!im', $m[1], $n) || !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 (is_array($object['categories'])) {
$object['categories'] = array_unique(array_filter($object['categories']));
}
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;
}
}
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index 45ddcbee..527e89bf 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -1,428 +1,431 @@
<?php
class kolab_notes_ui
{
private $rc;
private $plugin;
private $ready = false;
function __construct($plugin)
{
$this->plugin = $plugin;
$this->rc = $plugin->rc;
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) // already done
return;
// add taskbar button
$this->plugin->add_button(array(
'command' => 'notes',
'class' => 'button-notes',
'classsel' => 'button-notes button-selected',
'innerclass' => 'button-inner',
'label' => 'kolab_notes.navtitle',
), 'taskbar');
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/notes.css');
$this->plugin->register_action('print', array($this, 'print_template'));
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist'));
$this->plugin->register_handler('plugin.notebooks', array($this, 'folders'));
#$this->plugin->register_handler('plugin.folders_select', array($this, 'folders_select'));
$this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
$this->plugin->register_handler('plugin.listing', array($this, 'listing'));
$this->plugin->register_handler('plugin.editform', array($this, 'editform'));
$this->plugin->register_handler('plugin.notetitle', array($this, 'notetitle'));
$this->plugin->register_handler('plugin.detailview', array($this, 'detailview'));
$this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
$this->rc->output->include_script('list.js');
$this->rc->output->include_script('treelist.js');
$this->plugin->include_script('notes.js');
$this->plugin->include_script('jquery.tagedit.js');
// include kolab folderlist widget if available
if (is_readable($this->plugin->api->dir . 'libkolab/js/folderlist.js')) {
$this->plugin->api->include_script('libkolab/js/folderlist.js');
}
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
// load config options and user prefs relevant for the UI
$settings = array(
'sort_col' => $this->rc->config->get('kolab_notes_sort_col', 'changed'),
'print_template' => $this->rc->url('print'),
);
if ($list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC)) {
$settings['selected_list'] = $list;
}
if ($uid = rcube_utils::get_input_value('_id', RCUBE_INPUT_GPC)) {
$settings['selected_uid'] = $uid;
}
$lang_codes = array($_SESSION['language']);
if ($pos = strpos($_SESSION['language'], '_')) {
$lang_codes[] = substr($_SESSION['language'], 0, $pos);
}
foreach ($lang_codes as $code) {
if (file_exists(INSTALL_PATH . "program/js/tinymce/langs/$code.js")) {
$lang = $code;
break;
}
}
if (empty($lang)) {
$lang = 'en';
}
$settings['editor'] = array(
'lang' => $lang,
'spellcheck' => intval($this->rc->config->get('enable_spellcheck')),
'spelldict' => intval($this->rc->config->get('spellcheck_dictionary'))
);
$this->rc->output->set_env('kolab_notes_settings', $settings);
$this->rc->output->add_label('save','cancel');
}
public function folders($attrib)
{
$attrib += array('id' => 'rcmkolabnotebooks');
if ($attrib['type'] == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
}
$tree = $attrib['type'] != 'select' ? true : null;
$lists = $this->plugin->get_lists($tree);
$jsenv = array();
if (is_object($tree)) {
$html = $this->folder_tree_html($tree, $lists, $jsenv, $attrib);
}
else {
$html = '';
foreach ($lists as $prop) {
$id = $prop['id'];
if (!$prop['virtual']) {
unset($prop['user_id']);
$jsenv[$id] = $prop;
}
if ($attrib['type'] == 'select') {
if ($prop['editable']) {
$select->add($prop['name'], $prop['id']);
}
}
else {
$html .= html::tag('li', array('id' => 'rcmliknb' . rcube_utils::html_identifier($id), 'class' => $prop['group']),
$this->folder_list_item($id, $prop, $jsenv)
);
}
}
}
$this->rc->output->set_env('kolab_notebooks', $jsenv);
$this->rc->output->add_gui_object('notebooks', $attrib['id']);
return $attrib['type'] == 'select' ? $select->show() : html::tag('ul', $attrib, $html, html::$common_attrib);
}
/**
* Return html for a structured list <ul> for the folder tree
*/
public function folder_tree_html($node, $data, &$jsenv, $attrib)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$prop = $data[$id];
$is_collapsed = false; // TODO: determine this somehow?
- $content = $this->folder_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+ $content = $this->folder_list_item($id, $prop, $jsenv);
if (!empty($folder->children)) {
$content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
$this->folder_tree_html($folder, $data, $jsenv, $attrib));
}
if (strlen($content)) {
$out .= html::tag('li', array(
'id' => 'rcmliknb' . rcube_utils::html_identifier($id),
'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
),
$content);
}
}
return $out;
}
/**
* Helper method to build a tasklist item (HTML content and js data)
*/
- public function folder_list_item($id, $prop, &$jsenv, $activeonly = false)
+ public function folder_list_item($id, $prop, &$jsenv, $checkbox = false)
{
if (!$prop['virtual']) {
unset($prop['user_id']);
$jsenv[$id] = $prop;
}
$classes = array('folder');
if ($prop['virtual']) {
$classes[] = 'virtual';
}
else if (!$prop['editable']) {
$classes[] = 'readonly';
}
if ($prop['subscribed']) {
$classes[] = 'subscribed';
}
if ($prop['class']) {
$classes[] = $prop['class'];
}
$title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
$label_id = 'nl:' . $id;
$attr = $prop['virtual'] ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id)));
return html::div(join(' ', $classes),
html::a($attr + array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
($prop['virtual'] ? '' :
- html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) .
+ ($checkbox ?
+ html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) :
+ ''
+ ) .
html::span('handle', '') .
(isset($prop['subscribed']) ?
html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('foldersubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') :
''
)
)
);
return '';
}
public function listing($attrib)
{
$attrib += array('id' => 'rcmkolabnoteslist');
$this->rc->output->add_gui_object('noteslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
public function tagslist($attrib)
{
$attrib += array('id' => 'rcmkolabnotestagslist');
$this->rc->output->add_gui_object('notestagslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
public function editform($attrib)
{
$attrib += array('action' => '#', 'id' => 'rcmkolabnoteseditform');
$this->rc->output->add_gui_object('noteseditform', $attrib['id']);
$this->rc->output->include_script('tinymce/tinymce.min.js');
$textarea = new html_textarea(array('name' => 'content', 'id' => 'notecontent', 'cols' => 60, 'rows' => 20, 'tabindex' => 0));
return html::tag('form', $attrib, $textarea->show(), array_merge(html::$common_attrib, array('action')));
}
public function detailview($attrib)
{
$attrib += array('id' => 'rcmkolabnotesdetailview');
$this->rc->output->add_gui_object('notesdetailview', $attrib['id']);
return html::div($attrib, '');
}
public function notetitle($attrib)
{
$attrib += array('id' => 'rcmkolabnotestitle');
$this->rc->output->add_gui_object('noteviewtitle', $attrib['id']);
$summary = new html_inputfield(array('name' => 'summary', 'class' => 'notetitle inline-edit', 'size' => 60, 'tabindex' => 0));
$html = $summary->show();
$html .= html::div(array('class' => 'tagline tagedit', 'style' => 'display:none'), '&nbsp;');
$html .= html::div(array('class' => 'dates', 'style' => 'display:none'),
html::label(array(), $this->plugin->gettext('created')) .
html::span('notecreated', '') .
html::label(array(), $this->plugin->gettext('changed')) .
html::span('notechanged', '')
);
return html::div($attrib, $html);
}
public function attachments_list($attrib)
{
$attrib += array('id' => 'rcmkolabnotesattachmentslist');
$this->rc->output->add_gui_object('notesattachmentslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
/**
* Render edit for notes lists (folders)
*/
public function list_editform($action, $list, $folder)
{
if (is_object($folder)) {
$folder_name = $folder->name; // UTF7
}
else {
$folder_name = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder_name)) {
$options = $storage->folder_info($folder_name);
$path_imap = explode($delim, $folder_name);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
}
else {
$path_imap = '';
$options = array();
}
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
// folder name (default field)
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'noteslist-name', 'size' => 20));
$form['properties']['fields']['name'] = array(
'label' => $this->plugin->gettext('listname'),
'value' => $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected']))),
'id' => 'noteslist-name',
);
// prevent user from moving folder
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = kolab_storage::folder_selector('note', array('name' => 'parent', 'id' => 'parent-folder'), $folder_name);
$form['properties']['fields']['path'] = array(
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show(strlen($folder_name) ? $path_imap : ''),
'id' => 'parent-folder',
);
}
// add folder ACL tab
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->plugin->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->rc->url(array('_action' => 'folder-acl', '_folder' => $folder_name, 'framed' => 1)),
'width' => '100%',
'height' => 280,
'border' => 0,
'style' => 'border:0'),
'')
);
}
$form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$form_html .= $hiddenfield->show() . "\n";
}
}
// create form output
foreach ($form as $tab) {
if (is_array($tab['fields']) && empty($tab['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($tab['fields'] as $col => $colprop) {
$label = !empty($colprop['label']) ? $colprop['label'] : $this->plugin->gettext($col);
$table->add('title', html::label($colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $tab['content'];
}
if (!empty($content)) {
$form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) . "\n";
}
}
return html::tag('form', array('action' => "#", 'method' => "post", 'id' => "noteslistpropform"), $form_html);
}
/**
* Handler to render ACL form for a notes folder
*/
public function folder_acl()
{
$this->plugin->require_plugin('acl');
$this->rc->output->add_handler('folderacl', array($this, 'folder_acl_form'));
$this->rc->output->send('kolab_notes.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function folder_acl_form()
{
$folder = rcube_utils::get_input_value('_folder', RCUBE_INPUT_GPC);
if (strlen($folder)) {
$storage = $this->rc->get_storage();
$options = $storage->folder_info($folder);
// get sharing UI from acl plugin
$acl = $this->rc->plugins->exec_hook('folder_form',
array('form' => array(), 'options' => $options, 'name' => $folder));
}
return $acl['form']['sharing']['content'] ?: html::div('hint', $this->plugin->gettext('aclnorights'));
}
/**
* Render the template for printing with placeholders
*/
public function print_template()
{
$this->rc->output->reset(true);
echo $this->rc->output->parse('kolab_notes.print', false, false);
exit;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Dec 18, 10:58 AM (54 m, 5 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
418754
Default Alt Text
(58 KB)

Event Timeline