Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2533839
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
218 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index c2b4945c..b472d2a4 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -1,1196 +1,1176 @@
<?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_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'] != '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);
}
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,
'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);
$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;
$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);
}
}
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($this->get_message_reference(kolab_storage_config::get_message_uri($message, $folder))),
+ '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);
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) 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'])
);
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, 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];
}
$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']);
}
return $result;
}
/**
* 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
- $me = $this;
- $note['links'] = array_map(function($link) use ($me, $resolve) {
- return $me->get_message_reference($link, $resolve) ?: array('uri' => $link);
+ $note['links'] = array_map(function($link) {
+ return kolab_storage_config::get_message_reference($link, 'note') ?: array('uri' => $link);
}, $this->get_links($note['uid']));
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 = 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);
// email links and tags are handled separately
$links = $object['links'];
$tags = $object['tags'];
unset($object['links']);
unset($object['tags']);
$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 {
// 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;
}
/**
* 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();
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(' ', count($path_imap)) . '» ' . $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', 'info');
}
$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))) {
$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 associated with the given message
*/
public function mail_message_load($p)
{
if (!$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',
), 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();
}
private function save_links($uid, $links)
{
if (empty($links)) {
$links = array();
}
$config = kolab_storage_config::get_instance();
$remove = array_diff($config->get_object_links($uid), $links);
return $config->save_object_links($uid, $links, $remove);
}
/**
* 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;
}
- /**
- * Resolve the email message reference from the given URI
- */
- public function get_message_reference($uri, $resolve = false)
- {
- if ($linkref = kolab_storage_config::parse_member_url($uri)) {
- $linkref['subject'] = $linkref['params']['subject'];
- $linkref['uri'] = $uri;
- $linkref['mailurl'] = $this->rc->url(array(
- 'task' => 'mail',
- 'action' => 'show',
- 'mbox' => $linkref['folder'],
- 'uid' => $linkref['uid'],
- 'rel' => 'note',
- ));
-
- unset($linkref['params']);
- }
-
- return $linkref;
- }
-
/**
* 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 {
$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['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;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_config.php b/plugins/libkolab/lib/kolab_storage_config.php
index 59c191da..036b8270 100644
--- a/plugins/libkolab/lib/kolab_storage_config.php
+++ b/plugins/libkolab/lib/kolab_storage_config.php
@@ -1,858 +1,866 @@
<?php
/**
* Kolab storage class providing access to configuration objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-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_storage_config
{
const FOLDER_TYPE = 'configuration';
/**
* Singleton instace of kolab_storage_config
*
* @var kolab_storage_config
*/
static protected $instance;
private $folders;
private $default;
private $enabled;
/**
* This implements the 'singleton' design pattern
*
* @return kolab_storage_config The one and only instance
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_storage_config();
}
return self::$instance;
}
/**
* Private constructor (finds default configuration folder as a config source)
*/
private function __construct()
{
// get all configuration folders
$this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
foreach ($this->folders as $folder) {
if ($folder->default) {
$this->default = $folder;
break;
}
}
// if no folder is set as default, choose the first one
if (!$this->default) {
$this->default = reset($this->folders);
}
// attempt to create a default folder if it does not exist
if (!$this->default) {
$folder_name = 'Configuration';
$folder_type = self::FOLDER_TYPE . '.default';
if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
$this->default = new kolab_storage_folder($folder_name, $folder_type);
}
}
// check if configuration folder exist
if ($this->default && $this->default->name) {
$this->enabled = true;
}
}
/**
* Check wether any configuration storage (folder) exists
*
* @return bool
*/
public function is_enabled()
{
return $this->enabled;
}
/**
* Get configuration objects
*
* @param array $filter Search filter
* @param bool $default Enable to get objects only from default folder
* @param int $limit Max. number of records (per-folder)
*
* @return array List of objects
*/
public function get_objects($filter = array(), $default = false, $limit = 0)
{
$list = array();
foreach ($this->folders as $folder) {
// we only want to read from default folder
if ($default && !$folder->default) {
continue;
}
// for better performance it's good to assume max. number of records
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($filter) as $object) {
unset($object['_formatobj']);
$list[] = $object;
}
}
return $list;
}
/**
* Get configuration object
*
* @param string $uid Object UID
* @param bool $default Enable to get objects only from default folder
*
* @return array Object data
*/
public function get_object($uid, $default = false)
{
foreach ($this->folders as $folder) {
// we only want to read from default folder
if ($default && !$folder->default) {
continue;
}
if ($object = $folder->get_object($uid)) {
return $object;
}
}
}
/**
* Create/update configuration object
*
* @param array $object Object data
* @param string $type Object type
*
* @return bool True on success, False on failure
*/
public function save(&$object, $type)
{
if (!$this->enabled) {
return false;
}
$folder = $this->find_folder($object);
if ($type) {
$object['type'] = $type;
}
return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
}
/**
* Remove configuration object
*
* @param string $uid Object UID
*
* @return bool True on success, False on failure
*/
public function delete($uid)
{
if (!$this->enabled) {
return false;
}
// fetch the object to find folder
$object = $this->get_object($uid);
if (!$object) {
return false;
}
$folder = $this->find_folder($object);
return $folder->delete($uid);
}
/**
* Find folder
*/
public function find_folder($object = array())
{
// find folder object
if ($object['_mailbox']) {
foreach ($this->folders as $folder) {
if ($folder->name == $object['_mailbox']) {
break;
}
}
}
else {
$folder = $this->default;
}
return $folder;
}
/**
* Builds relation member URI
*
* @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
*
* @return string $url Member URI
*/
public static function build_member_url($params)
{
// param is object UUID
if (is_string($params) && !empty($params)) {
return 'urn:uuid:' . $params;
}
if (empty($params) || !strlen($params['folder'])) {
return null;
}
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
// modify folder spec. according to namespace
$folder = $params['folder'];
$ns = $storage->folder_namespace($folder);
if ($ns == 'shared') {
// Note: this assumes there's only one shared namespace root
if ($ns = $storage->get_namespace('shared')) {
if ($prefix = $ns[0][0]) {
$folder = 'shared' . substr($folder, strlen($prefix));
}
}
}
else {
if ($ns == 'other') {
// Note: this assumes there's only one other users namespace root
if ($ns = $storage->get_namespace('shared')) {
if ($prefix = $ns[0][0]) {
$folder = 'user' . substr($folder, strlen($prefix));
}
}
}
else {
$folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
}
}
$folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
// build URI
$url = 'imap:///' . $folder;
// UID is optional here because sometimes we want
// to build just a member uri prefix
if ($params['uid']) {
$url .= '/' . $params['uid'];
}
unset($params['folder']);
unset($params['uid']);
if (!empty($params)) {
$url .= '?' . http_build_query($params, '', '&');
}
return $url;
}
/**
* Parses relation member string
*
* @param string $url Member URI
*
* @return array Message folder, UID, Search headers (Message-Id, Date)
*/
public static function parse_member_url($url)
{
// Look for IMAP URI:
// imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
if (strpos($url, 'imap:///') === 0) {
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
// parse_url does not work with imap:/// prefix
$url = parse_url(substr($url, 8));
$path = explode('/', $url['path']);
parse_str($url['query'], $params);
$uid = array_pop($path);
$ns = array_shift($path);
$path = array_map('rawurldecode', $path);
// resolve folder name
if ($ns == 'shared') {
$folder = implode('/', $path);
// Note: this assumes there's only one shared namespace root
if ($ns = $storage->get_namespace('shared')) {
if ($prefix = $ns[0][0]) {
$folder = $prefix . '/' . $folder;
}
}
}
else if ($ns == 'user') {
$username = array_shift($path);
$folder = implode('/', $path);
if ($username != $rcube->get_user_name()) {
// Note: this assumes there's only one other users namespace root
if ($ns = $storage->get_namespace('other')) {
if ($prefix = $ns[0][0]) {
$folder = $prefix . '/' . $username . '/' . $folder;
}
}
}
else if (!strlen($folder)) {
$folder = 'INBOX';
}
}
else {
return;
}
return array(
'folder' => $folder,
'uid' => $uid,
'params' => $params,
);
}
return false;
}
- /**
- * Simplify the given message URI by converting the mailbox
- * part into a relative IMAP path valid for the current user.
- */
- public static function local_message_uri($uri)
- {
- if (strpos($uri, 'imap:///') === 0) {
- $linkref = kolab_storage_config::parse_member_url($uri);
-
- return 'imap:///' . implode('/', array_map('rawurlencode', explode('/', $linkref['folder']))) .
- '/' . $linkref['uid'] .
- '?' . http_build_query($linkref['params'], '', '&');
- }
-
- return $uri;
- }
-
-
/**
* Build array of member URIs from set of messages
*
* @param string $folder Folder name
* @param array $messages Array of rcube_message objects
*
* @return array List of members (IMAP URIs)
*/
public static function build_members($folder, $messages)
{
$members = array();
foreach ((array) $messages as $msg) {
$params = array(
'folder' => $folder,
'uid' => $msg->uid,
);
// add search parameters:
// we don't want to build "invalid" searches e.g. that
// will return false positives (more or wrong messages)
if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
$params['message-id'] = $messageid;
$params['date'] = $date;
if ($subject = $msg->get('subject', false)) {
$params['subject'] = substr($subject, 0, 256);
}
}
$members[] = self::build_member_url($params);
}
return $members;
}
/**
* Resolve/validate/update members (which are IMAP URIs) of relation object.
*
* @param array $tag Tag object
* @param bool $force Force members list update
*
* @return array Folder/UIDs list
*/
public static function resolve_members(&$tag, $force = true)
{
$result = array();
foreach ((array) $tag['members'] as $member) {
// IMAP URI members
if ($url = self::parse_member_url($member)) {
$folder = $url['folder'];
if (!$force) {
$result[$folder][] = $url['uid'];
}
else {
$result[$folder]['uid'][] = $url['uid'];
$result[$folder]['params'][] = $url['params'];
$result[$folder]['member'][] = $member;
}
}
}
if (empty($result) || !$force) {
return $result;
}
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$search = array();
$missing = array();
// first we search messages by Folder+UID
foreach ($result as $folder => $data) {
// @FIXME: maybe better use index() which is cached?
// @TODO: consider skip_deleted option
$index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
$uids = $index->get();
// messages that were not found need to be searched by search parameters
$not_found = array_diff($data['uid'], $uids);
if (!empty($not_found)) {
foreach ($not_found as $uid) {
$idx = array_search($uid, $data['uid']);
if ($p = $data['params'][$idx]) {
$search[] = $p;
}
$missing[] = $result[$folder]['member'][$idx];
unset($result[$folder]['uid'][$idx]);
unset($result[$folder]['params'][$idx]);
unset($result[$folder]['member'][$idx]);
}
}
$result[$folder] = $uids;
}
// search in all subscribed mail folders using search parameters
if (!empty($search)) {
// remove not found members from the members list
$tag['members'] = array_diff($tag['members'], $missing);
// get subscribed folders
$folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
// @TODO: do this search in chunks (for e.g. 10 messages)?
$search_str = '';
foreach ($search as $p) {
$search_params = array();
foreach ($p as $key => $val) {
$key = strtoupper($key);
// don't search by subject, we don't want false-positives
if ($key != 'SUBJECT') {
$search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
}
}
$search_str .= ' (' . implode(' ', $search_params) . ')';
}
$search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
// search
$search = $storage->search_once($folders, $search_str);
// handle search result
$folders = (array) $search->get_parameters('MAILBOX');
foreach ($folders as $folder) {
$set = $search->get_set($folder);
$uids = $set->get();
if (!empty($uids)) {
$msgs = $storage->fetch_headers($folder, $uids, false);
$members = self::build_members($folder, $msgs);
// merge new members into the tag members list
$tag['members'] = array_merge($tag['members'], $members);
// add UIDs into the result
$result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
}
}
// update tag object with new members list
$tag['members'] = array_unique($tag['members']);
kolab_storage_config::get_instance()->save($tag, 'relation', false);
}
return $result;
}
/**
* Assign tags to kolab objects
*
* @param array $records List of kolab objects
*
* @return array List of tags
*/
public function apply_tags(&$records)
{
// first convert categories into tags
foreach ($records as $i => $rec) {
if (!empty($rec['categories'])) {
$folder = new kolab_storage_folder($rec['_mailbox']);
if ($object = $folder->get_object($rec['uid'])) {
$tags = $rec['categories'];
unset($object['categories']);
unset($records[$i]['categories']);
$this->save_tags($rec['uid'], $tags);
$folder->save($object, $rec['_type'], $rec['uid']);
}
}
}
$tags = array();
// assign tags to objects
foreach ($this->get_tags() as $tag) {
foreach ($records as $idx => $rec) {
$uid = self::build_member_url($rec['uid']);
if (in_array($uid, (array) $tag['members'])) {
$records[$idx]['tags'][] = $tag['name'];
}
}
$tags[] = $tag['name'];
}
$tags = array_unique($tags);
return $tags;
}
/**
* Update object tags
*
* @param string $uid Kolab object UID
* @param array $tags List of tag names
*/
public function save_tags($uid, $tags)
{
$url = self::build_member_url($uid);
$relations = $this->get_tags();
foreach ($relations as $idx => $relation) {
$selected = !empty($tags) && in_array($relation['name'], $tags);
$found = !empty($relation['members']) && in_array($url, $relation['members']);
$update = false;
// remove member from the relation
if ($found && !$selected) {
$relation['members'] = array_diff($relation['members'], (array) $url);
$update = true;
}
// add member to the relation
else if (!$found && $selected) {
$relation['members'][] = $url;
$update = true;
}
if ($update) {
if ($this->save($relation, 'relation')) {
$this->tags[$idx] = $relation; // update in-memory cache
}
}
if ($selected) {
$tags = array_diff($tags, (array)$relation['name']);
}
}
// create new relations
if (!empty($tags)) {
foreach ($tags as $tag) {
$relation = array(
'name' => $tag,
'members' => (array) $url,
'category' => 'tag',
);
if ($this->save($relation, 'relation')) {
$this->tags[] = $relation; // update in-memory cache
}
}
}
}
/**
* Get tags (all or referring to specified object)
*
* @param string $uid Optional object UID
*
* @return array List of Relation objects
*/
public function get_tags($uid = '*')
{
if (!isset($this->tags)) {
$default = true;
$filter = array(
array('type', '=', 'relation'),
array('category', '=', 'tag')
);
// use faster method
if ($uid && $uid != '*') {
$filter[] = array('member', '=', $uid);
$tags = $this->get_objects($filter, $default);
}
else {
$this->tags = $tags = $this->get_objects($filter, $default);
}
}
else {
$tags = $this->tags;
}
if ($uid === '*') {
return $tags;
}
$result = array();
$search = self::build_member_url($uid);
foreach ($tags as $tag) {
if (in_array($search, (array) $tag['members'])) {
$result[] = $tag;
}
}
return $result;
}
/**
* Find objects linked with the given groupware object through a relation
*
* @param string Object UUID
* @param array List of related URIs
*/
public function get_object_links($uid)
{
$links = array();
$object_uri = self::build_member_url($uid);
foreach ($this->get_relations_for_member($uid) as $relation) {
if (in_array($object_uri, (array) $relation['members'])) {
// make relation members up-to-date
kolab_storage_config::resolve_members($relation);
foreach ($relation['members'] as $member) {
if ($member != $object_uri) {
$links[] = $member;
}
}
}
}
return array_unique($links);
}
/**
*
*/
public function save_object_links($uid, $links, $remove = array())
{
$object_uri = self::build_member_url($uid);
$relations = $this->get_relations_for_member($uid);
$done = false;
foreach ($relations as $relation) {
// make relation members up-to-date
kolab_storage_config::resolve_members($relation);
// remove and add links
$members = array_diff($relation['members'], (array)$remove);
$members = array_unique(array_merge($members, $links));
// make sure the object_uri is still a member
if (!in_array($object_uri, $members)) {
$members[$object_uri];
}
// remove relation if no other members remain
if (count($members) <= 1) {
$done = $this->delete($relation['uid']);
}
// update relation object if members changed
else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
$relation['members'] = $members;
$done = $this->save($relation, 'relation');
$links = array();
}
// no changes, we're happy
else {
$done = true;
$links = array();
}
}
// create a new relation
if (!$done && !empty($links)) {
$relation = array(
'members' => array_merge($links, array($object_uri)),
'category' => 'generic',
);
$ret = $this->save($relation, 'relation');
}
return $ret;
}
/**
* Find relation objects referring to specified note
*/
public function get_relations_for_member($uid, $reltype = 'generic')
{
$default = true;
$filter = array(
array('type', '=', 'relation'),
array('category', '=', $reltype),
array('member', '=', $uid),
);
return $this->get_objects($filter, $default, 100);
}
/**
* Find kolab objects assigned to specified e-mail message
*
* @param rcube_message $message E-mail message
* @param string $folder Folder name
* @param string $type Result objects type
*
* @return array List of kolab objects
*/
public function get_message_relations($message, $folder, $type)
{
static $_cache = array();
$result = array();
$uids = array();
$default = true;
$uri = self::get_message_uri($message, $folder);
$filter = array(
array('type', '=', 'relation'),
array('category', '=', 'generic'),
);
// query by message-id
$member_id = $message->get('message-id', false);
if (empty($member_id)) {
// derive message identifier from URI
$member_id = md5($uri);
}
$filter[] = array('member', '=', $member_id);
if (!isset($_cache[$uri])) {
// get UIDs of related groupware objects
foreach ($this->get_objects($filter, $default) as $relation) {
// we don't need to update members if the URI is found
if (!in_array($uri, $relation['members'])) {
// update members...
$messages = kolab_storage_config::resolve_members($relation);
// ...and check again
if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
continue;
}
}
// find groupware object UID(s)
foreach ($relation['members'] as $member) {
if (strpos($member, 'urn:uuid:') === 0) {
$uids[] = substr($member, 9);
}
}
}
// remember this lookup
$_cache[$uri] = $uids;
}
else {
$uids = $_cache[$uri];
}
// get kolab objects of specified type
if (!empty($uids)) {
$query = array(array('uid', '=', array_unique($uids)));
$result = kolab_storage::select($query, $type);
}
return $result;
}
/**
* Build a URI representing the given message reference
*/
public static function get_message_uri($headers, $folder)
{
$params = array(
'folder' => $headers->folder ?: $folder,
'uid' => $headers->uid,
);
if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
$params['message-id'] = $messageid;
$params['date'] = $date;
if ($subject = $headers->get('subject')) {
$params['subject'] = $subject;
}
}
return self::build_member_url($params);
}
+
+ /**
+ * Resolve the email message reference from the given URI
+ */
+ public function get_message_reference($uri, $rel = null)
+ {
+ if ($linkref = self::parse_member_url($uri)) {
+ $linkref['subject'] = $linkref['params']['subject'];
+ $linkref['uri'] = $uri;
+
+ $rcmail = rcube::get_instance();
+ if (method_exists($rcmail, 'url')) {
+ $linkref['mailurl'] = $rcmail->url(array(
+ 'task' => 'mail',
+ 'action' => 'show',
+ 'mbox' => $linkref['folder'],
+ 'uid' => $linkref['uid'],
+ 'rel' => $rel,
+ ));
+ }
+
+ unset($linkref['params']);
+ }
+
+ return $linkref;
+ }
}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 6c9f4188..0cb51ae7 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -1,1432 +1,1427 @@
<?php
/**
* Kolab Groupware driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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 tasklist_kolab_driver extends tasklist_driver
{
// features supported by the backend
public $alarms = false;
public $attachments = true;
public $attendees = true;
public $undelete = false; // task undelete action
public $alarm_types = array('DISPLAY','AUDIO');
public $search_more_results;
private $rc;
private $plugin;
private $lists;
private $folders = array();
private $tasks = array();
private $tags = array();
/**
* Default constructor
*/
public function __construct($plugin)
{
$this->rc = $plugin->rc;
$this->plugin = $plugin;
if (kolab_storage::$version == '2.0') {
$this->alarm_absolute = false;
}
// tasklist use fully encoded identifiers
kolab_storage::$encode_ids = true;
$this->_read_lists();
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
}
/**
* 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('task'));
$this->lists = $this->folders = array();
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// find default folder
$default_index = 0;
foreach ($folders as $i => $folder) {
if ($folder->default && strpos($folder->name, $delim) === false)
$default_index = $i;
}
// put default folder (aka INBOX) on top of the list
if ($default_index > 0) {
$default_folder = $folders[$default_index];
unset($folders[$default_index]);
array_unshift($folders, $default_folder);
}
$prefs = $this->rc->config->get('kolab_tasklists', array());
foreach ($folders as $folder) {
$tasklist = $this->folder_props($folder, $prefs);
$this->lists[$tasklist['id']] = $tasklist;
$this->folders[$tasklist['id']] = $folder;
$this->folders[$folder->name] = $folder;
}
}
/**
* Derive list properties from the given kolab_storage_folder object
*/
protected function folder_props($folder, $prefs)
{
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; #kolab_storage::folder_id($folder->name);
$old_id = kolab_storage::folder_id($folder->name, false);
if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) {
$prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms'];
}
return array(
'id' => $list_id,
'name' => $folder->get_name(),
'listname' => $folder->get_foldername(),
'editname' => $folder->get_foldername(),
'color' => $folder->get_color('0000CC'),
'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
'editable' => !$readonly,
'norename' => $norename,
'active' => $folder->is_active(),
'parentfolder' => $folder->get_parent(),
'default' => $folder->default,
'virtual' => $folder->virtual,
'children' => true, // TODO: determine if that folder indeed has child folders
'subscribed' => (bool)$folder->is_subscribed(),
'removable' => !$folder->default,
'subtype' => $folder->subtype,
'group' => $folder->default ? 'default' : $folder->get_namespace(),
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
}
/**
* Get a list of available task lists from this source
*/
public function get_lists(&$tree = null)
{
// attempt to create a default list for this user
if (empty($this->lists) && !isset($this->search_more_results)) {
$prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
if ($this->create_list($prop))
$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();
$prefs = $this->rc->config->get('kolab_tasklists', array());
$lists = array();
foreach ($folders as $folder) {
$list_id = $folder->id; // kolab_storage::folder_id($folder->name);
$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' => $folder->get_name(),
'listname' => $listname,
'title' => $folder->get_title(),
'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(),
'class' => 'folder',
'parent' => $parent_id,
);
}
else {
if (!$this->lists[$list_id]) {
$this->lists[$list_id] = $this->folder_props($folder, $prefs);
$this->folders[$list_id] = $folder;
}
$this->lists[$list_id]['parent'] = $parent_id;
$lists[$list_id] = $this->lists[$list_id];
}
}
return $lists;
}
/**
* 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
*/
protected 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->config->get('kolab_tasklists', array()));
}
}
return $this->folders[$id];
}
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
public function create_list(&$prop)
{
$prop['type'] = 'task' . ($prop['default'] ? '.default' : '');
$prop['active'] = true; // activate folder by default
$prop['subscribed'] = true;
$folder = kolab_storage::folder_update($prop);
if ($folder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
$id = kolab_storage::folder_id($folder);
$prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
if (isset($prop['showalarms']))
$prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_tasklists'][$id])
$this->rc->user->save_prefs($prefs);
// force page reload to properly render folder hierarchy
if (!empty($prop['parent'])) {
$prop['_reload'] = true;
}
else {
$folder = kolab_storage::get_folder($folder);
$prop += $this->folder_props($folder, array());
}
return $id;
}
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
public function edit_list(&$prop)
{
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$prop['oldname'] = $folder->name;
$prop['type'] = 'task';
$newfolder = kolab_storage::folder_update($prop);
if ($newfolder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
$id = kolab_storage::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
unset($prefs['kolab_tasklists'][$prop['id']]);
if (isset($prop['showalarms']))
$prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_tasklists'][$id])
$this->rc->user->save_prefs($prefs);
// force page reload if folder name/hierarchy changed
if ($newfolder != $prop['oldname'])
$prop['_reload'] = true;
return $id;
}
return false;
}
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* permanent: True if list is to be subscribed permanently
* @return boolean True on success, Fales on failure
*/
public function subscribe_list($prop)
{
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$ret = false;
if (isset($prop['permanent']))
$ret |= $folder->subscribe(intval($prop['permanent']));
if (isset($prop['active']))
$ret |= $folder->activate(intval($prop['active']));
// apply to child folders, too
if ($prop['recursive']) {
foreach ((array)kolab_storage::list_folders($folder->name, '*', 'task') as $subfolder) {
if (isset($prop['permanent']))
($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
if (isset($prop['active']))
($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
}
}
return $ret;
}
return false;
}
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
public function delete_list($prop)
{
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
if (kolab_storage::folder_delete($folder->name))
return true;
else
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* Search for shared or otherwise not listed tasklists the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of tasklists
*/
public 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('task', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, array());
}
}
// 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, 'task', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'task');
}
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, array());
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, array());
$count++;
}
}
if ($count >= $limit) {
$this->search_more_results = true;
break;
}
}
}
return $this->get_lists();
}
/**
* Get a list of tags to assign tasks to
*
* @return array List of tags
*/
public function get_tags()
{
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags();
$backend_tags = array_map(function($v) { return $v['name']; }, $tags);
return array_values(array_unique(array_merge($this->tags, $backend_tags)));
}
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
public function count_tasks($lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
$today_date = new DateTime('now', $this->plugin->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0, 'mytasks' => 0);
foreach ($lists as $list_id) {
if (!$folder = $this->get_folder($list_id)) {
continue;
}
foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) {
$rec = $this->_to_rcube_task($record, $list_id, false);
if ($this->is_complete($rec)) // don't count complete tasks
continue;
$counts['all']++;
if ($rec['flagged'])
$counts['flagged']++;
if (empty($rec['date']))
$counts['nodate']++;
else if ($rec['date'] == $today)
$counts['today']++;
else if ($rec['date'] == $tomorrow)
$counts['tomorrow']++;
else if ($rec['date'] < $today)
$counts['overdue']++;
if ($this->plugin->is_attendee($rec) !== false)
$counts['mytasks']++;
}
}
// avoid session race conditions that will loose temporary subscriptions
$this->plugin->rc->session->nowrite = true;
return $counts;
}
/**
* Get all taks records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
public function list_tasks($filter, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
$results = array();
// query Kolab storage
$query = array();
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
$query[] = array('tags','~','x-complete');
else if (empty($filter['since']))
$query[] = array('tags','!~','x-complete');
// full text search (only works with cache enabled)
if ($filter['search']) {
$search = mb_strtolower($filter['search']);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', '~', $word);
}
}
if ($filter['since']) {
$query[] = array('changed', '>=', $filter['since']);
}
// load all tags into memory first
kolab_storage_config::get_instance()->get_tags();
foreach ($lists as $list_id) {
if (!$folder = $this->get_folder($list_id)) {
continue;
}
foreach ($folder->select($query) as $record) {
$this->load_tags($record);
$task = $this->_to_rcube_task($record, $list_id);
// TODO: post-filter tasks returned from storage
$results[] = $task;
}
}
// avoid session race conditions that will loose temporary subscriptions
$this->plugin->rc->session->nowrite = true;
return $results;
}
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
public function get_task($prop)
{
$this->_parse_id($prop);
$id = $prop['uid'];
$list_id = $prop['list'];
$folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders;
// find task in the available folders
foreach ($folders as $list_id => $folder) {
if (is_numeric($list_id) || !$folder)
continue;
if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
$this->load_tags($object);
$this->tasks[$id] = $this->_to_rcube_task($object, $list_id);
break;
}
}
return $this->tasks[$id];
}
/**
* Get all decendents of the given task record
*
* @param mixed Hash array with task properties or task UID
* @param boolean True if all childrens children should be fetched
* @return array List of all child task IDs
*/
public function get_childs($prop, $recursive = false)
{
if (is_string($prop)) {
$task = $this->get_task($prop);
$prop = array('uid' => $task['uid'], 'list' => $task['list']);
}
else {
$this->_parse_id($prop);
}
$childs = array();
$list_id = $prop['list'];
$task_ids = array($prop['uid']);
$folder = $this->get_folder($list_id);
// query for childs (recursively)
while ($folder && !empty($task_ids)) {
$query_ids = array();
foreach ($task_ids as $task_id) {
$query = array(array('tags','=','x-parent:' . $task_id));
foreach ($folder->select($query) as $record) {
// don't rely on kolab_storage_folder filtering
if ($record['parent_id'] == $task_id) {
$childs[] = $list_id . ':' . $record['uid'];
$query_ids[] = $record['uid'];
}
}
}
if (!$recursive)
break;
$task_ids = $query_ids;
}
return $childs;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* @see tasklist_driver::pending_alarms()
*/
public function pending_alarms($time, $lists = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return array();
if ($lists && is_string($lists))
$lists = explode(',', $lists);
$time = $slot + $interval;
$candidates = array();
$query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete'));
foreach ($this->lists as $lid => $list) {
// skip lists with alarms disabled
if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
continue;
$folder = $this->get_folder($lid);
foreach ($folder->select($query) as $record) {
if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-)
continue;
$task = $this->_to_rcube_task($record, $lid, false);
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($task, 'task');
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $task['title'],
'date' => $task['date'],
'time' => $task['time'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query("SELECT *"
. " FROM " . $this->rc->db->table_name('kolab_alarms', true)
. " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
. " AND `user_id` = ?",
$this->rc->user->ID
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$dbdata[$rec['alarm_id']] = $rec;
}
}
$alarms = array();
foreach ($candidates as $id => $task) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat'];
if ($notifyat <= $time)
$alarms[] = $task;
}
return $alarms;
}
/**
* (User) feedback after showing an alarm notification
* This should mark the alarm as 'shown' or snooze it for the given amount of time
*
* @param string Task identifier
* @param integer Suspend the alarm for this number of seconds
*/
public function dismiss_alarm($id, $snooze = 0)
{
// delete old alarm entry
$this->rc->db->query(
"DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . "
WHERE `alarm_id` = ? AND `user_id` = ?",
$id,
$this->rc->user->ID
);
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(
"INSERT INTO " . $this->rc->db->table_name('kolab_alarms', true) . "
(`alarm_id`, `user_id`, `dismissed`, `notifyat`)
VALUES (?, ?, ?, ?)",
$id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* Remove alarm dismissal or snooze state
*
* @param string Task identifier
*/
public function clear_alarms($id)
{
// delete alarm entry
$this->rc->db->query(
"DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . "
WHERE `alarm_id` = ? AND `user_id` = ?",
$id,
$this->rc->user->ID
);
return true;
}
/**
* Get task tags
*/
private function load_tags(&$object)
{
// this task hasn't been migrated yet
if (!empty($object['categories'])) {
// OPTIONAL: call kolab_storage_config::apply_tags() to migrate the object
$object['tags'] = (array)$object['categories'];
if (!empty($object['tags'])) {
$this->tags = array_merge($this->tags, $object['tags']);
}
}
else {
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($object['uid']);
$object['tags'] = array_map(function($v) { return $v['name']; }, $tags);
}
}
/**
* Update task tags
*/
private function save_tags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Find messages linked with a task record
*/
private function get_links($uid)
{
$config = kolab_storage_config::get_instance();
- return array_map(array('kolab_storage_config','local_message_uri'), $config->get_object_links($uid));
+ return $config->get_object_links($uid);
}
/**
*
*/
private function save_links($uid, $links)
{
// make sure we have a valid array
if (empty($links)) {
$links = array();
}
- // convert the given (simplified) message links into absolute IMAP URIs
- $links = array_map(function($link) {
- $url = parse_url(substr($link, 8));
- parse_str($url['query'], $linkref);
-
- $path = explode('/', $url['path']);
- $linkref['uid'] = array_pop($path);
- $linkref['folder'] = join('/', array_map('rawurldecode', $path));
-
- return kolab_storage_config::build_member_url($linkref);
- }, $links);
-
$config = kolab_storage_config::get_instance();
$remove = array_diff($config->get_object_links($uid), $links);
return $config->save_object_links($uid, $links, $remove);
}
/**
* Extract uid + list identifiers from the given input
*
* @param mixed array or string with task identifier(s)
*/
private function _parse_id(&$prop)
{
$id_ = null;
if (is_array($prop)) {
// 'uid' + 'list' available, nothing to be done
if (!empty($prop['uid']) && !empty($prop['list'])) {
return;
}
// 'id' is given
if (!empty($prop['id'])) {
if (!empty($prop['list'])) {
$list_id = $prop['_fromlist'] ?: $prop['list'];
if (strpos($prop['id'], $list_id.':') === 0) {
$prop['uid'] = substr($prop['id'], strlen($list_id)+1);
}
else {
$prop['uid'] = $prop['id'];
}
}
else {
$id_ = $prop['id'];
}
}
}
else {
$id_ = strval($prop);
$prop = array();
}
// split 'id' into list + uid
if (!empty($id_)) {
list($list, $uid) = explode(':', $id_, 2);
if (!empty($uid)) {
$prop['uid'] = $uid;
$prop['list'] = $list;
}
else {
$prop['uid'] = $id_;
}
}
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_task($record, $list_id, $all = true)
{
$id_prefix = $list_id . ':';
$task = array(
'id' => $id_prefix . $record['uid'],
'uid' => $record['uid'],
'title' => $record['title'],
// 'location' => $record['location'],
'description' => $record['description'],
'flagged' => $record['priority'] == 1,
'complete' => floatval($record['complete'] / 100),
'status' => $record['status'],
'parent_id' => $record['parent_id'] ? $id_prefix . $record['parent_id'] : null,
'recurrence' => $record['recurrence'],
'attendees' => $record['attendees'],
'organizer' => $record['organizer'],
'sequence' => $record['sequence'],
'tags' => $record['tags'],
'list' => $list_id,
);
// we can sometimes skip this expensive operation
if ($all) {
$task['links'] = $this->get_links($task['uid']);
}
// convert from DateTime to internal date format
if (is_a($record['due'], 'DateTime')) {
$due = $this->plugin->lib->adjust_timezone($record['due']);
$task['date'] = $due->format('Y-m-d');
if (!$record['due']->_dateonly)
$task['time'] = $due->format('H:i');
}
// convert from DateTime to internal date format
if (is_a($record['start'], 'DateTime')) {
$start = $this->plugin->lib->adjust_timezone($record['start']);
$task['startdate'] = $start->format('Y-m-d');
if (!$record['start']->_dateonly)
$task['starttime'] = $start->format('H:i');
}
if (is_a($record['changed'], 'DateTime')) {
$task['changed'] = $record['changed'];
}
if (is_a($record['created'], 'DateTime')) {
$task['created'] = $record['created'];
}
if ($record['valarms']) {
$task['valarms'] = $record['valarms'];
}
else if ($record['alarms']) {
$task['alarms'] = $record['alarms'];
}
if (!empty($task['attendees'])) {
foreach ((array)$task['attendees'] as $i => $attendee) {
if (is_array($attendee['delegated-from'])) {
$task['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
}
if (is_array($attendee['delegated-to'])) {
$task['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
}
}
}
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
$attachments[] = $attachment;
}
}
$task['attachments'] = $attachments;
}
return $task;
}
/**
* Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_task($task, $old = array())
{
$object = $task;
$id_prefix = $task['list'] . ':';
if (!empty($task['date'])) {
$object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->timezone);
if (empty($task['time']))
$object['due']->_dateonly = true;
unset($object['date']);
}
if (!empty($task['startdate'])) {
$object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
if (empty($task['starttime']))
$object['start']->_dateonly = true;
unset($object['startdate']);
}
$object['complete'] = $task['complete'] * 100;
if ($task['complete'] == 1.0 && empty($task['complete']))
$object['status'] = 'COMPLETED';
if ($task['flagged'])
$object['priority'] = 1;
else
$object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0;
// remove list: prefix from parent_id
if (!empty($task['parent_id']) && strpos($task['parent_id'], $id_prefix) === 0) {
$object['parent_id'] = substr($task['parent_id'], strlen($id_prefix));
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// copy recurrence rules if the client didn't submit it (#2713)
if (!array_key_exists('recurrence', $object) && $old['recurrence']) {
$object['recurrence'] = $old['recurrence'];
}
// delete existing attachment(s)
if (!empty($task['deleted_attachments'])) {
foreach ($task['deleted_attachments'] as $attachment) {
if (is_array($object['_attachments'])) {
foreach ($object['_attachments'] as $idx => $att) {
if ($att['id'] == $attachment)
$object['_attachments'][$idx] = false;
}
}
}
unset($task['deleted_attachments']);
}
// in kolab_storage attachments are indexed by content-id
if (is_array($task['attachments'])) {
foreach ($task['attachments'] as $idx => $attachment) {
$key = null;
// Roundcube ID has nothing to do with the storage ID, remove it
if ($attachment['content'] || $attachment['path']) {
unset($attachment['id']);
}
else {
foreach ((array)$old['_attachments'] as $cid => $oldatt) {
if ($oldatt && $attachment['id'] == $oldatt['id'])
$key = $cid;
}
}
// replace existing entry
if ($key) {
$object['_attachments'][$key] = $attachment;
}
// append as new attachment
else {
$object['_attachments'][] = $attachment;
}
}
unset($object['attachments']);
}
// allow sequence increments if I'm the organizer
if ($this->plugin->is_organizer($object)) {
unset($object['sequence']);
}
else if (isset($old['sequence'])) {
$object['sequence'] = $old['sequence'];
}
unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']);
return $object;
}
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return mixed New task ID on success, False on error
*/
public function create_task($task)
{
return $this->edit_task($task);
}
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return boolean True on success, False on error
*/
public function edit_task($task)
{
$this->_parse_id($task);
$list_id = $task['list'];
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// email links and tags are stored separately
$links = $task['links'];
$tags = $task['tags'];
unset($task['tags'], $task['links']);
// moved from another folder
if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
if (!$fromfolder->move($task['uid'], $folder))
return false;
unset($task['_fromlist']);
}
// load previous version of this task to merge
if ($task['id']) {
$old = $folder->get_object($task['uid']);
if (!$old || PEAR::isError($old))
return false;
// merge existing properties if the update isn't complete
if (!isset($task['title']) || !isset($task['complete']))
$task += $this->_to_rcube_task($old, $list_id);
}
// generate new task object from RC input
$object = $this->_from_rcube_task($task, $old);
$saved = $folder->save($object, 'task', $task['uid']);
if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving task 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);
$task = $this->_to_rcube_task($object, $list_id);
$task['tags'] = (array) $tags;
$this->tasks[$task['uid']] = $task;
}
return $saved;
}
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* @return boolean True on success, False on error
* @see tasklist_driver::move_task()
*/
public function move_task($task)
{
$this->_parse_id($task);
$list_id = $task['list'];
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// execute move command
if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
return $fromfolder->move($task['uid'], $folder);
}
return false;
}
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
public function delete_task($task, $force = true)
{
$this->_parse_id($task);
$list_id = $task['list'];
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
$status = $folder->delete($task['uid']);
if ($status) {
// remove tag assignments
// @TODO: don't do this when undelete feature will be implemented
$this->save_tags($task['uid'], null);
}
return $status;
}
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
// TODO: implement this
return false;
}
/**
* Get attachment properties
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function get_attachment($id, $task)
{
$task = $this->get_task($task);
if ($task && !empty($task['attachments'])) {
foreach ($task['attachments'] as $att) {
if ($att['id'] == $id)
return $att;
}
}
return null;
}
/**
* Get attachment body
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return string Attachment body
*/
public function get_attachment_body($id, $task)
{
$this->_parse_id($task);
if ($storage = $this->get_folder($task['list'])) {
return $storage->get_attachment($task['uid'], $id);
}
return false;
}
/**
- * Build a URI representing the given message reference
+ * Build a struct representing the given message reference
*
- * @see tasklist_driver::get_message_uri()
+ * @see tasklist_driver::get_message_reference()
*/
- public function get_message_uri($headers, $folder)
+ public function get_message_reference($uri_or_headers, $folder = null)
{
- $uri = kolab_storage_config::get_message_uri($headers, $folder);
- return kolab_storage_config::local_message_uri($uri);
+ if (is_object($uri_or_headers)) {
+ $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
+ }
+
+ if (is_string($uri_or_headers)) {
+ return kolab_storage_config::get_message_reference($uri_or_headers, 'task');
+ }
+
+ return false;
}
/**
* Find tasks assigned to a specified message
*
* @see tasklist_driver::get_message_related_tasks()
*/
public function get_message_related_tasks($headers, $folder)
{
$config = kolab_storage_config::get_instance();
$result = $config->get_message_relations($headers, $folder, 'task');
foreach ($result as $idx => $rec) {
$result[$idx] = $this->_to_rcube_task($rec, kolab_storage::folder_id($rec['_mailbox']));
}
return $result;
}
/**
*
*/
public function tasklist_edit_form($action, $list, $fieldprop)
{
if ($list['id'] && ($list = $this->lists[$list['id']])) {
$folder_name = $this->get_folder($list['id'])->name; // UTF7
}
else {
$folder_name = '';
}
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder_name)) {
$path_imap = explode($delim, $folder_name);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder_name);
}
else {
$path_imap = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
// folder name (default field)
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'taskedit-tasklistame', 'size' => 20));
$fieldprop['name']['value'] = $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected'])));
// 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('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), $folder_name);
$fieldprop['parent'] = array(
'id' => 'taskedit-parentfolder',
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show($path_imap),
);
}
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
foreach (array('name','parent','showalarms') as $f) {
$form['properties']['fields'][$f] = $fieldprop[$f];
}
// 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 $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('tasklist.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function folder_acl_form()
{
$folder = rcube_utils::get_input_value('_folder', rcube_utils::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'));
}
}
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 791d2ab4..1de03532 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -1,371 +1,372 @@
<?php
/**
* Driver interface for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
/**
* Struct of an internal task object how it is passed from/to the driver classes:
*
* $task = array(
* 'id' => 'Task ID used for editing', // must be unique for the current user
* 'parent_id' => 'ID of parent task', // null if top-level task
* 'uid' => 'Unique identifier of this task',
* 'list' => 'Task list identifier to add the task to or where the task is stored',
* 'changed' => <DateTime>, // Last modification date/time of the record
* 'title' => 'Event title/summary',
* 'description' => 'Event description',
* 'tags' => array(), // List of tags for this task
* 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set
* 'time' => 'Due time', // as string of format hh::ii or null if no due time is set
* 'startdate' => 'Start date' // Delay start of the task until that date
* 'starttime' => 'Start time' // ...and time
* 'categories' => 'Task category',
* 'flagged' => 'Boolean value whether this record is flagged',
* 'complete' => 'Float value representing the completeness state (range 0..1)',
* 'status' => 'Task status string according to (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) RFC 2445',
* 'valarms' => array( // List of reminders (new format), each represented as a hash array:
* array(
* 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object
* 'action' => 'DISPLAY|EMAIL|AUDIO',
* 'duration' => 'PT15M', // ISO 8601 period string
* 'repeat' => 0, // number of repetitions
* 'description' => '', // text to display for DISPLAY actions
* 'summary' => '', // message text for EMAIL actions
* 'attendees' => array(), // list of email addresses to receive alarm messages
* ),
* ),
* 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs
* 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY',
* 'INTERVAL' => 1...n,
* 'UNTIL' => DateTime,
* 'COUNT' => 1..n, // number of times
* 'RDATE' => array(), // complete list of DateTime objects denoting individual repeat dates
* ),
* '_fromlist' => 'List identifier where the task was stored before',
* );
*/
/**
* Driver interface for the Tasklist plugin
*/
abstract class tasklist_driver
{
// features supported by the backend
public $alarms = false;
public $attachments = false;
public $attendees = false;
public $undelete = false; // task undelete action
public $sortable = false;
public $alarm_types = array('DISPLAY');
public $alarm_absolute = true;
public $last_error;
/**
* Get a list of available task lists from this source
*/
abstract function get_lists();
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
abstract function create_list(&$prop);
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
abstract function edit_list(&$prop);
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* @return boolean True on success, Fales on failure
*/
abstract function subscribe_list($prop);
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
abstract function delete_list($prop);
/**
* Search for shared or otherwise not listed tasklists the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of tasklists
*/
abstract function search_lists($query, $source);
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
abstract function count_tasks($lists = null);
/**
* Get all taks records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
abstract function list_tasks($filter, $lists = null);
/**
* Get a list of tags to assign tasks to
*
* @return array List of tags
*/
abstract function get_tags();
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* id: Task identifier
* uid: Unique identifier of this task
* date: Task due date
* time: Task due time
* title: Task title/summary
*/
abstract function pending_alarms($time, $lists = null);
/**
* (User) feedback after showing an alarm notification
* This should mark the alarm as 'shown' or snooze it for the given amount of time
*
* @param string Task identifier
* @param integer Suspend the alarm for this number of seconds
*/
abstract function dismiss_alarm($id, $snooze = 0);
/**
* Remove alarm dismissal or snooze state
*
* @param string Task identifier
*/
abstract public function clear_alarms($id);
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
abstract public function get_task($prop);
/**
* Get decendents of the given task record
*
* @param mixed Hash array with task properties or task UID
* @param boolean True if all childrens children should be fetched
* @return array List of all child task IDs
*/
abstract public function get_childs($prop, $recursive = false);
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of this file)
* @return mixed New event ID on success, False on error
*/
abstract function create_task($prop);
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of this file)
* @return boolean True on success, False on error
*/
abstract function edit_task($prop);
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* id: Task identifier
* list: New list identifier to move to
* _fromlist: Previous list identifier
* @return boolean True on success, False on error
*/
abstract function move_task($prop);
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
abstract function delete_task($prop, $force = true);
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
return false;
}
/**
* Get attachment properties
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function get_attachment($id, $task) { }
/**
* Get attachment body
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return string Attachment body
*/
public function get_attachment_body($id, $task) { }
/**
- * Build a URI representing the given message reference
+ * Build a struct representing the given message reference
*
- * @param object $headers rcube_message_header instance holding the message headers
+ * @param object|string $uri_or_headers rcube_message_header instance holding the message headers
+ * or an URI from a stored link referencing a mail message.
* @param string $folder IMAP folder the message resides in
*
- * @return string An URI referencing the given IMAP message
+ * @return array An struct referencing the given IMAP message
*/
- public function get_message_uri($headers, $folder)
+ public function get_message_reference($uri_or_headers, $folder = null)
{
// to be implemented by the derived classes
return false;
}
/**
* Find tasks assigned to a specified message
*
* @param object $message rcube_message_header instance
* @param string $folder IMAP folder the message resides in
*
* @param array List of linked task objects
*/
public function get_message_related_tasks($headers, $folder)
{
// to be implemented by the derived classes
return array();
}
/**
* Helper method to determine whether the given task is considered "complete"
*
* @param array $task Hash array with event properties:
* @return boolean True if complete, False otherwiese
*/
public function is_complete($task)
{
return ($task['complete'] >= 1.0 && empty($task['status'])) || $task['status'] === 'COMPLETED';
}
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
$rcmail = rcube::get_instance();
return $rcmail->config->get('tasklist_categories', array());
}
/**
* Build the edit/create form for lists.
* This gives the drivers the opportunity to add more list properties
*
* @param string The action called this form
* @param array Tasklist properties
* @param array List with form fields to be rendered
* @return string HTML content of the form
*/
public function tasklist_edit_form($action, $list, $formfields)
{
$html = '';
foreach ($formfields as $field) {
$html .= html::div('form-section',
html::label($field['id'], $field['label']) .
$field['value']);
}
return $html;
}
/**
* Handler for user_delete plugin hook
*
* @param array Hash array with hook arguments
* @return array Return arguments for plugin hooks
*/
public function user_delete($args)
{
// TO BE OVERRIDDEN
return $args;
}
}
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index d65507e7..b2607142 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1,2141 +1,2102 @@
<?php
/**
* Tasks plugin for Roundcube webmail
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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 tasklist extends rcube_plugin
{
const FILTER_MASK_TODAY = 1;
const FILTER_MASK_TOMORROW = 2;
const FILTER_MASK_WEEK = 4;
const FILTER_MASK_LATER = 8;
const FILTER_MASK_NODATE = 16;
const FILTER_MASK_OVERDUE = 32;
const FILTER_MASK_FLAGGED = 64;
const FILTER_MASK_COMPLETE = 128;
const FILTER_MASK_ASSIGNED = 256;
const FILTER_MASK_MYTASKS = 512;
const SESSION_KEY = 'tasklist_temp';
public static $filter_masks = array(
'today' => self::FILTER_MASK_TODAY,
'tomorrow' => self::FILTER_MASK_TOMORROW,
'week' => self::FILTER_MASK_WEEK,
'later' => self::FILTER_MASK_LATER,
'nodate' => self::FILTER_MASK_NODATE,
'overdue' => self::FILTER_MASK_OVERDUE,
'flagged' => self::FILTER_MASK_FLAGGED,
'complete' => self::FILTER_MASK_COMPLETE,
'assigned' => self::FILTER_MASK_ASSIGNED,
'mytasks' => self::FILTER_MASK_MYTASKS,
);
public $task = '?(?!login|logout).*';
public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order');
public $rc;
public $lib;
public $driver;
public $timezone;
public $ui;
public $home; // declare public to be used in other classes
private $collapsed_tasks = array();
private $message_tasks = array();
private $itip;
private $ical;
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->require_plugin('jqueryui');
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('tasks', 'tasklist');
// load plugin configuration
$this->load_config();
$this->timezone = $this->lib->timezone;
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('user_delete', array($this, 'user_delete'));
}
/**
* Startup hook
*/
public function startup($args)
{
// the tasks module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('tasklist_disabled', false) || !$this->rc->config->get('tasklist_enabled', true))
return;
// load localizations
$this->add_texts('localization/', $args['task'] == 'tasks' && (!$args['action'] || $args['action'] == 'print'));
$this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle'))); // add label for task title
if ($args['task'] == 'tasks' && $args['action'] != 'save-pref') {
$this->load_driver();
// register calendar actions
$this->register_action('index', array($this, 'tasklist_view'));
$this->register_action('task', array($this, 'task_action'));
$this->register_action('tasklist', array($this, 'tasklist_action'));
$this->register_action('counts', array($this, 'fetch_counts'));
$this->register_action('fetch', array($this, 'fetch_tasks'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('mailimportitip', array($this, 'mail_import_itip'));
$this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
$this->register_action('itip-status', array($this, 'task_itip_status'));
$this->register_action('itip-remove', array($this, 'task_itip_remove'));
$this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
$this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
$this->add_hook('refresh', array($this, 'refresh'));
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
}
else if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
if ($this->rc->config->get('tasklist_mail_embed', true)) {
$this->add_hook('message_load', array($this, 'mail_message_load'));
}
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'tasklist-create-from-mail',
'label' => 'tasklist.createfrommail',
'type' => 'link',
'classact' => 'icon taskaddlink active',
'class' => 'icon taskaddlink',
'innerclass' => 'icon taskadd',
))),
'messagemenu');
$this->api->output->add_label('tasklist.createfrommail');
}
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->load_ui();
$this->ui->init();
}
// add hooks for alarms handling
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
*
*/
private function load_ui()
{
if (!$this->ui) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
}
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('tasklist_driver', 'database');
$driver_class = 'tasklist_' . $driver_name . '_driver';
require_once($this->home . '/drivers/tasklist_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('libkolab');
default:
$this->driver = new $driver_class($this);
break;
}
$this->rc->output->set_env('tasklist_driver', $driver_name);
}
/**
* Dispatcher for task-related actions initiated by the client
*/
public function task_action()
{
$filter = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
$oldrec = $rec;
$success = $refresh = false;
// force notify if hidden + active
$itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
if ($itip_send_option === 1 && empty($rec['_reportpartstat']))
$rec['_notify'] = 1;
switch ($action) {
case 'new':
$oldrec = null;
$rec = $this->prepare_task($rec);
$rec['uid'] = $this->generate_uid();
$temp_id = $rec['tempid'];
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
$this->cleanup_task($rec);
}
break;
case 'complete':
$complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST));
if (!($rec = $this->driver->get_task($rec))) {
break;
}
$oldrec = $rec;
$rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION');
// sent itip notifications if enabled (no user interaction here)
if (($itip_send_option & 1)) {
if ($this->is_attendee($rec)) {
$rec['_reportpartstat'] = $rec['status'];
}
else if ($this->is_organizer($rec)) {
$rec['_notify'] = 1;
}
}
case 'edit':
$oldrec = $this->driver->get_task($rec);
$rec = $this->prepare_task($rec);
$clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
if ($success = $this->driver->edit_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
$this->cleanup_task($rec);
// add clone from recurring task
if ($clone && $this->driver->create_task($clone)) {
$refresh[] = $this->driver->get_task($clone);
$this->driver->clear_alarms($rec['id']);
}
// move all childs if list assignment was changed
if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) {
foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']);
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
$refresh[] = $r;
}
}
}
}
}
break;
case 'move':
foreach ((array)$rec['id'] as $id) {
$r = $rec;
$r['id'] = $id;
if ($this->driver->move_task($r)) {
$new_task = $this->driver->get_task($r);
$new_task['tempid'] = $id;
$refresh[] = $new_task;
$success = true;
// move all childs, too
foreach ($this->driver->get_childs(array('id' => $id, 'list' => $rec['_fromlist']), true) as $cid) {
$child = $rec;
$child['id'] = $cid;
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
$r['tempid'] = $cid;
$refresh[] = $r;
}
}
}
}
}
break;
case 'delete':
$mode = intval(rcube_utils::get_input_value('mode', rcube_utils::INPUT_POST));
$oldrec = $this->driver->get_task($rec);
if ($success = $this->driver->delete_task($rec, false)) {
// delete/modify all childs
foreach ($this->driver->get_childs($rec, $mode) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list']);
if ($mode == 1) { // delete all childs
if ($this->driver->delete_task($child, false)) {
if ($this->driver->undelete)
$_SESSION['tasklist_undelete'][$rec['id']][] = $cid;
}
else
$success = false;
}
else {
$child['parent_id'] = strval($oldrec['parent_id']);
$this->driver->edit_task($child);
}
}
// update parent task to adjust list of children
if (!empty($oldrec['parent_id'])) {
$refresh[] = $this->driver->get_task(array('id' => $oldrec['parent_id'], 'list' => $rec['list']));
}
}
if (!$success)
$this->rc->output->command('plugin.reload_data');
break;
case 'undelete':
if ($success = $this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) {
if ($this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
}
}
}
break;
case 'collapse':
foreach (explode(',', $rec['id']) as $rec_id) {
if (intval(rcube_utils::get_input_value('collapsed', rcube_utils::INPUT_GPC))) {
$this->collapsed_tasks[] = $rec_id;
}
else {
$i = array_search($rec_id, $this->collapsed_tasks);
if ($i !== false)
unset($this->collapsed_tasks[$i]);
}
}
$this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));
return; // avoid further actions
case 'rsvp':
$status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
$noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action';
$task = $this->driver->get_task($rec);
$task['attendees'] = $rec['attendees'];
$task['_type'] = 'task';
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $rec['to']) {
$itip = $this->load_itip();
if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) {
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
$refresh[] = $task;
$noreply = false;
}
}
$rec = $task;
if ($success = $this->driver->edit_task($rec)) {
if (!$noreply) {
// let the reply clause further down send the iTip message
$rec['_reportpartstat'] = $status;
}
}
break;
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
else {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
// send out notifications
if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) {
// make sure we have the complete record
$task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);
// only notify if data really changed (TODO: do diff check on client already)
if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) {
$sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
if ($sent > 0)
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
else if ($sent < 0)
$this->rc->output->show_message('tasklist.errornotifying', 'error');
}
}
else if ($success && $rec['_reportpartstat'] && $rec['_reportpartstat'] != 'NEEDS-ACTION') {
// get the full record after update
$task = $this->driver->get_task($rec);
// send iTip REPLY with the updated partstat
if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
$sender = $task['attendees'][$idx];
$status = strtolower($sender['status']);
if (!empty($_POST['comment']))
$task['comment'] = rcube_utils::get_input_value('comment', rcube_utils::INPUT_POST);
$itip = $this->load_itip();
$itip->set_sender_email($sender['email']);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
if ($refresh['id']) {
$this->encode_task($refresh);
}
else if (is_array($refresh)) {
foreach ($refresh as $i => $r)
$this->encode_task($refresh[$i]);
}
$this->rc->output->command('plugin.update_task', $refresh);
}
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined','delegated'));
$this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
}
return $this->itip;
}
/**
* repares new/edited task properties before save
*/
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
if ($rec['raw']) {
foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
$locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
$normwords[] = $word;
$datewords[] = $word;
}
foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
$locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
$normwords[] = $month;
$datewords[] = $month;
}
foreach (array('on','this','next','at') as $word) {
$fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
$fillwords[] = $word;
}
$raw = trim($rec['raw']);
$date_str = '';
// translate localized keywords
$raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
$raw = preg_replace($locwords, $normwords, $raw);
// find date pattern
$date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
if (preg_match($date_pattern, $raw, $m)) {
$date_str .= $m[1] . $m[2] . $m[3];
$raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
// add year to date string
if ($m[1] && !$m[3])
$date_str .= date('Y');
}
// find time pattern
$time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
if (preg_match($time_pattern, $raw, $m)) {
$has_time = true;
$date_str .= ($date_str ? ' ' : 'today ') . $m[1];
$raw = preg_replace($time_pattern, '', $raw);
}
// yes, raw input matched a (valid) date
if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
$rec['date'] = $date->format('Y-m-d');
if ($has_time)
$rec['time'] = $date->format('H:i');
$rec['title'] = $raw;
}
else
$rec['title'] = $rec['raw'];
}
// normalize input from client
if (isset($rec['complete'])) {
$rec['complete'] = floatval($rec['complete']);
if ($rec['complete'] > 1)
$rec['complete'] /= 100;
}
if (isset($rec['flagged']))
$rec['flagged'] = intval($rec['flagged']);
// fix for garbage input
if ($rec['description'] == 'null')
$rec['description'] = '';
foreach ($rec as $key => $val) {
if ($val === 'null')
$rec[$key] = null;
}
if (!empty($rec['date'])) {
$this->normalize_dates($rec, 'date', 'time');
}
if (!empty($rec['startdate'])) {
$this->normalize_dates($rec, 'startdate', 'starttime');
}
// convert tags to array, filter out empty entries
if (isset($rec['tags']) && !is_array($rec['tags'])) {
$rec['tags'] = array_filter((array)$rec['tags']);
}
// convert the submitted alarm values
if ($rec['valarms']) {
$valarms = array();
foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) {
// alarms can only work with a date (either task start, due or absolute alarm date)
if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate'])
$valarms[] = $alarm;
}
$rec['valarms'] = $valarms;
}
// convert the submitted recurrence settings
if (is_array($rec['recurrence'])) {
$refdate = null;
if (!empty($rec['date'])) {
$refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
}
else if (!empty($rec['startdate'])) {
$refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
}
if ($refdate) {
$rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate);
// translate count into an absolute end date.
// why? because when shifting completed tasks to the next recurrence,
// the initial start date to count from gets lost.
if ($rec['recurrence']['COUNT']) {
$engine = libcalendaring::get_recurrence();
$engine->init($rec['recurrence'], $refdate);
if ($until = $engine->end()) {
$rec['recurrence']['UNTIL'] = $until;
unset($rec['recurrence']['COUNT']);
}
}
}
else { // recurrence requires a reference date
$rec['recurrence'] = '';
}
}
$attachments = array();
$taskid = $rec['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
unset($attachments[$id]['abort'], $attachments[$id]['group']);
}
}
}
}
$rec['attachments'] = $attachments;
// convert link references into simple URIs
if (array_key_exists('links', $rec)) {
$rec['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$rec['links']);
}
// convert invalid data
if (isset($rec['attendees']) && !is_array($rec['attendees']))
$rec['attendees'] = array();
// copy the task status to my attendee partstat
if (!empty($rec['_reportpartstat'])) {
if (($idx = $this->is_attendee($rec)) !== false) {
if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
$rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
else
unset($rec['_reportpartstat']);
}
}
// set organizer from identity selector
if ((isset($rec['_identity']) || (!empty($rec['attendees']) && empty($rec['organizer']))) &&
($identity = $this->rc->user->get_identity($rec['_identity']))) {
$rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
}
if (is_numeric($rec['id']) && $rec['id'] < 0)
unset($rec['id']);
return $rec;
}
/**
* Utility method to convert a tasks date/time values into a normalized format
*/
private function normalize_dates(&$rec, $date_key, $time_key)
{
try {
// parse date from user format (#2801)
$date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d');
$date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone);
// fall back to default strtotime logic
if (empty($date)) {
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
}
$rec[$date_key] = $date->format('Y-m-d');
if (!empty($rec[$time_key]))
$rec[$time_key] = $date->format('H:i');
return true;
}
catch (Exception $e) {
$rec[$date_key] = $rec[$time_key] = null;
}
return false;
}
/**
* Releases some resources after successful save
*/
private function cleanup_task(&$rec)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* When flagging a recurring task as complete,
* clone it and shift dates to the next occurrence
*/
private function handle_recurrence(&$rec, $old)
{
$clone = null;
if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) {
$engine = libcalendaring::get_recurrence();
$rrule = $rec['recurrence'];
$updates = array();
// compute the next occurrence of date attributes
foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) {
if (empty($rec[$date_key]))
continue;
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
$engine->init($rrule, $date);
if ($next = $engine->next()) {
$updates[$date_key] = $next->format('Y-m-d');
if (!empty($rec[$time_key]))
$updates[$time_key] = $next->format('H:i');
}
}
// shift absolute alarm dates
if (!empty($updates) && is_array($rec['valarms'])) {
$updates['valarms'] = array();
unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited
foreach ($rec['valarms'] as $i => $alarm) {
if ($alarm['trigger'] instanceof DateTime) {
$engine->init($rrule, $alarm['trigger']);
if ($next = $engine->next()) {
$alarm['trigger'] = $next;
}
}
$updates['valarms'][$i] = $alarm;
}
}
if (!empty($updates)) {
// clone task to save a completed copy
$clone = $rec;
$clone['uid'] = $this->generate_uid();
$clone['parent_id'] = $rec['id'];
unset($clone['id'], $clone['recurrence'], $clone['attachments']);
// update the task but unset completed flag
$rec = array_merge($rec, $updates);
$rec['complete'] = $old['complete'];
$rec['status'] = $old['status'];
}
}
return $clone;
}
/**
* Send out an invitation/notification to all task attendees
*/
private function notify_attendees($task, $old, $action = 'edit', $comment = null)
{
if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) {
$task['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->lib->get_user_emails();
$itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3);
// add comment to the iTip attachment
$task['comment'] = $comment;
// needed to generate VTODO instead of VEVENT entry
$task['_type'] = 'task';
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'delete' ? 'CANCEL' : 'REQUEST';
$object = $this->to_libcal($task);
$message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']);
// list existing attendees from the $old task
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0; $current = array();
foreach ((array)$task['attendees'] as $attendee) {
$current[] = strtolower($attendee['email']);
// skip myself for obvious reasons
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) {
continue;
}
// skip if notification is disabled for this attendee
if ($attendee['noreply'] && $itip_notify & 2) {
continue;
}
// skip if this attendee has delegated and set RSVP=FALSE
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) {
continue;
}
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$is_rsvp = $is_new || $task['sequence'] > $old['sequence'];
$bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody');
$subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty'));
// finally send the message
if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
$sent++;
else
$sent = -100;
}
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
continue;
}
$vtodo = $this->to_libcal($old);
$vtodo['cancelled'] = $is_cancelled;
$vtodo['attendees'] = array($attendee);
$vtodo['comment'] = $comment;
if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Compare two task objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing task properties
*/
public static function task_diff($a, $b)
{
$diff = array();
$ignore = array('changed' => 1, 'attachments' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
}
// only compare number of attachments
if (count($a['attachments']) != count($b['attachments']))
$diff[] = 'attachments';
return $diff;
}
/**
* Dispatcher for tasklist actions initiated by the client
*/
public function tasklist_action()
{
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true);
$success = false;
if (isset($list['showalarms']))
$list['showalarms'] = intval($list['showalarms']);
switch ($action) {
case 'form-new':
case 'form-edit':
echo $this->ui->tasklist_editform($action, $list);
exit;
case 'new':
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
if (!$list['_reload']) {
$this->load_ui();
$list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
$list += (array)$jsenv[$insert_id];
}
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
break;
case 'edit':
if ($newid = $this->driver->edit_list($list)) {
$list['oldid'] = $list['id'];
$list['id'] = $newid;
$this->rc->output->command('plugin.update_tasklist', $list);
$success = true;
}
break;
case 'subscribe':
$success = $this->driver->subscribe_list($list);
break;
case 'delete':
if (($success = $this->driver->delete_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
case 'search':
$this->load_ui();
$results = array();
$query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
foreach ((array)$this->driver->search_lists($query, $source) as $id => $prop) {
$editname = $prop['editname'];
unset($prop['editname']); // force full name to be displayed
$prop['active'] = false;
// let the UI generate HTML and CSS representation for this calendar
$html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
$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_utils::INPUT_GPC));
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
$this->rc->output->command('plugin.unlock_saving');
}
/**
* Get counts for active tasks divided into different selectors
*/
public function fetch_counts()
{
if (isset($_REQUEST['lists'])) {
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
}
else {
foreach ($this->driver->get_lists() as $list) {
if ($list['active'])
$lists[] = $list['id'];
}
}
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
/**
* Adjust the cached counts after changing a task
*/
public function update_counts($oldrec, $newrec)
{
// rebuild counts until this function is finally implemented
$this->fetch_counts();
// $this->rc->output->command('plugin.update_counts', $counts);
}
/**
*
*/
public function fetch_tasks()
{
$f = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
$filter = array('mask' => $f, 'search' => $search);
/*
// convert magic date filters into a real date range
switch ($f) {
case self::FILTER_MASK_TODAY:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $filter['to'] = $today->format('Y-m-d');
break;
case self::FILTER_MASK_TOMORROW:
$tomorrow = new DateTime('now + 1 day', $this->timezone);
$filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
break;
case self::FILTER_MASK_OVERDUE:
$yesterday = new DateTime('yesterday', $this->timezone);
$filter['to'] = $yesterday->format('Y-m-d');
break;
case self::FILTER_MASK_WEEK:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $today->format('Y-m-d');
$weekend = new DateTime('now + 7 days', $this->timezone);
$filter['to'] = $weekend->format('Y-m-d');
break;
case self::FILTER_MASK_LATER:
$date = new DateTime('now + 8 days', $this->timezone);
$filter['from'] = $date->format('Y-m-d');
break;
}
*/
$data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f);
$this->rc->output->command('plugin.data_ready', array(
'filter' => $f,
'lists' => $lists,
'search' => $search,
'data' => $data,
'tags' => $this->driver->get_tags(),
));
}
/**
* Prepare and sort the given task records to be sent to the client
*/
private function tasks_data($records, $f)
{
$data = $this->task_tree = $this->task_titles = array();
foreach ($records as $rec) {
if ($rec['parent_id']) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
$this->encode_task($rec);
// apply filter; don't trust the driver on this :-)
if ((!$f && !$this->driver->is_complete($rec)) || ($rec['mask'] & $f))
$data[] = $rec;
}
// assign hierarchy level indicators for later sorting
array_walk($data, array($this, 'task_walk_tree'));
return $data;
}
/**
* Prepare the given task record before sending it to the client
*/
private function encode_task(&$rec)
{
$rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']);
if (is_object($rec['created'])) {
$rec['created_'] = $this->rc->format_date($rec['created']);
$rec['created'] = $rec['created']->format('U');
}
if (is_object($rec['changed'])) {
$rec['changed_'] = $this->rc->format_date($rec['changed']);
$rec['changed'] = $rec['changed']->format('U');
}
else {
$rec['changed'] = null;
}
if ($rec['date']) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['datetime'] = intval($date->format('U'));
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
$rec['_hasdate'] = 1;
}
catch (Exception $e) {
$rec['date'] = $rec['datetime'] = null;
}
}
else {
$rec['date'] = $rec['datetime'] = null;
$rec['_hasdate'] = 0;
}
if ($rec['startdate']) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdatetime'] = intval($date->format('U'));
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
}
catch (Exception $e) {
$rec['startdate'] = $rec['startdatetime'] = null;
}
}
if ($rec['valarms']) {
$rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']);
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
}
if ($rec['recurrence']) {
$rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
}
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
// convert link URIs references into structs
if (array_key_exists('links', $rec)) {
foreach ((array)$rec['links'] as $i => $link) {
- if (strpos($link, 'imap://') === 0) {
- $rec['links'][$i] = $this->get_message_reference($link);
+ if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) {
+ $rec['links'][$i] = $msgref;
}
}
}
if (!is_array($rec['tags']))
$rec['tags'] = (array)$rec['tags'];
sort($rec['tags'], SORT_LOCALE_STRING);
if (in_array($rec['id'], $this->collapsed_tasks))
$rec['collapsed'] = true;
if (empty($rec['parent_id']))
$rec['parent_id'] = null;
$this->task_titles[$rec['id']] = $rec['title'];
}
/**
* Callback function for array_walk over all tasks.
* Sets tree depth and parent titles
*/
private function task_walk_tree(&$rec)
{
$rec['_depth'] = 0;
$parent_id = $this->task_tree[$rec['id']];
while ($parent_id) {
$rec['_depth']++;
$rec['parent_title'] = $this->task_titles[$parent_id];
$parent_id = $this->task_tree[$parent_id];
}
}
/**
* Compute the filter mask of the given task
*
* @param array Hash array with Task record properties
* @return int Filter mask
*/
public function filter_mask($rec)
{
static $today, $tomorrow, $weeklimit;
if (!$today) {
$today_date = new DateTime('now', $this->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$week_date = new DateTime('now + 7 days', $this->timezone);
$weeklimit = $week_date->format('Y-m-d');
}
$mask = 0;
$start = $rec['startdate'] ?: '1900-00-00';
$duedate = $rec['date'] ?: '3000-00-00';
if ($rec['flagged'])
$mask |= self::FILTER_MASK_FLAGGED;
if ($this->driver->is_complete($rec))
$mask |= self::FILTER_MASK_COMPLETE;
if (empty($rec['date']))
$mask |= self::FILTER_MASK_NODATE;
else if ($rec['date'] < $today)
$mask |= self::FILTER_MASK_OVERDUE;
if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
$mask |= self::FILTER_MASK_TODAY;
if ($duedate <= $tomorrow || ($rec['startdate'] && $start <= $tomorrow))
$mask |= self::FILTER_MASK_TOMORROW;
if (($start > $tomorrow && $start <= $weeklimit) || ($duedate > $tomorrow && $duedate <= $weeklimit))
$mask |= self::FILTER_MASK_WEEK;
else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
$mask |= self::FILTER_MASK_LATER;
// add masks for assigned tasks
if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false)
$mask |= self::FILTER_MASK_ASSIGNED;
else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false)
$mask |= self::FILTER_MASK_MYTASKS;
return $mask;
}
/**
* Determine whether the current user is an attendee of the given task
*/
public function is_attendee($task)
{
$emails = $this->lib->get_user_emails();
foreach ((array)$task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
return $i;
}
}
return false;
}
/**
* Determine whether the current user is the organizer of the given task
*/
public function is_organizer($task)
{
$emails = $this->lib->get_user_emails();
return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function tasklist_view()
{
$this->ui->init();
$this->ui->init_templates();
// set autocompletion env
$this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
$this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['tasklist.'.$label] = $this->gettext($label);
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
// collect env variables
$env = array(
'tasklists' => array(),
'tasklist_settings' => $this->ui->load_settings(),
);
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
$script_add = '';
foreach ($this->ui->get_gui_objects() as $obj => $id) {
$script_add .= rcmail_output::JS_OBJECT_NAME . ".gui_object('$obj', '$id');\n";
}
echo html::tag('script', array('type' => 'text/javascript'),
rcmail_output::JS_OBJECT_NAME . ".set_env(" . json_encode($env) . ");\n".
rcmail_output::JS_OBJECT_NAME . ".add_label(" . json_encode($texts) . ");\n".
$script_add
);
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active lists and sync them to the client
*/
public function refresh($attr)
{
// refresh the entire list every 10th time to also sync deleted items
if (rand(0,10) == 10) {
$this->rc->output->command('plugin.reload_data');
return;
}
$filter = array(
'since' => $attr['last'],
'search' => rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
'mask' => intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)) & self::FILTER_MASK_COMPLETE,
);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);;
$updates = $this->driver->list_tasks($filter, $lists);
if (!empty($updates)) {
$this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates, 255), true);
// update counts
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
}
/**
* Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
* This will check for pending notifications and pass them to the client
*/
public function pending_alarms($p)
{
$this->load_driver();
if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
foreach ($alarms as $alarm) {
// encode alarm object to suit the expectations of the calendaring code
if ($alarm['date'])
$alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone);
$alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task:
$alarm['allday'] = empty($alarm['time']) ? 1 : 0;
$p['alarms'][] = $alarm;
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by the calendar module
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'task:') === 0)
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']);
}
return $p;
}
/******* Attachment handling *******/
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
$this->lib->attachment_upload(self::SESSION_KEY);
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
// show loading page
if (!empty($_GET['_preload'])) {
return $this->lib->attachment_loading_page();
}
$task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$task = array('id' => $task, 'list' => $list);
$attachment = $this->driver->get_attachment($id, $task);
// show part page
if (!empty($_GET['_frame'])) {
$this->lib->attachment = $attachment;
$this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
$this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
$this->rc->output->send('tasklist.attachment');
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
$this->lib->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/******* Email related function *******/
public function mail_message2task()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$task = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$task['title'] = trim($message->subject);
$task['description'] = trim($message->first_text_part());
$task['id'] = -$uid;
$this->load_driver();
// add a reference to the email message
- if ($msguri = $this->driver->get_message_uri($message->headers, $mbox)) {
- $task['links'] = array($this->get_message_reference($msguri));
+ if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
+ $task['links'] = array($msgref);
}
// copy mail attachments to task
else if ($message->attachments && $this->driver->attachments) {
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $task['id'];
$_SESSION[self::SESSION_KEY]['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $task['id'],
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
$attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
$task['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2taskdialog', $task);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/**
* Add UI element to copy task invitations or updates to the tasklist
*/
public function mail_messagebody_html($p)
{
// load iCalendar functions (if necessary)
if (!empty($this->lib->ical_parts)) {
$this->get_ical();
$this->load_itip();
}
$html = '';
$has_tasks = false;
$ical_objects = $this->lib->get_mail_ical_objects();
// show a box for every task in the file
foreach ($ical_objects as $idx => $task) {
if ($task['_type'] != 'task') {
continue;
}
$has_tasks = true;
// get prepared inline UI for this event object
if ($ical_objects->method) {
$html .= html::div('tasklist-invitebox',
$this->itip->mail_itip_inline_ui(
$task,
$ical_objects->method,
$ical_objects->mime_id . ':' . $idx,
'tasks',
rcube_utils::anytodatetime($ical_objects->message_date)
)
);
}
// limit listing
if ($idx >= 3) {
break;
}
}
// list linked tasks
$links = array();
foreach ($this->message_tasks as $task) {
$checkbox = new html_checkbox(array(
'name' => 'completed',
'class' => 'complete',
'title' => $this->gettext('complete'),
'data-list' => $task['list'],
));
$complete = $this->driver->is_complete($task);
$links[] = html::tag('li', 'messagetaskref' . ($complete ? ' complete' : ''),
$checkbox->show($complete ? $task['id'] : null, array('value' => $task['id'])) . ' ' .
html::a(array(
'href' => $this->rc->url(array(
'task' => 'tasks',
'list' => $task['list'],
'id' => $task['id'],
)),
'class' => 'messagetasklink',
'rel' => $task['id'] . '@' . $task['list'],
'target' => '_blank',
), Q($task['title']))
);
}
if (count($links)) {
$html .= html::div('messagetasklinks', html::tag('ul', 'tasklist', join("\n", $links)));
}
// prepend iTip/relation boxes to message body
if ($html) {
$this->load_ui();
$this->ui->init();
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
}
// add "Save to tasks" button into attachment menu
if ($has_tasks) {
$this->add_button(array(
'id' => 'attachmentsavetask',
'name' => 'attachmentsavetask',
'type' => 'link',
'wrapper' => 'li',
'command' => 'attachment-save-task',
'class' => 'icon tasklistlink',
'classact' => 'icon tasklistlink active',
'innerclass' => 'icon taskadd',
'label' => 'tasklist.savetotasklist',
), 'attachmentmenu');
}
return $p;
}
/**
* Lookup backend storage and find notes associated with the given message
*/
public function mail_message_load($p)
{
if (!$p['object']->headers->others['x-kolab-type']) {
$this->load_driver();
$this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder);
// sort message tasks by completeness and due date
$driver = $this->driver;
array_walk($this->message_tasks, array($this, 'encode_task'));
usort($this->message_tasks, function($a, $b) use ($driver) {
$a_complete = intval($driver->is_complete($a));
$b_complete = intval($driver->is_complete($b));
$d = $a_complete - $b_complete;
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
});
}
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
$this->ical = libcalendaring::get_ical();
}
return $this->ical;
}
/**
* Get properties of the tasklist this user has specified as default
*/
public function get_default_tasklist($writeable = false, $confidential = false)
{
$lists = $this->driver->get_lists();
$list = null;
if (!$list || ($writeable && !$list['editable'])) {
foreach ($lists as $l) {
if ($confidential && $l['subtype'] == 'confidential') {
$list = $l;
break;
}
if ($l['default']) {
$list = $l;
if (!$confidential)
break;
}
if (!$writeable || $l['editable']) {
$first = $l;
}
}
}
return $list ?: $first;
}
- /**
- * Resolve the email message reference from the given URI
- */
- public function get_message_reference($uri)
- {
- if (strpos($uri, 'imap:///') === 0) {
- $url = parse_url(substr($uri, 8));
- parse_str($url['query'], $params);
-
- $path = explode('/', $url['path']);
- $uid = array_pop($path);
- $folder = join('/', array_map('rawurldecode', $path));
- }
-
- if ($folder && $uid) {
- // TODO: check if folder/uid still references an existing message
- // TODO: validate message or resovle the new URI using the message-id parameter
-
- $linkref = array(
- 'folder' => $folder,
- 'uid' => $uid,
- 'subject' => $params['subject'],
- 'uri' => $uri,
- 'mailurl' => $this->rc->url(array(
- 'task' => 'mail',
- 'action' => 'show',
- 'mbox' => $folder,
- 'uid' => $uid,
- 'rel' => 'task',
- ))
- );
- }
- else {
- $linkref = array();
- }
-
- return $linkref;
- }
-
/**
* Import the full payload from a mail message attachment
*/
public function mail_import_attachment()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$charset = RCMAIL_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
if ($uid && $mime_id) {
$part = $imap->get_message_part($uid, $mime_id);
// $headers = $imap->get_message_headers($uid);
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$tasks = $this->get_ical()->import($part, $charset);
}
}
$success = $existing = 0;
if (!empty($tasks)) {
// find writeable tasklist to store task
$cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
$lists = $this->driver->get_lists();
foreach ($tasks as $task) {
// save to tasklist
$list = $lists[$cal_id] ?: $this->get_default_tasklist(true, $task['sensitivity'] == 'confidential');
if ($list && $list['editable'] && $task['_type'] == 'task') {
$task = $this->from_ical($task);
$task['list'] = $list['id'];
if (!$this->driver->get_task($task['uid'])) {
$success += (bool) $this->driver->create_task($task);
}
else {
$existing++;
}
}
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array(
'name' => 'importsuccess',
'vars' => array('nr' => $success),
)), 'confirmation');
}
else if ($existing) {
$this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
}
else {
$this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error');
}
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_itip()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action';
$error_msg = $this->gettext('errorimportingtask');
$success = false;
$delegate = null;
if ($status == 'delegated') {
$delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
$delegate = reset($delegates);
if (empty($delegate) || empty($delegate['mailto'])) {
$this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
return;
}
}
// successfully parsed tasks?
if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) {
$task = $this->from_ical($task);
// forward iTip request to delegatee
if ($delegate) {
$rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST));
$itip = $this->load_itip();
if ($itip->delegate_to($task, $delegate, $rsvpme ? true : false)) {
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
}
else {
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
// find writeable list to store the task
$list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
$lists = $this->driver->get_lists();
$list = $lists[$list_id];
$dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST');
// select default list except user explicitly selected 'none'
if (!$list && !$dontsave) {
$list = $this->get_default_tasklist(true, $task['sensitivity'] == 'confidential');
}
$metadata = array(
'uid' => $task['uid'],
'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0,
'sequence' => intval($task['sequence']),
'fallback' => strtoupper($status),
'method' => $task['_method'],
'task' => 'tasks',
);
// update my attendee status according to submitted method
if (!empty($status)) {
$organizer = $task['organizer'];
$emails = $this->lib->get_user_emails();
foreach ($task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
$reply_sender = $attendee['email'];
$task['attendees'][$i]['status'] = strtoupper($status);
if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) {
$task['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
}
}
}
// add attendee with this user's default identity if not listed
if (!$reply_sender) {
$sender_identity = $this->rc->user->list_emails(true);
$task['attendees'][] = array(
'name' => $sender_identity['name'],
'email' => $sender_identity['email'],
'role' => 'OPT-PARTICIPANT',
'status' => strtoupper($status),
);
$metadata['attendee'] = $sender_identity['email'];
}
}
// save to tasklist
if ($list && $list['editable']) {
$task['list'] = $list['id'];
// check for existing task with the same UID
$existing = $this->driver->get_task($task['uid']);
if ($existing) {
// only update attendee status
if ($task['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
$existing_attendee = -1;
$existing_attendee_emails = array();
foreach ($existing['attendees'] as $i => $attendee) {
$existing_attendee_emails[] = $attendee['email'];
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
$existing_attendee = $i;
}
}
$task_attendee = null;
foreach ($task['attendees'] as $attendee) {
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
$task_attendee = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee['status'] != 'DELEGATED') {
break;
}
}
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) &&
(stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) &&
(!in_array($attendee['email'], $existing_attendee_emails))) {
$existing['attendees'][] = $attendee;
}
}
// if delegatee has declined, set delegator's RSVP=True
if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) {
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] == $task_attendee['delegated-from']) {
$existing['attendees'][$i]['rsvp'] = true;
break;
}
}
}
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $task_attendee) {
$existing['attendees'][$existing_attendee] = $task_attendee;
$success = $this->driver->edit_task($existing);
}
// update the entire attendees block
else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) {
$existing['attendees'][] = $task_attendee;
$success = $this->driver->edit_task($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// delete the task when declined
else if ($status == 'declined' && $delete) {
$deleted = $this->driver->delete_task($existing, true);
$success = true;
}
// import the (newer) task
else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) {
$task['id'] = $existing['id'];
$task['list'] = $existing['list'];
// preserve my participant status for regular updates
if (empty($status)) {
$emails = $this->lib->get_user_emails();
foreach ($task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
foreach ($existing['attendees'] as $j => $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$task['attendees'][$i] = $existing['attendees'][$j];
break;
}
}
}
}
}
// set status=CANCELLED on CANCEL messages
if ($task['_method'] == 'CANCEL') {
$task['status'] = 'CANCELLED';
}
// show me as free when declined (#1670)
if ($status == 'declined' || $task['status'] == 'CANCELLED') {
$task['free_busy'] = 'free';
}
$success = $this->driver->edit_task($task);
}
else if (!empty($status)) {
$existing['attendees'] = $task['attendees'];
if ($status == 'declined') { // show me as free when declined (#1670)
$existing['free_busy'] = 'free';
}
$success = $this->driver->edit_event($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) {
$success = $this->driver->create_task($task);
}
else if ($status == 'declined') {
$error_msg = null;
}
}
else if ($status == 'declined' || $dontsave) {
$error_msg = null;
}
else {
$error_msg = $this->gettext('nowritetasklistfound');
}
}
if ($success || $dontsave) {
if ($success) {
$message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
}
$metadata['rsvp'] = intval($metadata['rsvp']);
$metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
}
else if ($error_msg) {
$this->rc->output->command('display_message', $error_msg, 'error');
}
// send iTip reply
if ($task['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
$task['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**** Task invitation plugin hooks ****/
/**
* Handler for task/itip-delegate requests
*/
function mail_itip_delegate()
{
// forward request to mail_import_itip() with the right status
$_POST['_status'] = $_REQUEST['_status'] = 'delegated';
$this->mail_import_itip();
}
/**
* Handler for task/itip-status requests
*/
public function task_itip_status()
{
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
// find local copy of the referenced task
$existing = $this->driver->get_task($data);
$itip = $this->load_itip();
$response = $itip->get_itip_status($data, $existing);
// get a list of writeable lists to save new tasks to
if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
$lists = $this->driver->get_lists();
$select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true));
$select->add('--', '');
foreach ($lists as $list) {
if ($list['editable']) {
$select->add($list['name'], $list['id']);
}
}
}
if ($select) {
$default_list = $this->get_default_tasklist(true, $data['sensitivity'] == 'confidential');
$response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' .
$select->show($default_list['id']));
}
$this->rc->output->command('plugin.update_itip_object_status', $response);
}
/**
* Handler for task/itip-remove requests
*/
public function task_itip_remove()
{
$success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
// search for event if only UID is given
if ($task = $this->driver->get_task($uid)) {
$success = $this->driver->delete_task($task, true);
}
if ($success) {
$this->rc->output->show_message('tasklist.successremoval', 'confirmation');
}
else {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
}
/******* Utility functions *******/
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
/**
* Map task properties for ical exprort using libcalendaring
*/
public function to_libcal($task)
{
$object = $task;
$object['_type'] = 'task';
$object['categories'] = (array)$task['tags'];
// convert to datetime objects
if (!empty($task['date'])) {
$object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone);
if (empty($task['time']))
$object['due']->_dateonly = true;
unset($object['date']);
}
if (!empty($task['startdate'])) {
$object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone);
if (empty($task['starttime']))
$object['start']->_dateonly = true;
unset($object['startdate']);
}
$object['complete'] = $task['complete'] * 100;
if ($task['complete'] == 1.0 && empty($task['complete'])) {
$object['status'] = 'COMPLETED';
}
if ($task['flagged']) {
$object['priority'] = 1;
}
else if (!$task['priority']) {
$object['priority'] = 0;
}
return $object;
}
/**
* Convert task properties from ical parser to the internal format
*/
public function from_ical($vtodo)
{
$task = $vtodo;
$task['tags'] = array_filter((array)$vtodo['categories']);
$task['flagged'] = $vtodo['priority'] == 1;
$task['complete'] = floatval($vtodo['complete'] / 100);
// convert from DateTime to internal date format
if (is_a($vtodo['due'], 'DateTime')) {
$due = $this->lib->adjust_timezone($vtodo['due']);
$task['date'] = $due->format('Y-m-d');
if (!$vtodo['due']->_dateonly)
$task['time'] = $due->format('H:i');
}
// convert from DateTime to internal date format
if (is_a($vtodo['start'], 'DateTime')) {
$start = $this->lib->adjust_timezone($vtodo['start']);
$task['startdate'] = $start->format('Y-m-d');
if (!$vtodo['start']->_dateonly)
$task['starttime'] = $start->format('H:i');
}
if (is_a($vtodo['dtstamp'], 'DateTime')) {
$task['changed'] = $vtodo['dtstamp'];
}
unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']);
return $task;
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
$this->load_driver();
return $this->driver->user_delete($args);
}
/**
* Magic getter for public access to protected members
*/
public function __get($name)
{
switch ($name) {
case 'ical':
return $this->get_ical();
case 'itip':
return $this->load_itip();
case 'driver':
$this->load_driver();
return $this->driver;
}
return null;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Feb 6, 12:28 AM (2 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
428009
Default Alt Text
(218 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment