").addClass("input-group").append(input).append(color).append($("
").append(button))
+ .appendTo("#calendarcategories");
+ color.minicolors(rcmail.env.minicolors_config || {});
+ $("#rcmfd_new_category").val("");
+ }
+}',
+ 'foot'
+ );
+
+ $this->rc->output->add_script('
+$("#rcmfd_new_category").keypress(function(event) {
+ if (event.which == 13) {
+ rcube_calendar_add_category();
+ event.preventDefault();
+ }
+});',
+ 'docready'
+ );
+
+ // load miniColors js/css files
+ jqueryui::miniColors();
+ }
+
+ // virtual birthdays calendar
+ if (!isset($no_override['calendar_contact_birthdays'])) {
+ $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar');
+
+ if (empty($p['current'])) {
+ $p['blocks']['birthdays']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_contact_birthdays';
+ $input = new html_checkbox([
+ 'name' => '_contact_birthdays',
+ 'id' => $field_id,
+ 'value' => 1,
+ 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)'
+ ]);
+
+ $p['blocks']['birthdays']['options']['contact_birthdays'] = [
+ 'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')),
+ 'content' => $input->show($this->rc->config->get('calendar_contact_birthdays') ? 1 : 0),
+ ];
+
+ $input_attrib = [
+ 'class' => 'calendar_birthday_props',
+ 'disabled' => !$this->rc->config->get('calendar_contact_birthdays'),
+ ];
+
+ $sources = [];
+ $checkbox = new html_checkbox(['name' => '_birthday_adressbooks[]'] + $input_attrib);
+
+ foreach ($this->rc->get_address_sources(false, true) as $source) {
+ $active = in_array($source['id'], (array) $this->rc->config->get('calendar_birthday_adressbooks')) ? $source['id'] : '';
+ $sources[] = html::tag('li', null,
+ html::label(null,
+ $checkbox->show($active, ['value' => $source['id']])
+ . rcube::Q(!empty($source['realname']) ? $source['realname'] : $source['name'])
+ )
+ );
+ }
+
+ $p['blocks']['birthdays']['options']['birthday_adressbooks'] = [
+ 'title' => rcube::Q($this->gettext('birthdayscalendarsources')),
+ 'content' => html::tag('ul', 'proplist', implode("\n", $sources)),
+ ];
+
+ $field_id = 'rcmfd_birthdays_alarm';
+ $select_type = new html_select(['name' => '_birthdays_alarm_type', 'id' => $field_id] + $input_attrib);
+ $select_type->add($this->gettext('none'), '');
+
+ foreach ($this->driver->alarm_types as $type) {
+ $select_type->add($this->rc->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
+ }
+
+ $input_value = new html_inputfield(['name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3] + $input_attrib);
+ $select_offset = new html_select(['name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset'] + $input_attrib);
+
+ foreach (['-M','-H','-D'] as $trigger) {
+ $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);
+ }
+
+ $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D'));
+ $preset_type = $this->rc->config->get('calendar_birthdays_alarm_type', '');
+
+ $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = [
+ 'title' => html::label($field_id, rcube::Q($this->gettext('showalarms'))),
+ 'content' => html::div('input-group',
+ $select_type->show($preset_type)
+ . $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1])
+ ),
+ ];
+ }
+
+ return $p;
+ }
+
+ /**
+ * Handler for preferences_save hook.
+ * Executed on Calendar settings form submit.
+ *
+ * @param array Original parameters
+ *
+ * @return array Modified parameters
+ */
+ function preferences_save($p)
+ {
+ if ($p['section'] == 'calendar') {
$this->load_driver();
- $invitation = $itip->get_invitation($token);
- $existing = $this->driver->get_event($this->event);
+ // compose default alarm preset value
+ $alarm_offset = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST);
+ $alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST);
+ $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1];
- // save the event to his/her default calendar if not yet present
- if (!$existing && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) {
- $invitation['event']['calendar'] = $calendar['id'];
- if ($this->driver->new_event($invitation['event']))
- $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
- else
- $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
+ $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST);
+ $birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST);
+ $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1];
+
+ $p['prefs'] = [
+ 'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST),
+ 'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)),
+ 'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)),
+ 'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)),
+ 'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)),
+ 'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)),
+ 'calendar_show_weekno' => intval(rcube_utils::get_input_value('_show_weekno', rcube_utils::INPUT_POST)),
+ 'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)),
+ 'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST),
+ 'calendar_default_alarm_offset' => $default_alarm,
+ 'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST),
+ 'calendar_date_format' => null, // clear previously saved values
+ 'calendar_time_format' => null,
+ 'calendar_contact_birthdays' => !empty(rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST)),
+ 'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST),
+ 'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST),
+ 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,
+ 'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)),
+ ];
+
+ if ($p['prefs']['calendar_itip_after_action'] == 4) {
+ $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true);
}
- else if ($existing
- && ($this->event['sequence'] >= $existing['sequence'] || $this->event['changed'] >= $existing['changed'])
- && ($calendar = $this->driver->get_calendar($existing['calendar']))
- ) {
- $this->event = $invitation['event'];
- $this->event['id'] = $existing['id'];
- unset($this->event['comment']);
+ // categories
+ if (empty($this->driver->nocategories)) {
+ $old_categories = $new_categories = [];
- // merge attendees status
- // e.g. preserve my participant status for regular updates
- $this->lib->merge_attendees($this->event, $existing, $status);
+ foreach ($this->driver->list_categories() as $name => $color) {
+ $old_categories[md5($name)] = $name;
+ }
- // update attachments list
- $event['deleted_attachments'] = true;
+ $categories = (array) rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST);
+ $colors = (array) rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST);
- // show me as free when declined (#1670)
- if ($status == 'declined')
- $this->event['free_busy'] = 'free';
+ foreach ($categories as $key => $name) {
+ if (!isset($colors[$key])) {
+ continue;
+ }
- if ($this->driver->edit_event($this->event))
- $this->rc->output->command('display_message', $this->gettext(array('name' => 'updatedsuccessfully', 'vars' => array('calendar' => $calendar->get_name()))), 'confirmation');
- else
- $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
+ $color = preg_replace('/^#/', '', strval($colors[$key]));
+
+ // rename categories in existing events -> driver's job
+ if (!empty($old_categories[$key])) {
+ $oldname = $old_categories[$key];
+ $this->driver->replace_category($oldname, $name, $color);
+ unset($old_categories[$key]);
+ }
+ else {
+ $this->driver->add_category($name, $color);
+ }
+
+ $new_categories[$name] = $color;
+ }
+
+ // these old categories have been removed, alter events accordingly -> driver's job
+ foreach ((array) $old_categories as $key => $name) {
+ $this->driver->remove_category($name);
+ }
+
+ $p['prefs']['calendar_categories'] = $new_categories;
}
- }
}
-
- $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
- $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
-
- if (!$this->invitestatus) {
- $this->itip->set_rsvp_actions(array('accepted','tentative','declined'));
- $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
- }
-
- $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']);
- }
- else
- $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
-
- $this->rc->output->send('calendar.itipattend');
+
+ return $p;
}
- }
-
- /**
- *
- */
- public function itip_event_inviteform($attrib)
- {
- $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
- return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
- }
- /**
- *
- */
- private function mail_agenda_event_row($event, $class = '')
- {
- $time = $event['allday'] ? $this->gettext('all-day') :
- $this->rc->format_date($event['start'], $this->rc->config->get('time_format')) . ' - ' .
- $this->rc->format_date($event['end'], $this->rc->config->get('time_format'));
+ /**
+ * Dispatcher for calendar actions initiated by the client
+ */
+ function calendar_action()
+ {
+ $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
+ $cal = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC);
+ $success = false;
+ $reload = false;
- return html::div(rtrim('event-row ' . ($class ?: $event['className'])),
- html::span('event-date', $time) .
- html::span('event-title', rcube::Q($event['title']))
- );
- }
-
- /**
- *
- */
- public function mail_messages_list($p)
- {
- if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) {
- foreach ($p['messages'] as $header) {
- $part = new StdClass;
- $part->mimetype = $header->ctype;
- if (libcalendaring::part_is_vcalendar($part)) {
- $header->list_flags['attachmentClass'] = 'ical';
+ if (isset($cal['showalarms'])) {
+ $cal['showalarms'] = intval($cal['showalarms']);
}
- else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) {
- // TODO: fetch bodystructure and search for ical parts. Maybe too expensive?
- if (!empty($header->structure) && is_array($header->structure->parts)) {
- foreach ($header->structure->parts as $part) {
- if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) {
- $header->list_flags['attachmentClass'] = 'ical';
- break;
- }
+
+ switch ($action) {
+ case "form-new":
+ case "form-edit":
+ echo $this->ui->calendar_editform($action, $cal);
+ exit;
+
+ case "new":
+ $success = $this->driver->create_calendar($cal);
+ $reload = true;
+ break;
+
+ case "edit":
+ $success = $this->driver->edit_calendar($cal);
+ $reload = true;
+ break;
+
+ case "delete":
+ if ($success = $this->driver->delete_calendar($cal)) {
+ $this->rc->output->command('plugin.destroy_source', ['id' => $cal['id']]);
}
- }
- }
- }
- }
- }
+ break;
- /**
- * Add UI element to copy event invitations or updates to the calendar
- */
- public function mail_messagebody_html($p)
- {
- // load iCalendar functions (if necessary)
- if (!empty($this->lib->ical_parts)) {
- $this->get_ical();
- $this->load_itip();
- }
+ case "subscribe":
+ if (!$this->driver->subscribe_calendar($cal)) {
+ $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
+ }
+ else {
+ $calendars = $this->driver->list_calendars();
+ $calendar = !empty($calendars[$cal['id']]) ? $calendars[$cal['id']] : null;
- $html = '';
- $has_events = false;
- $ical_objects = $this->lib->get_mail_ical_objects();
+ // find parent folder and check if it's a "user calendar"
+ // if it's also activated we need to refresh it (#5340)
+ while (!empty($calendar['parent'])) {
+ if (isset($calendars[$calendar['parent']])) {
+ $calendar = $calendars[$calendar['parent']];
+ }
+ else {
+ break;
+ }
+ }
- // show a box for every event in the file
- foreach ($ical_objects as $idx => $event) {
- if ($event['_type'] != 'event') // skip non-event objects (#2928)
- continue;
+ if ($calendar && $calendar['id'] != $cal['id']
+ && !empty($calendar['active'])
+ && $calendar['group'] == "other user"
+ ) {
+ $this->rc->output->command('plugin.refresh_source', $calendar['id']);
+ }
+ }
+ return;
- $has_events = true;
+ case "search":
+ $results = [];
+ $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
+ $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
+ $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
- // get prepared inline UI for this event object
- if ($ical_objects->method) {
- $append = '';
- $date_str = $this->rc->format_date($event['start'], $this->rc->config->get('date_format'), empty($event['start']->_dateonly));
- $date = new DateTime($event['start']->format('Y-m-d') . ' 12:00:00', new DateTimeZone('UTC'));
+ foreach ((array) $this->driver->search_calendars($query, $source) as $id => $prop) {
+ $editname = $prop['editname'];
+ unset($prop['editname']); // force full name to be displayed
+ $prop['active'] = false;
- // prepare a small agenda preview to be filled with actual event data on async request
- if ($ical_objects->method == 'REQUEST') {
- $append = html::div('calendar-agenda-preview',
- html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' . html::span('date', $date_str))
- . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%');
+ // let the UI generate HTML and CSS representation for this calendar
+ $html = $this->ui->calendar_list_item($id, $prop, $jsenv);
+ $cal = $jsenv[$id];
+ $cal['editname'] = $editname;
+ $cal['html'] = $html;
+
+ if (!empty($prop['color'])) {
+ $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);
+ }
+
+ $results[] = $cal;
+ }
+
+ // report more results available
+ if (!empty($this->driver->search_more_results)) {
+ $this->rc->output->show_message('autocompletemore', 'notice');
+ }
+
+ $reqid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
+ $this->rc->output->command('multi_thread_http_response', $results, $reqid);
+ return;
}
- $html .= html::div('calendar-invitebox invitebox boxinformation',
- $this->itip->mail_itip_inline_ui(
- $event,
- $ical_objects->method,
- $ical_objects->mime_id . ':' . $idx,
- 'calendar',
- rcube_utils::anytodatetime($ical_objects->message_date),
- $this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $date->format('U')
- ) . $append
- );
- }
-
- // limit listing
- if ($idx >= 3)
- break;
- }
-
- // prepend event boxes to message body
- if ($html) {
- $this->ui->init();
- $p['content'] = $html . $p['content'];
- $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
- }
-
- // add "Save to calendar" button into attachment menu
- if ($has_events) {
- $this->add_button(array(
- 'id' => 'attachmentsavecal',
- 'name' => 'attachmentsavecal',
- 'type' => 'link',
- 'wrapper' => 'li',
- 'command' => 'attachment-save-calendar',
- 'class' => 'icon calendarlink disabled',
- 'classact' => 'icon calendarlink active',
- 'innerclass' => 'icon calendar',
- 'label' => 'calendar.savetocalendar',
- ), 'attachmentmenu');
- }
-
- return $p;
- }
-
-
- /**
- * Handler for POST request to import an event attached to a mail message
- */
- public function mail_import_itip()
- {
- $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
-
- $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));
- $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
- $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
- $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
- $comment = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
-
- $error_msg = $this->gettext('errorimportingevent');
- $success = false;
-
- 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->rc->gettext('libcalendaring.delegateinvalidaddress'), 'error');
- return;
- }
- }
-
- // successfully parsed events?
- if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
- // forward iTip request to delegatee
- if ($delegate) {
- $rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST);
- $itip = $this->load_itip();
-
- $event['comment'] = $comment;
-
- if ($itip->delegate_to($event, $delegate, !empty($rsvpme))) {
- $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ if ($success) {
+ $this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
- $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ $error_msg = $this->gettext('errorsaving');
+ if (!empty($this->driver->last_error)) {
+ $error_msg .= ': ' . $this->driver->last_error;
+ }
+ $this->rc->output->show_message($error_msg, 'error');
}
- unset($event['comment']);
+ $this->rc->output->command('plugin.unlock_saving');
- // the delegator is set to non-participant, thus save as non-blocking
- $event['free_busy'] = 'free';
- }
+ if ($success && $reload) {
+ $this->rc->output->command('plugin.reload_view');
+ }
+ }
- $mode = calendar_driver::FILTER_PERSONAL
- | calendar_driver::FILTER_SHARED
- | calendar_driver::FILTER_WRITEABLE;
+ /**
+ * 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;
+ $old = null;
- // find writeable calendar to store event
- $cal_id = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST);
- $dontsave = $cal_id === '' && $event['_method'] == 'REQUEST';
- $calendars = $this->driver->list_calendars($mode);
- $calendar = $calendars[$cal_id];
+ // read old event data in order to find changes
+ if ((!empty($event['_notify']) || !empty($event['_decline'])) && $action != 'new') {
+ $old = $this->driver->get_event($event);
- // select default calendar except user explicitly selected 'none'
- if (!$calendar && !$dontsave)
- $calendar = $this->get_default_calendar($event['sensitivity'], $calendars);
-
- $metadata = array(
- 'uid' => $event['uid'],
- '_instance' => $event['_instance'],
- 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
- 'sequence' => intval($event['sequence']),
- 'fallback' => strtoupper($status),
- 'method' => $event['_method'],
- 'task' => 'calendar',
- );
-
- // update my attendee status according to submitted method
- if (!empty($status)) {
- $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)) {
- $event['attendees'][$i]['status'] = strtoupper($status);
- if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED')))
- $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
-
- $metadata['attendee'] = $attendee['email'];
- $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
- $reply_sender = $attendee['email'];
- $event_attendee = $attendee;
- }
+ // load main event if savemode is 'all' or if deleting 'future' events
+ if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && empty($event['_decline'])))
+ && !empty($old['recurrence_id'])
+ ) {
+ $old['id'] = $old['recurrence_id'];
+ $old = $this->driver->get_event($old);
+ }
}
- // add attendee with this user's default identity if not listed
- if (!$reply_sender) {
- $sender_identity = $this->rc->user->list_emails(true);
- $event['attendees'][] = array(
- 'name' => $sender_identity['name'],
- 'email' => $sender_identity['email'],
- 'role' => 'OPT-PARTICIPANT',
- 'status' => strtoupper($status),
- );
- $metadata['attendee'] = $sender_identity['email'];
- }
- }
-
- // save to calendar
- if ($calendar && $calendar['editable']) {
- // check for existing event with the same UID
- $existing = $this->find_event($event, $mode);
+ switch ($action) {
+ case "new":
+ // create UID for new event
+ $event['uid'] = $this->generate_uid();
+ if (!$this->write_preprocess($event, $action)) {
+ $got_msg = true;
+ }
+ else if ($success = $this->driver->new_event($event)) {
+ $event['id'] = $event['uid'];
+ $event['_savemode'] = 'all';
- // we'll create a new copy if user decided to change the calendar
- if ($existing && $cal_id && $calendar && $calendar['id'] != $existing['calendar']) {
- $existing = null;
- }
-
- if ($existing) {
- $calendar = $calendars[$existing['calendar']];
-
- // forward savemode for correct updates of recurring events
- $existing['_savemode'] = $savemode ?: $event['_savemode'];
-
- // only update attendee status
- if ($event['_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 ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
- $existing_attendee = $i;
- }
+ $this->cleanup_event($event);
+ $this->event_save_success($event, null, $action, true);
}
- $event_attendee = null;
- $update_attendees = array();
+ $reload = $success && !empty($event['recurrence']) ? 2 : 1;
+ break;
- foreach ($event['attendees'] as $attendee) {
- if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
- $event_attendee = $attendee;
- $update_attendees[] = $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'])
- && $this->itip->compare_email($attendee['delegated-from'], $event['_sender'], $event['_sender_utf'])
- ) {
- $update_attendees[] = $attendee;
- if (!in_array_nocase($attendee['email'], $existing_attendee_emails)) {
- $existing['attendees'][] = $attendee;
- }
- }
+ case "edit":
+ if (!$this->write_preprocess($event, $action)) {
+ $got_msg = true;
+ }
+ else if ($success = $this->driver->edit_event($event)) {
+ $this->cleanup_event($event);
+ $this->event_save_success($event, $old, $action, $success);
}
- // if delegatee has declined, set delegator's RSVP=True
- if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
- foreach ($existing['attendees'] as $i => $attendee) {
- if ($attendee['email'] == $event_attendee['delegated-from']) {
- $existing['attendees'][$i]['rsvp'] = true;
- break;
- }
- }
+ $reload = $success && (!empty($event['recurrence']) || !empty($event['_savemode']) || !empty($event['_fromcalendar'])) ? 2 : 1;
+ break;
+
+ case "resize":
+ if (!$this->write_preprocess($event, $action)) {
+ $got_msg = true;
+ }
+ else if ($success = $this->driver->resize_event($event)) {
+ $this->event_save_success($event, $old, $action, $success);
}
- // Accept sender as a new participant (different email in From: and the iTip)
- // Use ATTENDEE entry from the iTip with replaced email address
- if (!$event_attendee) {
- // remove the organizer
- $itip_attendees = array_filter($event['attendees'], function($item) { return $item['role'] != 'ORGANIZER'; });
+ $reload = !empty($event['_savemode']) ? 2 : 1;
+ break;
- // there must be only one attendee
- if (is_array($itip_attendees) && count($itip_attendees) == 1) {
- $event_attendee = $itip_attendees[key($itip_attendees)];
- $event_attendee['email'] = $event['_sender'];
- $update_attendees[] = $event_attendee;
- $metadata['fallback'] = $event_attendee['status'];
- $metadata['attendee'] = $event_attendee['email'];
- $metadata['rsvp'] = $event_attendee['rsvp'] || $event_attendee['role'] != 'NON-PARTICIPANT';
- }
+ case "move":
+ if (!$this->write_preprocess($event, $action)) {
+ $got_msg = true;
+ }
+ else if ($success = $this->driver->move_event($event)) {
+ $this->event_save_success($event, $old, $action, $success);
}
- // found matching attendee entry in both existing and new events
- if ($existing_attendee >= 0 && $event_attendee) {
- $existing['attendees'][$existing_attendee] = $event_attendee;
- $success = $this->driver->update_attendees($existing, $update_attendees);
- }
- // update the entire attendees block
- else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
- $existing['attendees'][] = $event_attendee;
- $success = $this->driver->update_attendees($existing, $update_attendees);
- }
- else if (!$event_attendee) {
- $error_msg = $this->gettext('errorunknownattendee');
- }
- else {
- $error_msg = $this->gettext('newerversionexists');
- }
- }
- // delete the event when declined (#1670)
- else if ($status == 'declined' && $delete) {
- $deleted = $this->driver->remove_event($existing, true);
- $success = true;
- }
- // import the (newer) event
- else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
- $event['id'] = $existing['id'];
- $event['calendar'] = $existing['calendar'];
+ $reload = $success && !empty($event['_savemode']) ? 2 : 1;
+ break;
- // merge attendees status
- // e.g. preserve my participant status for regular updates
- $this->lib->merge_attendees($event, $existing, $status);
+ case "remove":
+ // remove previous deletes
+ $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
- // set status=CANCELLED on CANCEL messages
- if ($event['_method'] == 'CANCEL')
- $event['status'] = 'CANCELLED';
-
- // update attachments list, allow attachments update only on REQUEST (#5342)
- if ($event['_method'] == 'REQUEST')
- $event['deleted_attachments'] = true;
- else
- unset($event['attachments']);
-
- // show me as free when declined (#1670)
- if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT')
- $event['free_busy'] = 'free';
-
- $success = $this->driver->edit_event($event);
- }
- else if (!empty($status)) {
- $existing['attendees'] = $event['attendees'];
- if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // 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_calendars'))) {
- if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
- $event['free_busy'] = 'free';
- }
-
- // if the RSVP reply only refers to a single instance:
- // store unmodified master event with current instance as exception
- if (!empty($instance) && !empty($savemode) && $savemode != 'all') {
- $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
- if ($master['recurrence'] && !$master['_instance']) {
- // compute recurring events until this instance's date
- if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
- $recurrence_date->setTime(23,59,59);
-
- foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
- if ($recurring['_instance'] == $instance) {
- // copy attendees block with my partstat to exception
- $recurring['attendees'] = $event['attendees'];
- $master['recurrence']['EXCEPTIONS'][] = $recurring;
- $event = $recurring; // set reference for iTip reply
+ // search for event if only UID is given
+ if (!isset($event['calendar']) && !empty($event['uid'])) {
+ if (!($event = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) {
break;
- }
+ }
+ $undo_time = 0;
+ }
+
+ // Note: the driver is responsible for setting $_SESSION['calendar_event_undo']
+ // containing 'ts' and 'data' elements
+ $success = $this->driver->remove_event($event, $undo_time < 1);
+ $reload = (!$success || !empty($event['_savemode'])) ? 2 : 1;
+
+ if ($undo_time > 0 && $success) {
+ // display message with Undo link.
+ $onclick = sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
+ rcmail_output::JS_OBJECT_NAME,
+ rcmail_output::JS_OBJECT_NAME
+ );
+ $msg = html::span(null, $this->gettext('successremoval'))
+ . ' ' . html::a(['onclick' => $onclick], $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']);
+ }
+ // send an update for the main event's recurrence rule instead of a cancellation message
+ 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 && !empty($event['_decline'])) {
+ $emails = $this->get_user_emails();
+ $organizer = null;
+
+ foreach ($old['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if (!empty($attendee['email']) && in_array(strtolower($attendee['email']), $emails)) {
+ $old['attendees'][$i]['status'] = 'DECLINED';
+ $reply_sender = $attendee['email'];
+ }
}
- $master['calendar'] = $event['calendar'] = $calendar['id'];
- $success = $this->driver->new_event($master);
- }
- else {
- $master = null;
- }
+ 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')) {
+ $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];
+ $msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);
+
+ $this->rc->output->command('display_message', $msg, '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
+ if (!empty($_SESSION['calendar_event_undo']['data'])) {
+ $event = $_SESSION['calendar_event_undo']['data'];
+ $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'];
+ $ev['comment'] = $reply_comment;
+
+ // send invitation to delegatee + add it as attendee
+ if ($status == 'delegated' && !empty($event['to'])) {
+ $itip = $this->load_itip();
+ if ($itip->delegate_to($ev, $event['to'], !empty($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'] || !empty($event['recurrence']) ? 2 : 1;
+ $emails = $this->get_user_emails();
+ $organizer = null;
+
+ foreach ($event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if (!empty($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['thisandfuture'] = $event['_savemode'] == 'future';
+
+ if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) {
+ $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];
+ $msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);
+
+ $this->rc->output->command('display_message', $msg, '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', ['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 (!empty($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 {
- $master = null;
+ $this->rc->output->command('plugin.render_event_changelog', false);
}
- }
- // save to the selected/default calendar
- if (!$master) {
- $event['calendar'] = $calendar['id'];
- $success = $this->driver->new_event($event);
- }
- }
- else if ($status == 'declined')
- $error_msg = null;
- }
- else if ($status == 'declined' || $dontsave)
- $error_msg = null;
- else
- $error_msg = $this->gettext('nowritecalendarfound');
- }
+ $got_msg = true;
+ $reload = false;
- if ($success) {
- $message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
- $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
- }
+ break;
- if ($success || $dontsave) {
- $metadata['calendar'] = $event['calendar'];
- $metadata['nosave'] = $dontsave;
- $metadata['rsvp'] = intval($metadata['rsvp']);
- $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
- $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');
- }
+ 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 (['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']);
+ }
+ });
- // send iTip reply
- if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
- $event['comment'] = $comment;
- $itip = $this->load_itip();
- $itip->set_sender_email($reply_sender);
- if ($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');
- }
+ $this->rc->output->command('plugin.event_show_diff', $data);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
+ }
- $this->rc->output->send();
- }
+ $got_msg = true;
+ $reload = false;
- /**
- * Handler for calendar/itip-remove requests
- */
- function mail_itip_decline_reply()
- {
- $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);
+ break;
- if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') {
- $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+ 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');
+ }
- foreach ($event['attendees'] as $_attendee) {
- if ($_attendee['role'] != 'ORGANIZER') {
- $attendee = $_attendee;
- break;
- }
- }
+ $got_msg = true;
+ $reload = false;
+ break;
- $itip = $this->load_itip();
- if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel'))
- $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation');
- else
- $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
- }
- else {
- $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
- }
- }
+ case "restore":
+ if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
+ $_event = $this->driver->get_event($event);
+ $reload = $_event['recurrence'] ? 2 : 1;
+ $msg = $this->gettext(['name' => 'objectrestoresuccess', 'vars' => ['rev' => $event['rev']]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ $this->rc->output->command('plugin.close_history_dialog');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
+ $reload = 0;
+ }
- /**
- * Handler for calendar/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();
- }
-
- /**
- * 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 = RCUBE_CHARSET;
-
- // establish imap connection
- $imap = $this->rc->get_storage();
- $imap->set_folder($mbox);
-
- if ($uid && $mime_id) {
- $part = $imap->get_message_part($uid, $mime_id);
- if ($part->ctype_parameters['charset'])
- $charset = $part->ctype_parameters['charset'];
-// $headers = $imap->get_message_headers($uid);
-
- if ($part) {
- $events = $this->get_ical()->import($part, $charset);
- }
- }
-
- $success = $existing = 0;
- if (!empty($events)) {
- // find writeable calendar to store event
- $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null;
- $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
-
- foreach ($events as $event) {
- // save to calendar
- $calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']);
- if ($calendar && $calendar['editable'] && $event['_type'] == 'event') {
- $event['calendar'] = $calendar['id'];
-
- if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) {
- $success += (bool)$this->driver->new_event($event);
- }
- 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('errorimportingevent'), 'error');
- }
- }
-
- /**
- * Read email message and return contents for a new event based on that message
- */
- public function mail_message2event()
- {
- $this->ui->init();
- $this->ui->addJS();
- $this->ui->init_templates();
- $this->ui->calendar_list(array(), true); // set env['calendars']
-
- $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
- $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET);
- $event = array();
-
- // establish imap connection
- $imap = $this->rc->get_storage();
- $message = new rcube_message($uid, $mbox);
-
- if ($message->headers) {
- $event['title'] = trim($message->subject);
- $event['description'] = trim($message->first_text_part());
-
- $this->load_driver();
-
- // add a reference to the email message
- if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
- $event['links'] = array($msgref);
- }
- // copy mail attachments to event
- else if ($message->attachments) {
- $eventid = 'cal-';
- if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
- $_SESSION[self::SESSION_KEY] = array();
- $_SESSION[self::SESSION_KEY]['id'] = $eventid;
- $_SESSION[self::SESSION_KEY]['attachments'] = array();
+ $got_msg = true;
+ break;
}
- 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' => $eventid,
- );
-
- $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'
- $event['attachments'][] = $attachment;
- }
+ // 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');
+ }
}
- }
- $this->rc->output->set_env('event_prop', $event);
- }
- else {
- $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
+ // unlock client
+ $this->rc->output->command('plugin.unlock_saving', $success);
+
+ // update event object on the client or trigger a complete refresh if too complicated
+ if ($reload && empty($_REQUEST['_framed'])) {
+ $args = ['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);
+ }
}
- $this->rc->output->send('calendar.dialog');
- }
+ /**
+ * Helper method sending iTip notifications after successful event updates
+ */
+ private function event_save_success(&$event, $old, $action, $success)
+ {
+ // $success is a new event ID
+ if ($success !== true) {
+ // send update notification on the main event
+ if ($event['_savemode'] == 'future' && !empty($event['_notify'])
+ && !empty($old['attendees']) && !empty($old['recurrence_id'])
+ ) {
+ $master = $this->driver->get_event(['id' => $old['recurrence_id'], 'calendar' => $old['calendar']], 0, true);
+ unset($master['_instance'], $master['recurrence_date']);
- /**
- * Handler for the 'message_compose' plugin hook. This will check for
- * a compose parameter 'calendar_event' and create an attachment with the
- * referenced event in iCal format
- */
- public function mail_message_compose($args)
- {
- // set the submitted event ID as attachment
- if (!empty($args['param']['calendar_event'])) {
- $this->load_driver();
+ $sent = $this->notify_attendees($master, null, $action, $event['_comment'], false);
+ if ($sent < 0) {
+ $this->rc->output->show_message('calendar.errornotifying', 'error');
+ }
- list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
- if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) {
- $filename = asciiwords($event['title']);
- if (empty($filename))
- $filename = 'event';
+ $event['attendees'] = $master['attendees']; // this tricks us into the next if clause
+ }
- // save ics to a temp file and register as attachment
- $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
- file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body')));
+ // delete old reference if saved as new
+ if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') {
+ $old = null;
+ }
- $args['attachments'][] = array(
- 'path' => $tmp_path,
- 'name' => $filename . '.ics',
- 'mimetype' => 'text/calendar',
- 'size' => filesize($tmp_path),
- );
- $args['param']['subject'] = $event['title'];
- }
+ $event['id'] = $success;
+ $event['_savemode'] = 'all';
+ }
+
+ // send out notifications
+ if (!empty($event['_notify']) && (!empty($event['attendees']) || !empty($old['attendees']))) {
+ $_savemode = $event['_savemode'];
+
+ // send notification for the main event when savemode is 'all'
+ if ($action != 'remove' && $_savemode == 'all'
+ && (!empty($event['recurrence_id']) || !empty($old['recurrence_id']) || ($old && $old['id'] != $event['id']))
+ ) {
+ if (!empty($event['recurrence_id'])) {
+ $event['id'] = $event['recurrence_id'];
+ }
+ else if (!empty($old['recurrence_id'])) {
+ $event['id'] = $old['recurrence_id'];
+ }
+ else {
+ $event['id'] = $old['id'];
+ }
+ $event = $this->driver->get_event($event, 0, true);
+ unset($event['_instance'], $event['recurrence_date']);
+ }
+ else {
+ // make sure we have the complete record
+ $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true);
+ }
+
+ $event['_savemode'] = $_savemode;
+
+ if ($old) {
+ $old['thisandfuture'] = $_savemode == 'future';
+ }
+
+ // only notify if data really changed (TODO: do diff check on client already)
+ if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
+ $comment = isset($event['_comment']) ? $event['_comment'] : null;
+ $sent = $this->notify_attendees($event, $old, $action, $comment);
+
+ if ($sent > 0) {
+ $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ }
+ else if ($sent < 0) {
+ $this->rc->output->show_message('calendar.errornotifying', 'error');
+ }
+ }
+ }
}
- return $args;
- }
+ /**
+ * Handler for load-requests from fullcalendar
+ * This will return pure JSON formatted output
+ */
+ function load_events()
+ {
+ $start = $this->input_timestamp('start', rcube_utils::INPUT_GET);
+ $end = $this->input_timestamp('end', rcube_utils::INPUT_GET);
+ $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET);
+ $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
-
- /**
- * Get a list of email addresses of the current user (from login and identities)
- */
- public function get_user_emails()
- {
- return $this->lib->get_user_emails();
- }
-
-
- /**
- * Build an absolute URL with the given parameters
- */
- public function get_url($param = array())
- {
- $param += array('task' => 'calendar');
- return $this->rc->url($param, true, true);
- }
-
-
- public function ical_feed_hash($source)
- {
- return base64_encode($this->rc->user->get_username() . ':' . $source);
- }
-
- /**
- * Handler for user_delete plugin hook
- */
- public function user_delete($args)
- {
- // delete itipinvitations entries related to this user
- $db = $this->rc->get_dbh();
- $table_itipinvitations = $db->table_name('itipinvitations', true);
- $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID);
-
- $this->setup();
- $this->load_driver();
- return $this->driver->user_delete($args);
- }
-
- /**
- * Find first occurrence of a recurring event excluding start date
- *
- * @param array $event Event data (with 'start' and 'recurrence')
- *
- * @return DateTime Date of the first occurrence
- */
- public function find_first_occurrence($event)
- {
- // Make sure libkolab plugin is loaded in case of Kolab driver
- $this->load_driver();
-
- // Use libkolab to compute recurring events (and libkolab plugin)
- // Horde-based fallback has many bugs
- if (class_exists('kolabformat') && class_exists('kolabcalendaring') && class_exists('kolab_date_recurrence')) {
- $object = kolab_format::factory('event', 3.0);
- $object->set($event);
-
- $recurrence = new kolab_date_recurrence($object);
- }
- else {
- // fallback to libcalendaring (Horde-based) recurrence implementation
- require_once(__DIR__ . '/lib/calendar_recurrence.php');
- $recurrence = new calendar_recurrence($this, $event);
+ $events = $this->driver->load_events($start, $end, $query, $source);
+ echo $this->encode($events, !empty($query));
+ exit;
}
- return $recurrence->first_occurrence();
- }
+ /**
+ * Handler for requests fetching event counts for calendars
+ */
+ public function count_events()
+ {
+ // don't update session on these requests (avoiding race conditions)
+ $this->rc->session->nowrite = true;
- /**
- * Get date-time input from UI and convert to unix timestamp
- */
- protected function input_timestamp($name, $type)
- {
- $ts = rcube_utils::get_input_value($name, $type);
+ $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
+ $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
+ $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);
- if ($ts && (!is_numeric($ts) || strpos($ts, 'T'))) {
- $ts = new DateTime($ts, $this->timezone);
- $ts = $ts->getTimestamp();
+ if (!$start) {
+ $start = new DateTime('today 00:00:00', $this->timezone);
+ $start = $start->format('U');
+ }
+
+ $counts = $this->driver->count_events($source, $start, $end);
+
+ $this->rc->output->command('plugin.update_counts', ['counts' => $counts]);
}
- return $ts;
- }
+ /**
+ * Load event data from an iTip message attachment
+ */
+ public function itip_events($msgref)
+ {
+ $path = explode('/', $msgref);
+ $msg = array_pop($path);
+ $mbox = join('/', $path);
+ list($uid, $mime_id) = explode('#', $msg);
+ $events = [];
- /**
- * Magic getter for public access to protected members
- */
- public function __get($name)
- {
- switch ($name) {
- case 'ical':
- return $this->get_ical();
+ if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
+ $partstat = 'NEEDS-ACTION';
- case 'itip':
- return $this->load_itip();
+ $event['id'] = $event['uid'];
+ $event['temporary'] = true;
+ $event['readonly'] = true;
+ $event['calendar'] = '--invitation--itip';
+ $event['className'] = 'fc-invitation-' . strtolower($partstat);
+ $event['_mbox'] = $mbox;
+ $event['_uid'] = $uid;
+ $event['_part'] = $mime_id;
- case 'driver':
+ $events[] = $this->_client_event($event, true);
+
+ // add recurring instances
+ if (!empty($event['recurrence'])) {
+ // Some installations can't handle all occurrences (aborting the request w/o an error in log)
+ $freq = !empty($event['recurrence']['FREQ']) ? $event['recurrence']['FREQ'] : null;
+ $end = clone $event['start'];
+ $end->add(new DateInterval($freq == 'DAILY' ? 'P1Y' : 'P10Y'));
+
+ foreach ($this->driver->get_recurring_events($event, $event['start'], $end) as $recurring) {
+ $recurring['temporary'] = true;
+ $recurring['readonly'] = true;
+ $recurring['calendar'] = '--invitation--itip';
+
+ $events[] = $this->_client_event($recurring, true);
+ }
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Handler for keep-alive requests
+ * This will check for updated data in active calendars and sync them to the client
+ */
+ public function refresh($attr)
+ {
+ // refresh the entire calendar every 10th time to also sync deleted events
+ if (rand(0, 10) == 10) {
+ $this->rc->output->command('plugin.refresh_calendar', ['refetch' => true]);
+ return;
+ }
+
+ $counts = [];
+
+ foreach ($this->driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) {
+ $events = $this->driver->load_events(
+ rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC),
+ rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC),
+ rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
+ $cal['id'],
+ 1,
+ $attr['last']
+ );
+
+ foreach ($events as $event) {
+ $this->rc->output->command(
+ 'plugin.refresh_calendar',
+ ['source' => $cal['id'], 'update' => $this->_client_event($event)]
+ );
+ }
+
+ // refresh count for this calendar
+ if (!empty($cal['counts'])) {
+ $today = new DateTime('today 00:00:00', $this->timezone);
+ $counts += $this->driver->count_events($cal['id'], $today->format('U'));
+ }
+ }
+
+ if (!empty($counts)) {
+ $this->rc->output->command('plugin.update_counts', ['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();
- return $this->driver;
+
+ $time = !empty($p['time']) ? $p['time'] : time();
+
+ if ($alarms = $this->driver->pending_alarms($time)) {
+ foreach ($alarms as $alarm) {
+ $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
+ $p['alarms'][] = $alarm;
+ }
+ }
+
+ // get alarms for birthdays calendar
+ if (
+ $this->rc->config->get('calendar_contact_birthdays')
+ && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY'
+ ) {
+ $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db');
+
+ foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) {
+ $alarm = libcalendaring::get_next_alarm($e);
+
+ // overwrite alarm time with snooze value (or null if dismissed)
+ if ($dismissed = $cache->get($e['id'])) {
+ $alarm['time'] = $dismissed['notifyat'];
+ }
+
+ // add to list if alarm is set
+ if ($alarm && !empty($alarm['time']) && $alarm['time'] <= $time) {
+ $e['id'] = 'cal:bday:' . $e['id'];
+ $e['notifyat'] = $alarm['time'];
+ $p['alarms'][] = $e;
+ }
+ }
+ }
+
+ return $p;
}
- return null;
- }
+ /**
+ * Handler for alarm dismiss hook triggered by libcalendaring
+ */
+ public function dismiss_alarms($p)
+ {
+ $this->load_driver();
+ foreach ((array) $p['ids'] as $id) {
+ if (strpos($id, 'cal:bday:') === 0) {
+ $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']);
+ }
+ else if (strpos($id, 'cal:') === 0) {
+ $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);
+ }
+ }
+
+ return $p;
+ }
+
+ /**
+ * Handler for check-recent requests which are accidentally sent to calendar
+ */
+ function check_recent()
+ {
+ // NOP
+ $this->rc->output->send();
+ }
+
+ /**
+ * Hook triggered when a contact is saved
+ */
+ function contact_update($p)
+ {
+ // clear birthdays calendar cache
+ if (!empty($p['record']['birthday'])) {
+ $cache = $this->rc->get_cache('calendar.birthdays', 'db');
+ $cache->remove();
+ }
+ }
+
+ /**
+ *
+ */
+ function import_events()
+ {
+ // Upload progress update
+ if (!empty($_GET['_progress'])) {
+ $this->rc->upload_progress();
+ }
+
+ @set_time_limit(0);
+
+ // process uploaded file if there is no error
+ $err = $_FILES['_data']['error'];
+
+ if (!$err && !empty($_FILES['_data']['tmp_name'])) {
+ $calendar = rcube_utils::get_input_value('calendar', rcube_utils::INPUT_GPC);
+ $rangestart = !empty($_REQUEST['_range']) ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
+
+ // extract zip file
+ if ($_FILES['_data']['type'] == 'application/zip') {
+ $count = 0;
+ if (class_exists('ZipArchive', false)) {
+ $zip = new ZipArchive();
+ if ($zip->open($_FILES['_data']['tmp_name'])) {
+ $randname = uniqid('zip-' . session_id(), true);
+ $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;
+ mkdir($tmpdir, 0700);
+
+ // extract each ical file from the archive and import it
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $filename = $zip->getNameIndex($i);
+ if (preg_match('/\.ics$/i', $filename)) {
+ $tmpfile = $tmpdir . '/' . basename($filename);
+ if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) {
+ $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors);
+ unlink($tmpfile);
+ }
+ }
+ }
+
+ rmdir($tmpdir);
+ $zip->close();
+ }
+ else {
+ $errors = 1;
+ $msg = 'Failed to open zip file.';
+ }
+ }
+ else {
+ $errors = 1;
+ $msg = 'Zip files are not supported for import.';
+ }
+ }
+ else {
+ // attempt to import teh uploaded file directly
+ $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors);
+ }
+
+ if ($count) {
+ $this->rc->output->command('display_message', $this->gettext(['name' => 'importsuccess', 'vars' => ['nr' => $count]]), 'confirmation');
+ $this->rc->output->command('plugin.import_success', ['source' => $calendar, 'refetch' => true]);
+ }
+ else if (!$errors) {
+ $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
+ $this->rc->output->command('plugin.import_success', ['source' => $calendar]);
+ }
+ else {
+ $this->rc->output->command('plugin.import_error', ['message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')]);
+ }
+ }
+ else {
+ if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+ $max = $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize')));
+ $msg = $this->rc->gettext(['name' => 'filesizeerror', 'vars' => ['size' => $max]]);
+ }
+ else {
+ $msg = $this->rc->gettext('fileuploaderror');
+ }
+
+ $this->rc->output->command('plugin.import_error', ['message' => $msg]);
+ }
+
+ $this->rc->output->send('iframe');
+ }
+
+ /**
+ * Helper function to parse and import a single .ics file
+ */
+ private function import_from_file($filepath, $calendar, $rangestart, &$errors)
+ {
+ $user_email = $this->rc->user->get_username();
+ $ical = $this->get_ical();
+ $errors = !$ical->fopen($filepath);
+
+ $count = $i = 0;
+
+ foreach ($ical as $event) {
+ // keep the browser connection alive on long import jobs
+ if (++$i > 100 && $i % 100 == 0) {
+ echo "";
+ ob_flush();
+ }
+
+ // TODO: correctly handle recurring events which start before $rangestart
+ if ($rangestart && $event['end'] < $rangestart
+ && (empty($event['recurrence']) || (!empty($event['recurrence']['until']) && $event['recurrence']['until'] < $rangestart))
+ ) {
+ continue;
+ }
+
+ $event['_owner'] = $user_email;
+ $event['calendar'] = $calendar;
+
+ if ($this->driver->new_event($event)) {
+ $count++;
+ }
+ else {
+ $errors++;
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Construct the ics file for exporting events to iCalendar format;
+ */
+ function export_events($terminate = true)
+ {
+ $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
+ $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);
+ $event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
+ $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET);
+ $calid = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
+
+ if (!isset($start)) {
+ $start = 'today -1 year';
+ }
+ if (!is_numeric($start)) {
+ $start = strtotime($start . ' 00:00:00');
+ }
+ if (!$end) {
+ $end = 'today +10 years';
+ }
+ if (!is_numeric($end)) {
+ $end = strtotime($end . ' 23:59:59');
+ }
+
+ $filename = $calid;
+ $calendars = $this->driver->list_calendars();
+ $events = [];
+
+ if (!empty($calendars[$calid])) {
+ $filename = !empty($calendars[$calid]['name']) ? $calendars[$calid]['name'] : $calid;
+ $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
+
+ if (!empty($event_id)) {
+ if ($event = $this->driver->get_event(['calendar' => $calid, 'id' => $event_id], 0, true)) {
+ if (!empty($event['recurrence_id'])) {
+ $event = $this->driver->get_event(['calendar' => $calid, 'id' => $event['recurrence_id']], 0, true);
+ }
+
+ $events = [$event];
+ $filename = asciiwords($event['title']);
+
+ if (empty($filename)) {
+ $filename = 'event';
+ }
+ }
+ }
+ else {
+ $events = $this->driver->load_events($start, $end, null, $calid, 0);
+ if (empty($filename)) {
+ $filename = $calid;
+ }
+ }
+ }
+
+ header("Content-Type: text/calendar");
+ header("Content-Disposition: inline; filename=".$filename.'.ics');
+
+ $this->get_ical()->export($events, '', true, $attachments ? [$this->driver, 'get_attachment_body'] : null);
+
+ if ($terminate) {
+ exit;
+ }
+ }
+
+ /**
+ * Handler for iCal feed requests
+ */
+ function ical_feed_export()
+ {
+ $session_exists = !empty($_SESSION['user_id']);
+
+ // process HTTP auth info
+ if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
+ $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
+ $auth = $this->rc->plugins->exec_hook('authenticate', [
+ 'host' => $this->rc->autoselect_host(),
+ 'user' => trim($_SERVER['PHP_AUTH_USER']),
+ 'pass' => $_SERVER['PHP_AUTH_PW'],
+ 'cookiecheck' => true,
+ 'valid' => true,
+ ]);
+
+ if ($auth['valid'] && !$auth['abort']) {
+ $this->rc->login($auth['user'], $auth['pass'], $auth['host']);
+ }
+ }
+
+ // require HTTP auth
+ if (empty($_SESSION['user_id'])) {
+ header('WWW-Authenticate: Basic realm="Kolab Calendar"');
+ header('HTTP/1.0 401 Unauthorized');
+ exit;
+ }
+
+ // decode calendar feed hash
+ $format = 'ics';
+ $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET);
+
+ if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
+ $format = strtolower($m[1]);
+ $calhash = preg_replace($suff_regex, '', $calhash);
+ }
+
+ if (!strpos($calhash, ':')) {
+ $calhash = base64_decode($calhash);
+ }
+
+ list($user, $_GET['source']) = explode(':', $calhash, 2);
+
+ // sanity check user
+ if ($this->rc->user->get_username() == $user) {
+ $this->setup();
+ $this->load_driver();
+ $this->export_events(false);
+ }
+ else {
+ header('HTTP/1.0 404 Not Found');
+ }
+
+ // don't save session data
+ if (!$session_exists) {
+ session_destroy();
+ }
+
+ exit;
+ }
+
+ /**
+ *
+ */
+ function load_settings()
+ {
+ $this->lib->load_settings();
+ $this->defaults += $this->lib->defaults;
+
+ $settings = [];
+
+ // configuration
+ $settings['default_view'] = (string) $this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
+ $settings['timeslots'] = (int) $this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
+ $settings['first_day'] = (int) $this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
+ $settings['first_hour'] = (int) $this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
+ $settings['work_start'] = (int) $this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
+ $settings['work_end'] = (int) $this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
+ $settings['agenda_range'] = (int) $this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
+ $settings['event_coloring'] = (int) $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
+ $settings['time_indicator'] = (int) $this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
+ $settings['invite_shared'] = (int) $this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+ $settings['itip_notify'] = (int) $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+ $settings['show_weekno'] = (int) $this->rc->config->get('calendar_show_weekno', $this->defaults['calendar_show_weekno']);
+ $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
+ $settings['invitation_calendars'] = (bool) $this->rc->config->get('kolab_invitation_calendars', false);
+
+ // 'table' view has been replaced by 'list' view
+ if ($settings['default_view'] == 'table') {
+ $settings['default_view'] = 'list';
+ }
+
+ // get user identity to create default attendee
+ if ($this->ui->screen == 'calendar') {
+ foreach ($this->rc->user->list_emails() as $rec) {
+ if (empty($identity)) {
+ $identity = $rec;
+ }
+
+ $identity['emails'][] = $rec['email'];
+ $settings['identities'][$rec['identity_id']] = $rec['email'];
+ }
+
+ $identity['emails'][] = $this->rc->user->get_username();
+ $settings['identity'] = [
+ 'name' => $identity['name'],
+ 'email' => strtolower($identity['email']),
+ 'emails' => ';' . strtolower(join(';', $identity['emails']))
+ ];
+ }
+
+ // freebusy token authentication URL
+ if (($url = $this->rc->config->get('calendar_freebusy_session_auth_url'))
+ && ($uniqueid = $this->rc->config->get('kolab_uniqueid'))
+ ) {
+ if ($url === true) {
+ $url = '/freebusy';
+ }
+ $url = rtrim(rcube_utils::resolve_url($url), '/ ');
+ $url .= '/' . urlencode($this->rc->get_user_name());
+ $url .= '/' . urlencode($uniqueid);
+
+ $settings['freebusy_url'] = $url;
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Encode events as JSON
+ *
+ * @param array Events as array
+ * @param bool Add CSS class names according to calendar and categories
+ *
+ * @return string JSON encoded events
+ */
+ function encode($events, $addcss = false)
+ {
+ $json = [];
+ foreach ($events as $event) {
+ $json[] = $this->_client_event($event, $addcss);
+ }
+ return rcube_output::json_serialize($json);
+ }
+
+ /**
+ * Convert an event object to be used on the client
+ */
+ private function _client_event($event, $addcss = false)
+ {
+ // compose a human readable strings for alarms_text and recurrence_text
+ if (!empty($event['valarms'])) {
+ $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']);
+ $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']);
+ }
+
+ if (!empty($event['recurrence'])) {
+ $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
+ $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
+ unset($event['recurrence_date']);
+ }
+
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $k => $attachment) {
+ $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
+
+ unset($event['attachments'][$k]['data'], $event['attachments'][$k]['content']);
+
+ if (empty($attachment['id'])) {
+ $event['attachments'][$k]['id'] = $k;
+ }
+ }
+ }
+
+ // convert link URIs references into structs
+ if (array_key_exists('links', $event)) {
+ foreach ((array) $event['links'] as $i => $link) {
+ if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) {
+ $event['links'][$i] = $msgref;
+ }
+ }
+ }
+
+ // check for organizer in attendees list
+ $organizer = null;
+ foreach ((array) $event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ if (!empty($attendee['status']) && $attendee['status'] == 'DELEGATED' && empty($attendee['rsvp'])) {
+ $event['attendees'][$i]['noreply'] = true;
+ }
+ else {
+ unset($event['attendees'][$i]['noreply']);
+ }
+ }
+
+ if ($organizer === null && !empty($event['organizer'])) {
+ $organizer = $event['organizer'];
+ $organizer['role'] = 'ORGANIZER';
+ if (!is_array($event['attendees']))
+ $event['attendees'] = [$organizer];
+ }
+
+ // Convert HTML description into plain text
+ if ($this->is_html($event)) {
+ $h2t = new rcube_html2text($event['description'], false, true, 0);
+ $event['description'] = trim($h2t->get_text());
+ }
+
+ // mapping url => vurl, allday => allDay because of the fullcalendar client script
+ $event['vurl'] = $event['url'];
+ $event['allDay'] = !empty($event['allday']);
+ unset($event['url']);
+ unset($event['allday']);
+
+ $event['className'] = !empty($event['className']) ? explode(' ', $event['className']) : [];
+
+ if ($event['allDay']) {
+ $event['end'] = $event['end']->add(new DateInterval('P1D'));
+ }
+
+ if (!empty($_GET['mode']) && $_GET['mode'] == 'print') {
+ $event['editable'] = false;
+ }
+
+ return [
+ '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
+ 'start' => $this->lib->adjust_timezone($event['start'], $event['allDay'])->format('c'),
+ 'end' => $this->lib->adjust_timezone($event['end'], $event['allDay'])->format('c'),
+ // 'changed' might be empty for event recurrences (Bug #2185)
+ 'changed' => !empty($event['changed']) ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
+ 'created' => !empty($event['created']) ? $this->lib->adjust_timezone($event['created'])->format('c') : null,
+ 'title' => strval($event['title']),
+ 'description' => strval($event['description']),
+ 'location' => strval($event['location']),
+ ] + $event;
+ }
+
+ /**
+ * 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));
+ }
+
+ /**
+ * TEMPORARY: generate random event data for testing
+ * Create events by opening http:///?_task=calendar&_action=randomdata&_num=500&_date=2014-08-01&_dev=120
+ */
+ public function generate_randomdata()
+ {
+ @set_time_limit(0);
+
+ $num = !empty($_REQUEST['_num']) ? intval($_REQUEST['_num']) : 100;
+ $date = !empty($_REQUEST['_date']) ? $_REQUEST['_date'] : 'now';
+ $dev = !empty($_REQUEST['_dev']) ? $_REQUEST['_dev'] : 30;
+ $cats = array_keys($this->driver->list_categories());
+ $cals = $this->driver->list_calendars(calendar_driver::FILTER_ACTIVE);
+ $count = 0;
+
+ while ($count++ < $num) {
+ $spread = intval($dev) * 86400; // days
+ $refdate = strtotime($date);
+ $start = round(($refdate + rand(-$spread, $spread)) / 600) * 600;
+ $duration = round(rand(30, 360) / 30) * 30 * 60;
+ $allday = rand(0,20) > 18;
+ $alarm = rand(-30,12) * 5;
+ $fb = rand(0,2);
+
+ if (date('G', $start) > 23) {
+ $start -= 3600;
+ }
+
+ if ($allday) {
+ $start = strtotime(date('Y-m-d 00:00:00', $start));
+ $duration = 86399;
+ }
+
+ $title = '';
+ $len = rand(2, 12);
+ $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962."
+ . " It is a technique which can be used to isolate features of a particular shape within an image."
+ . " Because it requires that the desired features be specified in some parametric form, the classical"
+ . " Hough transform is most commonly used for the de- tection of regular curves such as lines, circles,"
+ . " ellipses, etc. A generalized Hough transform can be employed in applications where a simple"
+ . " analytic description of a feature(s) is not possible. Due to the computational complexity of"
+ . " the generalized Hough algorithm, we restrict the main focus of this discussion to the classical"
+ . " Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter"
+ . " referred to without the classical prefix ) retains many applications, as most manufac- tured"
+ . " parts (and many anatomical parts investigated in medical imagery) contain feature boundaries"
+ . " which can be described by regular curves. The main advantage of the Hough transform technique"
+ . " is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected"
+ . " by image noise.");
+ // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
+ for ($i = 0; $i < $len; $i++) {
+ $title .= $words[rand(0,count($words)-1)] . " ";
+ }
+
+ $this->driver->new_event([
+ 'uid' => $this->generate_uid(),
+ 'start' => new DateTime('@'.$start),
+ 'end' => new DateTime('@'.($start + $duration)),
+ 'allday' => $allday,
+ 'title' => rtrim($title),
+ 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
+ 'categories' => $cats[array_rand($cats)],
+ 'calendar' => array_rand($cals),
+ 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
+ 'priority' => rand(0,9),
+ ]);
+ }
+
+ $this->rc->output->redirect('');
+ }
+
+ /**
+ * Handler for attachments upload
+ */
+ public function attachment_upload()
+ {
+ $handler = new kolab_attachments_handler();
+ $handler->attachment_upload(self::SESSION_KEY, 'cal-');
+ }
+
+ /**
+ * Handler for attachments download/displaying
+ */
+ public function attachment_get()
+ {
+ $handler = new kolab_attachments_handler();
+
+ // show loading page
+ if (!empty($_GET['_preload'])) {
+ return $handler->attachment_loading_page();
+ }
+
+ $event_id = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC);
+ $calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC);
+ $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
+
+ $event = ['id' => $event_id, 'calendar' => $calendar, 'rev' => $rev];
+
+ if ($calendar == '--invitation--itip') {
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GPC);
+ $part = rcube_utils::get_input_value('_part', rcube_utils::INPUT_GPC);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC);
+
+ $event = $this->lib->mail_get_itip_object($mbox, $uid, $part, 'event');
+ $attachment = $event['attachments'][$id];
+ $attachment['body'] = &$attachment['data'];
+ }
+ else {
+ $attachment = $this->driver->get_attachment($id, $event);
+ }
+
+ // show part page
+ if (!empty($_GET['_frame'])) {
+ $handler->attachment_page($attachment);
+ }
+ // deliver attachment content
+ else if ($attachment) {
+ if ($calendar != '--invitation--itip') {
+ $attachment['body'] = $this->driver->get_attachment_body($id, $event);
+ }
+
+ $handler->attachment_get($attachment);
+ }
+
+ // if we arrive here, the requested part was not found
+ header('HTTP/1.1 404 Not Found');
+ exit;
+ }
+
+ /**
+ * Determine whether the given event description is HTML formatted
+ */
+ private function is_html($event)
+ {
+ // check for opening and closing or tags
+ return preg_match('/<(html|body)(\s+[a-z]|>)/', $event['description'], $m)
+ && strpos($event['description'], ''.$m[1].'>') > 0;
+ }
+
+ /**
+ * Prepares new/edited event properties before save
+ */
+ private function write_preprocess(&$event, $action)
+ {
+ // Remove double timezone specification (T2313)
+ $event['start'] = preg_replace('/\s*\(.*\)/', '', $event['start']);
+ $event['end'] = preg_replace('/\s*\(.*\)/', '', $event['end']);
+
+ // convert dates into DateTime objects in user's current timezone
+ $event['start'] = new DateTime($event['start'], $this->timezone);
+ $event['end'] = new DateTime($event['end'], $this->timezone);
+ $event['allday'] = !empty($event['allDay']);
+ unset($event['allDay']);
+
+ // start/end is all we need for 'move' action (#1480)
+ if ($action == 'move') {
+ return true;
+ }
+
+ // convert the submitted recurrence settings
+ if (!empty($event['recurrence'])) {
+ $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);
+
+ // align start date with the first occurrence
+ if (!empty($event['recurrence']) && !empty($event['syncstart'])
+ && (empty($event['_savemode']) || $event['_savemode'] == 'all')
+ ) {
+ $next = $this->find_first_occurrence($event);
+
+ if (!$next) {
+ $this->rc->output->show_message('calendar.recurrenceerror', 'error');
+ return false;
+ }
+ else if ($event['start'] != $next) {
+ $diff = $event['start']->diff($event['end'], true);
+
+ $event['start'] = $next;
+ $event['end'] = clone $next;
+ $event['end']->add($diff);
+ }
+ }
+ }
+
+ // convert the submitted alarm values
+ if (!empty($event['valarms'])) {
+ $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']);
+ }
+
+ $attachments = [];
+ $eventid = 'cal-' . (!empty($event['id']) ? $event['id'] : 'new-event');
+
+ if (!empty($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
+ if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
+ foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
+ if (!empty($event['attachments']) && in_array($id, $event['attachments'])) {
+ $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
+ }
+ }
+ }
+ }
+
+ $event['attachments'] = $attachments;
+
+ // convert link references into simple URIs
+ if (array_key_exists('links', $event)) {
+ $event['links'] = array_map(function($link) {
+ return is_array($link) ? $link['uri'] : strval($link);
+ },
+ (array) $event['links']
+ );
+ }
+
+ // check for organizer in attendees
+ if ($action == 'new' || $action == 'edit') {
+ if (empty($event['attendees'])) {
+ $event['attendees'] = [];
+ }
+
+ $emails = $this->get_user_emails();
+ $organizer = $owner = false;
+
+ foreach ((array) $event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $i;
+ }
+ if (!empty($attendee['email']) && in_array(strtolower($attendee['email']), $emails)) {
+ $owner = $i;
+ }
+ if (!isset($attendee['rsvp'])) {
+ $event['attendees'][$i]['rsvp'] = true;
+ }
+ else if (is_string($attendee['rsvp'])) {
+ $event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
+ }
+ }
+
+ if (!empty($event['_identity'])) {
+ $identity = $this->rc->user->get_identity($event['_identity']);
+ }
+
+ // set new organizer identity
+ if ($organizer !== false && $identity) {
+ $event['attendees'][$organizer]['name'] = $identity['name'];
+ $event['attendees'][$organizer]['email'] = $identity['email'];
+ }
+ // set owner as organizer if yet missing
+ else if ($organizer === false && $owner !== false) {
+ $event['attendees'][$owner]['role'] = 'ORGANIZER';
+ unset($event['attendees'][$owner]['rsvp']);
+ }
+ // fallback to the selected identity
+ else if ($organizer === false && $identity) {
+ $event['attendees'][] = [
+ 'role' => 'ORGANIZER',
+ 'name' => $identity['name'],
+ 'email' => $identity['email'],
+ ];
+ }
+ }
+
+ // mapping url => vurl because of the fullcalendar client script
+ if (array_key_exists('vurl', $event)) {
+ $event['url'] = $event['vurl'];
+ unset($event['vurl']);
+ }
+
+ return true;
+ }
+
+ /**
+ * Releases some resources after successful event save
+ */
+ private function cleanup_event(&$event)
+ {
+ // remove temp. attachment files
+ if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
+ $this->rc->plugins->exec_hook('attachments_cleanup', ['group' => $eventid]);
+ $this->rc->session->remove(self::SESSION_KEY);
+ }
+ }
+
+ /**
+ * Send out an invitation/notification to all event attendees
+ */
+ private function notify_attendees($event, $old, $action = 'edit', $comment = null, $rsvp = null)
+ {
+ $is_cancelled = false;
+ if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) {
+ $event['cancelled'] = true;
+ $is_cancelled = true;
+ }
+
+ if ($rsvp === null) {
+ $rsvp = !$old || $event['sequence'] > $old['sequence'];
+ }
+
+ $itip = $this->load_itip();
+ $emails = $this->get_user_emails();
+ $itip_notify = (int) $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+
+ // add comment to the iTip attachment
+ $event['comment'] = $comment;
+
+ // set a valid recurrence-id if this is a recurrence instance
+ libcalendaring::identify_recurrence_instance($event);
+
+ // compose multipart message using PEAR:Mail_Mime
+ $method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
+ $message = $itip->compose_itip_message($event, $method, $rsvp);
+
+ // list existing attendees from $old event
+ $old_attendees = [];
+ if (!empty($old['attendees'])) {
+ foreach ((array) $old['attendees'] as $attendee) {
+ $old_attendees[] = $attendee['email'];
+ }
+ }
+
+ // send to every attendee
+ $sent = 0;
+ $current = [];
+ foreach ((array) $event['attendees'] as $attendee) {
+ // skip myself for obvious reasons
+ if (empty($attendee['email']) || in_array(strtolower($attendee['email']), $emails)) {
+ continue;
+ }
+
+ $current[] = strtolower($attendee['email']);
+
+ // skip if notification is disabled for this attendee
+ if (!empty($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 || $event['sequence'] > $old['sequence'];
+ $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
+ $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty'));
+
+ $event['comment'] = $comment;
+
+ // finally send the message
+ if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp)) {
+ $sent++;
+ }
+ else {
+ $sent = -100;
+ }
+ }
+
+ // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
+
+ // send CANCEL message to removed attendees
+ if (!empty($old['attendees'])) {
+ foreach ($old['attendees'] as $attendee) {
+ if ($attendee['role'] == 'ORGANIZER'
+ || empty($attendee['email'])
+ || in_array(strtolower($attendee['email']), $current)
+ ) {
+ continue;
+ }
+
+ $vevent = $old;
+ $vevent['cancelled'] = $is_cancelled;
+ $vevent['attendees'] = [$attendee];
+ $vevent['comment'] = $comment;
+
+ if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody')) {
+ $sent++;
+ }
+ else {
+ $sent = -100;
+ }
+ }
+ }
+
+ return $sent;
+ }
+
+ /**
+ * Echo simple free/busy status text for the given user and time range
+ */
+ public function freebusy_status()
+ {
+ $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
+ $start = $this->input_timestamp('start', rcube_utils::INPUT_GPC);
+ $end = $this->input_timestamp('end', rcube_utils::INPUT_GPC);
+
+ if (!$start) $start = time();
+ if (!$end) $end = $start + 3600;
+
+ $status = 'UNKNOWN';
+ $fbtypemap = [
+ calendar::FREEBUSY_UNKNOWN => 'UNKNOWN',
+ calendar::FREEBUSY_FREE => 'FREE',
+ calendar::FREEBUSY_BUSY => 'BUSY',
+ calendar::FREEBUSY_TENTATIVE => 'TENTATIVE',
+ calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE'
+ ];
+
+ // if the backend has free-busy information
+ $fblist = $this->driver->get_freebusy_list($email, $start, $end);
+
+ if (is_array($fblist)) {
+ $status = 'FREE';
+
+ foreach ($fblist as $slot) {
+ list($from, $to, $type) = $slot;
+ if ($from < $end && $to > $start) {
+ $status = isset($type) && !empty($fbtypemap[$type]) ? $fbtypemap[$type] : 'BUSY';
+ break;
+ }
+ }
+ }
+
+ // let this information be cached for 5min
+ $this->rc->output->future_expire_header(300);
+
+ echo $status;
+ exit;
+ }
+
+ /**
+ * Return a list of free/busy time slots within the given period
+ * Echo data in JSON encoding
+ */
+ public function freebusy_times()
+ {
+ $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
+ $start = $this->input_timestamp('start', rcube_utils::INPUT_GPC);
+ $end = $this->input_timestamp('end', rcube_utils::INPUT_GPC);
+ $interval = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC));
+ $strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
+
+ if (!$start) $start = time();
+ if (!$end) $end = $start + 86400 * 30;
+ if (!$interval) $interval = 60; // 1 hour
+
+ if (!$dte) {
+ $dts = new DateTime('@'.$start);
+ $dts->setTimezone($this->timezone);
+ }
+
+ $fblist = $this->driver->get_freebusy_list($email, $start, $end);
+ $slots = '';
+
+ // prepare freebusy list before use (for better performance)
+ if (is_array($fblist)) {
+ foreach ($fblist as $idx => $slot) {
+ list($from, $to, ) = $slot;
+
+ // check for possible all-day times
+ if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') {
+ // shift into the user's timezone for sane matching
+ $fblist[$idx][0] -= $this->gmt_offset;
+ $fblist[$idx][1] -= $this->gmt_offset;
+ }
+ }
+ }
+
+ // build a list from $start till $end with blocks representing the fb-status
+ for ($s = 0, $t = $start; $t <= $end; $s++) {
+ $t_end = $t + $interval * 60;
+ $dt = new DateTime('@'.$t);
+ $dt->setTimezone($this->timezone);
+
+ // determine attendee's status
+ if (is_array($fblist)) {
+ $status = self::FREEBUSY_FREE;
+
+ foreach ($fblist as $slot) {
+ list($from, $to, $type) = $slot;
+
+ if ($from < $t_end && $to > $t) {
+ $status = isset($type) ? $type : self::FREEBUSY_BUSY;
+ if ($status == self::FREEBUSY_BUSY) {
+ // can't get any worse :-)
+ break;
+ }
+ }
+ }
+ }
+ else {
+ $status = self::FREEBUSY_UNKNOWN;
+ }
+
+ // use most compact format, assume $status is one digit/character
+ $slots .= $status;
+ $t = $t_end;
+ }
+
+ $dte = new DateTime('@'.$t_end);
+ $dte->setTimezone($this->timezone);
+
+ // let this information be cached for 5min
+ $this->rc->output->future_expire_header(300);
+
+ echo rcube_output::json_serialize([
+ 'email' => $email,
+ 'start' => $dts->format('c'),
+ 'end' => $dte->format('c'),
+ 'interval' => $interval,
+ 'slots' => $slots,
+ ]);
+ exit;
+ }
+
+ /**
+ * Handler for printing calendars
+ */
+ public function print_view()
+ {
+ $title = $this->gettext('print');
+
+ $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
+ if (!in_array($view, ['agendaWeek', 'agendaDay', 'month', 'list'])) {
+ $view = 'agendaDay';
+ }
+
+ $this->rc->output->set_env('view', $view);
+
+ if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) {
+ $this->rc->output->set_env('date', $date);
+ }
+
+ if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC)) {
+ $this->rc->output->set_env('listRange', intval($range));
+ }
+
+ if ($search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC)) {
+ $this->rc->output->set_env('search', $search);
+ $title .= ' "' . $search . '"';
+ }
+
+ // Add JS to the page
+ $this->ui->addJS();
+
+ $this->register_handler('plugin.calendar_css', [$this->ui, 'calendar_css']);
+ $this->register_handler('plugin.calendar_list', [$this->ui, 'calendar_list']);
+
+ $this->rc->output->set_pagetitle($title);
+ $this->rc->output->send('calendar.print');
+ }
+
+ /**
+ * Compare two event objects and return differing properties
+ *
+ * @param array Event A
+ * @param array Event B
+ *
+ * @return array List of differing event properties
+ */
+ public static function event_diff($a, $b)
+ {
+ $diff = [];
+ $ignore = ['changed' => 1, 'attachments' => 1];
+
+ foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
+ if (empty($ignore[$key]) && $key[0] != '_') {
+ $av = isset($a[$key]) ? $a[$key] : null;
+ $bv = isset($b[$key]) ? $b[$key] : null;
+
+ if ($av != $bv) {
+ $diff[] = $key;
+ }
+ }
+ }
+
+ // only compare number of attachments
+ $ac = !empty($a['attachments']) ? count($a['attachments']) : 0;
+ $bc = !empty($b['attachments']) ? count($b['attachments']) : 0;
+
+ if ($ac != $bc) {
+ $diff[] = 'attachments';
+ }
+
+ return $diff;
+ }
+
+ /**
+ * Update attendee properties on the given event object
+ *
+ * @param array The event object to be altered
+ * @param array List of hash arrays each represeting an updated/added attendee
+ */
+ public static function merge_attendee_data(&$event, $attendees, $removed = null)
+ {
+ if (!empty($attendees) && !is_array($attendees[0])) {
+ $attendees = [$attendees];
+ }
+
+ foreach ($attendees as $attendee) {
+ $found = false;
+
+ foreach ($event['attendees'] as $i => $candidate) {
+ if ($candidate['email'] == $attendee['email']) {
+ $event['attendees'][$i] = $attendee;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $event['attendees'][] = $attendee;
+ }
+ }
+
+ // filter out removed attendees
+ if (!empty($removed)) {
+ $event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) {
+ return !in_array($attendee['email'], $removed);
+ });
+ }
+ }
+
+ /**** Resource management functions ****/
+
+ /**
+ * Getter for the configured implementation of the resource directory interface
+ */
+ private function resources_directory()
+ {
+ if (!empty($this->resources_dir)) {
+ return $this->resources_dir;
+ }
+
+ if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
+ $driver_class = 'resources_driver_' . $driver_name;
+
+ require_once($this->home . '/drivers/resources_driver.php');
+ require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+
+ $this->resources_dir = new $driver_class($this);
+ }
+
+ return $this->resources_dir;
+ }
+
+ /**
+ * Handler for resoruce autocompletion requests
+ */
+ public function resources_autocomplete()
+ {
+ $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
+ $sid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
+ $maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
+ $results = [];
+
+ if ($directory = $this->resources_directory()) {
+ foreach ($directory->load_resources($search, $maxnum) as $rec) {
+ $results[] = [
+ 'name' => $rec['name'],
+ 'email' => $rec['email'],
+ 'type' => $rec['_type'],
+ ];
+ }
+ }
+
+ $this->rc->output->command('ksearch_query_results', $results, $search, $sid);
+ $this->rc->output->send();
+ }
+
+ /**
+ * Handler for load-requests for resource data
+ */
+ function resources_list()
+ {
+ $data = [];
+
+ if ($directory = $this->resources_directory()) {
+ foreach ($directory->load_resources() as $rec) {
+ $data[] = $rec;
+ }
+ }
+
+ $this->rc->output->command('plugin.resource_data', $data);
+ $this->rc->output->send();
+ }
+
+ /**
+ * Handler for requests loading resource owner information
+ */
+ function resources_owner()
+ {
+ if ($directory = $this->resources_directory()) {
+ $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $data = $directory->get_resource_owner($id);
+ }
+
+ $this->rc->output->command('plugin.resource_owner', $data);
+ $this->rc->output->send();
+ }
+
+ /**
+ * Deliver event data for a resource's calendar
+ */
+ function resources_calendar()
+ {
+ $events = [];
+
+ if ($directory = $this->resources_directory()) {
+ $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $start = $this->input_timestamp('start', rcube_utils::INPUT_GET);
+ $end = $this->input_timestamp('end', rcube_utils::INPUT_GET);
+
+ $events = $directory->get_resource_calendar($id, $start, $end);
+ }
+
+ echo $this->encode($events);
+ exit;
+ }
+
+ /**** Event invitation plugin hooks ****/
+
+ /**
+ * Find an event in user calendars
+ */
+ protected function find_event($event, &$mode)
+ {
+ $this->load_driver();
+
+ // We search for writeable calendars in personal namespace by default
+ $mode = calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL;
+ $result = $this->driver->get_event($event, $mode);
+ // ... now check shared folders if not found
+ if (!$result) {
+ $result = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_SHARED);
+ if ($result) {
+ $mode |= calendar_driver::FILTER_SHARED;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ function event_itip_status()
+ {
+ $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+
+ $this->load_driver();
+
+ // find local copy of the referenced event (in personal namespace)
+ $existing = $this->find_event($data, $mode);
+ $is_shared = $mode & calendar_driver::FILTER_SHARED;
+ $itip = $this->load_itip();
+ $response = $itip->get_itip_status($data, $existing);
+
+ // get a list of writeable calendars to save new events to
+ if (
+ (!$existing || $is_shared)
+ && empty($data['nosave'])
+ && ($response['action'] == 'rsvp' || $response['action'] == 'import')
+ ) {
+ $calendars = $this->driver->list_calendars($mode);
+ $calendar_select = new html_select([
+ 'name' => 'calendar',
+ 'id' => 'itip-saveto',
+ 'is_escaped' => true,
+ 'class' => 'form-control custom-select'
+ ]);
+
+ $calendar_select->add('--', '');
+ $numcals = 0;
+ foreach ($calendars as $calendar) {
+ if (!empty($calendar['editable'])) {
+ $calendar_select->add($calendar['name'], $calendar['id']);
+ $numcals++;
+ }
+ }
+ if ($numcals < 1) {
+ $calendar_select = null;
+ }
+ }
+
+ if (!empty($calendar_select)) {
+ $default_calendar = $this->get_default_calendar($data['sensitivity'], $calendars);
+ $response['select'] = html::span('folder-select', $this->gettext('saveincalendar')
+ . ' '
+ . $calendar_select->show($is_shared ? $existing['calendar'] : $default_calendar['id'])
+ );
+ }
+ else if (!empty($data['nosave'])) {
+ $response['select'] = html::tag('input', ['type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => '']);
+ }
+
+ // render small agenda view for the respective day
+ if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') {
+ $event_start = rcube_utils::anytodatetime($data['date']);
+ $day_start = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone);
+ $day_end = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone);
+
+ // get events on that day from the user's personal calendars
+ $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
+ $events = $this->driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars));
+
+ usort($events, function($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; });
+
+ $before = $after = [];
+ foreach ($events as $event) {
+ // TODO: skip events with free_busy == 'free' ?
+ if ($event['uid'] == $data['uid']
+ || $event['end'] < $day_start || $event['start'] > $day_end
+ || $event['status'] == 'CANCELLED'
+ || (!empty($event['className']) && strpos($event['className'], 'declined') !== false)
+ ) {
+ continue;
+ }
+
+ if ($event['start'] < $event_start) {
+ $before[] = $this->mail_agenda_event_row($event);
+ }
+ else {
+ $after[] = $this->mail_agenda_event_row($event);
+ }
+ }
+
+ $response['append'] = [
+ 'selector' => '.calendar-agenda-preview',
+ 'replacements' => [
+ '%before%' => !empty($before) ? join("\n", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')),
+ '%after%' => !empty($after) ? join("\n", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')),
+ ],
+ ];
+ }
+
+ $this->rc->output->command('plugin.update_itip_object_status', $response);
+ }
+
+ /**
+ * Handler for calendar/itip-remove requests
+ */
+ function event_itip_remove()
+ {
+ $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
+ $listmode = calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL;
+ $success = false;
+
+ // search for event if only UID is given
+ if ($event = $this->driver->get_event(['uid' => $uid, '_instance' => $instance], $listmode)) {
+ $event['_savemode'] = $savemode;
+ $success = $this->driver->remove_event($event, true);
+ }
+
+ if ($success) {
+ $this->rc->output->show_message('calendar.successremoval', 'confirmation');
+ }
+ else {
+ $this->rc->output->show_message('calendar.errorsaving', 'error');
+ }
+ }
+
+ /**
+ * Handler for URLs that allow an invitee to respond on his invitation mail
+ */
+ public function itip_attend_response($p)
+ {
+ $this->setup();
+
+ if ($p['action'] == 'attend') {
+ $this->ui->init();
+
+ $this->rc->output->set_env('task', 'calendar'); // override some env vars
+ $this->rc->output->set_env('refresh_interval', 0);
+ $this->rc->output->set_pagetitle($this->gettext('calendar'));
+
+ $itip = $this->load_itip();
+ $token = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
+
+ // read event info stored under the given token
+ if ($invitation = $itip->get_invitation($token)) {
+ $this->token = $token;
+ $this->event = $invitation['event'];
+
+ // show message about cancellation
+ if (!empty($invitation['cancelled'])) {
+ $this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled'));
+ }
+ // save submitted RSVP status
+ else if (!empty($_POST['rsvp'])) {
+ $status = null;
+ foreach (['accepted', 'tentative', 'declined'] as $method) {
+ if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) {
+ $status = $method;
+ break;
+ }
+ }
+
+ // send itip reply to organizer
+ $invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+ if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
+ $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave'.strtolower($status)));
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
+ }
+
+ // if user is logged in...
+ // FIXME: we should really consider removing this functionality
+ // it's confusing that it creates/updates an event only for logged-in user
+ // what if the logged-in user is not the same as the attendee?
+ if ($this->rc->user->ID) {
+ $this->load_driver();
+
+ $invitation = $itip->get_invitation($token);
+ $existing = $this->driver->get_event($this->event);
+
+ // save the event to his/her default calendar if not yet present
+ if (!$existing && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) {
+ $invitation['event']['calendar'] = $calendar['id'];
+ if ($this->driver->new_event($invitation['event'])) {
+ $msg = $this->gettext(['name' => 'importedsuccessfully', 'vars' => ['calendar' => $calendar['name']]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
+ }
+ }
+ else if ($existing
+ && ($this->event['sequence'] >= $existing['sequence']
+ || $this->event['changed'] >= $existing['changed'])
+ && ($calendar = $this->driver->get_calendar($existing['calendar']))
+ ) {
+ $this->event = $invitation['event'];
+ $this->event['id'] = $existing['id'];
+
+ unset($this->event['comment']);
+
+ // merge attendees status
+ // e.g. preserve my participant status for regular updates
+ $this->lib->merge_attendees($this->event, $existing, $status);
+
+ // update attachments list
+ $event['deleted_attachments'] = true;
+
+ // show me as free when declined (#1670)
+ if ($status == 'declined') {
+ $this->event['free_busy'] = 'free';
+ }
+
+ if ($this->driver->edit_event($this->event)) {
+ $msg = $this->gettext(['name' => 'updatedsuccessfully', 'vars' => ['calendar' => $calendar->get_name()]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
+ }
+ }
+ }
+ }
+
+ $this->register_handler('plugin.event_inviteform', [$this, 'itip_event_inviteform']);
+ $this->register_handler('plugin.event_invitebox', [$this->ui, 'event_invitebox']);
+
+ if (empty($this->invitestatus)) {
+ $this->itip->set_rsvp_actions(['accepted', 'tentative', 'declined']);
+ $this->register_handler('plugin.event_rsvp_buttons', [$this->ui, 'event_rsvp_buttons']);
+ }
+
+ $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
+ }
+
+ $this->rc->output->send('calendar.itipattend');
+ }
+ }
+
+ /**
+ *
+ */
+ public function itip_event_inviteform($attrib)
+ {
+ $hidden = new html_hiddenfield(['name' => "_t", 'value' => $this->token]);
+
+ return html::tag('form', [
+ 'action' => $this->rc->url(['task' => 'calendar', 'action' => 'attend']),
+ 'method' => 'post',
+ 'noclose' => true
+ ] + $attrib
+ ) . $hidden->show();
+ }
+
+ /**
+ *
+ */
+ private function mail_agenda_event_row($event, $class = '')
+ {
+ $time = !empty($event['allday']) ? $this->gettext('all-day') :
+ $this->rc->format_date($event['start'], $this->rc->config->get('time_format'))
+ . ' - ' .
+ $this->rc->format_date($event['end'], $this->rc->config->get('time_format'));
+
+ return html::div(rtrim('event-row ' . ($class ?: $event['className'])),
+ html::span('event-date', $time)
+ . html::span('event-title', rcube::Q($event['title']))
+ );
+ }
+
+ /**
+ *
+ */
+ public function mail_messages_list($p)
+ {
+ if (!empty($p['cols']) && in_array('attachment', (array) $p['cols']) && !empty($p['messages'])) {
+ foreach ($p['messages'] as $header) {
+ $part = new StdClass;
+ $part->mimetype = $header->ctype;
+
+ if (libcalendaring::part_is_vcalendar($part)) {
+ $header->list_flags['attachmentClass'] = 'ical';
+ }
+ else if (in_array($header->ctype, ['multipart/alternative', 'multipart/mixed'])) {
+ // TODO: fetch bodystructure and search for ical parts. Maybe too expensive?
+ if (!empty($header->structure) && !empty($header->structure->parts)) {
+ foreach ($header->structure->parts as $part) {
+ if (libcalendaring::part_is_vcalendar($part)
+ && !empty($part->ctype_parameters['method'])
+ ) {
+ $header->list_flags['attachmentClass'] = 'ical';
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Add UI element to copy event invitations or updates to the calendar
+ */
+ 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_events = false;
+ $ical_objects = $this->lib->get_mail_ical_objects();
+
+ // show a box for every event in the file
+ foreach ($ical_objects as $idx => $event) {
+ if ($event['_type'] != 'event') {
+ // skip non-event objects (#2928)
+ continue;
+ }
+
+ $has_events = true;
+
+ // get prepared inline UI for this event object
+ if ($ical_objects->method) {
+ $append = '';
+ $date_str = $this->rc->format_date($event['start'], $this->rc->config->get('date_format'), empty($event['start']->_dateonly));
+ $date = new DateTime($event['start']->format('Y-m-d') . ' 12:00:00', new DateTimeZone('UTC'));
+
+ // prepare a small agenda preview to be filled with actual event data on async request
+ if ($ical_objects->method == 'REQUEST') {
+ $append = html::div('calendar-agenda-preview',
+ html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' . html::span('date', $date_str))
+ . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%'
+ );
+ }
+
+ $html .= html::div('calendar-invitebox invitebox boxinformation',
+ $this->itip->mail_itip_inline_ui(
+ $event,
+ $ical_objects->method,
+ $ical_objects->mime_id . ':' . $idx,
+ 'calendar',
+ rcube_utils::anytodatetime($ical_objects->message_date),
+ $this->rc->url(['task' => 'calendar']) . '&view=agendaDay&date=' . $date->format('U')
+ ) . $append
+ );
+ }
+
+ // limit listing
+ if ($idx >= 3) {
+ break;
+ }
+ }
+
+ // prepend event boxes to message body
+ if ($html) {
+ $this->ui->init();
+ $p['content'] = $html . $p['content'];
+ $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
+ }
+
+ // add "Save to calendar" button into attachment menu
+ if ($has_events) {
+ $this->add_button([
+ 'id' => 'attachmentsavecal',
+ 'name' => 'attachmentsavecal',
+ 'type' => 'link',
+ 'wrapper' => 'li',
+ 'command' => 'attachment-save-calendar',
+ 'class' => 'icon calendarlink disabled',
+ 'classact' => 'icon calendarlink active',
+ 'innerclass' => 'icon calendar',
+ 'label' => 'calendar.savetocalendar',
+ ],
+ 'attachmentmenu'
+ );
+ }
+
+ return $p;
+ }
+
+ /**
+ * Handler for POST request to import an event attached to a mail message
+ */
+ public function mail_import_itip()
+ {
+ $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+
+ $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));
+ $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
+ $comment = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+
+ $error_msg = $this->gettext('errorimportingevent');
+ $success = false;
+ $deleted = false;
+
+ if ($status == 'delegated') {
+ $to = rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true);
+ $delegates = rcube_mime::decode_address_list($to, 1, false);
+ $delegate = reset($delegates);
+
+ if (empty($delegate) || empty($delegate['mailto'])) {
+ $this->rc->output->command('display_message', $this->rc->gettext('libcalendaring.delegateinvalidaddress'), 'error');
+ return;
+ }
+ }
+
+ // successfully parsed events?
+ if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
+ // forward iTip request to delegatee
+ if (!empty($delegate)) {
+ $rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST);
+ $itip = $this->load_itip();
+
+ $event['comment'] = $comment;
+
+ if ($itip->delegate_to($event, $delegate, !empty($rsvpme))) {
+ $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+
+ unset($event['comment']);
+
+ // the delegator is set to non-participant, thus save as non-blocking
+ $event['free_busy'] = 'free';
+ }
+
+ $mode = calendar_driver::FILTER_PERSONAL
+ | calendar_driver::FILTER_SHARED
+ | calendar_driver::FILTER_WRITEABLE;
+
+ // find writeable calendar to store event
+ $cal_id = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST);
+ $dontsave = $cal_id === '' && $event['_method'] == 'REQUEST';
+ $calendars = $this->driver->list_calendars($mode);
+ $calendar = isset($calendars[$cal_id]) ? $calendars[$cal_id] : null;
+
+ // select default calendar except user explicitly selected 'none'
+ if (!$calendar && !$dontsave) {
+ $calendar = $this->get_default_calendar($event['sensitivity'], $calendars);
+ }
+
+ $metadata = [
+ 'uid' => $event['uid'],
+ '_instance' => isset($event['_instance']) ? $event['_instance'] : null,
+ 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
+ 'sequence' => intval($event['sequence']),
+ 'fallback' => strtoupper($status),
+ 'method' => $event['_method'],
+ 'task' => 'calendar',
+ ];
+
+ // update my attendee status according to submitted method
+ if (!empty($status)) {
+ $organizer = null;
+ $emails = $this->get_user_emails();
+ foreach ($event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if (!empty($attendee['email']) && in_array(strtolower($attendee['email']), $emails)) {
+ $event['attendees'][$i]['status'] = strtoupper($status);
+ if (!in_array($event['attendees'][$i]['status'], ['NEEDS-ACTION', 'DELEGATED'])) {
+ $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
+ }
+
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
+
+ $reply_sender = $attendee['email'];
+ $event_attendee = $attendee;
+ }
+ }
+
+ // add attendee with this user's default identity if not listed
+ if (!$reply_sender) {
+ $sender_identity = $this->rc->user->list_emails(true);
+ $event['attendees'][] = [
+ 'name' => $sender_identity['name'],
+ 'email' => $sender_identity['email'],
+ 'role' => 'OPT-PARTICIPANT',
+ 'status' => strtoupper($status),
+ ];
+ $metadata['attendee'] = $sender_identity['email'];
+ }
+ }
+
+ // save to calendar
+ if ($calendar && !empty($calendar['editable'])) {
+ // check for existing event with the same UID
+ $existing = $this->find_event($event, $mode);
+
+ // we'll create a new copy if user decided to change the calendar
+ if ($existing && $cal_id && $calendar && $calendar['id'] != $existing['calendar']) {
+ $existing = null;
+ }
+
+ if ($existing) {
+ $calendar = $calendars[$existing['calendar']];
+
+ // forward savemode for correct updates of recurring events
+ $existing['_savemode'] = $savemode ?: (!empty($event['_savemode']) ? $event['_savemode'] : null);
+
+ // only update attendee status
+ if ($event['_method'] == 'REPLY') {
+ // try to identify the attendee using the email sender address
+ $existing_attendee = -1;
+ $existing_attendee_emails = [];
+
+ foreach ($existing['attendees'] as $i => $attendee) {
+ $existing_attendee_emails[] = $attendee['email'];
+ if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
+ $existing_attendee = $i;
+ }
+ }
+
+ $event_attendee = null;
+ $update_attendees = [];
+
+ foreach ($event['attendees'] as $attendee) {
+ if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
+ $event_attendee = $attendee;
+ $update_attendees[] = $attendee;
+ $metadata['fallback'] = $attendee['status'];
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = !empty($attendee['rsvp']) || $attendee['role'] != 'NON-PARTICIPANT';
+
+ if ($attendee['status'] != 'DELEGATED') {
+ break;
+ }
+ }
+ // also copy delegate attendee
+ else if (!empty($attendee['delegated-from'])
+ && $this->itip->compare_email($attendee['delegated-from'], $event['_sender'], $event['_sender_utf'])
+ ) {
+ $update_attendees[] = $attendee;
+ if (!in_array_nocase($attendee['email'], $existing_attendee_emails)) {
+ $existing['attendees'][] = $attendee;
+ }
+ }
+ }
+
+ // if delegatee has declined, set delegator's RSVP=True
+ if ($event_attendee
+ && $event_attendee['status'] == 'DECLINED'
+ && !empty($event_attendee['delegated-from'])
+ ) {
+ foreach ($existing['attendees'] as $i => $attendee) {
+ if ($attendee['email'] == $event_attendee['delegated-from']) {
+ $existing['attendees'][$i]['rsvp'] = true;
+ break;
+ }
+ }
+ }
+
+ // Accept sender as a new participant (different email in From: and the iTip)
+ // Use ATTENDEE entry from the iTip with replaced email address
+ if (!$event_attendee) {
+ // remove the organizer
+ $itip_attendees = array_filter(
+ $event['attendees'],
+ function($item) { return $item['role'] != 'ORGANIZER'; }
+ );
+
+ // there must be only one attendee
+ if (is_array($itip_attendees) && count($itip_attendees) == 1) {
+ $event_attendee = $itip_attendees[key($itip_attendees)];
+ $event_attendee['email'] = $event['_sender'];
+ $update_attendees[] = $event_attendee;
+ $metadata['fallback'] = $event_attendee['status'];
+ $metadata['attendee'] = $event_attendee['email'];
+ $metadata['rsvp'] = !empty($event_attendee['rsvp']) || $event_attendee['role'] != 'NON-PARTICIPANT';
+ }
+ }
+
+ // found matching attendee entry in both existing and new events
+ if ($existing_attendee >= 0 && $event_attendee) {
+ $existing['attendees'][$existing_attendee] = $event_attendee;
+ $success = $this->driver->update_attendees($existing, $update_attendees);
+ }
+ // update the entire attendees block
+ else if (
+ ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed'])
+ && $event_attendee
+ ) {
+ $existing['attendees'][] = $event_attendee;
+ $success = $this->driver->update_attendees($existing, $update_attendees);
+ }
+ else if (!$event_attendee) {
+ $error_msg = $this->gettext('errorunknownattendee');
+ }
+ else {
+ $error_msg = $this->gettext('newerversionexists');
+ }
+ }
+ // delete the event when declined (#1670)
+ else if ($status == 'declined' && $delete) {
+ $deleted = $this->driver->remove_event($existing, true);
+ $success = true;
+ }
+ // import the (newer) event
+ else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
+ $event['id'] = $existing['id'];
+ $event['calendar'] = $existing['calendar'];
+
+ // merge attendees status
+ // e.g. preserve my participant status for regular updates
+ $this->lib->merge_attendees($event, $existing, $status);
+
+ // set status=CANCELLED on CANCEL messages
+ if ($event['_method'] == 'CANCEL') {
+ $event['status'] = 'CANCELLED';
+ }
+
+ // update attachments list, allow attachments update only on REQUEST (#5342)
+ if ($event['_method'] == 'REQUEST') {
+ $event['deleted_attachments'] = true;
+ }
+ else {
+ unset($event['attachments']);
+ }
+
+ // show me as free when declined (#1670)
+ if ($status == 'declined'
+ || (!empty($event['status']) && $event['status'] == 'CANCELLED')
+ || $event_attendee['role'] == 'NON-PARTICIPANT'
+ ) {
+ $event['free_busy'] = 'free';
+ }
+
+ $success = $this->driver->edit_event($event);
+ }
+ else if (!empty($status)) {
+ $existing['attendees'] = $event['attendees'];
+ if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') {
+ // 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_calendars'))) {
+ if ($status == 'declined'
+ || $event['status'] == 'CANCELLED'
+ || $event_attendee['role'] == 'NON-PARTICIPANT'
+ ) {
+ $event['free_busy'] = 'free';
+ }
+
+ // if the RSVP reply only refers to a single instance:
+ // store unmodified master event with current instance as exception
+ if (!empty($instance) && !empty($savemode) && $savemode != 'all') {
+ $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
+ if ($master['recurrence'] && empty($master['_instance'])) {
+ // compute recurring events until this instance's date
+ if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
+ $recurrence_date->setTime(23,59,59);
+
+ foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
+ if ($recurring['_instance'] == $instance) {
+ // copy attendees block with my partstat to exception
+ $recurring['attendees'] = $event['attendees'];
+ $master['recurrence']['EXCEPTIONS'][] = $recurring;
+ $event = $recurring; // set reference for iTip reply
+ break;
+ }
+ }
+
+ $master['calendar'] = $event['calendar'] = $calendar['id'];
+ $success = $this->driver->new_event($master);
+ }
+ else {
+ $master = null;
+ }
+ }
+ else {
+ $master = null;
+ }
+ }
+
+ // save to the selected/default calendar
+ if (!$master) {
+ $event['calendar'] = $calendar['id'];
+ $success = $this->driver->new_event($event);
+ }
+ }
+ else if ($status == 'declined') {
+ $error_msg = null;
+ }
+ }
+ else if ($status == 'declined' || $dontsave) {
+ $error_msg = null;
+ }
+ else {
+ $error_msg = $this->gettext('nowritecalendarfound');
+ }
+ }
+
+ if ($success) {
+ if ($event['_method'] == 'REPLY') {
+ $message = 'attendeupdateesuccess';
+ }
+ else {
+ $message = $deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully');
+ }
+
+ $msg = $this->gettext(['name' => $message, 'vars' => ['calendar' => $calendar['name']]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ }
+
+ if ($success || $dontsave) {
+ $metadata['calendar'] = isset($event['calendar']) ? $event['calendar'] : null;
+ $metadata['nosave'] = $dontsave;
+ $metadata['rsvp'] = !empty($metadata['rsvp']);
+
+ $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
+ $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 ($event['_method'] == 'REQUEST' && !empty($organizer) && !$noreply
+ && !in_array(strtolower($organizer['email']), $emails) && !$error_msg
+ ) {
+ $event['comment'] = $comment;
+ $itip = $this->load_itip();
+ $itip->set_sender_email($reply_sender);
+
+ if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) {
+ $mailto = $organizer['name'] ? $organizer['name'] : $organizer['email'];
+ $msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ }
+
+ $this->rc->output->send();
+ }
+
+ /**
+ * Handler for calendar/itip-remove requests
+ */
+ function mail_itip_decline_reply()
+ {
+ $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);
+
+ if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event'))
+ && $event['_method'] == 'REPLY'
+ ) {
+ $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+
+ foreach ($event['attendees'] as $_attendee) {
+ if ($_attendee['role'] != 'ORGANIZER') {
+ $attendee = $_attendee;
+ break;
+ }
+ }
+
+ $itip = $this->load_itip();
+
+ if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel')) {
+ $mailto = !empty($attendee['name']) ? $attendee['name'] : $attendee['email'];
+ $msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ }
+
+ /**
+ * Handler for calendar/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();
+ }
+
+ /**
+ * 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 = RCUBE_CHARSET;
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_folder($mbox);
+
+ if ($uid && $mime_id) {
+ $part = $imap->get_message_part($uid, $mime_id);
+ // $headers = $imap->get_message_headers($uid);
+
+ if ($part) {
+ if (!empty($part->ctype_parameters['charset'])) {
+ $charset = $part->ctype_parameters['charset'];
+ }
+ $events = $this->get_ical()->import($part, $charset);
+ }
+ }
+
+ $success = $existing = 0;
+
+ if (!empty($events)) {
+ // find writeable calendar to store event
+ $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null;
+ $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
+
+ foreach ($events as $event) {
+ // save to calendar
+ $calendar = !empty($calendars[$cal_id]) ? $calendars[$cal_id] : $this->get_default_calendar($event['sensitivity']);
+ if ($calendar && $calendar['editable'] && $event['_type'] == 'event') {
+ $event['calendar'] = $calendar['id'];
+
+ if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) {
+ $success += (bool)$this->driver->new_event($event);
+ }
+ else {
+ $existing++;
+ }
+ }
+ }
+ }
+
+ if ($success) {
+ $msg = $this->gettext(['name' => 'importsuccess', 'vars' => ['nr' => $success]]);
+ $this->rc->output->command('display_message', $msg, 'confirmation');
+ }
+ else if ($existing) {
+ $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
+ }
+ }
+
+ /**
+ * Read email message and return contents for a new event based on that message
+ */
+ public function mail_message2event()
+ {
+ $this->ui->init();
+ $this->ui->addJS();
+ $this->ui->init_templates();
+ $this->ui->calendar_list([], true); // set env['calendars']
+
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET);
+ $event = [];
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $message = new rcube_message($uid, $mbox);
+
+ if ($message->headers) {
+ $event['title'] = trim($message->subject);
+ $event['description'] = trim($message->first_text_part());
+
+ $this->load_driver();
+
+ // add a reference to the email message
+ if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
+ $event['links'] = [$msgref];
+ }
+ // copy mail attachments to event
+ else if ($message->attachments) {
+ $eventid = 'cal-';
+ if (empty($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
+ $_SESSION[self::SESSION_KEY] = [
+ 'id' => $eventid,
+ 'attachments' => [],
+ ];
+ }
+
+ foreach ((array) $message->attachments as $part) {
+ $attachment = [
+ 'data' => $imap->get_message_part($uid, $part->mime_id, $part),
+ 'size' => $part->size,
+ 'name' => $part->filename,
+ 'mimetype' => $part->mimetype,
+ 'group' => $eventid,
+ ];
+
+ $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
+
+ if (!empty($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'
+ $event['attachments'][] = $attachment;
+ }
+ }
+ }
+
+ $this->rc->output->set_env('event_prop', $event);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
+ }
+
+ $this->rc->output->send('calendar.dialog');
+ }
+
+ /**
+ * Handler for the 'message_compose' plugin hook. This will check for
+ * a compose parameter 'calendar_event' and create an attachment with the
+ * referenced event in iCal format
+ */
+ public function mail_message_compose($args)
+ {
+ // set the submitted event ID as attachment
+ if (!empty($args['param']['calendar_event'])) {
+ $this->load_driver();
+
+ list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
+
+ if ($event = $this->driver->get_event(['id' => $id, 'calendar' => $cal])) {
+ $filename = asciiwords($event['title']);
+ if (empty($filename)) {
+ $filename = 'event';
+ }
+
+ // save ics to a temp file and register as attachment
+ $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
+ $export = $this->get_ical()->export([$event], '', false, [$this->driver, 'get_attachment_body']);
+
+ file_put_contents($tmp_path, $export);
+
+ $args['attachments'][] = [
+ 'path' => $tmp_path,
+ 'name' => $filename . '.ics',
+ 'mimetype' => 'text/calendar',
+ 'size' => filesize($tmp_path),
+ ];
+ $args['param']['subject'] = $event['title'];
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Get a list of email addresses of the current user (from login and identities)
+ */
+ public function get_user_emails()
+ {
+ return $this->lib->get_user_emails();
+ }
+
+ /**
+ * Build an absolute URL with the given parameters
+ */
+ public function get_url($param = [])
+ {
+ $param += ['task' => 'calendar'];
+ return $this->rc->url($param, true, true);
+ }
+
+ public function ical_feed_hash($source)
+ {
+ return base64_encode($this->rc->user->get_username() . ':' . $source);
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ // delete itipinvitations entries related to this user
+ $db = $this->rc->get_dbh();
+ $table_itipinvitations = $db->table_name('itipinvitations', true);
+
+ $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID);
+
+ $this->setup();
+ $this->load_driver();
+
+ return $this->driver->user_delete($args);
+ }
+
+ /**
+ * Find first occurrence of a recurring event excluding start date
+ *
+ * @param array $event Event data (with 'start' and 'recurrence')
+ *
+ * @return DateTime Date of the first occurrence
+ */
+ public function find_first_occurrence($event)
+ {
+ // Make sure libkolab plugin is loaded in case of Kolab driver
+ $this->load_driver();
+
+ // Use libkolab to compute recurring events (and libkolab plugin)
+ // Horde-based fallback has many bugs
+ if (class_exists('kolabformat') && class_exists('kolabcalendaring') && class_exists('kolab_date_recurrence')) {
+ $object = kolab_format::factory('event', 3.0);
+ $object->set($event);
+
+ $recurrence = new kolab_date_recurrence($object);
+ }
+ else {
+ // fallback to libcalendaring (Horde-based) recurrence implementation
+ require_once(__DIR__ . '/lib/calendar_recurrence.php');
+ $recurrence = new calendar_recurrence($this, $event);
+ }
+
+ return $recurrence->first_occurrence();
+ }
+
+ /**
+ * Get date-time input from UI and convert to unix timestamp
+ */
+ protected function input_timestamp($name, $type)
+ {
+ $ts = rcube_utils::get_input_value($name, $type);
+
+ if ($ts && (!is_numeric($ts) || strpos($ts, 'T'))) {
+ $ts = new DateTime($ts, $this->timezone);
+ $ts = $ts->getTimestamp();
+ }
+
+ return $ts;
+ }
+
+ /**
+ * 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;
+ }
}
diff --git a/plugins/calendar/composer.json b/plugins/calendar/composer.json
index aac326f9..195ae845 100644
--- a/plugins/calendar/composer.json
+++ b/plugins/calendar/composer.json
@@ -24,7 +24,7 @@
}
],
"require": {
- "php": ">=5.3.0",
+ "php": ">=5.4.0",
"roundcube/plugin-installer": ">=0.1.3",
"kolab/libcalendaring": ">=3.4.0",
"kolab/libkolab": ">=3.4.0"
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index a0161193..2c0fc6c8 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -94,740 +94,770 @@
*/
abstract class calendar_driver
{
- const FILTER_ALL = 0;
- const FILTER_WRITEABLE = 1;
- const FILTER_INSERTABLE = 2;
- const FILTER_ACTIVE = 4;
- const FILTER_PERSONAL = 8;
- const FILTER_PRIVATE = 16;
- const FILTER_CONFIDENTIAL = 32;
- const FILTER_SHARED = 64;
- const BIRTHDAY_CALENDAR_ID = '__bdays__';
+ const FILTER_ALL = 0;
+ const FILTER_WRITEABLE = 1;
+ const FILTER_INSERTABLE = 2;
+ const FILTER_ACTIVE = 4;
+ const FILTER_PERSONAL = 8;
+ const FILTER_PRIVATE = 16;
+ const FILTER_CONFIDENTIAL = 32;
+ const FILTER_SHARED = 64;
+ const BIRTHDAY_CALENDAR_ID = '__bdays__';
- // features supported by backend
- public $alarms = false;
- public $attendees = false;
- public $freebusy = false;
- public $attachments = false;
- public $undelete = false;
- public $history = false;
- public $categoriesimmutable = false;
- public $alarm_types = array('DISPLAY');
- public $alarm_absolute = true;
- public $last_error;
+ // features supported by backend
+ public $alarms = false;
+ public $attendees = false;
+ public $freebusy = false;
+ public $attachments = false;
+ public $undelete = false;
+ public $history = false;
+ public $alarm_types = ['DISPLAY'];
+ public $alarm_absolute = true;
+ public $categoriesimmutable = false;
+ public $last_error;
- protected $default_categories = array(
- 'Personal' => 'c0c0c0',
- 'Work' => 'ff0000',
- 'Family' => '00ff00',
- 'Holiday' => 'ff6600',
- );
+ protected $default_categories = [
+ 'Personal' => 'c0c0c0',
+ 'Work' => 'ff0000',
+ 'Family' => '00ff00',
+ 'Holiday' => 'ff6600',
+ ];
- /**
- * Get a list of available calendars from this source
- *
- * @param integer Bitmask defining filter criterias.
- * See FILTER_* constants for possible values.
- * @return array List of calendars
- */
- abstract function list_calendars($filter = 0);
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param int $filter Bitmask defining filter criterias.
+ * See FILTER_* constants for possible values.
+ *
+ * @return array List of calendars
+ */
+ abstract function list_calendars($filter = 0);
- /**
- * Create a new calendar assigned to the current user
- *
- * @param array Hash array with calendar properties
- * name: Calendar name
- * color: The color of the calendar
- * showalarms: True if alarms are enabled
- * @return mixed ID of the calendar on success, False on error
- */
- abstract function create_calendar($prop);
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array $prop Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * showalarms: True if alarms are enabled
+ *
+ * @return mixed ID of the calendar on success, False on error
+ */
+ abstract function create_calendar($prop);
- /**
- * Update properties of an existing calendar
- *
- * @param array Hash array with calendar properties
- * id: Calendar Identifier
- * name: Calendar name
- * color: The color of the calendar
- * showalarms: True if alarms are enabled (if supported)
- * @return boolean True on success, Fales on failure
- */
- abstract function edit_calendar($prop);
-
- /**
- * Set active/subscribed state of a calendar
- *
- * @param array Hash array with calendar properties
- * id: Calendar Identifier
- * active: True if calendar is active, false if not
- * @return boolean True on success, Fales on failure
- */
- abstract function subscribe_calendar($prop);
+ /**
+ * Update properties of an existing calendar
+ *
+ * @param array $prop Hash array with calendar properties
+ * id: Calendar Identifier
+ * name: Calendar name
+ * color: The color of the calendar
+ * showalarms: True if alarms are enabled (if supported)
+ *
+ * @return bool True on success, Fales on failure
+ */
+ abstract function edit_calendar($prop);
- /**
- * Delete the given calendar with all its contents
- *
- * @param array Hash array with calendar properties
- * id: Calendar Identifier
- * @return boolean True on success, Fales on failure
- */
- abstract function delete_calendar($prop);
+ /**
+ * Set active/subscribed state of a calendar
+ *
+ * @param array $prop Hash array with calendar properties
+ * id: Calendar Identifier
+ * active: True if calendar is active, false if not
+ *
+ * @return bool True on success, Fales on failure
+ */
+ abstract function subscribe_calendar($prop);
- /**
- * Search for shared or otherwise not listed calendars the user has access
- *
- * @param string Search string
- * @param string Section/source to search
- * @return array List of calendars
- */
- abstract function search_calendars($query, $source);
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @param array $prop Hash array with calendar properties
+ * id: Calendar Identifier
+ *
+ * @return bool True on success, Fales on failure
+ */
+ abstract function delete_calendar($prop);
- /**
- * Add a single event to the database
- *
- * @param array Hash array with event properties (see header of this file)
- * @return mixed New event ID on success, False on error
- */
- abstract function new_event($event);
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string $query Search string
+ * @param string $source Section/source to search
+ *
+ * @return array List of calendars
+ */
+ abstract function search_calendars($query, $source);
- /**
- * Update an event entry with the given data
- *
- * @param array Hash array with event properties (see header of this file)
- * @return boolean True on success, False on error
- */
- abstract function edit_event($event);
+ /**
+ * Add a single event to the database
+ *
+ * @param array $event Hash array with event properties (see header of this file)
+ *
+ * @return mixed New event ID on success, False on error
+ */
+ abstract function new_event($event);
- /**
- * Extended event editing with possible changes to the argument
- *
- * @param array Hash array with event properties
- * @param string New participant status
- * @param array List of hash arrays with updated attendees
- * @return boolean True on success, False on error
- */
- public function edit_rsvp(&$event, $status, $attendees)
- {
- return $this->edit_event($event);
- }
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array $event Hash array with event properties (see header of this file)
+ *
+ * @return bool True on success, False on error
+ */
+ abstract function edit_event($event);
- /**
- * Update the participant status for the given attendee
- *
- * @param array Hash array with event properties
- * @param array List of hash arrays each represeting an updated attendee
- * @return boolean True on success, False on error
- */
- public function update_attendees(&$event, $attendees)
- {
- return $this->edit_event($event);
- }
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array &$event Hash array with event properties
+ * @param string $status New participant status
+ * @param array $attendees List of hash arrays with updated attendees
+ *
+ * @return bool True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ return $this->edit_event($event);
+ }
- /**
- * Move a single event
- *
- * @param array Hash array with event properties:
- * id: Event identifier
- * start: Event start date/time as DateTime object
- * end: Event end date/time as DateTime object
- * allday: Boolean flag if this is an all-day event
- * @return boolean True on success, False on error
- */
- abstract function move_event($event);
+ /**
+ * Update the participant status for the given attendee
+ *
+ * @param array &$event Hash array with event properties
+ * @param array $attendees List of hash arrays each represeting an updated attendee
+ *
+ * @return bool True on success, False on error
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ return $this->edit_event($event);
+ }
- /**
- * Resize a single event
- *
- * @param array Hash array with event properties:
- * id: Event identifier
- * start: Event start date/time as DateTime object with timezone
- * end: Event end date/time as DateTime object with timezone
- * @return boolean True on success, False on error
- */
- abstract function resize_event($event);
+ /**
+ * Move a single event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * start: Event start date/time as DateTime object
+ * end: Event end date/time as DateTime object
+ * allday: Boolean flag if this is an all-day event
+ *
+ * @return bool True on success, False on error
+ */
+ abstract function move_event($event);
- /**
- * Remove a single event from the database
- *
- * @param array Hash array with event properties:
- * id: Event identifier
- * @param boolean Remove event irreversible (mark as deleted otherwise,
- * if supported by the backend)
- *
- * @return boolean True on success, False on error
- */
- abstract function remove_event($event, $force = true);
+ /**
+ * Resize a single event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * start: Event start date/time as DateTime object with timezone
+ * end: Event end date/time as DateTime object with timezone
+ *
+ * @return bool True on success, False on error
+ */
+ abstract function resize_event($event);
- /**
- * Restores a single deleted event (if supported)
- *
- * @param array Hash array with event properties:
- * id: Event identifier
- *
- * @return boolean True on success, False on error
- */
- public function restore_event($event)
- {
- return false;
- }
+ /**
+ * Remove a single event from the database
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * @param bool $force Remove event irreversible (mark as deleted otherwise,
+ * if supported by the backend)
+ *
+ * @return bool True on success, False on error
+ */
+ abstract function remove_event($event, $force = true);
- /**
- * Return data of a single event
- *
- * @param mixed UID string or hash array with event properties:
- * id: Event identifier
- * uid: Event UID
- * _instance: Instance identifier in combination with uid (optional)
- * calendar: Calendar identifier (optional)
- * @param integer Bitmask defining the scope to search events in.
- * See FILTER_* constants for possible values.
- * @param boolean If true, recurrence exceptions shall be added
- *
- * @return array Event object as hash array
- */
- abstract function get_event($event, $scope = 0, $full = false);
+ /**
+ * Restores a single deleted event (if supported)
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ *
+ * @return bool True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
- /**
- * Get events from source.
- *
- * @param integer Date range start (unix timestamp)
- * @param integer Date range end (unix timestamp)
- * @param string Search query (optional)
- * @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
- * @param boolean Include virtual/recurring events (optional)
- * @param integer Only list events modified since this time (unix timestamp)
- * @return array A list of event objects (see header of this file for struct of an event)
- */
- abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
+ /**
+ * Return data of a single event
+ *
+ * @param mixed $event UID string or hash array with event properties:
+ * id: Event identifier
+ * uid: Event UID
+ * _instance: Instance identifier in combination with uid (optional)
+ * calendar: Calendar identifier (optional)
+ * @param int $scope Bitmask defining the scope to search events in.
+ * See FILTER_* constants for possible values.
+ * @param bool $full If true, recurrence exceptions shall be added
+ *
+ * @return array Event object as hash array
+ */
+ abstract function get_event($event, $scope = 0, $full = false);
- /**
- * Get number of events in the given calendar
- *
- * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
- * @param integer Date range start (unix timestamp)
- * @param integer Date range end (unix timestamp)
- * @return array Hash array with counts grouped by calendar ID
- */
- abstract function count_events($calendars, $start, $end = null);
+ /**
+ * Get events from source.
+ *
+ * @param int $start Date range start (unix timestamp)
+ * @param int $end Date range end (unix timestamp)
+ * @param string $query Search query (optional)
+ * @param mixed $calendars List of calendar IDs to load events from (either as array or comma-separated string)
+ * @param bool $virtual Include virtual/recurring events (optional)
+ * @param int $modifiedsince Only list events modified since this time (unix timestamp)
+ *
+ * @return array A list of event objects (see header of this file for struct of an event)
+ */
+ abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
- /**
- * Get a list of pending alarms to be displayed to the user
- *
- * @param integer Current time (unix timestamp)
- * @param mixed List of calendar IDs to show alarms for (either as array or comma-separated string)
- * @return array A list of alarms, each encoded as hash array:
- * id: Event identifier
- * uid: Unique identifier of this event
- * start: Event start date/time as DateTime object
- * end: Event end date/time as DateTime object
- * allday: Boolean flag if this is an all-day event
- * title: Event title/summary
- * location: Location string
- */
- abstract function pending_alarms($time, $calendars = null);
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed $calendars List of calendar IDs to count events (either as array or comma-separated string)
+ * @param int $start Date range start (unix timestamp)
+ * @param int $end Date range end (unix timestamp)
+ *
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ abstract function count_events($calendars, $start, $end = 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 Event identifier
- * @param integer Suspend the alarm for this number of seconds
- */
- abstract function dismiss_alarm($event_id, $snooze = 0);
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @param int $time Current time (unix timestamp)
+ * @param mixed $calendars List of calendar IDs to show alarms for (either as array or comma-separated string)
+ *
+ * @return array A list of alarms, each encoded as hash array:
+ * id: Event identifier
+ * uid: Unique identifier of this event
+ * start: Event start date/time as DateTime object
+ * end: Event end date/time as DateTime object
+ * allday: Boolean flag if this is an all-day event
+ * title: Event title/summary
+ * location: Location string
+ */
+ abstract function pending_alarms($time, $calendars = null);
- /**
- * Check the given event object for validity
- *
- * @param array Event object as hash array
- * @return boolean True if valid, false if not
- */
- public function validate($event)
- {
- $valid = true;
+ /**
+ * (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 $event_id Event identifier
+ * @param int $snooze Suspend the alarm for this number of seconds
+ */
+ abstract function dismiss_alarm($event_id, $snooze = 0);
- if (!is_object($event['start']) || !is_a($event['start'], 'DateTime'))
- $valid = false;
- if (!is_object($event['end']) || !is_a($event['end'], 'DateTime'))
- $valid = false;
+ /**
+ * Check the given event object for validity
+ *
+ * @param array $event Event object as hash array
+ *
+ * @return boolean True if valid, false if not
+ */
+ public function validate($event)
+ {
+ $valid = true;
- return $valid;
- }
-
-
- /**
- * Get list of event's attachments.
- * Drivers can return list of attachments as event property.
- * If they will do not do this list_attachments() method will be used.
- *
- * @param array $event Hash array with event properties:
- * id: Event identifier
- * calendar: Calendar identifier
- *
- * @return array List of attachments, each as hash array:
- * id: Attachment identifier
- * name: Attachment name
- * mimetype: MIME content type of the attachment
- * size: Attachment size
- */
- public function list_attachments($event) { }
-
- /**
- * Get attachment properties
- *
- * @param string $id Attachment identifier
- * @param array $event Hash array with event properties:
- * id: Event identifier
- * calendar: Calendar 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, $event) { }
-
- /**
- * Get attachment body
- *
- * @param string $id Attachment identifier
- * @param array $event Hash array with event properties:
- * id: Event identifier
- * calendar: Calendar identifier
- *
- * @return string Attachment body
- */
- public function get_attachment_body($id, $event) { }
-
- /**
- * Build a struct representing the given message reference
- *
- * @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 array An struct referencing the given IMAP message
- */
- public function get_message_reference($uri_or_headers, $folder = null)
- {
- // to be implemented by the derived classes
- return false;
- }
-
- /**
- * List availabale categories
- * The default implementation reads them from config/user prefs
- */
- public function list_categories()
- {
- $rcmail = rcube::get_instance();
- return $rcmail->config->get('calendar_categories', $this->default_categories);
- }
-
- /**
- * Create a new category
- */
- public function add_category($name, $color) { }
-
- /**
- * Remove the given category
- */
- public function remove_category($name) { }
-
- /**
- * Update/replace a category
- */
- public function replace_category($oldname, $name, $color) { }
-
- /**
- * Fetch free/busy information from a person within the given range
- *
- * @param string E-mail address of attendee
- * @param integer Requested period start date/time as unix timestamp
- * @param integer Requested period end date/time as unix timestamp
- *
- * @return array List of busy timeslots within the requested range
- */
- public function get_freebusy_list($email, $start, $end)
- {
- return false;
- }
-
- /**
- * Create instances of a recurring event
- *
- * @param array Hash array with event properties
- * @param object DateTime Start date of the recurrence window
- * @param object DateTime End date of the recurrence window
- * @return array List of recurring event instances
- */
- public function get_recurring_events($event, $start, $end = null)
- {
- $events = array();
-
- if ($event['recurrence']) {
- // include library class
- require_once(dirname(__FILE__) . '/../lib/calendar_recurrence.php');
-
- $rcmail = rcmail::get_instance();
- $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
- $recurrence_id_format = libcalendaring::recurrence_id_format($event);
-
- // determine a reasonable end date if none given
- if (!$end) {
- switch ($event['recurrence']['FREQ']) {
- case 'YEARLY': $intvl = 'P100Y'; break;
- case 'MONTHLY': $intvl = 'P20Y'; break;
- default: $intvl = 'P10Y'; break;
+ if (empty($event['start']) || !is_object($event['start']) || !is_a($event['start'], 'DateTime')) {
+ $valid = false;
}
- $end = clone $event['start'];
- $end->add(new DateInterval($intvl));
- }
-
- $i = 0;
- while ($next_event = $recurrence->next_instance()) {
- // add to output if in range
- if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
- $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
- $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
- $next_event['recurrence_id'] = $event['uid'];
- $events[] = $next_event;
- }
- else if ($next_event['start'] > $end) { // stop loop if out of range
- break;
+ if (empty($event['end']) || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
+ $valid = false;
}
- // avoid endless recursion loops
- if (++$i > 1000) {
- break;
- }
- }
+ return $valid;
}
- return $events;
- }
+ /**
+ * Get list of event's attachments.
+ * Drivers can return list of attachments as event property.
+ * If they will do not do this list_attachments() method will be used.
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of attachments, each as hash array:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
+ */
+ public function list_attachments($event) { }
- /**
- * Provide a list of revisions for the given event
- *
- * @param array $event Hash array with event properties:
- * id: Event identifier
- * calendar: Calendar 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
- * destination: Destination calendar for 'move' type
- */
- public function get_event_changelog($event)
- {
- return false;
- }
+ /**
+ * Get attachment properties
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar 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, $event) { }
- /**
- * Get a list of property changes beteen two revisions of an event
- *
- * @param array $event Hash array with event properties:
- * id: Event identifier
- * calendar: Calendar 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_event_diff($event, $rev1, $rev2)
- {
- return false;
- }
+ /**
+ * Get attachment body
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return string Attachment body
+ */
+ public function get_attachment_body($id, $event) { }
- /**
- * Return full data of a specific revision of an event
- *
- * @param mixed UID string or hash array with event properties:
- * id: Event identifier
- * calendar: Calendar identifier
- * @param mixed $rev Revision number
- *
- * @return array Event object as hash array
- * @see self::get_event()
- */
- public function get_event_revison($event, $rev)
- {
- return false;
- }
-
- /**
- * Command the backend to restore a certain revision of an event.
- * This shall replace the current event with an older version.
- *
- * @param mixed UID string or hash array with event properties:
- * id: Event identifier
- * calendar: Calendar identifier
- * @param mixed $rev Revision number
- *
- * @return boolean True on success, False on failure
- */
- public function restore_event_revision($event, $rev)
- {
- return false;
- }
-
-
- /**
- * Callback function to produce driver-specific calendar create/edit form
- *
- * @param string Request action 'form-edit|form-new'
- * @param array Calendar properties (e.g. id, color)
- * @param array Edit form fields
- *
- * @return string HTML content of the form
- */
- public function calendar_form($action, $calendar, $formfields)
- {
- $table = new html_table(array('cols' => 2, 'class' => 'propform'));
-
- foreach ($formfields as $col => $colprop) {
- $label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext("$domain.$col");
-
- $table->add('title', html::label($colprop['id'], rcube::Q($label)));
- $table->add(null, $colprop['value']);
+ /**
+ * Build a struct representing the given message reference
+ *
+ * @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 array An struct referencing the given IMAP message
+ */
+ public function get_message_reference($uri_or_headers, $folder = null)
+ {
+ // to be implemented by the derived classes
+ return false;
}
- return $table->show();
- }
-
- /**
- * Compose a list of birthday events from the contact records in the user's address books.
- *
- * This is a default implementation using Roundcube's address book API.
- * It can be overriden with a more optimized version by the individual drivers.
- *
- * @param integer Event's new start (unix timestamp)
- * @param integer Event's new end (unix timestamp)
- * @param string Search query (optional)
- * @param integer Only list events modified since this time (unix timestamp)
- * @return array A list of event records
- */
- public function load_birthday_events($start, $end, $search = null, $modifiedsince = null)
- {
- // ignore update requests for simplicity reasons
- if (!empty($modifiedsince)) {
- return array();
+ /**
+ * List availabale categories
+ * The default implementation reads them from config/user prefs
+ */
+ public function list_categories()
+ {
+ $rcmail = rcube::get_instance();
+ return $rcmail->config->get('calendar_categories', $this->default_categories);
}
- // convert to DateTime for comparisons
- $start = new DateTime('@'.$start);
- $end = new DateTime('@'.$end);
- // extract the current year
- $year = $start->format('Y');
- $year2 = $end->format('Y');
+ /**
+ * Create a new category
+ */
+ public function add_category($name, $color) { }
- $events = array();
- $search = mb_strtolower($search);
- $rcmail = rcmail::get_instance();
- $cache = $rcmail->get_cache('calendar.birthdays', 'db', 3600);
- $cache->expunge();
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name) { }
- $alarm_type = $rcmail->config->get('calendar_birthdays_alarm_type', '');
- $alarm_offset = $rcmail->config->get('calendar_birthdays_alarm_offset', '-1D');
- $alarms = $alarm_type ? $alarm_offset . ':' . $alarm_type : null;
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color) { }
- // let the user select the address books to consider in prefs
- $selected_sources = $rcmail->config->get('calendar_birthday_adressbooks');
- $sources = $selected_sources ?: array_keys($rcmail->get_address_sources(false, true));
- foreach ($sources as $source) {
- $abook = $rcmail->get_address_book($source);
+ /**
+ * Fetch free/busy information from a person within the given range
+ *
+ * @param string $email E-mail address of attendee
+ * @param int $start Requested period start date/time as unix timestamp
+ * @param int $end Requested period end date/time as unix timestamp
+ *
+ * @return array List of busy timeslots within the requested range
+ */
+ public function get_freebusy_list($email, $start, $end)
+ {
+ return false;
+ }
- // skip LDAP address books unless selected by the user
- if (!$abook || ($abook instanceof rcube_ldap && empty($selected_sources))) {
- continue;
- }
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array $event Hash array with event properties
+ * @param DateTime $start Start date of the recurrence window
+ * @param DateTime $end End date of the recurrence window
+ *
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null)
+ {
+ $events = [];
- $abook->set_pagesize(10000);
+ if (!empty($event['recurrence'])) {
+ // include library class
+ require_once(dirname(__FILE__) . '/../lib/calendar_recurrence.php');
- // check for cached results
- $cache_records = array();
- $cached = $cache->get($source);
+ $rcmail = rcmail::get_instance();
+ $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
- // iterate over (cached) contacts
- foreach (($cached ?: $abook->search('*', '', 2, true, true, array('birthday'))) as $contact) {
- $event = self::parse_contact($contact, $source);
+ // determine a reasonable end date if none given
+ if (!$end) {
+ switch ($event['recurrence']['FREQ']) {
+ case 'YEARLY': $intvl = 'P100Y'; break;
+ case 'MONTHLY': $intvl = 'P20Y'; break;
+ default: $intvl = 'P10Y'; break;
+ }
- if (empty($event)) {
- continue;
+ $end = clone $event['start'];
+ $end->add(new DateInterval($intvl));
+ }
+
+ $i = 0;
+ while ($next_event = $recurrence->next_instance()) {
+ // add to output if in range
+ if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
+ $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
+ $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
+ $next_event['recurrence_id'] = $event['uid'];
+ $events[] = $next_event;
+ }
+ else if ($next_event['start'] > $end) { // stop loop if out of range
+ break;
+ }
+
+ // avoid endless recursion loops
+ if (++$i > 1000) {
+ break;
+ }
+ }
}
- // add stripped record to cache
- if (empty($cached)) {
- $cache_records[] = array(
- 'ID' => $contact['ID'],
- 'name' => $event['_displayname'],
- 'birthday' => $event['start']->format('Y-m-d'),
- );
+ return $events;
+ }
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar 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
+ * destination: Destination calendar for 'move' type
+ */
+ public function get_event_changelog($event)
+ {
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar 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_event_diff($event, $rev1, $rev2)
+ {
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param mixed $event UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see self::get_event()
+ */
+ public function get_event_revison($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed $event UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string $action Request action 'form-edit|form-new'
+ * @param array $calendar Calendar properties (e.g. id, color)
+ * @param array $formfields Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ $table = new html_table(['cols' => 2, 'class' => 'propform']);
+
+ foreach ($formfields as $col => $colprop) {
+ $label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext("$domain.$col");
+
+ $table->add('title', html::label($colprop['id'], rcube::Q($label)));
+ $table->add(null, $colprop['value']);
}
- // filter by search term (only name is involved here)
- if (!empty($search) && strpos(mb_strtolower($event['title']), $search) === false) {
- continue;
+ return $table->show();
+ }
+
+ /**
+ * Compose a list of birthday events from the contact records in the user's address books.
+ *
+ * This is a default implementation using Roundcube's address book API.
+ * It can be overriden with a more optimized version by the individual drivers.
+ *
+ * @param int $start Event's new start (unix timestamp)
+ * @param int $end Event's new end (unix timestamp)
+ * @param string $search Search query (optional)
+ * @param int $modifiedsince Only list events modified since this time (unix timestamp)
+ *
+ * @return array A list of event records
+ */
+ public function load_birthday_events($start, $end, $search = null, $modifiedsince = null)
+ {
+ // ignore update requests for simplicity reasons
+ if (!empty($modifiedsince)) {
+ return [];
}
- $bday = clone $event['start'];
- $byear = $bday->format('Y');
+ // convert to DateTime for comparisons
+ $start = new DateTime('@'.$start);
+ $end = new DateTime('@'.$end);
+ // extract the current year
+ $year = $start->format('Y');
+ $year2 = $end->format('Y');
- // quick-and-dirty recurrence computation: just replace the year
- $bday->setDate($year, $bday->format('n'), $bday->format('j'));
- $bday->setTime(12, 0, 0);
- $this_year = $year;
+ $events = [];
+ $search = mb_strtolower($search);
+ $rcmail = rcmail::get_instance();
+ $cache = $rcmail->get_cache('calendar.birthdays', 'db', 3600);
+ $cache->expunge();
- // date range reaches over multiple years: use end year if not in range
- if (($bday > $end || $bday < $start) && $year2 != $year) {
- $bday->setDate($year2, $bday->format('n'), $bday->format('j'));
- $this_year = $year2;
+ $alarm_type = $rcmail->config->get('calendar_birthdays_alarm_type', '');
+ $alarm_offset = $rcmail->config->get('calendar_birthdays_alarm_offset', '-1D');
+ $alarms = $alarm_type ? $alarm_offset . ':' . $alarm_type : null;
+
+ // let the user select the address books to consider in prefs
+ $selected_sources = $rcmail->config->get('calendar_birthday_adressbooks');
+ $sources = $selected_sources ?: array_keys($rcmail->get_address_sources(false, true));
+
+ foreach ($sources as $source) {
+ $abook = $rcmail->get_address_book($source);
+
+ // skip LDAP address books unless selected by the user
+ if (!$abook || ($abook instanceof rcube_ldap && empty($selected_sources))) {
+ continue;
+ }
+
+ // skip collected recipients/senders addressbooks
+ if (is_a($abook, 'rcube_addresses')) {
+ continue;
+ }
+
+ $abook->set_pagesize(10000);
+
+ // check for cached results
+ $cache_records = [];
+ $cached = $cache->get($source);
+
+ // iterate over (cached) contacts
+ foreach (($cached ?: $abook->search('*', '', 2, true, true, ['birthday'])) as $contact) {
+ $event = self::parse_contact($contact, $source);
+
+ if (empty($event)) {
+ continue;
+ }
+
+ // add stripped record to cache
+ if (empty($cached)) {
+ $cache_records[] = [
+ 'ID' => $contact['ID'],
+ 'name' => $event['_displayname'],
+ 'birthday' => $event['start']->format('Y-m-d'),
+ ];
+ }
+
+ // filter by search term (only name is involved here)
+ if (!empty($search) && strpos(mb_strtolower($event['title']), $search) === false) {
+ continue;
+ }
+
+ $bday = clone $event['start'];
+ $byear = $bday->format('Y');
+
+ // quick-and-dirty recurrence computation: just replace the year
+ $bday->setDate($year, $bday->format('n'), $bday->format('j'));
+ $bday->setTime(12, 0, 0);
+ $this_year = $year;
+
+ // date range reaches over multiple years: use end year if not in range
+ if (($bday > $end || $bday < $start) && $year2 != $year) {
+ $bday->setDate($year2, $bday->format('n'), $bday->format('j'));
+ $this_year = $year2;
+ }
+
+ // birthday is within requested range
+ if ($bday <= $end && $bday >= $start) {
+ unset($event['_displayname']);
+ $event['alarms'] = $alarms;
+
+ // if this is not the first occurence modify event details
+ // but not when this is "all birthdays feed" request
+ if ($year2 - $year < 10 && ($age = ($this_year - $byear))) {
+ $label = ['name' => 'birthdayage', 'vars' => ['age' => $age]];
+
+ $event['description'] = $rcmail->gettext($label, 'calendar');
+ $event['start'] = $bday;
+ $event['end'] = clone $bday;
+
+ unset($event['recurrence']);
+ }
+
+ // add the main instance
+ $events[] = $event;
+ }
+ }
+
+ // store collected contacts in cache
+ if (empty($cached)) {
+ $cache->write($source, $cache_records);
+ }
}
- // birthday is within requested range
- if ($bday <= $end && $bday >= $start) {
- unset($event['_displayname']);
- $event['alarms'] = $alarms;
+ return $events;
+ }
- // if this is not the first occurence modify event details
- // but not when this is "all birthdays feed" request
- if ($year2 - $year < 10 && ($age = ($this_year - $byear))) {
- $event['description'] = $rcmail->gettext(array('name' => 'birthdayage', 'vars' => array('age' => $age)), 'calendar');
- $event['start'] = $bday;
- $event['end'] = clone $bday;
- unset($event['recurrence']);
- }
+ /**
+ * Get a single birthday calendar event
+ */
+ public function get_birthday_event($id)
+ {
+ // decode $id
+ list(, $source, $contact_id, $year) = explode(':', rcube_ldap::dn_decode($id));
- // add the main instance
- $events[] = $event;
+ $rcmail = rcmail::get_instance();
+
+ if (strlen($source) && $contact_id && ($abook = $rcmail->get_address_book($source))) {
+ if ($contact = $abook->get_record($contact_id, true)) {
+ return self::parse_contact($contact, $source);
+ }
}
- }
-
- // store collected contacts in cache
- if (empty($cached)) {
- $cache->write($source, $cache_records);
- }
}
- return $events;
- }
+ /**
+ * Parse contact and create an event for its birthday
+ *
+ * @param array $contact Contact data
+ * @param string $source Addressbook source ID
+ *
+ * @return array|null Birthday event data
+ */
+ public static function parse_contact($contact, $source)
+ {
+ if (!is_array($contact)) {
+ return;
+ }
- /**
- * Get a single birthday calendar event
- */
- public function get_birthday_event($id)
- {
- // decode $id
- list(,$source,$contact_id,$year) = explode(':', rcube_ldap::dn_decode($id));
+ if (!empty($contact['birthday']) && is_array($contact['birthday'])) {
+ $contact['birthday'] = reset($contact['birthday']);
+ }
- $rcmail = rcmail::get_instance();
+ if (empty($contact['birthday'])) {
+ return;
+ }
- if (strlen($source) && $contact_id && ($abook = $rcmail->get_address_book($source))) {
- if ($contact = $abook->get_record($contact_id, true)) {
- return self::parse_contact($contact, $source);
- }
- }
- }
+ try {
+ $bday = $contact['birthday'];
+ if (!$bday instanceof DateTime) {
+ $bday = new DateTime($bday, new DateTimezone('UTC'));
+ }
+ $bday->_dateonly = true;
+ }
+ catch (Exception $e) {
+ rcube::raise_error([
+ 'code' => 600,
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => 'BIRTHDAY PARSE ERROR: ' . $e->getMessage()
+ ],
+ true, false
+ );
+ return;
+ }
- /**
- * Parse contact and create an event for its birthday
- *
- * @param array $contact Contact data
- * @param string $source Addressbook source ID
- *
- * @return array Birthday event data
- */
- public static function parse_contact($contact, $source)
- {
- if (!is_array($contact)) {
- return;
+ $rcmail = rcmail::get_instance();
+ $birthyear = $bday->format('Y');
+ $display_name = rcube_addressbook::compose_display_name($contact);
+ $label = ['name' => 'birthdayeventtitle', 'vars' => ['name' => $display_name]];
+ $event_title = $rcmail->gettext($label, 'calendar');
+ $uid = rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $birthyear);
+
+ return [
+ 'id' => $uid,
+ 'uid' => $uid,
+ 'calendar' => self::BIRTHDAY_CALENDAR_ID,
+ 'title' => $event_title,
+ 'description' => '',
+ 'allday' => true,
+ 'start' => $bday,
+ 'end' => clone $bday,
+ 'recurrence' => ['FREQ' => 'YEARLY', 'INTERVAL' => 1],
+ 'free_busy' => 'free',
+ '_displayname' => $display_name,
+ ];
}
- if (is_array($contact['birthday'])) {
- $contact['birthday'] = reset($contact['birthday']);
+ /**
+ * Store alarm dismissal for birtual birthay events
+ *
+ * @param string $event_id Event identifier
+ * @param int $snooze Suspend the alarm for this number of seconds
+ */
+ public function dismiss_birthday_alarm($event_id, $snooze = 0)
+ {
+ $rcmail = rcmail::get_instance();
+ $cache = $rcmail->get_cache('calendar.birthdayalarms', 'db', 86400 * 30);
+ $cache->remove($event_id);
+
+ // compute new notification time or disable if not snoozed
+ $notifyat = $snooze > 0 ? time() + $snooze : null;
+ $cache->set($event_id, ['snooze' => $snooze, 'notifyat' => $notifyat]);
+
+ return true;
}
- if (empty($contact['birthday'])) {
- return;
+ /**
+ * Handler for user_delete plugin hook
+ *
+ * @param array $args Hash array with hook arguments
+ *
+ * @return array Return arguments for plugin hooks
+ */
+ public function user_delete($args)
+ {
+ // TO BE OVERRIDDEN
+ return $args;
}
-
- try {
- $bday = $contact['birthday'];
- if (!$bday instanceof DateTime) {
- $bday = new DateTime($bday, new DateTimezone('UTC'));
- }
- $bday->_dateonly = true;
- }
- catch (Exception $e) {
- rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => 'BIRTHDAY PARSE ERROR: ' . $e->getMessage()),
- true, false);
- return;
- }
-
- $rcmail = rcmail::get_instance();
- $birthyear = $bday->format('Y');
- $display_name = rcube_addressbook::compose_display_name($contact);
- $label = array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name));
- $event_title = $rcmail->gettext($label, 'calendar');
- $uid = rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $birthyear);
-
- $event = array(
- 'id' => $uid,
- 'uid' => $uid,
- 'calendar' => self::BIRTHDAY_CALENDAR_ID,
- 'title' => $event_title,
- 'description' => '',
- 'allday' => true,
- 'start' => $bday,
- 'end' => clone $bday,
- 'recurrence' => array('FREQ' => 'YEARLY', 'INTERVAL' => 1),
- 'free_busy' => 'free',
- '_displayname' => $display_name,
- );
-
- return $event;
- }
-
- /**
- * Store alarm dismissal for birtual birthay events
- *
- * @param string Event identifier
- * @param integer Suspend the alarm for this number of seconds
- */
- public function dismiss_birthday_alarm($event_id, $snooze = 0)
- {
- $rcmail = rcmail::get_instance();
- $cache = $rcmail->get_cache('calendar.birthdayalarms', 'db', 86400 * 30);
- $cache->remove($event_id);
-
- // compute new notification time or disable if not snoozed
- $notifyat = $snooze > 0 ? time() + $snooze : null;
- $cache->set($event_id, array('snooze' => $snooze, 'notifyat' => $notifyat));
-
- return true;
- }
-
- /**
- * 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/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index c057825b..7ecf0dcf 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -136,7 +136,7 @@ class database_driver extends calendar_driver
$hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', '')));
$id = self::BIRTHDAY_CALENDAR_ID;
- if (!$active || !in_array($id, $hidden)) {
+ if (empty($active) || !in_array($id, $hidden)) {
$calendars[$id] = array(
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
@@ -172,7 +172,7 @@ class database_driver extends calendar_driver
$this->rc->user->ID,
$prop['name'],
strval($prop['color']),
- $prop['showalarms'] ? 1 : 0
+ !empty($prop['showalarms']) ? 1 : 0
);
if ($result) {
@@ -321,24 +321,24 @@ class database_driver extends calendar_driver
. " VALUES (?, $now, $now, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$event['calendar'],
strval($event['uid']),
- intval($event['recurrence_id']),
- strval($event['_instance']),
- intval($event['isexception']),
+ isset($event['recurrence_id']) ? intval($event['recurrence_id']) : 0,
+ isset($event['_instance']) ? strval($event['_instance']) : '',
+ isset($event['isexception']) ? intval($event['isexception']) : 0,
$event['start']->format(self::DB_DATE_FORMAT),
$event['end']->format(self::DB_DATE_FORMAT),
intval($event['all_day']),
$event['_recurrence'],
strval($event['title']),
- strval($event['description']),
- strval($event['location']),
- join(',', (array)$event['categories']),
- strval($event['url']),
+ isset($event['description']) ? strval($event['description']) : '',
+ isset($event['location']) ? strval($event['location']) : '',
+ isset($event['categories']) ? join(',', (array) $event['categories']) : '',
+ isset($event['url']) ? strval($event['url']) : '',
intval($event['free_busy']),
intval($event['priority']),
intval($event['sensitivity']),
- strval($event['status']),
+ isset($event['status']) ? strval($event['status']) : '',
$event['attendees'],
- $event['alarms'],
+ isset($event['alarms']) ? $event['alarms'] : null,
$event['notifyat']
);
@@ -381,7 +381,7 @@ class database_driver extends calendar_driver
// increment sequence number
if (empty($event['sequence']) && $reschedule) {
- $event['sequence'] = max($event['sequence'], $old['sequence']) + 1;
+ $event['sequence'] = $old['sequence'] + 1;
}
// modify a recurring event, check submitted savemode to do the right things
@@ -389,11 +389,12 @@ class database_driver extends calendar_driver
$master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
// keep saved exceptions (not submitted by the client)
- if ($old['recurrence']['EXDATE']) {
+ if (!empty($old['recurrence']['EXDATE'])) {
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
}
- switch ($event['_savemode']) {
+ $savemode = isset($event['_savemode']) ? $event['_savemode'] : null;
+ switch ($savemode) {
case 'new':
$event['uid'] = $this->cal->generate_uid();
return $this->new_event($event);
@@ -582,10 +583,12 @@ class database_driver extends calendar_driver
// iterate through the list of properties considered 'significant' for scheduling
foreach (self::$scheduling_properties as $prop) {
- $a = $old[$prop];
- $b = $event[$prop];
+ $a = isset($old[$prop]) ? $old[$prop] : null;
+ $b = isset($event[$prop]) ? $event[$prop] : null;
- if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ if (!empty($event['allday']) && ($prop == 'start' || $prop == 'end')
+ && $a instanceof DateTime && $b instanceof DateTime
+ ) {
$a = $a->format('Y-m-d');
$b = $b->format('Y-m-d');
}
@@ -596,10 +599,10 @@ class database_driver extends calendar_driver
$b = array_filter($b);
// advanced rrule comparison: no rescheduling if series was shortened
- if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
+ if (!empty($a['COUNT']) && !empty($b['COUNT']) && $b['COUNT'] < $a['COUNT']) {
unset($a['COUNT'], $b['COUNT']);
}
- else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
+ else if (!empty($a['UNTIL']) && !empty($b['UNTIL']) && $b['UNTIL'] < $a['UNTIL']) {
unset($a['UNTIL'], $b['UNTIL']);
}
}
@@ -652,24 +655,24 @@ class database_driver extends calendar_driver
}
// compose vcalendar-style recurrencue rule from structured data
- $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
+ $rrule = !empty($event['recurrence']) ? libcalendaring::to_rrule($event['recurrence']) : '';
+
+ $sensitivity = strtolower($event['sensitivity']);
+ $free_busy = strtolower($event['free_busy']);
$event['_recurrence'] = rtrim($rrule, ';');
- $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
- $event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]);
+ $event['free_busy'] = isset($this->free_busy_map[$free_busy]) ? $this->free_busy_map[$free_busy] : null;
+ $event['sensitivity'] = isset($this->sensitivity_map[$sensitivity]) ? $this->sensitivity_map[$sensitivity] : null;
+ $event['all_day'] = !empty($event['allday']) ? 1 : 0;
if ($event['free_busy'] == 'tentative') {
$event['status'] = 'TENTATIVE';
}
- if (isset($event['allday'])) {
- $event['all_day'] = $event['allday'] ? 1 : 0;
- }
-
// compute absolute time to notify the user
$event['notifyat'] = $this->_get_notification($event);
- if (is_array($event['valarms'])) {
+ if (!empty($event['valarms'])) {
$event['alarms'] = $this->serialize_alarms($event['valarms']);
}
@@ -689,7 +692,7 @@ class database_driver extends calendar_driver
*/
private function _get_notification($event)
{
- if ($event['valarms'] && $event['start'] > new DateTime()) {
+ if (!empty($event['valarms']) && $event['start'] > new DateTime()) {
$alarm = libcalendaring::get_next_alarm($event);
if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) {
@@ -714,26 +717,23 @@ class database_driver extends calendar_driver
);
foreach ($set_cols as $col) {
- if (is_object($event[$col]) && is_a($event[$col], 'DateTime')) {
+ if (!empty($event[$col]) && is_a($event[$col], 'DateTime')) {
$sql_args[$col] = $event[$col]->format(self::DB_DATE_FORMAT);
}
- else if (is_array($event[$col])) {
- $sql_args[$col] = join(',', $event[$col]);
- }
else if (array_key_exists($col, $event)) {
- $sql_args[$col] = $event[$col];
+ $sql_args[$col] = is_array($event[$col]) ? join(',', $event[$col]) : $event[$col];
}
}
- if ($event['_recurrence']) {
+ if (!empty($event['_recurrence'])) {
$sql_args['recurrence'] = $event['_recurrence'];
}
- if ($event['_instance']) {
+ if (!empty($event['_instance'])) {
$sql_args['instance'] = $event['_instance'];
}
- if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
+ if (!empty($event['_fromcalendar']) && $event['_fromcalendar'] != $event['calendar']) {
$sql_args['calendar_id'] = $event['calendar'];
}
@@ -763,7 +763,7 @@ class database_driver extends calendar_driver
}
// remove attachments
- if ($success && !empty($event['deleted_attachments'])) {
+ if ($success && !empty($event['deleted_attachments']) && is_array($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
$this->remove_attachment($attachment, $event['id']);
}
@@ -822,7 +822,7 @@ class database_driver extends calendar_driver
// skip exceptions
// TODO: merge updated data from master event
- if ($exdata[$datestr]) {
+ if (!empty($exdata[$datestr])) {
continue;
}
@@ -831,7 +831,7 @@ class database_driver extends calendar_driver
$next_end->add($duration);
$notify_at = $this->_get_notification(array(
- 'alarms' => $event['alarms'],
+ 'alarms' => !empty($event['alarms']) ? $event['alarms'] : null,
'start' => $next_start,
'end' => $next_end,
'status' => $event['status']
@@ -860,13 +860,13 @@ class database_driver extends calendar_driver
}
// stop adding events for inifinite recurrence after 20 years
- if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20)) {
+ if (++$count > 999 || (empty($recurrence->recurEnd) && empty($recurrence->recurCount) && $next_start->format('Y') > date('Y') + 20)) {
break;
}
}
// remove all exceptions after recurrence end
- if ($next_end && !empty($exceptions)) {
+ if (!empty($next_end) && !empty($exceptions)) {
$this->rc->db->query(
"DELETE FROM `{$this->db_events}`"
. " WHERE `recurrence_id` = ? AND `isexception` = 1 AND `start` > ?"
@@ -1025,11 +1025,11 @@ class database_driver extends calendar_driver
*/
public function get_event($event, $scope = 0, $full = false)
{
- $id = is_array($event) ? ($event['id'] ?: $event['uid']) : $event;
- $cal = is_array($event) ? $event['calendar'] : null;
+ $id = is_array($event) ? (!empty($event['id']) ? $event['id'] : $event['uid']) : $event;
+ $cal = is_array($event) && !empty($event['calendar']) ? $event['calendar'] : null;
$col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid';
- if ($this->cache[$id]) {
+ if (!empty($this->cache[$id])) {
return $this->cache[$id];
}
@@ -1039,15 +1039,15 @@ class database_driver extends calendar_driver
}
$where_add = '';
- if (is_array($event) && !$event['id'] && !empty($event['_instance'])) {
+ if (is_array($event) && empty($event['id']) && !empty($event['_instance'])) {
$where_add = " AND e.instance = " . $this->rc->db->quote($event['_instance']);
}
if ($scope & self::FILTER_ACTIVE) {
- $calendars = $this->calendars;
- foreach ($calendars as $idx => $cal) {
- if (!$cal['active']) {
- unset($calendars[$idx]);
+ $calendars = [];
+ foreach ($this->calendars as $idx => $cal) {
+ if (!empty($cal['active'])) {
+ $calendars[] = $idx;
}
}
$cals = join(',', $calendars);
@@ -1099,11 +1099,12 @@ class database_driver extends calendar_driver
// compose (slow) SQL query for searching
// FIXME: improve searching using a dedicated col and normalized values
+ $sql_add = '';
if ($query) {
foreach (array('title','location','description','categories','attendees') as $col) {
$sql_query[] = $this->rc->db->ilike($col, '%'.$query.'%');
}
- $sql_add = " AND (" . join(' OR ', $sql_query) . ")";
+ $sql_add .= " AND (" . join(' OR ', $sql_query) . ")";
}
if (!$virtual) {
@@ -1155,7 +1156,7 @@ class database_driver extends calendar_driver
// add events from the address books birthday calendar
if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars) && empty($query)) {
- $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
+ $events = array_merge($events, $this->load_birthday_events($start, $end, null, $modifiedsince));
}
return $events;
@@ -1229,7 +1230,7 @@ class database_driver extends calendar_driver
}
}
- if ($event['_attachments'] > 0) {
+ if (!empty($event['_attachments'])) {
$event['attachments'] = (array)$this->list_attachments($event);
}
@@ -1398,7 +1399,7 @@ class database_driver extends calendar_driver
. "SELECT `event_id` FROM `{$this->db_events}`"
. " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids}))",
$id,
- $event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
+ !empty($event['recurrence_id']) ? $event['recurrence_id'] : $event['id']
);
if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 440d6e39..4b817951 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -26,888 +26,933 @@
class kolab_calendar extends kolab_storage_folder_api
{
- public $ready = false;
- public $rights = 'lrs';
- public $editable = false;
- public $attachments = true;
- public $alarms = false;
- public $history = false;
- public $subscriptions = true;
- public $categories = array();
- public $storage;
+ public $ready = false;
+ public $rights = 'lrs';
+ public $editable = false;
+ public $attachments = true;
+ public $alarms = false;
+ public $history = false;
+ public $subscriptions = true;
+ public $categories = [];
+ public $storage;
- public $type = 'event';
+ public $type = 'event';
- protected $cal;
- protected $events = array();
- protected $search_fields = array('title', 'description', 'location', 'attendees', 'categories');
+ protected $cal;
+ protected $events = [];
+ protected $search_fields = ['title', 'description', 'location', 'attendees', 'categories'];
- /**
- * Factory method to instantiate a kolab_calendar object
- *
- * @param string Calendar ID (encoded IMAP folder name)
- * @param object calendar plugin object
- * @return object kolab_calendar instance
- */
- public static function factory($id, $calendar)
- {
- $imap = $calendar->rc->get_storage();
- $imap_folder = kolab_storage::id_decode($id);
- $info = $imap->folder_info($imap_folder, true);
- if (empty($info) || $info['noselect'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
- return new kolab_user_calendar($imap_folder, $calendar);
- }
- else {
- return new kolab_calendar($imap_folder, $calendar);
- }
- }
+ /**
+ * Factory method to instantiate a kolab_calendar object
+ *
+ * @param string Calendar ID (encoded IMAP folder name)
+ * @param object Calendar plugin object
+ *
+ * @return kolab_calendar Self instance
+ */
+ public static function factory($id, $calendar)
+ {
+ $imap = $calendar->rc->get_storage();
+ $imap_folder = kolab_storage::id_decode($id);
+ $info = $imap->folder_info($imap_folder, true);
- /**
- * Default constructor
- */
- public function __construct($imap_folder, $calendar)
- {
- $this->cal = $calendar;
- $this->imap = $calendar->rc->get_storage();
- $this->name = $imap_folder;
-
- // ID is derrived from folder name
- $this->id = kolab_storage::folder_id($this->name, true);
- $old_id = kolab_storage::folder_id($this->name, false);
-
- // fetch objects from the given IMAP folder
- $this->storage = kolab_storage::get_folder($this->name);
- $this->ready = $this->storage && $this->storage->valid;
-
- // Set writeable and alarms flags according to folder permissions
- if ($this->ready) {
- if ($this->storage->get_namespace() == 'personal') {
- $this->editable = true;
- $this->rights = 'lrswikxteav';
- $this->alarms = true;
- }
- else {
- $rights = $this->storage->get_myrights();
- if ($rights && !PEAR::isError($rights)) {
- $this->rights = $rights;
- if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
- $this->editable = strpos($rights, 'i');;
+ if (
+ empty($info)
+ || !empty($info['noselect'])
+ || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0
+ ) {
+ return new kolab_user_calendar($imap_folder, $calendar);
}
- }
-
- // user-specific alarms settings win
- $prefs = $this->cal->rc->config->get('kolab_calendars', array());
- if (isset($prefs[$this->id]['showalarms']))
- $this->alarms = $prefs[$this->id]['showalarms'];
- else if (isset($prefs[$old_id]['showalarms']))
- $this->alarms = $prefs[$old_id]['showalarms'];
+
+ return new kolab_calendar($imap_folder, $calendar);
}
- $this->default = $this->storage->default;
- $this->subtype = $this->storage->subtype;
- }
+ /**
+ * Default constructor
+ */
+ public function __construct($imap_folder, $calendar)
+ {
+ $this->cal = $calendar;
+ $this->imap = $calendar->rc->get_storage();
+ $this->name = $imap_folder;
+ // ID is derrived from folder name
+ $this->id = kolab_storage::folder_id($this->name, true);
+ $old_id = kolab_storage::folder_id($this->name, false);
- /**
- * Getter for the IMAP folder name
- *
- * @return string Name of the IMAP folder
- */
- public function get_realname()
- {
- return $this->name;
- }
+ // fetch objects from the given IMAP folder
+ $this->storage = kolab_storage::get_folder($this->name);
+ $this->ready = $this->storage && $this->storage->valid;
- /**
- *
- */
- public function get_title()
- {
- return null;
- }
+ // Set writeable and alarms flags according to folder permissions
+ if ($this->ready) {
+ if ($this->storage->get_namespace() == 'personal') {
+ $this->editable = true;
+ $this->rights = 'lrswikxteav';
+ $this->alarms = true;
+ }
+ else {
+ $rights = $this->storage->get_myrights();
+ if ($rights && !PEAR::isError($rights)) {
+ $this->rights = $rights;
+ if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
+ $this->editable = strpos($rights, 'i');;
+ }
+ }
+ }
-
- /**
- * Return color to display this calendar
- */
- public function get_color($default = null)
- {
- // color is defined in folder METADATA
- if ($color = $this->storage->get_color()) {
- return $color;
- }
-
- // calendar color is stored in user prefs (temporary solution)
- $prefs = $this->cal->rc->config->get('kolab_calendars', array());
-
- if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
- return $prefs[$this->id]['color'];
-
- return $default ?: 'cc0000';
- }
-
- /**
- * Compose an URL for CalDAV access to this calendar (if configured)
- */
- public function get_caldav_url()
- {
- if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
- return strtr($template, array(
- '%h' => $_SERVER['HTTP_HOST'],
- '%u' => urlencode($this->cal->rc->get_user_name()),
- '%i' => urlencode($this->storage->get_uid()),
- '%n' => urlencode($this->name),
- ));
- }
-
- return false;
- }
-
-
- /**
- * Update properties of this calendar folder
- *
- * @see calendar_driver::edit_calendar()
- */
- public function update(&$prop)
- {
- $prop['oldname'] = $this->get_realname();
- $newfolder = kolab_storage::folder_update($prop);
-
- if ($newfolder === false) {
- $this->cal->last_error = $this->cal->gettext(kolab_storage::$last_error);
- return false;
- }
-
- // create ID
- return kolab_storage::folder_id($newfolder);
- }
-
- /**
- * Getter for a single event object
- */
- public function get_event($id)
- {
- // remove our occurrence identifier if it's there
- $master_id = preg_replace('/-\d{8}(T\d{6})?$/', '', $id);
-
- // directly access storage object
- if (!$this->events[$id] && $master_id == $id && ($record = $this->storage->get_object($id))) {
- $this->events[$id] = $this->_to_driver_event($record, true);
- }
-
- // maybe a recurring instance is requested
- if (!$this->events[$id] && $master_id != $id) {
- $instance_id = substr($id, strlen($master_id) + 1);
-
- if ($record = $this->storage->get_object($master_id)) {
- $master = $this->_to_driver_event($record);
- }
-
- if ($master) {
- // check for match in top-level exceptions (aka loose single occurrences)
- if ($master['_formatobj'] && ($instance = $master['_formatobj']->get_instance($instance_id))) {
- $this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []);
+ if (isset($prefs[$this->id]['showalarms'])) {
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ }
+ else if (isset($prefs[$old_id]['showalarms'])) {
+ $this->alarms = $prefs[$old_id]['showalarms'];
+ }
}
- // check for match on the first instance already
- else if ($master['_instance'] && $master['_instance'] == $instance_id) {
- $this->events[$id] = $master;
- }
- else if (is_array($master['recurrence'])) {
- // For performance reasons we'll get only the specific instance
- if (($date = substr($id, strlen($master_id) + 1, 8)) && strlen($date) == 8 && is_numeric($date)) {
- $start_date = new DateTime($date . 'T000000', $master['start']->getTimezone());
- }
- $this->get_recurring_events($record, $start_date ?: $master['start'], null, $id, 1);
- }
- }
+ $this->default = $this->storage->default;
+ $this->subtype = $this->storage->subtype;
}
- return $this->events[$id];
- }
+ /**
+ * Getter for the IMAP folder name
+ *
+ * @return string Name of the IMAP folder
+ */
+ public function get_realname()
+ {
+ return $this->name;
+ }
+
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return null;
+ }
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color($default = null)
+ {
+ // color is defined in folder METADATA
+ if ($color = $this->storage->get_color()) {
+ return $color;
+ }
+
+ // calendar color is stored in user prefs (temporary solution)
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []);
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color'])) {
+ return $prefs[$this->id]['color'];
+ }
+
+ return $default ?: 'cc0000';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
+ return strtr($template, [
+ '%h' => $_SERVER['HTTP_HOST'],
+ '%u' => urlencode($this->cal->rc->get_user_name()),
+ '%i' => urlencode($this->storage->get_uid()),
+ '%n' => urlencode($this->name),
+ ]);
+ }
- /**
- * Get attachment body
- * @see calendar_driver::get_attachment_body()
- */
- public function get_attachment_body($id, $event)
- {
- if (!$this->ready)
return false;
-
- $data = $this->storage->get_attachment($event['id'], $id);
-
- if ($data == null) {
- // try again with master UID
- $uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
- if ($uid != $event['id']) {
- $data = $this->storage->get_attachment($uid, $id);
- }
}
- return $data;
- }
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ $prop['oldname'] = $this->get_realname();
+ $newfolder = kolab_storage::folder_update($prop);
- /**
- * @param integer Event's new start (unix timestamp)
- * @param integer Event's new end (unix timestamp)
- * @param string Search query (optional)
- * @param boolean Include virtual events (optional)
- * @param array Additional parameters to query storage
- * @param array Additional query to filter events
- * @return array A list of event records
- */
- public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null)
- {
- // convert to DateTime for comparisons
- // #5190: make the range a little bit wider
- // to workaround possible timezone differences
- try {
- $start = new DateTime('@' . ($start - 12 * 3600));
- }
- catch (Exception $e) {
- $start = new DateTime('@0');
- }
- try {
- $end = new DateTime('@' . ($end + 12 * 3600));
- }
- catch (Exception $e) {
- $end = new DateTime('today +10 years');
- }
-
- // get email addresses of the current user
- $user_emails = $this->cal->get_user_emails();
-
- // query Kolab storage
- $query[] = array('dtstart', '<=', $end);
- $query[] = array('dtend', '>=', $start);
-
- if (is_array($filter_query)) {
- $query = array_merge($query, $filter_query);
- }
-
- if (!empty($search)) {
- $search = mb_strtolower($search);
- $words = rcube_utils::tokenize_string($search, 1);
- foreach (rcube_utils::normalize_string($search, true) as $word) {
- $query[] = array('words', 'LIKE', $word);
- }
- }
- else {
- $words = array();
- }
-
- // set partstat filter to skip pending and declined invitations
- if (empty($filter_query) && $this->cal->rc->config->get('kolab_invitation_calendars')
- && $this->get_namespace() != 'other'
- ) {
- $partstat_exclude = array('NEEDS-ACTION','DECLINED');
- }
- else {
- $partstat_exclude = array();
- }
-
- $events = array();
- foreach ($this->storage->select($query) as $record) {
- $event = $this->_to_driver_event($record, !$virtual, false);
-
- // remember seen categories
- if ($event['categories']) {
- $cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
- $this->categories[$cat]++;
- }
-
- // list events in requested time window
- if ($event['start'] <= $end && $event['end'] >= $start) {
- unset($event['_attendees']);
- $add = true;
- // skip the first instance of a recurring event if listed in exdate
- if ($virtual && !empty($event['recurrence']['EXDATE'])) {
- $event_date = $event['start']->format('Ymd');
- $event_tz = $event['start']->getTimezone();
-
- foreach ((array) $event['recurrence']['EXDATE'] as $exdate) {
- $ex = clone $exdate;
- $ex->setTimezone($event_tz);
-
- if ($ex->format('Ymd') == $event_date) {
- $add = false;
- break;
- }
- }
- }
-
- // find and merge exception for the first instance
- if ($virtual && !empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) {
- foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
- if ($event['_instance'] == $exception['_instance']) {
- unset($exception['calendar'], $exception['className'], $exception['_folder_id']);
- // clone date objects from main event before adjusting them with exception data
- if (is_object($event['start'])) $event['start'] = clone $record['start'];
- if (is_object($event['end'])) $event['end'] = clone $record['end'];
- kolab_driver::merge_exception_data($event, $exception);
- }
- }
- }
-
- if ($add)
- $events[] = $event;
- }
-
- // resolve recurring events
- if ($record['recurrence'] && $virtual == 1) {
- $events = array_merge($events, $this->get_recurring_events($record, $start, $end));
- }
- // add top-level exceptions (aka loose single occurrences)
- else if (is_array($record['exceptions'])) {
- foreach ($record['exceptions'] as $ex) {
- $component = $this->_to_driver_event($ex, false, false, $record);
- if ($component['start'] <= $end && $component['end'] >= $start) {
- $events[] = $component;
- }
- }
- }
- }
-
- // post-filter all events by fulltext search and partstat values
- $me = $this;
- $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
- // fulltext search
- if (count($words)) {
- $hits = 0;
- foreach ($words as $word) {
- $hits += $me->fulltext_match($event, $word, false);
- }
- if ($hits < count($words)) {
- return false;
- }
- }
-
- // partstat filter
- if (count($partstat_exclude) && is_array($event['attendees'])) {
- foreach ($event['attendees'] as $attendee) {
- if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
+ if ($newfolder === false) {
+ $this->cal->last_error = $this->cal->gettext(kolab_storage::$last_error);
return false;
- }
- }
- }
-
- return true;
- });
-
- // Apply event-to-mail relations
- $config = kolab_storage_config::get_instance();
- $config->apply_links($events);
-
- // avoid session race conditions that will loose temporary subscriptions
- $this->cal->rc->session->nowrite = true;
-
- return $events;
- }
-
- /**
- * Get number of events in the given calendar
- *
- * @param integer Date range start (unix timestamp)
- * @param integer Date range end (unix timestamp)
- * @param array Additional query to filter events
- *
- * @return integer Count
- */
- public function count_events($start, $end = null, $filter_query = null)
- {
- // convert to DateTime for comparisons
- try {
- $start = new DateTime('@'.$start);
- }
- catch (Exception $e) {
- $start = new DateTime('@0');
- }
- if ($end) {
- try {
- $end = new DateTime('@'.$end);
- }
- catch (Exception $e) {
- $end = null;
- }
- }
-
- // query Kolab storage
- $query[] = array('dtend', '>=', $start);
-
- if ($end)
- $query[] = array('dtstart', '<=', $end);
-
- // add query to exclude pending/declined invitations
- if (empty($filter_query)) {
- foreach ($this->cal->get_user_emails() as $email) {
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
- $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
- }
- }
- else if (is_array($filter_query)) {
- $query = array_merge($query, $filter_query);
- }
-
- // we rely the Kolab storage query (no post-filtering)
- return $this->storage->count($query);
- }
-
- /**
- * Create a new event record
- *
- * @see calendar_driver::new_event()
- *
- * @return mixed The created record ID on success, False on error
- */
- public function insert_event($event)
- {
- if (!is_array($event))
- return false;
-
- // email links are stored separately
- $links = $event['links'];
- unset($event['links']);
-
- //generate new event from RC input
- $object = $this->_from_driver_event($event);
- $saved = $this->storage->save($object, 'event');
-
- if (!$saved) {
- rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving event object to Kolab server"),
- true, false);
- $saved = false;
- }
- else {
- // save links in configuration.relation object
- if ($this->save_links($event['uid'], $links)) {
- $object['links'] = $links;
- }
-
- $this->events = array($event['uid'] => $this->_to_driver_event($object, true));
- }
-
- return $saved;
- }
-
- /**
- * Update a specific event record
- *
- * @see calendar_driver::new_event()
- *
- * @return boolean True on success, False on error
- */
- public function update_event($event, $exception_id = null)
- {
- $updated = false;
- $old = $this->storage->get_object($event['uid'] ?: $event['id']);
- if (!$old || PEAR::isError($old))
- return false;
-
- // email links are stored separately
- $links = $event['links'];
- unset($event['links']);
-
- $object = $this->_from_driver_event($event, $old);
- $saved = $this->storage->save($object, 'event', $old['uid']);
-
- if (!$saved) {
- rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving event object to Kolab server"),
- true, false);
- }
- else {
- // save links in configuration.relation object
- if ($this->save_links($event['uid'], $links)) {
- $object['links'] = $links;
- }
-
- $updated = true;
- $this->events = array($event['uid'] => $this->_to_driver_event($object, true));
-
- // refresh local cache with recurring instances
- if ($exception_id) {
- $this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
- }
- }
-
- return $updated;
- }
-
- /**
- * Delete an event record
- *
- * @see calendar_driver::remove_event()
- *
- * @return boolean True on success, False on error
- */
- public function delete_event($event, $force = true)
- {
- $deleted = $this->storage->delete($event['uid'] ?: $event['id'], $force);
-
- if (!$deleted) {
- rcube::raise_error(array(
- 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
- 'message' => sprintf("Error deleting event object '%s' from Kolab server", $event['id'])),
- true, false);
- }
-
- return $deleted;
- }
-
- /**
- * Restore deleted event record
- *
- * @see calendar_driver::undelete_event()
- *
- * @return boolean True on success, False on error
- */
- public function restore_event($event)
- {
- // Make sure this is not an instance identifier
- $uid = preg_replace('/-\d{8}(T\d{6})?$/', '', $event['id']);
-
- if ($this->storage->undelete($uid)) {
- return true;
- }
- else {
- rcube::raise_error(array(
- 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
- 'message' => sprintf("Error undeleting the event object '%s' from the Kolab server", $event['id'])),
- true, false);
- }
-
- return false;
- }
-
- /**
- * Find messages linked with an event
- */
- protected function get_links($uid)
- {
- $storage = kolab_storage_config::get_instance();
- return $storage->get_object_links($uid);
- }
-
- /**
- *
- */
- protected function save_links($uid, $links)
- {
- $storage = kolab_storage_config::get_instance();
- return $storage->save_object_links($uid, (array) $links);
- }
-
- /**
- * Create instances of a recurring event
- *
- * @param array $event Hash array with event properties
- * @param DateTime $start Start date of the recurrence window
- * @param DateTime $end End date of the recurrence window
- * @param string $event_id ID of a specific recurring event instance
- * @param int $limit Max. number of instances to return
- *
- * @return array List of recurring event instances
- */
- public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
- {
- $object = $event['_formatobj'];
- if (!$object) {
- $rec = $this->storage->get_object($event['uid'] ?: $event['id']);
- $object = $rec['_formatobj'];
- }
-
- if (!is_object($object))
- return array();
-
- // determine a reasonable end date if none given
- if (!$end) {
- $end = clone $event['start'];
- $end->add(new DateInterval('P100Y'));
- }
-
- // copy the recurrence rule from the master event (to be used in the UI)
- $recurrence_rule = $event['recurrence'];
- unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
-
- // read recurrence exceptions first
- $events = array();
- $exdata = array();
- $futuredata = array();
- $recurrence_id_format = libcalendaring::recurrence_id_format($event);
-
- if (is_array($event['recurrence']['EXCEPTIONS'])) {
- foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
- if (!$exception['_instance'])
- $exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception, $event['allday']);
-
- $rec_event = $this->_to_driver_event($exception, false, false, $event);
- $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
- $rec_event['isexception'] = 1;
-
- // found the specifically requested instance: register exception (single occurrence wins)
- if ($rec_event['id'] == $event_id && (!$this->events[$event_id] || $this->events[$event_id]['thisandfuture'])) {
- $rec_event['recurrence'] = $recurrence_rule;
- $rec_event['recurrence_id'] = $event['uid'];
- $this->events[$rec_event['id']] = $rec_event;
}
- // remember this exception's date
- $exdate = substr($exception['_instance'], 0, 8);
- if (!$exdata[$exdate] || $exdata[$exdate]['thisandfuture']) {
- $exdata[$exdate] = $rec_event;
+ // create ID
+ return kolab_storage::folder_id($newfolder);
+ }
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // remove our occurrence identifier if it's there
+ $master_id = preg_replace('/-\d{8}(T\d{6})?$/', '', $id);
+
+ // directly access storage object
+ if (empty($this->events[$id]) && $master_id == $id && ($record = $this->storage->get_object($id))) {
+ $this->events[$id] = $this->_to_driver_event($record, true);
}
- if ($rec_event['thisandfuture']) {
- $futuredata[$exdate] = $rec_event;
+
+ // maybe a recurring instance is requested
+ if (empty($this->events[$id]) && $master_id != $id) {
+ $instance_id = substr($id, strlen($master_id) + 1);
+
+ if ($record = $this->storage->get_object($master_id)) {
+ $master = $this->_to_driver_event($record);
+ }
+
+ if ($master) {
+ // check for match in top-level exceptions (aka loose single occurrences)
+ if (!empty($master['_formatobj']) && ($instance = $master['_formatobj']->get_instance($instance_id))) {
+ $this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
+ }
+ // check for match on the first instance already
+ else if (!empty($master['_instance']) && $master['_instance'] == $instance_id) {
+ $this->events[$id] = $master;
+ }
+ else if (!empty($master['recurrence'])) {
+ $start_date = $master['start'];
+ // For performance reasons we'll get only the specific instance
+ if (($date = substr($id, strlen($master_id) + 1, 8)) && strlen($date) == 8 && is_numeric($date)) {
+ $start_date = new DateTime($date . 'T000000', $master['start']->getTimezone());
+ }
+
+ $this->get_recurring_events($record, $start_date, null, $id, 1);
+ }
+ }
}
- }
+
+ return $this->events[$id];
}
- // found the specifically requested instance, exiting...
- if ($event_id && !empty($this->events[$event_id])) {
- return array($this->events[$event_id]);
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!$this->ready) {
+ return false;
+ }
+
+ $data = $this->storage->get_attachment($event['id'], $id);
+
+ if ($data == null) {
+ // try again with master UID
+ $uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
+ if ($uid != $event['id']) {
+ $data = $this->storage->get_attachment($uid, $id);
+ }
+ }
+
+ return $data;
}
- // Check first occurrence, it might have been moved
- if ($first = $exdata[$event['start']->format('Ymd')]) {
- // return it only if not already in the result, but in the requested period
- if (!($event['start'] <= $end && $event['end'] >= $start)
- && ($first['start'] <= $end && $first['end'] >= $start)
- ) {
- $events[] = $first;
- }
+ /**
+ * @param int Event's new start (unix timestamp)
+ * @param int Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param bool Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @param array Additional query to filter events
+ *
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = [], $filter_query = null)
+ {
+ // convert to DateTime for comparisons
+ // #5190: make the range a little bit wider
+ // to workaround possible timezone differences
+ try {
+ $start = new DateTime('@' . ($start - 12 * 3600));
+ }
+ catch (Exception $e) {
+ $start = new DateTime('@0');
+ }
+ try {
+ $end = new DateTime('@' . ($end + 12 * 3600));
+ }
+ catch (Exception $e) {
+ $end = new DateTime('today +10 years');
+ }
+
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+
+ // query Kolab storage
+ $query[] = ['dtstart', '<=', $end];
+ $query[] = ['dtend', '>=', $start];
+
+ if (is_array($filter_query)) {
+ $query = array_merge($query, $filter_query);
+ }
+
+ $words = [];
+ $partstat_exclude = [];
+ $events = [];
+
+ if (!empty($search)) {
+ $search = mb_strtolower($search);
+ $words = rcube_utils::tokenize_string($search, 1);
+ foreach (rcube_utils::normalize_string($search, true) as $word) {
+ $query[] = ['words', 'LIKE', $word];
+ }
+ }
+
+ // set partstat filter to skip pending and declined invitations
+ if (empty($filter_query)
+ && $this->cal->rc->config->get('kolab_invitation_calendars')
+ && $this->get_namespace() != 'other'
+ ) {
+ $partstat_exclude = ['NEEDS-ACTION', 'DECLINED'];
+ }
+
+ foreach ($this->storage->select($query) as $record) {
+ $event = $this->_to_driver_event($record, !$virtual, false);
+
+ // remember seen categories
+ if (!empty($event['categories'])) {
+ $cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
+ $this->categories[$cat]++;
+ }
+
+ // list events in requested time window
+ if ($event['start'] <= $end && $event['end'] >= $start) {
+ unset($event['_attendees']);
+ $add = true;
+
+ // skip the first instance of a recurring event if listed in exdate
+ if ($virtual && !empty($event['recurrence']['EXDATE'])) {
+ $event_date = $event['start']->format('Ymd');
+ $event_tz = $event['start']->getTimezone();
+
+ foreach ((array) $event['recurrence']['EXDATE'] as $exdate) {
+ $ex = clone $exdate;
+ $ex->setTimezone($event_tz);
+
+ if ($ex->format('Ymd') == $event_date) {
+ $add = false;
+ break;
+ }
+ }
+ }
+
+ // find and merge exception for the first instance
+ if ($virtual && !empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS'])) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
+ if ($event['_instance'] == $exception['_instance']) {
+ unset($exception['calendar'], $exception['className'], $exception['_folder_id']);
+ // clone date objects from main event before adjusting them with exception data
+ if (is_object($event['start'])) {
+ $event['start'] = clone $record['start'];
+ }
+ if (is_object($event['end'])) {
+ $event['end'] = clone $record['end'];
+ }
+ kolab_driver::merge_exception_data($event, $exception);
+ }
+ }
+ }
+
+ if ($add) {
+ $events[] = $event;
+ }
+ }
+
+ // resolve recurring events
+ if (!empty($record['recurrence']) && $virtual == 1) {
+ $events = array_merge($events, $this->get_recurring_events($record, $start, $end));
+ }
+ // add top-level exceptions (aka loose single occurrences)
+ else if (!empty($record['exceptions'])) {
+ foreach ($record['exceptions'] as $ex) {
+ $component = $this->_to_driver_event($ex, false, false, $record);
+ if ($component['start'] <= $end && $component['end'] >= $start) {
+ $events[] = $component;
+ }
+ }
+ }
+ }
+
+ // post-filter all events by fulltext search and partstat values
+ $me = $this;
+ $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
+ // fulltext search
+ if (count($words)) {
+ $hits = 0;
+ foreach ($words as $word) {
+ $hits += $me->fulltext_match($event, $word, false);
+ }
+ if ($hits < count($words)) {
+ return false;
+ }
+ }
+
+ // partstat filter
+ if (count($partstat_exclude) && !empty($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (
+ in_array($attendee['email'], $user_emails)
+ && in_array($attendee['status'], $partstat_exclude)
+ ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ });
+
+ // Apply event-to-mail relations
+ $config = kolab_storage_config::get_instance();
+ $config->apply_links($events);
+
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->cal->rc->session->nowrite = true;
+
+ return $events;
}
- if ($limit && count($events) >= $limit) {
- return $events;
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param int Date range start (unix timestamp)
+ * @param int Date range end (unix timestamp)
+ * @param array Additional query to filter events
+ *
+ * @return int Count
+ */
+ public function count_events($start, $end = null, $filter_query = null)
+ {
+ // convert to DateTime for comparisons
+ try {
+ $start = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start = new DateTime('@0');
+ }
+ if ($end) {
+ try {
+ $end = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end = null;
+ }
+ }
+
+ // query Kolab storage
+ $query[] = ['dtend', '>=', $start];
+
+ if ($end) {
+ $query[] = ['dtstart', '<=', $end];
+ }
+
+ // add query to exclude pending/declined invitations
+ if (empty($filter_query)) {
+ foreach ($this->cal->get_user_emails() as $email) {
+ $query[] = ['tags', '!=', 'x-partstat:' . $email . ':needs-action'];
+ $query[] = ['tags', '!=', 'x-partstat:' . $email . ':declined'];
+ }
+ }
+ else if (is_array($filter_query)) {
+ $query = array_merge($query, $filter_query);
+ }
+
+ // we rely the Kolab storage query (no post-filtering)
+ return $this->storage->count($query);
}
- // use libkolab to compute recurring events
- $recurrence = new kolab_date_recurrence($object);
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return array|false The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ if (!is_array($event)) {
+ return false;
+ }
- $i = 0;
- while ($next_event = $recurrence->next_instance()) {
- $datestr = $next_event['start']->format('Ymd');
- $instance_id = $next_event['start']->format($recurrence_id_format);
+ // email links are stored separately
+ $links = !empty($event['links']) ? $event['links'] : [];
+ unset($event['links']);
- // use this event data for future recurring instances
- if ($futuredata[$datestr])
- $overlay_data = $futuredata[$datestr];
+ //generate new event from RC input
+ $object = $this->_from_driver_event($event);
+ $saved = $this->storage->save($object, 'event');
- $rec_id = $event['uid'] . '-' . $instance_id;
- $exception = $exdata[$datestr] ?: $overlay_data;
- $event_start = $next_event['start'];
- $event_end = $next_event['end'];
+ if (!$saved) {
+ rcube::raise_error([
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving event object to Kolab server"
+ ],
+ true, false
+ );
+ $saved = false;
+ }
+ else {
+ // save links in configuration.relation object
+ if ($this->save_links($event['uid'], $links)) {
+ $object['links'] = $links;
+ }
- // copy some event from exception to get proper start/end dates
- if ($exception) {
- $event_copy = $next_event;
- kolab_driver::merge_exception_dates($event_copy, $exception);
- $event_start = $event_copy['start'];
- $event_end = $event_copy['end'];
- }
+ $this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
+ }
- // add to output if in range
- if (($event_start <= $end && $event_end >= $start) || ($event_id && $rec_id == $event_id)) {
- $rec_event = $this->_to_driver_event($next_event, false, false, $event);
- $rec_event['_instance'] = $instance_id;
- $rec_event['_count'] = $i + 1;
+ return $saved;
+ }
- if ($exception) // copy data from exception
- kolab_driver::merge_exception_data($rec_event, $exception);
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return bool True on success, False on error
+ */
+ public function update_event($event, $exception_id = null)
+ {
+ $updated = false;
+ $old = $this->storage->get_object(!empty($event['uid']) ? $event['uid'] : $event['id']);
- $rec_event['id'] = $rec_id;
- $rec_event['recurrence_id'] = $event['uid'];
- $rec_event['recurrence'] = $recurrence_rule;
- unset($rec_event['_attendees']);
- $events[] = $rec_event;
+ if (!$old || PEAR::isError($old)) {
+ return false;
+ }
- if ($rec_id == $event_id) {
- $this->events[$rec_id] = $rec_event;
- break;
+ // email links are stored separately
+ $links = !empty($event['links']) ? $event['links'] : [];
+ unset($event['links']);
+
+ $object = $this->_from_driver_event($event, $old);
+ $saved = $this->storage->save($object, 'event', $old['uid']);
+
+ if (!$saved) {
+ rcube::raise_error([
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving event object to Kolab server"
+ ],
+ true, false
+ );
+ }
+ else {
+ // save links in configuration.relation object
+ if ($this->save_links($event['uid'], $links)) {
+ $object['links'] = $links;
+ }
+
+ $updated = true;
+ $this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
+
+ // refresh local cache with recurring instances
+ if ($exception_id) {
+ $this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
+ }
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ *
+ * @return bool True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ $deleted = $this->storage->delete(!empty($event['uid']) ? $event['uid'] : $event['id'], $force);
+
+ if (!$deleted) {
+ rcube::raise_error([
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => sprintf("Error deleting event object '%s' from Kolab server", $event['id'])
+ ],
+ true, false
+ );
+ }
+
+ return $deleted;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ *
+ * @return bool True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ // Make sure this is not an instance identifier
+ $uid = preg_replace('/-\d{8}(T\d{6})?$/', '', $event['id']);
+
+ if ($this->storage->undelete($uid)) {
+ return true;
+ }
+
+ rcube::raise_error([
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => sprintf("Error undeleting the event object '%s' from the Kolab server", $event['id'])
+ ],
+ true, false
+ );
+
+ return false;
+ }
+
+ /**
+ * Find messages linked with an event
+ */
+ protected function get_links($uid)
+ {
+ $storage = kolab_storage_config::get_instance();
+ return $storage->get_object_links($uid);
+ }
+
+ /**
+ *
+ */
+ protected function save_links($uid, $links)
+ {
+ $storage = kolab_storage_config::get_instance();
+ return $storage->save_object_links($uid, (array) $links);
+ }
+
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array $event Hash array with event properties
+ * @param DateTime $start Start date of the recurrence window
+ * @param DateTime $end End date of the recurrence window
+ * @param string $event_id ID of a specific recurring event instance
+ * @param int $limit Max. number of instances to return
+ *
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
+ {
+ if (empty($event['_formatobj'])) {
+ $rec = $this->storage->get_object(!empty($event['uid']) ? $event['uid'] : $event['id']);
+ $object = $rec['_formatobj'];
+ }
+ else {
+ $object = $event['_formatobj'];
+ }
+
+ if (!is_object($object)) {
+ return [];
+ }
+
+ // determine a reasonable end date if none given
+ if (!$end) {
+ $end = clone $event['start'];
+ $end->add(new DateInterval('P100Y'));
+ }
+
+ // read recurrence exceptions first
+ $events = [];
+ $exdata = [];
+ $futuredata = [];
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+
+ if (!empty($event['recurrence'])) {
+ // copy the recurrence rule from the master event (to be used in the UI)
+ $recurrence_rule = $event['recurrence'];
+ unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
+
+ if (!empty($event['recurrence']['EXCEPTIONS'])) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
+ if (empty($exception['_instance'])) {
+ $exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception, !empty($event['allday']));
+ }
+
+ $rec_event = $this->_to_driver_event($exception, false, false, $event);
+ $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
+ $rec_event['isexception'] = 1;
+
+ // found the specifically requested instance: register exception (single occurrence wins)
+ if (
+ $rec_event['id'] == $event_id
+ && (empty($this->events[$event_id]) || !empty($this->events[$event_id]['thisandfuture']))
+ ) {
+ $rec_event['recurrence'] = $recurrence_rule;
+ $rec_event['recurrence_id'] = $event['uid'];
+ $this->events[$rec_event['id']] = $rec_event;
+ }
+
+ // remember this exception's date
+ $exdate = substr($exception['_instance'], 0, 8);
+ if (empty($exdata[$exdate]) || !empty($exdata[$exdate]['thisandfuture'])) {
+ $exdata[$exdate] = $rec_event;
+ }
+ if (!empty($rec_event['thisandfuture'])) {
+ $futuredata[$exdate] = $rec_event;
+ }
+ }
+ }
+ }
+
+ // found the specifically requested instance, exiting...
+ if ($event_id && !empty($this->events[$event_id])) {
+ return [$this->events[$event_id]];
+ }
+
+ // Check first occurrence, it might have been moved
+ if ($first = $exdata[$event['start']->format('Ymd')]) {
+ // return it only if not already in the result, but in the requested period
+ if (!($event['start'] <= $end && $event['end'] >= $start)
+ && ($first['start'] <= $end && $first['end'] >= $start)
+ ) {
+ $events[] = $first;
+ }
}
if ($limit && count($events) >= $limit) {
- return $events;
+ return $events;
}
- }
- else if ($next_event['start'] > $end) // stop loop if out of range
- break;
- // avoid endless recursion loops
- if (++$i > 100000)
- break;
- }
+ // use libkolab to compute recurring events
+ $recurrence = new kolab_date_recurrence($object);
- return $events;
- }
+ $i = 0;
+ while ($next_event = $recurrence->next_instance()) {
+ $datestr = $next_event['start']->format('Ymd');
+ $instance_id = $next_event['start']->format($recurrence_id_format);
- /**
- * Convert from Kolab_Format to internal representation
- */
- private function _to_driver_event($record, $noinst = false, $links = true, $master_event = null)
- {
- $record['calendar'] = $this->id;
-
- // remove (possibly outdated) cached parameters
- unset($record['_folder_id'], $record['className']);
-
- if ($links && !array_key_exists('links', $record)) {
- $record['links'] = $this->get_links($record['uid']);
- }
-
- $ns = $this->get_namespace();
-
- if ($ns == 'other') {
- $record['className'] = 'fc-event-ns-other';
- }
-
- if ($ns == 'other' || !$this->cal->rc->config->get('kolab_invitation_calendars')) {
- $record = kolab_driver::add_partstat_class($record, array('NEEDS-ACTION', 'DECLINED'), $this->get_owner());
-
- // Modify invitation status class name, when invitation calendars are disabled
- // we'll use opacity only for declined/needs-action events
- $record['className'] = str_replace('-invitation', '', $record['className']);
- }
-
- // add instance identifier to first occurrence (master event)
- $recurrence_id_format = libcalendaring::recurrence_id_format($master_event ? $master_event : $record);
- if (!$noinst && $record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
- $record['_instance'] = $record['start']->format($recurrence_id_format);
- }
- else if (is_a($record['recurrence_date'], 'DateTime')) {
- $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
- }
-
- // clean up exception data
- if ($record['recurrence'] && is_array($record['recurrence']['EXCEPTIONS'])) {
- array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
- unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
- });
- }
-
- return $record;
- }
-
- /**
- * Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
- * (opposite of self::_to_driver_event())
- */
- private function _from_driver_event($event, $old = array())
- {
- // set current user as ORGANIZER
- if ($identity = $this->cal->rc->user->list_emails(true)) {
- $event['attendees'] = (array) $event['attendees'];
- $found = false;
-
- // there can be only resources on attendees list (T1484)
- // let's check the existence of an organizer
- foreach ($event['attendees'] as $attendee) {
- if ($attendee['role'] == 'ORGANIZER') {
- $found = true;
- break;
- }
- }
-
- if (!$found) {
- $event['attendees'][] = array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']);
- }
-
- $event['_owner'] = $identity['email'];
- }
-
- // remove EXDATE values if RDATE is given
- if (!empty($event['recurrence']['RDATE'])) {
- $event['recurrence']['EXDATE'] = array();
- }
-
- // remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
- if ($event['recurrence'] && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
- $event['recurrence'] = array();
- }
-
- // keep 'comment' from initial itip invitation
- if (!empty($old['comment'])) {
- $event['comment'] = $old['comment'];
- }
-
- // remove some internal properties which should not be cached
- $cleanup_fn = function(&$event) {
- unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
- $event['calendar'], $event['className'], $event['recurrence_id'],
- $event['attachments'], $event['deleted_attachments']);
- };
-
- $cleanup_fn($event);
-
- // clean up exception data
- if (is_array($event['exceptions'])) {
- array_walk($event['exceptions'], function(&$exception) use ($cleanup_fn) {
- unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj']);
- $cleanup_fn($exception);
- });
- }
-
- // copy meta data (starting with _) from old object
- foreach ((array)$old as $key => $val) {
- if (!isset($event[$key]) && $key[0] == '_')
- $event[$key] = $val;
- }
-
- return $event;
- }
-
- /**
- * Match the given word in the event contents
- */
- public function fulltext_match($event, $word, $recursive = true)
- {
- $hits = 0;
- foreach ($this->search_fields as $col) {
- $sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
- if (empty($sval))
- continue;
-
- // do a simple substring matching (to be improved)
- $val = mb_strtolower($sval);
- if (strpos($val, $word) !== false) {
- $hits++;
- break;
- }
- }
-
- return $hits;
- }
-
- /**
- * Convert a complex event attribute to a string value
- */
- private static function _complex2string($prop)
- {
- static $ignorekeys = array('role','status','rsvp');
-
- $out = '';
- if (is_array($prop)) {
- foreach ($prop as $key => $val) {
- if (is_numeric($key)) {
- $out .= self::_complex2string($val);
- }
- else if (!in_array($key, $ignorekeys)) {
- $out .= $val . ' ';
+ // use this event data for future recurring instances
+ if (!empty($futuredata[$datestr])) {
+ $overlay_data = $futuredata[$datestr];
}
- }
- }
- else if (is_string($prop) || is_numeric($prop)) {
- $out .= $prop . ' ';
- }
- return rtrim($out);
- }
+ $rec_id = $event['uid'] . '-' . $instance_id;
+ $exception = !empty($exdata[$datestr]) ? $exdata[$datestr] : $overlay_data;
+ $event_start = $next_event['start'];
+ $event_end = $next_event['end'];
+ // copy some event from exception to get proper start/end dates
+ if ($exception) {
+ $event_copy = $next_event;
+ kolab_driver::merge_exception_dates($event_copy, $exception);
+ $event_start = $event_copy['start'];
+ $event_end = $event_copy['end'];
+ }
+
+ // add to output if in range
+ if (($event_start <= $end && $event_end >= $start) || ($event_id && $rec_id == $event_id)) {
+ $rec_event = $this->_to_driver_event($next_event, false, false, $event);
+ $rec_event['_instance'] = $instance_id;
+ $rec_event['_count'] = $i + 1;
+
+ if ($exception) {
+ // copy data from exception
+ kolab_driver::merge_exception_data($rec_event, $exception);
+ }
+
+ $rec_event['id'] = $rec_id;
+ $rec_event['recurrence_id'] = $event['uid'];
+ $rec_event['recurrence'] = $recurrence_rule;
+ unset($rec_event['_attendees']);
+ $events[] = $rec_event;
+
+ if ($rec_id == $event_id) {
+ $this->events[$rec_id] = $rec_event;
+ break;
+ }
+
+ if ($limit && count($events) >= $limit) {
+ return $events;
+ }
+ }
+ else if ($next_event['start'] > $end) {
+ // stop loop if out of range
+ break;
+ }
+
+ // avoid endless recursion loops
+ if (++$i > 100000) {
+ break;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ private function _to_driver_event($record, $noinst = false, $links = true, $master_event = null)
+ {
+ $record['calendar'] = $this->id;
+
+ // remove (possibly outdated) cached parameters
+ unset($record['_folder_id'], $record['className']);
+
+ if ($links && !array_key_exists('links', $record)) {
+ $record['links'] = $this->get_links($record['uid']);
+ }
+
+ $ns = $this->get_namespace();
+
+ if ($ns == 'other') {
+ $record['className'] = 'fc-event-ns-other';
+ }
+
+ if ($ns == 'other' || !$this->cal->rc->config->get('kolab_invitation_calendars')) {
+ $record = kolab_driver::add_partstat_class($record, ['NEEDS-ACTION', 'DECLINED'], $this->get_owner());
+
+ // Modify invitation status class name, when invitation calendars are disabled
+ // we'll use opacity only for declined/needs-action events
+ $record['className'] = str_replace('-invitation', '', $record['className']);
+ }
+
+ // add instance identifier to first occurrence (master event)
+ $recurrence_id_format = libcalendaring::recurrence_id_format($master_event ? $master_event : $record);
+ if (!$noinst && !empty($record['recurrence']) && empty($record['recurrence_id']) && empty($record['_instance'])) {
+ $record['_instance'] = $record['start']->format($recurrence_id_format);
+ }
+ else if (isset($record['recurrence_date']) && is_a($record['recurrence_date'], 'DateTime')) {
+ $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
+ }
+
+ // clean up exception data
+ if (!empty($record['recurrence']) && !empty($record['recurrence']['EXCEPTIONS'])) {
+ array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
+ unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
+ });
+ }
+
+ return $record;
+ }
+
+ /**
+ * Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
+ * (opposite of self::_to_driver_event())
+ */
+ private function _from_driver_event($event, $old = [])
+ {
+ // set current user as ORGANIZER
+ if ($identity = $this->cal->rc->user->list_emails(true)) {
+ $event['attendees'] = !empty($event['attendees']) ? $event['attendees'] : [];
+ $found = false;
+
+ // there can be only resources on attendees list (T1484)
+ // let's check the existence of an organizer
+ foreach ($event['attendees'] as $attendee) {
+ if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') {
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $event['attendees'][] = ['role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']];
+ }
+
+ $event['_owner'] = $identity['email'];
+ }
+
+ // remove EXDATE values if RDATE is given
+ if (!empty($event['recurrence']['RDATE'])) {
+ $event['recurrence']['EXDATE'] = [];
+ }
+
+ // remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
+ if (!empty($event['recurrence']) && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
+ $event['recurrence'] = [];
+ }
+
+ // keep 'comment' from initial itip invitation
+ if (!empty($old['comment'])) {
+ $event['comment'] = $old['comment'];
+ }
+
+ // remove some internal properties which should not be cached
+ $cleanup_fn = function(&$event) {
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
+ $event['calendar'], $event['className'], $event['recurrence_id'],
+ $event['attachments'], $event['deleted_attachments']);
+ };
+
+ $cleanup_fn($event);
+
+ // clean up exception data
+ if (!empty($event['exceptions'])) {
+ array_walk($event['exceptions'], function(&$exception) use ($cleanup_fn) {
+ unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj']);
+ $cleanup_fn($exception);
+ });
+ }
+
+ // copy meta data (starting with _) from old object
+ foreach ((array) $old as $key => $val) {
+ if (!isset($event[$key]) && $key[0] == '_') {
+ $event[$key] = $val;
+ }
+ }
+
+ return $event;
+ }
+
+ /**
+ * Match the given word in the event contents
+ */
+ public function fulltext_match($event, $word, $recursive = true)
+ {
+ $hits = 0;
+ foreach ($this->search_fields as $col) {
+ if (empty($event[$col])) {
+ continue;
+ }
+
+ $sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
+ if (empty($sval)) {
+ continue;
+ }
+
+ // do a simple substring matching (to be improved)
+ $val = mb_strtolower($sval);
+ if (strpos($val, $word) !== false) {
+ $hits++;
+ break;
+ }
+ }
+
+ return $hits;
+ }
+
+ /**
+ * Convert a complex event attribute to a string value
+ */
+ private static function _complex2string($prop)
+ {
+ static $ignorekeys = ['role', 'status', 'rsvp'];
+
+ $out = '';
+ if (is_array($prop)) {
+ foreach ($prop as $key => $val) {
+ if (is_numeric($key)) {
+ $out .= self::_complex2string($val);
+ }
+ else if (!in_array($key, $ignorekeys)) {
+ $out .= $val . ' ';
+ }
+ }
+ }
+ else if (is_string($prop) || is_numeric($prop)) {
+ $out .= $prop . ' ';
+ }
+
+ return rtrim($out);
+ }
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 9b859bbd..0ab811a5 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -25,2409 +25,2619 @@
class kolab_driver extends calendar_driver
{
- const INVITATIONS_CALENDAR_PENDING = '--invitation--pending';
- const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
+ const INVITATIONS_CALENDAR_PENDING = '--invitation--pending';
+ const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
- // features this backend supports
- public $alarms = true;
- public $attendees = true;
- public $freebusy = true;
- public $attachments = true;
- public $undelete = true;
- public $alarm_types = array('DISPLAY','AUDIO');
- public $categoriesimmutable = true;
+ // features this backend supports
+ public $alarms = true;
+ public $attendees = true;
+ public $freebusy = true;
+ public $attachments = true;
+ public $undelete = true;
+ public $alarm_types = ['DISPLAY', 'AUDIO'];
+ public $categoriesimmutable = true;
- private $rc;
- private $cal;
- private $calendars;
- private $has_writeable = false;
- private $freebusy_trigger = false;
- private $bonnie_api = false;
+ private $rc;
+ private $cal;
+ private $calendars;
+ private $has_writeable = false;
+ private $freebusy_trigger = false;
+ private $bonnie_api = false;
- /**
- * Default constructor
- */
- public function __construct($cal)
- {
- $cal->require_plugin('libkolab');
+ /**
+ * Default constructor
+ */
+ public function __construct($cal)
+ {
+ $cal->require_plugin('libkolab');
- // load helper classes *after* libkolab has been loaded (#3248)
- require_once(dirname(__FILE__) . '/kolab_calendar.php');
- require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
- require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php');
+ // load helper classes *after* libkolab has been loaded (#3248)
+ require_once(__DIR__ . '/kolab_calendar.php');
+ require_once(__DIR__ . '/kolab_user_calendar.php');
+ require_once(__DIR__ . '/kolab_invitation_calendar.php');
- $this->cal = $cal;
- $this->rc = $cal->rc;
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
- $this->cal->register_action('push-freebusy', array($this, 'push_freebusy'));
- $this->cal->register_action('calendar-acl', array($this, 'calendar_acl'));
+ $this->cal->register_action('push-freebusy', [$this, 'push_freebusy']);
+ $this->cal->register_action('calendar-acl', [$this, 'calendar_acl']);
- $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
+ $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
- if (kolab_storage::$version == '2.0') {
- $this->alarm_types = array('DISPLAY');
- $this->alarm_absolute = false;
- }
-
- // get configuration for the Bonnie API
- $this->bonnie_api = libkolab::get_bonnie_api();
-
- // calendar uses fully encoded identifiers
- kolab_storage::$encode_ids = true;
- }
-
-
- /**
- * Read available calendars from server
- */
- private function _read_calendars()
- {
- // already read sources
- if (isset($this->calendars))
- return $this->calendars;
-
- // get all folders that have "event" type, sorted by namespace/name
- $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
-
- $this->calendars = array();
- foreach ($folders as $folder) {
- $calendar = $this->_to_calendar($folder);
- if ($calendar->ready) {
- $this->calendars[$calendar->id] = $calendar;
- if ($calendar->editable) {
- $this->has_writeable = true;
+ if (kolab_storage::$version == '2.0') {
+ $this->alarm_types = ['DISPLAY'];
+ $this->alarm_absolute = false;
}
- }
+
+ // get configuration for the Bonnie API
+ $this->bonnie_api = libkolab::get_bonnie_api();
+
+ // calendar uses fully encoded identifiers
+ kolab_storage::$encode_ids = true;
}
- return $this->calendars;
- }
+ /**
+ * Read available calendars from server
+ */
+ private function _read_calendars()
+ {
+ // already read sources
+ if (isset($this->calendars)) {
+ return $this->calendars;
+ }
- /**
- * Convert kolab_storage_folder into kolab_calendar
- */
- private function _to_calendar($folder)
- {
- if ($folder instanceof kolab_calendar) {
- return $folder;
+ // get all folders that have "event" type, sorted by namespace/name
+ $folders = kolab_storage::sort_folders(
+ kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true)
+ );
+
+ $this->calendars = [];
+
+ foreach ($folders as $folder) {
+ $calendar = $this->_to_calendar($folder);
+ if ($calendar->ready) {
+ $this->calendars[$calendar->id] = $calendar;
+ if ($calendar->editable) {
+ $this->has_writeable = true;
+ }
+ }
+ }
+
+ return $this->calendars;
}
- if ($folder instanceof kolab_storage_folder_user) {
- $calendar = new kolab_user_calendar($folder, $this->cal);
- $calendar->subscriptions = count($folder->children) > 0;
- }
- else {
- $calendar = new kolab_calendar($folder->name, $this->cal);
+ /**
+ * Convert kolab_storage_folder into kolab_calendar
+ */
+ private function _to_calendar($folder)
+ {
+ if ($folder instanceof kolab_calendar) {
+ return $folder;
+ }
+
+ if ($folder instanceof kolab_storage_folder_user) {
+ $calendar = new kolab_user_calendar($folder, $this->cal);
+ $calendar->subscriptions = count($folder->children) > 0;
+ }
+ else {
+ $calendar = new kolab_calendar($folder->name, $this->cal);
+ }
+
+ return $calendar;
}
- return $calendar;
- }
-
- /**
- * Get a list of available calendars from this source
- *
- * @param integer $filter Bitmask defining filter criterias
- * @param object $tree Reference to hierarchical folder tree object
- *
- * @return array List of calendars
- */
- public function list_calendars($filter = 0, &$tree = null)
- {
- $this->_read_calendars();
-
- // attempt to create a default calendar for this user
- if (!$this->has_writeable) {
- if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
- unset($this->calendars);
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param int $filter Bitmask defining filter criterias
+ * @param object $tree Reference to hierarchical folder tree object
+ *
+ * @return array List of calendars
+ */
+ public function list_calendars($filter = 0, &$tree = null)
+ {
$this->_read_calendars();
- }
- }
- $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
- $folders = $this->filter_calendars($filter);
- $calendars = array();
-
- // include virtual folders for a full folder tree
- if (!is_null($tree))
- $folders = kolab_storage::folder_hierarchy($folders, $tree);
-
- $parents = array_keys($this->calendars);
-
- foreach ($folders as $id => $cal) {
- $imap_path = explode($delim, $cal->name);
-
- // find parent
- do {
- array_pop($imap_path);
- $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
- }
- while (count($imap_path) > 1 && !in_array($parent_id, $parents));
-
- // restore "real" parent ID
- if ($parent_id && !in_array($parent_id, $parents)) {
- $parent_id = kolab_storage::folder_id($cal->get_parent());
- }
-
- $parents[] = $cal->id;
-
- if ($cal->virtual) {
- $calendars[$cal->id] = array(
- 'id' => $cal->id,
- 'name' => $cal->get_name(),
- 'listname' => $cal->get_foldername(),
- 'editname' => $cal->get_foldername(),
- 'virtual' => true,
- 'editable' => false,
- 'group' => $cal->get_namespace(),
- );
- }
- else {
- // additional folders may come from kolab_storage::folder_hierarchy() above
- // make sure we deal with kolab_calendar instances
- $cal = $this->_to_calendar($cal);
- $this->calendars[$cal->id] = $cal;
-
- $is_user = ($cal instanceof kolab_user_calendar);
-
- $calendars[$cal->id] = array(
- 'id' => $cal->id,
- 'name' => $cal->get_name(),
- 'listname' => $cal->get_foldername(),
- 'editname' => $cal->get_foldername(),
- 'title' => $cal->get_title(),
- 'color' => $cal->get_color(),
- 'editable' => $cal->editable,
- 'group' => $is_user ? 'other user' : $cal->get_namespace(),
- 'active' => $cal->is_active(),
- 'owner' => $cal->get_owner(),
- 'removable' => !$cal->default,
- );
-
- if (!$is_user) {
- $calendars[$cal->id] += array(
- 'default' => $cal->default,
- 'rights' => $cal->rights,
- 'showalarms' => $cal->alarms,
- 'history' => !empty($this->bonnie_api),
- 'children' => true, // TODO: determine if that folder indeed has child folders
- 'parent' => $parent_id,
- 'subtype' => $cal->subtype,
- 'caldavurl' => $cal->get_caldav_url(),
- );
+ // attempt to create a default calendar for this user
+ if (!$this->has_writeable) {
+ if ($this->create_calendar(['name' => 'Calendar', 'color' => 'cc0000'])) {
+ unset($this->calendars);
+ $this->_read_calendars();
+ }
}
- }
- if ($cal->subscriptions) {
- $calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
- }
- }
+ $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+ $folders = $this->filter_calendars($filter);
+ $calendars = [];
- // list virtual calendars showing invitations
- if ($this->rc->config->get('kolab_invitation_calendars') && !($filter & self::FILTER_INSERTABLE)) {
- foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
- $cal = new kolab_invitation_calendar($id, $this->cal);
- if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
- $calendars[$id] = array(
- 'id' => $cal->id,
- 'name' => $cal->get_name(),
- 'listname' => $cal->get_name(),
- 'editname' => $cal->get_foldername(),
- 'title' => $cal->get_title(),
- 'color' => $cal->get_color(),
- 'editable' => $cal->editable,
- 'rights' => $cal->rights,
- 'showalarms' => $cal->alarms,
- 'history' => !empty($this->bonnie_api),
- 'group' => 'x-invitations',
- 'default' => false,
- 'active' => $cal->is_active(),
- 'owner' => $cal->get_owner(),
- 'children' => false,
- );
-
- if ($id == self::INVITATIONS_CALENDAR_PENDING) {
- $calendars[$id]['counts'] = true;
- }
-
- if (is_object($tree)) {
- $tree->children[] = $cal;
- }
+ // include virtual folders for a full folder tree
+ if (!is_null($tree)) {
+ $folders = kolab_storage::folder_hierarchy($folders, $tree);
}
- }
- }
- // append the virtual birthdays calendar
- if ($this->rc->config->get('calendar_contact_birthdays', false) && !($filter & self::FILTER_INSERTABLE)) {
- $id = self::BIRTHDAY_CALENDAR_ID;
- $prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs
- if (!($filter & self::FILTER_ACTIVE) || $prefs[$id]['active']) {
- $calendars[$id] = array(
- 'id' => $id,
- 'name' => $this->cal->gettext('birthdays'),
- 'listname' => $this->cal->gettext('birthdays'),
- 'color' => $prefs[$id]['color'] ?: '87CEFA',
- 'active' => (bool)$prefs[$id]['active'],
- 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
- 'group' => 'x-birthdays',
- 'editable' => false,
- 'default' => false,
- 'children' => false,
- 'history' => false,
- );
- }
- }
+ $parents = array_keys($this->calendars);
- return $calendars;
- }
+ foreach ($folders as $id => $cal) {
+ $imap_path = explode($delim, $cal->name);
- /**
- * Get list of calendars according to specified filters
- *
- * @param integer Bitmask defining restrictions. See FILTER_* constants for possible values.
- *
- * @return array List of calendars
- */
- protected function filter_calendars($filter)
- {
- $this->_read_calendars();
+ // find parent
+ do {
+ array_pop($imap_path);
+ $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+ }
+ while (count($imap_path) > 1 && !in_array($parent_id, $parents));
- $calendars = array();
+ // restore "real" parent ID
+ if ($parent_id && !in_array($parent_id, $parents)) {
+ $parent_id = kolab_storage::folder_id($cal->get_parent());
+ }
- $plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array(
- 'list' => $this->calendars,
- 'calendars' => $calendars,
- 'filter' => $filter,
- ));
+ $parents[] = $cal->id;
- if ($plugin['abort']) {
- return $plugin['calendars'];
- }
+ if ($cal->virtual) {
+ $calendars[$cal->id] = [
+ 'id' => $cal->id,
+ 'name' => $cal->get_name(),
+ 'listname' => $cal->get_foldername(),
+ 'editname' => $cal->get_foldername(),
+ 'virtual' => true,
+ 'editable' => false,
+ 'group' => $cal->get_namespace(),
+ ];
+ }
+ else {
+ // additional folders may come from kolab_storage::folder_hierarchy() above
+ // make sure we deal with kolab_calendar instances
+ $cal = $this->_to_calendar($cal);
+ $this->calendars[$cal->id] = $cal;
- $personal = $filter & self::FILTER_PERSONAL;
- $shared = $filter & self::FILTER_SHARED;
+ $is_user = ($cal instanceof kolab_user_calendar);
- foreach ($this->calendars as $cal) {
- if (!$cal->ready) {
- continue;
- }
- if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) {
- continue;
- }
- if (($filter & self::FILTER_INSERTABLE) && !$cal->editable) {
- continue;
- }
- if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) {
- continue;
- }
- if (($filter & self::FILTER_PRIVATE) && $cal->subtype != 'private') {
- continue;
- }
- if (($filter & self::FILTER_CONFIDENTIAL) && $cal->subtype != 'confidential') {
- continue;
- }
- if ($personal || $shared) {
- $ns = $cal->get_namespace();
- if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) {
- continue;
+ $calendars[$cal->id] = [
+ 'id' => $cal->id,
+ 'name' => $cal->get_name(),
+ 'listname' => $cal->get_foldername(),
+ 'editname' => $cal->get_foldername(),
+ 'title' => $cal->get_title(),
+ 'color' => $cal->get_color(),
+ 'editable' => $cal->editable,
+ 'group' => $is_user ? 'other user' : $cal->get_namespace(),
+ 'active' => $cal->is_active(),
+ 'owner' => $cal->get_owner(),
+ 'removable' => !$cal->default,
+ ];
+
+ if (!$is_user) {
+ $calendars[$cal->id] += [
+ 'default' => $cal->default,
+ 'rights' => $cal->rights,
+ 'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
+ 'children' => true, // TODO: determine if that folder indeed has child folders
+ 'parent' => $parent_id,
+ 'subtype' => $cal->subtype,
+ 'caldavurl' => $cal->get_caldav_url(),
+ ];
+ }
+ }
+
+ if ($cal->subscriptions) {
+ $calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
+ }
}
- }
- $calendars[$cal->id] = $cal;
- }
+ // list virtual calendars showing invitations
+ if ($this->rc->config->get('kolab_invitation_calendars') && !($filter & self::FILTER_INSERTABLE)) {
+ foreach ([self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED] as $id) {
+ $cal = new kolab_invitation_calendar($id, $this->cal);
+ if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
+ $calendars[$id] = [
+ 'id' => $cal->id,
+ 'name' => $cal->get_name(),
+ 'listname' => $cal->get_name(),
+ 'editname' => $cal->get_foldername(),
+ 'title' => $cal->get_title(),
+ 'color' => $cal->get_color(),
+ 'editable' => $cal->editable,
+ 'rights' => $cal->rights,
+ 'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
+ 'group' => 'x-invitations',
+ 'default' => false,
+ 'active' => $cal->is_active(),
+ 'owner' => $cal->get_owner(),
+ 'children' => false,
+ 'counts' => $id == self::INVITATIONS_CALENDAR_PENDING,
+ ];
- return $calendars;
- }
- /**
- * Get the kolab_calendar instance for the given calendar ID
- *
- * @param string Calendar identifier (encoded imap folder name)
- *
- * @return object kolab_calendar Object nor null if calendar doesn't exist
- */
- public function get_calendar($id)
- {
- $this->_read_calendars();
-
- // create calendar object if necesary
- if (!$this->calendars[$id]) {
- if (in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
- return new kolab_invitation_calendar($id, $this->cal);
- }
- // for unsubscribed calendar folders
- if ($id !== self::BIRTHDAY_CALENDAR_ID) {
- $calendar = kolab_calendar::factory($id, $this->cal);
- if ($calendar->ready) {
- $this->calendars[$calendar->id] = $calendar;
+ if (is_object($tree)) {
+ $tree->children[] = $cal;
+ }
+ }
+ }
}
- }
- }
- return $this->calendars[$id];
- }
+ // append the virtual birthdays calendar
+ if ($this->rc->config->get('calendar_contact_birthdays', false) && !($filter & self::FILTER_INSERTABLE)) {
+ $id = self::BIRTHDAY_CALENDAR_ID;
+ $prefs = $this->rc->config->get('kolab_calendars', []); // read local prefs
- /**
- * Create a new calendar assigned to the current user
- *
- * @param array Hash array with calendar properties
- * name: Calendar name
- * color: The color of the calendar
- *
- * @return mixed ID of the calendar on success, False on error
- */
- public function create_calendar($prop)
- {
- $prop['type'] = 'event';
- $prop['active'] = true;
- $prop['subscribed'] = true;
-
- $folder = kolab_storage::folder_update($prop);
-
- if ($folder === false) {
- $this->last_error = $this->cal->gettext(kolab_storage::$last_error);
- return false;
- }
-
- // create ID
- $id = kolab_storage::folder_id($folder);
-
- // save color in user prefs (temp. solution)
- $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
-
- if (isset($prop['color']))
- $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
- if (isset($prop['showalarms']))
- $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
-
- if ($prefs['kolab_calendars'][$id])
- $this->rc->user->save_prefs($prefs);
-
- return $id;
- }
-
-
- /**
- * Update properties of an existing calendar
- *
- * @see calendar_driver::edit_calendar()
- */
- public function edit_calendar($prop)
- {
- if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
- $id = $cal->update($prop);
- }
- else {
- $id = $prop['id'];
- }
-
- // fallback to local prefs
- $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
- unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']);
-
- if (isset($prop['color']))
- $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
-
- if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID)
- $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
- else if (isset($prop['showalarms']))
- $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
-
- if (!empty($prefs['kolab_calendars'][$id]))
- $this->rc->user->save_prefs($prefs);
-
- return true;
- }
-
-
- /**
- * Set active/subscribed state of a calendar
- *
- * @see calendar_driver::subscribe_calendar()
- */
- public function subscribe_calendar($prop)
- {
- if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) {
- $ret = false;
- if (isset($prop['permanent']))
- $ret |= $cal->storage->subscribe(intval($prop['permanent']));
- if (isset($prop['active']))
- $ret |= $cal->storage->activate(intval($prop['active']));
-
- // apply to child folders, too
- if ($prop['recursive']) {
- foreach ((array)kolab_storage::list_folders($cal->storage->name, '*', 'event') 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));
+ if (!($filter & self::FILTER_ACTIVE) || !empty($prefs[$id]['active'])) {
+ $calendars[$id] = [
+ 'id' => $id,
+ 'name' => $this->cal->gettext('birthdays'),
+ 'listname' => $this->cal->gettext('birthdays'),
+ 'color' => !empty($prefs[$id]['color']) ? $prefs[$id]['color'] : '87CEFA',
+ 'active' => !empty($prefs[$id]['active']),
+ 'showalarms' => (bool) $this->rc->config->get('calendar_birthdays_alarm_type'),
+ 'group' => 'x-birthdays',
+ 'editable' => false,
+ 'default' => false,
+ 'children' => false,
+ 'history' => false,
+ ];
+ }
}
- }
- return $ret;
- }
- else {
- // save state in local prefs
- $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
- $prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active'];
- $this->rc->user->save_prefs($prefs);
- return true;
+
+ return $calendars;
}
- return false;
- }
+ /**
+ * Get list of calendars according to specified filters
+ *
+ * @param int Bitmask defining restrictions. See FILTER_* constants for possible values.
+ *
+ * @return array List of calendars
+ */
+ protected function filter_calendars($filter)
+ {
+ $this->_read_calendars();
+ $calendars = [];
- /**
- * Delete the given calendar with all its contents
- *
- * @see calendar_driver::delete_calendar()
- */
- public function delete_calendar($prop)
- {
- if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
- $folder = $cal->get_realname();
- // TODO: unsubscribe if no admin rights
- if (kolab_storage::folder_delete($folder)) {
- // remove color in user prefs (temp. solution)
- $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
- unset($prefs['kolab_calendars'][$prop['id']]);
+ $plugin = $this->rc->plugins->exec_hook('calendar_list_filter', [
+ 'list' => $this->calendars,
+ 'calendars' => $calendars,
+ 'filter' => $filter,
+ ]);
+
+ if ($plugin['abort']) {
+ return $plugin['calendars'];
+ }
+
+ $personal = $filter & self::FILTER_PERSONAL;
+ $shared = $filter & self::FILTER_SHARED;
+
+ foreach ($this->calendars as $cal) {
+ if (!$cal->ready) {
+ continue;
+ }
+ if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) {
+ continue;
+ }
+ if (($filter & self::FILTER_INSERTABLE) && !$cal->editable) {
+ continue;
+ }
+ if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) {
+ continue;
+ }
+ if (($filter & self::FILTER_PRIVATE) && $cal->subtype != 'private') {
+ continue;
+ }
+ if (($filter & self::FILTER_CONFIDENTIAL) && $cal->subtype != 'confidential') {
+ continue;
+ }
+ if ($personal || $shared) {
+ $ns = $cal->get_namespace();
+ if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) {
+ continue;
+ }
+ }
+
+ $calendars[$cal->id] = $cal;
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Get the kolab_calendar instance for the given calendar ID
+ *
+ * @param string Calendar identifier (encoded imap folder name)
+ *
+ * @return kolab_calendar Object nor null if calendar doesn't exist
+ */
+ public function get_calendar($id)
+ {
+ $this->_read_calendars();
+
+ // create calendar object if necesary
+ if (empty($this->calendars[$id])) {
+ if (in_array($id, [self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED])) {
+ return new kolab_invitation_calendar($id, $this->cal);
+ }
+
+ // for unsubscribed calendar folders
+ if ($id !== self::BIRTHDAY_CALENDAR_ID) {
+ $calendar = kolab_calendar::factory($id, $this->cal);
+ if ($calendar->ready) {
+ $this->calendars[$calendar->id] = $calendar;
+ }
+ }
+ }
+
+ return !empty($this->calendars[$id]) ? $this->calendars[$id] : null;
+ }
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ *
+ * @return mixed ID of the calendar on success, False on error
+ */
+ public function create_calendar($prop)
+ {
+ $prop['type'] = 'event';
+ $prop['active'] = true;
+ $prop['subscribed'] = true;
+
+ $folder = kolab_storage::folder_update($prop);
+
+ if ($folder === false) {
+ $this->last_error = $this->cal->gettext(kolab_storage::$last_error);
+ return false;
+ }
+
+ // create ID
+ $id = kolab_storage::folder_id($folder);
+
+ // save color in user prefs (temp. solution)
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
+
+ if (isset($prop['color'])) {
+ $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
+ }
+
+ if (isset($prop['showalarms'])) {
+ $prefs['kolab_calendars'][$id]['showalarms'] = !empty($prop['showalarms']);
+ }
+
+ if (!empty($prefs['kolab_calendars'][$id])) {
+ $this->rc->user->save_prefs($prefs);
+ }
+
+ return $id;
+ }
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function edit_calendar($prop)
+ {
+ if (!empty($prop['id']) && ($cal = $this->get_calendar($prop['id']))) {
+ $id = $cal->update($prop);
+ }
+ else {
+ $id = $prop['id'];
+ }
+
+ // fallback to local prefs
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
+ unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']);
+
+ if (isset($prop['color'])) {
+ $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
+ }
+
+ if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID) {
+ $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
+ }
+ else if (isset($prop['showalarms'])) {
+ $prefs['kolab_calendars'][$id]['showalarms'] = !empty($prop['showalarms']);
+ }
+
+ if (!empty($prefs['kolab_calendars'][$id])) {
+ $this->rc->user->save_prefs($prefs);
+ }
- $this->rc->user->save_prefs($prefs);
return true;
- }
- else
- $this->last_error = kolab_storage::$last_error;
}
- return false;
- }
+ /**
+ * Set active/subscribed state of a calendar
+ *
+ * @see calendar_driver::subscribe_calendar()
+ */
+ public function subscribe_calendar($prop)
+ {
+ if (!empty($prop['id']) && ($cal = $this->get_calendar($prop['id'])) && !empty($cal->storage)) {
+ $ret = false;
+ if (isset($prop['permanent'])) {
+ $ret |= $cal->storage->subscribe(intval($prop['permanent']));
+ }
+ if (isset($prop['active'])) {
+ $ret |= $cal->storage->activate(intval($prop['active']));
+ }
+ // apply to child folders, too
+ if (!empty($prop['recursive'])) {
+ foreach ((array) kolab_storage::list_folders($cal->storage->name, '*', 'event') as $subfolder) {
+ if (isset($prop['permanent'])) {
+ if ($prop['permanent']) {
+ kolab_storage::folder_subscribe($subfolder);
+ }
+ else {
+ kolab_storage::folder_unsubscribe($subfolder);
+ }
+ }
- /**
- * Search for shared or otherwise not listed calendars the user has access
- *
- * @param string Search string
- * @param string Section/source to search
- * @return array List of calendars
- */
- public function search_calendars($query, $source)
- {
- if (!kolab_storage::setup())
- return array();
-
- $this->calendars = array();
- $this->search_more_results = false;
-
- // find unsubscribed IMAP folders that have "event" type
- if ($source == 'folders') {
- foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
- $calendar = new kolab_calendar($folder->name, $this->cal);
- $this->calendars[$calendar->id] = $calendar;
- }
- }
- // find other user's virtual calendars
- 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, $count) as $user) {
- $calendar = new kolab_user_calendar($user, $this->cal);
- $this->calendars[$calendar->id] = $calendar;
-
- // search for calendar folders shared by this user
- foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
- $cal = new kolab_calendar($foldername, $this->cal);
- $this->calendars[$cal->id] = $cal;
- $calendar->subscriptions = true;
+ if (isset($prop['active'])) {
+ if ($prop['active']) {
+ kolab_storage::folder_activate($subfolder);
+ }
+ else {
+ kolab_storage::folder_deactivate($subfolder);
+ }
+ }
+ }
+ }
+ return $ret;
}
- }
-
- if ($count > $limit) {
- $this->search_more_results = true;
- }
- }
-
- // don't list the birthday calendar
- $this->rc->config->set('calendar_contact_birthdays', false);
- $this->rc->config->set('kolab_invitation_calendars', false);
-
- return $this->list_calendars();
- }
-
-
- /**
- * Fetch a single event
- *
- * @see calendar_driver::get_event()
- * @return array Hash array with event properties, false if not found
- */
- public function get_event($event, $scope = 0, $full = false)
- {
- if (is_array($event)) {
- $id = $event['id'] ?: $event['uid'];
- $cal = $event['calendar'];
-
- // we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
- if (!$event['id'] && $event['_instance']) {
- $id .= '-' . $event['_instance'];
- }
- }
- else {
- $id = $event;
- }
-
- if ($cal) {
- if ($storage = $this->get_calendar($cal)) {
- $result = $storage->get_event($id);
- return self::to_rcube_event($result);
- }
- // get event from the address books birthday calendar
- else if ($cal == self::BIRTHDAY_CALENDAR_ID) {
- return $this->get_birthday_event($id);
- }
- }
- // iterate over all calendar folders and search for the event ID
- else {
- foreach ($this->filter_calendars($scope) as $calendar) {
- if ($result = $calendar->get_event($id)) {
- return self::to_rcube_event($result);
+ else {
+ // save state in local prefs
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
+ $prefs['kolab_calendars'][$prop['id']]['active'] = !empty($prop['active']);
+ $this->rc->user->save_prefs($prefs);
+ return true;
}
- }
- }
- return false;
- }
-
- /**
- * Add a single event to the database
- *
- * @see calendar_driver::new_event()
- */
- public function new_event($event)
- {
- if (!$this->validate($event))
- return false;
-
- $event = self::from_rcube_event($event);
-
- if (!$event['calendar']) {
- $this->_read_calendars();
- $event['calendar'] = reset(array_keys($this->calendars));
- }
-
- if ($storage = $this->get_calendar($event['calendar'])) {
- // if this is a recurrence instance, append as exception to an already existing object for this UID
- if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
- self::add_exception($master, $event);
- $success = $storage->update_event($master);
- }
- else {
- $success = $storage->insert_event($event);
- }
-
- if ($success && $this->freebusy_trigger) {
- $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
- $this->freebusy_trigger = false; // disable after first execution (#2355)
- }
-
- return $success;
- }
-
- return false;
- }
-
- /**
- * Update an event entry with the given data
- *
- * @see calendar_driver::new_event()
- * @return boolean True on success, False on error
- */
- public function edit_event($event)
- {
- if (!($storage = $this->get_calendar($event['calendar'])))
- return false;
-
- return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
- }
-
- /**
- * Extended event editing with possible changes to the argument
- *
- * @param array Hash array with event properties
- * @param string New participant status
- * @param array List of hash arrays with updated attendees
- * @return boolean True on success, False on error
- */
- public function edit_rsvp(&$event, $status, $attendees)
- {
- $update_event = $event;
-
- // apply changes to master (and all exceptions)
- if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
- if ($storage = $this->get_calendar($event['calendar'])) {
- $update_event = $storage->get_event($event['recurrence_id']);
- $update_event['_savemode'] = $event['_savemode'];
- $update_event['id'] = $update_event['uid'];
- unset($update_event['recurrence_id']);
- calendar::merge_attendee_data($update_event, $attendees);
- }
- }
-
- if ($ret = $this->update_attendees($update_event, $attendees)) {
- // replace with master event (for iTip reply)
- $event = self::to_rcube_event($update_event);
-
- // re-assign to the according (virtual) calendar
- if ($this->rc->config->get('kolab_invitation_calendars')) {
- if (strtoupper($status) == 'DECLINED')
- $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
- else if (strtoupper($status) == 'NEEDS-ACTION')
- $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
- else if ($event['_folder_id'])
- $event['calendar'] = $event['_folder_id'];
- }
- }
-
- return $ret;
- }
-
- /**
- * Update the participant status for the given attendees
- *
- * @see calendar_driver::update_attendees()
- */
- public function update_attendees(&$event, $attendees)
- {
- // for this-and-future updates, merge the updated attendees onto all exceptions in range
- if (($event['_savemode'] == 'future' && $event['recurrence_id']) || (!empty($event['recurrence']) && !$event['recurrence_id'])) {
- if (!($storage = $this->get_calendar($event['calendar'])))
return false;
+ }
- // load master event
- $master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @see calendar_driver::delete_calendar()
+ */
+ public function delete_calendar($prop)
+ {
+ if (!empty($prop['id']) && ($cal = $this->get_calendar($prop['id']))) {
+ $folder = $cal->get_realname();
+
+ // TODO: unsubscribe if no admin rights
+ if (kolab_storage::folder_delete($folder)) {
+ // remove color in user prefs (temp. solution)
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
+ unset($prefs['kolab_calendars'][$prop['id']]);
+
+ $this->rc->user->save_prefs($prefs);
+ return true;
+ }
+ else {
+ $this->last_error = kolab_storage::$last_error;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ *
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ if (!kolab_storage::setup()) {
+ return [];
+ }
+
+ $this->calendars = [];
+ $this->search_more_results = false;
+
+ // find unsubscribed IMAP folders that have "event" type
+ if ($source == 'folders') {
+ foreach ((array) kolab_storage::search_folders('event', $query, ['other']) as $folder) {
+ $calendar = new kolab_calendar($folder->name, $this->cal);
+ $this->calendars[$calendar->id] = $calendar;
+ }
+ }
+ // find other user's virtual calendars
+ else if ($source == 'users') {
+ // we have slightly more space, so display twice the number
+ $limit = $this->rc->config->get('autocomplete_max', 15) * 2;
+
+ foreach (kolab_storage::search_users($query, 0, [], $limit, $count) as $user) {
+ $calendar = new kolab_user_calendar($user, $this->cal);
+ $this->calendars[$calendar->id] = $calendar;
+
+ // search for calendar folders shared by this user
+ foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ $calendar->subscriptions = true;
+ }
+ }
+
+ if ($count > $limit) {
+ $this->search_more_results = true;
+ }
+ }
+
+ // don't list the birthday calendar
+ $this->rc->config->set('calendar_contact_birthdays', false);
+ $this->rc->config->set('kolab_invitation_calendars', false);
+
+ return $this->list_calendars();
+ }
+
+ /**
+ * Fetch a single event
+ *
+ * @see calendar_driver::get_event()
+ * @return array Hash array with event properties, false if not found
+ */
+ public function get_event($event, $scope = 0, $full = false)
+ {
+ if (is_array($event)) {
+ $id = !empty($event['id']) ? $event['id'] : $event['uid'];
+ $cal = $event['calendar'];
+
+ // we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
+ if (empty($event['id']) && !empty($event['_instance'])) {
+ $id .= '-' . $event['_instance'];
+ }
+ }
+ else {
+ $id = $event;
+ }
+
+ if (!empty($cal)) {
+ if ($storage = $this->get_calendar($cal)) {
+ $result = $storage->get_event($id);
+ return self::to_rcube_event($result);
+ }
+
+ // get event from the address books birthday calendar
+ if ($cal == self::BIRTHDAY_CALENDAR_ID) {
+ return $this->get_birthday_event($id);
+ }
+ }
+ // iterate over all calendar folders and search for the event ID
+ else {
+ foreach ($this->filter_calendars($scope) as $calendar) {
+ if ($result = $calendar->get_event($id)) {
+ return self::to_rcube_event($result);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add a single event to the database
+ *
+ * @see calendar_driver::new_event()
+ */
+ public function new_event($event)
+ {
+ if (!$this->validate($event)) {
+ return false;
+ }
+
+ $event = self::from_rcube_event($event);
+
+ if (!$event['calendar']) {
+ $this->_read_calendars();
+ $cal_ids = array_keys($this->calendars);
+ $event['calendar'] = reset($cal_ids);
+ }
+
+ if ($storage = $this->get_calendar($event['calendar'])) {
+ // if this is a recurrence instance, append as exception to an already existing object for this UID
+ if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
+ self::add_exception($master, $event);
+ $success = $storage->update_event($master);
+ }
+ else {
+ $success = $storage->insert_event($event);
+ }
+
+ if ($success && $this->freebusy_trigger) {
+ $this->rc->output->command('plugin.ping_url', ['action' => 'calendar/push-freebusy', 'source' => $storage->id]);
+ $this->freebusy_trigger = false; // disable after first execution (#2355)
+ }
+
+ return $success;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @see calendar_driver::new_event()
+ * @return bool True on success, False on error
+ */
+ public function edit_event($event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar']))) {
+ return false;
+ }
+
+ return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
+ }
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ *
+ * @return bool True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && !empty($event['recurrence_id'])) {
+ if ($storage = $this->get_calendar($event['calendar'])) {
+ $update_event = $storage->get_event($event['recurrence_id']);
+ $update_event['_savemode'] = $event['_savemode'];
+ $update_event['id'] = $update_event['uid'];
+ unset($update_event['recurrence_id']);
+ calendar::merge_attendee_data($update_event, $attendees);
+ }
+ }
+
+ if ($ret = $this->update_attendees($update_event, $attendees)) {
+ // replace with master event (for iTip reply)
+ $event = self::to_rcube_event($update_event);
+
+ // re-assign to the according (virtual) calendar
+ if ($this->rc->config->get('kolab_invitation_calendars')) {
+ if (strtoupper($status) == 'DECLINED') {
+ $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
+ }
+ else if (strtoupper($status) == 'NEEDS-ACTION') {
+ $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
+ }
+ else if (!empty($event['_folder_id'])) {
+ $event['calendar'] = $event['_folder_id'];
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ // for this-and-future updates, merge the updated attendees onto all exceptions in range
+ if (
+ ($event['_savemode'] == 'future' && !empty($event['recurrence_id']))
+ || (!empty($event['recurrence']) && empty($event['recurrence_id']))
+ ) {
+ if (!($storage = $this->get_calendar($event['calendar']))) {
+ return false;
+ }
+
+ // load master event
+ $master = !empty($event['recurrence_id']) ? $storage->get_event($event['recurrence_id']) : $event;
+
+ // apply attendee update to each existing exception
+ if (!empty($master['recurrence']) && !empty($master['recurrence']['EXCEPTIONS'])) {
+ $saved = false;
+ foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ // merge the new event properties onto future exceptions
+ if ($exception['_instance'] >= strval($event['_instance'])) {
+ calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
+ }
+ // update a specific instance
+ if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
+ $saved = true;
+ }
+ }
+
+ // add the given event as new exception
+ if (!$saved && $event['id'] != $master['id']) {
+ $event['thisandfuture'] = true;
+ $master['recurrence']['EXCEPTIONS'][] = $event;
+ }
+
+ // set link to top-level exceptions
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+
+ return $this->update_event($master);
+ }
+ }
+
+ // just update the given event (instance)
+ return $this->update_event($event);
+ }
+
+ /**
+ * Move a single event
+ *
+ * @see calendar_driver::move_event()
+ * @return boolean True on success, False on error
+ */
+ public function move_event($event)
+ {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
+ unset($ev['sequence']);
+ self::clear_attandee_noreply($ev);
+
+ return $this->update_event($event + $ev);
+ }
+
+ return false;
+ }
+
+ /**
+ * Resize a single event
+ *
+ * @see calendar_driver::resize_event()
+ * @return boolean True on success, False on error
+ */
+ public function resize_event($event)
+ {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
+ unset($ev['sequence']);
+ self::clear_attandee_noreply($ev);
+
+ return $this->update_event($event + $ev);
+ }
+
+ return false;
+ }
+
+ /**
+ * Remove a single event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * @param bool Remove record(s) irreversible (mark as deleted otherwise)
+ *
+ * @return bool True on success, False on error
+ */
+ public function remove_event($event, $force = true)
+ {
+ $ret = true;
+ $success = false;
+
+ if (!$force) {
+ unset($event['attendees']);
+ $this->rc->session->remove('calendar_event_undo');
+ $this->rc->session->remove('calendar_restore_event_data');
+ $sess_data = $event;
+ }
+
+ if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
+ $decline = $event['_decline'];
+ $savemode = 'all';
+ $master = $event;
+
+ // read master if deleting a recurring event
+ if (!empty($event['recurrence']) || !empty($event['recurrence_id']) || !empty($event['isexception'])) {
+ $master = $storage->get_event($event['uid']);
+
+ if (!empty($event['_savemode'])) {
+ $savemode = $event['_savemode'];
+ }
+ else if (!empty($event['_instance']) || !empty($event['isexception'])) {
+ $savemode = 'current';
+ }
+
+ // force 'current' mode for single occurrences stored as exception
+ if (empty($event['recurrence']) && empty($event['recurrence_id']) && !empty($event['isexception'])) {
+ $savemode = 'current';
+ }
+ }
+
+ // removing an exception instance
+ if ((!empty($event['recurrence_id']) || !empty($event['isexception'])) && !empty(($master['exceptions']))) {
+ foreach ($master['exceptions'] as $i => $exception) {
+ if ($exception['_instance'] == $event['_instance']) {
+ unset($master['exceptions'][$i]);
+ // set event date back to the actual occurrence
+ if (!empty($exception['recurrence_date'])) {
+ $event['start'] = $exception['recurrence_date'];
+ }
+ }
+ }
+
+ if (!empty($master['recurrence'])) {
+ $master['recurrence']['EXCEPTIONS'] = &$master['exceptions'];
+ }
+ }
+
+ switch ($savemode) {
+ case 'current':
+ $_SESSION['calendar_restore_event_data'] = $master;
+
+ // remove the matching RDATE entry
+ if (!empty($master['recurrence']['RDATE'])) {
+ foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
+ unset($master['recurrence']['RDATE'][$j]);
+ break;
+ }
+ }
+ }
+
+ // add exception to master event
+ $master['recurrence']['EXDATE'][] = $event['start'];
+
+ $success = $storage->update_event($master);
+ break;
+
+ case 'future':
+ $master['_instance'] = libcalendaring::recurrence_instance_identifier($master);
+ if ($master['_instance'] != $event['_instance']) {
+ $_SESSION['calendar_restore_event_data'] = $master;
+
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+
+ // if all future instances are deleted, remove recurrence rule entirely (bug #1677)
+ if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
+ $master['recurrence'] = [];
+ }
+ // remove matching RDATE entries
+ else if (!empty($master['recurrence']['RDATE'])) {
+ foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
+ $master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
+ break;
+ }
+ }
+ }
+
+ $success = $storage->update_event($master);
+ $ret = $master['uid'];
+ break;
+ }
+
+ default: // 'all' is default
+ // removing the master event with loose exceptions (not recurring though)
+ if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
+ // make the first exception the new master
+ $newmaster = array_shift($master['exceptions']);
+ $newmaster['exceptions'] = $master['exceptions'];
+ $newmaster['_attachments'] = $master['_attachments'];
+ $newmaster['_mailbox'] = $master['_mailbox'];
+ $newmaster['_msguid'] = $master['_msguid'];
+
+ $success = $storage->update_event($newmaster);
+ }
+ else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
+ // don't delete but set PARTSTAT=DECLINED
+ if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
+ $success = $storage->update_event($master);
+ }
+ }
+
+ if (!$success) {
+ $success = $storage->delete_event($master, $force);
+ }
+ break;
+ }
+ }
+
+ if ($success && !$force) {
+ if (!empty($master['_folder_id'])) {
+ $sess_data['_folder_id'] = $master['_folder_id'];
+ }
+ $_SESSION['calendar_event_undo'] = ['ts' => time(), 'data' => $sess_data];
+ }
+
+ if ($success && $this->freebusy_trigger) {
+ $this->rc->output->command('plugin.ping_url', [
+ 'action' => 'calendar/push-freebusy',
+ // _folder_id may be set by invitations calendar
+ 'source' => !empty($master['_folder_id']) ? $master['_folder_id'] : $storage->id,
+ ]);
+ }
+
+ return $success ? $ret : false;
+ }
+
+ /**
+ * Restore a single deleted event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * calendar: Event calendar
+ *
+ * @return bool True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ if ($storage = $this->get_calendar($event['calendar'])) {
+ if (!empty($_SESSION['calendar_restore_event_data'])) {
+ $success = $storage->update_event($event = $_SESSION['calendar_restore_event_data']);
+ }
+ else {
+ $success = $storage->restore_event($event);
+ }
+
+ if ($success && $this->freebusy_trigger) {
+ $this->rc->output->command('plugin.ping_url', [
+ 'action' => 'calendar/push-freebusy',
+ // _folder_id may be set by invitations calendar
+ 'source' => !empty($event['_folder_id']) ? $event['_folder_id'] : $storage->id,
+ ]);
+ }
+
+ return $success;
+ }
+
+ return false;
+ }
+
+ /**
+ * Wrapper to update an event object depending on the given savemode
+ */
+ private function update_event($event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar']))) {
+ return false;
+ }
+
+ // move event to another folder/calendar
+ if (!empty($event['_fromcalendar']) && $event['_fromcalendar'] != $event['calendar']) {
+ if (!($fromcalendar = $this->get_calendar($event['_fromcalendar']))) {
+ return false;
+ }
+
+ $old = $fromcalendar->get_event($event['id']);
+
+ if ($event['_savemode'] != 'new') {
+ if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
+ return false;
+ }
+
+ $fromcalendar = $storage;
+ }
+ }
+ else {
+ $fromcalendar = $storage;
+ }
+
+ $success = false;
+ $savemode = 'all';
+ $attachments = [];
+ $old = $master = $storage->get_event($event['id']);
+
+ if (!$old || empty($old['start'])) {
+ rcube::raise_error([
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to load event object to update: id=" . $event['id']
+ ],
+ true, false
+ );
+ return false;
+ }
+
+ // modify a recurring event, check submitted savemode to do the right things
+ if (!empty($old['recurrence']) || !empty($old['recurrence_id']) || !empty($old['isexception'])) {
+ $master = $storage->get_event($old['uid']);
+
+ if (!empty($event['_savemode'])) {
+ $savemode = $event['_savemode'];
+ }
+ else {
+ $savemode = (!empty($old['recurrence_id']) || !empty($old['isexception'])) ? 'current' : 'all';
+ }
+
+ // this-and-future on the first instance equals to 'all'
+ if ($savemode == 'future' && !empty($master['start'])
+ && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master)
+ ) {
+ $savemode = 'all';
+ }
+ // force 'current' mode for single occurrences stored as exception
+ else if (empty($old['recurrence']) && empty($old['recurrence_id']) && !empty($old['isexception'])) {
+ $savemode = 'current';
+ }
+
+ // Stick to the master timezone for all occurrences (Bifrost#T104637)
+ $master_tz = $master['start']->getTimezone();
+ $event_tz = $event['start']->getTimezone();
+
+ if ($master_tz->getName() != $event_tz->getName()) {
+ $event['start']->setTimezone($master_tz);
+ $event['end']->setTimezone($master_tz);
+ }
+ }
+
+ // check if update affects scheduling and update attendee status accordingly
+ $reschedule = $this->check_scheduling($event, $old, true);
+
+ // keep saved exceptions (not submitted by the client)
+ if (!empty($old['recurrence']['EXDATE']) && !isset($event['recurrence']['EXDATE'])) {
+ $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
+ }
+
+ if (isset($event['recurrence']['EXCEPTIONS'])) {
+ // exceptions already provided (e.g. from iCal import)
+ $with_exceptions = true;
+ }
+ else if (!empty($old['recurrence']['EXCEPTIONS'])) {
+ $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
+ }
+ else if (!empty($old['exceptions'])) {
+ $event['exceptions'] = $old['exceptions'];
+ }
+
+ // remove some internal properties which should not be saved
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
+ $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']
+ );
+
+ switch ($savemode) {
+ case 'new':
+ // save submitted data as new (non-recurring) event
+ $event['recurrence'] = [];
+ $event['_copyfrom'] = $master['_msguid'];
+ $event['_mailbox'] = $master['_mailbox'];
+ $event['uid'] = $this->cal->generate_uid();
+
+ unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
+
+ // copy attachment metadata to new event
+ $event = self::from_rcube_event($event, $master);
+
+ self::clear_attandee_noreply($event);
+ if ($success = $storage->insert_event($event)) {
+ $success = $event['uid'];
+ }
+ break;
+
+ case 'future':
+ // create a new recurring event
+ $event['_copyfrom'] = $master['_msguid'];
+ $event['_mailbox'] = $master['_mailbox'];
+ $event['uid'] = $this->cal->generate_uid();
+
+ unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
+
+ // copy attachment metadata to new event
+ $event = self::from_rcube_event($event, $master);
+
+ // remove recurrence exceptions on re-scheduling
+ if ($reschedule) {
+ unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
+ }
+ else if (isset($event['recurrence']['EXCEPTIONS']) && is_array($event['recurrence']['EXCEPTIONS'])) {
+ // only keep relevant exceptions
+ $event['recurrence']['EXCEPTIONS'] = array_filter(
+ $event['recurrence']['EXCEPTIONS'],
+ function($exception) use ($event) {
+ return $exception['start'] > $event['start'];
+ }
+ );
+ if (isset($event['recurrence']['EXDATE']) && is_array($event['recurrence']['EXDATE'])) {
+ $event['recurrence']['EXDATE'] = array_filter(
+ $event['recurrence']['EXDATE'],
+ function($exdate) use ($event) {
+ return $exdate > $event['start'];
+ }
+ );
+ }
+ // set link to top-level exceptions
+ $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+ }
+
+ // compute remaining occurrences
+ if ($event['recurrence']['COUNT']) {
+ if (empty($old['_count'])) {
+ $old['_count'] = $this->get_recurrence_count($master, $old['start']);
+ }
+ $event['recurrence']['COUNT'] -= intval($old['_count']);
+ }
+
+ // remove fixed weekday when date changed
+ if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) {
+ if (!empty($event['recurrence']['BYDAY']) && strlen($event['recurrence']['BYDAY']) == 2) {
+ unset($event['recurrence']['BYDAY']);
+ }
+ if (!empty($old['recurrence']['BYMONTH']) && $old['recurrence']['BYMONTH'] == $old['start']->format('n')) {
+ unset($event['recurrence']['BYMONTH']);
+ }
+ }
+
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $old['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+
+ // remove all exceptions after $event['start']
+ if (isset($master['recurrence']['EXCEPTIONS']) && is_array($master['recurrence']['EXCEPTIONS'])) {
+ $master['recurrence']['EXCEPTIONS'] = array_filter(
+ $master['recurrence']['EXCEPTIONS'],
+ function($exception) use ($event) {
+ return $exception['start'] < $event['start'];
+ }
+ );
+ // set link to top-level exceptions
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+ }
+
+ if (isset($master['recurrence']['EXDATE']) && is_array($master['recurrence']['EXDATE'])) {
+ $master['recurrence']['EXDATE'] = array_filter(
+ $master['recurrence']['EXDATE'],
+ function($exdate) use ($event) {
+ return $exdate < $event['start'];
+ }
+ );
+ }
+
+ // save new event
+ if ($success = $storage->insert_event($event)) {
+ $success = $event['uid'];
+
+ // update master event (no rescheduling!)
+ self::clear_attandee_noreply($master);
+ $storage->update_event($master);
+ }
+ break;
+
+ case 'current':
+ // recurring instances shall not store recurrence rules and attachments
+ $event['recurrence'] = [];
+ $event['thisandfuture'] = $savemode == 'future';
+ unset($event['attachments'], $event['id']);
+
+ // increment sequence of this instance if scheduling is affected
+ if ($reschedule) {
+ $event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
+ }
+ else if (!isset($event['sequence'])) {
+ $event['sequence'] = !empty($old['sequence']) ? $old['sequence'] : $master['sequence'];
+ }
+
+ // save properties to a recurrence exception instance
+ if (!empty($old['_instance']) && isset($master['recurrence']['EXCEPTIONS'])) {
+ if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
+ $success = $storage->update_event($master, $old['id']);
+ break;
+ }
+ }
+
+ $add_exception = true;
+
+ // adjust matching RDATE entry if dates changed
+ if (
+ !empty($master['recurrence']['RDATE'])
+ && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')
+ ) {
+ foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $old_date) {
+ $master['recurrence']['RDATE'][$j] = $event['start'];
+ sort($master['recurrence']['RDATE']);
+ $add_exception = false;
+ break;
+ }
+ }
+ }
+
+ // save as new exception to master event
+ if ($add_exception) {
+ self::add_exception($master, $event, $old);
+ }
+
+ $success = $storage->update_event($master);
+ break;
+
+ default: // 'all' is the default
+ $event['id'] = $master['uid'];
+ $event['uid'] = $master['uid'];
+
+ // use start date from master but try to be smart on time or duration changes
+ $old_start_date = $old['start']->format('Y-m-d');
+ $old_start_time = !empty($old['allday']) ? '' : $old['start']->format('H:i');
+ $old_duration = self::event_duration($old['start'], $old['end'], !empty($old['allday']));
+
+ $new_start_date = $event['start']->format('Y-m-d');
+ $new_start_time = !empty($event['allday']) ? '' : $event['start']->format('H:i');
+ $new_duration = self::event_duration($event['start'], $event['end'], !empty($event['allday']));
+
+ $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
+
+ // shifted or resized
+ if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
+ $event['start'] = $master['start']->add($date_shift);
+ $event['end'] = clone $event['start'];
+ $event['end']->add(new DateInterval($new_duration));
+
+ // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
+ if ($old_start_date != $new_start_date && !empty($event['recurrence'])) {
+ if (!empty($event['recurrence']['BYDAY']) && strlen($event['recurrence']['BYDAY']) == 2)
+ unset($event['recurrence']['BYDAY']);
+ if (!empty($old['recurrence']['BYMONTH']) && $old['recurrence']['BYMONTH'] == $old['start']->format('n'))
+ unset($event['recurrence']['BYMONTH']);
+ }
+ }
+ // dates did not change, use the ones from master
+ else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
+ $event['start'] = $master['start'];
+ $event['end'] = $master['end'];
+ }
+
+ // when saving an instance in 'all' mode, copy recurrence exceptions over
+ if (!empty($old['recurrence_id'])) {
+ $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
+ $event['recurrence']['EXDATE'] = $master['recurrence']['EXDATE'];
+ }
+ else if (!empty($master['_instance'])) {
+ $event['_instance'] = $master['_instance'];
+ $event['recurrence_date'] = $master['recurrence_date'];
+ }
+
+ // TODO: forward changes to exceptions (which do not yet have differing values stored)
+ if (!empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
+ // determine added and removed attendees
+ $old_attendees = $current_attendees = $added_attendees = [];
+
+ if (!empty($old['attendees'])) {
+ foreach ((array) $old['attendees'] as $attendee) {
+ $old_attendees[] = $attendee['email'];
+ }
+ }
+
+ if (!empty($event['attendees'])) {
+ foreach ((array) $event['attendees'] as $attendee) {
+ $current_attendees[] = $attendee['email'];
+ if (!in_array($attendee['email'], $old_attendees)) {
+ $added_attendees[] = $attendee;
+ }
+ }
+ }
+
+ $removed_attendees = array_diff($old_attendees, $current_attendees);
+
+ foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+
+ foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ if (isset($exception['recurrence_date']) && is_a($exception['recurrence_date'], 'DateTime')) {
+ $recurrence_id = $exception['recurrence_date'];
+ }
+ else {
+ $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ }
+
+ if ($recurrence_id instanceof DateTime) {
+ $recurrence_id->add($date_shift);
+ $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
+ $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
+ }
+ }
+ }
+
+ // set link to top-level exceptions
+ $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+ }
+
+ // unset _dateonly flags in (cached) date objects
+ unset($event['start']->_dateonly, $event['end']->_dateonly);
+
+ $success = $storage->update_event($event) ? $event['id'] : false; // return master UID
+ break;
+ }
+
+ if ($success && $this->freebusy_trigger) {
+ $this->rc->output->command('plugin.ping_url', [
+ 'action' => 'calendar/push-freebusy',
+ 'source' => $storage->id
+ ]);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Calculate event duration, returns string in DateInterval format
+ */
+ protected static function event_duration($start, $end, $allday = false)
+ {
+ if ($allday) {
+ $diff = $start->diff($end);
+ return 'P' . $diff->days . 'D';
+ }
+
+ return 'PT' . ($end->format('U') - $start->format('U')) . 'S';
+ }
+
+ /**
+ * Determine whether the current change affects scheduling and reset attendee status accordingly
+ */
+ public function check_scheduling(&$event, $old, $update = true)
+ {
+ // skip this check when importing iCal/iTip events
+ if (isset($event['sequence']) || !empty($event['_method'])) {
+ return false;
+ }
+
+ // iterate through the list of properties considered 'significant' for scheduling
+ $kolab_event = !empty($old['_formatobj']) ? $old['_formatobj'] : new kolab_format_event();
+ $reschedule = $kolab_event->check_rescheduling($event, $old);
+
+ // reset all attendee status to needs-action (#4360)
+ if ($update && $reschedule && !empty($event['attendees'])) {
+ $is_organizer = false;
+ $emails = $this->cal->get_user_emails();
+ $attendees = $event['attendees'];
+
+ foreach ($attendees as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER'
+ && !empty($attendee['email'])
+ && in_array(strtolower($attendee['email']), $emails)
+ ) {
+ $is_organizer = true;
+ }
+ else if ($attendee['role'] != 'ORGANIZER'
+ && $attendee['role'] != 'NON-PARTICIPANT'
+ && $attendee['status'] != 'DELEGATED'
+ ) {
+ $attendees[$i]['status'] = 'NEEDS-ACTION';
+ $attendees[$i]['rsvp'] = true;
+ }
+ }
+
+ // update attendees only if I'm the organizer
+ if ($is_organizer || (!empty($event['organizer']) && in_array(strtolower($event['organizer']['email']), $emails))) {
+ $event['attendees'] = $attendees;
+ }
+ }
+
+ return $reschedule;
+ }
+
+ /**
+ * Apply the given changes to already existing exceptions
+ */
+ protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
+ {
+ $saved = false;
+ $existing = null;
+
+ // determine added and removed attendees
+ $added_attendees = $removed_attendees = [];
+
+ if ($savemode == 'future') {
+ $old_attendees = $current_attendees = [];
+
+ if (!empty($old['attendees'])) {
+ foreach ((array) $old['attendees'] as $attendee) {
+ $old_attendees[] = $attendee['email'];
+ }
+ }
+
+ if (!empty($event['attendees'])) {
+ foreach ((array) $event['attendees'] as $attendee) {
+ $current_attendees[] = $attendee['email'];
+ if (!in_array($attendee['email'], $old_attendees)) {
+ $added_attendees[] = $attendee;
+ }
+ }
+ }
+
+ $removed_attendees = array_diff($old_attendees, $current_attendees);
+ }
- // apply attendee update to each existing exception
- if ($master['recurrence'] && !empty($master['recurrence']['EXCEPTIONS'])) {
- $saved = false;
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
- // merge the new event properties onto future exceptions
- if ($exception['_instance'] >= strval($event['_instance'])) {
- calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
- }
- // update a specific instance
- if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
- $saved = true;
- }
- }
+ // update a specific instance
+ if ($exception['_instance'] == $old['_instance']) {
+ $existing = $i;
- // add the given event as new exception
- if (!$saved && $event['id'] != $master['id']) {
- $event['thisandfuture'] = true;
- $master['recurrence']['EXCEPTIONS'][] = $event;
+ // check savemode against existing exception mode.
+ // if matches, we can update this existing exception
+ $thisandfuture = !empty($exception['thisandfuture']);
+ if ($thisandfuture === ($savemode == 'future')) {
+ $event['_instance'] = $old['_instance'];
+ $event['thisandfuture'] = $old['thisandfuture'];
+ $event['recurrence_date'] = $old['recurrence_date'];
+ $master['recurrence']['EXCEPTIONS'][$i] = $event;
+ $saved = true;
+ }
+ }
+
+ // merge the new event properties onto future exceptions
+ if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
+ unset($event['thisandfuture']);
+ self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, ['attendees']);
+
+ if (!empty($added_attendees) || !empty($removed_attendees)) {
+ calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
+ }
+ }
}
+/*
+ // we could not update the existing exception due to savemode mismatch...
+ if (!$saved && isset($existing) && !empty($master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture'])) {
+ // ... try to move the existing this-and-future exception to the next occurrence
+ foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
+ // our old this-and-future exception is obsolete
+ if (!empty($candidate['thisandfuture'])) {
+ unset($master['recurrence']['EXCEPTIONS'][$existing]);
+ $saved = true;
+ break;
+ }
+ // this occurrence doesn't yet have an exception
+ else if (empty($candidate['isexception'])) {
+ $event['_instance'] = $candidate['_instance'];
+ $event['recurrence_date'] = $candidate['recurrence_date'];
+ $master['recurrence']['EXCEPTIONS'][$i] = $event;
+ $saved = true;
+ break;
+ }
+ }
+ }
+*/
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
- return $this->update_event($master);
- }
+ // returning false here will add a new exception
+ return $saved;
}
- // just update the given event (instance)
- return $this->update_event($event);
- }
-
- /**
- * Move a single event
- *
- * @see calendar_driver::move_event()
- * @return boolean True on success, False on error
- */
- public function move_event($event)
- {
- if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
- unset($ev['sequence']);
- self::clear_attandee_noreply($ev);
- return $this->update_event($event + $ev);
- }
-
- return false;
- }
-
- /**
- * Resize a single event
- *
- * @see calendar_driver::resize_event()
- * @return boolean True on success, False on error
- */
- public function resize_event($event)
- {
- if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
- unset($ev['sequence']);
- self::clear_attandee_noreply($ev);
- return $this->update_event($event + $ev);
- }
-
- return false;
- }
-
- /**
- * Remove a single event
- *
- * @param array Hash array with event properties:
- * id: Event identifier
- * @param boolean Remove record(s) irreversible (mark as deleted otherwise)
- *
- * @return boolean True on success, False on error
- */
- public function remove_event($event, $force = true)
- {
- $ret = true;
- $success = false;
- $savemode = $event['_savemode'];
- $decline = $event['_decline'];
-
- if (!$force) {
- unset($event['attendees']);
- $this->rc->session->remove('calendar_event_undo');
- $this->rc->session->remove('calendar_restore_event_data');
- $sess_data = $event;
- }
-
- if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
- $event['_savemode'] = $savemode;
- $savemode = 'all';
- $master = $event;
-
- // read master if deleting a recurring event
- if ($event['recurrence'] || $event['recurrence_id'] || $event['isexception']) {
- $master = $storage->get_event($event['uid']);
- $savemode = $event['_savemode'] ?: ($event['_instance'] || $event['isexception'] ? 'current' : 'all');
-
- // force 'current' mode for single occurrences stored as exception
- if (!$event['recurrence'] && !$event['recurrence_id'] && $event['isexception'])
- $savemode = 'current';
- }
-
- // removing an exception instance
- if (($event['recurrence_id'] || $event['isexception']) && is_array($master['exceptions'])) {
- foreach ($master['exceptions'] as $i => $exception) {
- if ($exception['_instance'] == $event['_instance']) {
- unset($master['exceptions'][$i]);
- // set event date back to the actual occurrence
- if ($exception['recurrence_date'])
- $event['start'] = $exception['recurrence_date'];
- }
+ /**
+ * Add or update the given event as an exception to $master
+ */
+ public static function add_exception(&$master, $event, $old = null)
+ {
+ if ($old) {
+ $event['_instance'] = $old['_instance'];
+ if (empty($event['recurrence_date'])) {
+ $event['recurrence_date'] = !empty($old['recurrence_date']) ? $old['recurrence_date'] : $old['start'];
+ }
+ }
+ else if (empty($event['recurrence_date'])) {
+ $event['recurrence_date'] = $event['start'];
}
- if (is_array($master['recurrence'])) {
- $master['recurrence']['EXCEPTIONS'] = &$master['exceptions'];
+ if (empty($event['_instance']) && is_a($event['recurrence_date'], 'DateTime')) {
+ $event['_instance'] = libcalendaring::recurrence_instance_identifier($event, !empty($master['allday']));
}
- }
- switch ($savemode) {
- case 'current':
- $_SESSION['calendar_restore_event_data'] = $master;
+ if (!is_array($master['exceptions']) && isset($master['recurrence']['EXCEPTIONS'])) {
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+ }
- // remove the matching RDATE entry
- if ($master['recurrence']['RDATE']) {
- foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
- if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
- unset($master['recurrence']['RDATE'][$j]);
- break;
- }
+ $existing = false;
+ foreach ((array) $master['exceptions'] as $i => $exception) {
+ if ($exception['_instance'] == $event['_instance']) {
+ $master['exceptions'][$i] = $event;
+ $existing = true;
}
- }
+ }
- // add exception to master event
- $master['recurrence']['EXDATE'][] = $event['start'];
+ if (!$existing) {
+ $master['exceptions'][] = $event;
+ }
- $success = $storage->update_event($master);
- break;
+ return true;
+ }
- case 'future':
- $master['_instance'] = libcalendaring::recurrence_instance_identifier($master);
- if ($master['_instance'] != $event['_instance']) {
- $_SESSION['calendar_restore_event_data'] = $master;
-
- // set until-date on master event
- $master['recurrence']['UNTIL'] = clone $event['start'];
- $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
- unset($master['recurrence']['COUNT']);
-
- // if all future instances are deleted, remove recurrence rule entirely (bug #1677)
- if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
- $master['recurrence'] = array();
+ /**
+ * Remove the noreply flags from attendees
+ */
+ public static function clear_attandee_noreply(&$event)
+ {
+ if (!empty($event['attendees'])) {
+ foreach ((array) $event['attendees'] as $i => $attendee) {
+ unset($event['attendees'][$i]['noreply']);
}
- // remove matching RDATE entries
- else if ($master['recurrence']['RDATE']) {
- foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
- if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
- $master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
- break;
+ }
+ }
+
+ /**
+ * Merge certain properties from the overlay event to the base event object
+ *
+ * @param array The event object to be altered
+ * @param array The overlay event object to be merged over $event
+ * @param array List of properties not allowed to be overwritten
+ */
+ public static function merge_exception_data(&$event, $overlay, $blacklist = null)
+ {
+ $forbidden = ['id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments'];
+
+ if (is_array($blacklist)) {
+ $forbidden = array_merge($forbidden, $blacklist);
+ }
+
+ foreach ($overlay as $prop => $value) {
+ if ($prop == 'start' || $prop == 'end') {
+ // handled by merge_exception_dates() below
+ }
+ else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
+ $event[$prop] = $value;
+ }
+ else if ($prop[0] != '_' && !in_array($prop, $forbidden)) {
+ $event[$prop] = $value;
+ }
+ }
+
+ self::merge_exception_dates($event, $overlay);
+ }
+
+ /**
+ * Merge start/end date from the overlay event to the base event object
+ *
+ * @param array The event object to be altered
+ * @param array The overlay event object to be merged over $event
+ */
+ public static function merge_exception_dates(&$event, $overlay)
+ {
+ // compute date offset from the exception
+ if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) {
+ $date_offset = $overlay['recurrence_date']->diff($overlay['start']);
+ }
+
+ foreach (['start', 'end'] as $prop) {
+ $value = $overlay[$prop];
+ if (isset($event[$prop]) && $event[$prop] instanceof DateTime) {
+ // set date value if overlay is an exception of the current instance
+ if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
+ $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
}
- }
+ // apply date offset
+ else if (!empty($date_offset)) {
+ $event[$prop]->add($date_offset);
+ }
+ // adjust time of the recurring event instance
+ $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
}
+ }
+ }
- $success = $storage->update_event($master);
- $ret = $master['uid'];
- break;
- }
+ /**
+ * Get events from source.
+ *
+ * @param int Event's new start (unix timestamp)
+ * @param int Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
+ * @param bool Include virtual events (optional)
+ * @param int Only list events modified since this time (unix timestamp)
+ *
+ * @return array A list of event records
+ */
+ public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if ($calendars && is_string($calendars)) {
+ $calendars = explode(',', $calendars);
+ }
+ else if (!$calendars) {
+ $this->_read_calendars();
+ $calendars = array_keys($this->calendars);
+ }
- default: // 'all' is default
- // removing the master event with loose exceptions (not recurring though)
- if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
- // make the first exception the new master
- $newmaster = array_shift($master['exceptions']);
- $newmaster['exceptions'] = $master['exceptions'];
- $newmaster['_attachments'] = $master['_attachments'];
- $newmaster['_mailbox'] = $master['_mailbox'];
- $newmaster['_msguid'] = $master['_msguid'];
+ $query = [];
+ $events = [];
+ $categories = [];
- $success = $storage->update_event($newmaster);
- }
- else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
- // don't delete but set PARTSTAT=DECLINED
- if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
- $success = $storage->update_event($master);
+ if ($modifiedsince) {
+ $query[] = ['changed', '>=', $modifiedsince];
+ }
+
+ foreach ($calendars as $cid) {
+ if ($storage = $this->get_calendar($cid)) {
+ $events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
+ $categories += $storage->categories;
}
- }
-
- if (!$success)
- $success = $storage->delete_event($master, $force);
- break;
- }
- }
-
- if ($success && !$force) {
- if ($master['_folder_id'])
- $sess_data['_folder_id'] = $master['_folder_id'];
- $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $sess_data);
- }
-
- if ($success && $this->freebusy_trigger)
- $this->rc->output->command('plugin.ping_url', array(
- 'action' => 'calendar/push-freebusy',
- // _folder_id may be set by invitations calendar
- 'source' => $master['_folder_id'] ?: $storage->id,
- ));
-
- return $success ? $ret : false;
- }
-
- /**
- * Restore a single deleted event
- *
- * @param array Hash array with event properties:
- * id: Event identifier
- * calendar: Event calendar
- *
- * @return boolean True on success, False on error
- */
- public function restore_event($event)
- {
- if ($storage = $this->get_calendar($event['calendar'])) {
- if (!empty($_SESSION['calendar_restore_event_data']))
- $success = $storage->update_event($event = $_SESSION['calendar_restore_event_data']);
- else
- $success = $storage->restore_event($event);
-
- if ($success && $this->freebusy_trigger)
- $this->rc->output->command('plugin.ping_url', array(
- 'action' => 'calendar/push-freebusy',
- // _folder_id may be set by invitations calendar
- 'source' => $event['_folder_id'] ?: $storage->id,
- ));
-
- return $success;
- }
-
- return false;
- }
-
- /**
- * Wrapper to update an event object depending on the given savemode
- */
- private function update_event($event)
- {
- if (!($storage = $this->get_calendar($event['calendar'])))
- return false;
-
- // move event to another folder/calendar
- if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
- if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
- return false;
-
- $old = $fromcalendar->get_event($event['id']);
-
- if ($event['_savemode'] != 'new') {
- if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
- return false;
}
- $fromcalendar = $storage;
- }
- }
- else
- $fromcalendar = $storage;
-
- $success = false;
- $savemode = 'all';
- $attachments = array();
- $old = $master = $storage->get_event($event['id']);
-
- if (!$old || !$old['start']) {
- rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Failed to load event object to update: id=" . $event['id']),
- true, false);
- return false;
- }
-
- // modify a recurring event, check submitted savemode to do the right things
- if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) {
- $master = $storage->get_event($old['uid']);
- $savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all');
-
- // this-and-future on the first instance equals to 'all'
- if ($savemode == 'future' && $master['start'] && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master))
- $savemode = 'all';
- // force 'current' mode for single occurrences stored as exception
- else if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception'])
- $savemode = 'current';
-
- // Stick to the master timezone for all occurrences (Bifrost#T104637)
- $master_tz = $master['start']->getTimezone();
- $event_tz = $event['start']->getTimezone();
-
- if ($master_tz->getName() != $event_tz->getName()) {
- $event['start']->setTimezone($master_tz);
- $event['end']->setTimezone($master_tz);
- }
- }
-
- // check if update affects scheduling and update attendee status accordingly
- $reschedule = $this->check_scheduling($event, $old, true);
-
- // keep saved exceptions (not submitted by the client)
- if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
- $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
- if (isset($event['recurrence']['EXCEPTIONS']))
- $with_exceptions = true; // exceptions already provided (e.g. from iCal import)
- else if ($old['recurrence']['EXCEPTIONS'])
- $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
- else if ($old['exceptions'])
- $event['exceptions'] = $old['exceptions'];
-
- // remove some internal properties which should not be saved
- unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
- $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']);
-
- switch ($savemode) {
- case 'new':
- // save submitted data as new (non-recurring) event
- $event['recurrence'] = array();
- $event['_copyfrom'] = $master['_msguid'];
- $event['_mailbox'] = $master['_mailbox'];
- $event['uid'] = $this->cal->generate_uid();
- unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
-
- // copy attachment metadata to new event
- $event = self::from_rcube_event($event, $master);
-
- self::clear_attandee_noreply($event);
- if ($success = $storage->insert_event($event))
- $success = $event['uid'];
- break;
-
- case 'future':
- // create a new recurring event
- $event['_copyfrom'] = $master['_msguid'];
- $event['_mailbox'] = $master['_mailbox'];
- $event['uid'] = $this->cal->generate_uid();
- unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
-
- // copy attachment metadata to new event
- $event = self::from_rcube_event($event, $master);
-
- // remove recurrence exceptions on re-scheduling
- if ($reschedule) {
- unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
- }
- else if (is_array($event['recurrence']['EXCEPTIONS'])) {
- // only keep relevant exceptions
- $event['recurrence']['EXCEPTIONS'] = array_filter($event['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
- return $exception['start'] > $event['start'];
- });
- if (is_array($event['recurrence']['EXDATE'])) {
- $event['recurrence']['EXDATE'] = array_filter($event['recurrence']['EXDATE'], function($exdate) use ($event) {
- return $exdate > $event['start'];
- });
- }
- // set link to top-level exceptions
- $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+ // add events from the address books birthday calendar
+ if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
+ $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
}
- // compute remaining occurrences
- if ($event['recurrence']['COUNT']) {
- if (!$old['_count'])
- $old['_count'] = $this->get_recurrence_count($master, $old['start']);
- $event['recurrence']['COUNT'] -= intval($old['_count']);
- }
+ // add new categories to user prefs
+ $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
+ $newcats = array_udiff(
+ array_keys($categories),
+ array_keys($old_categories),
+ function($a, $b) { return strcasecmp($a, $b); }
+ );
- // remove fixed weekday when date changed
- if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) {
- if (strlen($event['recurrence']['BYDAY']) == 2)
- unset($event['recurrence']['BYDAY']);
- if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
- unset($event['recurrence']['BYMONTH']);
- }
-
- // set until-date on master event
- $master['recurrence']['UNTIL'] = clone $old['start'];
- $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
- unset($master['recurrence']['COUNT']);
-
- // remove all exceptions after $event['start']
- if (is_array($master['recurrence']['EXCEPTIONS'])) {
- $master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
- return $exception['start'] < $event['start'];
- });
- // set link to top-level exceptions
- $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
- }
- if (is_array($master['recurrence']['EXDATE'])) {
- $master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function($exdate) use ($event) {
- return $exdate < $event['start'];
- });
- }
-
- // save new event
- if ($success = $storage->insert_event($event)) {
- $success = $event['uid'];
-
- // update master event (no rescheduling!)
- self::clear_attandee_noreply($master);
- $storage->update_event($master);
- }
- break;
-
- case 'current':
- // recurring instances shall not store recurrence rules and attachments
- $event['recurrence'] = array();
- $event['thisandfuture'] = $savemode == 'future';
- unset($event['attachments'], $event['id']);
-
- // increment sequence of this instance if scheduling is affected
- if ($reschedule) {
- $event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
- }
- else if (!isset($event['sequence'])) {
- $event['sequence'] = $old['sequence'] ?: $master['sequence'];
- }
-
- // save properties to a recurrence exception instance
- if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
- if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
- $success = $storage->update_event($master, $old['id']);
- break;
- }
- }
-
- $add_exception = true;
-
- // adjust matching RDATE entry if dates changed
- if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
- foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
- if ($rdate->format('Ymd') == $old_date) {
- $master['recurrence']['RDATE'][$j] = $event['start'];
- sort($master['recurrence']['RDATE']);
- $add_exception = false;
- break;
+ if (!empty($newcats)) {
+ foreach ($newcats as $category) {
+ $old_categories[$category] = ''; // no color set yet
}
- }
+ $this->rc->user->save_prefs(['calendar_categories' => $old_categories]);
}
- // save as new exception to master event
- if ($add_exception) {
- self::add_exception($master, $event, $old);
+ array_walk($events, 'kolab_driver::to_rcube_event');
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param int Date range start (unix timestamp)
+ * @param int Date range end (unix timestamp)
+ *
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ public function count_events($calendars, $start, $end = null)
+ {
+ $counts = [];
+
+ if ($calendars && is_string($calendars)) {
+ $calendars = explode(',', $calendars);
+ }
+ else if (!$calendars) {
+ $this->_read_calendars();
+ $calendars = array_keys($this->calendars);
}
- $success = $storage->update_event($master);
- break;
-
- default: // 'all' is default
- $event['id'] = $master['uid'];
- $event['uid'] = $master['uid'];
-
- // use start date from master but try to be smart on time or duration changes
- $old_start_date = $old['start']->format('Y-m-d');
- $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
- $old_duration = self::event_duration($old['start'], $old['end'], $old['allday']);
-
- $new_start_date = $event['start']->format('Y-m-d');
- $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
- $new_duration = self::event_duration($event['start'], $event['end'], $event['allday']);
-
- $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
- $date_shift = $old['start']->diff($event['start']);
-
- // shifted or resized
- if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
- $event['start'] = $master['start']->add($date_shift);
- $event['end'] = clone $event['start'];
- $event['end']->add(new DateInterval($new_duration));
-
- // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
- if ($old_start_date != $new_start_date && $event['recurrence']) {
- if (strlen($event['recurrence']['BYDAY']) == 2)
- unset($event['recurrence']['BYDAY']);
- if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
- unset($event['recurrence']['BYMONTH']);
- }
- }
- // dates did not change, use the ones from master
- else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
- $event['start'] = $master['start'];
- $event['end'] = $master['end'];
- }
-
- // when saving an instance in 'all' mode, copy recurrence exceptions over
- if ($old['recurrence_id']) {
- $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
- $event['recurrence']['EXDATE'] = $master['recurrence']['EXDATE'];
- }
- else if ($master['_instance']) {
- $event['_instance'] = $master['_instance'];
- $event['recurrence_date'] = $master['recurrence_date'];
- }
-
- // TODO: forward changes to exceptions (which do not yet have differing values stored)
- if (is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
- // determine added and removed attendees
- $old_attendees = $current_attendees = $added_attendees = array();
- foreach ((array)$old['attendees'] as $attendee) {
- $old_attendees[] = $attendee['email'];
- }
- foreach ((array)$event['attendees'] as $attendee) {
- $current_attendees[] = $attendee['email'];
- if (!in_array($attendee['email'], $old_attendees)) {
- $added_attendees[] = $attendee;
+ foreach ($calendars as $cid) {
+ if ($storage = $this->get_calendar($cid)) {
+ $counts[$cid] = $storage->count_events($start, $end);
}
- }
- $removed_attendees = array_diff($old_attendees, $current_attendees);
-
- foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
- calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
- }
-
- // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
- if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
- $recurrence_id_format = libcalendaring::recurrence_id_format($event);
- foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
- $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
- rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
- if (is_a($recurrence_id, 'DateTime')) {
- $recurrence_id->add($date_shift);
- $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
- $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
- }
- }
- }
-
- // set link to top-level exceptions
- $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
- // unset _dateonly flags in (cached) date objects
- unset($event['start']->_dateonly, $event['end']->_dateonly);
-
- $success = $storage->update_event($event) ? $event['id'] : false; // return master UID
- break;
+ return $counts;
}
- if ($success && $this->freebusy_trigger)
- $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @see calendar_driver::pending_alarms()
+ */
+ public function pending_alarms($time, $calendars = null)
+ {
+ $interval = 300;
+ $time -= $time % 60;
- return $success;
- }
+ $slot = $time;
+ $slot -= $slot % $interval;
- /**
- * Calculate event duration, returns string in DateInterval format
- */
- protected static function event_duration($start, $end, $allday = false)
- {
- if ($allday) {
- $diff = $start->diff($end);
- return 'P' . $diff->days . 'D';
- }
+ $last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
+ $last -= $last % $interval;
- return 'PT' . ($end->format('U') - $start->format('U')) . 'S';
- }
-
- /**
- * Determine whether the current change affects scheduling and reset attendee status accordingly
- */
- public function check_scheduling(&$event, $old, $update = true)
- {
- // skip this check when importing iCal/iTip events
- if (isset($event['sequence']) || !empty($event['_method'])) {
- return false;
- }
-
- // iterate through the list of properties considered 'significant' for scheduling
- $kolab_event = $old['_formatobj'] ?: new kolab_format_event();
- $reschedule = $kolab_event->check_rescheduling($event, $old);
-
- // reset all attendee status to needs-action (#4360)
- if ($update && $reschedule && is_array($event['attendees'])) {
- $is_organizer = false;
- $emails = $this->cal->get_user_emails();
- $attendees = $event['attendees'];
- foreach ($attendees as $i => $attendee) {
- if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
- $is_organizer = true;
+ // only check for alerts once in 5 minutes
+ if ($last == $slot) {
+ return [];
}
- else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
- $attendees[$i]['status'] = 'NEEDS-ACTION';
- $attendees[$i]['rsvp'] = true;
+
+ if ($calendars && is_string($calendars)) {
+ $calendars = explode(',', $calendars);
}
- }
- // update attendees only if I'm the organizer
- if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
- $event['attendees'] = $attendees;
- }
- }
+ $time = $slot + $interval;
- return $reschedule;
- }
+ $alarms = [];
+ $candidates = [];
+ $query = [['tags', '=', 'x-has-alarms']];
- /**
- * Apply the given changes to already existing exceptions
- */
- protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
- {
- $saved = false;
- $existing = null;
-
- // determine added and removed attendees
- $added_attendees = $removed_attendees = array();
- if ($savemode == 'future') {
- $old_attendees = $current_attendees = array();
- foreach ((array)$old['attendees'] as $attendee) {
- $old_attendees[] = $attendee['email'];
- }
- foreach ((array)$event['attendees'] as $attendee) {
- $current_attendees[] = $attendee['email'];
- if (!in_array($attendee['email'], $old_attendees)) {
- $added_attendees[] = $attendee;
- }
- }
- $removed_attendees = array_diff($old_attendees, $current_attendees);
- }
-
- foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
- // update a specific instance
- if ($exception['_instance'] == $old['_instance']) {
- $existing = $i;
-
- // check savemode against existing exception mode.
- // if matches, we can update this existing exception
- if ((bool)$exception['thisandfuture'] === ($savemode == 'future')) {
- $event['_instance'] = $old['_instance'];
- $event['thisandfuture'] = $old['thisandfuture'];
- $event['recurrence_date'] = $old['recurrence_date'];
- $master['recurrence']['EXCEPTIONS'][$i] = $event;
- $saved = true;
- }
- }
- // merge the new event properties onto future exceptions
- if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
- unset($event['thisandfuture']);
- self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, array('attendees'));
-
- if (!empty($added_attendees) || !empty($removed_attendees)) {
- calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
- }
- }
- }
-/*
- // we could not update the existing exception due to savemode mismatch...
- if (!$saved && $existing !== null && $master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture']) {
- // ... try to move the existing this-and-future exception to the next occurrence
- foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
- // our old this-and-future exception is obsolete
- if ($candidate['thisandfuture']) {
- unset($master['recurrence']['EXCEPTIONS'][$existing]);
- $saved = true;
- break;
- }
- // this occurrence doesn't yet have an exception
- else if (!$candidate['isexception']) {
- $event['_instance'] = $candidate['_instance'];
- $event['recurrence_date'] = $candidate['recurrence_date'];
- $master['recurrence']['EXCEPTIONS'][$i] = $event;
- $saved = true;
- break;
- }
- }
- }
-*/
-
- // set link to top-level exceptions
- $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
-
- // returning false here will add a new exception
- return $saved;
- }
-
- /**
- * Add or update the given event as an exception to $master
- */
- public static function add_exception(&$master, $event, $old = null)
- {
- if ($old) {
- $event['_instance'] = $old['_instance'];
- if (!$event['recurrence_date'])
- $event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
- }
- else if (!$event['recurrence_date']) {
- $event['recurrence_date'] = $event['start'];
- }
-
- if (!$event['_instance'] && is_a($event['recurrence_date'], 'DateTime')) {
- $event['_instance'] = libcalendaring::recurrence_instance_identifier($event, $master['allday']);
- }
-
- if (!is_array($master['exceptions']) && is_array($master['recurrence']['EXCEPTIONS'])) {
- $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
- }
-
- $existing = false;
- foreach ((array)$master['exceptions'] as $i => $exception) {
- if ($exception['_instance'] == $event['_instance']) {
- $master['exceptions'][$i] = $event;
- $existing = true;
- }
- }
-
- if (!$existing) {
- $master['exceptions'][] = $event;
- }
-
- return true;
- }
-
- /**
- * Remove the noreply flags from attendees
- */
- public static function clear_attandee_noreply(&$event)
- {
- foreach ((array)$event['attendees'] as $i => $attendee) {
- unset($event['attendees'][$i]['noreply']);
- }
- }
-
- /**
- * Merge certain properties from the overlay event to the base event object
- *
- * @param array The event object to be altered
- * @param array The overlay event object to be merged over $event
- * @param array List of properties not allowed to be overwritten
- */
- public static function merge_exception_data(&$event, $overlay, $blacklist = null)
- {
- $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
-
- if (is_array($blacklist))
- $forbidden = array_merge($forbidden, $blacklist);
-
- foreach ($overlay as $prop => $value) {
- if ($prop == 'start' || $prop == 'end') {
- // handled by merge_exception_dates() below
- }
- else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
- $event[$prop] = $value;
- }
- else if ($prop[0] != '_' && !in_array($prop, $forbidden))
- $event[$prop] = $value;
- }
-
- self::merge_exception_dates($event, $overlay);
- }
-
- /**
- * Merge start/end date from the overlay event to the base event object
- *
- * @param array The event object to be altered
- * @param array The overlay event object to be merged over $event
- */
- public static function merge_exception_dates(&$event, $overlay)
- {
- // compute date offset from the exception
- if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) {
- $date_offset = $overlay['recurrence_date']->diff($overlay['start']);
- }
-
- foreach (array('start', 'end') as $prop) {
- $value = $overlay[$prop];
- if (is_object($event[$prop]) && $event[$prop] instanceof DateTime) {
- // set date value if overlay is an exception of the current instance
- if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
- $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
- }
- // apply date offset
- else if ($date_offset) {
- $event[$prop]->add($date_offset);
- }
- // adjust time of the recurring event instance
- $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
- }
- }
- }
-
- /**
- * Get events from source.
- *
- * @param integer Event's new start (unix timestamp)
- * @param integer Event's new end (unix timestamp)
- * @param string Search query (optional)
- * @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
- * @param boolean Include virtual events (optional)
- * @param integer Only list events modified since this time (unix timestamp)
- * @return array A list of event records
- */
- public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
- {
- if ($calendars && is_string($calendars))
- $calendars = explode(',', $calendars);
- else if (!$calendars) {
- $this->_read_calendars();
- $calendars = array_keys($this->calendars);
- }
-
- $query = array();
- if ($modifiedsince)
- $query[] = array('changed', '>=', $modifiedsince);
-
- $events = $categories = array();
- foreach ($calendars as $cid) {
- if ($storage = $this->get_calendar($cid)) {
- $events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
- $categories += $storage->categories;
- }
- }
-
- // add events from the address books birthday calendar
- if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
- $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
- }
-
- // add new categories to user prefs
- $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
- if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) {
- foreach ($newcats as $category)
- $old_categories[$category] = ''; // no color set yet
- $this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
- }
-
- array_walk($events, 'kolab_driver::to_rcube_event');
- return $events;
- }
-
- /**
- * Get number of events in the given calendar
- *
- * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
- * @param integer Date range start (unix timestamp)
- * @param integer Date range end (unix timestamp)
- * @return array Hash array with counts grouped by calendar ID
- */
- public function count_events($calendars, $start, $end = null)
- {
- $counts = array();
-
- if ($calendars && is_string($calendars))
- $calendars = explode(',', $calendars);
- else if (!$calendars) {
$this->_read_calendars();
- $calendars = array_keys($this->calendars);
- }
- foreach ($calendars as $cid) {
- if ($storage = $this->get_calendar($cid)) {
- $counts[$cid] = $storage->count_events($start, $end);
- }
- }
-
- return $counts;
- }
-
- /**
- * Get a list of pending alarms to be displayed to the user
- *
- * @see calendar_driver::pending_alarms()
- */
- public function pending_alarms($time, $calendars = 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 ($calendars && is_string($calendars))
- $calendars = explode(',', $calendars);
-
- $time = $slot + $interval;
-
- $candidates = array();
- $query = array(array('tags', '=', 'x-has-alarms'));
-
- $this->_read_calendars();
-
- foreach ($this->calendars as $cid => $calendar) {
- // skip calendars with alarms disabled
- if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
- continue;
-
- foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
- // add to list if alarm is set
- $alarm = libcalendaring::get_next_alarm($e);
- if ($alarm && $alarm['time'] && $alarm['time'] >= $last && in_array($alarm['action'], $this->alarm_types)) {
- $id = $alarm['id']; // use alarm-id as primary identifier
- $candidates[$id] = array(
- 'id' => $id,
- 'title' => $e['title'],
- 'location' => $e['location'],
- 'start' => $e['start'],
- 'end' => $e['end'],
- '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 && ($e = $this->rc->db->fetch_assoc($result))) {
- $dbdata[$e['alarm_id']] = $e;
- }
- }
-
- $alarms = array();
- foreach ($candidates as $id => $alarm) {
- // skip dismissed alarms
- if ($dbdata[$id]['dismissed'])
- continue;
-
- // snooze function may have shifted alarm time
- $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
- if ($notifyat <= $time)
- $alarms[] = $alarm;
- }
-
- return $alarms;
- }
-
- /**
- * Feedback after showing/sending an alarm notification
- *
- * @see calendar_driver::dismiss_alarm()
- */
- public function dismiss_alarm($alarm_id, $snooze = 0)
- {
- $alarms_table = $this->rc->db->table_name('kolab_alarms', true);
- // delete old alarm entry
- $this->rc->db->query("DELETE FROM $alarms_table"
- . " WHERE `alarm_id` = ? AND `user_id` = ?",
- $alarm_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 $alarms_table"
- . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
- . " VALUES (?, ?, ?, ?)",
- $alarm_id,
- $this->rc->user->ID,
- $snooze > 0 ? 0 : 1,
- $notifyat
- );
-
- return $this->rc->db->affected_rows($query);
- }
-
- /**
- * List attachments from the given event
- */
- public function list_attachments($event)
- {
- if (!($storage = $this->get_calendar($event['calendar'])))
- return false;
-
- $event = $storage->get_event($event['id']);
-
- return $event['attachments'];
- }
-
- /**
- * Get attachment properties
- */
- public function get_attachment($id, $event)
- {
- if (!($storage = $this->get_calendar($event['calendar'])))
- return false;
-
- // get old revision of event
- if ($event['rev']) {
- $event = $this->get_event_revison($event, $event['rev'], true);
- }
- else {
- $event = $storage->get_event($event['id']);
- }
-
- if ($event) {
- $attachments = isset($event['_attachments']) ? $event['_attachments'] : $event['attachments'];
- foreach ((array) $attachments as $att) {
- if ($att['id'] == $id) {
- return $att;
- }
- }
- }
- }
-
- /**
- * Get attachment body
- * @see calendar_driver::get_attachment_body()
- */
- public function get_attachment_body($id, $event)
- {
- if (!($cal = $this->get_calendar($event['calendar'])))
- return false;
-
- // get old revision of event
- if ($event['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_event_identity($event);
- if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['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;
- }
-
- return $cal->get_attachment_body($id, $event);
- }
-
- /**
- * Build a struct representing the given message reference
- *
- * @see calendar_driver::get_message_reference()
- */
- public function get_message_reference($uri_or_headers, $folder = null)
- {
- 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, 'event');
- }
-
- return false;
- }
-
- /**
- * List availabale categories
- * The default implementation reads them from config/user prefs
- */
- public function list_categories()
- {
- // FIXME: complete list with categories saved in config objects (KEP:12)
- return $this->rc->config->get('calendar_categories', $this->default_categories);
- }
-
- /**
- * Create instances of a recurring event
- *
- * @param array Hash array with event properties
- * @param object DateTime Start date of the recurrence window
- * @param object DateTime End date of the recurrence window
- * @return array List of recurring event instances
- */
- public function get_recurring_events($event, $start, $end = null)
- {
- // load the given event data into a libkolabxml container
- if (!$event['_formatobj']) {
- $event_xml = new kolab_format_event();
- $event_xml->set($event);
- $event['_formatobj'] = $event_xml;
- }
-
- $this->_read_calendars();
- $storage = reset($this->calendars);
- return $storage->get_recurring_events($event, $start, $end);
- }
-
- /**
- *
- */
- private function get_recurrence_count($event, $dtstart)
- {
- // load the given event data into a libkolabxml container
- if (!$event['_formatobj']) {
- $event_xml = new kolab_format_event();
- $event_xml->set($event);
- $event['_formatobj'] = $event_xml;
- }
-
- // use libkolab to compute recurring events
- $recurrence = new kolab_date_recurrence($event['_formatobj']);
-
- $count = 0;
- while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
- $count++;
- }
-
- return $count;
- }
-
- /**
- * Fetch free/busy information from a person within the given range
- */
- public function get_freebusy_list($email, $start, $end)
- {
- if (empty($email)/* || $end < time()*/)
- 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/*, 'freebusyurl'*/)) {
- while ($contact = $result->iterate()) {
- if ($fburl = $contact['freebusyurl']) {
- $fbdata = @file_get_contents($fburl);
- break;
+ foreach ($this->calendars as $cid => $calendar) {
+ // skip calendars with alarms disabled
+ if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars))) {
+ continue;
}
- }
- }
- 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;
- }
-
- /**
- * Handler to push folder triggers when sent from client.
- * Used to push free-busy changes asynchronously after updating an event
- */
- public function push_freebusy()
- {
- // make shure triggering completes
- set_time_limit(0);
- ignore_user_abort(true);
-
- $cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
- if (!($cal = $this->get_calendar($cal)))
- return false;
-
- // trigger updates on folder
- $trigger = $cal->storage->trigger();
- if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
- rcube::raise_error(array(
- 'code' => 900, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
- true, false);
- }
-
- exit;
- }
-
-
- /**
- * Convert from driver format to external caledar app data
- */
- public static function to_rcube_event(&$record)
- {
- if (!is_array($record))
- return $record;
-
- $record['id'] = $record['uid'];
-
- if ($record['_instance']) {
- $record['id'] .= '-' . $record['_instance'];
-
- if (!$record['recurrence_id'] && !empty($record['recurrence']))
- $record['recurrence_id'] = $record['uid'];
- }
-
- // all-day events go from 12:00 - 13:00
- if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) {
- $record['end'] = clone $record['start'];
- $record['end']->add(new DateInterval('PT1H'));
- }
-
- // translate internal '_attachments' to external 'attachments' list
- if (!empty($record['_attachments'])) {
- foreach ($record['_attachments'] as $key => $attachment) {
- if ($attachment !== false) {
- if (!$attachment['name'])
- $attachment['name'] = $key;
-
- unset($attachment['path'], $attachment['content']);
- $attachments[] = $attachment;
- }
- }
-
- $record['attachments'] = $attachments;
- }
-
- if (!empty($record['attendees'])) {
- foreach ((array)$record['attendees'] as $i => $attendee) {
- if (is_array($attendee['delegated-from'])) {
- $record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
- }
- if (is_array($attendee['delegated-to'])) {
- $record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
- }
- }
- }
-
- // Roundcube only supports one category assignment
- if (is_array($record['categories']))
- $record['categories'] = $record['categories'][0];
-
- // the cancelled flag transltes into status=CANCELLED
- if ($record['cancelled'])
- $record['status'] = 'CANCELLED';
-
- // The web client only supports DISPLAY type of alarms
- if (!empty($record['alarms']))
- $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
-
- // remove empty recurrence array
- if (empty($record['recurrence']))
- unset($record['recurrence']);
-
- // clean up exception data
- if (is_array($record['recurrence']['EXCEPTIONS'])) {
- array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
- unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
- });
- }
-
- unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
- $record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']);
-
- return $record;
- }
-
- /**
- *
- */
- public static function from_rcube_event($event, $old = array())
- {
- kolab_format::merge_attachments($event, $old);
-
- return $event;
- }
-
-
- /**
- * Set CSS class according to the event's attendde partstat
- */
- public static function add_partstat_class($event, $partstats, $user = null)
- {
- // set classes according to PARTSTAT
- if (is_array($event['attendees'])) {
- $user_emails = libcalendaring::get_instance()->get_user_emails($user);
- $partstat = 'UNKNOWN';
- foreach ($event['attendees'] as $attendee) {
- if (in_array($attendee['email'], $user_emails)) {
- $partstat = $attendee['status'];
- break;
- }
- }
-
- if (in_array($partstat, $partstats)) {
- $event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
- }
- }
-
- return $event;
- }
-
- /**
- * Provide a list of revisions for the given event
- *
- * @param array $event Hash array with event properties
- *
- * @return array List of changes, each as a hash array
- * @see calendar_driver::get_event_changelog()
- */
- public function get_event_changelog($event)
- {
- if (empty($this->bonnie_api)) {
- return false;
- }
-
- list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
-
- $result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid);
- if (is_array($result) && $result['uid'] == $uid) {
- return $result['changes'];
- }
-
- return false;
- }
-
- /**
- * Get a list of property changes beteen two revisions of an event
- *
- * @param array $event Hash array with event properties
- * @param mixed $rev1 Old Revision
- * @param mixed $rev2 New Revision
- *
- * @return array List of property changes, each as a hash array
- * @see calendar_driver::get_event_diff()
- */
- public function get_event_diff($event, $rev1, $rev2)
- {
- if (empty($this->bonnie_api)) {
- return false;
- }
-
- list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
-
- // get diff for the requested recurrence instance
- $instance_id = $event['id'] != $uid ? substr($event['id'], strlen($uid) + 1) : null;
-
- // call Bonnie API
- $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
- if (is_array($result) && $result['uid'] == $uid) {
- $result['rev1'] = $rev1;
- $result['rev2'] = $rev2;
-
- $keymap = array(
- 'dtstart' => 'start',
- 'dtend' => 'end',
- 'dstamp' => 'changed',
- 'summary' => 'title',
- 'alarm' => 'alarms',
- 'attendee' => 'attendees',
- 'attach' => 'attachments',
- 'rrule' => 'recurrence',
- 'transparency' => 'free_busy',
- 'classification' => 'sensitivity',
- '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']];
- }
- // translate free_busy values
- if ($change['property'] == 'free_busy') {
- $change['old'] = $old['old'] ? 'free' : 'busy';
- $change['new'] = $old['new'] ? 'free' : 'busy';
- }
- // 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;
+ foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
+ // add to list if alarm is set
+ $alarm = libcalendaring::get_next_alarm($e);
+ if ($alarm && !empty($alarm['time']) && $alarm['time'] >= $last
+ && in_array($alarm['action'], $this->alarm_types)
+ ) {
+ $id = $alarm['id']; // use alarm-id as primary identifier
+ $candidates[$id] = [
+ 'id' => $id,
+ 'title' => $e['title'],
+ 'location' => $e['location'],
+ 'start' => $e['start'],
+ 'end' => $e['end'],
+ 'notifyat' => $alarm['time'],
+ 'action' => $alarm['action'],
+ ];
+ }
}
- }
}
- // 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]);
+
+ // get alarm information stored in local database
+ if (!empty($candidates)) {
+ $dbdata = [];
+ $alarm_ids = array_map([$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 && ($e = $this->rc->db->fetch_assoc($result))) {
+ $dbdata[$e['alarm_id']] = $e;
}
- if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
- $change['new'][$dest] = $change['new'][$k];
- unset($change['new'][$k]);
+
+ foreach ($candidates as $id => $alarm) {
+ // skip dismissed alarms
+ if ($dbdata[$id]['dismissed']) {
+ continue;
+ }
+
+ // snooze function may have shifted alarm time
+ $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
+ if ($notifyat <= $time) {
+ $alarms[] = $alarm;
+ }
}
- }
}
- 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 $alarms;
}
- return false;
- }
+ /**
+ * Feedback after showing/sending an alarm notification
+ *
+ * @see calendar_driver::dismiss_alarm()
+ */
+ public function dismiss_alarm($alarm_id, $snooze = 0)
+ {
+ $alarms_table = $this->rc->db->table_name('kolab_alarms', true);
- /**
- * Return full data of a specific revision of an event
- *
- * @param array Hash array with event properties
- * @param mixed $rev Revision number
- *
- * @return array Event object as hash array
- * @see calendar_driver::get_event_revison()
- */
- public function get_event_revison($event, $rev, $internal = false)
- {
- if (empty($this->bonnie_api)) {
- return false;
+ // delete old alarm entry
+ $this->rc->db->query("DELETE FROM $alarms_table"
+ . " WHERE `alarm_id` = ? AND `user_id` = ?",
+ $alarm_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 $alarms_table"
+ . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
+ . " VALUES (?, ?, ?, ?)",
+ $alarm_id,
+ $this->rc->user->ID,
+ $snooze > 0 ? 0 : 1,
+ $notifyat
+ );
+
+ return $this->rc->db->affected_rows($query);
}
- $eventid = $event['id'];
- $calid = $event['calendar'];
- list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+ /**
+ * List attachments from the given event
+ */
+ public function list_attachments($event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar']))) {
+ return false;
+ }
- // call Bonnie API
- $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid);
- if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
- $format = kolab_format::factory('event');
- $format->load($result['xml']);
- $event = $format->to_array();
- $format->get_attachments($event, true);
+ $event = $storage->get_event($event['id']);
- // get the right instance from a recurring event
- if ($eventid != $event['uid']) {
- $instance_id = substr($eventid, strlen($event['uid']) + 1);
+ return $event['attachments'];
+ }
- // check for recurrence exception first
- if ($instance = $format->get_instance($instance_id)) {
- $event = $instance;
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar']))) {
+ return false;
+ }
+
+ // get old revision of event
+ if (!empty($event['rev'])) {
+ $event = $this->get_event_revison($event, $event['rev'], true);
}
else {
- // not a exception, compute recurrence...
- $event['_formatobj'] = $format;
- $recurrence_date = rcube_utils::anytodatetime($instance_id, $event['start']->getTimezone());
- foreach ($this->get_recurring_events($event, $event['start'], $recurrence_date) as $instance) {
- if ($instance['id'] == $eventid) {
- $event = $instance;
- break;
+ $event = $storage->get_event($event['id']);
+ }
+
+ if ($event) {
+ $attachments = isset($event['_attachments']) ? $event['_attachments'] : $event['attachments'];
+ foreach ((array) $attachments as $att) {
+ if ($att['id'] == $id) {
+ return $att;
+ }
}
- }
}
- }
-
- if ($format->is_valid()) {
- $event['calendar'] = $calid;
- $event['rev'] = $result['rev'];
- return $internal ? $event : self::to_rcube_event($event);
- }
}
- return false;
- }
-
- /**
- * Command the backend to restore a certain revision of an event.
- * This shall replace the current event with an older version.
- *
- * @param mixed UID string or hash array with event properties:
- * id: Event identifier
- * calendar: Calendar identifier
- * @param mixed $rev Revision number
- *
- * @return boolean True on success, False on failure
- */
- public function restore_event_revision($event, $rev)
- {
- if (empty($this->bonnie_api)) {
- return false;
- }
-
- list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
- $calendar = $this->get_calendar($event['calendar']);
- $success = false;
-
- if ($calendar && $calendar->storage && $calendar->editable) {
- if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) {
- $imap = $this->rc->get_storage();
-
- // insert $raw_msg as new message
- if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) {
- $success = true;
-
- // delete old revision from imap and cache
- $imap->delete_message($msguid, $calendar->storage->name);
- $calendar->storage->cache->set($msguid, false);
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!($cal = $this->get_calendar($event['calendar']))) {
+ return false;
}
- }
- }
- return $success;
- }
+ // get old revision of event
+ if (!empty($event['rev'])) {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
- /**
- * Helper method to resolved the given event identifier into uid and folder
- *
- * @return array (uid,folder,msguid) tuple
- */
- private function _resolve_event_identity($event)
- {
- $mailbox = $msguid = null;
- if (is_array($event)) {
- $uid = $event['uid'] ?: $event['id'];
- if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
- $mailbox = $cal->get_mailbox_id();
+ $cid = substr($id, 4);
- // get event object from storage in order to get the real object uid an msguid
- if ($ev = $cal->get_event($event['id'])) {
- $msguid = $ev['_msguid'];
- $uid = $ev['uid'];
+ // call Bonnie API and get the raw mime message
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+ if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['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 (!empty($part->headers['content-id']) && trim($part->headers['content-id'], '<>') == $cid) {
+ return $part->body;
+ }
+ }
+ }
+
+ return false;
}
- }
- }
- else {
- $uid = $event;
- // get event object from storage in order to get the real object uid an msguid
- if ($ev = $this->get_event($event)) {
- $mailbox = $ev['_mailbox'];
- $msguid = $ev['_msguid'];
- $uid = $ev['uid'];
- }
+ return $cal->get_attachment_body($id, $event);
}
- return array($uid, $mailbox, $msguid);
- }
+ /**
+ * Build a struct representing the given message reference
+ *
+ * @see calendar_driver::get_message_reference()
+ */
+ public function get_message_reference($uri_or_headers, $folder = null)
+ {
+ if (is_object($uri_or_headers)) {
+ $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
+ }
- /**
- * Callback function to produce driver-specific calendar create/edit form
- *
- * @param string Request action 'form-edit|form-new'
- * @param array Calendar properties (e.g. id, color)
- * @param array Edit form fields
- *
- * @return string HTML content of the form
- */
- public function calendar_form($action, $calendar, $formfields)
- {
- // show default dialog for birthday calendar
- if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
- if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID)
- unset($formfields['showalarms']);
+ if (is_string($uri_or_headers)) {
+ return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
+ }
- // General tab
- $form['props'] = array(
- 'name' => $this->rc->gettext('properties'),
- 'fields' => $formfields,
- );
-
- return kolab_utils::folder_form($form, '', 'calendar');
+ return false;
}
- $this->_read_calendars();
-
- if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
- $folder = $cal->get_realname(); // UTF7
- $color = $cal->get_color();
- }
- else {
- $folder = '';
- $color = '';
+ /**
+ * List availabale categories
+ * The default implementation reads them from config/user prefs
+ */
+ public function list_categories()
+ {
+ // FIXME: complete list with categories saved in config objects (KEP:12)
+ return $this->rc->config->get('calendar_categories', $this->default_categories);
}
- $hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array Hash array with event properties
+ * @param DateTime Start date of the recurrence window
+ * @param DateTime End date of the recurrence window
+ *
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null)
+ {
+ // load the given event data into a libkolabxml container
+ if (empty($event['_formatobj'])) {
+ $event_xml = new kolab_format_event();
+ $event_xml->set($event);
+ $event['_formatobj'] = $event_xml;
+ }
- $storage = $this->rc->get_storage();
- $delim = $storage->get_hierarchy_delimiter();
- $form = array();
+ $this->_read_calendars();
+ $storage = reset($this->calendars);
- if (strlen($folder)) {
- $path_imap = explode($delim, $folder);
- array_pop($path_imap); // pop off name part
- $path_imap = implode($delim, $path_imap);
-
- $options = $storage->folder_info($folder);
- }
- else {
- $path_imap = '';
+ return $storage->get_recurring_events($event, $start, $end);
}
- // General tab
- $form['props'] = array(
- 'name' => $this->rc->gettext('properties'),
- 'fields' => array(),
- );
+ /**
+ *
+ */
+ private function get_recurrence_count($event, $dtstart)
+ {
+ // load the given event data into a libkolabxml container
+ if (empty($event['_formatobj'])) {
+ $event_xml = new kolab_format_event();
+ $event_xml->set($event);
+ $event['_formatobj'] = $event_xml;
+ }
- // Disable folder name input
- if (!empty($options) && ($options['norename'] || $options['protected'])) {
- $input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
- $formfields['name']['value'] = kolab_storage::object_name($folder)
- . $input_name->show($folder);
+ // use libkolab to compute recurring events
+ $recurrence = new kolab_date_recurrence($event['_formatobj']);
+
+ $count = 0;
+ while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
+ $count++;
+ }
+
+ return $count;
}
- // calendar name (default field)
- $form['props']['fields']['location'] = $formfields['name'];
+ /**
+ * Fetch free/busy information from a person within the given range
+ */
+ public function get_freebusy_list($email, $start, $end)
+ {
+ if (empty($email)/* || $end < time()*/) {
+ return false;
+ }
- if (!empty($options) && ($options['norename'] || $options['protected'])) {
- // prevent user from moving folder
- $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
- }
- else {
- $select = kolab_storage::folder_selector('event', array('name' => 'parent', 'id' => 'calendar-parent'), $folder);
- $form['props']['fields']['path'] = array(
- 'id' => 'calendar-parent',
- 'label' => $this->cal->gettext('parentcalendar'),
- 'value' => $select->show(strlen($folder) ? $path_imap : ''),
- );
+ // map vcalendar fbtypes to internal values
+ $fbtypemap = [
+ '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 = [
+ '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 (empty($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(['email'], $email, true, true, true/*, 'freebusyurl'*/)) {
+ while ($contact = $result->iterate()) {
+ if (!empty($contact['freebusyurl'])) {
+ $fbdata = @file_get_contents($contact['freebusyurl']);
+ break;
+ }
+ }
+ }
+
+ if (!empty($fbdata)) {
+ break;
+ }
+ }
+ }
+
+ // parse free-busy information using Horde classes
+ if (!empty($fbdata)) {
+ $ical = $this->cal->get_ical();
+ $ical->import($fbdata);
+ if ($fb = $ical->freebusy) {
+ $result = [];
+ foreach ($fb['periods'] as $tuple) {
+ list($from, $to, $type) = $tuple;
+ $result[] = [
+ $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 (!empty($fb['start']) && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
+ array_unshift($result, [$start, $fbstart, calendar::FREEBUSY_UNKNOWN]);
+ }
+ // pad period till $end with status 'unknown'
+ if (!empty($fb['end']) && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
+ $result[] = [$fbend, $end, calendar::FREEBUSY_UNKNOWN];
+ }
+
+ return $result;
+ }
+ }
+
+ return false;
}
- // calendar color (default field)
- $form['props']['fields']['color'] = $formfields['color'];
- $form['props']['fields']['alarms'] = $formfields['showalarms'];
+ /**
+ * Handler to push folder triggers when sent from client.
+ * Used to push free-busy changes asynchronously after updating an event
+ */
+ public function push_freebusy()
+ {
+ // make shure triggering completes
+ set_time_limit(0);
+ ignore_user_abort(true);
- return kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
- }
+ $cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
+ if (!($cal = $this->get_calendar($cal))) {
+ return false;
+ }
- /**
- * Handler for user_delete plugin hook
- */
- public function user_delete($args)
- {
- $db = $this->rc->get_dbh();
- foreach (array('kolab_alarms', 'itipinvitations') as $table) {
- $db->query("DELETE FROM " . $this->rc->db->table_name($table, true)
- . " WHERE `user_id` = ?", $args['user']->ID);
+ // trigger updates on folder
+ $trigger = $cal->storage->trigger();
+ if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
+ rcube::raise_error([
+ 'code' => 900, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed triggering folder. Error was " . $trigger->getMessage()
+ ],
+ true, false
+ );
+ }
+
+ exit;
+ }
+
+ /**
+ * Convert from driver format to external caledar app data
+ */
+ public static function to_rcube_event(&$record)
+ {
+ if (!is_array($record)) {
+ return $record;
+ }
+
+ $record['id'] = $record['uid'];
+
+ if (!empty($record['_instance'])) {
+ $record['id'] .= '-' . $record['_instance'];
+
+ if (empty($record['recurrence_id']) && !empty($record['recurrence'])) {
+ $record['recurrence_id'] = $record['uid'];
+ }
+ }
+
+ // all-day events go from 12:00 - 13:00
+ if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && !empty($record['allday'])) {
+ $record['end'] = clone $record['start'];
+ $record['end']->add(new DateInterval('PT1H'));
+ }
+
+ // translate internal '_attachments' to external 'attachments' list
+ if (!empty($record['_attachments'])) {
+ foreach ($record['_attachments'] as $key => $attachment) {
+ if ($attachment !== false) {
+ if (empty($attachment['name'])) {
+ $attachment['name'] = $key;
+ }
+
+ unset($attachment['path'], $attachment['content']);
+ $attachments[] = $attachment;
+ }
+ }
+
+ $record['attachments'] = $attachments;
+ }
+
+ if (!empty($record['attendees'])) {
+ foreach ((array) $record['attendees'] as $i => $attendee) {
+ if (isset($attendee['delegated-from']) && is_array($attendee['delegated-from'])) {
+ $record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
+ }
+ if (isset($attendee['delegated-to']) && is_array($attendee['delegated-to'])) {
+ $record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
+ }
+ }
+ }
+
+ // Roundcube only supports one category assignment
+ if (!empty($record['categories']) && is_array($record['categories'])) {
+ $record['categories'] = $record['categories'][0];
+ }
+
+ // the cancelled flag transltes into status=CANCELLED
+ if (!empty($record['cancelled'])) {
+ $record['status'] = 'CANCELLED';
+ }
+
+ // The web client only supports DISPLAY type of alarms
+ if (!empty($record['alarms'])) {
+ $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
+ }
+
+ // remove empty recurrence array
+ if (empty($record['recurrence'])) {
+ unset($record['recurrence']);
+ }
+ // clean up exception data
+ else if (!empty($record['recurrence']['EXCEPTIONS'])) {
+ array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
+ unset($exception['_mailbox'], $exception['_msguid'],
+ $exception['_formatobj'], $exception['_attachments']
+ );
+ });
+ }
+
+ unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
+ $record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']
+ );
+
+ return $record;
+ }
+
+ /**
+ *
+ */
+ public static function from_rcube_event($event, $old = [])
+ {
+ kolab_format::merge_attachments($event, $old);
+
+ return $event;
+ }
+
+
+ /**
+ * Set CSS class according to the event's attendde partstat
+ */
+ public static function add_partstat_class($event, $partstats, $user = null)
+ {
+ // set classes according to PARTSTAT
+ if (!empty($event['attendees'])) {
+ $user_emails = libcalendaring::get_instance()->get_user_emails($user);
+ $partstat = 'UNKNOWN';
+
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails)) {
+ $partstat = $attendee['status'];
+ break;
+ }
+ }
+
+ if (in_array($partstat, $partstats)) {
+ $event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
+ }
+ }
+
+ return $event;
+ }
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties
+ *
+ * @return array List of changes, each as a hash array
+ * @see calendar_driver::get_event_changelog()
+ */
+ public function get_event_changelog($event)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ $result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid);
+ if (is_array($result) && $result['uid'] == $uid) {
+ return $result['changes'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties
+ * @param mixed $rev1 Old Revision
+ * @param mixed $rev2 New Revision
+ *
+ * @return array List of property changes, each as a hash array
+ * @see calendar_driver::get_event_diff()
+ */
+ public function get_event_diff($event, $rev1, $rev2)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ // get diff for the requested recurrence instance
+ $instance_id = $event['id'] != $uid ? substr($event['id'], strlen($uid) + 1) : null;
+
+ // call Bonnie API
+ $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
+
+ if (is_array($result) && $result['uid'] == $uid) {
+ $result['rev1'] = $rev1;
+ $result['rev2'] = $rev2;
+
+ $keymap = [
+ 'dtstart' => 'start',
+ 'dtend' => 'end',
+ 'dstamp' => 'changed',
+ 'summary' => 'title',
+ 'alarm' => 'alarms',
+ 'attendee' => 'attendees',
+ 'attach' => 'attachments',
+ 'rrule' => 'recurrence',
+ 'transparency' => 'free_busy',
+ 'classification' => 'sensitivity',
+ 'lastmodified-date' => 'changed',
+ ];
+
+ $prop_keymaps = [
+ 'attachments' => ['fmttype' => 'mimetype', 'label' => 'name'],
+ 'attendees' => ['partstat' => 'status'],
+ ];
+
+ $special_changes = [];
+
+ // 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']];
+ }
+ // translate free_busy values
+ if ($change['property'] == 'free_busy') {
+ $change['old'] = !empty($old['old']) ? 'free' : 'busy';
+ $change['new'] = !empty($old['new']) ? 'free' : 'busy';
+ }
+
+ // map alarms trigger value
+ if ($change['property'] == 'alarms') {
+ if (!empty($change['old']['trigger'])) {
+ $change['old']['trigger'] = $change['old']['trigger']['value'];
+ }
+ if (!empty($change['new']['trigger'])) {
+ $change['new']['trigger'] = $change['new']['trigger']['value'];
+ }
+ }
+
+ // make all property keys uppercase
+ if ($change['property'] == 'recurrence') {
+ $special_changes['recurrence'] = $i;
+ foreach (['old', 'new'] as $m) {
+ if (!empty($change[$m])) {
+ $props = [];
+ foreach ($change[$m] as $k => $v) {
+ $props[strtoupper($k)] = $v;
+ }
+ $change[$m] = $props;
+ }
+ }
+ }
+
+ // map property keys names
+ if (!empty($prop_keymaps[$change['property']])) {
+ foreach ($prop_keymaps[$change['property']] as $k => $dest) {
+ if (!empty($change['old']) && array_key_exists($k, $change['old'])) {
+ $change['old'][$dest] = $change['old'][$k];
+ unset($change['old'][$k]);
+ }
+ if (!empty($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 (['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] = ['property' => 'recurrence', 'old' => [], 'new' => []];
+ $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;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param array Hash array with event properties
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see calendar_driver::get_event_revison()
+ */
+ public function get_event_revison($event, $rev, $internal = false)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $eventid = $event['id'];
+ $calid = $event['calendar'];
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid);
+ if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+ $format = kolab_format::factory('event');
+ $format->load($result['xml']);
+ $event = $format->to_array();
+ $format->get_attachments($event, true);
+
+ // get the right instance from a recurring event
+ if ($eventid != $event['uid']) {
+ $instance_id = substr($eventid, strlen($event['uid']) + 1);
+
+ // check for recurrence exception first
+ if ($instance = $format->get_instance($instance_id)) {
+ $event = $instance;
+ }
+ else {
+ // not a exception, compute recurrence...
+ $event['_formatobj'] = $format;
+ $recurrence_date = rcube_utils::anytodatetime($instance_id, $event['start']->getTimezone());
+ foreach ($this->get_recurring_events($event, $event['start'], $recurrence_date) as $instance) {
+ if ($instance['id'] == $eventid) {
+ $event = $instance;
+ break;
+ }
+ }
+ }
+ }
+
+ if ($format->is_valid()) {
+ $event['calendar'] = $calid;
+ $event['rev'] = $result['rev'];
+
+ return $internal ? $event : self::to_rcube_event($event);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed $event UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return bool True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ $calendar = $this->get_calendar($event['calendar']);
+ $success = false;
+
+ if ($calendar && $calendar->storage && $calendar->editable) {
+ if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) {
+ $imap = $this->rc->get_storage();
+
+ // insert $raw_msg as new message
+ if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) {
+ $success = true;
+
+ // delete old revision from imap and cache
+ $imap->delete_message($msguid, $calendar->storage->name);
+ $calendar->storage->cache->set($msguid, false);
+ }
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Helper method to resolved the given event identifier into uid and folder
+ *
+ * @return array (uid,folder,msguid) tuple
+ */
+ private function _resolve_event_identity($event)
+ {
+ $mailbox = $msguid = null;
+
+ if (is_array($event)) {
+ $uid = !empty($event['uid']) ? $event['uid'] : $event['id'];
+
+ if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
+ $mailbox = $cal->get_mailbox_id();
+
+ // get event object from storage in order to get the real object uid an msguid
+ if ($ev = $cal->get_event($event['id'])) {
+ $msguid = $ev['_msguid'];
+ $uid = $ev['uid'];
+ }
+ }
+ }
+ else {
+ $uid = $event;
+
+ // get event object from storage in order to get the real object uid an msguid
+ if ($ev = $this->get_event($event)) {
+ $mailbox = $ev['_mailbox'];
+ $msguid = $ev['_msguid'];
+ $uid = $ev['uid'];
+ }
+ }
+
+ return array($uid, $mailbox, $msguid);
+ }
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string Request action 'form-edit|form-new'
+ * @param array Calendar properties (e.g. id, color)
+ * @param array Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ $special_calendars = [
+ self::BIRTHDAY_CALENDAR_ID,
+ self::INVITATIONS_CALENDAR_PENDING,
+ self::INVITATIONS_CALENDAR_DECLINED
+ ];
+
+ // show default dialog for birthday calendar
+ if (in_array($calendar['id'], $special_calendars)) {
+ if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID) {
+ unset($formfields['showalarms']);
+ }
+
+ // General tab
+ $form['props'] = [
+ 'name' => $this->rc->gettext('properties'),
+ 'fields' => $formfields,
+ ];
+
+ return kolab_utils::folder_form($form, '', 'calendar');
+ }
+
+ $this->_read_calendars();
+
+ if (!empty($calendar['id']) && ($cal = $this->calendars[$calendar['id']])) {
+ $folder = $cal->get_realname(); // UTF7
+ $color = $cal->get_color();
+ }
+ else {
+ $folder = '';
+ $color = '';
+ }
+
+ $hidden_fields[] = ['name' => 'oldname', 'value' => $folder];
+
+ $storage = $this->rc->get_storage();
+ $delim = $storage->get_hierarchy_delimiter();
+ $form = [];
+
+ if (strlen($folder)) {
+ $path_imap = explode($delim, $folder);
+ array_pop($path_imap); // pop off name part
+ $path_imap = implode($delim, $path_imap);
+
+ $options = $storage->folder_info($folder);
+ }
+ else {
+ $path_imap = '';
+ }
+
+ // General tab
+ $form['props'] = [
+ 'name' => $this->rc->gettext('properties'),
+ 'fields' => [],
+ ];
+
+ $protected = !empty($options) && (!empty($options['norename']) || !empty($options['protected']));
+ // Disable folder name input
+ if ($protected) {
+ $input_name = new html_hiddenfield(['name' => 'name', 'id' => 'calendar-name']);
+ $formfields['name']['value'] = kolab_storage::object_name($folder)
+ . $input_name->show($folder);
+ }
+
+ // calendar name (default field)
+ $form['props']['fields']['location'] = $formfields['name'];
+
+ if ($protected) {
+ // prevent user from moving folder
+ $hidden_fields[] = ['name' => 'parent', 'value' => $path_imap];
+ }
+ else {
+ $select = kolab_storage::folder_selector('event', ['name' => 'parent', 'id' => 'calendar-parent'], $folder);
+
+ $form['props']['fields']['path'] = [
+ 'id' => 'calendar-parent',
+ 'label' => $this->cal->gettext('parentcalendar'),
+ 'value' => $select->show(strlen($folder) ? $path_imap : ''),
+ ];
+ }
+
+ // calendar color (default field)
+ $form['props']['fields']['color'] = $formfields['color'];
+ $form['props']['fields']['alarms'] = $formfields['showalarms'];
+
+ return kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ $db = $this->rc->get_dbh();
+ foreach (['kolab_alarms', 'itipinvitations'] as $table) {
+ $db->query("DELETE FROM " . $this->rc->db->table_name($table, true)
+ . " WHERE `user_id` = ?", $args['user']->ID);
+ }
}
- }
}
diff --git a/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
index adb73e3c..cae04552 100644
--- a/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
@@ -23,389 +23,402 @@
class kolab_invitation_calendar
{
- public $id = '__invitation__';
- public $ready = true;
- public $alarms = false;
- public $rights = 'lrsv';
- public $editable = false;
- public $attachments = false;
- public $subscriptions = false;
- public $partstats = array('unknown');
- public $categories = array();
- public $name = 'Invitations';
+ public $id = '__invitation__';
+ public $ready = true;
+ public $alarms = false;
+ public $rights = 'lrsv';
+ public $editable = false;
+ public $attachments = false;
+ public $subscriptions = false;
+ public $partstats = ['unknown'];
+ public $categories = [];
+ public $name = 'Invitations';
- /**
- * Default constructor
- */
- public function __construct($id, $calendar)
- {
- $this->cal = $calendar;
- $this->id = $id;
+ /**
+ * Default constructor
+ */
+ public function __construct($id, $calendar)
+ {
+ $this->cal = $calendar;
+ $this->id = $id;
- switch ($this->id) {
- case kolab_driver::INVITATIONS_CALENDAR_PENDING:
- $this->partstats = array('NEEDS-ACTION');
- $this->name = $this->cal->gettext('invitationspending');
- if (!empty($_REQUEST['_quickview']))
- $this->partstats[] = 'TENTATIVE';
- break;
+ switch ($this->id) {
+ case kolab_driver::INVITATIONS_CALENDAR_PENDING:
+ $this->partstats = ['NEEDS-ACTION'];
+ $this->name = $this->cal->gettext('invitationspending');
- case kolab_driver::INVITATIONS_CALENDAR_DECLINED:
- $this->partstats = array('DECLINED');
- $this->name = $this->cal->gettext('invitationsdeclined');
- break;
- }
-
- // user-specific alarms settings win
- $prefs = $this->cal->rc->config->get('kolab_calendars', array());
- if (isset($prefs[$this->id]['showalarms']))
- $this->alarms = $prefs[$this->id]['showalarms'];
- }
-
- /**
- * Getter for a nice and human readable name for this calendar
- *
- * @return string Name of this calendar
- */
- public function get_name()
- {
- return $this->name;
- }
-
- /**
- * Getter for the IMAP folder owner
- *
- * @return string Name of the folder owner
- */
- public function get_owner()
- {
- return $this->cal->rc->get_user_name();
- }
-
- /**
- *
- */
- public function get_title()
- {
- return $this->get_name();
- }
-
- /**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- return 'x-special';
- }
-
- /**
- * Getter for the top-end calendar folder name (not the entire path)
- *
- * @return string Name of this calendar
- */
- public function get_foldername()
- {
- return $this->get_name();
- }
-
- /**
- * Getter for the Cyrus mailbox identifier corresponding to this folder
- *
- * @return string Mailbox ID
- */
- public function get_mailbox_id()
- {
- // this is a virtual collection and has no concrete mailbox ID
- return null;
- }
-
- /**
- * Return color to display this calendar
- */
- public function get_color()
- {
- // calendar color is stored in local user prefs
- $prefs = $this->cal->rc->config->get('kolab_calendars', array());
-
- if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
- return $prefs[$this->id]['color'];
-
- return 'ffffff';
- }
-
- /**
- * Compose an URL for CalDAV access to this calendar (if configured)
- */
- public function get_caldav_url()
- {
- return false;
- }
-
- /**
- * Check activation status of this folder
- *
- * @return boolean True if enabled, false if not
- */
- public function is_active()
- {
- $prefs = $this->cal->rc->config->get('kolab_calendars', array()); // read local prefs
- return (bool)$prefs[$this->id]['active'];
- }
-
- /**
- * Update properties of this calendar folder
- *
- * @see calendar_driver::edit_calendar()
- */
- public function update(&$prop)
- {
- // don't change anything.
- // let kolab_driver save props in local prefs
- return $prop['id'];
- }
-
- /**
- * Getter for a single event object
- */
- public function get_event($id)
- {
- // redirect call to kolab_driver::get_event()
- $event = $this->cal->driver->get_event($id, calendar_driver::FILTER_WRITEABLE);
-
- if (is_array($event)) {
- $event = $this->_mod_event($event, $event['calendar']);
- }
-
- return $event;
- }
-
- /**
- * Create instances of a recurring event
- *
- * @see kolab_calendar::get_recurring_events()
- */
- public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
- {
- // forward call to the actual storage folder
- if ($event['_folder_id']) {
- $cal = $this->cal->driver->get_calendar($event['_folder_id']);
- if ($cal && $cal->ready) {
- return $cal->get_recurring_events($event, $start, $end, $event_id, $limit);
- }
- }
- }
-
- /**
- * Get attachment body
- *
- * @see calendar_driver::get_attachment_body()
- */
- public function get_attachment_body($id, $event)
- {
- // find the actual folder this event resides in
- if (!empty($event['_folder_id'])) {
- $cal = $this->cal->driver->get_calendar($event['_folder_id']);
- }
- else {
- $cal = null;
- foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
- $cal = $this->_get_calendar($foldername);
- if ($cal->ready && $cal->storage && $cal->get_event($event['id'])) {
- break;
- }
- }
- }
-
- if ($cal && $cal->storage) {
- return $cal->get_attachment_body($id, $event);
- }
-
- return false;
- }
-
- /**
- * @param integer Event's new start (unix timestamp)
- * @param integer Event's new end (unix timestamp)
- * @param string Search query (optional)
- * @param boolean Include virtual events (optional)
- * @param array Additional parameters to query storage
- *
- * @return array A list of event records
- */
- public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
- {
- // get email addresses of the current user
- $user_emails = $this->cal->get_user_emails();
- $subquery = array();
- foreach ($user_emails as $email) {
- foreach ($this->partstats as $partstat) {
- $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat));
- }
- }
-
- // aggregate events from all calendar folders
- $events = array();
- foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
- $cal = $this->_get_calendar($foldername);
- if (!$cal || $cal->get_namespace() == 'other')
- continue;
-
- foreach ($cal->list_events($start, $end, $search, 1, $query, array(array($subquery, 'OR'))) as $event) {
- $match = false;
-
- // post-filter events to match out partstats
- if (is_array($event['attendees'])) {
- foreach ($event['attendees'] as $attendee) {
- if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $this->partstats)) {
- $match = true;
- break;
+ if (!empty($_REQUEST['_quickview'])) {
+ $this->partstats[] = 'TENTATIVE';
}
- }
+ break;
+
+ case kolab_driver::INVITATIONS_CALENDAR_DECLINED:
+ $this->partstats = ['DECLINED'];
+ $this->name = $this->cal->gettext('invitationsdeclined');
+ break;
}
- if ($match) {
- $events[$event['id'] ?: $event['uid']] = $this->_mod_event($event, $cal->id);
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []);
+ if (isset($prefs[$this->id]['showalarms'])) {
+ $this->alarms = $prefs[$this->id]['showalarms'];
}
- }
-
- // merge list of event categories (really?)
- $this->categories += $cal->categories;
}
- return $events;
- }
-
- /**
- * Get number of events in the given calendar
- *
- * @param integer Date range start (unix timestamp)
- * @param integer Date range end (unix timestamp)
- * @param array Additional query to filter events
- *
- * @return integer Count
- */
- public function count_events($start, $end = null, $filter = null)
- {
- // get email addresses of the current user
- $user_emails = $this->cal->get_user_emails();
- $subquery = array();
- foreach ($user_emails as $email) {
- foreach ($this->partstats as $partstat) {
- $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat));
- }
+ /**
+ * Getter for a nice and human readable name for this calendar
+ *
+ * @return string Name of this calendar
+ */
+ public function get_name()
+ {
+ return $this->name;
}
- $filter = array(
- array('tags','!=','x-status:cancelled'),
- array($subquery, 'OR')
- );
-
- // aggregate counts from all calendar folders
- $count = 0;
- foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
- $cal = $this->_get_calendar($foldername);
- if (!$cal || $cal->get_namespace() == 'other')
- continue;
-
- $count += $cal->count_events($start, $end, $filter);
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner()
+ {
+ return $this->cal->rc->get_user_name();
}
- return $count;
- }
-
- /**
- * Get calendar object instance (that maybe already initialized)
- */
- private function _get_calendar($folder_name)
- {
- $id = kolab_storage::folder_id($folder_name, true);
- return $this->cal->driver->get_calendar($id);
- }
-
- /**
- * Helper method to modify some event properties
- */
- private function _mod_event($event, $calendar_id = null)
- {
- // set classes according to PARTSTAT
- $event = kolab_driver::add_partstat_class($event, $this->partstats);
-
- if (strpos($event['className'], 'fc-invitation-') !== false) {
- $event['calendar'] = $this->id;
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return $this->get_name();
}
- // add pointer to original calendar folder
- if ($calendar_id) {
- $event['_folder_id'] = $calendar_id;
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return 'x-special';
}
- return $event;
- }
-
- /**
- * Create a new event record
- *
- * @see kolab_calendar::insert_event()
- */
- public function insert_event($event)
- {
- return false;
- }
-
- /**
- * Update a specific event record
- *
- * @see kolab_calendar::update_event()
- */
- public function update_event($event, $exception_id = null)
- {
- // forward call to the actual storage folder
- if ($event['_folder_id']) {
- $cal = $this->cal->driver->get_calendar($event['_folder_id']);
- if ($cal && $cal->ready) {
- return $cal->update_event($event, $exception_id);
- }
+ /**
+ * Getter for the top-end calendar folder name (not the entire path)
+ *
+ * @return string Name of this calendar
+ */
+ public function get_foldername()
+ {
+ return $this->get_name();
}
- return false;
- }
-
- /**
- * Delete an event record
- *
- * @see kolab_calendar::delete_event()
- */
- public function delete_event($event, $force = true)
- {
- // forward call to the actual storage folder
- if ($event['_folder_id']) {
- $cal = $this->cal->driver->get_calendar($event['_folder_id']);
- if ($cal && $cal->ready) {
- return $cal->delete_event($event, $force);
- }
+ /**
+ * Getter for the Cyrus mailbox identifier corresponding to this folder
+ *
+ * @return string Mailbox ID
+ */
+ public function get_mailbox_id()
+ {
+ // this is a virtual collection and has no concrete mailbox ID
+ return null;
}
- return false;
- }
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color()
+ {
+ // calendar color is stored in local user prefs
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []);
- /**
- * Restore deleted event record
- *
- * @see kolab_calendar::restore_event()
- */
- public function restore_event($event)
- {
- // forward call to the actual storage folder
- if ($event['_folder_id']) {
- $cal = $this->cal->driver->get_calendar($event['_folder_id']);
- if ($cal && $cal->ready) {
- return $cal->restore_event($event);
- }
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color'])) {
+ return $prefs[$this->id]['color'];
+ }
+
+ return 'ffffff';
}
- return false;
- }
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ return false;
+ }
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return bool True if enabled, false if not
+ */
+ public function is_active()
+ {
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []); // read local prefs
+ return !empty($prefs[$this->id]['active']);
+ }
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ // don't change anything.
+ // let kolab_driver save props in local prefs
+ return $prop['id'];
+ }
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // redirect call to kolab_driver::get_event()
+ $event = $this->cal->driver->get_event($id, calendar_driver::FILTER_WRITEABLE);
+
+ if (is_array($event)) {
+ $event = $this->_mod_event($event, $event['calendar']);
+ }
+
+ return $event;
+ }
+
+ /**
+ * Create instances of a recurring event
+ *
+ * @see kolab_calendar::get_recurring_events()
+ */
+ public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
+ {
+ // forward call to the actual storage folder
+ if (!empty($event['_folder_id'])) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ if ($cal && $cal->ready) {
+ return $cal->get_recurring_events($event, $start, $end, $event_id, $limit);
+ }
+ }
+ }
+
+ /**
+ * Get attachment body
+ *
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ // find the actual folder this event resides in
+ if (!empty($event['_folder_id'])) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ }
+ else {
+ $cal = null;
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = $this->_get_calendar($foldername);
+ if ($cal->ready && $cal->storage && $cal->get_event($event['id'])) {
+ break;
+ }
+ }
+ }
+
+ if ($cal && $cal->storage) {
+ return $cal->get_attachment_body($id, $event);
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int Event's new start (unix timestamp)
+ * @param int Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param bool Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ *
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = [])
+ {
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+ $subquery = [];
+
+ foreach ($user_emails as $email) {
+ foreach ($this->partstats as $partstat) {
+ $subquery[] = ['tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat)];
+ }
+ }
+
+ $events = [];
+
+ // aggregate events from all calendar folders
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = $this->_get_calendar($foldername);
+ if (!$cal || $cal->get_namespace() == 'other') {
+ continue;
+ }
+
+ foreach ($cal->list_events($start, $end, $search, 1, $query, [[$subquery, 'OR']]) as $event) {
+ $match = false;
+
+ // post-filter events to match out partstats
+ if (!empty($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (
+ in_array($attendee['email'], $user_emails)
+ && in_array($attendee['status'], $this->partstats)
+ ) {
+ $match = true;
+ break;
+ }
+ }
+ }
+
+ if ($match) {
+ $uid = !empty($event['id']) ? $event['id'] : $event['uid'];
+ $events[$uid] = $this->_mod_event($event, $cal->id);
+ }
+ }
+
+ // merge list of event categories (really?)
+ $this->categories += $cal->categories;
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param int Date range start (unix timestamp)
+ * @param int Date range end (unix timestamp)
+ * @param array Additional query to filter events
+ *
+ * @return int Count
+ */
+ public function count_events($start, $end = null, $filter = null)
+ {
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+ $subquery = [];
+
+ foreach ($user_emails as $email) {
+ foreach ($this->partstats as $partstat) {
+ $subquery[] = ['tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat)];
+ }
+ }
+
+ $filter = [
+ ['tags', '!=', 'x-status:cancelled'],
+ [$subquery, 'OR']
+ ];
+
+ // aggregate counts from all calendar folders
+ $count = 0;
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = $this->_get_calendar($foldername);
+ if (!$cal || $cal->get_namespace() == 'other') {
+ continue;
+ }
+
+ $count += $cal->count_events($start, $end, $filter);
+ }
+
+ return $count;
+ }
+
+ /**
+ * Get calendar object instance (that maybe already initialized)
+ */
+ private function _get_calendar($folder_name)
+ {
+ $id = kolab_storage::folder_id($folder_name, true);
+ return $this->cal->driver->get_calendar($id);
+ }
+
+ /**
+ * Helper method to modify some event properties
+ */
+ private function _mod_event($event, $calendar_id = null)
+ {
+ // set classes according to PARTSTAT
+ $event = kolab_driver::add_partstat_class($event, $this->partstats);
+
+ if (strpos($event['className'], 'fc-invitation-') !== false) {
+ $event['calendar'] = $this->id;
+ }
+
+ // add pointer to original calendar folder
+ if ($calendar_id) {
+ $event['_folder_id'] = $calendar_id;
+ }
+
+ return $event;
+ }
+
+ /**
+ * Create a new event record
+ *
+ * @see kolab_calendar::insert_event()
+ */
+ public function insert_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see kolab_calendar::update_event()
+ */
+ public function update_event($event, $exception_id = null)
+ {
+ // forward call to the actual storage folder
+ if (!empty($event['_folder_id'])) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ if ($cal && $cal->ready) {
+ return $cal->update_event($event, $exception_id);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see kolab_calendar::delete_event()
+ */
+ public function delete_event($event, $force = true)
+ {
+ // forward call to the actual storage folder
+ if (!empty($event['_folder_id'])) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ if ($cal && $cal->ready) {
+ return $cal->delete_event($event, $force);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see kolab_calendar::restore_event()
+ */
+ public function restore_event($event)
+ {
+ // forward call to the actual storage folder
+ if (!empty($event['_folder_id'])) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ if ($cal && $cal->ready) {
+ return $cal->restore_event($event);
+ }
+ }
+
+ return false;
+ }
}
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index b3c1cd1a..98eaca67 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -23,402 +23,423 @@
class kolab_user_calendar extends kolab_calendar
{
- public $id = 'unknown';
- public $ready = false;
- public $editable = false;
- public $attachments = false;
- public $subscriptions = false;
+ public $id = 'unknown';
+ public $ready = false;
+ public $editable = false;
+ public $attachments = false;
+ public $subscriptions = false;
- protected $userdata = array();
- protected $timeindex = array();
+ protected $userdata = [];
+ protected $timeindex = [];
- /**
- * Default constructor
- */
- public function __construct($user_or_folder, $calendar)
- {
- $this->cal = $calendar;
- $this->imap = $calendar->rc->get_storage();
+ /**
+ * Default constructor
+ */
+ public function __construct($user_or_folder, $calendar)
+ {
+ $this->cal = $calendar;
+ $this->imap = $calendar->rc->get_storage();
- // full user record is provided
- if (is_array($user_or_folder)) {
- $this->userdata = $user_or_folder;
- $this->storage = new kolab_storage_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata);
- }
- else if ($user_or_folder instanceof kolab_storage_folder_user) {
- $this->storage = $user_or_folder;
- $this->userdata = $this->storage->ldaprec;
- }
- else { // get user record from LDAP
- $this->storage = new kolab_storage_folder_user($user_or_folder);
- $this->userdata = $this->storage->ldaprec;
- }
-
- $this->ready = !empty($this->userdata['kolabtargetfolder']);
- $this->storage->type = 'event';
-
- if ($this->ready) {
- // ID is derrived from the user's kolabtargetfolder attribute
- $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
- $this->imap_folder = $this->userdata['kolabtargetfolder'];
- $this->name = $this->storage->name;
- $this->parent = ''; // user calendars are top level
-
- // user-specific alarms settings win
- $prefs = $this->cal->rc->config->get('kolab_calendars', array());
- if (isset($prefs[$this->id]['showalarms']))
- $this->alarms = $prefs[$this->id]['showalarms'];
- }
- }
-
- /**
- * Getter for a nice and human readable name for this calendar
- *
- * @return string Name of this calendar
- */
- public function get_name()
- {
- return $this->userdata['displayname'] ?: ($this->userdata['name'] ?: $this->userdata['mail']);
- }
-
- /**
- * Getter for the IMAP folder owner
- *
- * @param bool Return a fully qualified owner name (unused)
- *
- * @return string Name of the folder owner
- */
- public function get_owner($fully_qualified = false)
- {
- return $this->userdata['mail'];
- }
-
- /**
- *
- */
- public function get_title()
- {
- return trim($this->userdata['displayname'] . '; ' . $this->userdata['mail'], '; ');
- }
-
- /**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- return 'other user';
- }
-
- /**
- * Getter for the top-end calendar folder name (not the entire path)
- *
- * @return string Name of this calendar
- */
- public function get_foldername()
- {
- return $this->get_name();
- }
-
- /**
- * Return color to display this calendar
- */
- public function get_color($default = null)
- {
- // calendar color is stored in local user prefs
- $prefs = $this->cal->rc->config->get('kolab_calendars', array());
-
- if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
- return $prefs[$this->id]['color'];
-
- return $default ?: 'cc0000';
- }
-
- /**
- * Compose an URL for CalDAV access to this calendar (if configured)
- */
- public function get_caldav_url()
- {
- return false;
- }
-
- /**
- * Check subscription status of this folder
- *
- * @return boolean True if subscribed, false if not
- */
- public function is_subscribed()
- {
- return $this->storage->is_subscribed();
- }
-
- /**
- * Update properties of this calendar folder
- *
- * @see calendar_driver::edit_calendar()
- */
- public function update(&$prop)
- {
- // don't change anything.
- // let kolab_driver save props in local prefs
- return $prop['id'];
- }
-
- /**
- * Getter for a single event object
- */
- public function get_event($id)
- {
- // TODO: implement this
- return $this->events[$id];
- }
-
- /**
- * Get attachment body
- * @see calendar_driver::get_attachment_body()
- */
- public function get_attachment_body($id, $event)
- {
- if (!$event['calendar'] && ($ev = $this->get_event($event['id']))) {
- $event['calendar'] = $ev['calendar'];
- }
-
- if ($event['calendar'] && ($cal = $this->cal->get_calendar($event['calendar']))) {
- return $cal->get_attachment_body($id, $event);
- }
-
- return false;
- }
-
- /**
- * @param integer Event's new start (unix timestamp)
- * @param integer Event's new end (unix timestamp)
- * @param string Search query (optional)
- * @param boolean Include virtual events (optional)
- * @param array Additional parameters to query storage
- * @param array Additional query to filter events
- *
- * @return array A list of event records
- */
- public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null)
- {
- // convert to DateTime for comparisons
- try {
- $start_dt = new DateTime('@'.$start);
- }
- catch (Exception $e) {
- $start_dt = new DateTime('@0');
- }
- try {
- $end_dt = new DateTime('@'.$end);
- }
- catch (Exception $e) {
- $end_dt = new DateTime('today +10 years');
- }
-
- $limit_changed = null;
- if (!empty($query)) {
- foreach ($query as $q) {
- if ($q[0] == 'changed' && $q[1] == '>=') {
- try { $limit_changed = new DateTime('@'.$q[2]); }
- catch (Exception $e) { /* ignore */ }
+ // full user record is provided
+ if (is_array($user_or_folder)) {
+ $this->userdata = $user_or_folder;
+ $this->storage = new kolab_storage_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata);
}
- }
- }
-
- // aggregate all calendar folders the user shares (but are not activated)
- foreach (kolab_storage::list_user_folders($this->userdata, 'event', 2) as $foldername) {
- $cal = new kolab_calendar($foldername, $this->cal);
- foreach ($cal->list_events($start, $end, $search, 1) as $event) {
- $uid = $event['id'] ?: $event['uid'];
- $this->events[$uid] = $event;
- $this->timeindex[$this->time_key($event)] = $uid;
- }
- }
-
- // get events from the user's free/busy feed (for quickview only)
- $fbview = $this->cal->rc->config->get('calendar_include_freebusy_data', 1);
- if ($fbview && ($fbview == 1 || !empty($_REQUEST['_quickview'])) && empty($search)) {
- $this->fetch_freebusy($limit_changed);
- }
-
- $events = array();
- foreach ($this->events as $event) {
- // list events in requested time window
- if ($event['start'] <= $end_dt && $event['end'] >= $start_dt &&
- (!$limit_changed || !$event['changed'] || $event['changed'] >= $limit_changed)) {
- $events[] = $event;
- }
- }
-
- // avoid session race conditions that will loose temporary subscriptions
- $this->cal->rc->session->nowrite = true;
-
- return $events;
- }
-
- /**
- * Get number of events in the given calendar
- *
- * @param integer Date range start (unix timestamp)
- * @param integer Date range end (unix timestamp)
- * @param array Additional query to filter events
- *
- * @return integer Count
- */
- public function count_events($start, $end = null, $filter_query = null)
- {
- // not implemented
- return 0;
- }
-
- /**
- * 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->cal->rc->user->get_username(), $this->cal->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'),
- );
-
- // rcube::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;
+ else if ($user_or_folder instanceof kolab_storage_folder_user) {
+ $this->storage = $user_or_folder;
+ $this->userdata = $this->storage->ldaprec;
+ }
+ else {
+ // get user record from LDAP
+ $this->storage = new kolab_storage_folder_user($user_or_folder);
+ $this->userdata = $this->storage->ldaprec;
}
- foreach ($fb['periods'] as $tuple) {
- list($from, $to, $type) = $tuple;
- $event = array(
- 'uid' => 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'],
- ),
- );
+ $this->ready = !empty($this->userdata['kolabtargetfolder']);
+ $this->storage->type = 'event';
- // avoid duplicate entries
- $key = $this->time_key($event);
- if (!$this->timeindex[$key]) {
- $this->events[$event['uid']] = $event;
- $this->timeindex[$key] = $event['uid'];
- $count++;
- }
+ if ($this->ready) {
+ // ID is derrived from the user's kolabtargetfolder attribute
+ $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
+ $this->imap_folder = $this->userdata['kolabtargetfolder'];
+ $this->name = $this->storage->name;
+ $this->parent = ''; // user calendars are top level
+
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []);
+ if (isset($prefs[$this->id]['showalarms'])) {
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ }
}
- }
}
- return $count;
- }
+ /**
+ * Getter for a nice and human readable name for this calendar
+ *
+ * @return string Name of this calendar
+ */
+ public function get_name()
+ {
+ if (!empty($this->userdata['displayname'])) {
+ return $this->userdata['displayname'];
+ }
- /**
- * Helper to build a key for the absolute time slot the given event convers
- */
- private function time_key($event)
- {
- return sprintf('%s/%s', $event['start']->format('U'), is_object($event['end']) ? $event['end']->format('U') : '0');
- }
+ return !empty($this->userdata['name']) ? $this->userdata['name'] : $this->userdata['mail'];
+ }
- /**
- * Create a new event record
- *
- * @see calendar_driver::new_event()
- *
- * @return mixed The created record ID on success, False on error
- */
- public function insert_event($event)
- {
- return false;
- }
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @param bool Return a fully qualified owner name (unused)
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner($fully_qualified = false)
+ {
+ return $this->userdata['mail'];
+ }
- /**
- * Update a specific event record
- *
- * @see calendar_driver::new_event()
- * @return boolean True on success, False on error
- */
- public function update_event($event, $exception_id = null)
- {
- return false;
- }
+ /**
+ *
+ */
+ public function get_title()
+ {
+ $title = [];
- /**
- * Delete an event record
- *
- * @see calendar_driver::remove_event()
- * @return boolean True on success, False on error
- */
- public function delete_event($event, $force = true)
- {
- return false;
- }
+ if (!empty($this->userdata['displayname'])) {
+ $title[] = $this->userdata['displayname'];
+ }
- /**
- * Restore deleted event record
- *
- * @see calendar_driver::undelete_event()
- * @return boolean True on success, False on error
- */
- public function restore_event($event)
- {
- return false;
- }
+ $title[] = $this->userdata['mail'];
+
+ return implode('; ', $title);
+ }
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return 'other user';
+ }
+
+ /**
+ * Getter for the top-end calendar folder name (not the entire path)
+ *
+ * @return string Name of this calendar
+ */
+ public function get_foldername()
+ {
+ return $this->get_name();
+ }
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color($default = null)
+ {
+ // calendar color is stored in local user prefs
+ $prefs = $this->cal->rc->config->get('kolab_calendars', []);
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color'])) {
+ return $prefs[$this->id]['color'];
+ }
+
+ return $default ?: 'cc0000';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ return false;
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return $this->storage->is_subscribed();
+ }
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ // don't change anything.
+ // let kolab_driver save props in local prefs
+ return $prop['id'];
+ }
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // TODO: implement this
+ return isset($this->events[$id]) ? $this->events[$id] : null;
+ }
+
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (empty($event['calendar']) && ($ev = $this->get_event($event['id']))) {
+ $event['calendar'] = $ev['calendar'];
+ }
+
+ if (!empty($event['calendar']) && ($cal = $this->cal->get_calendar($event['calendar']))) {
+ return $cal->get_attachment_body($id, $event);
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int Event's new start (unix timestamp)
+ * @param int Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param bool Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @param array Additional query to filter events
+ *
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = [], $filter_query = null)
+ {
+ // convert to DateTime for comparisons
+ try {
+ $start_dt = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start_dt = new DateTime('@0');
+ }
+ try {
+ $end_dt = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end_dt = new DateTime('today +10 years');
+ }
+
+ $limit_changed = null;
+
+ if (!empty($query)) {
+ foreach ($query as $q) {
+ if ($q[0] == 'changed' && $q[1] == '>=') {
+ try { $limit_changed = new DateTime('@'.$q[2]); }
+ catch (Exception $e) { /* ignore */ }
+ }
+ }
+ }
+
+ // aggregate all calendar folders the user shares (but are not activated)
+ foreach (kolab_storage::list_user_folders($this->userdata, 'event', 2) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ foreach ($cal->list_events($start, $end, $search, 1) as $event) {
+ $uid = !empty($event['id']) ? $event['id'] : $event['uid'];
+ $this->events[$uid] = $event;
+ $this->timeindex[$this->time_key($event)] = $uid;
+ }
+ }
+
+ // get events from the user's free/busy feed (for quickview only)
+ $fbview = $this->cal->rc->config->get('calendar_include_freebusy_data', 1);
+ if ($fbview && ($fbview == 1 || !empty($_REQUEST['_quickview'])) && empty($search)) {
+ $this->fetch_freebusy($limit_changed);
+ }
+
+ $events = [];
+ foreach ($this->events as $event) {
+ // list events in requested time window
+ if (
+ $event['start'] <= $end_dt
+ && $event['end'] >= $start_dt
+ && (!$limit_changed || empty($event['changed']) || $event['changed'] >= $limit_changed)
+ ) {
+ $events[] = $event;
+ }
+ }
+
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->cal->rc->session->nowrite = true;
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param int Date range start (unix timestamp)
+ * @param int Date range end (unix timestamp)
+ * @param array Additional query to filter events
+ *
+ * @return integer Count
+ */
+ public function count_events($start, $end = null, $filter_query = null)
+ {
+ // not implemented
+ return 0;
+ }
+
+ /**
+ * 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 = [
+ '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->cal->rc->user->get_username(), $this->cal->rc->decrypt($_SESSION['password']));
+ $response = $request->send();
+ }
+
+ if ($response->getStatus() == 200) {
+ $fbdata = $response->getBody();
+ }
+
+ unset($request, $response);
+ }
+ catch (Exception $e) {
+ rcube::raise_error([
+ 'code' => 900, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error fetching free/busy information: " . $e->getMessage()
+ ],
+ true, false
+ );
+
+ return false;
+ }
+
+ $statusmap = [
+ 'FREE' => 'free',
+ 'BUSY' => 'busy',
+ 'BUSY-TENTATIVE' => 'tentative',
+ 'X-OUT-OF-OFFICE' => 'outofoffice',
+ 'OOF' => 'outofoffice',
+ ];
+
+ $titlemap = [
+ 'FREE' => $this->cal->gettext('availfree'),
+ 'BUSY' => $this->cal->gettext('availbusy'),
+ 'BUSY-TENTATIVE' => $this->cal->gettext('availtentative'),
+ 'X-OUT-OF-OFFICE' => $this->cal->gettext('availoutofoffice'),
+ ];
+
+ // rcube::console('_fetch_freebusy', kolab_storage::get_freebusy_url($this->userdata['mail']), $fbdata);
+
+ $count = 0;
+
+ // parse free-busy information
+ if (!empty($fbdata)) {
+ $ical = $this->cal->get_ical();
+ $ical->import($fbdata);
+ if ($fb = $ical->freebusy) {
+ // consider 'changed >= X' queries
+ if ($limit_changed && !empty($fb['created']) && $fb['created'] < $limit_changed) {
+ return 0;
+ }
+
+ foreach ($fb['periods'] as $tuple) {
+ list($from, $to, $type) = $tuple;
+ $event = [
+ 'uid' => md5($this->id . $from->format('U') . '/' . $to->format('U')),
+ 'calendar' => $this->id,
+ 'changed' => !empty($fb['created']) ? $fb['created'] : new DateTime(),
+ 'title' => $this->get_name() . ' ' . (!empty($titlemap[$type]) ? $titlemap[$type] : $type),
+ 'start' => $from,
+ 'end' => $to,
+ 'free_busy' => !empty($statusmap[$type]) ? $statusmap[$type] : 'busy',
+ 'className' => 'fc-type-freebusy',
+ 'organizer' => [
+ 'email' => $this->userdata['mail'],
+ 'name' => isset($this->userdata['displayname']) ? $this->userdata['displayname'] : null,
+ ],
+ ];
+
+ // avoid duplicate entries
+ $key = $this->time_key($event);
+ if (empty($this->timeindex[$key])) {
+ $this->events[$event['uid']] = $event;
+ $this->timeindex[$key] = $event['uid'];
+ $count++;
+ }
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Helper to build a key for the absolute time slot the given event convers
+ */
+ private function time_key($event)
+ {
+ return sprintf('%s/%s', $event['start']->format('U'), is_object($event['end']) ? $event['end']->format('U') : '0');
+ }
+
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ * @return bool True on success, False on error
+ */
+ public function update_event($event, $exception_id = null)
+ {
+ return false;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ * @return bool True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ return false;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ * @return bool True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
}
diff --git a/plugins/calendar/drivers/ldap/resources_driver_ldap.php b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
index bd2d610c..0be41261 100644
--- a/plugins/calendar/drivers/ldap/resources_driver_ldap.php
+++ b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
@@ -41,72 +41,76 @@ class resources_driver_ldap extends resources_driver
/**
* Fetch resource objects to be displayed for booking
*
- * @param string Search query (optional)
- * @return array List of resource records available for booking
+ * @param string $query Search query (optional)
+ * @param int $num Max size of the result
+ *
+ * @return array List of resource records available for booking
*/
public function load_resources($query = null, $num = 5000)
{
- if (!($ldap = $this->connect())) {
- return array();
- }
-
- // TODO: apply paging
- $ldap->set_pagesize($num);
-
- if (isset($query)) {
- $results = $ldap->search('*', $query, 0, true, true);
- }
- else {
- $results = $ldap->list_records();
- }
-
- if ($results instanceof ArrayAccess) {
- foreach ($results as $i => $rec) {
- $results[$i] = $this->decode_resource($rec);
+ if (!($ldap = $this->connect())) {
+ return [];
}
- }
- return $results;
+ // TODO: apply paging
+ $ldap->set_pagesize($num);
+
+ if (isset($query)) {
+ $results = $ldap->search('*', $query, 0, true, true);
+ }
+ else {
+ $results = $ldap->list_records();
+ }
+
+ if ($results instanceof ArrayAccess) {
+ foreach ($results as $i => $rec) {
+ $results[$i] = $this->decode_resource($rec);
+ }
+ }
+
+ return $results;
}
/**
* Return properties of a single resource
*
- * @param string Unique resource identifier
+ * @param string $id Unique resource identifier
+ *
* @return array Resource object as hash array
*/
public function get_resource($dn)
{
- $rec = null;
+ $rec = null;
- if ($ldap = $this->connect()) {
- $rec = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
+ if ($ldap = $this->connect()) {
+ $rec = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
- if (!empty($rec)) {
- $rec = $this->decode_resource($rec);
+ if (!empty($rec)) {
+ $rec = $this->decode_resource($rec);
+ }
}
- }
- return $rec;
+ return $rec;
}
/**
* Return properties of a resource owner
*
- * @param string Owner identifier
- * @return array Resource object as hash array
+ * @param string $dn Owner identifier
+ *
+ * @return array Resource object as hash array
*/
public function get_resource_owner($dn)
{
- $owner = null;
+ $owner = null;
- if ($ldap = $this->connect()) {
- $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
- $owner['ID'] = rcube_ldap::dn_decode($owner['ID']);
- unset($owner['_raw_attrib'], $owner['_type']);
- }
+ if ($ldap = $this->connect()) {
+ $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
+ $owner['ID'] = rcube_ldap::dn_decode($owner['ID']);
+ unset($owner['_raw_attrib'], $owner['_type']);
+ }
- return $owner;
+ return $owner;
}
/**
@@ -114,41 +118,40 @@ class resources_driver_ldap extends resources_driver
*/
private function decode_resource($rec)
{
- $rec['ID'] = rcube_ldap::dn_decode($rec['ID']);
+ $rec['ID'] = rcube_ldap::dn_decode($rec['ID']);
- $attributes = array();
+ $attributes = [];
- foreach ((array) $rec['attributes'] as $sattr) {
- $sattr = trim($sattr);
- if ($sattr && $sattr[0] === '{') {
- $attr = @json_decode($sattr, true);
- $attributes += $attr;
+ foreach ((array) $rec['attributes'] as $sattr) {
+ $sattr = trim($sattr);
+ if (!empty($sattr) && $sattr[0] === '{') {
+ $attr = @json_decode($sattr, true);
+ $attributes += $attr;
+ }
+ else if (!empty($sattr) && empty($rec['description'])) {
+ $rec['description'] = $sattr;
+ }
}
- else if ($sattr && empty($rec['description'])) {
- $rec['description'] = $sattr;
+
+ $rec['attributes'] = $attributes;
+
+ // force $rec['members'] to be an array
+ if (!empty($rec['members']) && !is_array($rec['members'])) {
+ $rec['members'] = [$rec['members']];
}
- }
- $rec['attributes'] = $attributes;
+ // remove unused cruft
+ unset($rec['_raw_attrib']);
- // force $rec['members'] to be an array
- if (!empty($rec['members']) && !is_array($rec['members'])) {
- $rec['members'] = array($rec['members']);
- }
-
- // remove unused cruft
- unset($rec['_raw_attrib']);
-
- return $rec;
+ return $rec;
}
private function connect()
{
- if (!isset($this->ldap)) {
- $this->ldap = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true);
- }
+ if (!isset($this->ldap)) {
+ $this->ldap = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true);
+ }
- return $this->ldap->ready ? $this->ldap : null;
+ return $this->ldap->ready ? $this->ldap : null;
}
-
-}
\ No newline at end of file
+}
diff --git a/plugins/calendar/drivers/resources_driver.php b/plugins/calendar/drivers/resources_driver.php
index 4c141cdf..a81a0fff 100644
--- a/plugins/calendar/drivers/resources_driver.php
+++ b/plugins/calendar/drivers/resources_driver.php
@@ -26,87 +26,93 @@
*/
abstract class resources_driver
{
- protected $cal;
+ protected $cal;
- /**
- * Default constructor
- */
- function __construct($cal)
- {
- $this->cal = $cal;
- }
+ /**
+ * Default constructor
+ */
+ function __construct($cal)
+ {
+ $this->cal = $cal;
+ }
- /**
- * Fetch resource objects to be displayed for booking
- *
- * @param string Search query (optional)
- * @return array List of resource records available for booking
- */
- abstract public function load_resources($query = null);
+ /**
+ * Fetch resource objects to be displayed for booking
+ *
+ * @param string $query Search query (optional)
+ *
+ * @return array List of resource records available for booking
+ */
+ abstract public function load_resources($query = null);
- /**
- * Return properties of a single resource
- *
- * @param string Unique resource identifier
- * @return array Resource object as hash array
- */
- abstract public function get_resource($id);
+ /**
+ * Return properties of a single resource
+ *
+ * @param string $id Unique resource identifier
+ *
+ * @return array Resource object as hash array
+ */
+ abstract public function get_resource($id);
- /**
- * Return properties of a resource owner
- *
- * @param string Owner identifier
- * @return array Resource object as hash array
- */
- public function get_resource_owner($id)
- {
- return null;
- }
+ /**
+ * Return properties of a resource owner
+ *
+ * @param string $id Owner identifier
+ *
+ * @return array Resource object as hash array
+ */
+ public function get_resource_owner($id)
+ {
+ return null;
+ }
- /**
- * Get event data to display a resource's calendar
- *
- * The default implementation extracts the resource's email address
- * and fetches free-busy data using the calendar backend driver.
- *
- * @param integer Event's new start (unix timestamp)
- * @param integer Event's new end (unix timestamp)
- * @return array A list of event objects (see calendar_driver specification)
- */
- public function get_resource_calendar($id, $start, $end)
- {
- $events = array();
- $rec = $this->get_resource($id);
- if ($rec && !empty($rec['email']) && $this->cal->driver) {
- $fbtypemap = array(
- calendar::FREEBUSY_BUSY => 'busy',
- calendar::FREEBUSY_TENTATIVE => 'tentative',
- calendar::FREEBUSY_OOF => 'outofoffice',
- );
+ /**
+ * Get event data to display a resource's calendar
+ *
+ * The default implementation extracts the resource's email address
+ * and fetches free-busy data using the calendar backend driver.
+ *
+ * @param string $id Calendar identifier
+ * @param int $start Event's new start (unix timestamp)
+ * @param int $end Event's new end (unix timestamp)
+ *
+ * @return array A list of event objects (see calendar_driver specification)
+ */
+ public function get_resource_calendar($id, $start, $end)
+ {
+ $events = [];
+ $rec = $this->get_resource($id);
- // if the backend has free-busy information
- $fblist = $this->cal->driver->get_freebusy_list($rec['email'], $start, $end);
- if (is_array($fblist)) {
- foreach ($fblist as $slot) {
- list($from, $to, $type) = $slot;
- if ($type == calendar::FREEBUSY_FREE || $type == calendar::FREEBUSY_UNKNOWN) {
- continue;
- }
- if ($from < $end && $to > $start) {
- $event = array(
- 'id' => sha1($id . $from . $to),
- 'title' => $rec['name'],
- 'start' => new DateTime('@' . $from),
- 'end' => new DateTime('@' . $to),
- 'status' => $fbtypemap[$type],
- 'calendar' => '_resource',
- );
- $events[] = $event;
- }
- }
- }
- }
+ if ($rec && !empty($rec['email']) && !empty($this->cal->driver)) {
+ $fbtypemap = [
+ calendar::FREEBUSY_BUSY => 'busy',
+ calendar::FREEBUSY_TENTATIVE => 'tentative',
+ calendar::FREEBUSY_OOF => 'outofoffice',
+ ];
- return $events;
- }
+ // if the backend has free-busy information
+ $fblist = $this->cal->driver->get_freebusy_list($rec['email'], $start, $end);
+ if (is_array($fblist)) {
+ foreach ($fblist as $slot) {
+ list($from, $to, $type) = $slot;
+ if ($type == calendar::FREEBUSY_FREE || $type == calendar::FREEBUSY_UNKNOWN) {
+ continue;
+ }
+
+ if ($from < $end && $to > $start) {
+ $events[] = [
+ 'id' => sha1($id . $from . $to),
+ 'title' => $rec['name'],
+ 'start' => new DateTime('@' . $from),
+ 'end' => new DateTime('@' . $to),
+ 'status' => $fbtypemap[$type],
+ 'calendar' => '_resource',
+ ];
+ }
+ }
+ }
+ }
+
+ return $events;
+ }
}
diff --git a/plugins/calendar/lib/calendar_itip.php b/plugins/calendar/lib/calendar_itip.php
index e2a2402b..4f7f382f 100644
--- a/plugins/calendar/lib/calendar_itip.php
+++ b/plugins/calendar/lib/calendar_itip.php
@@ -28,213 +28,231 @@ require_once realpath(__DIR__ . '/../../libcalendaring/lib/libcalendaring_itip.p
*/
class calendar_itip extends libcalendaring_itip
{
- /**
- * Constructor to set text domain to calendar
- */
- function __construct($plugin, $domain = 'calendar')
- {
- parent::__construct($plugin, $domain);
+ /**
+ * Constructor to set text domain to calendar
+ */
+ function __construct($plugin, $domain = 'calendar')
+ {
+ parent::__construct($plugin, $domain);
- $this->db_itipinvitations = $this->rc->db->table_name('itipinvitations', true);
- }
-
- /**
- * Handler for calendar/itip-status requests
- */
- public function get_itip_status($event, $existing = null)
- {
- $status = parent::get_itip_status($event, $existing);
-
- // don't ask for deleting events when declining
- if ($this->rc->config->get('kolab_invitation_calendars'))
- $status['saved'] = false;
-
- return $status;
- }
-
- /**
- * Find invitation record by token
- *
- * @param string Invitation token
- * @return mixed Invitation record as hash array or False if not found
- */
- public function get_invitation($token)
- {
- if ($parts = $this->decode_token($token)) {
- $result = $this->rc->db->query("SELECT * FROM $this->db_itipinvitations WHERE `token` = ?", $parts['base']);
- if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
- $rec['event'] = unserialize($rec['event']);
- $rec['attendee'] = $parts['attendee'];
- return $rec;
- }
+ $this->db_itipinvitations = $this->rc->db->table_name('itipinvitations', true);
}
-
- return false;
- }
- /**
- * Update the attendee status of the given invitation record
- *
- * @param array Invitation record as fetched with calendar_itip::get_invitation()
- * @param string Attendee email address
- * @param string New attendee status
- */
- public function update_invitation($invitation, $email, $newstatus)
- {
- if (is_string($invitation))
- $invitation = $this->get_invitation($invitation);
-
- if ($invitation['token'] && $invitation['event']) {
- // update attendee record in event data
- foreach ($invitation['event']['attendees'] as $i => $attendee) {
- if ($attendee['role'] == 'ORGANIZER') {
- $organizer = $attendee;
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ public function get_itip_status($event, $existing = null)
+ {
+ $status = parent::get_itip_status($event, $existing);
+
+ // don't ask for deleting events when declining
+ if ($this->rc->config->get('kolab_invitation_calendars')) {
+ $status['saved'] = false;
}
- else if ($attendee['email'] == $email) {
- // nothing to be done here
- if ($attendee['status'] == $newstatus)
- return true;
-
- $invitation['event']['attendees'][$i]['status'] = $newstatus;
- $this->sender = $attendee;
+
+ return $status;
+ }
+
+ /**
+ * Find invitation record by token
+ *
+ * @param string $token Invitation token
+ *
+ * @return mixed Invitation record as hash array or False if not found
+ */
+ public function get_invitation($token)
+ {
+ if ($parts = $this->decode_token($token)) {
+ $result = $this->rc->db->query("SELECT * FROM $this->db_itipinvitations WHERE `token` = ?", $parts['base']);
+ if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ $rec['event'] = unserialize($rec['event']);
+ $rec['attendee'] = $parts['attendee'];
+
+ return $rec;
+ }
}
- }
- $invitation['event']['changed'] = new DateTime();
-
- // send iTIP REPLY message to organizer
- if ($organizer) {
- $status = strtolower($newstatus);
- if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
- $this->rc->output->command('display_message', $this->plugin->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
- else
- $this->rc->output->command('display_message', $this->plugin->gettext('itipresponseerror'), 'error');
- }
-
- // update record in DB
- $query = $this->rc->db->query(
- "UPDATE $this->db_itipinvitations
- SET `event` = ?
- WHERE `token` = ?",
- self::serialize_event($invitation['event']),
- $invitation['token']
- );
- if ($this->rc->db->affected_rows($query))
- return true;
+ return false;
}
-
- return false;
- }
+ /**
+ * Update the attendee status of the given invitation record
+ *
+ * @param array $invitation Invitation record as fetched with calendar_itip::get_invitation()
+ * @param string $email Attendee email address
+ * @param string $newstatus New attendee status
+ */
+ public function update_invitation($invitation, $email, $newstatus)
+ {
+ if (is_string($invitation)) {
+ $invitation = $this->get_invitation($invitation);
+ }
- /**
- * Create iTIP invitation token for later replies via URL
- *
- * @param array Hash array with event properties
- * @param string Attendee email address
- * @return string Invitation token
- */
- public function store_invitation($event, $attendee)
- {
- static $stored = array();
-
- if (!$event['uid'] || !$attendee)
- return false;
-
- // generate token for this invitation
- $token = $this->generate_token($event, $attendee);
- $base = substr($token, 0, 40);
-
- // already stored this
- if ($stored[$base])
- return $token;
+ if (!empty($invitation['token']) && !empty($invitation['event'])) {
+ // update attendee record in event data
+ foreach ($invitation['event']['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if ($attendee['email'] == $email) {
+ // nothing to be done here
+ if ($attendee['status'] == $newstatus) {
+ return true;
+ }
- // delete old entry
- $this->rc->db->query("DELETE FROM $this->db_itipinvitations WHERE `token` = ?", $base);
+ $invitation['event']['attendees'][$i]['status'] = $newstatus;
+ $this->sender = $attendee;
+ }
+ }
- $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+ $invitation['event']['changed'] = new DateTime();
- $query = $this->rc->db->query(
- "INSERT INTO $this->db_itipinvitations
- (`token`, `event_uid`, `user_id`, `event`, `expires`)
- VALUES(?, ?, ?, ?, ?)",
- $base,
- $event_uid,
- $this->rc->user->ID,
- self::serialize_event($event),
- date('Y-m-d H:i:s', $event['end']->format('U') + 86400 * 2)
- );
-
- if ($this->rc->db->affected_rows($query)) {
- $stored[$base] = 1;
- return $token;
+ // send iTIP REPLY message to organizer
+ if (!empty($organizer)) {
+ $status = strtolower($newstatus);
+ if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) {
+ $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];
+ $message = $this->plugin->gettext([
+ 'name' => 'sentresponseto',
+ 'vars' => ['mailto' => $mailto]
+ ]);
+ $this->rc->output->command('display_message', $message, 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->plugin->gettext('itipresponseerror'), 'error');
+ }
+ }
+
+ // update record in DB
+ $query = $this->rc->db->query(
+ "UPDATE $this->db_itipinvitations SET `event` = ? WHERE `token` = ?",
+ self::serialize_event($invitation['event']),
+ $invitation['token']
+ );
+
+ if ($this->rc->db->affected_rows($query)) {
+ return true;
+ }
+ }
+
+ return false;
}
-
- return false;
- }
- /**
- * Mark invitations for the given event as cancelled
- *
- * @param array Hash array with event properties
- */
- public function cancel_itip_invitation($event)
- {
- $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+ /**
+ * Create iTIP invitation token for later replies via URL
+ *
+ * @param array $event Hash array with event properties
+ * @param string $attendee Attendee email address
+ *
+ * @return string Invitation token
+ */
+ public function store_invitation($event, $attendee)
+ {
+ static $stored = [];
- // flag invitation record as cancelled
- $this->rc->db->query(
- "UPDATE $this->db_itipinvitations
- SET `cancelled` = 1
- WHERE `event_uid` = ? AND `user_id` = ?",
- $event_uid,
- $this->rc->user->ID
- );
- }
+ if (empty($event['uid']) || !$attendee) {
+ return false;
+ }
- /**
- * Generate an invitation request token for the given event and attendee
- *
- * @param array Event hash array
- * @param string Attendee email address
- */
- public function generate_token($event, $attendee)
- {
- $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
- $base = sha1($event_uid . ';' . $this->rc->user->ID);
- $mail = base64_encode($attendee);
- $hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6);
-
- return "$base.$mail.$hash";
- }
+ // generate token for this invitation
+ $token = $this->generate_token($event, $attendee);
+ $base = substr($token, 0, 40);
- /**
- * Decode the given iTIP request token and return its parts
- *
- * @param string Request token to decode
- * @return mixed Hash array with parts or False if invalid
- */
- public function decode_token($token)
- {
- list($base, $mail, $hash) = explode('.', $token);
-
- // validate and return parts
- if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) {
- return array('base' => $base, 'attendee' => base64_decode($mail));
+ // already stored this
+ if (!empty($stored[$base])) {
+ return $token;
+ }
+
+ // delete old entry
+ $this->rc->db->query("DELETE FROM $this->db_itipinvitations WHERE `token` = ?", $base);
+
+ $event_uid = $event['uid'] . (!empty($event['_instance']) ? '-' . $event['_instance'] : '');
+
+ $query = $this->rc->db->query(
+ "INSERT INTO $this->db_itipinvitations"
+ . " (`token`, `event_uid`, `user_id`, `event`, `expires`)"
+ . " VALUES(?, ?, ?, ?, ?)",
+ $base,
+ $event_uid,
+ $this->rc->user->ID,
+ self::serialize_event($event),
+ date('Y-m-d H:i:s', $event['end']->format('U') + 86400 * 2)
+ );
+
+ if ($this->rc->db->affected_rows($query)) {
+ $stored[$base] = 1;
+ return $token;
+ }
+
+ return false;
}
-
- return false;
- }
- /**
- * Helper method to serialize the given event for storing in invitations table
- */
- private static function serialize_event($event)
- {
- $ev = $event;
- $ev['description'] = abbreviate_string($ev['description'], 100);
- unset($ev['attachments']);
- return serialize($ev);
- }
+ /**
+ * Mark invitations for the given event as cancelled
+ *
+ * @param array $event Hash array with event properties
+ */
+ public function cancel_itip_invitation($event)
+ {
+ $event_uid = $event['uid'] . (!empty($event['_instance']) ? '-' . $event['_instance'] : '');
+ // flag invitation record as cancelled
+ $this->rc->db->query(
+ "UPDATE $this->db_itipinvitations SET `cancelled` = 1"
+ . " WHERE `event_uid` = ? AND `user_id` = ?",
+ $event_uid,
+ $this->rc->user->ID
+ );
+ }
+
+ /**
+ * Generate an invitation request token for the given event and attendee
+ *
+ * @param array $event Event hash array
+ * @param string $attendee Attendee email address
+ */
+ public function generate_token($event, $attendee)
+ {
+ $event_uid = $event['uid'] . (!empty($event['_instance']) ? '-' . $event['_instance'] : '');
+ $base = sha1($event_uid . ';' . $this->rc->user->ID);
+ $mail = base64_encode($attendee);
+ $hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6);
+
+ return "$base.$mail.$hash";
+ }
+
+ /**
+ * Decode the given iTIP request token and return its parts
+ *
+ * @param string $token Request token to decode
+ *
+ * @return mixed Hash array with parts or False if invalid
+ */
+ public function decode_token($token)
+ {
+ list($base, $mail, $hash) = explode('.', $token);
+
+ // validate and return parts
+ if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) {
+ return ['base' => $base, 'attendee' => base64_decode($mail)];
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to serialize the given event for storing in invitations table
+ */
+ private static function serialize_event($event)
+ {
+ $ev = $event;
+
+ if (!empty($ev['description'])) {
+ $ev['description'] = abbreviate_string($ev['description'], 100);
+ }
+
+ unset($ev['attachments']);
+
+ return serialize($ev);
+ }
}
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index c0d7c793..4888fe18 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -26,63 +26,64 @@ require_once realpath(__DIR__ . '/../../libcalendaring/lib/libcalendaring_recurr
*/
class calendar_recurrence extends libcalendaring_recurrence
{
- private $event;
- private $duration;
+ private $event;
+ private $duration;
- /**
- * Default constructor
- *
- * @param object calendar The calendar plugin instance
- * @param array The event object to operate on
- */
- function __construct($cal, $event)
- {
- parent::__construct($cal->lib);
+ /**
+ * Default constructor
+ *
+ * @param calendar $cal The calendar plugin instance
+ * @param array $event The event object to operate on
+ */
+ function __construct($cal, $event)
+ {
+ parent::__construct($cal->lib);
- $this->event = $event;
+ $this->event = $event;
- if (is_object($event['start']) && is_object($event['end']))
- $this->duration = $event['start']->diff($event['end']);
+ if (is_object($event['start']) && is_object($event['end'])) {
+ $this->duration = $event['start']->diff($event['end']);
+ }
- $event['start']->_dateonly |= $event['allday'];
- $this->init($event['recurrence'], $event['start']);
- }
+ $event['start']->_dateonly = !empty($event['allday']);
- /**
- * Alias of libcalendaring_recurrence::next()
- *
- * @return mixed DateTime object or False if recurrence ended
- */
- public function next_start()
- {
- return $this->next();
- }
-
- /**
- * Get the next recurring instance of this event
- *
- * @return mixed Array with event properties or False if recurrence ended
- */
- public function next_instance()
- {
- if ($next_start = $this->next()) {
- $next = $this->event;
- $next['start'] = $next_start;
-
- if ($this->duration) {
- $next['end'] = clone $next_start;
- $next['end']->add($this->duration);
- }
-
- $next['recurrence_date'] = clone $next_start;
- $next['_instance'] = libcalendaring::recurrence_instance_identifier($next, $this->event['allday']);
-
- unset($next['_formatobj']);
-
- return $next;
+ $this->init($event['recurrence'], $event['start']);
}
- return false;
- }
+ /**
+ * Alias of libcalendaring_recurrence::next()
+ *
+ * @return mixed DateTime object or False if recurrence ended
+ */
+ public function next_start()
+ {
+ return $this->next();
+ }
+ /**
+ * Get the next recurring instance of this event
+ *
+ * @return mixed Array with event properties or False if recurrence ended
+ */
+ public function next_instance()
+ {
+ if ($next_start = $this->next()) {
+ $next = $this->event;
+ $next['start'] = $next_start;
+
+ if ($this->duration) {
+ $next['end'] = clone $next_start;
+ $next['end']->add($this->duration);
+ }
+
+ $next['recurrence_date'] = clone $next_start;
+ $next['_instance'] = libcalendaring::recurrence_instance_identifier($next, $this->event['allday']);
+
+ unset($next['_formatobj']);
+
+ return $next;
+ }
+
+ return false;
+ }
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 63ab9c3b..0d1b1297 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -25,819 +25,1001 @@
class calendar_ui
{
- private $rc;
- private $cal;
- private $ready = false;
- public $screen;
+ private $rc;
+ private $cal;
+ private $ready = false;
- function __construct($cal)
- {
- $this->cal = $cal;
- $this->rc = $cal->rc;
- $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other';
- }
+ public $screen;
- /**
- * Calendar UI initialization and requests handlers
- */
- public function init()
- {
- if ($this->ready) // already done
- return;
-
- // add taskbar button
- $this->cal->add_button(array(
- 'command' => 'calendar',
- 'class' => 'button-calendar',
- 'classsel' => 'button-calendar button-selected',
- 'innerclass' => 'button-inner',
- 'label' => 'calendar.calendar',
- 'type' => 'link'
- ), 'taskbar');
-
- // load basic client script
- if ($this->rc->action != 'print') {
- $this->cal->include_script('calendar_base.js');
+ function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ?: 'calendar') : 'other';
}
- $this->addCSS();
+ /**
+ * Calendar UI initialization and requests handlers
+ */
+ public function init()
+ {
+ if ($this->ready) {
+ // already done
+ return;
+ }
- $this->ready = true;
- }
+ // add taskbar button
+ $this->cal->add_button([
+ 'command' => 'calendar',
+ 'class' => 'button-calendar',
+ 'classsel' => 'button-calendar button-selected',
+ 'innerclass' => 'button-inner',
+ 'label' => 'calendar.calendar',
+ 'type' => 'link'
+ ],
+ 'taskbar'
+ );
- /**
- * Register handler methods for the template engine
- */
- public function init_templates()
- {
- $this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css'));
- $this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list'));
- $this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select'));
- $this->cal->register_handler('plugin.identity_select', array($this, 'identity_select'));
- $this->cal->register_handler('plugin.category_select', array($this, 'category_select'));
- $this->cal->register_handler('plugin.status_select', array($this, 'status_select'));
- $this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select'));
- $this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
- $this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
- $this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
- $this->cal->register_handler('plugin.recurrence_form', array($this->cal->lib, 'recurrence_form'));
- $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
- $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
- $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form'));
- $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list'));
- $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form'));
- $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info'));
- $this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
- $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
- $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
- $this->cal->register_handler('plugin.edit_recurrence_sync', array($this, 'edit_recurrence_sync'));
- $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
- $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
- $this->cal->register_handler('plugin.agenda_options', array($this, 'agenda_options'));
- $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
- $this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
- $this->cal->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
- $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
+ // load basic client script
+ if ($this->rc->action != 'print') {
+ $this->cal->include_script('calendar_base.js');
+ }
- kolab_attachments_handler::ui();
- }
+ $this->addCSS();
- /**
- * Adds CSS stylesheets to the page header
- */
- public function addCSS()
- {
- $skin_path = $this->cal->local_skin_path();
+ $this->ready = true;
+ }
+
+ /**
+ * Register handler methods for the template engine
+ */
+ public function init_templates()
+ {
+ $this->cal->register_handler('plugin.calendar_css', [$this, 'calendar_css']);
+ $this->cal->register_handler('plugin.calendar_list', [$this, 'calendar_list']);
+ $this->cal->register_handler('plugin.calendar_select', [$this, 'calendar_select']);
+ $this->cal->register_handler('plugin.identity_select', [$this, 'identity_select']);
+ $this->cal->register_handler('plugin.category_select', [$this, 'category_select']);
+ $this->cal->register_handler('plugin.status_select', [$this, 'status_select']);
+ $this->cal->register_handler('plugin.freebusy_select', [$this, 'freebusy_select']);
+ $this->cal->register_handler('plugin.priority_select', [$this, 'priority_select']);
+ $this->cal->register_handler('plugin.sensitivity_select', [$this, 'sensitivity_select']);
+ $this->cal->register_handler('plugin.alarm_select', [$this, 'alarm_select']);
+ $this->cal->register_handler('plugin.recurrence_form', [$this->cal->lib, 'recurrence_form']);
+ $this->cal->register_handler('plugin.attendees_list', [$this, 'attendees_list']);
+ $this->cal->register_handler('plugin.attendees_form', [$this, 'attendees_form']);
+ $this->cal->register_handler('plugin.resources_form', [$this, 'resources_form']);
+ $this->cal->register_handler('plugin.resources_list', [$this, 'resources_list']);
+ $this->cal->register_handler('plugin.resources_searchform', [$this, 'resources_search_form']);
+ $this->cal->register_handler('plugin.resource_info', [$this, 'resource_info']);
+ $this->cal->register_handler('plugin.resource_calendar', [$this, 'resource_calendar']);
+ $this->cal->register_handler('plugin.attendees_freebusy_table', [$this, 'attendees_freebusy_table']);
+ $this->cal->register_handler('plugin.edit_attendees_notify', [$this, 'edit_attendees_notify']);
+ $this->cal->register_handler('plugin.edit_recurrence_sync', [$this, 'edit_recurrence_sync']);
+ $this->cal->register_handler('plugin.edit_recurring_warning', [$this, 'recurring_event_warning']);
+ $this->cal->register_handler('plugin.event_rsvp_buttons', [$this, 'event_rsvp_buttons']);
+ $this->cal->register_handler('plugin.agenda_options', [$this, 'agenda_options']);
+ $this->cal->register_handler('plugin.events_import_form', [$this, 'events_import_form']);
+ $this->cal->register_handler('plugin.events_export_form', [$this, 'events_export_form']);
+ $this->cal->register_handler('plugin.object_changelog_table', ['libkolab', 'object_changelog_table']);
+ $this->cal->register_handler('plugin.searchform', [$this->rc->output, 'search_form']);
+
+ kolab_attachments_handler::ui();
+ }
+
+ /**
+ * Adds CSS stylesheets to the page header
+ */
+ public function addCSS()
+ {
+ $skin_path = $this->cal->local_skin_path();
- if ($this->rc->task == 'calendar' && (!$this->rc->action || in_array($this->rc->action, array('index', 'print')))) {
- // Include fullCalendar style before skin file for simpler style overriding
- $this->cal->include_stylesheet($skin_path . '/fullcalendar.css');
+ if (
+ $this->rc->task == 'calendar'
+ && (!$this->rc->action || in_array($this->rc->action, ['index', 'print']))
+ ) {
+ // Include fullCalendar style before skin file for simpler style overriding
+ $this->cal->include_stylesheet($skin_path . '/fullcalendar.css');
+ }
+
+ $this->cal->include_stylesheet($skin_path . '/calendar.css');
+
+ if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
+ $this->cal->include_stylesheet($skin_path . '/print.css');
+ }
}
- $this->cal->include_stylesheet($skin_path . '/calendar.css');
+ /**
+ * Adds JS files to the page header
+ */
+ public function addJS()
+ {
+ $this->cal->include_script('lib/js/moment.js');
+ $this->cal->include_script('lib/js/fullcalendar.js');
- if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
- $this->cal->include_stylesheet($skin_path . '/print.css');
- }
- }
-
- /**
- * Adds JS files to the page header
- */
- public function addJS()
- {
- $this->cal->include_script('lib/js/moment.js');
- $this->cal->include_script('lib/js/fullcalendar.js');
-
- if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
- $this->cal->include_script('print.js');
- }
- else {
- $this->rc->output->include_script('treelist.js');
- $this->cal->api->include_script('libkolab/libkolab.js');
- $this->cal->include_script('calendar_ui.js');
- jqueryui::miniColors();
- }
- }
-
- /**
- *
- */
- function calendar_css($attrib = array())
- {
- $categories = $this->cal->driver->list_categories();
- $js_categories = array();
- $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
- $css = "\n";
-
- foreach ((array)$categories as $class => $color) {
- if (!empty($color)) {
- $js_categories[$class] = $color;
-
- $color = ltrim($color, '#');
- $class = 'cat-' . asciiwords(strtolower($class), true);
- $css .= ".$class { color: #$color; }\n";
- }
+ if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
+ $this->cal->include_script('print.js');
+ }
+ else {
+ $this->rc->output->include_script('treelist.js');
+ $this->cal->api->include_script('libkolab/libkolab.js');
+ $this->cal->include_script('calendar_ui.js');
+ jqueryui::miniColors();
+ }
}
- $this->rc->output->set_env('calendar_categories', $js_categories);
+ /**
+ * Add custom style for the calendar UI
+ */
+ function calendar_css($attrib = [])
+ {
+ $categories = $this->cal->driver->list_categories();
+ $calendars = $this->cal->driver->list_calendars();
+ $js_categories = [];
- $calendars = $this->cal->driver->list_calendars();
- foreach ((array)$calendars as $id => $prop) {
- if ($prop['color']) {
- $css .= $this->calendar_css_classes($id, $prop, $mode, $attrib);
- }
+ $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
+ $css = "\n";
+
+ foreach ((array) $categories as $class => $color) {
+ if (!empty($color)) {
+ $js_categories[$class] = $color;
+
+ $color = ltrim($color, '#');
+ $class = 'cat-' . asciiwords(strtolower($class), true);
+ $css .= ".$class { color: #$color; }\n";
+ }
+ }
+
+ $this->rc->output->set_env('calendar_categories', $js_categories);
+
+ foreach ((array) $calendars as $id => $prop) {
+ if (!empty($prop['color'])) {
+ $css .= $this->calendar_css_classes($id, $prop, $mode, $attrib);
+ }
+ }
+
+ return html::tag('style', ['type' => 'text/css'], $css);
}
- return html::tag('style', array('type' => 'text/css'), $css);
- }
+ /**
+ * Calendar folder specific CSS classes
+ */
+ public function calendar_css_classes($id, $prop, $mode, $attrib = [])
+ {
+ $color = $folder_color = $prop['color'];
- /**
- *
- */
- public function calendar_css_classes($id, $prop, $mode, $attrib = array())
- {
- $color = $folder_color = $prop['color'];
+ // replace white with skin-defined color
+ if (!empty($attrib['folder-fallback-color']) && preg_match('/^f+$/i', $folder_color)) {
+ $folder_color = ltrim($attrib['folder-fallback-color'], '#');
+ }
- // replace white with skin-defined color
- if (!empty($attrib['folder-fallback-color']) && preg_match('/^f+$/i', $folder_color)) {
- $folder_color = ltrim($attrib['folder-fallback-color'], '#');
+ $class = 'cal-' . asciiwords($id, true);
+ $css = "li .$class";
+ if (!empty($attrib['folder-class'])) {
+ $css = str_replace('$class', $class, $attrib['folder-class']);
+ }
+ $css .= " { color: #$folder_color; }\n";
+
+ return $css . ".$class .handle { background-color: #$color; }\n";
}
- $class = 'cal-' . asciiwords($id, true);
- $css = str_replace('$class', $class, $attrib['folder-class']) ?: "li .$class";
- $css .= " { color: #$folder_color; }\n";
+ /**
+ * Generate HTML content of the calendars list (or metadata only)
+ */
+ function calendar_list($attrib = [], $js_only = false)
+ {
+ $html = '';
+ $jsenv = [];
+ $tree = true;
+ $calendars = $this->cal->driver->list_calendars(0, $tree);
- return $css . ".$class .handle { background-color: #$color; }\n";
- }
+ // walk folder tree
+ if (is_object($tree)) {
+ $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
- /**
- *
- */
- function calendar_list($attrib = array(), $js_only = false)
- {
- $html = '';
- $jsenv = array();
- $tree = true;
- $calendars = $this->cal->driver->list_calendars(0, $tree);
+ // append birthdays calendar which isn't part of $tree
+ if (!empty($calendars[calendar_driver::BIRTHDAY_CALENDAR_ID])) {
+ $bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID];
+ $calendars = [calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal];
+ }
+ else {
+ $calendars = []; // clear array for flat listing
+ }
+ }
+ else if (isset($attrib['class'])) {
+ // fall-back to flat folder listing
+ $attrib['class'] .= ' flat';
+ }
- // walk folder tree
- if (is_object($tree)) {
- $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
+ foreach ((array) $calendars as $id => $prop) {
+ if (!empty($attrib['activeonly']) && empty($prop['active'])) {
+ continue;
+ }
- // append birthdays calendar which isn't part of $tree
- if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
- $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
- }
- else {
- $calendars = array(); // clear array for flat listing
- }
- }
- else {
- // fall-back to flat folder listing
- $attrib['class'] .= ' flat';
+ $li_content = $this->calendar_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']));
+ $li_attr = [
+ 'id' => 'rcmlical' . $id,
+ 'class' => isset($prop['group']) ? $prop['group'] : null,
+ ];
+
+ $html .= html::tag('li', $li_attr, $li_content);
+ }
+
+ $this->rc->output->set_env('calendars', $jsenv);
+
+ if ($js_only) {
+ return;
+ }
+
+ $this->rc->output->set_env('source', rcube_utils::get_input_value('source', rcube_utils::INPUT_GET));
+ $this->rc->output->add_gui_object('calendarslist', !empty($attrib['id']) ? $attrib['id'] : 'rccalendarlist');
+
+ return html::tag('ul', $attrib, $html, html::$common_attrib);
}
- foreach ((array)$calendars as $id => $prop) {
- if ($attrib['activeonly'] && !$prop['active'])
- continue;
+ /**
+ * Return html for a structured list for the folder tree
+ */
+ public function list_tree_html($node, $data, &$jsenv, $attrib)
+ {
+ $out = '';
+ foreach ($node->children as $folder) {
+ $id = $folder->id;
+ $prop = $data[$id];
+ $is_collapsed = false; // TODO: determine this somehow?
- $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
- $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
- );
+ $content = $this->calendar_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']));
+
+ if (!empty($folder->children)) {
+ $content .= html::tag('ul', ['style' => $is_collapsed ? "display:none;" : null],
+ $this->list_tree_html($folder, $data, $jsenv, $attrib)
+ );
+ }
+
+ if (strlen($content)) {
+ $li_attr = [
+ 'id' => 'rcmlical' . rcube_utils::html_identifier($id),
+ 'class' => $prop['group'] . (!empty($prop['virtual']) ? ' virtual' : ''),
+ ];
+ $out .= html::tag('li', $li_attr, $content);
+ }
+ }
+
+ return $out;
}
- $this->rc->output->set_env('calendars', $jsenv);
+ /**
+ * Helper method to build a calendar list item (HTML content and js data)
+ */
+ public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
+ {
+ // enrich calendar properties with settings from the driver
+ if (empty($prop['virtual'])) {
+ unset($prop['user_id']);
- if ($js_only) {
- return;
+ $prop['alarms'] = $this->cal->driver->alarms;
+ $prop['attendees'] = $this->cal->driver->attendees;
+ $prop['freebusy'] = $this->cal->driver->freebusy;
+ $prop['attachments'] = $this->cal->driver->attachments;
+ $prop['undelete'] = $this->cal->driver->undelete;
+ $prop['feedurl'] = $this->cal->get_url([
+ '_cal' => $this->cal->ical_feed_hash($id) . '.ics',
+ 'action' => 'feed'
+ ]
+ );
+
+ $jsenv[$id] = $prop;
+ }
+
+ if (!empty($prop['title'])) {
+ $title = $prop['title'];
+ }
+ else if ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25) {
+ $title = html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET);
+ }
+ else {
+ $title = '';
+ }
+
+ $classes = ['calendar', 'cal-' . asciiwords($id, true)];
+
+ if (!empty($prop['virtual'])) {
+ $classes[] = 'virtual';
+ }
+ else if (empty($prop['editable'])) {
+ $classes[] = 'readonly';
+ }
+ if (!empty($prop['subscribed'])) {
+ $classes[] = 'subscribed';
+
+ if ($prop['subscribed'] === 2) {
+ $classes[] = 'partial';
+ }
+ }
+ if (!empty($prop['class'])) {
+ $classes[] = $prop['class'];
+ }
+
+ $content = '';
+
+ if (!$activeonly || !empty($prop['active'])) {
+ $label_id = 'cl:' . $id;
+ $content = html::a(
+ ['class' => 'calname', 'id' => $label_id, 'title' => $title, 'href' => '#'],
+ rcube::Q(!empty($prop['editname']) ? $prop['editname'] : $prop['listname'])
+ );
+
+ if (empty($prop['virtual'])) {
+ $color = !empty($prop['color']) ? $prop['color'] : 'f00';
+ $actions = '';
+
+ if (!EMPTY($prop['removable'])) {
+ $actions .= html::a([
+ 'href' => '#',
+ 'class' => 'remove',
+ 'title' => $this->cal->gettext('removelist')
+ ], ' '
+ );
+ }
+
+ $actions .= html::a([
+ 'href' => '#',
+ 'class' => 'quickview',
+ 'title' => $this->cal->gettext('quickview'),
+ 'role' => 'checkbox',
+ 'aria-checked' => 'false'
+ ], ''
+ );
+
+ if (!empty($prop['subscribed'])) {
+ $actions .= html::a([
+ 'href' => '#',
+ 'class' => 'subscribed',
+ 'title' => $this->cal->gettext('calendarsubscribe'),
+ 'role' => 'checkbox',
+ 'aria-checked' => !empty($prop['subscribed']) ? 'true' : 'false'
+ ], ' '
+ );
+ }
+
+ $content .= html::tag('input', [
+ 'type' => 'checkbox',
+ 'name' => '_cal[]',
+ 'value' => $id,
+ 'checked' => !empty($prop['active']),
+ 'aria-labelledby' => $label_id
+ ])
+ . html::span('actions', $actions)
+ . html::span(['class' => 'handle', 'style' => "background-color: #$color"], ' ');
+ }
+
+ $content = html::div(join(' ', $classes), $content);
+ }
+
+ return $content;
}
- $this->rc->output->set_env('source', rcube_utils::get_input_value('source', rcube_utils::INPUT_GET));
- $this->rc->output->add_gui_object('calendarslist', $attrib['id'] ?: 'unknown');
+ /**
+ * Render a HTML for agenda options form
+ */
+ function agenda_options($attrib = [])
+ {
+ $attrib += ['id' => 'agendaoptions'];
+ $attrib['style'] = 'display:none';
- return html::tag('ul', $attrib, $html, html::$common_attrib);
- }
+ $select_range = new html_select(['name' => 'listrange', 'id' => 'agenda-listrange', 'class' => 'form-control custom-select']);
+ $select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), '');
- /**
- * Return html for a structured list for the folder tree
- */
- public function list_tree_html($node, $data, &$jsenv, $attrib)
- {
- $out = '';
- foreach ($node->children as $folder) {
- $id = $folder->id;
- $prop = $data[$id];
- $is_collapsed = false; // TODO: determine this somehow?
+ foreach ([2,5,7,14,30,60,90,180,365] as $days) {
+ $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
+ }
- $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+ $html = html::span('input-group',
+ html::label(['for' => 'agenda-listrange', 'class' => 'input-group-prepend'],
+ html::span('input-group-text', $this->cal->gettext('listrange'))
+ )
+ . $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']))
+ );
- if (!empty($folder->children)) {
- $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
- $this->list_tree_html($folder, $data, $jsenv, $attrib));
- }
-
- if (strlen($content)) {
- $out .= html::tag('li', array(
- 'id' => 'rcmlical' . rcube_utils::html_identifier($id),
- 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
- ),
- $content);
- }
+ return html::div($attrib, $html);
}
- return $out;
- }
+ /**
+ * Render a HTML select box for calendar selection
+ */
+ function calendar_select($attrib = [])
+ {
+ $attrib['name'] = 'calendar';
+ $attrib['is_escaped'] = true;
- /**
- * Helper method to build a calendar list item (HTML content and js data)
- */
- public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
- {
- // enrich calendar properties with settings from the driver
- if (!$prop['virtual']) {
- unset($prop['user_id']);
- $prop['alarms'] = $this->cal->driver->alarms;
- $prop['attendees'] = $this->cal->driver->attendees;
- $prop['freebusy'] = $this->cal->driver->freebusy;
- $prop['attachments'] = $this->cal->driver->attachments;
- $prop['undelete'] = $this->cal->driver->undelete;
- $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+ $select = new html_select($attrib);
- $jsenv[$id] = $prop;
+ foreach ((array) $this->cal->driver->list_calendars() as $id => $prop) {
+ if (
+ !empty($prop['editable'])
+ || (!empty($prop['rights']) && strpos($prop['rights'], 'i') !== false)
+ ) {
+ $select->add($prop['name'], $id);
+ }
+ }
+
+ return $select->show(null);
}
- $classes = array('calendar', 'cal-' . asciiwords($id, true));
- $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
- html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : '');
+ /**
+ * Render a HTML select box for user identity selection
+ */
+ function identity_select($attrib = [])
+ {
+ $attrib['name'] = 'identity';
- if ($prop['virtual'])
- $classes[] = 'virtual';
- else if (!$prop['editable'])
- $classes[] = 'readonly';
- if ($prop['subscribed'])
- $classes[] = 'subscribed';
- if ($prop['subscribed'] === 2)
- $classes[] = 'partial';
- if ($prop['class'])
- $classes[] = $prop['class'];
+ $select = new html_select($attrib);
+ $identities = $this->rc->user->list_emails();
- $content = '';
- if (!$activeonly || $prop['active']) {
- $label_id = 'cl:' . $id;
- $content = html::div(join(' ', $classes),
- html::a(array('class' => 'calname', 'id' => $label_id, 'title' => $title, 'href' => '#'), rcube::Q($prop['editname'] ?: $prop['listname']))
- . ($prop['virtual'] ? '' :
- html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) .
- html::span('actions',
- ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->cal->gettext('removelist')), ' ') : '') .
- html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->cal->gettext('quickview'), 'role' => 'checkbox', 'aria-checked' => 'false'), '') .
- (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
- ) .
- html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
- )
- );
+ foreach ($identities as $ident) {
+ $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
+ }
+
+ return $select->show(null);
}
- return $content;
- }
+ /**
+ * Render a HTML select box to select an event category
+ */
+ function category_select($attrib = [])
+ {
+ $attrib['name'] = 'categories';
- /**
- * Render a HTML for agenda options form
- */
- function agenda_options($attrib = array())
- {
- $attrib += array('id' => 'agendaoptions');
- $attrib['style'] .= 'display:none';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ foreach (array_keys((array) $this->cal->driver->list_categories()) as $cat) {
+ $select->add($cat, $cat);
+ }
- $select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange', 'class' => 'form-control custom-select'));
- $select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), $days);
- foreach (array(2,5,7,14,30,60,90,180,365) as $days)
- $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
-
- $html = html::span('input-group',
- html::label(array('for' => 'agenda-listrange', 'class' => 'input-group-prepend'),
- html::span('input-group-text', $this->cal->gettext('listrange')))
- . $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']))
- );
-
- return html::div($attrib, $html);
- }
-
- /**
- * Render a HTML select box for calendar selection
- */
- function calendar_select($attrib = array())
- {
- $attrib['name'] = 'calendar';
- $attrib['is_escaped'] = true;
- $select = new html_select($attrib);
-
- foreach ((array)$this->cal->driver->list_calendars() as $id => $prop) {
- if ($prop['editable'] || strpos($prop['rights'], 'i') !== false)
- $select->add($prop['name'], $id);
+ return $select->show(null);
}
- return $select->show(null);
- }
+ /**
+ * Render a HTML select box for status property
+ */
+ function status_select($attrib = [])
+ {
+ $attrib['name'] = 'status';
- /**
- * Render a HTML select box for user identity selection
- */
- function identity_select($attrib = array())
- {
- $attrib['name'] = 'identity';
- $select = new html_select($attrib);
- $identities = $this->rc->user->list_emails();
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
+ $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
+ $select->add($this->cal->gettext('status-tentative'), 'TENTATIVE');
- foreach ($identities as $ident) {
- $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
+ return $select->show(null);
}
- return $select->show(null);
- }
+ /**
+ * Render a HTML select box for free/busy/out-of-office property
+ */
+ function freebusy_select($attrib = [])
+ {
+ $attrib['name'] = 'freebusy';
- /**
- * Render a HTML select box to select an event category
- */
- function category_select($attrib = array())
- {
- $attrib['name'] = 'categories';
- $select = new html_select($attrib);
- $select->add('---', '');
- foreach (array_keys((array)$this->cal->driver->list_categories()) as $cat) {
- $select->add($cat, $cat);
+ $select = new html_select($attrib);
+ $select->add($this->cal->gettext('free'), 'free');
+ $select->add($this->cal->gettext('busy'), 'busy');
+ // out-of-office is not supported by libkolabxml (#3220)
+ // $select->add($this->cal->gettext('outofoffice'), 'outofoffice');
+ $select->add($this->cal->gettext('tentative'), 'tentative');
+
+ return $select->show(null);
}
- return $select->show(null);
- }
+ /**
+ * Render a HTML select for event priorities
+ */
+ function priority_select($attrib = [])
+ {
+ $attrib['name'] = 'priority';
- /**
- * Render a HTML select box for status property
- */
- function status_select($attrib = array())
- {
- $attrib['name'] = 'status';
- $select = new html_select($attrib);
- $select->add('---', '');
- $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
- $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
- $select->add($this->cal->gettext('status-tentative'), 'TENTATIVE');
- return $select->show(null);
- }
+ $select = new html_select($attrib);
+ $select->add('---', '0');
+ $select->add('1 ' . $this->cal->gettext('highest'), '1');
+ $select->add('2 ' . $this->cal->gettext('high'), '2');
+ $select->add('3 ', '3');
+ $select->add('4 ', '4');
+ $select->add('5 ' . $this->cal->gettext('normal'), '5');
+ $select->add('6 ', '6');
+ $select->add('7 ', '7');
+ $select->add('8 ' . $this->cal->gettext('low'), '8');
+ $select->add('9 ' . $this->cal->gettext('lowest'), '9');
- /**
- * Render a HTML select box for free/busy/out-of-office property
- */
- function freebusy_select($attrib = array())
- {
- $attrib['name'] = 'freebusy';
- $select = new html_select($attrib);
- $select->add($this->cal->gettext('free'), 'free');
- $select->add($this->cal->gettext('busy'), 'busy');
- // out-of-office is not supported by libkolabxml (#3220)
- // $select->add($this->cal->gettext('outofoffice'), 'outofoffice');
- $select->add($this->cal->gettext('tentative'), 'tentative');
- return $select->show(null);
- }
-
- /**
- * Render a HTML select for event priorities
- */
- function priority_select($attrib = array())
- {
- $attrib['name'] = 'priority';
- $select = new html_select($attrib);
- $select->add('---', '0');
- $select->add('1 '.$this->cal->gettext('highest'), '1');
- $select->add('2 '.$this->cal->gettext('high'), '2');
- $select->add('3 ', '3');
- $select->add('4 ', '4');
- $select->add('5 '.$this->cal->gettext('normal'), '5');
- $select->add('6 ', '6');
- $select->add('7 ', '7');
- $select->add('8 '.$this->cal->gettext('low'), '8');
- $select->add('9 '.$this->cal->gettext('lowest'), '9');
- return $select->show(null);
- }
-
- /**
- * Render HTML input for sensitivity selection
- */
- function sensitivity_select($attrib = array())
- {
- $attrib['name'] = 'sensitivity';
- $select = new html_select($attrib);
- $select->add($this->cal->gettext('public'), 'public');
- $select->add($this->cal->gettext('private'), 'private');
- $select->add($this->cal->gettext('confidential'), 'confidential');
- return $select->show(null);
- }
-
- /**
- * Render HTML form for alarm configuration
- */
- function alarm_select($attrib = array())
- {
- return $this->cal->lib->alarm_select($attrib, $this->cal->driver->alarm_types, $this->cal->driver->alarm_absolute);
- }
-
- /**
- * Render HTML for attendee notification warning
- */
- function edit_attendees_notify($attrib = array())
- {
- $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1, 'class' => 'pretty-checkbox'));
- return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
- }
-
- /**
- * Render HTML for recurrence option to align start date with the recurrence rule
- */
- function edit_recurrence_sync($attrib = array())
- {
- $checkbox = new html_checkbox(array('name' => '_start_sync', 'value' => 1, 'class' => 'pretty-checkbox'));
- return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('eventstartsync')));
- }
-
- /**
- * Generate the form for recurrence settings
- */
- function recurring_event_warning($attrib = array())
- {
- $attrib['id'] = 'edit-recurring-warning';
-
- $radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode'));
- $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' .
- html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' .
- html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' .
- html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew'));
-
- return html::div($attrib, html::div('message', $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form));
- }
-
- /**
- * Form for uploading and importing events
- */
- function events_import_form($attrib = array())
- {
- if (!$attrib['id'])
- $attrib['id'] = 'rcmImportForm';
-
- // Get max filesize, enable upload progress bar
- $max_filesize = $this->rc->upload_init();
-
- $accept = '.ics, text/calendar, text/x-vcalendar, application/ics';
- if (class_exists('ZipArchive', false)) {
- $accept .= ', .zip, application/zip';
+ return $select->show(null);
}
- $input = new html_inputfield(array(
- 'id' => 'importfile',
- 'type' => 'file',
- 'name' => '_data',
- 'size' => $attrib['uploadfieldsize'],
- 'accept' => $accept
- ));
+ /**
+ * Render HTML input for sensitivity selection
+ */
+ function sensitivity_select($attrib = [])
+ {
+ $attrib['name'] = 'sensitivity';
- $select = new html_select(array('name' => '_range', 'id' => 'event-import-range'));
- $select->add(array(
- $this->cal->gettext('onemonthback'),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
- $this->cal->gettext('all'),
- ),
- array('1','2','3','6','12',0));
+ $select = new html_select($attrib);
+ $select->add($this->cal->gettext('public'), 'public');
+ $select->add($this->cal->gettext('private'), 'private');
+ $select->add($this->cal->gettext('confidential'), 'confidential');
- $html = html::div('form-section form-group row',
- html::label(array('class' => 'col-sm-4 col-form-label', 'for' => 'importfile'), rcube::Q($this->rc->gettext('importfromfile')))
- . html::div('col-sm-8', $input->show()
- . html::div('hint', $this->rc->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))))
- );
-
- $html .= html::div('form-section form-group row',
- html::label(array('for' => 'event-import-calendar', 'class' => 'col-form-label col-sm-4'), $this->cal->gettext('calendar'))
- . html::div('col-sm-8', $this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar')))
- );
-
- $html .= html::div('form-section form-group row',
- html::label(array('for' => 'event-import-range', 'class' => 'col-form-label col-sm-4'), $this->cal->gettext('importrange'))
- . html::div('col-sm-8', $select->show(1))
- );
-
- $this->rc->output->add_gui_object('importform', $attrib['id']);
- $this->rc->output->add_label('import');
-
- return html::tag('p', null, $this->cal->gettext('importtext'))
- . html::tag('form', array(
- 'action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')),
- 'method' => 'post',
- 'enctype' => 'multipart/form-data',
- 'id' => $attrib['id']
- ), $html);
- }
-
- /**
- * Form to select options for exporting events
- */
- function events_export_form($attrib = array())
- {
- if (!$attrib['id'])
- $attrib['id'] = 'rcmExportForm';
-
- $html = html::div('form-section form-group row',
- html::label(array('for' => 'event-export-calendar', 'class' => 'col-sm-4 col-form-label'), $this->cal->gettext('calendar'))
- . html::div('col-sm-8', $this->calendar_select(array('name' => 'calendar', 'id' => 'event-export-calendar', 'class' => 'form-control custom-select'))));
-
- $select = new html_select(array('name' => 'range', 'id' => 'event-export-range', 'class' => 'form-control custom-select rounded-right'));
- $select->add(array(
- $this->cal->gettext('all'),
- $this->cal->gettext('onemonthback'),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
- $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
- $this->cal->gettext('customdate'),
- ),
- array(0,'1','2','3','6','12','custom'));
-
- $startdate = new html_inputfield(array('name' => 'start', 'size' => 11, 'id' => 'event-export-startdate', 'style' => 'display:none'));
-
- $html .= html::div('form-section form-group row',
- html::label(array('for' => 'event-export-range', 'class' => 'col-sm-4 col-form-label'), $this->cal->gettext('exportrange'))
- . html::div('col-sm-8 input-group', $select->show(0) . $startdate->show()));
-
- $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'event-export-attachments', 'value' => 1, 'class' => 'form-check-input pretty-checkbox'));
- $html .= html::div('form-section form-check row',
- html::label(array('for' => 'event-export-attachments', 'class' => 'col-sm-4 col-form-label'), $this->cal->gettext('exportattachments'))
- . html::div('col-sm-8', $checkbox->show(1)));
-
- $this->rc->output->add_gui_object('exportform', $attrib['id']);
-
- return html::tag('form', $attrib + array(
- 'action' => $this->rc->url(array('task' => 'calendar', 'action' => 'export_events')),
- 'method' => "post",
- 'id' => $attrib['id']
- ),
- $html
- );
- }
-
- /**
- * Handler for calendar form template.
- * The form content could be overriden by the driver
- */
- function calendar_editform($action, $calendar = array())
- {
- $this->action = $action;
- $this->calendar = $calendar;
-
- // load miniColors js/css files
- jqueryui::miniColors();
-
- $this->rc->output->set_env('pagetitle', $this->cal->gettext('calendarprops'));
- $this->rc->output->add_handler('folderform', array($this, 'calendarform'));
- $this->rc->output->send('libkolab.folderform');
- }
-
- /**
- * Handler for calendar form template.
- * The form content could be overriden by the driver
- */
- function calendarform($attrib)
- {
- // compose default calendar form fields
- $input_name = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20));
- $input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 7, 'class' => 'colors'));
-
- $formfields = array(
- 'name' => array(
- 'label' => $this->cal->gettext('name'),
- 'value' => $input_name->show($calendar['name']),
- 'id' => 'calendar-name',
- ),
- 'color' => array(
- 'label' => $this->cal->gettext('color'),
- 'value' => $input_color->show($calendar['color']),
- 'id' => 'calendar-color',
- ),
- );
-
- if ($this->cal->driver->alarms) {
- $checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1));
- $formfields['showalarms'] = array(
- 'label' => $this->cal->gettext('showalarms'),
- 'value' => $checkbox->show($this->calendar['showalarms'] ? 1 :0),
- 'id' => 'calendar-showalarms',
- );
+ return $select->show(null);
}
- // allow driver to extend or replace the form content
- return html::tag('form', $attrib + array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'),
- $this->cal->driver->calendar_form($this->action, $this->calendar, $formfields)
- );
- }
-
- /**
- *
- */
- function attendees_list($attrib = array())
- {
- // add "noreply" checkbox to attendees table only
- $invitations = strpos($attrib['id'], 'attend') !== false;
-
- $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
- $table = new html_table(array('cols' => 5 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
-
- $table->add_header('role', $this->cal->gettext('role'));
- $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee'));
- $table->add_header('availability', $this->cal->gettext('availability'));
- $table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
- if ($invitations) {
- $table->add_header(array('class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')),
- $invite->show(1) . html::label('edit-attendees-invite', html::span('inner', $this->cal->gettext('sendinvitations'))));
- }
- $table->add_header('options', '');
-
- // hide invite column if disabled by config
- $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']);
- if ($invitations && !($itip_notify & 2)) {
- $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
- $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css));
+ /**
+ * Render HTML form for alarm configuration
+ */
+ function alarm_select($attrib = [])
+ {
+ return $this->cal->lib->alarm_select($attrib, $this->cal->driver->alarm_types, $this->cal->driver->alarm_absolute);
}
- return $table->show($attrib);
- }
-
- /**
- *
- */
- function attendees_form($attrib = array())
- {
- $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'class' => 'form-control'));
- $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment', 'class' => 'form-control',
- 'rows' => 4, 'cols' => 55, 'title' => $this->cal->gettext('itipcommenttitle')));
-
- return html::div($attrib,
- html::div('form-searchbar', $input->show() . " " .
- html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " .
- html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) .
- html::p('attendees-commentbox', html::label('edit-attendees-comment', $this->cal->gettext('itipcomment')) . $textarea->show())
- );
- }
-
- /**
- *
- */
- function resources_form($attrib = array())
- {
- $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'class' => 'form-control'));
-
- return html::div($attrib,
- html::div('form-searchbar', $input->show() . " " .
- html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " .
- html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...')))
- );
- }
-
- /**
- *
- */
- function resources_list($attrib = array())
- {
- $attrib += array('id' => 'calendar-resources-list');
-
- $this->rc->output->add_gui_object('resourceslist', $attrib['id']);
-
- return html::tag('ul', $attrib, '', html::$common_attrib);
- }
-
- /**
- *
- */
- public function resource_info($attrib = array())
- {
- $attrib += array('id' => 'calendar-resources-info');
-
- $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
- $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
-
- // copy address book labels for owner details to client
- $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
-
- $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border');
-
- return html::tag('table', $attrib,
- html::tag('tbody', null, ''), $table_attrib) .
-
- html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib,
- html::tag('thead', null,
- html::tag('tr', null,
- html::tag('td', array('colspan' => 2), rcube::Q($this->cal->gettext('resourceowner')))
- )
- ) .
- html::tag('tbody', null, ''),
- $table_attrib);
- }
-
- /**
- *
- */
- public function resource_calendar($attrib = array())
- {
- $attrib += array('id' => 'calendar-resources-calendar');
-
- $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
-
- return html::div($attrib, '');
- }
-
- /**
- * GUI object 'searchform' for the resource finder dialog
- *
- * @param array Named parameters
- * @return string HTML code for the gui object
- */
- function resources_search_form($attrib)
- {
- $attrib += array(
- 'command' => 'search-resource',
- 'reset-command' => 'reset-resource-search',
- 'id' => 'rcmcalresqsearchbox',
- 'autocomplete' => 'off',
- 'form-name' => 'rcmcalresoursqsearchform',
- 'gui-object' => 'resourcesearchform',
- );
-
- // add form tag around text field
- return $this->rc->output->search_form($attrib);
- }
-
- /**
- *
- */
- function attendees_freebusy_table($attrib = array())
- {
- $table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
- $table->add('attendees',
- html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) .
- html::div('timesheader', ' ') .
- html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
- );
- $table->add('times',
- html::div('scroll',
- html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) .
- html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), ' ')
- )
- );
-
- return $table->show($attrib);
- }
-
- /**
- *
- */
- function event_invitebox($attrib = array())
- {
- if ($this->cal->event) {
- return html::div($attrib,
- $this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation')) .
- $this->cal->invitestatus
- );
+ /**
+ * Render HTML for attendee notification warning
+ */
+ function edit_attendees_notify($attrib = [])
+ {
+ $checkbox = new html_checkbox(['name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1, 'class' => 'pretty-checkbox']);
+ return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
}
-
- return '';
- }
- function event_rsvp_buttons($attrib = array())
- {
- $actions = array('accepted','tentative','declined');
- if ($attrib['delegate'] !== 'false')
- $actions[] = 'delegated';
+ /**
+ * Render HTML for recurrence option to align start date with the recurrence rule
+ */
+ function edit_recurrence_sync($attrib = [])
+ {
+ $checkbox = new html_checkbox(['name' => '_start_sync', 'value' => 1, 'class' => 'pretty-checkbox']);
+ return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('eventstartsync')));
+ }
- return $this->cal->itip->itip_rsvp_buttons($attrib, $actions);
- }
+ /**
+ * Generate the form for recurrence settings
+ */
+ function recurring_event_warning($attrib = [])
+ {
+ $attrib['id'] = 'edit-recurring-warning';
+ $radio = new html_radiobutton(['name' => '_savemode', 'class' => 'edit-recurring-savemode']);
+
+ $form = html::label(null, $radio->show('', ['value' => 'current']) . $this->cal->gettext('currentevent')) . ' '
+ . html::label(null, $radio->show('', ['value' => 'future']) . $this->cal->gettext('futurevents')) . ' '
+ . html::label(null, $radio->show('all', ['value' => 'all']) . $this->cal->gettext('allevents')) . ' '
+ . html::label(null, $radio->show('', ['value' => 'new']) . $this->cal->gettext('saveasnew'));
+
+ return html::div($attrib,
+ html::div('message', $this->cal->gettext('changerecurringeventwarning'))
+ . html::div('savemode', $form)
+ );
+ }
+
+ /**
+ * Form for uploading and importing events
+ */
+ function events_import_form($attrib = [])
+ {
+ if (empty($attrib['id'])) {
+ $attrib['id'] = 'rcmImportForm';
+ }
+
+ // Get max filesize, enable upload progress bar
+ $max_filesize = $this->rc->upload_init();
+
+ $accept = '.ics, text/calendar, text/x-vcalendar, application/ics';
+ if (class_exists('ZipArchive', false)) {
+ $accept .= ', .zip, application/zip';
+ }
+
+ $input = new html_inputfield([
+ 'id' => 'importfile',
+ 'type' => 'file',
+ 'name' => '_data',
+ 'size' => !empty($attrib['uploadfieldsize']) ? $attrib['uploadfieldsize'] : null,
+ 'accept' => $accept
+ ]);
+
+ $select = new html_select(['name' => '_range', 'id' => 'event-import-range']);
+ $select->add([
+ $this->cal->gettext('onemonthback'),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>2]]),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>3]]),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>6]]),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>12]]),
+ $this->cal->gettext('all'),
+ ],
+ ['1','2','3','6','12',0]
+ );
+
+ $html = html::div('form-section form-group row',
+ html::label(['class' => 'col-sm-4 col-form-label', 'for' => 'importfile'],
+ rcube::Q($this->rc->gettext('importfromfile'))
+ )
+ . html::div('col-sm-8', $input->show()
+ . html::div('hint', $this->rc->gettext(['name' => 'maxuploadsize', 'vars' => ['size' => $max_filesize]]))
+ )
+ );
+
+ $html .= html::div('form-section form-group row',
+ html::label(['for' => 'event-import-calendar', 'class' => 'col-form-label col-sm-4'],
+ $this->cal->gettext('calendar')
+ )
+ . html::div('col-sm-8', $this->calendar_select(['name' => 'calendar', 'id' => 'event-import-calendar']))
+ );
+
+ $html .= html::div('form-section form-group row',
+ html::label(['for' => 'event-import-range', 'class' => 'col-form-label col-sm-4'],
+ $this->cal->gettext('importrange')
+ )
+ . html::div('col-sm-8', $select->show(1))
+ );
+
+ $this->rc->output->add_gui_object('importform', $attrib['id']);
+ $this->rc->output->add_label('import');
+
+ return html::tag('p', null, $this->cal->gettext('importtext'))
+ . html::tag('form', [
+ 'action' => $this->rc->url(['task' => 'calendar', 'action' => 'import_events']),
+ 'method' => 'post',
+ 'enctype' => 'multipart/form-data',
+ 'id' => $attrib['id']
+ ], $html
+ );
+ }
+
+ /**
+ * Form to select options for exporting events
+ */
+ function events_export_form($attrib = [])
+ {
+ if (empty($attrib['id'])) {
+ $attrib['id'] = 'rcmExportForm';
+ }
+
+ $html = html::div('form-section form-group row',
+ html::label(['for' => 'event-export-calendar', 'class' => 'col-sm-4 col-form-label'],
+ $this->cal->gettext('calendar')
+ )
+ . html::div('col-sm-8', $this->calendar_select(['name' => 'calendar', 'id' => 'event-export-calendar', 'class' => 'form-control custom-select']))
+ );
+
+ $select = new html_select([
+ 'name' => 'range',
+ 'id' => 'event-export-range',
+ 'class' => 'form-control custom-select rounded-right'
+ ]);
+
+ $select->add([
+ $this->cal->gettext('all'),
+ $this->cal->gettext('onemonthback'),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 2]]),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 3]]),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 6]]),
+ $this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 12]]),
+ $this->cal->gettext('customdate'),
+ ],
+ [0,'1','2','3','6','12','custom']
+ );
+
+ $startdate = new html_inputfield([
+ 'name' => 'start',
+ 'size' => 11,
+ 'id' => 'event-export-startdate',
+ 'style' => 'display:none'
+ ]);
+
+ $html .= html::div('form-section form-group row',
+ html::label(['for' => 'event-export-range', 'class' => 'col-sm-4 col-form-label'],
+ $this->cal->gettext('exportrange')
+ )
+ . html::div('col-sm-8 input-group', $select->show(0) . $startdate->show())
+ );
+
+ $checkbox = new html_checkbox([
+ 'name' => 'attachments',
+ 'id' => 'event-export-attachments',
+ 'value' => 1,
+ 'class' => 'form-check-input pretty-checkbox'
+ ]);
+
+ $html .= html::div('form-section form-check row',
+ html::label(['for' => 'event-export-attachments', 'class' => 'col-sm-4 col-form-label'],
+ $this->cal->gettext('exportattachments')
+ )
+ . html::div('col-sm-8', $checkbox->show(1))
+ );
+
+ $this->rc->output->add_gui_object('exportform', $attrib['id']);
+
+ return html::tag('form', $attrib + [
+ 'action' => $this->rc->url(['task' => 'calendar', 'action' => 'export_events']),
+ 'method' => 'post',
+ 'id' => $attrib['id']
+ ],
+ $html
+ );
+ }
+
+ /**
+ * Handler for calendar form template.
+ * The form content could be overriden by the driver
+ */
+ function calendar_editform($action, $calendar = [])
+ {
+ $this->action = $action;
+ $this->calendar = $calendar;
+
+ // load miniColors js/css files
+ jqueryui::miniColors();
+
+ $this->rc->output->set_env('pagetitle', $this->cal->gettext('calendarprops'));
+ $this->rc->output->add_handler('folderform', [$this, 'calendarform']);
+ $this->rc->output->send('libkolab.folderform');
+ }
+
+ /**
+ * Handler for calendar form template.
+ * The form content could be overriden by the driver
+ */
+ function calendarform($attrib)
+ {
+ // compose default calendar form fields
+ $input_name = new html_inputfield(['name' => 'name', 'id' => 'calendar-name', 'size' => 20]);
+ $input_color = new html_inputfield(['name' => 'color', 'id' => 'calendar-color', 'size' => 7, 'class' => 'colors']);
+
+ $formfields = [
+ 'name' => [
+ 'label' => $this->cal->gettext('name'),
+ 'value' => $input_name->show(isset($this->calendar['name']) ? $this->calendar['name'] : ''),
+ 'id' => 'calendar-name',
+ ],
+ 'color' => [
+ 'label' => $this->cal->gettext('color'),
+ 'value' => $input_color->show(isset($this->calendar['color']) ? $this->calendar['color'] : ''),
+ 'id' => 'calendar-color',
+ ],
+ ];
+
+ if (!empty($this->cal->driver->alarms)) {
+ $checkbox = new html_checkbox(['name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1]);
+
+ $formfields['showalarms'] = [
+ 'label' => $this->cal->gettext('showalarms'),
+ 'value' => $checkbox->show(!empty($this->calendar['showalarms']) ? 1 : 0),
+ 'id' => 'calendar-showalarms',
+ ];
+ }
+
+ // allow driver to extend or replace the form content
+ return html::tag('form', $attrib + ['action' => '#', 'method' => 'get', 'id' => 'calendarpropform'],
+ $this->cal->driver->calendar_form($this->action, $this->calendar, $formfields)
+ );
+ }
+
+ /**
+ * Render HTML for attendees table
+ */
+ function attendees_list($attrib = [])
+ {
+ // add "noreply" checkbox to attendees table only
+ $invitations = strpos($attrib['id'], 'attend') !== false;
+
+ $invite = new html_checkbox(['value' => 1, 'id' => 'edit-attendees-invite']);
+ $table = new html_table(['cols' => 5 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable']);
+
+ $table->add_header('role', $this->cal->gettext('role'));
+ $table->add_header('name', $this->cal->gettext(!empty($attrib['coltitle']) ? $attrib['coltitle'] : 'attendee'));
+ $table->add_header('availability', $this->cal->gettext('availability'));
+ $table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
+
+ if ($invitations) {
+ $table->add_header(['class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')],
+ $invite->show(1)
+ . html::label('edit-attendees-invite', html::span('inner', $this->cal->gettext('sendinvitations')))
+ );
+ }
+
+ $table->add_header('options', '');
+
+ // hide invite column if disabled by config
+ $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']);
+ if ($invitations && !($itip_notify & 2)) {
+ $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
+ $this->rc->output->add_footer(html::tag('style', ['type' => 'text/css'], $css));
+ }
+
+ return $table->show($attrib);
+ }
+
+ /**
+ * Render HTML for attendees adding form
+ */
+ function attendees_form($attrib = [])
+ {
+ $input = new html_inputfield([
+ 'name' => 'participant',
+ 'id' => 'edit-attendee-name',
+ 'class' => 'form-control'
+ ]);
+ $textarea = new html_textarea([
+ 'name' => 'comment',
+ 'id' => 'edit-attendees-comment',
+ 'class' => 'form-control',
+ 'rows' => 4,
+ 'cols' => 55,
+ 'title' => $this->cal->gettext('itipcommenttitle')
+ ]);
+
+ return html::div($attrib,
+ html::div('form-searchbar',
+ $input->show()
+ . ' ' .
+ html::tag('input', [
+ 'type' => 'button',
+ 'class' => 'button',
+ 'id' => 'edit-attendee-add',
+ 'value' => $this->cal->gettext('addattendee')
+ ])
+ . ' ' .
+ html::tag('input', [
+ 'type' => 'button',
+ 'class' => 'button',
+ 'id' => 'edit-attendee-schedule',
+ 'value' => $this->cal->gettext('scheduletime') . '...'
+ ])
+ )
+ . html::p('attendees-commentbox', html::label('edit-attendees-comment', $this->cal->gettext('itipcomment')) . $textarea->show())
+ );
+ }
+
+ /**
+ * Render HTML for resources adding form
+ */
+ function resources_form($attrib = [])
+ {
+ $input = new html_inputfield(['name' => 'resource', 'id' => 'edit-resource-name', 'class' => 'form-control']);
+
+ return html::div($attrib,
+ html::div('form-searchbar',
+ $input->show()
+ . ' ' .
+ html::tag('input', [
+ 'type' => 'button',
+ 'class' => 'button',
+ 'id' => 'edit-resource-add',
+ 'value' => $this->cal->gettext('addresource')
+ ])
+ . ' ' .
+ html::tag('input', [
+ 'type' => 'button',
+ 'class' => 'button',
+ 'id' => 'edit-resource-find',
+ 'value' => $this->cal->gettext('findresources') . '...'
+ ])
+ )
+ );
+ }
+
+ /**
+ * Render HTML for resources list
+ */
+ function resources_list($attrib = [])
+ {
+ $attrib += ['id' => 'calendar-resources-list'];
+
+ $this->rc->output->add_gui_object('resourceslist', $attrib['id']);
+
+ return html::tag('ul', $attrib, '', html::$common_attrib);
+ }
+
+ /**
+ *
+ */
+ public function resource_info($attrib = [])
+ {
+ $attrib += ['id' => 'calendar-resources-info'];
+
+ $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
+ $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
+
+ // copy address book labels for owner details to client
+ $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
+
+ $table_attrib = ['id','class','style','width','summary','cellpadding','cellspacing','border'];
+
+ return html::tag('table', $attrib, html::tag('tbody', null, ''), $table_attrib)
+ . html::tag('table', ['id' => $attrib['id'] . '-owner', 'style' => 'display:none'] + $attrib,
+ html::tag('thead', null,
+ html::tag('tr', null,
+ html::tag('td', ['colspan' => 2], rcube::Q($this->cal->gettext('resourceowner')))
+ )
+ )
+ . html::tag('tbody', null, ''),
+ $table_attrib
+ );
+ }
+
+ /**
+ *
+ */
+ public function resource_calendar($attrib = [])
+ {
+ $attrib += ['id' => 'calendar-resources-calendar'];
+
+ $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
+
+ return html::div($attrib, '');
+ }
+
+ /**
+ * GUI object 'searchform' for the resource finder dialog
+ *
+ * @param array $attrib Named parameters
+ *
+ * @return string HTML code for the gui object
+ */
+ function resources_search_form($attrib)
+ {
+ $attrib += [
+ 'command' => 'search-resource',
+ 'reset-command' => 'reset-resource-search',
+ 'id' => 'rcmcalresqsearchbox',
+ 'autocomplete' => 'off',
+ 'form-name' => 'rcmcalresoursqsearchform',
+ 'gui-object' => 'resourcesearchform',
+ ];
+
+ // add form tag around text field
+ return $this->rc->output->search_form($attrib);
+ }
+
+ /**
+ *
+ */
+ function attendees_freebusy_table($attrib = [])
+ {
+ $table = new html_table(['cols' => 2, 'border' => 0, 'cellspacing' => 0]);
+ $table->add('attendees',
+ html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees'))
+ . html::div('timesheader', ' ')
+ . html::div(['id' => 'schedule-attendees-list', 'class' => 'attendees-list'], '')
+ );
+ $table->add('times',
+ html::div('scroll',
+ html::tag('table', ['id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0],
+ html::tag('thead') . html::tag('tbody')
+ )
+ . html::div(['id' => 'schedule-event-time', 'style' => 'display:none'], ' ')
+ )
+ );
+
+ return $table->show($attrib);
+ }
+
+ /**
+ *
+ */
+ function event_invitebox($attrib = [])
+ {
+ if (!empty($this->cal->event)) {
+ return html::div($attrib,
+ $this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation'))
+ . $this->cal->invitestatus
+ );
+ }
+
+ return '';
+ }
+
+ function event_rsvp_buttons($attrib = [])
+ {
+ $actions = ['accepted', 'tentative', 'declined'];
+
+ if (empty($attrib['delegate']) || $attrib['delegate'] !== 'false') {
+ $actions[] = 'delegated';
+ }
+
+ return $this->cal->itip->itip_rsvp_buttons($attrib, $actions);
+ }
}
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index aeb88d44..923b75a5 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1196,5 +1196,4 @@ class kolab_addressbook extends rcube_plugin
$this->rc->user->save_prefs(array('calendar_birthday_adressbooks' => $bday_addressbooks));
}
}
-
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index f6805eb0..1f9ac291 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -34,52 +34,78 @@ class rcube_kolab_contacts extends rcube_addressbook
public $undelete = true;
public $groups = true;
public $coltypes = array(
- 'name' => array('limit' => 1),
- 'firstname' => array('limit' => 1),
- 'surname' => array('limit' => 1),
- 'middlename' => array('limit' => 1),
- 'prefix' => array('limit' => 1),
- 'suffix' => array('limit' => 1),
- 'nickname' => array('limit' => 1),
- 'jobtitle' => array('limit' => 1),
- 'organization' => array('limit' => 1),
- 'department' => array('limit' => 1),
- 'email' => array('subtypes' => array('home','work','other')),
- 'phone' => array(),
- 'address' => array('subtypes' => array('home','work','office')),
- 'website' => array('subtypes' => array('homepage','blog')),
- 'im' => array('subtypes' => null),
- 'gender' => array('limit' => 1),
- 'birthday' => array('limit' => 1),
- 'anniversary' => array('limit' => 1),
- 'profession' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
- 'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
- 'manager' => array('limit' => null),
- 'assistant' => array('limit' => null),
- 'spouse' => array('limit' => 1),
- 'children' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => null,
- 'label' => 'kolab_addressbook.children', 'category' => 'personal'),
- 'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1,
- 'label' => 'kolab_addressbook.freebusyurl'),
- 'pgppublickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
- 'label' => 'kolab_addressbook.pgppublickey'),
- 'pkcs7publickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
- 'label' => 'kolab_addressbook.pkcs7publickey'),
- 'notes' => array('limit' => 1),
- 'photo' => array('limit' => 1),
- // TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
+ 'name' => array('limit' => 1),
+ 'firstname' => array('limit' => 1),
+ 'surname' => array('limit' => 1),
+ 'middlename' => array('limit' => 1),
+ 'prefix' => array('limit' => 1),
+ 'suffix' => array('limit' => 1),
+ 'nickname' => array('limit' => 1),
+ 'jobtitle' => array('limit' => 1),
+ 'organization' => array('limit' => 1),
+ 'department' => array('limit' => 1),
+ 'email' => array('subtypes' => array('home','work','other')),
+ 'phone' => array(),
+ 'address' => array('subtypes' => array('home','work','office')),
+ 'website' => array('subtypes' => array('homepage','blog')),
+ 'im' => array('subtypes' => null),
+ 'gender' => array('limit' => 1),
+ 'birthday' => array('limit' => 1),
+ 'anniversary' => array('limit' => 1),
+ 'profession' => array(
+ 'type' => 'text',
+ 'size' => 40,
+ 'maxlength' => 80,
+ 'limit' => 1,
+ 'label' => 'kolab_addressbook.profession',
+ 'category' => 'personal'
+ ),
+ 'manager' => array('limit' => null),
+ 'assistant' => array('limit' => null),
+ 'spouse' => array('limit' => 1),
+ 'children' => array(
+ 'type' => 'text',
+ 'size' => 40,
+ 'maxlength' => 80,
+ 'limit' => null,
+ 'label' => 'kolab_addressbook.children',
+ 'category' => 'personal'
+ ),
+ 'freebusyurl' => array(
+ 'type' => 'text',
+ 'size' => 40,
+ 'limit' => 1,
+ 'label' => 'kolab_addressbook.freebusyurl'
+ ),
+ 'pgppublickey' => array(
+ 'type' => 'textarea',
+ 'size' => 70,
+ 'rows' => 10,
+ 'limit' => 1,
+ 'label' => 'kolab_addressbook.pgppublickey'
+ ),
+ 'pkcs7publickey' => array(
+ 'type' => 'textarea',
+ 'size' => 70,
+ 'rows' => 10,
+ 'limit' => 1,
+ 'label' => 'kolab_addressbook.pkcs7publickey'
+ ),
+ 'notes' => array('limit' => 1),
+ 'photo' => array('limit' => 1),
+ // TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
);
/**
* vCard additional fields mapping
*/
public $vcard_map = array(
- 'profession' => 'X-PROFESSION',
- 'officelocation' => 'X-OFFICE-LOCATION',
- 'initials' => 'X-INITIALS',
- 'children' => 'X-CHILDREN',
- 'freebusyurl' => 'X-FREEBUSY-URL',
- 'pgppublickey' => 'KEY',
+ 'profession' => 'X-PROFESSION',
+ 'officelocation' => 'X-OFFICE-LOCATION',
+ 'initials' => 'X-INITIALS',
+ 'children' => 'X-CHILDREN',
+ 'freebusyurl' => 'X-FREEBUSY-URL',
+ 'pgppublickey' => 'KEY',
);
/**
@@ -102,25 +128,25 @@ class rcube_kolab_contacts extends rcube_addressbook
// list of fields used for searching in "All fields" mode
private $search_fields = array(
- 'name',
- 'firstname',
- 'surname',
- 'middlename',
- 'prefix',
- 'suffix',
- 'nickname',
- 'jobtitle',
- 'organization',
- 'department',
- 'email',
- 'phone',
- 'address',
- 'profession',
- 'manager',
- 'assistant',
- 'spouse',
- 'children',
- 'notes',
+ 'name',
+ 'firstname',
+ 'surname',
+ 'middlename',
+ 'prefix',
+ 'suffix',
+ 'nickname',
+ 'jobtitle',
+ 'organization',
+ 'department',
+ 'email',
+ 'phone',
+ 'address',
+ 'profession',
+ 'manager',
+ 'assistant',
+ 'spouse',
+ 'children',
+ 'notes',
);
@@ -132,15 +158,17 @@ class rcube_kolab_contacts extends rcube_addressbook
// extend coltypes configuration
$format = kolab_format::factory('contact');
- $this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
+
+ $this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
$this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
$rcube = rcube::get_instance();
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
- if (is_string($prop['label']))
+ if (is_string($prop['label'])) {
$this->coltypes[$col]['label'] = $rcube->gettext($prop['label']);
+ }
}
// fetch objects from the given IMAP folder
@@ -157,8 +185,9 @@ class rcube_kolab_contacts extends rcube_addressbook
$rights = $this->storagefolder->get_myrights();
if ($rights && !PEAR::isError($rights)) {
$this->rights = $rights;
- if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false)
+ if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false) {
$this->readonly = false;
+ }
}
}
}
@@ -233,17 +262,17 @@ class rcube_kolab_contacts extends rcube_addressbook
*/
public function get_carddav_url()
{
- $rcmail = rcmail::get_instance();
- if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
- return strtr($template, array(
- '%h' => $_SERVER['HTTP_HOST'],
- '%u' => urlencode($rcmail->get_user_name()),
- '%i' => urlencode($this->storagefolder->get_uid()),
- '%n' => urlencode($this->imap_folder),
- ));
- }
+ $rcmail = rcmail::get_instance();
+ if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
+ return strtr($template, array(
+ '%h' => $_SERVER['HTTP_HOST'],
+ '%u' => urlencode($rcmail->get_user_name()),
+ '%i' => urlencode($this->storagefolder->get_uid()),
+ '%n' => urlencode($this->imap_folder),
+ ));
+ }
- return false;
+ return false;
}
/**
@@ -254,7 +283,6 @@ class rcube_kolab_contacts extends rcube_addressbook
$this->gid = $gid;
}
-
/**
* Save a search string for future listings
*
@@ -265,7 +293,6 @@ class rcube_kolab_contacts extends rcube_addressbook
$this->filter = $filter;
}
-
/**
* Getter for saved search properties
*
@@ -276,7 +303,6 @@ class rcube_kolab_contacts extends rcube_addressbook
return $this->filter;
}
-
/**
* Reset saved results and search parameters
*/
@@ -286,14 +312,13 @@ class rcube_kolab_contacts extends rcube_addressbook
$this->filter = null;
}
-
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @param int Search mode. Sum of self::SEARCH_*
*
- * @return array Indexed list of contact groups, each a hash array
+ * @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
@@ -312,15 +337,14 @@ class rcube_kolab_contacts extends rcube_addressbook
return array_values($groups);
}
-
/**
* List the current set of contact records
*
* @param array List of cols to show
- * @param int Only return this number of records, use negative values for tail
- * @param boolean True to skip the count query (select only)
+ * @param int Only return this number of records, use negative values for tail
+ * @param bool True to skip the count query (select only)
*
- * @return array Indexed list of contact records, each a hash array
+ * @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols = null, $subset = 0, $nocount = false)
{
@@ -409,22 +433,21 @@ class rcube_kolab_contacts extends rcube_addressbook
return $this->result;
}
-
/**
* Search records
*
- * @param mixed $fields The field name of array of field names to search in
- * @param mixed $value Search value (or array of values when $fields is array)
- * @param int $mode Matching mode:
- * 0 - partial (*abc*),
- * 1 - strict (=),
- * 2 - prefix (abc*)
- * 4 - include groups (if supported)
- * @param boolean $select True if results are requested, False if count only
- * @param boolean $nocount True to skip the count query (select only)
- * @param array $required List of fields that cannot be empty
+ * @param mixed $fields The field name of array of field names to search in
+ * @param mixed $value Search value (or array of values when $fields is array)
+ * @param int $mode Matching mode:
+ * 0 - partial (*abc*),
+ * 1 - strict (=),
+ * 2 - prefix (abc*)
+ * 4 - include groups (if supported)
+ * @param bool $select True if results are requested, False if count only
+ * @param bool $nocount True to skip the count query (select only)
+ * @param array $required List of fields that cannot be empty
*
- * @return object rcube_result_set List of contact records and 'count' value
+ * @return rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
@@ -445,18 +468,21 @@ class rcube_kolab_contacts extends rcube_addressbook
$fields = $this->search_fields;
}
- if (!is_array($fields))
+ if (!is_array($fields)) {
$fields = array($fields);
- if (!is_array($required) && !empty($required))
+ }
+ if (!is_array($required) && !empty($required)) {
$required = array($required);
+ }
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
- else
+ else {
$value = mb_strtolower($value);
+ }
$scount = count($fields);
// build key name regexp
@@ -526,19 +552,18 @@ class rcube_kolab_contacts extends rcube_addressbook
return $this->list_records();
}
-
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
- if ($this->filter)
+ if ($this->filter) {
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['mode']);
+ }
return $this->get_search_set();
}
-
/**
* Count number of available contacts in database
*
@@ -560,7 +585,6 @@ class rcube_kolab_contacts extends rcube_addressbook
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
-
/**
* Return the last result set
*
@@ -571,15 +595,15 @@ class rcube_kolab_contacts extends rcube_addressbook
return $this->result;
}
-
/**
* Get a specific contact record
*
- * @param mixed record identifier(s)
- * @param boolean True to return record as associative array, otherwise a result set is returned
+ * @param mixed Record identifier(s)
+ * @param bool True to return record as associative array, otherwise a result set is returned
+ *
* @return mixed Result object with all record fields or False if not found
*/
- public function get_record($id, $assoc=false)
+ public function get_record($id, $assoc = false)
{
$rec = null;
$uid = $this->id2uid($id);
@@ -612,11 +636,11 @@ class rcube_kolab_contacts extends rcube_addressbook
return false;
}
-
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
+ *
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
@@ -624,28 +648,33 @@ class rcube_kolab_contacts extends rcube_addressbook
$out = array();
$this->_fetch_groups();
- foreach ((array)$this->groupmembers[$id] as $gid) {
- if ($group = $this->distlists[$gid])
- $out[$gid] = $group['name'];
+ if (!empty($this->groupmembers[$id])) {
+ foreach ((array) $this->groupmembers[$id] as $gid) {
+ if (!empty($this->distlists[$gid])) {
+ $group = $this->distlists[$gid];
+ $out[$gid] = $group['name'];
+ }
+ }
}
return $out;
}
-
/**
* Create a new contact record
*
- * @param array Assoziative array with save data
+ * @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
- * @param boolean True to check for duplicates first
+ * @param bool True to check for duplicates first
+ *
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
- if (!is_array($save_data))
+ if (!is_array($save_data)) {
return false;
+ }
$insert_id = $existing = false;
@@ -682,15 +711,15 @@ class rcube_kolab_contacts extends rcube_addressbook
return $insert_id;
}
-
/**
* Update a specific contact record
*
* @param mixed Record identifier
- * @param array Assoziative array with save data
+ * @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
- * @return boolean True on success, False on error
+ *
+ * @return bool True on success, False on error
*/
public function update($id, $save_data)
{
@@ -700,10 +729,11 @@ class rcube_kolab_contacts extends rcube_addressbook
if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving contact object to Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving contact object to Kolab server"
+ ),
+ true, false
+ );
}
else {
$updated = true;
@@ -715,12 +745,11 @@ class rcube_kolab_contacts extends rcube_addressbook
return $updated;
}
-
/**
* Mark one or more contact records as deleted
*
- * @param array Record identifiers
- * @param boolean Remove record(s) irreversible (mark as deleted otherwise)
+ * @param array Record identifiers
+ * @param bool Remove record(s) irreversible (mark as deleted otherwise)
*
* @return int Number of records deleted
*/
@@ -728,8 +757,9 @@ class rcube_kolab_contacts extends rcube_addressbook
{
$this->_fetch_groups();
- if (!is_array($ids))
+ if (!is_array($ids)) {
$ids = explode(',', $ids);
+ }
$count = 0;
foreach ($ids as $id) {
@@ -739,16 +769,18 @@ class rcube_kolab_contacts extends rcube_addressbook
if (!$deleted) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error deleting a contact object $uid from the Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error deleting a contact object $uid from the Kolab server"
+ ),
+ true, false
+ );
}
else {
// remove from distribution lists
- foreach ((array)$this->groupmembers[$id] as $gid) {
- if (!$is_mailto || $gid == $this->gid)
+ foreach ((array) $this->groupmembers[$id] as $gid) {
+ if (!$is_mailto || $gid == $this->gid) {
$this->remove_from_group($gid, $id);
+ }
}
// clear internal cache
@@ -761,19 +793,19 @@ class rcube_kolab_contacts extends rcube_addressbook
return $count;
}
-
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
- * @param array Record identifiers
+ * @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
- if (!is_array($ids))
+ if (!is_array($ids)) {
$ids = explode(',', $ids);
+ }
$count = 0;
foreach ($ids as $id) {
@@ -783,17 +815,17 @@ class rcube_kolab_contacts extends rcube_addressbook
}
else {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error undeleting a contact object $uid from the Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error undeleting a contact object $uid from the Kolab server"
+ ),
+ true, false
+ );
}
}
return $count;
}
-
/**
* Remove all records from the database
*
@@ -809,7 +841,6 @@ class rcube_kolab_contacts extends rcube_addressbook
}
}
-
/**
* Close connection to source
* Called on script shutdown
@@ -818,11 +849,11 @@ class rcube_kolab_contacts extends rcube_addressbook
{
}
-
/**
* Create a contact group with the given name
*
* @param string The group name
+ *
* @return mixed False on error, array with record props in success
*/
function create_group($name)
@@ -838,10 +869,11 @@ class rcube_kolab_contacts extends rcube_addressbook
if (!$saved) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list object to Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list object to Kolab server"
+ ),
+ true, false
+ );
return false;
}
else {
@@ -857,7 +889,8 @@ class rcube_kolab_contacts extends rcube_addressbook
* Delete the given group and all linked group members
*
* @param string Group identifier
- * @return boolean True on success, false if no data was changed
+ *
+ * @return bool True on success, false if no data was changed
*/
function delete_group($gid)
{
@@ -870,10 +903,11 @@ class rcube_kolab_contacts extends rcube_addressbook
if (!$deleted) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error deleting distribution-list object from the Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error deleting distribution-list object from the Kolab server"
+ ),
+ true, false
+ );
}
else {
$result = true;
@@ -889,7 +923,7 @@ class rcube_kolab_contacts extends rcube_addressbook
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
*
- * @return boolean New name on success, false if no data was changed
+ * @return bool New name on success, false if no data was changed
*/
function rename_group($gid, $newname, &$newid)
{
@@ -903,10 +937,11 @@ class rcube_kolab_contacts extends rcube_addressbook
if (!$saved) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list object to Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list object to Kolab server"
+ ),
+ true, false
+ );
return false;
}
@@ -916,9 +951,9 @@ class rcube_kolab_contacts extends rcube_addressbook
/**
* Add the given contact records the a certain group
*
- * @param string Group identifier
- * @param array List of contact identifiers to be added
- * @return int Number of contacts added
+ * @param string Group identifier
+ * @param array List of contact identifiers to be added
+ * @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
@@ -965,17 +1000,21 @@ class rcube_kolab_contacts extends rcube_addressbook
}
}
- if ($added)
+ if ($added) {
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
- else
+ }
+ else {
$saved = true;
+ }
if (!$saved) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list to Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list to Kolab server"
+ ),
+ true, false
+ );
+
$added = false;
$this->set_error(self::ERROR_SAVING, 'errorsaving');
}
@@ -989,23 +1028,26 @@ class rcube_kolab_contacts extends rcube_addressbook
/**
* Remove the given contact records from a certain group
*
- * @param string Group identifier
- * @param array List of contact identifiers to be removed
- * @return int Number of deleted group members
+ * @param string Group identifier
+ * @param array List of contact identifiers to be removed
+ * @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
- if (!is_array($ids))
+ if (!is_array($ids)) {
$ids = explode(',', $ids);
+ }
$this->_fetch_groups();
- if (!($list = $this->distlists[$gid]))
+ if (!($list = $this->distlists[$gid])) {
return false;
+ }
$new_member = array();
foreach ((array)$list['member'] as $member) {
- if (!in_array($member['ID'], $ids))
+ if (!in_array($member['ID'], $ids)) {
$new_member[] = $member;
+ }
}
// write distribution list back to server
@@ -1014,10 +1056,11 @@ class rcube_kolab_contacts extends rcube_addressbook
if (!$saved) {
rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list object to Kolab server"),
- true, false);
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving distribution-list object to Kolab server"
+ ),
+ true, false
+ );
}
else {
// remove group assigments in local cache
@@ -1039,7 +1082,7 @@ class rcube_kolab_contacts extends rcube_addressbook
* @param array Associative array with contact data to save
* @param bool Attempt to fix/complete data automatically
*
- * @return boolean True if input is valid, False if not.
+ * @return bool True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
@@ -1245,15 +1288,23 @@ class rcube_kolab_contacts extends rcube_addressbook
}
// photo is stored as separate attachment
- if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
+ if ($record['photo'] && strlen($record['photo']) < 255 && !empty($record['_attachments'][$record['photo']])) {
+ $att = $record['_attachments'][$record['photo']];
// only fetch photo content if requested
- if ($this->action == 'photo')
- $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['id']);
+ if ($this->action == 'photo') {
+ if (!empty($att['content'])) {
+ $record['photo'] = $att['content'];
+ }
+ else {
+ $record['photo'] = $this->storagefolder->get_attachment($record['uid'], $att['id']);
+ }
+ }
}
// truncate publickey value for display
- if ($record['pgppublickey'] && $this->action == 'show')
+ if (!empty($record['pgppublickey']) && $this->action == 'show') {
$record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
+ }
// remove empty fields
$record = array_filter($record);
@@ -1269,10 +1320,12 @@ class rcube_kolab_contacts extends rcube_addressbook
*/
private function _from_rcube_contact($contact, $old = array())
{
- if (!$contact['uid'] && $contact['ID'])
+ if (!$contact['uid'] && $contact['ID']) {
$contact['uid'] = $this->id2uid($contact['ID']);
- else if (!$contact['uid'] && $old['uid'])
+ }
+ else if (!$contact['uid'] && $old['uid']) {
$contact['uid'] = $old['uid'];
+ }
$contact['im'] = array_filter($this->get_col_values('im', $contact, true));
@@ -1295,8 +1348,9 @@ class rcube_kolab_contacts extends rcube_addressbook
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
- if (empty($adr))
+ if (empty($adr)) {
continue;
+ }
$addresses[] = array(
'type' => $type,
@@ -1318,8 +1372,9 @@ class rcube_kolab_contacts extends rcube_addressbook
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
- if (!isset($contact[$key]) && $key[0] == '_')
+ if (!isset($contact[$key]) && $key[0] == '_') {
$contact[$key] = $val;
+ }
}
// convert one-item-array elements into string element
@@ -1334,7 +1389,12 @@ class rcube_kolab_contacts extends rcube_addressbook
unset($contact['vcard']);
// add empty values for some fields which can be removed in the UI
- return array_filter($contact) + array('nickname' => '', 'birthday' => '', 'anniversary' => '', 'freebusyurl' => '', 'photo' => $contact['photo']);
+ return array_filter($contact) + array(
+ 'nickname' => '',
+ 'birthday' => '',
+ 'anniversary' => '',
+ 'freebusyurl' => '',
+ 'photo' => $contact['photo']
+ );
}
-
}
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 42b6106d..93e0a26e 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -118,14 +118,15 @@ class libcalendaring_itip
// compose a list of all event attendees
$attendees_list = array();
foreach ((array)$event['attendees'] as $attendee) {
- $attendees_list[] = ($attendee['name'] && $attendee['email']) ?
+ $attendees_list[] = (!empty($attendee['name']) && !empty($attendee['email'])) ?
$attendee['name'] . ' <' . $attendee['email'] . '>' :
- ($attendee['name'] ? $attendee['name'] : $attendee['email']);
+ (!empty($attendee['name']) ? $attendee['name'] : $attendee['email']);
}
$recurrence_info = '';
if (!empty($event['recurrence_id'])) {
- $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
+ $msg = $this->gettext(!empty($event['thisandfuture']) ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence');
+ $recurrence_info = "\n\n** $msg **";
}
else if (!empty($event['recurrence'])) {
$recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
@@ -139,7 +140,7 @@ class libcalendaring_itip
'attendees' => join(",\n ", $attendees_list),
'sender' => $this->sender['name'],
'organizer' => $this->sender['name'],
- 'description' => $event['description'],
+ 'description' => isset($event['description']) ? $event['description'] : '',
)
));
@@ -243,8 +244,14 @@ class libcalendaring_itip
// set RSVP for every attendee
else if ($method == 'REQUEST') {
foreach ($event['attendees'] as $i => $attendee) {
- if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) {
- $event['attendees'][$i]['rsvp']= (bool)$rsvp;
+ if (
+ ($rsvp || !isset($attendee['rsvp']))
+ && (
+ (empty($attendee['status']) || $attendee['status'] != 'DELEGATED')
+ && $attendee['role'] != 'NON-PARTICIPANT'
+ )
+ ) {
+ $event['attendees'][$i]['rsvp']= (bool) $rsvp;
}
}
}
@@ -293,7 +300,7 @@ class libcalendaring_itip
// attach ics file for this event
$ical = libcalendaring::get_ical();
$ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
- $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+ $filename = !empty($event['_type']) && $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
$message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method);
return $message;
@@ -521,7 +528,7 @@ class libcalendaring_itip
protected function get_itip_diff($event, $existing)
{
- if (empty($event) || empty($existing) || empty($event['message_uid'])) {
+ if (empty($event) || empty($existing) || empty($event['message_uid']) || empty($event['mime_id'])) {
return;
}
@@ -556,14 +563,14 @@ class libcalendaring_itip
$attendee['status'] = 'ACCEPTED'; // sometimes is not set for exceptions
$existing['attendees'][$idx] = $attendee;
}
- $existing_attendees[] = $attendee['email'].$attendee['name'];
+ $existing_attendees[] = $attendee['email'] . (isset($attendee['name']) ? $attendee['name'] : '');
}
foreach ((array) $itip['attendees'] as $idx => $attendee) {
- if ($attendee['email'] && ($_status = $status[strtolower($attendee['email'])])) {
- $attendee['status'] = $_status;
+ if (!empty($attendee['email']) && !empty($status[strtolower($attendee['email'])])) {
+ $attendee['status'] = $status[strtolower($attendee['email'])];
$itip['attendees'][$idx] = $attendee;
}
- $itip_attendees[] = $attendee['email'].$attendee['name'];
+ $itip_attendees[] = $attendee['email'] . (isset($attendee['name']) ? $attendee['name'] : '');
}
if ($itip_attendees != $existing_attendees) {
@@ -597,14 +604,16 @@ class libcalendaring_itip
public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null)
{
$buttons = array();
- $dom_id = asciiwords($event['uid'], true);
- $rsvp_status = 'unknown';
+ $dom_id = asciiwords($event['uid'], true);
+
+ $rsvp_status = 'unknown';
+ $rsvp_buttons = '';
// pass some metadata about the event and trigger the asynchronous status check
$changed = is_object($event['changed']) ? $event['changed'] : $message_date;
$metadata = array(
'uid' => $event['uid'],
- '_instance' => $event['_instance'],
+ '_instance' => isset($event['_instance']) ? $event['_instance'] : null,
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'method' => $method,
@@ -744,7 +753,7 @@ class libcalendaring_itip
}
// add itip reply message controls
- $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave']));
+ $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, !empty($metadata['nosave'])));
$buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
$buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
@@ -759,8 +768,8 @@ class libcalendaring_itip
$title = $this->gettext('itipcancellation');
$event_prop = array_filter(array(
'uid' => $event['uid'],
- '_instance' => $event['_instance'],
- '_savemode' => $event['_savemode'],
+ '_instance' => isset($event['_instance']) ? $event['_instance'] : null,
+ '_savemode' => isset($event['_savemode']) ? $event['_savemode'] : null,
));
// 1. remove the event from our calendar
@@ -786,7 +795,7 @@ class libcalendaring_itip
}
// append generic import button
- if ($import_button) {
+ if (!empty($import_button)) {
$buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
}
@@ -815,13 +824,16 @@ class libcalendaring_itip
{
$attrib += array('type' => 'button');
- if (!$actions)
+ if (!$actions) {
$actions = $this->rsvp_actions;
+ }
+
+ $buttons = '';
foreach ($actions as $method) {
$buttons .= html::tag('input', array(
'type' => $attrib['type'],
- 'name' => $attrib['iname'],
+ 'name' => !empty($attrib['iname']) ? $attrib['iname'] : null,
'class' => 'button',
'rel' => $method,
'value' => $this->gettext('itip' . $method),
@@ -923,7 +935,7 @@ class libcalendaring_itip
$table->add('label', $this->gettext('recurring'));
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
}
- if ($location = trim($event['location'])) {
+ if (isset($event['location']) && ($location = trim($event['location']))) {
$table->add('label', $this->gettext('location'));
$table->add('location', rcube::Q($location));
}
@@ -931,11 +943,11 @@ class libcalendaring_itip
$table->add('label', $this->gettext('sensitivity'));
$table->add('sensitivity', ucfirst($this->gettext($sensitivity)) . '!');
}
- if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') {
+ if (!empty($event['status']) && ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED')) {
$table->add('label', $this->gettext('status'));
$table->add('status', $this->gettext('status-' . strtolower($event['status'])));
}
- if ($comment = trim($event['comment'])) {
+ if (isset($event['comment']) && ($comment = trim($event['comment']))) {
$table->add('label', $this->gettext('comment'));
$table->add('location', rcube::Q($comment));
}
diff --git a/plugins/libcalendaring/lib/libcalendaring_recurrence.php b/plugins/libcalendaring/lib/libcalendaring_recurrence.php
index f83d024c..4eebd68d 100644
--- a/plugins/libcalendaring/lib/libcalendaring_recurrence.php
+++ b/plugins/libcalendaring/lib/libcalendaring_recurrence.php
@@ -61,15 +61,15 @@ class libcalendaring_recurrence
$this->set_start($start);
- if (is_array($recurrence['EXDATE'])) {
- foreach ($recurrence['EXDATE'] as $exdate) {
+ if (!empty($recurrence['EXDATE'])) {
+ foreach ((array) $recurrence['EXDATE'] as $exdate) {
if (is_a($exdate, 'DateTime')) {
$this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
}
}
}
- if (is_array($recurrence['RDATE'])) {
- foreach ($recurrence['RDATE'] as $rdate) {
+ if (!empty($recurrence['RDATE'])) {
+ foreach ((array) $recurrence['RDATE'] as $rdate) {
if (is_a($rdate, 'DateTime')) {
$this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
}
@@ -160,9 +160,10 @@ class libcalendaring_recurrence
$start = clone $this->start;
$orig_start = clone $this->start;
$r = $this->recurrence;
- $interval = intval($r['INTERVAL'] ?: 1);
+ $interval = !empty($r['INTERVAL']) ? intval($r['INTERVAL']) : 1;
+ $frequency = isset($this->recurrence['FREQ']) ? $this->recurrence['FREQ'] : null;
- switch ($this->recurrence['FREQ']) {
+ switch ($frequency) {
case 'WEEKLY':
if (empty($this->recurrence['BYDAY'])) {
return $start;
@@ -193,7 +194,7 @@ class libcalendaring_recurrence
$r = $this->recurrence;
$r['INTERVAL'] = $interval;
- if ($r['COUNT']) {
+ if (!empty($r['COUNT'])) {
// Increase count so we do not stop the loop to early
$r['COUNT'] += 100;
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 379bd945..1d25c380 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -40,23 +40,23 @@ class libcalendaring extends rcube_plugin
public $ical_message;
public $defaults = array(
- 'calendar_date_format' => "Y-m-d",
- 'calendar_date_short' => "M-j",
- 'calendar_date_long' => "F j Y",
- 'calendar_date_agenda' => "l M-d",
- 'calendar_time_format' => "H:m",
- 'calendar_first_day' => 1,
- 'calendar_first_hour' => 6,
- 'calendar_date_format_sets' => array(
- 'Y-m-d' => array('d M Y', 'm-d', 'l m-d'),
- 'Y/m/d' => array('d M Y', 'm/d', 'l m/d'),
- 'Y.m.d' => array('d M Y', 'm.d', 'l m.d'),
- 'd-m-Y' => array('d M Y', 'd-m', 'l d-m'),
- 'd/m/Y' => array('d M Y', 'd/m', 'l d/m'),
- 'd.m.Y' => array('d M Y', 'd.m', 'l d.m'),
- 'j.n.Y' => array('d M Y', 'd.m', 'l d.m'),
- 'm/d/Y' => array('M d Y', 'm/d', 'l m/d'),
- ),
+ 'calendar_date_format' => "Y-m-d",
+ 'calendar_date_short' => "M-j",
+ 'calendar_date_long' => "F j Y",
+ 'calendar_date_agenda' => "l M-d",
+ 'calendar_time_format' => "H:m",
+ 'calendar_first_day' => 1,
+ 'calendar_first_hour' => 6,
+ 'calendar_date_format_sets' => array(
+ 'Y-m-d' => array('d M Y', 'm-d', 'l m-d'),
+ 'Y/m/d' => array('d M Y', 'm/d', 'l m/d'),
+ 'Y.m.d' => array('d M Y', 'm.d', 'l m.d'),
+ 'd-m-Y' => array('d M Y', 'd-m', 'l d-m'),
+ 'd/m/Y' => array('d M Y', 'd/m', 'l d/m'),
+ 'd.m.Y' => array('d M Y', 'd.m', 'l d.m'),
+ 'j.n.Y' => array('d M Y', 'd.m', 'l d.m'),
+ 'm/d/Y' => array('M d Y', 'm/d', 'l m/d'),
+ ),
);
private static $instance;
@@ -187,19 +187,20 @@ class libcalendaring extends rcube_plugin
*/
public function adjust_timezone($dt, $dateonly = false)
{
- if (is_numeric($dt))
+ if (is_numeric($dt)) {
$dt = new DateTime('@'.$dt);
- else if (is_string($dt))
+ }
+ else if (is_string($dt)) {
$dt = rcube_utils::anytodatetime($dt);
+ }
- if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
+ if ($dt instanceof DateTime && empty($dt->_dateonly) && !$dateonly) {
$dt->setTimezone($this->timezone);
}
return $dt;
}
-
/**
*
*/
@@ -289,11 +290,12 @@ class libcalendaring extends rcube_plugin
*/
public function event_date_text($event, $tzinfo = false)
{
- $fromto = '--';
+ $fromto = '--';
+ $is_task = !empty($event['_type']) && $event['_type'] == 'task';
// handle task objects
- if ($event['_type'] == 'task' && is_object($event['due'])) {
- $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
+ if ($is_task && !empty($event['due']) && is_object($event['due'])) {
+ $date_format = !empty($event['due']->_dateonly) ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
$fromto = $this->rc->format_date($event['due'], $date_format, false);
// add timezone information
@@ -351,18 +353,21 @@ class libcalendaring extends rcube_plugin
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type form-control', 'id' => $attrib['id']));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset form-control'));
$select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related form-control'));
- $object_type = $attrib['_type'] ?: 'event';
+ $object_type = !empty($attrib['_type']) ? $attrib['_type'] : 'event';
$select_type->add($this->gettext('none'), '');
- foreach ($alarm_types as $type)
+ foreach ($alarm_types as $type) {
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
+ }
- foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
+ foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) {
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
+ }
$select_offset->add($this->gettext('trigger0'), '0');
- if ($absolute_time)
+ if ($absolute_time) {
$select_offset->add($this->gettext('trigger@'), '@');
+ }
$select_related->add($this->gettext('relatedstart'), 'start');
$select_related->add($this->gettext('relatedend' . $object_type), 'end');
@@ -399,7 +404,7 @@ class libcalendaring extends rcube_plugin
}
// return cached result
- if (is_array($_emails[$user])) {
+ if (isset($_emails[$user])) {
return $_emails[$user];
}
@@ -802,12 +807,13 @@ class libcalendaring extends rcube_plugin
return rcmail::get_instance()->format_date($dt, $format);
};
- if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
+ if (!empty($rrule['EXDATE']) && is_array($rrule['EXDATE'])) {
$exdates = array_map($format_fn, $rrule['EXDATE']);
}
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
$rdates = array_map($format_fn, $rrule['RDATE']);
+ $more = false;
if (!empty($exdates)) {
$rdates = array_diff($rdates, $exdates);
@@ -818,8 +824,7 @@ class libcalendaring extends rcube_plugin
$more = true;
}
- return $this->gettext('ondate') . ' ' . join(', ', $rdates)
- . ($more ? '...' : '');
+ return $this->gettext('ondate') . ' ' . join(', ', $rdates) . ($more ? '...' : '');
}
$output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1);
@@ -839,10 +844,10 @@ class libcalendaring extends rcube_plugin
break;
}
- if ($rrule['COUNT']) {
+ if (!empty($rrule['COUNT'])) {
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
}
- else if ($rrule['UNTIL']) {
+ else if (!empty($rrule['UNTIL'])) {
$until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format);
}
else {
@@ -852,13 +857,13 @@ class libcalendaring extends rcube_plugin
$output .= ', ' . $until;
if (!empty($exdates)) {
+ $more = false;
if (count($exdates) > $limit) {
$exdates = array_slice($exdates, 0, $limit);
$more = true;
}
- $output .= '; ' . $this->gettext('except') . ' ' . join(', ', $exdates)
- . ($more ? '...' : '');
+ $output .= '; ' . $this->gettext('except') . ' ' . join(', ', $exdates) . ($more ? '...' : '');
}
return $output;
@@ -1056,16 +1061,16 @@ class libcalendaring extends rcube_plugin
*/
public function to_client_recurrence($recurrence, $allday = false)
{
- if ($recurrence['UNTIL']) {
+ if (!empty($recurrence['UNTIL'])) {
$recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
}
// format RDATE values
- if (is_array($recurrence['RDATE'])) {
+ if (!empty($recurrence['RDATE'])) {
$libcal = $this;
$recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
- }, $recurrence['RDATE']);
+ }, (array) $recurrence['RDATE']);
}
unset($recurrence['EXCEPTIONS']);
@@ -1082,7 +1087,7 @@ class libcalendaring extends rcube_plugin
$recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
}
- if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
+ if (is_array($recurrence) && !empty($recurrence['RDATE'])) {
$tz = $this->timezone;
$recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
@@ -1195,7 +1200,7 @@ class libcalendaring extends rcube_plugin
$headers = $imap->get_message_headers($uid);
$parser = $this->get_ical();
- if ($part->ctype_parameters['charset']) {
+ if (!empty($part->ctype_parameters['charset'])) {
$charset = $part->ctype_parameters['charset'];
}
@@ -1238,8 +1243,9 @@ class libcalendaring extends rcube_plugin
$level = explode('.', $part->mime_id);
while (array_pop($level) !== null) {
- $parent = $message->mime_parts[join('.', $level) ?: 0];
- if ($parent->mimetype == 'multipart/report') {
+ $id = join('.', $level) ?: 0;
+ $parent = !empty($message->mime_parts[$id]) ? $message->mime_parts[$id] : null;
+ if ($parent && $parent->mimetype == 'multipart/report') {
return false;
}
}
@@ -1248,7 +1254,7 @@ class libcalendaring extends rcube_plugin
return (
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
// Apple sends files as application/x-any (!?)
- ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
+ ($part->mimetype == 'application/x-any' && !empty($part->filename) && preg_match('/\.ics$/i', $part->filename))
);
}
@@ -1288,7 +1294,7 @@ class libcalendaring extends rcube_plugin
*/
public static function recurrence_id_format($event)
{
- return $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ return !empty($event['allday']) ? 'Ymd' : 'Ymd\THis';
}
/**
@@ -1301,13 +1307,13 @@ class libcalendaring extends rcube_plugin
*/
public static function recurrence_instance_identifier($event, $allday = null)
{
- $instance_date = $event['recurrence_date'] ?: $event['start'];
+ $instance_date = !empty($event['recurrence_date']) ? $event['recurrence_date'] : $event['start'];
- if ($instance_date && is_a($instance_date, 'DateTime')) {
+ if ($instance_date instanceof DateTime) {
// According to RFC5545 (3.8.4.4) RECURRENCE-ID format should
// be date/date-time depending on the main event type, not the exception
if ($allday === null) {
- $allday = $event['allday'];
+ $allday = !empty($event['allday']);
}
return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis');
@@ -1547,5 +1553,4 @@ class libcalendaring extends rcube_plugin
'c' => '',
));
}
-
}
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 3dda65d4..0bf58456 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -577,7 +577,7 @@ class libvcalendar implements Iterator
$schedule_agent = $attendee['schedule-agent'];
}
}
- else if ($attendee['email'] != $event['organizer']['email']) {
+ else if (empty($event['organizer']) || $attendee['email'] != $event['organizer']['email']) {
$event['attendees'][] = $attendee;
}
break;
@@ -756,7 +756,7 @@ class libvcalendar implements Iterator
}
// For date-only we'll keep the date and time intact
- if ($date->_dateonly) {
+ if (!empty($date->_dateonly)) {
$dt = new DateTime(null, $this->timezone);
$dt->setDate($date->format('Y'), $date->format('n'), $date->format('j'));
$dt->setTime($date->format('G'), $date->format('i'), 0);
diff --git a/plugins/libkolab/lib/kolab_attachments_handler.php b/plugins/libkolab/lib/kolab_attachments_handler.php
index d38739e6..bc41e13a 100644
--- a/plugins/libkolab/lib/kolab_attachments_handler.php
+++ b/plugins/libkolab/lib/kolab_attachments_handler.php
@@ -48,7 +48,7 @@ class kolab_attachments_handler
*/
public function files_list($attrib = array())
{
- if (!$attrib['id']) {
+ if (empty($attrib['id'])) {
$attrib['id'] = 'kolabattachmentlist';
}
@@ -67,7 +67,7 @@ class kolab_attachments_handler
public function files_form($attrib = array())
{
// add ID if not given
- if (!$attrib['id']) {
+ if (empty($attrib['id'])) {
$attrib['id'] = 'kolabuploadform';
}
@@ -80,7 +80,7 @@ class kolab_attachments_handler
public function files_drop_area($attrib = array())
{
// add ID if not given
- if (!$attrib['id']) {
+ if (empty($attrib['id'])) {
$attrib['id'] = 'kolabfiledroparea';
}
@@ -117,7 +117,7 @@ class kolab_attachments_handler
$recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
- if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
+ if (empty($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
$_SESSION[$session_key] = array();
$_SESSION[$session_key]['id'] = $recid;
$_SESSION[$session_key]['attachments'] = array();
@@ -151,13 +151,16 @@ class kolab_attachments_handler
unset($attachment['status'], $attachment['abort']);
$this->rc->session->append($session_key . '.attachments', $id, $attachment);
- if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
+ if (!empty($_SESSION[$session_key . '_deleteicon'])
+ && ($icon = $_SESSION[$session_key . '_deleteicon'])
+ && is_file($icon)
+ ) {
$button = html::img(array(
'src' => $icon,
'alt' => $this->rc->gettext('delete')
));
}
- else if ($_SESSION[$session_key . '_textbuttons']) {
+ else if (!empty($_SESSION[$session_key . '_textbuttons'])) {
$button = rcube::Q($this->rc->gettext('delete'));
}
else {
@@ -181,7 +184,8 @@ class kolab_attachments_handler
'onclick' => 'return false', // sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
), $link_content);
- $content .= $_SESSION[$session_key . '_icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link;
+ $left = !empty($_SESSION[$session_key . '_icon_pos']) && $_SESSION[$session_key . '_icon_pos'] == 'left';
+ $content = $left ? $delete_link.$content_link : $content_link.$delete_link;
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
@@ -196,7 +200,7 @@ class kolab_attachments_handler
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
- else if ($attachment['error']) {
+ else if (!empty($attachment['error'])) {
$msg = $attachment['error'];
}
else {
@@ -211,11 +215,13 @@ class kolab_attachments_handler
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
- if ($maxsize = ini_get('post_max_size'))
+ if ($maxsize = ini_get('post_max_size')) {
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => $this->rc->show_bytes(parse_bytes($maxsize)))));
- else
+ }
+ else {
$msg = $this->rc->gettext('fileuploaderror');
+ }
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
@@ -233,7 +239,7 @@ class kolab_attachments_handler
{
ob_end_clean();
- if ($attachment && $attachment['body']) {
+ if ($attachment && !empty($attachment['body'])) {
// allow post-processing of the attachment body
$part = new rcube_message_part;
$part->filename = $attachment['name'];
diff --git a/plugins/libkolab/lib/kolab_bonnie_api.php b/plugins/libkolab/lib/kolab_bonnie_api.php
index 6905dcaa..c4368e2c 100644
--- a/plugins/libkolab/lib/kolab_bonnie_api.php
+++ b/plugins/libkolab/lib/kolab_bonnie_api.php
@@ -93,5 +93,4 @@ class kolab_bonnie_api
{
return $this->client->execute($method, $params);
}
-
-}
\ No newline at end of file
+}
diff --git a/plugins/libkolab/lib/kolab_bonnie_api_client.php b/plugins/libkolab/lib/kolab_bonnie_api_client.php
index bc209f41..a69aa05e 100644
--- a/plugins/libkolab/lib/kolab_bonnie_api_client.php
+++ b/plugins/libkolab/lib/kolab_bonnie_api_client.php
@@ -235,5 +235,4 @@ class kolab_bonnie_api_client
rcube::write_log('bonnie', join(";\n", $msg));
}
-
-}
\ No newline at end of file
+}
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index cb35f98d..bc57e6b9 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -151,5 +151,4 @@ class kolab_format_task extends kolab_format_xcal
return array_unique($tags);
}
-
}
diff --git a/plugins/libkolab/lib/kolab_storage_dataset.php b/plugins/libkolab/lib/kolab_storage_dataset.php
index 9ddf3f9f..9f39b12e 100644
--- a/plugins/libkolab/lib/kolab_storage_dataset.php
+++ b/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -150,5 +150,4 @@ class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
{
return !empty($this->index[$this->iteratorkey]);
}
-
}
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index db120aa2..faadd262 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -93,7 +93,15 @@ class libkolab extends rcube_plugin
*/
function storage_init($p)
{
- $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID');
+ $kolab_headers = 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID';
+
+ if (!empty($p['fetch_headers'])) {
+ $p['fetch_headers'] .= ' ' . $kolab_headers;
+ }
+ else {
+ $p['fetch_headers'] = $kolab_headers;
+ }
+
return $p;
}
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index 4d61e1d3..628b7c52 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -118,8 +118,8 @@ class tasklist_database_driver extends tasklist_driver
. " VALUES (?, ?, ?, ?)",
$this->rc->user->ID,
strval($prop['name']),
- strval($prop['color']),
- $prop['showalarms'] ? 1 : 0
+ isset($prop['color']) ? strval($prop['color']) : '',
+ !empty($prop['showalarms']) ? 1 : 0
);
if ($result) {
@@ -143,8 +143,8 @@ class tasklist_database_driver extends tasklist_driver
"UPDATE " . $this->db_lists . " SET `name` = ?, `color` = ?, `showalarms` = ?"
. " WHERE `tasklist_id` = ? AND `user_id` = ?",
strval($prop['name']),
- strval($prop['color']),
- $prop['showalarms'] ? 1 : 0,
+ isset($prop['color']) ? strval($prop['color']) : '',
+ !empty($prop['showalarms']) ? 1 : 0,
$prop['id'],
$this->rc->user->ID
);
@@ -163,7 +163,7 @@ class tasklist_database_driver extends tasklist_driver
{
$hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', '')));
- if ($prop['active']) {
+ if (!empty($prop['active'])) {
unset($hidden[$prop['id']]);
}
else {
@@ -291,56 +291,53 @@ class tasklist_database_driver extends tasklist_driver
$sql_add = '';
// add filter criteria
- if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
- $sql_add .= " AND (`date` IS NULL OR `date` >= ?)";
- $datefrom = $filter['from'];
- }
- if ($filter['to']) {
- if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE) {
- $sql_add .= " AND (`date` IS NOT NULL AND `date` <= " . $this->rc->db->quote($filter['to']) . ")";
+ if ($filter) {
+ if (!empty($filter['from']) || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
+ $sql_add .= " AND (`date` IS NULL OR `date` >= " . $this->rc->db->quote($filter['from']) . ")";
}
- else {
- $sql_add .= " AND (`date` IS NULL OR `date` <= " . $this->rc->db->quote($filter['to']) . ")";
+
+ if (!empty($filter['to'])) {
+ if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE) {
+ $sql_add .= " AND (`date` IS NOT NULL AND `date` <= " . $this->rc->db->quote($filter['to']) . ")";
+ }
+ else {
+ $sql_add .= " AND (`date` IS NULL OR `date` <= " . $this->rc->db->quote($filter['to']) . ")";
+ }
}
- }
- // special case 'today': also show all events with date before today
- if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) {
- $datefrom = date('Y-m-d', 0);
- }
-
- if ($filter['mask'] & tasklist::FILTER_MASK_NODATE) {
- $sql_add = " AND `date` IS NULL";
- }
-
- if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) {
- $sql_add .= " AND " . self::IS_COMPLETE_SQL;
- }
- else if (empty($filter['since'])) {
- // don't show complete tasks by default
- $sql_add .= " AND NOT " . self::IS_COMPLETE_SQL;
- }
-
- if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED) {
- $sql_add .= " AND `flagged` = 1";
- }
-
- // compose (slow) SQL query for searching
- // FIXME: improve searching using a dedicated col and normalized values
- if ($filter['search']) {
- $sql_query = array();
- foreach (array('title', 'description', 'organizer', 'attendees') as $col) {
- $sql_query[] = $this->rc->db->ilike($col, '%' . $filter['search'] . '%');
+ if ($filter['mask'] & tasklist::FILTER_MASK_NODATE) {
+ $sql_add = " AND `date` IS NULL";
}
- $sql_add = " AND (" . join(" OR ", $sql_query) . ")";
- }
- if ($filter['since'] && is_numeric($filter['since'])) {
- $sql_add .= " AND `changed` >= " . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since']));
- }
+ if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) {
+ $sql_add .= " AND " . self::IS_COMPLETE_SQL;
+ }
+ else if (empty($filter['since'])) {
+ // don't show complete tasks by default
+ $sql_add .= " AND NOT " . self::IS_COMPLETE_SQL;
+ }
- if ($filter['uid']) {
- $sql_add .= " AND `uid` IN (" . implode(',', array_map(array($this->rc->db, 'quote'), $filter['uid'])) . ")";
+ if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED) {
+ $sql_add .= " AND `flagged` = 1";
+ }
+
+ // compose (slow) SQL query for searching
+ // FIXME: improve searching using a dedicated col and normalized values
+ if ($filter['search']) {
+ $sql_query = array();
+ foreach (array('title', 'description', 'organizer', 'attendees') as $col) {
+ $sql_query[] = $this->rc->db->ilike($col, '%' . $filter['search'] . '%');
+ }
+ $sql_add = " AND (" . join(" OR ", $sql_query) . ")";
+ }
+
+ if (!empty($filter['since']) && is_numeric($filter['since'])) {
+ $sql_add .= " AND `changed` >= " . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since']));
+ }
+
+ if (!empty($filter['uid'])) {
+ $sql_add .= " AND `uid` IN (" . implode(',', array_map(array($this->rc->db, 'quote'), $filter['uid'])) . ")";
+ }
}
$tasks = array();
@@ -348,8 +345,7 @@ class tasklist_database_driver extends tasklist_driver
$result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks
. " WHERE `tasklist_id` IN (" . join(',', $list_ids) . ")"
. " AND `del` = 0" . $sql_add
- . " ORDER BY `parent_id`, `task_id` ASC",
- $datefrom
+ . " ORDER BY `parent_id`, `task_id` ASC"
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
@@ -375,12 +371,12 @@ class tasklist_database_driver extends tasklist_driver
$prop['uid'] = $prop;
}
- $query_col = $prop['id'] ? 'task_id' : 'uid';
+ $query_col = !empty($prop['id']) ? 'task_id' : 'uid';
$result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks
. " WHERE `tasklist_id` IN (" . $this->list_ids . ")"
. " AND `$query_col` = ? AND `del` = 0",
- $prop['id'] ? $prop['id'] : $prop['uid']
+ !empty($prop['id']) ? $prop['id'] : $prop['uid']
);
if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
@@ -557,15 +553,15 @@ class tasklist_database_driver extends tasklist_driver
public function create_task($prop)
{
// check list permissions
- $list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists));
- if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly']) {
+ $list_id = !empty($prop['list']) ? $prop['list'] : reset(array_keys($this->lists));
+ if (empty($this->lists[$list_id]) || !empty($this->lists[$list_id]['readonly'])) {
return false;
}
- if (is_array($prop['valarms'])) {
+ if (!empty($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
- if (is_array($prop['recurrence'])) {
+ if (!empty($prop['recurrence'])) {
$prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']);
}
if (array_key_exists('complete', $prop)) {
@@ -594,13 +590,13 @@ class tasklist_database_driver extends tasklist_driver
$prop['time'],
$prop['startdate'],
$prop['starttime'],
- strval($prop['description']),
- join(',', (array)$prop['tags']),
- $prop['flagged'] ? 1 : 0,
- $prop['complete'] ?: 0,
+ isset($prop['description']) ? strval($prop['description']) : '',
+ !empty($prop['tags']) ? join(',', (array)$prop['tags']) : '',
+ !empty($prop['flagged']) ? 1 : 0,
+ !empty($prop['complete']) ?: 0,
strval($prop['status']),
- $prop['alarms'],
- $prop['recurrence'],
+ isset($prop['alarms']) ? $prop['alarms'] : '',
+ isset($prop['recurrence']) ? $prop['recurrence'] : '',
$notify_at
);
@@ -621,10 +617,10 @@ class tasklist_database_driver extends tasklist_driver
*/
public function edit_task($prop)
{
- if (is_array($prop['valarms'])) {
+ if (isset($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
- if (is_array($prop['recurrence'])) {
+ if (isset($prop['recurrence'])) {
$prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']);
}
if (array_key_exists('complete', $prop)) {
@@ -655,7 +651,7 @@ class tasklist_database_driver extends tasklist_driver
}
// moved from another list
- if ($prop['_fromlist'] && ($newlist = $prop['list'])) {
+ if (!empty($prop['_fromlist']) && ($newlist = $prop['list'])) {
$sql_set[] = $this->rc->db->quote_identifier('tasklist_id') . '=' . $this->rc->db->quote($newlist);
}
@@ -735,10 +731,10 @@ class tasklist_database_driver extends tasklist_driver
*/
private function _get_notification($task)
{
- if ($task['valarms'] && !$this->is_complete($task)) {
+ if (!empty($task['valarms']) && !$this->is_complete($task)) {
$alarm = libcalendaring::get_next_alarm($task, 'task');
- if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) {
+ if (!empty($alarm['time']) && in_array($alarm['action'], $this->alarm_types)) {
return date('Y-m-d H:i:s', $alarm['time']);
}
}
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index a5344dd9..2d991535 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -66,6 +66,7 @@ class tasklist extends rcube_plugin
private $collapsed_tasks = array();
private $message_tasks = array();
+ private $task_titles = array();
/**
@@ -139,7 +140,7 @@ class tasklist extends rcube_plugin
}
// add 'Create event' item to message menu
- if ($this->api->output->type == 'html' && $_GET['_rel'] != 'task') {
+ if ($this->api->output->type == 'html' && (empty($_GET['_rel']) || $_GET['_rel'] != 'task')) {
$this->api->add_content(html::tag('li', array('role' => 'menuitem'),
$this->api->output->button(array(
'command' => 'tasklist-create-from-mail',
@@ -155,7 +156,7 @@ class tasklist extends rcube_plugin
}
}
- if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
+ if (!$this->rc->output->ajax_call && empty($this->rc->output->env['framed'])) {
$this->load_ui();
$this->ui->init();
}
@@ -181,7 +182,7 @@ class tasklist extends rcube_plugin
*/
private function load_driver()
{
- if (is_object($this->driver)) {
+ if (!empty($this->driver)) {
return;
}
@@ -209,15 +210,16 @@ class tasklist extends rcube_plugin
// 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']))
+ 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'];
+ $temp_id = !empty($rec['tempid']) ? $rec['tempid'] : null;
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
@@ -514,7 +516,7 @@ class tasklist extends rcube_plugin
}
// send out notifications
- if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) {
+ if ($success && !empty($rec['_notify']) && ($rec['attendees'] || $oldrec['attendees'])) {
// make sure we have the complete record
$task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);
@@ -528,7 +530,7 @@ class tasklist extends rcube_plugin
}
}
- if ($success && $rec['_reportpartstat'] && $rec['_reportpartstat'] != 'NEEDS-ACTION') {
+ if ($success && !empty($rec['_reportpartstat']) && $rec['_reportpartstat'] != 'NEEDS-ACTION') {
// get the full record after update
if (!$task) {
$task = $this->driver->get_task($rec);
@@ -556,7 +558,7 @@ class tasklist extends rcube_plugin
$this->rc->output->command('plugin.unlock_saving', $success);
if ($refresh) {
- if ($refresh['id']) {
+ if (!empty($refresh['id'])) {
$this->encode_task($refresh);
}
else if (is_array($refresh)) {
@@ -575,8 +577,8 @@ class tasklist extends rcube_plugin
*/
private function load_itip()
{
- if (!$this->itip) {
- require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
+ if (empty($this->itip)) {
+ require_once __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'));
@@ -591,7 +593,7 @@ class tasklist extends rcube_plugin
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
- if ($rec['raw']) {
+ if (!empty($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;
@@ -675,7 +677,7 @@ class tasklist extends rcube_plugin
}
// convert the submitted alarm values
- if ($rec['valarms']) {
+ if (!empty($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)
@@ -701,7 +703,7 @@ class tasklist extends rcube_plugin
// 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']) {
+ if (!empty($rec['recurrence']['COUNT'])) {
$engine = libcalendaring::get_recurrence();
$engine->init($rec['recurrence'], $refdate);
if ($until = $engine->end()) {
@@ -717,7 +719,7 @@ class tasklist extends rcube_plugin
$attachments = array();
$taskid = $rec['id'];
- if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
+ if (!empty($_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'])) {
@@ -736,12 +738,15 @@ class tasklist extends rcube_plugin
}
// convert invalid data
- if (isset($rec['attendees']) && !is_array($rec['attendees']))
+ if (isset($rec['attendees']) && !is_array($rec['attendees'])) {
$rec['attendees'] = array();
+ }
- foreach ((array)$rec['attendees'] as $i => $attendee) {
- if (is_string($attendee['rsvp'])) {
- $rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
+ if (!empty($rec['attendees'])) {
+ foreach ((array) $rec['attendees'] as $i => $attendee) {
+ if (is_string($attendee['rsvp'])) {
+ $rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
+ }
}
}
@@ -1000,7 +1005,7 @@ class tasklist extends rcube_plugin
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
- if (!$list['_reload']) {
+ if (empty($list['_reload'])) {
$this->load_ui();
$list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
$list += (array)$jsenv[$insert_id];
@@ -1048,7 +1053,7 @@ class tasklist extends rcube_plugin
$results[] = $prop;
}
// report more results available
- if ($this->driver->search_more_results) {
+ if (!empty($this->driver->search_more_results)) {
$this->rc->output->show_message('autocompletemore', 'notice');
}
@@ -1056,10 +1061,12 @@ class tasklist extends rcube_plugin
return;
}
- if ($success)
+ if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
- else
+ }
+ else {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
+ }
$this->rc->output->command('plugin.unlock_saving');
}
@@ -1074,8 +1081,9 @@ class tasklist extends rcube_plugin
}
else {
foreach ($this->driver->get_lists() as $list) {
- if ($list['active'])
+ if (!empty($list['active'])) {
$lists[] = $list['id'];
+ }
}
}
$counts = $this->driver->count_tasks($lists);
@@ -1161,7 +1169,7 @@ class tasklist extends rcube_plugin
$data = $this->task_tree = $this->task_titles = array();
foreach ($records as $rec) {
- if ($rec['parent_id']) {
+ if (!empty($rec['parent_id'])) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
@@ -1224,18 +1232,20 @@ class tasklist extends rcube_plugin
}
}
- if ($rec['valarms']) {
+ if (!empty($rec['valarms'])) {
$rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']);
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
}
- if ($rec['recurrence']) {
+ if (!empty($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']);
+ if (!empty($rec['attachments'])) {
+ foreach ((array) $rec['attachments'] as $k => $attachment) {
+ $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
+ }
}
// convert link URIs references into structs
@@ -1283,11 +1293,13 @@ class tasklist extends rcube_plugin
{
$rec['_depth'] = 0;
$parent_titles = array();
- $parent_id = $this->task_tree[$rec['id']];
+ $parent_id = isset($this->task_tree[$rec['id']]) ? $this->task_tree[$rec['id']] : null;
while ($parent_id) {
$rec['_depth']++;
- array_unshift($parent_titles, $this->task_titles[$parent_id]);
- $parent_id = $this->task_tree[$parent_id];
+ if (isset($this->task_titles[$parent_id])) {
+ array_unshift($parent_titles, $this->task_titles[$parent_id]);
+ }
+ $parent_id = isset($this->task_tree[$parent_id]) ? $this->task_tree[$parent_id] : null;
}
if (count($parent_titles)) {
@@ -1702,7 +1714,7 @@ class tasklist extends rcube_plugin
header("Content-Disposition: inline; filename=\"". $plugin['filename'] ."\"");
$this->get_ical()->export($plugin['result'], '', true,
- $plugins['attachments'] ? array($this->driver, 'get_attachment_body') : null);
+ !empty($plugin['attachments']) ? array($this->driver, 'get_attachment_body') : null);
exit;
}
@@ -1927,7 +1939,7 @@ class tasklist extends rcube_plugin
*/
public function mail_message_load($p)
{
- if (!$p['object']->headers->others['x-kolab-type']) {
+ if (empty($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);
@@ -1950,7 +1962,7 @@ class tasklist extends rcube_plugin
*/
public function get_ical()
{
- if (!$this->ical) {
+ if (empty($this->ical)) {
$this->ical = libcalendaring::get_ical();
}
@@ -2442,7 +2454,7 @@ class tasklist extends rcube_plugin
if ($task['flagged']) {
$object['priority'] = 1;
}
- else if (!$task['priority']) {
+ else if (empty($task['priority'])) {
$object['priority'] = 0;
}
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index f4c262ec..249cfe7c 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -83,8 +83,9 @@ class tasklist_ui
// get user identity to create default attendee
foreach ($this->rc->user->list_emails() as $rec) {
- if (!$identity)
+ if (empty($identity)) {
$identity = $rec;
+ }
$identity['emails'][] = $rec['email'];
$settings['identities'][$rec['identity_id']] = $rec['email'];
@@ -184,14 +185,15 @@ class tasklist_ui
$html = '';
foreach ((array)$lists as $id => $prop) {
- if ($attrib['activeonly'] && !$prop['active'])
- continue;
+ if (!empty($attrib['activeonly']) && empty($prop['active'])) {
+ continue;
+ }
$html .= html::tag('li', array(
'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
- 'class' => $prop['group'],
+ 'class' => isset($prop['group']) ? $prop['group'] : null,
),
- $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly'])
+ $this->tasklist_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']))
);
}
}
@@ -241,7 +243,7 @@ class tasklist_ui
public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false)
{
// enrich list properties with settings from the driver
- if (!$prop['virtual']) {
+ if (empty($prop['virtual'])) {
unset($prop['user_id']);
$prop['alarms'] = $this->plugin->driver->alarms;
$prop['undelete'] = $this->plugin->driver->undelete;
@@ -253,17 +255,27 @@ class tasklist_ui
}
$classes = array('tasklist');
- $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
- html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : '');
+ $title = '';
- if ($prop['virtual'])
+ if (!empty($prop['title'])) {
+ $title = $prop['title'];
+ }
+ else if (empty($prop['listname']) || $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25) {
+ html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET);
+ }
+
+ if (!empty($prop['virtual'])) {
$classes[] = 'virtual';
- else if (!$prop['editable'])
+ }
+ else if (empty($prop['editable'])) {
$classes[] = 'readonly';
- if ($prop['subscribed'])
+ }
+ if (!empty($prop['subscribed'])) {
$classes[] = 'subscribed';
- if ($prop['class'])
+ }
+ if (!empty($prop['class'])) {
$classes[] = $prop['class'];
+ }
if (!$activeonly || $prop['active']) {
$label_id = 'tl:' . $id;
@@ -277,9 +289,10 @@ class tasklist_ui
));
return html::div(join(' ', $classes),
- html::a(array('class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
- ($prop['virtual'] ? '' : $chbox . html::span('actions',
- ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '')
+ html::a(array('class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id),
+ !empty($prop['listname']) ? $prop['listname'] : $prop['name']) .
+ (!empty($prop['virtual']) ? '' : $chbox . html::span('actions',
+ (!empty($prop['removable']) ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '')
. html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'), ' ')
. (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
)
@@ -319,15 +332,18 @@ class tasklist_ui
$select = new html_select($attrib);
$default = null;
- foreach ((array) $attrib['extra'] as $id => $name) {
- $select->add($name, $id);
+ if (!empty($attrib['extra'])) {
+ foreach ((array) $attrib['extra'] as $id => $name) {
+ $select->add($name, $id);
+ }
}
- foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) {
- if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) {
+ foreach ((array) $this->plugin->driver->get_lists() as $id => $prop) {
+ if (!empty($prop['editable']) || strpos($prop['rights'], 'i') !== false) {
$select->add($prop['name'], $id);
- if (!$default || $prop['default'])
+ if (!$default || !empty($prop['default'])) {
$default = $id;
+ }
}
}
@@ -421,7 +437,12 @@ class tasklist_ui
$attrib += array('id' => 'rcmtasktagsedit');
$this->register_gui_object('edittagline', $attrib['id']);
- $input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex']));
+ $input = new html_inputfield(array(
+ 'name' => 'tags[]',
+ 'class' => 'tag',
+ 'size' => !empty($attrib['size']) ? $attrib['size'] : null,
+ 'tabindex' => isset($attrib['tabindex']) ? $attrib['tabindex'] : null,
+ ));
unset($attrib['tabindex']);
return html::div($attrib, $input->show(''));
}
@@ -461,9 +482,21 @@ class tasklist_ui
*/
function attendees_form($attrib = array())
{
- $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => $attrib['size'], 'class' => 'form-control'));
- $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment',
- 'rows' => 4, 'cols' => 55, 'title' => $this->plugin->gettext('itipcommenttitle'), 'class' => 'form-control'));
+ $input = new html_inputfield(array(
+ 'name' => 'participant',
+ 'id' => 'edit-attendee-name',
+ 'size' => !empty($attrib['size']) ? $attrib['size'] : null,
+ 'class' => 'form-control'
+ ));
+
+ $textarea = new html_textarea(array(
+ 'name' => 'comment',
+ 'id' => 'edit-attendees-comment',
+ 'rows' => 4,
+ 'cols' => 55,
+ 'title' => $this->plugin->gettext('itipcommenttitle'),
+ 'class' => 'form-control'
+ ));
return html::div($attrib,
html::div('form-searchbar', $input->show() . " " .
@@ -488,7 +521,7 @@ class tasklist_ui
*/
function tasks_import_form($attrib = array())
{
- if (!$attrib['id']) {
+ if (empty($attrib['id'])) {
$attrib['id'] = 'rcmImportForm';
}
@@ -503,7 +536,7 @@ class tasklist_ui
'id' => 'importfile',
'type' => 'file',
'name' => '_data',
- 'size' => $attrib['uploadfieldsize'],
+ 'size' => !empty($attrib['uploadfieldsize']) ? $attrib['uploadfieldsize'] : null,
'accept' => $accept
));
@@ -537,11 +570,11 @@ class tasklist_ui
*/
function tasks_export_form($attrib = array())
{
- if (!$attrib['id']) {
+ if (empty($attrib['id'])) {
$attrib['id'] = 'rcmTaskExportForm';
}
- $html .= html::div('form-section form-group row',
+ $html = html::div('form-section form-group row',
html::label(array('for' => 'task-export-list', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('list'))
. html::div('col-sm-8', $this->tasklist_select(array(
'name' => 'source',