diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 4fccf7e5..4a192c66 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -38,6 +38,7 @@ class tasklist_kolab_driver extends tasklist_driver private $folders = array(); private $tasks = array(); private $tags = array(); + private $bonnie_api = false; /** @@ -55,6 +56,11 @@ class tasklist_kolab_driver extends tasklist_driver // tasklist use fully encoded identifiers kolab_storage::$encode_ids = true; + // get configuration for the Bonnie API + if ($bonnie_config = $this->rc->config->get('kolab_bonnie_api', false)) { + $this->bonnie_api = new kolab_bonnie_api($bonnie_config); + } + $this->_read_lists(); $this->plugin->register_action('folder-acl', array($this, 'folder_acl')); @@ -152,6 +158,7 @@ class tasklist_kolab_driver extends tasklist_driver 'group' => $folder->default ? 'default' : $folder->get_namespace(), 'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')), 'caldavuid' => $folder->get_uid(), + 'history' => !empty($this->bonnie_api), ); } @@ -658,6 +665,250 @@ class tasklist_kolab_driver extends tasklist_driver return $childs; } + /** + * Provide a list of revisions for the given task + * + * @param array $task Hash array with task properties + * @return array List of changes, each as a hash array + * @see tasklist_driver::get_task_changelog() + */ + public function get_task_changelog($prop) + { + if (empty($this->bonnie_api)) { + return false; + } + + list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); + + $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null; + if (is_array($result) && $result['uid'] == $uid) { + return $result['changes']; + } + + return false; + } + + /** + * Return full data of a specific revision of an event + * + * @param mixed $task UID string or hash array with task properties + * @param mixed $rev Revision number + * + * @return array Task object as hash array + * @see tasklist_driver::get_task_revision() + */ + public function get_task_revison($prop, $rev) + { + if (empty($this->bonnie_api)) { + return false; + } + + $this->_parse_id($prop); + $uid = $prop['uid']; + $list_id = $prop['list']; + list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); + + // call Bonnie API + $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid); + if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) { + $format = kolab_format::factory('task'); + $format->load($result['xml']); + $rec = $format->to_array(); + $format->get_attachments($rec, true); + + if ($format->is_valid()) { + $rec['rev'] = $result['rev']; + return self::_to_rcube_task($rec, $list_id, false); + } + } + + return false; + } + + /** + * Command the backend to restore a certain revision of a task. + * This shall replace the current object with an older version. + * + * @param mixed $task UID string or hash array with task properties + * @param mixed $rev Revision number + * + * @return boolean True on success, False on failure + * @see tasklist_driver::restore_task_revision() + */ + public function restore_task_revision($prop, $rev) + { + if (empty($this->bonnie_api)) { + return false; + } + + $this->_parse_id($prop); + $uid = $prop['uid']; + $list_id = $prop['list']; + list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); + + $folder = $this->get_folder($list_id); + $success = false; + + if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) { + $imap = $this->rc->get_storage(); + + // insert $raw_msg as new message + if ($imap->save_message($folder->name, $raw_msg, null, false)) { + $success = true; + + // delete old revision from imap and cache + $imap->delete_message($msguid, $folder->name); + $folder->cache->set($msguid, false); + } + } + + return $success; + } + + /** + * Get a list of property changes beteen two revisions of a task object + * + * @param array $task Hash array with task properties + * @param mixed $rev Revisions: "from:to" + * + * @return array List of property changes, each as a hash array + * @see tasklist_driver::get_task_diff() + */ + public function get_task_diff($prop, $rev1, $rev2) + { + $this->_parse_id($prop); + $uid = $prop['uid']; + $list_id = $prop['list']; + list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); + + // call Bonnie API + $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id); + if (is_array($result) && $result['uid'] == $uid) { + $result['rev1'] = $rev1; + $result['rev2'] = $rev2; + + $keymap = array( + 'start' => 'start', + 'due' => 'date', + 'dstamp' => 'changed', + 'summary' => 'title', + 'alarm' => 'alarms', + 'attendee' => 'attendees', + 'attach' => 'attachments', + 'rrule' => 'recurrence', + 'percent-complete' => 'complete', + 'lastmodified-date' => 'changed', + ); + $prop_keymaps = array( + 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'), + 'attendees' => array('partstat' => 'status'), + ); + $special_changes = array(); + + // map kolab event properties to keys the client expects + array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) { + if (array_key_exists($change['property'], $keymap)) { + $change['property'] = $keymap[$change['property']]; + } + if ($change['property'] == 'priority') { + $change['property'] = 'flagged'; + $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null; + $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null; + } + // map alarms trigger value + if ($change['property'] == 'alarms') { + if (is_array($change['old']) && is_array($change['old']['trigger'])) + $change['old']['trigger'] = $change['old']['trigger']['value']; + if (is_array($change['new']) && is_array($change['new']['trigger'])) + $change['new']['trigger'] = $change['new']['trigger']['value']; + } + // make all property keys uppercase + if ($change['property'] == 'recurrence') { + $special_changes['recurrence'] = $i; + foreach (array('old','new') as $m) { + if (is_array($change[$m])) { + $props = array(); + foreach ($change[$m] as $k => $v) { + $props[strtoupper($k)] = $v; + } + $change[$m] = $props; + } + } + } + // map property keys names + if (is_array($prop_keymaps[$change['property']])) { + foreach ($prop_keymaps[$change['property']] as $k => $dest) { + if (is_array($change['old']) && array_key_exists($k, $change['old'])) { + $change['old'][$dest] = $change['old'][$k]; + unset($change['old'][$k]); + } + if (is_array($change['new']) && array_key_exists($k, $change['new'])) { + $change['new'][$dest] = $change['new'][$k]; + unset($change['new'][$k]); + } + } + } + + if ($change['property'] == 'exdate') { + $special_changes['exdate'] = $i; + } + else if ($change['property'] == 'rdate') { + $special_changes['rdate'] = $i; + } + }); + + // merge some recurrence changes + foreach (array('exdate','rdate') as $prop) { + if (array_key_exists($prop, $special_changes)) { + $exdate = $result['changes'][$special_changes[$prop]]; + if (array_key_exists('recurrence', $special_changes)) { + $recurrence = &$result['changes'][$special_changes['recurrence']]; + } + else { + $i = count($result['changes']); + $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array()); + $recurrence = &$result['changes'][$i]['recurrence']; + } + $key = strtoupper($prop); + $recurrence['old'][$key] = $exdate['old']; + $recurrence['new'][$key] = $exdate['new']; + unset($result['changes'][$special_changes[$prop]]); + } + } + + return $result; + } + + return false; + } + + /** + * Helper method to resolved the given task identifier into uid and folder + * + * @return array (uid,folder,msguid) tuple + */ + private function _resolve_task_identity($prop) + { + $mailbox = $msguid = null; + + $this->_parse_id($prop); + $uid = $prop['uid']; + $list_id = $prop['list']; + + if ($folder = $this->get_folder($list_id)) { + $mailbox = $folder->get_mailbox_id(); + + // get task object from storage in order to get the real object uid an msguid + if ($rec = $folder->get_object($uid)) { + $msguid = $rec['_msguid']; + $uid = $rec['uid']; + } + } + + return array($uid, $mailbox, $msguid); + } + + /** * Get a list of pending alarms to be displayed to the user * @@ -1232,6 +1483,7 @@ class tasklist_kolab_driver extends tasklist_driver * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier + * rev: Revision (optional) * * @return array Hash array with attachment properties: * id: Attachment identifier @@ -1241,7 +1493,13 @@ class tasklist_kolab_driver extends tasklist_driver */ public function get_attachment($id, $task) { - $task = $this->get_task($task); + // get old revision of the object + if ($task['rev']) { + $task = $this->get_task_revison($task, $task['rev']); + } + else { + $task = $this->get_task($task); + } if ($task && !empty($task['attachments'])) { foreach ($task['attachments'] as $att) { @@ -1260,12 +1518,38 @@ class tasklist_kolab_driver extends tasklist_driver * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier + * rev: Revision (optional) * * @return string Attachment body */ public function get_attachment_body($id, $task) { $this->_parse_id($task); + + // get old revision of event + if ($task['rev']) { + if (empty($this->bonnie_api)) { + return false; + } + + $cid = substr($id, 4); + + // call Bonnie API and get the raw mime message + list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task); + if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) { + // parse the message and find the part with the matching content-id + $message = rcube_mime::parse_message($msg_raw); + foreach ((array)$message->parts as $part) { + if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) { + return $part->body; + } + } + } + + return false; + } + + if ($storage = $this->get_folder($task['list'])) { return $storage->get_attachment($task['uid'], $id); } diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index 3362e7f8..be823447 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -242,6 +242,7 @@ abstract class tasklist_driver * * @param array Hash array with task properties: * id: Task identifier + * list: Tasklist identifer * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) * @return boolean True on success, False on error */ @@ -266,6 +267,7 @@ abstract class tasklist_driver * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier + * rev: Revision (optional) * * @return array Hash array with attachment properties: * id: Attachment identifier @@ -282,6 +284,7 @@ abstract class tasklist_driver * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier + * rev: Revision (optional) * * @return string Attachment body */ @@ -319,7 +322,7 @@ abstract class tasklist_driver /** * Helper method to determine whether the given task is considered "complete" * - * @param array $task Hash array with event properties: + * @param array $task Hash array with event properties * @return boolean True if complete, False otherwiese */ public function is_complete($task) @@ -328,13 +331,74 @@ abstract class tasklist_driver } /** - * List availabale categories - * The default implementation reads them from config/user prefs + * Provide a list of revisions for the given task + * + * @param array $task Hash array with task properties: + * id: Task identifier + * list: List identifier + * + * @return array List of changes, each as a hash array: + * rev: Revision number + * type: Type of the change (create, update, move, delete) + * date: Change date + * user: The user who executed the change + * ip: Client IP + * mailbox: Destination list for 'move' type */ - public function list_categories() + public function get_task_changelog($task) { - $rcmail = rcube::get_instance(); - return $rcmail->config->get('tasklist_categories', array()); + return false; + } + + /** + * Get a list of property changes beteen two revisions of a task object + * + * @param array $task Hash array with task properties: + * id: Task identifier + * list: List identifier + * @param mixed $rev1 Old Revision + * @param mixed $rev2 New Revision + * + * @return array List of property changes, each as a hash array: + * property: Revision number + * old: Old property value + * new: Updated property value + */ + public function get_task_diff($task, $rev1, $rev2) + { + return false; + } + + /** + * Return full data of a specific revision of an event + * + * @param mixed $task UID string or hash array with task properties: + * id: Task identifier + * list: List identifier + * @param mixed $rev Revision number + * + * @return array Task object as hash array + * @see self::get_task() + */ + public function get_task_revison($task, $rev) + { + return false; + } + + /** + * Command the backend to restore a certain revision of a task. + * This shall replace the current object with an older version. + * + * @param mixed $task UID string or hash array with task properties: + * id: Task identifier + * list: List identifier + * @param mixed $rev Revision number + * + * @return boolean True on success, False on failure + */ + public function restore_task_revision($task, $rev) + { + return false; } /** diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index ee66759e..8dc2929c 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -50,6 +50,9 @@ $labels['status-cancelled'] = 'Cancelled'; $labels['assignedto'] = 'Assigned to'; $labels['created'] = 'Created'; $labels['changed'] = 'Last Modified'; +$labels['taskoptions'] = 'Options'; +$labels['taskhistory'] = 'History'; +$labels['compare'] = 'Compare'; $labels['all'] = 'All'; $labels['flagged'] = 'Flagged'; @@ -101,6 +104,7 @@ $labels['on'] = 'on'; $labels['at'] = 'at'; $labels['this'] = 'this'; $labels['next'] = 'next'; +$labels['yes'] = 'yes'; // messages $labels['savingdata'] = 'Saving data...'; @@ -150,6 +154,24 @@ $labels['itipcancelsubject'] = '"$title" has been canceled'; $labels['itipcancelmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details."; $labels['saveintasklist'] = 'save in '; +// history dialog +$labels['objectchangelog'] = 'Change History'; +$labels['objectdiff'] = 'Changes from $rev1 to $rev2'; + +$labels['actionappend'] = 'Saved'; +$labels['actionmove'] = 'Moved'; +$labels['actiondelete'] = 'Deleted'; +$labels['compare'] = 'Compare'; +$labels['showrevision'] = 'Show this version'; +$labels['restore'] = 'Restore this version'; + +$labels['objectnotfound'] = 'Failed to load task data'; +$labels['objectchangelognotavailable'] = 'Change history is not available for this task'; +$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions'; +$labels['revisionrestoreconfirm'] = 'Do you really want to restore revision $rev of this task? This will replace the current task with the old version.'; +$labels['objectrestoresuccess'] = 'Revision $rev successfully restored'; +$labels['objectrestoreerror'] = 'Failed to restore the old revision'; + // invitation handling (overrides labels from libcalendaring) $labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.'; diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css index bd5ec608..a39c1f25 100644 --- a/plugins/tasklist/skins/larry/tasklist.css +++ b/plugins/tasklist/skins/larry/tasklist.css @@ -642,7 +642,8 @@ ul.toolbarmenu li span.icon.taskadd, font-size: 12px; } -.taskhead .flagged { +.taskhead .flagged, +.taskshow.status-flagged h2:after { display: inline-block; width: 16px; height: 16px; @@ -657,7 +658,8 @@ ul.toolbarmenu li span.icon.taskadd, background-position: -2px -3px; } -.taskhead.flagged .flagged { +.taskhead.flagged .flagged, +.taskshow.status-flagged h2:after { background-position: -2px -23px; } @@ -839,8 +841,9 @@ ul.toolbarmenu .sortcol.by-auto a { /*** task edit form ***/ #taskedit, -#taskshow { - display:none; +#taskshow, +#taskdiff { + display: none; } #taskedit { @@ -850,15 +853,32 @@ ul.toolbarmenu .sortcol.by-auto a { margin: 0 -0.2em; } -#taskshow h2 { +.taskshow h2 { margin-top: -0.5em; } -#taskshow label { +#taskdiff h2 { + font-size: 18px; + margin: -0.3em 0 0.4em 0; +} + +.taskshow.status-completed h2 { + text-decoration: line-through; +} + +.taskshow.status-flagged h2:after { + content: " "; + position: relative; + margin-left: 0.6em; + top: 1px; + cursor: default; +} + +.taskshow label { color: #999; } -#taskshow.status-cancelled { +.taskshow.status-cancelled { background: url(images/badge_cancelled.png) top right no-repeat; } @@ -1048,10 +1068,33 @@ label.block { margin-bottom: 0.3em; } -#task-description { +.task-description { margin-bottom: 1em; } +.taskshow .task-text-old, +.taskshow .task-text-new, +.taskshow .task-text-diff { + padding: 2px; +} + +.taskshow .task-text-diff del, +.taskshow .task-text-diff ins { + text-decoration: none; + color: inherit; +} + +.taskshow .task-text-old, +.taskshow .task-text-diff del { + background-color: #fdd; + /* text-decoration: line-through; */ +} + +.taskshow .task-text-new, +.taskshow .task-text-diff ins { + background-color: #dfd; +} + #taskedit-completeness-slider { display: inline-block; margin-left: 2em; diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index f80b2f7e..dd384094 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -149,6 +149,9 @@