Page MenuHomePhorge

No OneTemporary

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 415c8e4e..75810b6c 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,1222 +1,1225 @@
<?php
/**
* Kolab driver for the Calendar plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require_once(dirname(__FILE__) . '/kolab_calendar.php');
class kolab_driver extends calendar_driver
{
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $alarm_types = array('DISPLAY');
public $categoriesimmutable = true;
private $rc;
private $cal;
private $calendars;
private $has_writeable = false;
private $freebusy_trigger = false;
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->_read_calendars();
$this->cal->register_action('push-freebusy', array($this, 'push_freebusy'));
$this->cal->register_action('calendar-acl', array($this, 'calendar_acl'));
$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;
}
}
/**
* 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'));
$this->calendars = array();
foreach ($folders as $folder) {
$calendar = new kolab_calendar($folder->name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
if (!$calendar->readonly)
$this->has_writeable = true;
}
return $this->calendars;
}
/**
* Get a list of available calendars from this source
*
* @param bool $active Return only active calendars
* @param bool $personal Return only personal calendars
*
* @return array List of calendars
*/
public function list_calendars($active = false, $personal = false)
{
// 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);
$this->_read_calendars();
}
}
$calendars = $this->filter_calendars(false, $active, $personal);
$names = array();
foreach ($calendars as $id => $cal) {
$name = kolab_storage::folder_displayname($cal->get_name(), $names);
$calendars[$id] = array(
'id' => $cal->id,
'name' => $name,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
'default' => $cal->storage->default,
'active' => $cal->storage->is_active(),
'owner' => $cal->get_owner(),
'children' => true, // TODO: determine if that folder indeed has child folders
);
}
return $calendars;
}
/**
* Get list of calendars according to specified filters
*
* @param bool $writeable Return only writeable calendars
* @param bool $active Return only active calendars
* @param bool $personal Return only personal calendars
*
* @return array List of calendars
*/
protected function filter_calendars($writeable = false, $active = false, $personal = false)
{
$calendars = array();
$plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array(
'list' => $this->calendars, 'calendars' => $calendars,
'writeable' => $writeable, 'active' => $active, 'personal' => $personal,
));
if ($plugin['abort']) {
return $plugin['calendars'];
}
foreach ($this->calendars as $cal) {
if (!$cal->ready) {
continue;
}
if ($writeable && $cal->readonly) {
continue;
}
if ($active && !$cal->storage->is_active()) {
continue;
}
if ($personal && $cal->get_namespace() != 'personal') {
continue;
}
$calendars[$cal->id] = $cal;
}
return $calendars;
}
/**
* 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->calendars[$prop['id']])) {
$prop['oldname'] = $cal->get_realname();
$newfolder = kolab_storage::folder_update($prop);
if ($newfolder === false) {
$this->last_error = $this->cal->gettext(kolab_storage::$last_error);
return false;
}
// create ID
$id = kolab_storage::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
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 true;
}
return false;
}
/**
* Set active/subscribed state of a calendar
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
return $cal->storage->activate($prop['active']);
}
return false;
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::remove_calendar()
*/
public function remove_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$folder = $cal->get_realname();
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']]);
$this->rc->user->save_prefs($prefs);
return true;
}
else
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* 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, $writeable = false, $active = false, $personal = false)
{
if (is_array($event)) {
$id = $event['id'] ? $event['id'] : $event['uid'];
$cal = $event['calendar'];
}
else {
$id = $event;
}
if ($cal) {
if ($storage = $this->calendars[$cal]) {
return $storage->get_event($id);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->filter_calendars($writeable, $active, $personal) as $calendar) {
if ($result = $calendar->get_event($id)) {
return $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;
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
if ($storage = $this->calendars[$cid]) {
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $idx => $attachment) {
// we'll read file contacts into memory, Horde/Kolab classes does the same
// So we cannot save memory, rcube_imap class can do this better
$event['attachments'][$idx]['content'] = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
}
}
$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));
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)
{
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->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
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->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
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)
{
$success = false;
$savemode = $event['_savemode'];
if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $savemode;
$savemode = 'all';
$master = $event;
$this->rc->session->remove('calendar_restore_event_data');
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
$savemode = $event['_savemode'];
}
// removing an exception instance
if ($event['recurrence_id']) {
$i = $event['_instance'] - 1;
if (!empty($master['recurrence']['EXCEPTIONS'][$i])) {
unset($master['recurrence']['EXCEPTIONS'][$i]);
}
}
switch ($savemode) {
case 'current':
$_SESSION['calendar_restore_event_data'] = $master;
// removing the first instance => just move to next occurence
if ($master['id'] == $event['id']) {
$recurring = reset($storage->_get_recurring_events($event, $event['start'], null, $event['id'].'-1'));
// no future instances found: delete the master event (bug #1677)
if (!$recurring['start']) {
$success = $storage->delete_event($master, $force);
break;
}
$master['start'] = $recurring['start'];
$master['end'] = $recurring['end'];
if ($master['recurrence']['COUNT'])
$master['recurrence']['COUNT']--;
}
else { // add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
}
$success = $storage->update_event($master);
break;
case 'future':
if ($master['id'] != $event['id']) {
$_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();
$success = $storage->update_event($master);
break;
}
default: // 'all' is default
$success = $storage->delete_event($master, $force);
break;
}
}
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Restore a single deleted event
*
* @param array Hash array with event properties:
* id: Event identifier
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
if ($storage = $this->calendars[$event['calendar']]) {
if (!empty($_SESSION['calendar_restore_event_data']))
$success = $storage->update_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', 'source' => $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->calendars[$event['calendar']]))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
if (!($fromcalendar = $this->calendars[$event['_fromcalendar']]))
return false;
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($event['id'], $storage->get_realname()))
return false;
$fromcalendar = $storage;
}
}
else
$fromcalendar = $storage;
$success = false;
$savemode = 'all';
$attachments = array();
$old = $master = $fromcalendar->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;
}
// delete existing attachment(s)
if (!empty($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
if (!empty($old['attachments'])) {
foreach ($old['attachments'] as $idx => $att) {
if ($att['id'] == $attachment) {
$old['attachments'][$idx]['_deleted'] = true;
}
}
}
}
unset($event['deleted_attachments']);
}
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
// skip entries without content (could be existing ones)
if (!$attachment['data'] && !$attachment['path'])
continue;
$attachments[] = array(
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'content' => $attachment['data'],
'path' => $attachment['path'],
);
}
}
$event['attachments'] = array_merge((array)$old['attachments'], $attachments);
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
$savemode = $event['_savemode'];
}
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'])
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
// copy attachment data to new event
foreach ((array)$event['attachments'] as $idx => $attachment) {
if (!$attachment['data'])
$attachment['data'] = $fromcalendar->get_attachment_body($attachment['id'], $event);
}
$success = $storage->insert_event($event);
break;
case 'future':
case 'current':
// recurring instances shall not store recurrence rules
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
// remove some internal properties which should not be saved
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']);
// save properties to a recurrence exception instance
if ($old['recurrence_id']) {
$i = $old['_instance'] - 1;
if (!empty($master['recurrence']['EXCEPTIONS'][$i])) {
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$success = $storage->update_event($master, $old['id']);
break;
}
}
// save as new exception to master event
$master['recurrence']['EXCEPTIONS'][] = $event;
$success = $storage->update_event($master);
break;
default: // 'all' is default
$event['id'] = $master['id'];
$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 = $old['end']->format('U') - $old['start']->format('U');
$new_start_date = $event['start']->format('Y-m-d');
$new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
$new_duration = $event['end']->format('U') - $event['start']->format('U');
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start']->add($old['start']->diff($event['start']));
$event['end'] = clone $event['start'];
$event['end']->add(new DateInterval('PT'.$new_duration.'S'));
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
if ($old_start_date != $new_start_date) {
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 ($event['start'] == $old['start'] && $event['end'] == $old['end']) {
$event['start'] = $master['start'];
$event['end'] = $master['end'];
}
+ // unset _dateonly flags in (cached) date objects
+ unset($event['start']->_dateonly, $event['end']->_dateonly);
+
$success = $storage->update_event($event);
break;
}
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* 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 Strip virtual events (optional)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$events = $categories = array();
foreach (array_keys($this->calendars) as $cid) {
if ($calendars && !in_array($cid, $calendars))
continue;
$events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual));
$categories += $this->calendars[$cid]->categories;
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
if ($newcats = array_diff(array_map('strtolower', array_keys($categories)), array_map('strtolower', array_keys($old_categories)))) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
return $events;
}
/**
* 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;
$events = array();
$query = array(array('tags', '=', 'x-has-alarms'));
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'] <= $time && $alarm['action'] == 'DISPLAY') {
$id = $e['id'];
$events[$id] = $e;
$events[$id]['notifyat'] = $alarm['time'];
}
}
}
// get alarm information stored in local database
if (!empty($events)) {
$event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events));
$result = $this->rc->db->query(sprintf(
"SELECT * FROM kolab_alarms
WHERE event_id IN (%s) AND user_id=?",
join(',', $event_ids),
$this->rc->db->now()
),
$this->rc->user->ID
);
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['event_id']] = $e;
}
}
$alarms = array();
foreach ($events as $id => $e) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $e['notifyat'];
if ($notifyat <= $time)
$alarms[] = $e;
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($event_id, $snooze = 0)
{
// delete old alarm entry
$this->rc->db->query(
"DELETE FROM kolab_alarms
WHERE event_id=? AND user_id=?",
$event_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 kolab_alarms
(event_id, user_id, dismissed, notifyat)
VALUES(?, ?, ?, ?)",
$event_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->calendars[$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->calendars[$event['calendar']]))
return false;
$event = $storage->get_event($event['id']);
if ($event && !empty($event['attachments'])) {
foreach ($event['attachments'] as $att) {
if ($att['id'] == $id) {
return $att;
}
}
}
return null;
}
/**
* Get attachment body
* @see calendar_driver::get_attachment_body()
*/
public function get_attachment_body($id, $event)
{
if (!($cal = $this->calendars[$event['calendar']]))
return false;
return $cal->storage->get_attachment($event['id'], $id);
}
/**
* 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);
}
/**
* 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;
}
}
}
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 = get_input_value('source', RCUBE_INPUT_GPC);
if (!($cal = $this->calendars[$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;
}
/**
* 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)
{
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
}
else {
$path_imap = '';
}
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
// 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'] = Q(str_replace($delim, ' &raquo; ', kolab_storage::object_name($folder)))
. $input_name->show($folder);
}
// calendar name (default field)
$form['props']['fieldsets']['location'] = array(
'name' => $this->rc->gettext('location'),
'content' => array(
'name' => $formfields['name']
),
);
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'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// calendar color (default field)
$form['props']['fieldsets']['settings'] = array(
'name' => $this->rc->gettext('settings'),
'content' => array(
'color' => $formfields['color'],
'showalarms' => $formfields['showalarms'],
),
);
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->cal->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)),
'width' => '100%',
'height' => 350,
'border' => 0,
'style' => 'border:0'),
''),
);
}
$this->form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$this->form_html .= $hiddenfield->show() . "\n";
}
}
// Create form output
foreach ($form as $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = $this->get_form_part($fieldset);
if ($subcontent) {
$content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
}
}
}
else {
$content = $this->get_form_part($tab);
}
if ($content) {
$this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
// Parse form template for skin-dependent stuff
$this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html'));
return $this->rc->output->parse('calendar.kolabform', false, false);
}
/**
* Handler for template object
*/
public function calendar_form_html()
{
return $this->form_html;
}
/**
* Helper function used in calendar_form_content(). Creates a part of the form.
*/
private function get_form_part($form)
{
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $form['content'];
}
return $content;
}
/**
* Handler to render ACL form for a calendar folder
*/
public function calendar_acl()
{
$this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form'));
$this->rc->output->send('calendar.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function calendar_acl_form()
{
$calid = get_input_value('_id', RCUBE_INPUT_GPC);
if ($calid && ($cal = $this->calendars[$calid])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
// Allow plugins to modify the form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('calendar_form_kolab',
array('form' => $form, 'options' => $options, 'name' => $folder));
}
if (!$plugin['form']['sharing']['content'])
$plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights'));
return $plugin['form']['sharing']['content'];
}
/**
* Return a (limited) list of color values to be used for calendar and category coloring
*
* @return mixed List for colors as hex values or false if no presets should be shown
*/
public function get_color_values()
{
// selection from http://msdn.microsoft.com/en-us/library/aa358802%28v=VS.85%29.aspx
return array('000000','006400','2F4F4F','800000','808000','008000',
'008080','000080','800080','4B0082','191970','8B0000','008B8B',
'00008B','8B008B','556B2F','8B4513','228B22','6B8E23','2E8B57',
'B8860B','483D8B','A0522D','0000CD','A52A2A','00CED1','696969',
'20B2AA','9400D3','B22222','C71585','3CB371','D2691E','DC143C',
'DAA520','00FA9A','4682B4','7CFC00','9932CC','FF0000','FF4500',
'FF8C00','FFA500','FFD700','FFFF00','9ACD32','32CD32','00FF00',
'00FF7F','00FFFF','5F9EA0','00BFFF','0000FF','FF00FF','808080',
'708090','CD853F','8A2BE2','778899','FF1493','48D1CC','1E90FF',
'40E0D0','4169E1','6A5ACD','BDB76B','BA55D3','CD5C5C','ADFF2F',
'66CDAA','FF6347','8FBC8B','DA70D6','BC8F8F','9370DB','DB7093',
'FF7F50','6495ED','A9A9A9','F4A460','7B68EE','D2B48C','E9967A',
'DEB887','FF69B4','FA8072','F08080','EE82EE','87CEEB','FFA07A',
'F0E68C','DDA0DD','90EE90','7FFFD4','C0C0C0','87CEFA','B0C4DE',
'98FB98','ADD8E6','B0E0E6','D8BFD8','EEE8AA','AFEEEE','D3D3D3',
'FFDEAD');
}
}
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index f64a8311..c23d40b1 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -1,895 +1,896 @@
<?php
/**
* iCalendar functions for the libcalendaring plugin
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use \Sabre\VObject;
// load Sabre\VObject classes
if (!class_exists('\Sabre\VObject\Reader')) {
require_once __DIR__ . '/lib/Sabre/VObject/includes.php';
}
/**
* Class to parse and build vCalendar (iCalendar) files
*
* Uses the SabreTooth VObject library, version 2.1.
*
* Download from https://github.com/fruux/sabre-vobject/archive/2.1.0.zip
* and place the lib files in this plugin's lib directory
*
*/
class libvcalendar
{
private $timezone;
private $attach_uri = null;
private $prodid = '-//Roundcube//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE', 'cutype' => 'CUTYPE', 'rsvp' => 'RSVP');
public $method;
public $agent = '';
public $objects = array();
public $freebusy = array();
/**
* Default constructor
*/
function __construct($tz = null)
{
$this->timezone = $tz;
$this->prodid = '-//Roundcube//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
}
/**
* Setter for timezone information
*/
public function set_timezone($tz)
{
$this->timezone = $tz;
}
/**
* Setter for URI template for attachment links
*/
public function set_attach_uri($uri)
{
$this->attach_uri = $uri;
}
/**
* Setter for a custom PRODID attribute
*/
public function set_prodid($prodid)
{
$this->prodid = $prodid;
}
/**
* Setter for a user-agent string to tweak input/output accordingly
*/
public function set_agent($agent)
{
$this->agent = $agent;
}
/**
* Free resources by clearing member vars
*/
public function reset()
{
$this->method = '';
$this->objects = array();
}
/**
* Import events from iCalendar format
*
* @param string vCalendar input
* @param string Input charset (from envelope)
* @param boolean True if parsing exceptions should be forwarded to the caller
* @return array List of events extracted from the input
*/
public function import($vcal, $charset = 'UTF-8', $forward_exceptions = false)
{
// TODO: convert charset to UTF-8 if other
try {
$vobject = VObject\Reader::read($vcal, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobject)
return $this->import_from_vobject($vobject);
}
catch (Exception $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "iCal data parse error: " . $e->getMessage()),
true, false);
if ($forward_exceptions) {
throw $e;
}
}
return array();
}
/**
* Read iCalendar events from a file
*
* @param string File path to read from
* @param string Input charset (from envelope)
* @param boolean True if parsing exceptions should be forwarded to the caller
* @return array List of events extracted from the file
*/
public function import_from_file($filepath, $charset = 'UTF-8', $forward_exceptions = false)
{
$this->objects = array();
$fp = fopen($filepath, 'r');
// check file content first
$begin = fread($fp, 1024);
if (!preg_match('/BEGIN:VCALENDAR/i', $begin)) {
return $this->objects;
}
fclose($fp);
return $this->import(file_get_contents($filepath), $charset, $forward_exceptions);
}
/**
* Import objects from an already parsed Sabre\VObject\Component object
*
* @param object Sabre\VObject\Component to read from
* @return array List of events extracted from the file
*/
public function import_from_vobject($vobject)
{
$this->objects = $this->freebusy = $seen = array();
if ($vobject->name == 'VCALENDAR') {
$this->method = strval($vobject->METHOD);
$this->agent = strval($vobject->PRODID);
foreach ($vobject->getBaseComponents() as $ve) {
if ($ve->name == 'VEVENT' || $ve->name == 'VTODO') {
// convert to hash array representation
$object = $this->_to_array($ve);
if (!$seen[$object['uid']]++) {
// parse recurrence exceptions
if ($object['recurrence']) {
foreach ($vobject->children as $i => $component) {
if ($component->name == 'VEVENT' && isset($component->{'RECURRENCE-ID'})) {
$object['recurrence']['EXCEPTIONS'][] = $this->_to_array($component);
}
}
}
$this->objects[] = $object;
}
}
else if ($ve->name == 'VFREEBUSY') {
$this->objects[] = $this->_parse_freebusy($ve);
}
}
}
return $this->objects;
}
/**
* Getter for free-busy periods
*/
public function get_busy_periods()
{
$out = array();
foreach ((array)$this->freebusy['periods'] as $period) {
if ($period[2] != 'FREE') {
$out[] = $period;
}
}
return $out;
}
/**
* Helper method to determine whether the connected client is an Apple device
*/
private function is_apple()
{
return stripos($this->agent, 'Apple') !== false
|| stripos($this->agent, 'Mac OS X') !== false
|| stripos($this->agent, 'iOS/') !== false;
}
/**
* Convert the given VEvent object to a libkolab compatible array representation
*
* @param object Vevent object to convert
* @return array Hash array with object properties
*/
private function _to_array($ve)
{
$event = array(
'uid' => strval($ve->UID),
'title' => strval($ve->SUMMARY),
'_type' => $ve->name == 'VTODO' ? 'task' : 'event',
// set defaults
'priority' => 0,
'attendees' => array(),
'x-custom' => array(),
);
// Catch possible exceptions when date is invalid (Bug #2144)
// We can skip these fields, they aren't critical
foreach (array('CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed') as $attr => $field) {
try {
if (!$event[$field] && $ve->{$attr}) {
$event[$field] = $ve->{$attr}->getDateTime();
}
} catch (Exception $e) {}
}
// map other attributes to internal fields
$_attendees = array();
foreach ($ve->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
switch ($prop->name) {
case 'DTSTART':
case 'DTEND':
case 'DUE':
$propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'DUE' => 'due');
$event[$propmap[$prop->name]] = self::convert_datetime($prop);
break;
case 'TRANSP':
$event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy';
break;
case 'STATUS':
if ($prop->value == 'TENTATIVE')
$event['free_busy'] = 'tentative';
else if ($prop->value == 'CANCELLED')
$event['cancelled'] = true;
else if ($prop->value == 'COMPLETED')
$event['complete'] = 100;
break;
case 'PRIORITY':
if (is_numeric($prop->value))
$event['priority'] = $prop->value;
break;
case 'RRULE':
$params = array();
// parse recurrence rule attributes
foreach (explode(';', $prop->value) as $par) {
list($k, $v) = explode('=', $par);
$params[$k] = $v;
}
if ($params['UNTIL'])
$params['UNTIL'] = date_create($params['UNTIL']);
if (!$params['INTERVAL'])
$params['INTERVAL'] = 1;
$event['recurrence'] = $params;
break;
case 'EXDATE':
$event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], (array)self::convert_datetime($prop));
break;
case 'RECURRENCE-ID':
// $event['recurrence_id'] = self::convert_datetime($prop);
break;
case 'RELATED-TO':
if ($prop->offsetGet('RELTYPE') == 'PARENT') {
$event['parent_id'] = $prop->value;
}
break;
case 'SEQUENCE':
$event['sequence'] = intval($prop->value);
break;
case 'PERCENT-COMPLETE':
$event['complete'] = intval($prop->value);
break;
case 'LOCATION':
case 'DESCRIPTION':
if ($this->is_apple()) {
$event[strtolower($prop->name)] = str_replace('\,', ',', $prop->value);
break;
}
// else: fall through
case 'URL':
$event[strtolower($prop->name)] = $prop->value;
break;
case 'CATEGORY':
case 'CATEGORIES':
$event['categories'] = $prop->getParts();
break;
case 'CLASS':
case 'X-CALENDARSERVER-ACCESS':
$event['sensitivity'] = strtolower($prop->value);
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
if ($prop->value == 'OOF')
$event['free_busy'] == 'outofoffice';
else if (in_array($prop->value, array('FREE', 'BUSY', 'TENTATIVE')))
$event['free_busy'] = strtolower($prop->value);
break;
case 'ATTENDEE':
case 'ORGANIZER':
$params = array();
foreach ($prop->parameters as $param) {
switch ($param->name) {
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
default: $params[$param->name] = $param->value; break;
}
}
$attendee = self::map_keys($params, array_flip($this->attendee_keymap));
$attendee['email'] = preg_replace('/^mailto:/i', '', $prop->value);
if ($prop->name == 'ORGANIZER') {
$attendee['role'] = 'ORGANIZER';
$attendee['status'] = 'ACCEPTED';
$event['organizer'] = $attendee;
}
else if ($attendee['email'] != $event['organizer']['email']) {
$event['attendees'][] = $attendee;
}
break;
case 'ATTACH':
$params = self::parameters_array($prop);
if (substr($prop->value, 0, 4) == 'http' && !strpos($prop->value, ':attachment:')) {
$event['links'][] = $prop->value;
}
else if (strlen($prop->value) && strtoupper($params['VALUE']) == 'BINARY') {
$attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name'));
$attachment['data'] = base64_decode($prop->value);
$attachment['size'] = strlen($attachment['data']);
$event['attachments'][] = $attachment;
}
break;
default:
if (substr($prop->name, 0, 2) == 'X-')
$event['x-custom'][] = array($prop->name, strval($prop->value));
break;
}
}
// check DURATION property if no end date is set
if (empty($event['end']) && $ve->DURATION) {
try {
$duration = new DateInterval(strval($ve->DURATION));
$end = clone $event['start'];
$end->add($duration);
$event['end'] = $end;
}
catch (\Exception $e) {
trigger_error(strval($e), E_USER_WARNING);
}
}
// validate event dates
if ($event['_type'] == 'event') {
// check for all-day dates
if ($event['start']->_dateonly) {
$event['allday'] = true;
}
// shift end-date by one day (except Thunderbird)
if ($event['allday'] && is_object($event['end'])) {
$event['end']->sub(new \DateInterval('PT23H'));
}
// sanity-check and fix end date
if (!empty($event['end']) && $event['end'] < $event['start']) {
$event['end'] = clone $event['start'];
}
}
// make organizer part of the attendees list for compatibility reasons
if (!empty($event['organizer']) && is_array($event['attendees'])) {
array_unshift($event['attendees'], $event['organizer']);
}
// find alarms
if ($valarms = $ve->select('VALARM')) {
$action = 'DISPLAY';
$trigger = null;
$valarm = reset($valarms);
foreach ($valarm->children as $prop) {
switch ($prop->name) {
case 'TRIGGER':
foreach ($prop->parameters as $param) {
if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') {
$trigger = '@' . $prop->getDateTime()->format('U');
}
}
if (!$trigger) {
$trigger = preg_replace('/PT?/', '', $prop->value);
}
break;
case 'ACTION':
$action = $prop->value;
break;
}
}
if ($trigger)
$event['alarms'] = $trigger . ':' . $action;
}
// assign current timezone to event start/end
if ($event['start'] instanceof DateTime) {
if ($this->timezone)
$event['start']->setTimezone($this->timezone);
}
else {
unset($event['start']);
}
if ($event['end'] instanceof DateTime) {
if ($this->timezone)
$event['end']->setTimezone($this->timezone);
}
else {
unset($event['end']);
}
// minimal validation
if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) {
throw new VObject\ParseException('Object validation failed: missing mandatory object properties');
}
return $event;
}
/**
* Parse the given vfreebusy component into an array representation
*/
private function _parse_freebusy($ve)
{
$this->freebusy = array('_type' => 'freebusy', 'periods' => array());
$seen = array();
foreach ($ve->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
switch ($prop->name) {
case 'DTSTART':
case 'DTEND':
$propmap = array('DTSTART' => 'start', 'DTEND' => 'end');
$this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop);
break;
case 'ORGANIZER':
$this->freebusy['organizer'] = preg_replace('/^mailto:/i', '', $prop->value);
break;
case 'FREEBUSY':
// The freebusy component can hold more than 1 value, separated by commas.
$periods = explode(',', $prop->value);
$fbtype = strval($prop['FBTYPE']) ?: 'BUSY';
// skip dupes
if ($seen[$prop->value.':'.$fbtype]++)
continue;
foreach ($periods as $period) {
// Every period is formatted as [start]/[end]. The start is an
// absolute UTC time, the end may be an absolute UTC time, or
// duration (relative) value.
list($busyStart, $busyEnd) = explode('/', $period);
$busyStart = VObject\DateTimeParser::parse($busyStart);
$busyEnd = VObject\DateTimeParser::parse($busyEnd);
if ($busyEnd instanceof \DateInterval) {
$tmp = clone $busyStart;
$tmp->add($busyEnd);
$busyEnd = $tmp;
}
if ($busyEnd && $busyEnd > $busyStart)
$this->freebusy['periods'][] = array($busyStart, $busyEnd, $fbtype);
}
break;
case 'COMMENT':
$this->freebusy['comment'] = $prop->value;
}
}
return $this->freebusy;
}
/**
* Helper method to correctly interpret an all-day date value
*/
public static function convert_datetime($prop)
{
if (empty($prop)) {
return null;
}
else if ($prop instanceof VObject\Property\MultiDateTime) {
$dt = array();
$dateonly = ($prop->getDateType() & VObject\Property\DateTime::DATE);
foreach ($prop->getDateTimes() as $item) {
$item->_dateonly = $dateonly;
$dt[] = $item;
}
}
else if ($prop instanceof VObject\Property\DateTime) {
$dt = $prop->getDateTime();
if ($prop->getDateType() & VObject\Property\DateTime::DATE) {
$dt->_dateonly = true;
}
}
else if ($prop instanceof DateTime) {
$dt = $prop;
}
return $dt;
}
/**
* Create a Sabre\VObject\Property instance from a PHP DateTime object
*
* @param string Property name
* @param object DateTime
*/
public static function datetime_prop($name, $dt, $utc = false, $dateonly = null)
{
$is_utc = $utc || (($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')));
+ $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly;
$vdt = new VObject\Property\DateTime($name);
- $vdt->setDateTime($dt, $dt->_dateonly || $dateonly ? VObject\Property\DateTime::DATE :
+ $vdt->setDateTime($dt, $is_dateonly ? VObject\Property\DateTime::DATE :
($is_utc ? VObject\Property\DateTime::UTC : VObject\Property\DateTime::LOCALTZ));
return $vdt;
}
/**
* Copy values from one hash array to another using a key-map
*/
public static function map_keys($values, $map)
{
$out = array();
foreach ($map as $from => $to) {
if (isset($values[$from]))
$out[$to] = $values[$from];
}
return $out;
}
/**
*
*/
private static function parameters_array($prop)
{
$params = array();
foreach ($prop->parameters as $param) {
$params[strtoupper($param->name)] = $param->value;
}
return $params;
}
/**
* Export events to iCalendar format
*
* @param array Events as array
* @param string VCalendar method to advertise
* @param boolean Directly send data to stdout instead of returning
* @param callable Callback function to fetch attachment contents, false if no attachment export
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
*/
public function export($objects, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
{
$memory_limit = parse_bytes(ini_get('memory_limit'));
$this->method = $method;
// encapsulate in VCALENDAR container
$vcal = VObject\Component::create('VCALENDAR');
$vcal->version = '2.0';
$vcal->prodid = $this->prodid;
$vcal->calscale = 'GREGORIAN';
if (!empty($method)) {
$vcal->METHOD = $method;
}
// TODO: include timezone information
// write vcalendar header
if ($write) {
echo preg_replace('/END:VCALENDAR[\r\n]*$/m', '', $vcal->serialize());
}
foreach ($objects as $object) {
$this->_to_ical($object, !$write?$vcal:false, $get_attachment);
}
if ($write) {
echo "END:VCALENDAR\r\n";
return true;
}
else {
return $vcal->serialize();
}
}
/**
* Build a valid iCal format block from the given event
*
* @param array Hash array with event/task properties from libkolab
* @param object VCalendar object to append event to or false for directly sending data to stdout
* @param callable Callback function to fetch attachment contents, false if no attachment export
* @param object RECURRENCE-ID property when serializing a recurrence exception
*/
private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null)
{
$type = $event['_type'] ?: 'event';
$ve = VObject\Component::create($this->type_component_map[$type]);
$ve->add('UID', $event['uid']);
// set DTSTAMP according to RFC 5545, 3.8.7.2.
$dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime();
$ve->add(self::datetime_prop('DTSTAMP', $dtstamp, true));
// all-day events end the next day
if ($event['allday'] && !empty($event['end'])) {
$event['end'] = clone $event['end'];
$event['end']->add(new \DateInterval('P1D'));
$event['end']->_dateonly = true;
}
if (!empty($event['created']))
$ve->add(self::datetime_prop('CREATED', $event['created'], true));
if (!empty($event['changed']))
$ve->add(self::datetime_prop('LAST-MODIFIED', $event['changed'], true));
if (!empty($event['start']))
- $ve->add(self::datetime_prop('DTSTART', $event['start'], false, $event['allday']));
+ $ve->add(self::datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
if (!empty($event['end']))
- $ve->add(self::datetime_prop('DTEND', $event['end'], false, $event['allday']));
+ $ve->add(self::datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
if (!empty($event['due']))
$ve->add(self::datetime_prop('DUE', $event['due'], false));
if ($recurrence_id)
$ve->add($recurrence_id);
$ve->add('SUMMARY', $event['title']);
if ($event['location'])
$ve->add($this->is_apple() ? new vobject_location_property('LOCATION', $event['location']) : new VObject\Property('LOCATION', $event['location']));
if ($event['description'])
$ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
if ($event['sequence'])
$ve->add('SEQUENCE', $event['sequence']);
if ($event['recurrence'] && !$recurrence_id) {
if ($exdates = $event['recurrence']['EXDATE']) {
unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value
}
$ve->add('RRULE', libcalendaring::to_rrule($event['recurrence']));
// add EXDATEs each one per line (for Thunderbird Lightning)
if ($exdates) {
foreach ($exdates as $ex) {
if ($ex instanceof \DateTime) {
$exd = clone $event['start'];
$exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
$exd->setTimeZone(new \DateTimeZone('UTC'));
$ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
}
}
}
}
if ($event['categories']) {
$cat = VObject\Property::create('CATEGORIES');
$cat->setParts((array)$event['categories']);
$ve->add($cat);
}
if (!empty($event['free_busy']))
$ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
if ($event['priority'])
$ve->add('PRIORITY', $event['priority']);
if ($event['cancelled'])
$ve->add('STATUS', 'CANCELLED');
else if ($event['free_busy'] == 'tentative')
$ve->add('STATUS', 'TENTATIVE');
else if ($event['complete'] == 100)
$ve->add('STATUS', 'COMPLETED');
if (!empty($event['sensitivity']))
$ve->add('CLASS', strtoupper($event['sensitivity']));
if (!empty($event['complete'])) {
$ve->add('PERCENT-COMPLETE', intval($event['complete']));
// Apple iCal required the COMPLETED date to be set in order to consider a task complete
if ($event['complete'] == 100)
$ve->add(self::datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
}
if ($event['alarms']) {
$va = VObject\Component::create('VALARM');
list($trigger, $va->action) = explode(':', $event['alarms']);
$val = libcalendaring::parse_alaram_value($trigger);
$period = $val[1] && preg_match('/[HMS]$/', $val[1]) ? 'PT' : 'P';
if ($val[1]) $va->add('TRIGGER', preg_replace('/^([-+])P?T?(.+)/', "\\1$period\\2", $trigger));
else $va->add('TRIGGER', gmdate('Ymd\THis\Z', $val[0]), array('VALUE' => 'DATE-TIME'));
$ve->add($va);
}
foreach ((array)$event['attendees'] as $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
if (empty($event['organizer']))
$event['organizer'] = $attendee;
}
else if (!empty($attendee['email'])) {
$attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
$ve->add('ATTENDEE', 'mailto:' . $attendee['email'], self::map_keys($attendee, $this->attendee_keymap));
}
}
if ($event['organizer']) {
$ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN')));
}
foreach ((array)$event['url'] as $url) {
if (!empty($url)) {
$ve->add('URL', $url);
}
}
if (!empty($event['parent_id'])) {
$ve->add('RELATED-TO', $event['parent_id'], array('RELTYPE' => 'PARENT'));
}
// export attachments
if (!empty($event['attachments'])) {
foreach ((array)$event['attachments'] as $attach) {
// check available memory and skip attachment export if we can't buffer it
if (is_callable($get_attachment) && $memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
&& $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
continue;
}
// embed attachments using the given callback function
if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'], $event))) {
// embed attachments for iCal
$ve->add('ATTACH',
base64_encode($data),
array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name']));
unset($data); // attempt to free memory
}
// list attachments as absolute URIs
else if (!empty($this->attach_uri)) {
$ve->add('ATTACH',
strtr($this->attach_uri, array(
'{{id}}' => urlencode($attach['id']),
'{{name}}' => urlencode($attach['name']),
'{{mimetype}}' => urlencode($attach['mimetype']),
)),
array('FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI'));
}
}
}
foreach ((array)$event['links'] as $uri) {
$ve->add('ATTACH', $uri);
}
// add custom properties
foreach ((array)$event['x-custom'] as $prop) {
$ve->add($prop[0], $prop[1]);
}
// append to vcalendar container
if ($vcal) {
$vcal->add($ve);
}
else { // serialize and send to stdout
echo $ve->serialize();
}
// append recurrence exceptions
if ($event['recurrence']['EXCEPTIONS']) {
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
$exdate = clone $event['start'];
$exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
$recurrence_id = self::datetime_prop('RECURRENCE-ID', $exdate, true);
// if ($ex['thisandfuture']) // not supported by any client :-(
// $recurrence_id->add('RANGE', 'THISANDFUTURE');
$this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
}
}
}
}
/**
* Override Sabre\VObject\Property that quotes commas in the location property
* because Apple clients treat that property as list.
*/
class vobject_location_property extends VObject\Property
{
/**
* Turns the object back into a serialized blob.
*
* @return string
*/
public function serialize()
{
$str = $this->name;
foreach ($this->parameters as $param) {
$str.=';' . $param->serialize();
}
$src = array(
'\\',
"\n",
',',
);
$out = array(
'\\\\',
'\n',
'\,',
);
$str.=':' . str_replace($src, $out, $this->value);
$out = '';
while (strlen($str) > 0) {
if (strlen($str) > 75) {
$out.= mb_strcut($str, 0, 75, 'utf-8') . "\r\n";
$str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8');
} else {
$out.= $str . "\r\n";
$str = '';
break;
}
}
return $out;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jun 8, 11:47 PM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196723
Default Alt Text
(71 KB)

Event Timeline