/**
  * 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;
     // get configuration for the Bonnie API
     $this->bonnie_api = libkolab::get_bonnie_api();
     $this->_read_lists();
     $this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
 }
 /**
  * Helper method to fetch free/busy data for the user and turn it into calendar data
  */
 private function fetch_freebusy($limit_changed = null)
 {
     // ask kolab server first
     try {
         $request_config = array('store_body' => true, 'follow_redirects' => true);
         $request = libkolab::http_request(kolab_storage::get_freebusy_url($this->userdata['mail']), 'GET', $request_config);
         $response = $request->send();
         // authentication required
         if ($response->getStatus() == 401) {
             $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
             $response = $request->send();
         }
         if ($response->getStatus() == 200) {
             $fbdata = $response->getBody();
         }
         unset($request, $response);
     } catch (Exception $e) {
         rcube::raise_error(array('code' => 900, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error fetching free/busy information: " . $e->getMessage()), true, false);
         return false;
     }
     $statusmap = array('FREE' => 'free', 'BUSY' => 'busy', 'BUSY-TENTATIVE' => 'tentative', 'X-OUT-OF-OFFICE' => 'outofoffice', 'OOF' => 'outofoffice');
     $titlemap = array('FREE' => $this->cal->gettext('availfree'), 'BUSY' => $this->cal->gettext('availbusy'), 'BUSY-TENTATIVE' => $this->cal->gettext('availtentative'), 'X-OUT-OF-OFFICE' => $this->cal->gettext('availoutofoffice'));
     // console('_fetch_freebusy', kolab_storage::get_freebusy_url($this->userdata['mail']), $fbdata);
     // parse free-busy information
     $count = 0;
     if ($fbdata) {
         $ical = $this->cal->get_ical();
         $ical->import($fbdata);
         if ($fb = $ical->freebusy) {
             // consider 'changed >= X' queries
             if ($limit_changed && $fb['created'] && $fb['created'] < $limit_changed) {
                 return 0;
             }
             foreach ($fb['periods'] as $tuple) {
                 list($from, $to, $type) = $tuple;
                 $event = array('id' => md5($this->id . $from->format('U') . '/' . $to->format('U')), 'calendar' => $this->id, 'changed' => $fb['created'] ?: new DateTime(), 'title' => $this->get_name() . ' ' . ($titlemap[$type] ?: $type), 'start' => $from, 'end' => $to, 'free_busy' => $statusmap[$type] ?: 'busy', 'className' => 'fc-type-freebusy', 'organizer' => array('email' => $this->userdata['mail'], 'name' => $this->userdata['displayname']));
                 // avoid duplicate entries
                 $key = $this->time_key($event);
                 if (!$this->timeindex[$key]) {
                     $this->events[$event['id']] = $event;
                     $this->timeindex[$key] = $event['id'];
                     $count++;
                 }
             }
         }
     }
     return $count;
 }
示例#3
0
 /**
  * Dispatcher for event actions initiated by the client
  */
 function event_action()
 {
     $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
     $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
     $success = $reload = $got_msg = false;
     // force notify if hidden + active
     if ((int) $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1) {
         $event['_notify'] = 1;
     }
     // read old event data in order to find changes
     if (($event['_notify'] || $event['_decline']) && $action != 'new') {
         $old = $this->driver->get_event($event);
         // load main event if savemode is 'all' or if deleting 'future' events
         if (($event['_savemode'] == 'all' || $event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline']) && $old['recurrence_id']) {
             $old['id'] = $old['recurrence_id'];
             $old = $this->driver->get_event($old);
         }
     }
     switch ($action) {
         case "new":
             // create UID for new event
             $event['uid'] = $this->generate_uid();
             $this->write_preprocess($event, $action);
             if ($success = $this->driver->new_event($event)) {
                 $event['id'] = $event['uid'];
                 $event['_savemode'] = 'all';
                 $this->cleanup_event($event);
                 $this->event_save_success($event, null, $action, true);
             }
             $reload = $success && $event['recurrence'] ? 2 : 1;
             break;
         case "edit":
             $this->write_preprocess($event, $action);
             if ($success = $this->driver->edit_event($event)) {
                 $this->cleanup_event($event);
                 $this->event_save_success($event, $old, $action, $success);
             }
             $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
             break;
         case "resize":
             $this->write_preprocess($event, $action);
             if ($success = $this->driver->resize_event($event)) {
                 $this->event_save_success($event, $old, $action, $success);
             }
             $reload = $event['_savemode'] ? 2 : 1;
             break;
         case "move":
             $this->write_preprocess($event, $action);
             if ($success = $this->driver->move_event($event)) {
                 $this->event_save_success($event, $old, $action, $success);
             }
             $reload = $success && $event['_savemode'] ? 2 : 1;
             break;
         case "remove":
             // remove previous deletes
             $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
             $this->rc->session->remove('calendar_event_undo');
             // search for event if only UID is given
             if (!isset($event['calendar']) && $event['uid']) {
                 if (!($event = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) {
                     break;
                 }
                 $undo_time = 0;
             }
             $success = $this->driver->remove_event($event, $undo_time < 1);
             $reload = !$success || $event['_savemode'] ? 2 : 1;
             if ($undo_time > 0 && $success) {
                 $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
                 // display message with Undo link.
                 $msg = html::span(null, $this->gettext('successremoval')) . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", rcmail_output::JS_OBJECT_NAME, rcmail_output::JS_OBJECT_NAME)), $this->gettext('undo'));
                 $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
                 $got_msg = true;
             } else {
                 if ($success) {
                     $this->rc->output->show_message('calendar.successremoval', 'confirmation');
                     $got_msg = true;
                 }
             }
             // send cancellation for the main event
             if ($event['_savemode'] == 'all') {
                 unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);
             } else {
                 if ($event['_savemode'] == 'future' && $success !== false && $success !== true) {
                     $event['_savemode'] = 'all';
                     // force event_save_success() to load master event
                     $action = 'edit';
                     $success = true;
                 }
             }
             // send iTIP reply that participant has declined the event
             if ($success && $event['_decline']) {
                 $emails = $this->get_user_emails();
                 foreach ($old['attendees'] as $i => $attendee) {
                     if ($attendee['role'] == 'ORGANIZER') {
                         $organizer = $attendee;
                     } else {
                         if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                             $old['attendees'][$i]['status'] = 'DECLINED';
                             $reply_sender = $attendee['email'];
                         }
                     }
                 }
                 if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) {
                     $old['thisandfuture'] = true;
                 }
                 $itip = $this->load_itip();
                 $itip->set_sender_email($reply_sender);
                 if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) {
                     $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
                 } else {
                     $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
                 }
             } else {
                 if ($success) {
                     $this->event_save_success($event, $old, $action, $success);
                 }
             }
             break;
         case "undo":
             // Restore deleted event
             $event = $_SESSION['calendar_event_undo']['data'];
             if ($event) {
                 $success = $this->driver->restore_event($event);
             }
             if ($success) {
                 $this->rc->session->remove('calendar_event_undo');
                 $this->rc->output->show_message('calendar.successrestore', 'confirmation');
                 $got_msg = true;
                 $reload = 2;
             }
             break;
         case "rsvp":
             $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
             $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);
             $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);
             $reply_comment = $event['comment'];
             $this->write_preprocess($event, 'edit');
             $ev = $this->driver->get_event($event);
             $ev['attendees'] = $event['attendees'];
             $ev['free_busy'] = $event['free_busy'];
             $ev['_savemode'] = $event['_savemode'];
             // send invitation to delegatee + add it as attendee
             if ($status == 'delegated' && $event['to']) {
                 $itip = $this->load_itip();
                 if ($itip->delegate_to($ev, $event['to'], (bool) $event['rsvp'], $attendees)) {
                     $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
                     $noreply = false;
                 }
             }
             $event = $ev;
             // compose a list of attendees affected by this change
             $updated_attendees = array_filter(array_map(function ($j) use($event) {
                 return $event['attendees'][$j];
             }, $attendees));
             if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) {
                 $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
                 $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
                 $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
                 $organizer = null;
                 $emails = $this->get_user_emails();
                 foreach ($event['attendees'] as $i => $attendee) {
                     if ($attendee['role'] == 'ORGANIZER') {
                         $organizer = $attendee;
                     } else {
                         if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                             $reply_sender = $attendee['email'];
                         }
                     }
                 }
                 if (!$noreply) {
                     $itip = $this->load_itip();
                     $itip->set_sender_email($reply_sender);
                     $event['comment'] = $reply_comment;
                     $event['thisandfuture'] = $event['_savemode'] == 'future';
                     if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) {
                         $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
                     } else {
                         $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
                     }
                 }
                 // refresh all calendars
                 if ($event['calendar'] != $ev['calendar']) {
                     $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true));
                     $reload = 0;
                 }
             }
             break;
         case "dismiss":
             $event['ids'] = explode(',', $event['id']);
             $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event);
             $success = $plugin['success'];
             foreach ($event['ids'] as $id) {
                 if (strpos($id, 'cal:') === 0) {
                     $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
                 }
             }
             break;
         case "changelog":
             $data = $this->driver->get_event_changelog($event);
             if (is_array($data) && !empty($data)) {
                 $lib = $this->lib;
                 $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                 array_walk($data, function (&$change) use($lib, $dtformat) {
                     if ($change['date']) {
                         $dt = $lib->adjust_timezone($change['date']);
                         if ($dt instanceof DateTime) {
                             $change['date'] = $this->rc->format_date($dt, $dtformat, false);
                         }
                     }
                 });
                 $this->rc->output->command('plugin.render_event_changelog', $data);
             } else {
                 $this->rc->output->command('plugin.render_event_changelog', false);
             }
             $got_msg = true;
             $reload = false;
             break;
         case "diff":
             $data = $this->driver->get_event_diff($event, $event['rev1'], $event['rev2']);
             if (is_array($data)) {
                 // convert some properties, similar to self::_client_event()
                 $lib = $this->lib;
                 array_walk($data['changes'], function (&$change, $i) use($event, $lib) {
                     // convert date cols
                     foreach (array('start', 'end', 'created', 'changed') as $col) {
                         if ($change['property'] == $col) {
                             $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
                             $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
                         }
                     }
                     // create textual representation for alarms and recurrence
                     if ($change['property'] == 'alarms') {
                         if (is_array($change['old'])) {
                             $change['old_'] = libcalendaring::alarm_text($change['old']);
                         }
                         if (is_array($change['new'])) {
                             $change['new_'] = libcalendaring::alarm_text(array_merge((array) $change['old'], $change['new']));
                         }
                     }
                     if ($change['property'] == 'recurrence') {
                         if (is_array($change['old'])) {
                             $change['old_'] = $lib->recurrence_text($change['old']);
                         }
                         if (is_array($change['new'])) {
                             $change['new_'] = $lib->recurrence_text(array_merge((array) $change['old'], $change['new']));
                         }
                     }
                     if ($change['property'] == 'attachments') {
                         if (is_array($change['old'])) {
                             $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
                         }
                         if (is_array($change['new'])) {
                             $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
                         }
                     }
                     // compute a nice diff of description texts
                     if ($change['property'] == 'description') {
                         $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
                     }
                 });
                 $this->rc->output->command('plugin.event_show_diff', $data);
             } else {
                 $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
             }
             $got_msg = true;
             $reload = false;
             break;
         case "show":
             if ($event = $this->driver->get_event_revison($event, $event['rev'])) {
                 $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
             } else {
                 $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
             }
             $got_msg = true;
             $reload = false;
             break;
         case "restore":
             if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
                 $_event = $this->driver->get_event($event);
                 $reload = $_event['recurrence'] ? 2 : 1;
                 $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation');
                 $this->rc->output->command('plugin.close_history_dialog');
             } else {
                 $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
                 $reload = 0;
             }
             $got_msg = true;
             break;
     }
     // show confirmation/error message
     if (!$got_msg) {
         if ($success) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
         } else {
             $this->rc->output->show_message('calendar.errorsaving', 'error');
         }
     }
     // unlock client
     $this->rc->output->command('plugin.unlock_saving');
     // update event object on the client or trigger a complete refretch if too complicated
     if ($reload) {
         $args = array('source' => $event['calendar']);
         if ($reload > 1) {
             $args['refetch'] = true;
         } else {
             if ($success && $action != 'remove') {
                 $args['update'] = $this->_client_event($this->driver->get_event($event), true);
             }
         }
         $this->rc->output->command('plugin.refresh_calendar', $args);
     }
 }
示例#4
0
 /**
  * Fetch free/busy information from a person within the given range
  */
 public function get_freebusy_list($email, $start, $end)
 {
     if (empty($email)) {
         return false;
     }
     // map vcalendar fbtypes to internal values
     $fbtypemap = array('FREE' => calendar::FREEBUSY_FREE, 'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE, 'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF, 'OOF' => calendar::FREEBUSY_OOF);
     // ask kolab server first
     try {
         $request_config = array('store_body' => true, 'follow_redirects' => true);
         $request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config);
         $response = $request->send();
         // authentication required
         if ($response->getStatus() == 401) {
             $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
             $response = $request->send();
         }
         if ($response->getStatus() == 200) {
             $fbdata = $response->getBody();
         }
         unset($request, $response);
     } catch (Exception $e) {
         PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage());
     }
     // get free-busy url from contacts
     if (!$fbdata) {
         $fburl = null;
         foreach ((array) $this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
             $abook = $this->rc->get_address_book($book);
             if ($result = $abook->search(array('email'), $email, true, true, true)) {
                 while ($contact = $result->iterate()) {
                     if ($fburl = $contact['freebusyurl']) {
                         $fbdata = @file_get_contents($fburl);
                         break;
                     }
                 }
             }
             if ($fbdata) {
                 break;
             }
         }
     }
     // parse free-busy information using Horde classes
     if ($fbdata) {
         $ical = $this->cal->get_ical();
         $ical->import($fbdata);
         if ($fb = $ical->freebusy) {
             $result = array();
             foreach ($fb['periods'] as $tuple) {
                 list($from, $to, $type) = $tuple;
                 $result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
             }
             // we take 'dummy' free-busy lists as "unknown"
             if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy')) {
                 return false;
             }
             // set period from $start till the begin of the free-busy information as 'unknown'
             if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
                 array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
             }
             // pad period till $end with status 'unknown'
             if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
                 $result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
             }
             return $result;
         }
     }
     return false;
 }
 /**
  * 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 = $got_msg = 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
         // 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;
         case 'changelog':
             $data = $this->driver->get_task_changelog($rec);
             if (is_array($data) && !empty($data)) {
                 $lib = $this->lib;
                 $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                 array_walk($data, function (&$change) use($lib, $dtformat) {
                     if ($change['date']) {
                         $dt = $lib->adjust_timezone($change['date']);
                         if ($dt instanceof DateTime) {
                             $change['date'] = $this->rc->format_date($dt, $dtformat, false);
                         }
                     }
                 });
                 $this->rc->output->command('plugin.task_render_changelog', $data);
             } else {
                 $this->rc->output->command('plugin.task_render_changelog', false);
             }
             $got_msg = true;
             break;
         case 'diff':
             $data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']);
             if (is_array($data)) {
                 // convert some properties, similar to self::_client_event()
                 $lib = $this->lib;
                 $date_format = $this->rc->config->get('date_format', 'Y-m-d');
                 $time_format = $this->rc->config->get('time_format', 'H:i');
                 array_walk($data['changes'], function (&$change, $i) use($lib, $date_format, $time_format) {
                     // convert date cols
                     if (in_array($change['property'], array('date', 'start', 'created', 'changed'))) {
                         if (!empty($change['old'])) {
                             $dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format;
                             $change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat);
                         }
                         if (!empty($change['new'])) {
                             $dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format;
                             $change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat);
                         }
                     }
                     // create textual representation for alarms and recurrence
                     if ($change['property'] == 'alarms') {
                         if (is_array($change['old'])) {
                             $change['old_'] = libcalendaring::alarm_text($change['old']);
                         }
                         if (is_array($change['new'])) {
                             $change['new_'] = libcalendaring::alarm_text(array_merge((array) $change['old'], $change['new']));
                         }
                     }
                     if ($change['property'] == 'recurrence') {
                         if (is_array($change['old'])) {
                             $change['old_'] = $lib->recurrence_text($change['old']);
                         }
                         if (is_array($change['new'])) {
                             $change['new_'] = $lib->recurrence_text(array_merge((array) $change['old'], $change['new']));
                         }
                     }
                     if ($change['property'] == 'complete') {
                         $change['old_'] = intval($change['old']) . '%';
                         $change['new_'] = intval($change['new']) . '%';
                     }
                     if ($change['property'] == 'attachments') {
                         if (is_array($change['old'])) {
                             $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
                         }
                         if (is_array($change['new'])) {
                             $change['new'] = array_merge((array) $change['old'], $change['new']);
                             $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
                         }
                     }
                     // resolve parent_id to the refered task title for display
                     if ($change['property'] == 'parent_id') {
                         $change['property'] = 'parent-title';
                         if (!empty($change['old']) && ($old_parent = $this->driver->get_task(array('id' => $change['old'], 'list' => $rec['list'])))) {
                             $change['old_'] = $old_parent['title'];
                         }
                         if (!empty($change['new']) && ($new_parent = $this->driver->get_task(array('id' => $change['new'], 'list' => $rec['list'])))) {
                             $change['new_'] = $new_parent['title'];
                         }
                     }
                     // compute a nice diff of description texts
                     if ($change['property'] == 'description') {
                         $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
                     }
                 });
                 $this->rc->output->command('plugin.task_show_diff', $data);
             } else {
                 $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
             }
             $got_msg = true;
             break;
         case 'show':
             if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) {
                 $this->encode_task($rec);
                 $rec['readonly'] = 1;
                 $this->rc->output->command('plugin.task_show_revision', $rec);
             } else {
                 $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
             }
             $got_msg = true;
             break;
         case 'restore':
             if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) {
                 $refresh = $this->driver->get_task($rec);
                 $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation');
                 $this->rc->output->command('plugin.close_history_dialog');
             } else {
                 $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
             }
             $got_msg = true;
             break;
     }
     if ($success) {
         $this->rc->output->show_message('successfullysaved', 'confirmation');
         $this->update_counts($oldrec, $refresh);
     } else {
         if (!$got_msg) {
             $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);
     }
 }