From 50569114acdc64e7c7cae1498635d3f821517c30 Mon Sep 17 00:00:00 2001 From: Daniel Lange Date: Mon, 7 Mar 2016 15:53:16 +0100 Subject: Initial commit of the Faster IT roundcube_calendar plugin distribution This includes: * Kolab plugins 3.2.9 (calendar and libcalendaring) * CalDAV driver 3.2.8 * .htaccess files for at least some security * SabreDAV updated to 1.8.12 (Jan 2015 release) * Support for CURLOPT_SSL_* settings to allow self-signed certificates * Small fixes & improved documentation --- calendar/drivers/calendar_driver.php | 819 +++++++++++++++++++++++++++++++++++ 1 file changed, 819 insertions(+) create mode 100644 calendar/drivers/calendar_driver.php (limited to 'calendar/drivers/calendar_driver.php') diff --git a/calendar/drivers/calendar_driver.php b/calendar/drivers/calendar_driver.php new file mode 100644 index 0000000..3202003 --- /dev/null +++ b/calendar/drivers/calendar_driver.php @@ -0,0 +1,819 @@ + + * @author Thomas Bruederli + * + * Copyright (C) 2010, Lazlo Westerhof + * Copyright (C) 2012-2015, Kolab Systems AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +/** + * Struct of an internal event object how it is passed from/to the driver classes: + * + * $event = array( + * 'id' => 'Event ID used for editing', + * 'uid' => 'Unique identifier of this event', + * 'calendar' => 'Calendar identifier to add event to or where the event is stored', + * 'start' => DateTime, // Event start date/time as DateTime object + * 'end' => DateTime, // Event end date/time as DateTime object + * 'allday' => true|false, // Boolean flag if this is an all-day event + * 'changed' => DateTime, // Last modification date of event + * 'title' => 'Event title/summary', + * 'location' => 'Location string', + * 'description' => 'Event description', + * 'url' => 'URL to more information', + * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs + * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY', + * 'INTERVAL' => 1...n, + * 'UNTIL' => DateTime, + * 'COUNT' => 1..n, // number of times + * // + more properties (see http://www.kanzaki.com/docs/ical/recur.html) + * 'EXDATE' => array(), // list of DateTime objects of exception Dates/Times + * 'EXCEPTIONS' => array(), list of event objects which denote exceptions in the recurrence chain + * ), + * 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event + * '_instance' => 'ID of the recurring instance', // identifies an instance within a recurrence chain + * 'categories' => 'Event category', + * 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as + * 'status' => 'TENTATIVE|CONFIRMED|CANCELLED', // event status according to RFC 2445 + * 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest) + * 'sensitivity' => 'public|private|confidential', // Event sensitivity + * 'alarms' => '-15M:DISPLAY', // DEPRECATED Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event) + * 'valarms' => array( // List of reminders (new format), each represented as a hash array: + * array( + * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object + * 'action' => 'DISPLAY|EMAIL|AUDIO', + * 'duration' => 'PT15M', // ISO 8601 period string + * 'repeat' => 0, // number of repetitions + * 'description' => '', // text to display for DISPLAY actions + * 'summary' => '', // message text for EMAIL actions + * 'attendees' => array(), // list of email addresses to receive alarm messages + * ), + * ), + * 'attachments' => array( // List of attachments + * 'name' => 'File name', + * 'mimetype' => 'Content type', + * 'size' => 1..n, // in bytes + * 'id' => 'Attachment identifier' + * ), + * 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated + * 'attendees' => array( // List of event participants + * 'name' => 'Participant name', + * 'email' => 'Participant e-mail address', // used as identifier + * 'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR', + * 'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED' + * 'rsvp' => true|false, + * ), + * + * '_savemode' => 'all|future|current|new', // How changes on recurring event should be handled + * '_notify' => true|false, // whether to notify event attendees about changes + * '_fromcalendar' => 'Calendar identifier where the event was stored before', + * ); + */ + +/** + * Interface definition for calendar driver classes + */ +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 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; + + protected $default_categories = array( + '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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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 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); + } + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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; + } + + /** + * 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); + + /** + * 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); + + /** + * 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 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); + + /** + * (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); + + /** + * 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; + + if (!is_object($event['start']) || !is_a($event['start'], 'DateTime')) + $valid = false; + if (!is_object($event['end']) || !is_a($event['end'], 'DateTime')) + $valid = false; + + 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; + } + + $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'] . '-' . $next_event['_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; + } + } + } + + 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 $rev Revisions: "from:to" + * + * @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, $rev) + { + return false; + } + + /** + * 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) + { + $html = ''; + foreach ($formfields as $field) { + $html .= html::div('form-section', + html::label($field['id'], $field['label']) . + $field['value']); + } + + return $html; + } + + /** + * 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(); + } + + // 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'); + + $events = array(); + $search = mb_strtolower($search); + $rcmail = rcmail::get_instance(); + $cache = $rcmail->get_cache('calendar.birthdays', 'db', 3600); + $cache->expunge(); + + $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; + } + + $abook->set_pagesize(10000); + + // check for cached results + $cache_records = array(); + $cached = $cache->get($source); + + // iterate over (cached) contacts + foreach (($cached ?: $abook->search('*', '', 2, true, true, array('birthday'))) as $contact) { + if (is_array($contact) && !empty($contact['birthday'])) { + try { + if (is_array($contact['birthday'])) + $contact['birthday'] = reset($contact['birthday']); + + $bday = $contact['birthday'] instanceof DateTime ? $contact['birthday'] : + new DateTime($contact['birthday'], new DateTimezone('UTC')); + $birthyear = $bday->format('Y'); + } + catch (Exception $e) { + rcube::raise_error(array( + 'code' => 600, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => 'BIRTHDAY PARSE ERROR: ' . $e), + true, false); + continue; + } + + $display_name = rcube_addressbook::compose_display_name($contact); + $event_title = $rcmail->gettext(array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name)), 'calendar'); + + // add stripped record to cache + if (empty($cached)) { + $cache_records[] = array( + 'ID' => $contact['ID'], + 'name' => $display_name, + 'birthday' => $bday->format('Y-m-d'), + ); + } + + // filter by search term (only name is involved here) + if (!empty($search) && strpos(mb_strtolower($event_title), $search) === false) { + continue; + } + + // quick-and-dirty recurrence computation: just replace the year + $bday->setDate($year, $bday->format('n'), $bday->format('j')); + $bday->setTime(12, 0, 0); + + // 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')); + $year = $year2; + } + + // birthday is within requested range + if ($bday <= $end && $bday >= $start) { + $age = $year - $birthyear; + $event = array( + 'id' => rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $year), + 'calendar' => self::BIRTHDAY_CALENDAR_ID, + 'title' => $event_title, + 'description' => $rcmail->gettext(array('name' => 'birthdayage', 'vars' => array('age' => $age)), 'calendar'), + // Add more contact information to description block? + 'allday' => true, + 'start' => $bday, + 'alarms' => $alarms, + ); + $event['end'] = clone $bday; + $event['end']->add(new DateInterval('PT1H')); + + $events[] = $event; + } + } + } + + // store collected contacts in cache + if (empty($cached)) { + $cache->write($source, $cache_records); + } + } + + return $events; + } + + /** + * 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)); + + $rcmail = rcmail::get_instance(); + + if ($source && $contact_id && ($abook = $rcmail->get_address_book($source))) { + $contact = $abook->get_record($contact_id, true); + + if (is_array($contact) && !empty($contact['birthday'])) { + try { + if (is_array($contact['birthday'])) + $contact['birthday'] = reset($contact['birthday']); + + $bday = $contact['birthday'] instanceof DateTime ? $contact['birthday'] : + new DateTime($contact['birthday'], new DateTimezone('UTC')); + $birthyear = $bday->format('Y'); + } + catch (Exception $e) { + rcube::raise_error(array( + 'code' => 600, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => 'BIRTHDAY PARSE ERROR: ' . $e), + true, false); + + return null; + } + + $display_name = rcube_addressbook::compose_display_name($contact); + $event_title = $rcmail->gettext(array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name)), 'calendar'); + + $event = array( + 'id' => rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $year), + 'uid' => rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $birthyear), + 'calendar' => self::BIRTHDAY_CALENDAR_ID, + 'title' => $event_title, + 'description' => '', + 'allday' => true, + 'start' => $bday, + 'recurrence' => array('FREQ' => 'YEARLY', 'INTERVAL' => 1), + 'free_busy' => 'free', + ); + $event['end'] = clone $bday; + $event['end']->add(new DateInterval('PT1H')); + + return $event; + } + } + + return null; + } + + /** + * 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; + } +} -- cgit v1.2.3