Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256649
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
71 KB
Referenced Files
None
Subscribers
None
View Options
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, ' » ', 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
Details
Attached
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)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment