Page MenuHomePhorge

No OneTemporary

Size
141 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index d4a3436a..e414b871 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,1971 +1,1972 @@
<?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-2014, 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/>.
*/
class kolab_driver extends calendar_driver
{
const INVITATIONS_CALENDAR_PENDING = '--invitation--pending';
const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $alarm_types = array('DISPLAY','AUDIO');
public $categoriesimmutable = true;
private $rc;
private $cal;
private $calendars;
private $has_writeable = false;
private $freebusy_trigger = false;
private $bonnie_api = false;
/**
* Default constructor
*/
public function __construct($cal)
{
$cal->require_plugin('libkolab');
// load helper classes *after* libkolab has been loaded (#3248)
require_once(dirname(__FILE__) . '/kolab_calendar.php');
require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php');
$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;
}
// get configuration for the Bonnie API
if ($bonnie_config = $this->cal->rc->config->get('kolab_bonnie_api', false))
$this->bonnie_api = new kolab_bonnie_api($bonnie_config);
// calendar uses fully encoded identifiers
kolab_storage::$encode_ids = true;
}
/**
* Read available calendars from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type, sorted by namespace/name
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
$this->calendars = array();
foreach ($folders as $folder) {
if ($folder instanceof kolab_storage_folder_user) {
$calendar = new kolab_user_calendar($folder->name, $this->cal);
$calendar->subscriptions = count($folder->children) > 0;
}
else {
$calendar = new kolab_calendar($folder->name, $this->cal);
}
if ($calendar->ready) {
$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
* @param object $tree Reference to hierarchical folder tree object
*
* @return array List of calendars
*/
public function list_calendars($active = false, $personal = false, &$tree = null)
{
// 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();
}
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$folders = $this->filter_calendars(false, $active, $personal);
$calendars = array();
// include virtual folders for a full folder tree
if (!is_null($tree))
$folders = kolab_storage::folder_hierarchy($folders, $tree);
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
$listname = $cal->get_foldername();
$imap_path = explode($delim, $cal->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->calendars[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->calendars[$parent_id]) {
$parent_id = kolab_storage::folder_id($cal->get_parent());
}
// turn a kolab_storage_folder object into a kolab_calendar
if ($cal instanceof kolab_storage_folder) {
$cal = new kolab_calendar($cal->name, $this->cal);
$this->calendars[$cal->id] = $cal;
}
// special handling for user or virtual folders
if ($cal instanceof kolab_storage_folder_user) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => kolab_storage::object_name($fullname),
'listname' => $listname,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'active' => $cal->is_active(),
'title' => $cal->get_owner(),
'owner' => $cal->get_owner(),
'history' => false,
'virtual' => false,
'readonly' => true,
'group' => 'other',
'class' => 'user',
'removable' => true,
);
}
else if ($cal->virtual) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
'virtual' => true,
'readonly' => true,
'group' => $cal->get_namespace(),
'class' => 'folder',
);
}
else {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => $cal->get_namespace(),
'default' => $cal->default,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => true, // TODO: determine if that folder indeed has child folders
'parent' => $parent_id,
'subtype' => $cal->subtype,
'caldavurl' => $cal->get_caldav_url(),
'removable' => !$cal->default,
);
}
if ($cal->subscriptions) {
$calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
}
}
// list virtual calendars showing invitations
if ($this->rc->config->get('kolab_invitation_calendars')) {
foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
$cal = new kolab_invitation_calendar($id, $this->cal);
$this->calendars[$cal->id] = $cal;
if (!$active || $cal->is_active()) {
$calendars[$id] = array(
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_name(),
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => false,
);
if ($id == self::INVITATIONS_CALENDAR_PENDING) {
$calendars[$id]['counts'] = true;
}
if (is_object($tree)) {
$tree->children[] = $cal;
}
}
}
}
// append the virtual birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays', false)) {
$id = self::BIRTHDAY_CALENDAR_ID;
$prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs
if (!$active || $prefs[$id]['active']) {
$calendars[$id] = array(
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
'listname' => $this->cal->gettext('birthdays'),
'color' => $prefs[$id]['color'] ?: '87CEFA',
'active' => (bool)$prefs[$id]['active'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'group' => 'x-birthdays',
'readonly' => true,
'default' => false,
'children' => false,
'history' => false,
);
}
}
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->is_active()) {
continue;
}
if ($personal && $cal->get_namespace() != 'personal') {
continue;
}
$calendars[$cal->id] = $cal;
}
return $calendars;
}
/**
* Get the kolab_calendar instance for the given calendar ID
*
* @param string Calendar identifier (encoded imap folder name)
* @return object kolab_calendar Object nor null if calendar doesn't exist
*/
public function get_calendar($id)
{
// create calendar object if necesary
if (!$this->calendars[$id] && in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
$this->calendars[$id] = new kolab_invitation_calendar($id, $this->cal);
}
else if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
$calendar = kolab_calendar::factory($id, $this->cal);
if ($calendar->ready)
$this->calendars[$calendar->id] = $calendar;
}
return $this->calendars[$id];
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$prop['type'] = 'event';
$prop['active'] = true;
$prop['subscribed'] = true;
$folder = kolab_storage::folder_update($prop);
if ($folder === false) {
$this->last_error = $this->cal->gettext(kolab_storage::$last_error);
return false;
}
// create ID
$id = kolab_storage::folder_id($folder);
// save color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return $id;
}
/**
* Update properties of an existing calendar
*
* @see calendar_driver::edit_calendar()
*/
public function edit_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$id = $cal->update($prop);
}
else {
$id = $prop['id'];
}
// fallback to local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']);
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID)
$prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
else if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if (!empty($prefs['kolab_calendars'][$id]))
$this->rc->user->save_prefs($prefs);
return true;
}
/**
* Set active/subscribed state of a calendar
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) {
$ret = false;
if (isset($prop['permanent']))
$ret |= $cal->storage->subscribe(intval($prop['permanent']));
if (isset($prop['active']))
$ret |= $cal->storage->activate(intval($prop['active']));
// apply to child folders, too
if ($prop['recursive']) {
foreach ((array)kolab_storage::list_folders($cal->storage->name, '*', 'event') as $subfolder) {
if (isset($prop['permanent']))
($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
if (isset($prop['active']))
($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
}
}
return $ret;
}
else {
// save state in local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
$prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active'];
$this->rc->user->save_prefs($prefs);
return true;
}
return false;
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::delete_calendar()
*/
public function delete_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$folder = $cal->get_realname();
// TODO: unsubscribe if no admin rights
if (kolab_storage::folder_delete($folder)) {
// remove color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
$this->rc->user->save_prefs($prefs);
return true;
}
else
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* Search for shared or otherwise not listed calendars the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of calendars
*/
public function search_calendars($query, $source)
{
if (!kolab_storage::setup())
return array();
$this->calendars = array();
$this->search_more_results = false;
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
$calendar = new kolab_calendar($folder->name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
}
}
// find other user's virtual calendars
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit, $count) as $user) {
$calendar = new kolab_user_calendar($user, $this->cal);
$this->calendars[$calendar->id] = $calendar;
// search for calendar folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
$cal = new kolab_calendar($foldername, $this->cal);
$this->calendars[$cal->id] = $cal;
$calendar->subscriptions = true;
}
}
if ($count > $limit) {
$this->search_more_results = true;
}
}
// don't list the birthday calendar
$this->rc->config->set('calendar_contact_birthdays', false);
$this->rc->config->set('kolab_invitation_calendars', false);
return $this->list_calendars();
}
/**
* Fetch a single event
*
* @see calendar_driver::get_event()
* @return array Hash array with event properties, false if not found
*/
public function get_event($event, $writeable = false, $active = false, $personal = false)
{
if (is_array($event)) {
$id = $event['id'] ?: $event['uid'];
$cal = $event['calendar'];
// we're looking for a recurring instance: expand the ID to our internal convention for recurring instanced
if (!$event['id'] && $event['_instance']) {
$id .= '-' . $event['_instance'];
}
}
else {
$id = $event;
}
if ($cal) {
if ($storage = $this->get_calendar($cal)) {
return $storage->get_event($id);
}
// get event from the address books birthday calendar
else if ($cal == self::BIRTHDAY_CALENDAR_ID) {
return $this->get_birthday_event($id);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->filter_calendars($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->get_calendar($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));
$this->freebusy_trigger = false; // disable after first execution (#2355)
}
return $success;
}
return false;
}
/**
* Update an event entry with the given data
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function edit_event($event)
{
return $this->update_event($event);
}
/**
* Extended event editing with possible changes to the argument
*
* @param array Hash array with event properties
* @param string New participant status
* @return boolean True on success, False on error
*/
public function edit_rsvp(&$event, $status)
{
if (($ret = $this->update_event($event)) && $this->rc->config->get('kolab_invitation_calendars')) {
// re-assign to the according (virtual) calendar
if (strtoupper($status) == 'DECLINED')
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
else if (strtoupper($status) == 'NEEDS-ACTION')
$event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
else if ($event['_folder_id'])
$event['calendar'] = $event['_folder_id'];
}
return $ret;
}
/**
* Move a single event
*
* @see calendar_driver::move_event()
* @return boolean True on success, False on error
*/
public function move_event($event)
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
return $this->update_event($event + $ev);
}
return false;
}
/**
* Resize a single event
*
* @see calendar_driver::resize_event()
* @return boolean True on success, False on error
*/
public function resize_event($event)
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
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'];
$decline = $event['decline'];
if (($storage = $this->get_calendar($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'] ?: ($event['_instance'] ? 'current' : 'all');
}
// removing an exception instance
if ($event['recurrence_id'] && $master['recurrence'] && is_array($master['recurrence']['EXCEPTIONS'])) {
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
unset($master['recurrence']['EXCEPTIONS'][$i]);
// set event date back to the actual occurrence
if ($exception['recurrence_date'])
$event['start'] = $exception['recurrence_date'];
break;
}
}
}
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']--;
}
// remove the matching RDATE entry
else if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
unset($master['recurrence']['RDATE'][$j]);
break;
}
}
}
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();
}
// remove matching RDATE entries
else if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
$master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
break;
}
}
}
$success = $storage->update_event($master);
break;
}
default: // 'all' is default
if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
// don't delete but set PARTSTAT=DECLINED
if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
$success = $storage->update_event($master);
}
}
if (!$success)
$success = $storage->delete_event($master, $force);
break;
}
}
if ($success && $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->get_calendar($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->get_calendar($event['calendar'])))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
return false;
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($event['id'], $storage->storage))
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'] ?: ($old['recurrence_id'] ? 'current' : 'all');
}
// check if update affects scheduling and update attendee status accordingly
$reschedule = $this->check_scheduling($event, $old, true);
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
if (isset($event['recurrence']['EXCEPTIONS']))
$with_exceptions = true; // exceptions already provided (e.g. from iCal import)
else if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
+ // remove some internal properties which should not be saved
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
+ $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']);
+
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
- unset($event['recurrence_id'], $event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
+ unset($event['recurrence_id'], $event['_instance'], $event['id']);
// copy attachment data to new event
foreach ((array)$event['attachments'] as $idx => $attachment) {
if (!$attachment['content'])
$event['attachments'][$idx]['content'] = $this->get_attachment_body($attachment['id'], $master);
}
if ($success = $storage->insert_event($event))
$success = $event['uid'];
break;
case 'future':
case 'current':
// recurring instances shall not store recurrence rules and attachments
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
- unset($event['attachments']);
+ unset($event['attachments'], $event['id']);
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
$event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
- // remove some internal properties which should not be saved
- unset($event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
-
// save properties to a recurrence exception instance
if ($old['recurrence_id'] && is_array($master['recurrence']['EXCEPTIONS'])) {
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
if ($exception['_instance'] == $old['_instance']) {
$event['_instance'] = $old['_instance'];
$event['recurrence_date'] = $old['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$success = $storage->update_event($master, $old['id']);
break 2;
}
}
}
$add_exception = true;
// adjust matching RDATE entry if dates changed
if ($savemode == 'current' && $master['recurrence']['RDATE'] && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $old_date) {
$master['recurrence']['RDATE'][$j] = $event['start'];
sort($master['recurrence']['RDATE']);
$add_exception = false;
break;
}
}
}
// save as new exception to master event
if ($add_exception) {
$event['_instance'] = $old['_instance'];
$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;
$date_shift = $old['start']->diff($event['start']);
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start']->add($date_shift);
$event['end'] = clone $event['start'];
$event['end']->add(new DateInterval('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'];
}
// adjust recurrence-id when start changed and therefore the entire recurrence chain changes
if (($old_start_date != $new_start_date || $old_start_time != $new_start_time) &&
is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
$recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
if (is_a($recurrence_id, 'DateTime')) {
$recurrence_id->add($date_shift);
$event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
$event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
}
}
}
// 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;
}
/**
* Determine whether the current change affects scheduling and reset attendee status accordingly
*/
public function check_scheduling(&$event, $old, $update = true)
{
// skip this check when importing iCal/iTip events
if (isset($event['sequence']) || !empty($event['_method'])) {
return false;
}
// iterate through the list of properties considered 'significant' for scheduling
$kolab_event = $old['_formatobj'] ?: new kolab_format_event();
$reschedule = $kolab_event->check_rescheduling($event, $old);
// reset all attendee status to needs-action (#4360)
if ($update && $reschedule && is_array($event['attendees'])) {
$is_organizer = false;
$emails = $this->cal->get_user_emails();
$attendees = $event['attendees'];
foreach ($attendees as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$is_organizer = true;
}
else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
$attendees[$i]['status'] = 'NEEDS-ACTION';
$attendees[$i]['rsvp'] = true;
}
}
// update attendees only if I'm the organizer
if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
$event['attendees'] = $attendees;
}
}
return $reschedule;
}
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Include virtual events (optional)
* @param integer Only list events modified since this time (unix timestamp)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
else if (!$calendars)
$calendars = array_keys($this->calendars);
$query = array();
if ($modifiedsince)
$query[] = array('changed', '>=', $modifiedsince);
$events = $categories = array();
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
$categories += $storage->categories;
}
}
// add events from the address books birthday calendar
if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
$events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
return $events;
}
/**
* Get number of events in the given calendar
*
* @param mixed List of calendar IDs to count events (either as array or comma-separated string)
* @param integer Date range start (unix timestamp)
* @param integer Date range end (unix timestamp)
* @return array Hash array with counts grouped by calendar ID
*/
public function count_events($calendars, $start, $end = null)
{
$counts = array();
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
else if (!$calendars)
$calendars = array_keys($this->calendars);
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$counts[$cid] = $storage->count_events($start, $end);
}
}
return $counts;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return array();
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$time = $slot + $interval;
$candidates = array();
$query = array(array('tags', '=', 'x-has-alarms'));
foreach ($this->calendars as $cid => $calendar) {
// skip calendars with alarms disabled
if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
continue;
foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($e);
if ($alarm && $alarm['time'] && $alarm['time'] >= $last && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $e['title'],
'location' => $e['location'],
'start' => $e['start'],
'end' => $e['end'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query("SELECT *"
. " FROM " . $this->rc->db->table_name('kolab_alarms', true)
. " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
. " AND `user_id` = ?",
$this->rc->user->ID
);
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['alarm_id']] = $e;
}
}
$alarms = array();
foreach ($candidates as $id => $alarm) {
// skip dismissed alarms
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
if ($notifyat <= $time)
$alarms[] = $alarm;
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($alarm_id, $snooze = 0)
{
$alarms_table = $this->rc->db->table_name('kolab_alarms', true);
// delete old alarm entry
$this->rc->db->query("DELETE FROM $alarms_table"
. " WHERE `alarm_id` = ? AND `user_id` = ?",
$alarm_id,
$this->rc->user->ID
);
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query("INSERT INTO $alarms_table"
. " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
. " VALUES (?, ?, ?, ?)",
$alarm_id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments from the given event
*/
public function list_attachments($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
$event = $storage->get_event($event['id']);
return $event['attachments'];
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
$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->get_calendar($event['calendar'])))
return false;
return $cal->storage->get_attachment($event['id'], $id);
}
/**
* Build a struct representing the given message reference
*
* @see calendar_driver::get_message_reference()
*/
public function get_message_reference($uri_or_headers, $folder = null)
{
if (is_object($uri_or_headers)) {
$uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
}
if (is_string($uri_or_headers)) {
return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
}
return false;
}
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
// FIXME: complete list with categories saved in config objects (KEP:12)
return $this->rc->config->get('calendar_categories', $this->default_categories);
}
/**
* Create instances of a recurring event
*
* @param array Hash array with event properties
* @param object DateTime Start date of the recurrence window
* @param object DateTime End date of the recurrence window
* @return array List of recurring event instances
*/
public function get_recurring_events($event, $start, $end = null)
{
// load the given event data into a libkolabxml container
if (!$event['_formatobj']) {
$event_xml = new kolab_format_event();
$event_xml->set($event);
$event['_formatobj'] = $event_xml;
}
$this->_read_calendars();
$storage = reset($this->calendars);
return $storage->get_recurring_events($event, $start, $end);
}
/**
* 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 = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
if (!($cal = $this->get_calendar($cal)))
return false;
// trigger updates on folder
$trigger = $cal->storage->trigger();
if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
true, false);
}
exit;
}
/**
* Convert from Kolab_Format to internal representation
*/
public static function to_rcube_event($record)
{
$record['id'] = $record['uid'];
// all-day events go from 12:00 - 13:00
if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = clone $record['start'];
$record['end']->add(new DateInterval('PT1H'));
}
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
unset($attachment['path'], $attachment['content']);
$attachments[] = $attachment;
}
}
$record['attachments'] = $attachments;
}
if (!empty($record['attendees'])) {
foreach ((array)$record['attendees'] as $i => $attendee) {
if (is_array($attendee['delegated-from'])) {
$record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
}
if (is_array($attendee['delegated-to'])) {
$record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
}
}
}
// Roundcube only supports one category assignment
if (is_array($record['categories']))
$record['categories'] = $record['categories'][0];
// the cancelled flag transltes into status=CANCELLED
if ($record['cancelled'])
$record['status'] = 'CANCELLED';
// The web client only supports DISPLAY type of alarms
if (!empty($record['alarms']))
$record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
// remove empty recurrence array
if (empty($record['recurrence']))
unset($record['recurrence']);
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
return $record;
}
/**
* Set CSS class according to the event's attendde partstat
*/
public static function add_partstat_class($event, $partstats, $user = null)
{
// set classes according to PARTSTAT
if (is_array($event['attendees'])) {
$user_emails = libcalendaring::get_instance()->get_user_emails($user);
$partstat = 'UNKNOWN';
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails)) {
$partstat = $attendee['status'];
break;
}
}
if (in_array($partstat, $partstats)) {
$event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
}
}
return $event;
}
/**
* Provide a list of revisions for the given event
*
* @param array $event Hash array with event properties
*
* @return array List of changes, each as a hash array
* @see calendar_driver::get_event_changelog()
*/
public function get_event_changelog($event)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox) = $this->_resolve_event_identity($event);
$result = $this->bonnie_api->changelog('event', $uid, $mailbox);
if (is_array($result) && $result['uid'] == $uid) {
return $result['changes'];
}
return false;
}
/**
* Get a list of property changes beteen two revisions of an event
*
* @param array $event Hash array with event properties
* @param mixed $rev Revisions: "from:to"
*
* @return array List of property changes, each as a hash array
* @see calendar_driver::get_event_diff()
*/
public function get_event_diff($event, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox) = $this->_resolve_event_identity($event);
// call Bonnie API
$result = $this->bonnie_api->diff('event', $uid, $rev, $mailbox);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev'] = $rev;
$keymap = array(
'dtstart' => 'start',
'dtend' => 'end',
'dstamp' => 'changed',
'summary' => 'title',
'alarm' => 'alarms',
'attendee' => 'attendees',
'attach' => 'attachments',
'rrule' => 'recurrence',
'transparency' => 'free_busy',
'classification' => 'sensitivity',
'lastmodified-date' => 'changed',
);
$prop_keymaps = array(
'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
'attendees' => array('partstat' => 'status'),
);
$special_changes = array();
// map kolab event properties to keys the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// translate free_busy values
if ($change['property'] == 'free_busy') {
$change['old'] = $old['old'] ? 'free' : 'busy';
$change['new'] = $old['new'] ? 'free' : 'busy';
}
// map alarms trigger value
if ($change['property'] == 'alarms') {
if (is_array($change['old']) && is_array($change['old']['trigger']))
$change['old']['trigger'] = $change['old']['trigger']['value'];
if (is_array($change['new']) && is_array($change['new']['trigger']))
$change['new']['trigger'] = $change['new']['trigger']['value'];
}
// make all property keys uppercase
if ($change['property'] == 'recurrence') {
$special_changes['recurrence'] = $i;
foreach (array('old','new') as $m) {
if (is_array($change[$m])) {
$props = array();
foreach ($change[$m] as $k => $v)
$props[strtoupper($k)] = $v;
$change[$m] = $props;
}
}
}
// map property keys names
if (is_array($prop_keymaps[$change['property']])) {
foreach ($prop_keymaps[$change['property']] as $k => $dest) {
if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
$change['old'][$dest] = $change['old'][$k];
unset($change['old'][$k]);
}
if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
$change['new'][$dest] = $change['new'][$k];
unset($change['new'][$k]);
}
}
}
if ($change['property'] == 'exdate') {
$special_changes['exdate'] = $i;
}
else if ($change['property'] == 'rdate') {
$special_changes['rdate'] = $i;
}
});
// merge some recurrence changes
foreach (array('exdate','rdate') as $prop) {
if (array_key_exists($prop, $special_changes)) {
$exdate = $result['changes'][$special_changes[$prop]];
if (array_key_exists('recurrence', $special_changes)) {
$recurrence = &$result['changes'][$special_changes['recurrence']];
}
else {
$i = count($result['changes']);
$result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
$recurrence = &$result['changes'][$i]['recurrence'];
}
$key = strtoupper($prop);
$recurrence['old'][$key] = $exdate['old'];
$recurrence['new'][$key] = $exdate['new'];
unset($result['changes'][$special_changes[$prop]]);
}
}
return $result;
}
return false;
}
/**
* Return full data of a specific revision of an event
*
* @param array Hash array with event properties
* @param mixed $rev Revision number
*
* @return array Event object as hash array
* @see calendar_driver::get_event_revison()
*/
public function get_event_revison($event, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
$calid = $event['calendar'];
list($uid, $mailbox) = $this->_resolve_event_identity($event);
// call Bonnie API
$result = $this->bonnie_api->get('event', $uid, $rev, $mailbox);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('event');
$format->load($result['xml']);
$event = $format->to_array();
if ($format->is_valid()) {
$event['calendar'] = $calid;
$event['rev'] = $result['rev'];
return self::to_rcube_event($event);
}
}
return false;
}
/**
* Helper method to resolved the given event identifier into uid and folder
*
* @return array (uid,folder) tuple
*/
private function _resolve_event_identity($event)
{
$mailbox = null;
if (is_array($event)) {
$uid = $event['id'] ?: $event['uid'];
if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
$mailbox = $cal->get_mailbox_id();
}
}
else {
$uid = $event;
}
return array($uid, $mailbox);
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
// show default dialog for birthday calendar
if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID)
unset($formfields['showalarms']);
return parent::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'] = 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', 'id' => 'calendar-parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'id' => 'calendar-parent',
'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) {
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', html::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 = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
if ($calid && ($cal = $this->get_calendar($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'];
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
$db = $this->rc->get_dbh();
foreach (array('kolab_alarms', 'itipinvitations') as $table) {
$db->query("DELETE FROM " . $this->rc->db->table_name($table, true)
. " WHERE `user_id` = ?", $args['user']->ID);
}
}
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index c3bf6256..63a05488 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1,1584 +1,1584 @@
<?php
/**
* Library providing common functions for calendaring plugins
*
* Provides utility functions for calendar-related modules such as
* - alarms display and dismissal
* - attachment handling
* - recurrence computation and UI elements
* - ical parsing and exporting
* - itip scheduling protocol
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2014, 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/>.
*/
class libcalendaring extends rcube_plugin
{
public $rc;
public $timezone;
public $gmt_offset;
public $dst_active;
public $timezone_offset;
public $ical_parts = array();
public $ical_message;
public $defaults = array(
'calendar_date_format' => "yyyy-MM-dd",
'calendar_date_short' => "M-d",
'calendar_date_long' => "MMM d yyyy",
'calendar_date_agenda' => "ddd MM-dd",
'calendar_time_format' => "HH:mm",
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'calendar_date_format_sets' => array(
'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
),
);
private static $instance;
private static $email_regex = '/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/';
private $mail_ical_parser;
/**
* Singleton getter to allow direct access from other plugins
*/
public static function get_instance()
{
return self::$instance;
}
/**
* Required plugin startup method
*/
public function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
// set user's timezone
try {
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
$this->timezone = new DateTimeZone('GMT');
}
$now = new DateTime('now', $this->timezone);
$this->gmt_offset = $now->getOffset();
$this->dst_active = $now->format('I');
$this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
$this->add_texts('localization/', false);
// include client scripts and styles
if ($this->rc->output) {
// add hook to display alarms
$this->add_hook('refresh', array($this, 'refresh'));
$this->register_action('plugin.alarms', array($this, 'alarms_action'));
$this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
}
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
if ($this->rc->output && $this->rc->output->type == 'html') {
$this->rc->output->set_env('libcal_settings', $this->load_settings());
$this->include_script('libcalendaring.js');
$this->include_stylesheet($this->local_skin_path() . '/libcal.css');
}
if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
}
}
}
/**
* Load iCalendar functions
*/
public static function get_ical()
{
$self = self::get_instance();
require_once($self->home . '/libvcalendar.php');
return new libvcalendar();
}
/**
* Load iTip functions
*/
public static function get_itip($domain = 'libcalendaring')
{
$self = self::get_instance();
require_once($self->home . '/lib/libcalendaring_itip.php');
return new libcalendaring_itip($self, $domain);
}
/**
* Load recurrence computation engine
*/
public static function get_recurrence()
{
$self = self::get_instance();
require_once($self->home . '/lib/libcalendaring_recurrence.php');
return new libcalendaring_recurrence($self);
}
/**
* Shift dates into user's current timezone
*
* @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
* @return object DateTime object in user's timezone
*/
public function adjust_timezone($dt, $dateonly = false)
{
if (is_numeric($dt))
$dt = new DateTime('@'.$dt);
else if (is_string($dt))
$dt = rcube_utils::anytodatetime($dt);
if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
$dt->setTimezone($this->timezone);
}
return $dt;
}
/**
*
*/
public function load_settings()
{
$this->date_format_defaults();
$settings = array();
// configuration
$settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
$settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
$settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['timezone'] = $this->timezone_offset;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
$this->rc->gettext('sunday'), $this->rc->gettext('monday'),
$this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'),
$this->rc->gettext('thursday'), $this->rc->gettext('friday'),
$this->rc->gettext('saturday')
);
$settings['days_short'] = array(
$this->rc->gettext('sun'), $this->rc->gettext('mon'),
$this->rc->gettext('tue'), $this->rc->gettext('wed'),
$this->rc->gettext('thu'), $this->rc->gettext('fri'),
$this->rc->gettext('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
// define list of file types which can be displayed inline
// same as in program/steps/mail/show.inc
$settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes');
return $settings;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private function date_format_defaults()
{
static $defaults = array();
// nothing to be done
if (isset($defaults['date_format']))
return;
$defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
$defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
// override defaults
if ($defaults['date_format'])
$this->defaults['calendar_date_format'] = $defaults['date_format'];
if ($defaults['time_format'])
$this->defaults['calendar_time_format'] = $defaults['time_format'];
// derive format variants from basic date format
$format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
$this->defaults['calendar_date_long'] = $format_set[0];
$this->defaults['calendar_date_short'] = $format_set[1];
$this->defaults['calendar_date_agenda'] = $format_set[2];
}
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$fromto = '--';
// handle task objects
if ($event['_type'] == 'task' && is_object($event['due'])) {
$date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
$fromto = $this->rc->format_date($event['due'], $date_format, false);
// add timezone information
if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
// abort if no valid event dates are given
if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
return $fromto;
}
$duration = $event['start']->diff($event['end'])->format('s');
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = format_date($event['start'], $date_format);
if (($todate = format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $time_format);
}
else {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
/**
* Render HTML form for alarm configuration
*/
public function alarm_select($attrib, $alarm_types, $absolute_time = true)
{
unset($attrib['name']);
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id']));
$select_type->add($this->gettext('none'), '');
foreach ($alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
if ($absolute_time)
$select_offset->add($this->gettext('trigger@'), '@');
// pre-set with default values from user settings
$preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$hidden = array('style' => 'display:none');
$html = html::span('edit-alarm-set',
$select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
$input_value->show($preset[0]) . ' ' .
$select_offset->show($preset[1]) . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return $html;
}
/**
* Get a list of email addresses of the given user (from login and identities)
*
* @param string User Email (default to current user)
* @return array Email addresses related to the user
*/
public function get_user_emails($user = null)
{
static $_emails = array();
if (empty($user)) {
$user = $this->rc->user->get_username();
}
// return cached result
if (is_array($_emails[$user])) {
return $_emails[$user];
}
$emails = array($user);
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
$emails = array_map('strtolower', $plugin['emails']);
// add all emails from the current user's identities
if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
foreach ($this->rc->user->list_emails() as $identity) {
$emails[] = strtolower($identity['email']);
}
}
$_emails[$user] = array_unique($emails);
return $_emails[$user];
}
/**
* Set the given participant status to the attendee matching the current user's identities
*
* @param array Hash array with event struct
* @param string The PARTSTAT value to set
* @return mixed Email address of the updated attendee or False if none matching found
*/
public function set_partstat(&$event, $status)
{
$emails = $this->get_user_emails();
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
return $attendee['email'];
}
}
return false;
}
/********* Alarms handling *********/
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public static function parse_alarm_value($val)
{
if ($val[0] == '@') {
return array(new DateTime($val));
}
else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
if ($m[1] == '')
$m[1] = '+';
foreach ($m2 as $seg) {
$prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
if ($seg[1] > 0) { // ignore zero values
// convert seconds to minutes
if ($seg[2] == 'S') {
$seg[2] = 'M';
$seg[1] = max(1, round($seg[1]/60));
}
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
}
// return zero value nevertheless
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
return false;
}
/**
* Convert the alarms list items to be processed on the client
*/
public static function to_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'] instanceof DateTime) {
$alarm['trigger'] = '@' . $alarm['trigger']->format('U');
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[2];
}
return $alarm;
}, (array)$valarms);
}
/**
* Process the alarms values submitted by the client
*/
public static function from_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'][0] == '@') {
try {
$alarm['trigger'] = new DateTime($alarm['trigger']);
$alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
}
catch (Exception $e) { /* handle this ? */ }
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[3];
}
return $alarm;
}, (array)$valarms);
}
/**
* Render localized text for alarm settings
*/
public static function alarms_text($alarms)
{
if (is_array($alarms) && is_array($alarms[0])) {
$texts = array();
foreach ($alarms as $alarm) {
if ($text = self::alarm_text($alarm))
$texts[] = $text;
}
return join(', ', $texts);
}
else {
return self::alarm_text($alarms);
}
}
/**
* Render localized text for a single alarm property
*/
public static function alarm_text($alarm)
{
if (is_string($alarm)) {
list($trigger, $action) = explode(':', $alarm);
}
else {
$trigger = $alarm['trigger'];
$action = $alarm['action'];
}
$text = '';
$rcube = rcube::get_instance();
switch ($action) {
case 'EMAIL':
$text = $rcube->gettext('libcalendaring.alarmemail');
break;
case 'DISPLAY':
$text = $rcube->gettext('libcalendaring.alarmdisplay');
break;
case 'AUDIO':
$text = $rcube->gettext('libcalendaring.alarmaudio');
break;
}
if ($trigger instanceof DateTime) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($trigger))
));
}
else if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($m[1]))
));
}
else if ($val = self::parse_alarm_value($trigger)) {
// TODO: for all-day events say 'on date of event at XX' ?
if ($val[0] == 0)
$text .= ' ' . $rcube->gettext('libcalendaring.triggerattime');
else
$text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext('libcalendaring.trigger' . $val[1]);
}
else {
return false;
}
return $text;
}
/**
* Get the next alarm (time & action) for the given event
*
* @param array Record data
* @return array Hash array with alarm time/type or null if no alarms are configured
*/
public static function get_next_alarm($rec, $type = 'event')
{
if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
return null;
if ($type == 'task') {
$timezone = self::get_instance()->timezone;
if ($rec['startdate'])
$rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
if ($rec['date'])
$rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
}
if (!$rec['end'])
$rec['end'] = $rec['start'];
// support legacy format
if (!$rec['valarms']) {
list($trigger, $action) = explode(':', $rec['alarms'], 2);
if ($alarm = self::parse_alarm_value($trigger)) {
$rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
}
}
$expires = new DateTime('now - 12 hours');
$alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
// handle multiple alarms
$notify_at = null;
foreach ($rec['valarms'] as $alarm) {
$notify_time = null;
if ($alarm['trigger'] instanceof DateTime) {
$notify_time = $alarm['trigger'];
}
else if (is_string($alarm['trigger'])) {
$refdate = $alarm['trigger'][0] == '+' ? $rec['end'] : $rec['start'];
// abort if no reference date is available to compute notification time
if (!is_a($refdate, 'DateTime'))
continue;
// TODO: for all-day events, take start @ 00:00 as reference date ?
try {
$interval = new DateInterval(trim($alarm['trigger'], '+-'));
$interval->invert = $alarm['trigger'][0] != '+';
$notify_time = clone $refdate;
$notify_time->add($interval);
}
catch (Exception $e) {
rcube::raise_error($e, true);
continue;
}
}
if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
$notify_at = $notify_time;
$action = $alarm['action'];
$alarm_prop = $alarm;
// generate a unique alarm ID if multiple alarms are set
if (count($rec['valarms']) > 1) {
$alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
}
}
}
return !$notify_at ? null : array(
'time' => $notify_at->format('U'),
'action' => $action ? strtoupper($action) : 'DISPLAY',
'id' => $alarm_id,
'prop' => $alarm_prop,
);
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
public function refresh($attr)
{
// collect pending alarms from all providers (e.g. calendar, tasks)
$plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
'time' => time(),
'alarms' => array(),
));
if (!$plugin['abort'] && !empty($plugin['alarms'])) {
// make sure texts and env vars are available on client
$this->add_texts('localization/', true);
$this->rc->output->add_label('close');
$this->rc->output->set_env('snooze_select', $this->snooze_select());
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
}
}
/**
* Handler for alarm dismiss/snooze requests
*/
public function alarms_action()
{
// $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$data['ids'] = explode(',', $data['id']);
$plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
if ($plugin['success'])
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private function _alarms_output($alarms)
{
$out = array();
foreach ($alarms as $alarm) {
$out[] = array(
'id' => $alarm['id'],
'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '',
'allDay' => ($alarm['allday'] == 1)?true:false,
'title' => $alarm['title'],
'location' => $alarm['location'],
'calendar' => $alarm['calendar'],
);
}
return $out;
}
/**
* Render a dropdown menu to choose snooze time
*/
private function snooze_select($attrib = array())
{
$steps = array(
5 => 'repeatinmin',
10 => 'repeatinmin',
15 => 'repeatinmin',
20 => 'repeatinmin',
30 => 'repeatinmin',
60 => 'repeatinhr',
120 => 'repeatinhrs',
1440 => 'repeattomorrow',
10080 => 'repeatinweek',
);
$items = array();
foreach ($steps as $n => $label) {
$items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
$this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
}
return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
}
/********* Recurrence rules handling ********/
/**
* Render localized text describing the recurrence rule of an event
*/
public function recurrence_text($rrule)
{
// derive missing FREQ and INTERVAL from RDATE list
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
$first = $rrule['RDATE'][0];
$second = $rrule['RDATE'][1];
$third = $rrule['RDATE'][2];
if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
$diff = $first->diff($second);
foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
if ($diff->$k != 0) {
$rrule['FREQ'] = $freq;
$rrule['INTERVAL'] = $diff->$k;
// verify interval with next item
if (is_a($third, 'DateTime')) {
$diff2 = $second->diff($third);
if ($diff2->$k != $diff->$k) {
unset($rrule['INTERVAL']);
}
}
break;
}
}
}
if (!$rrule['INTERVAL']) {
$rrule['FREQ'] = 'RDATE';
}
$rrule['UNTIL'] = end($rrule['RDATE']);
}
$freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
$details = '';
switch ($rrule['FREQ']) {
case 'DAILY':
$freq .= $this->gettext('days');
break;
case 'WEEKLY':
$freq .= $this->gettext('weeks');
break;
case 'MONTHLY':
$freq .= $this->gettext('months');
break;
case 'YEARLY':
$freq .= $this->gettext('years');
break;
}
if ($rrule['INTERVAL'] <= 1) {
$freq = $this->gettext(strtolower($rrule['FREQ']));
}
if ($rrule['COUNT']) {
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
}
else if ($rrule['UNTIL']) {
$until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
}
else {
$until = $this->gettext('forever');
}
$except = '';
if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
$format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$exdates = array_map(
function($dt) use ($format) { return format_date($dt, $format); },
array_slice($rrule['EXDATE'], 0, 10)
);
$except = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates);
}
return rtrim($freq . $details . ', ' . $until . $except);
}
/**
* Generate the form for recurrence settings
*/
public function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
$select->add($this->gettext('never'), '');
$select->add($this->gettext('daily'), 'DAILY');
$select->add($this->gettext('weekly'), 'WEEKLY');
$select->add($this->gettext('monthly'), 'MONTHLY');
$select->add($this->gettext('yearly'), 'YEARLY');
$select->add($this->gettext('rdate'), 'RDATE');
$html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show('');
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days')));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks')));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'),
$checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
$this->gettext($daymap[$d])
) . ' ';
}
$html .= html::div($attrib, html::label(null, $this->gettext('bydays')) . $weekdays);
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months')));
$checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery')));
$table->add(null, $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, $table->show());
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years')));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
// day rule selection
$html .= html::div($attrib, html::label(null, $this->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
$html = html::div('line first',
html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
$this->gettext('forever'))
);
$forntimes = $this->gettext(array(
'name' => 'forntimes',
'vars' => array('nr' => '%s'))
);
$html .= html::div('line',
$radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' .
sprintf($forntimes, $select->show(1))
);
$html .= html::div('line',
$radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' .
$this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
);
$html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html);
break;
case 'rdate':
$ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
$input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10"));
$button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
$html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->gettext('first'),
$this->gettext('second'),
$this->gettext('third'),
$this->gettext('fourth'),
$this->gettext('last')
),
array(1, 2, 3, 4, -1));
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
return $select_prefix->show() . '&nbsp;' . $select_wday->show();
}
/**
* Convert the recurrence settings to be processed on the client
*/
public function to_client_recurrence($recurrence, $allday = false)
{
if ($recurrence['UNTIL'])
$recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
// format RDATE values
if (is_array($recurrence['RDATE'])) {
$libcal = $this;
$recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
}, $recurrence['RDATE']);
}
unset($recurrence['EXCEPTIONS']);
return $recurrence;
}
/**
* Process the alarms values submitted by the client
*/
public function from_client_recurrence($recurrence, $start = null)
{
if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
$recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
}
if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
$tz = $this->timezone;
$recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
$dt = new DateTime($rdate, $tz);
if (is_a($start, 'DateTime'))
$dt->setTime($start->format('G'), $start->format('i'));
return $dt;
}
catch (Exception $e) {
return null;
}
}, $recurrence['RDATE']);
}
return $recurrence;
}
/********* Attachments handling *********/
/**
* Handler for attachment uploads
*/
public function attachment_upload($session_key, $id_prefix = '')
{
// Upload progress update
if (!empty($_GET['_progress'])) {
$this->rc->upload_progress();
}
$recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
$_SESSION[$session_key] = array();
$_SESSION[$session_key]['id'] = $recid;
$_SESSION[$session_key]['attachments'] = array();
}
// clear all stored output properties (like scripts and env vars)
$this->rc->output->reset();
if (is_array($_FILES['_attachments']['tmp_name'])) {
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_attachments']['error'][$i];
if (!$err) {
$attachment = array(
'path' => $filepath,
'size' => $_FILES['_attachments']['size'][$i],
'name' => $_FILES['_attachments']['name'][$i],
'mimetype' => rcube_mime::file_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
'group' => $recid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
}
if (!$err && $attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$_SESSION[$session_key]['attachments'][$id] = $attachment;
if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => $this->rc->gettext('delete')
));
}
else {
$button = Q($this->rc->gettext('delete'));
}
$content = html::a(array(
'href' => "#delete",
'class' => 'delete',
'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
), $button);
$content .= Q($attachment['name']);
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true), $uploadid);
}
else { // upload failed
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else if ($attachment['error']) {
$msg = $attachment['error'];
}
else {
$msg = $this->rc->gettext('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size'))
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes($maxsize)))));
else
$msg = $this->rc->gettext('fileuploaderror');
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
$this->rc->output->send('iframe');
}
/**
* Deliver an event/task attachment to the client
* (similar as in Roundcube core program/steps/mail/get.inc)
*/
public function attachment_get($attachment)
{
ob_end_clean();
if ($attachment && $attachment['body']) {
// allow post-processing of the attachment body
$part = new rcube_message_part;
$part->filename = $attachment['name'];
$part->size = $attachment['size'];
$part->mimetype = $attachment['mimetype'];
$plugin = $this->rc->plugins->exec_hook('message_part_get', array(
'body' => $attachment['body'],
'mimetype' => strtolower($attachment['mimetype']),
'download' => !empty($_GET['_download']),
'part' => $part,
));
if ($plugin['abort'])
exit;
$mimetype = $plugin['mimetype'];
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$browser = $this->rc->output->browser;
// send download headers
if ($plugin['download']) {
header("Content-Type: application/octet-stream");
if ($browser->ie)
header("Content-Type: application/force-download");
}
else if ($ctype_primary == 'text') {
header("Content-Type: text/$ctype_secondary");
}
else {
header("Content-Type: $mimetype");
header("Content-Transfer-Encoding: binary");
}
// display page, @TODO: support text/plain (and maybe some other text formats)
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$OUTPUT = new rcube_html_page();
// @TODO: use washtml on $body
$OUTPUT->write($plugin['body']);
}
else {
// don't kill the connection if download takes more than 30 sec.
@set_time_limit(0);
$filename = $attachment['name'];
$filename = preg_replace('[\r\n]', '', $filename);
if ($browser->ie && $browser->ver < 7)
$filename = rawurlencode(abbreviate_string($filename, 55));
else if ($browser->ie)
$filename = rawurlencode($filename);
else
$filename = addcslashes($filename, '"');
$disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
header("Content-Disposition: $disposition; filename=\"$filename\"");
echo $plugin['body'];
}
exit;
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Show "loading..." page in attachment iframe
*/
public function attachment_loading_page()
{
$url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
$message = $this->rc->gettext('loadingdata');
header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
print "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset='.RCUBE_CHARSET.'">' . "\n"
. "</head>\n<body>\n$message\n</body>\n</html>";
exit;
}
/**
* Template object for attachment display frame
*/
public function attachment_frame($attrib = array())
{
$mimetype = strtolower($this->attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
$this->rc->output->add_gui_object('attachmentframe', $attrib['id']);
return html::iframe($attrib);
}
/**
*
*/
public function attachment_header($attrib = array())
{
$rcmail = rcmail::get_instance();
$dl_link = strtolower($attrib['downloadlink']) == 'true';
$dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET);
$table = new html_table(array('cols' => $dl_link ? 3 : 2));
if (!empty($this->attachment['name'])) {
$table->add('title', Q($this->rc->gettext('filename')));
$table->add('header', Q($this->attachment['name']));
if ($dl_link) {
$table->add('download-link', html::a($dl_url, Q($this->rc->gettext('download'))));
}
}
if (!empty($this->attachment['mimetype'])) {
$table->add('title', Q($this->rc->gettext('type')));
$table->add('header', Q($this->attachment['mimetype']));
}
if (!empty($this->attachment['size'])) {
$table->add('title', Q($this->rc->gettext('filesize')));
$table->add('header', Q(show_bytes($this->attachment['size'])));
}
$this->rc->output->set_env('attachment_download_url', $dl_url);
return $table->show($attrib);
}
/********* iTip message detection *********/
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->ical_message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->ical_message->mime_parts as $part) {
if (self::part_is_vcalendar($part)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ical_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part) {
$this->ical_parts = array($itip_part);
}
}
/**
* Getter for the parsed iCal objects attached to the current email message
*
* @return object libvcalendar parser instance with the parsed objects
*/
public function get_mail_ical_objects()
{
// create parser and load ical objects
if (!$this->mail_ical_parser) {
$this->mail_ical_parser = $this->get_ical();
foreach ($this->ical_parts as $mime_id) {
$part = $this->ical_message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET;
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
// check if the parsed object is an instance of a recurring event/task
array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
// stop on the part that has an iTip method specified
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
$this->mail_ical_parser->mime_id = $mime_id;
// store the message's sender address for comparisons
$this->mail_ical_parser->sender = preg_match(self::$email_regex, $this->ical_message->headers->from, $m) ? $m[1] : '';
if (!empty($this->mail_ical_parser->sender)) {
foreach ($this->mail_ical_parser->objects as $i => $object) {
$this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
$this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
}
}
break;
}
}
}
return $this->mail_ical_parser;
}
/**
* Read the given mime message from IMAP and parse ical data
*
* @param string Mailbox name
* @param string Message UID
* @param string Message part ID and object index (e.g. '1.2:0')
* @param string Object type filter (optional)
*
* @return array Hash array with the parsed iCal
*/
public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
{
$charset = RCMAIL_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $imap->get_message_part($uid, $mime_id);
$headers = $imap->get_message_headers($uid);
$parser = $this->get_ical();
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$objects = $parser->import($part, $charset);
}
}
// successfully parsed events/tasks?
if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
if ($parser->method)
$object['_method'] = $parser->method;
// store the message's sender address for comparisons
$object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
// check if this is an instance of a recurring event/task
self::identify_recurrence_instance($object);
return $object;
}
return null;
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @return boolean True if part is of type vcard
*/
public static function part_is_vcalendar($part)
{
return (
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
// Apple sends files as application/x-any (!?)
($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
);
}
/**
* Single occourrences of recurring events are identified by their RECURRENCE-ID property
* in iCal which is represented as 'recurrence_date' in our internal data structure.
*
* Check if such a property exists and derive the '_instance' identifier and '_savemode'
* attributes which are used in the storage backend to identify the nested exception item.
*/
public static function identify_recurrence_instance(&$object)
{
// set instance and 'savemode' according to recurrence-id
if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
$recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
$object['_instance'] = $object['recurrence_date']->format($recurrence_id_format);
- $object['_savemode'] = $event['thisandfuture'] ? 'future' : 'current';
+ $object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
}
else if (!empty($object['recurrence_id']) || !empty($object['_instance'])) {
if (strlen($object['_instance']) > 4) {
$object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
}
else {
$object['recurrence_date'] = clone $object['start'];
}
}
}
/********* Attendee handling functions *********/
/**
* Handler for attendee group expansion requests
*/
public function expand_attendee_group()
{
$id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$result = array('id' => $id, 'members' => array());
$maxnum = 500;
// iterate over all autocomplete address books (we don't know the source of the group)
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
foreach ($abook->list_groups($data['name'], 1) as $group) {
// this is the matching group to expand
if (in_array($data['email'], (array)$group['email'])) {
$abook->set_pagesize($maxnum);
$abook->set_group($group['ID']);
// get all members
$res = $abook->list_records($this->rc->config->get('contactlist_fields'));
// handle errors (e.g. sizelimit, timelimit)
if ($abook->get_error()) {
$result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
$res = false;
}
// check for maximum number of members (we don't wanna bloat the UI too much)
else if ($res->count > $maxnum) {
$result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
$res = false;
}
while ($res && ($member = $res->iterate())) {
$emails = (array)$abook->get_col_values('email', $member, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$result['members'][] = array(
'email' => $email,
'name' => rcube_addressbook::compose_list_name($member),
);
}
}
break 2;
}
}
}
}
$this->rc->output->command('plugin.expand_attendee_callback', $result);
}
/********* Static utility functions *********/
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public static function to_rrule($recurrence)
{
if (is_string($recurrence))
return $recurrence;
$rrule = '';
foreach ((array)$recurrence as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
// convert to UTC according to RFC 5545
if (is_a($val, 'DateTime')) {
$until = clone $val;
$until->setTimezone(new DateTimeZone('UTC'));
$val = $until->format('Ymd\THis\Z');
}
break;
case 'RDATE':
case 'EXDATE':
foreach ((array)$val as $i => $ex)
$val[$i] = $ex->format('Ymd\THis');
$val = join(',', (array)$val);
break;
case 'EXCEPTIONS':
continue 2;
}
$rrule .= $k . '=' . $val . ';';
}
return rtrim($rrule, ';');
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
public static function to_php_date_format($from)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return strtr(strtr($from, array(
'yyyy' => 'Y',
'yy' => 'y',
'MMMM' => 'F',
'MMM' => 'M',
'MM' => 'm',
'M' => 'n',
'dddd' => 'l',
'ddd' => 'D',
'dd' => 'd',
'd' => 'j',
'HH' => '**',
'hh' => '%%',
'H' => 'G',
'h' => 'g',
'mm' => 'i',
'ss' => 's',
'TT' => 'A',
'tt' => 'a',
'T' => 'A',
't' => 'a',
'u' => 'c',
)), array(
'**' => 'H',
'%%' => 'h',
));
}
/**
* Convert from PHP date() format to fullcalendar format string
*/
public static function from_php_date_format($from)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return strtr($from, array(
'y' => 'yy',
'Y' => 'yyyy',
'M' => 'MMM',
'F' => 'MMMM',
'm' => 'MM',
'n' => 'M',
'j' => 'd',
'd' => 'dd',
'D' => 'ddd',
'l' => 'dddd',
'H' => 'HH',
'h' => 'hh',
'G' => 'H',
'g' => 'h',
'i' => 'mm',
's' => 'ss',
'A' => 'TT',
'a' => 'tt',
'c' => 'u',
));
}
}
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index fe10f9d6..a5b0f737 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -1,281 +1,280 @@
<?php
/**
* Kolab Event model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@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/>.
*/
class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'status', 'cancelled');
protected $objclass = 'Event';
protected $read_func = 'readEvent';
protected $write_func = 'writeEvent';
/**
* Default constructor
*/
function __construct($data = null, $version = 3.0)
{
parent::__construct(is_string($data) ? $data : null, $version);
// got an Event object as argument
if (is_object($data) && is_a($data, $this->objclass)) {
$this->obj = $data;
$this->loaded = true;
}
// copy static property overriden by this class
$this->_scheduling_properties = self::$scheduling_properties;
}
/**
* Clones into an instance of libcalendaring's extended EventCal class
*
* @return mixed EventCal object or false on failure
*/
public function to_libcal()
{
static $error_logged = false;
if (class_exists('kolabcalendaring')) {
return new EventCal($this->obj);
}
else if (!$error_logged) {
$error_logged = true;
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabcalendaring module not found"
), true);
}
return false;
}
/**
* Set event properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
// set common xcal properties
parent::set($object);
// do the hard work of setting object values
$this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
$this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
$this->obj->setTransparency($object['free_busy'] == 'free');
$status = kolabformat::StatusUndefined;
if ($object['free_busy'] == 'tentative')
$status = kolabformat::StatusTentative;
if ($object['cancelled'])
$status = kolabformat::StatusCancelled;
else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
$status = $this->status_map[$object['status']];
$this->obj->setStatus($status);
// save recurrence exceptions
if (is_array($object['recurrence']) && is_array($object['recurrence']['EXCEPTIONS'])) {
$recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
$vexceptions = new vectorevent;
foreach((array)$object['recurrence']['EXCEPTIONS'] as $i => $exception) {
$exevent = new kolab_format_event;
$exevent->set(($compacted = $this->compact_exception($exception, $object))); // only save differing values
- console('COMPACTED', $compacted);
// get value for recurrence-id
if (!empty($exception['recurrence_date']) && is_a($exception['recurrence_date'], 'DateTime')) {
$recurrence_id = $exception['recurrence_date'];
$compacted['_instance'] = $recurrence_id->format($recurrence_id_format);
}
else if (!empty($exception['_instance']) && strlen($exception['_instance']) > 4) {
$recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $object['start']->getTimezone());
$compacted['recurrence_date'] = $recurrence_id;
}
$exevent->obj->setRecurrenceID(self::get_datetime($recurrence_id ?: $exception['start'], null, $object['allday']), (bool)$exception['thisandfuture']);
$vexceptions->push($exevent->obj);
// write cleaned-up exception data back to memory/cache
$object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
}
$this->obj->setExceptions($vexceptions);
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
(is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
}
/**
* Convert the Event object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Event data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common xcal props
$object = parent::to_array($data);
// read object properties
$object += array(
'end' => self::php_datetime($this->obj->end()),
'allday' => $this->obj->start()->isDateOnly(),
'free_busy' => $this->obj->transparency() ? 'free' : 'busy', // TODO: transparency is only boolean
'attendees' => array(),
);
// derive event end from duration (#1916)
if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
$interval = new DateInterval('PT0S');
$interval->d = $duration->weeks() * 7 + $duration->days();
$interval->h = $duration->hours();
$interval->i = $duration->minutes();
$interval->s = $duration->seconds();
$object['end'] = clone $object['start'];
$object['end']->add($interval);
}
// organizer is part of the attendees list in Roundcube
if ($object['organizer']) {
$object['organizer']['role'] = 'ORGANIZER';
array_unshift($object['attendees'], $object['organizer']);
}
// status defines different event properties...
$status = $this->obj->status();
if ($status == kolabformat::StatusTentative)
$object['free_busy'] = 'tentative';
else if ($status == kolabformat::StatusCancelled)
$object['cancelled'] = true;
// this is an exception object
if ($this->obj->recurrenceID()->isValid()) {
$object['thisandfuture'] = $this->obj->thisAndFuture();
$object['recurrence_date'] = self::php_datetime($this->obj->recurrenceID());
}
// read exception event objects
else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
$recurrence_exceptions = array();
$recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
for ($i=0; $i < $exceptions->size(); $i++) {
if (($exobj = $exceptions->get($i))) {
$exception = new kolab_format_event($exobj);
if ($exception->is_valid()) {
$exdata = $exception->to_array();
// fix date-only recurrence ID saved by old versions
if ($exdata['recurrence_date'] && $exdata['recurrence_date']->_dateonly && !$object['allday']) {
$exdata['recurrence_date']->setTimezone($object['start']->getTimezone());
$exdata['recurrence_date']->setTime($object['start']->format('G'), intval($object['start']->format('i')), intval($object['start']->format('s')));
}
$recurrence_id = $exdata['recurrence_date'] ?: $exdata['start'];
$exdata['_instance'] = $recurrence_id->format($recurrence_id_format);
$recurrence_exceptions[] = $this->expand_exception($exdata, $object);
}
}
}
$object['recurrence']['EXCEPTIONS'] = $recurrence_exceptions;
}
return $this->data = $object;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = parent::get_tags();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
return $tags;
}
/**
* Remove some attributes from the exception container
*/
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','_attachments');
$whitelist = array('start','end');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
unset($exception[$prop]);
}
}
foreach ($master as $prop => $value) {
if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value && !in_array($prop, $whitelist)) {
unset($exception[$prop]);
}
}
// preserve this property for date serialization
$exception['allday'] = $master['allday'];
return $exception;
}
/**
* Copy attributes not specified by the exception from the master event
*/
private function expand_exception($exception, $master)
{
foreach ($master as $prop => $value) {
if (empty($exception[$prop]) && !empty($value)) {
$exception[$prop] = $value;
if ($prop == 'recurrence') {
unset($exception[$prop]['EXCEPTIONS']);
}
}
}
return $exception;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, Feb 4, 10:31 AM (5 h, 11 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427648
Default Alt Text
(141 KB)

Event Timeline