Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2571855
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
215 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index cf6c2ff1..9b859bbd 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,2433 +1,2433 @@
<?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-2015, 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->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
$this->bonnie_api = libkolab::get_bonnie_api();
// calendar uses fully encoded identifiers
kolab_storage::$encode_ids = true;
}
/**
* Read available calendars from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type, sorted by namespace/name
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
$this->calendars = array();
foreach ($folders as $folder) {
$calendar = $this->_to_calendar($folder);
if ($calendar->ready) {
$this->calendars[$calendar->id] = $calendar;
if ($calendar->editable) {
$this->has_writeable = true;
}
}
}
return $this->calendars;
}
/**
* Convert kolab_storage_folder into kolab_calendar
*/
private function _to_calendar($folder)
{
if ($folder instanceof kolab_calendar) {
return $folder;
}
if ($folder instanceof kolab_storage_folder_user) {
$calendar = new kolab_user_calendar($folder, $this->cal);
$calendar->subscriptions = count($folder->children) > 0;
}
else {
$calendar = new kolab_calendar($folder->name, $this->cal);
}
return $calendar;
}
/**
* Get a list of available calendars from this source
*
* @param integer $filter Bitmask defining filter criterias
* @param object $tree Reference to hierarchical folder tree object
*
* @return array List of calendars
*/
public function list_calendars($filter = 0, &$tree = null)
{
$this->_read_calendars();
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
unset($this->calendars);
$this->_read_calendars();
}
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$folders = $this->filter_calendars($filter);
$calendars = array();
// include virtual folders for a full folder tree
if (!is_null($tree))
$folders = kolab_storage::folder_hierarchy($folders, $tree);
$parents = array_keys($this->calendars);
foreach ($folders as $id => $cal) {
$imap_path = explode($delim, $cal->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !in_array($parent_id, $parents));
// restore "real" parent ID
if ($parent_id && !in_array($parent_id, $parents)) {
$parent_id = kolab_storage::folder_id($cal->get_parent());
}
$parents[] = $cal->id;
if ($cal->virtual) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_foldername(),
'editname' => $cal->get_foldername(),
'virtual' => true,
'editable' => false,
'group' => $cal->get_namespace(),
);
}
else {
// additional folders may come from kolab_storage::folder_hierarchy() above
// make sure we deal with kolab_calendar instances
$cal = $this->_to_calendar($cal);
$this->calendars[$cal->id] = $cal;
$is_user = ($cal instanceof kolab_user_calendar);
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_foldername(),
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'editable' => $cal->editable,
'group' => $is_user ? 'other user' : $cal->get_namespace(),
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'removable' => !$cal->default,
);
if (!$is_user) {
$calendars[$cal->id] += array(
'default' => $cal->default,
'rights' => $cal->rights,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'children' => true, // TODO: determine if that folder indeed has child folders
'parent' => $parent_id,
'subtype' => $cal->subtype,
'caldavurl' => $cal->get_caldav_url(),
);
}
}
if ($cal->subscriptions) {
$calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
}
}
// list virtual calendars showing invitations
if ($this->rc->config->get('kolab_invitation_calendars') && !($filter & self::FILTER_INSERTABLE)) {
foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
$cal = new kolab_invitation_calendar($id, $this->cal);
if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
$calendars[$id] = array(
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_name(),
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'editable' => $cal->editable,
'rights' => $cal->rights,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => false,
);
if ($id == self::INVITATIONS_CALENDAR_PENDING) {
$calendars[$id]['counts'] = true;
}
if (is_object($tree)) {
$tree->children[] = $cal;
}
}
}
}
// append the virtual birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays', false) && !($filter & self::FILTER_INSERTABLE)) {
$id = self::BIRTHDAY_CALENDAR_ID;
$prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs
if (!($filter & self::FILTER_ACTIVE) || $prefs[$id]['active']) {
$calendars[$id] = array(
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
'listname' => $this->cal->gettext('birthdays'),
'color' => $prefs[$id]['color'] ?: '87CEFA',
'active' => (bool)$prefs[$id]['active'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'group' => 'x-birthdays',
'editable' => false,
'default' => false,
'children' => false,
'history' => false,
);
}
}
return $calendars;
}
/**
* Get list of calendars according to specified filters
*
* @param integer Bitmask defining restrictions. See FILTER_* constants for possible values.
*
* @return array List of calendars
*/
protected function filter_calendars($filter)
{
$this->_read_calendars();
$calendars = array();
$plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array(
'list' => $this->calendars,
'calendars' => $calendars,
'filter' => $filter,
));
if ($plugin['abort']) {
return $plugin['calendars'];
}
$personal = $filter & self::FILTER_PERSONAL;
$shared = $filter & self::FILTER_SHARED;
foreach ($this->calendars as $cal) {
if (!$cal->ready) {
continue;
}
if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) {
continue;
}
if (($filter & self::FILTER_INSERTABLE) && !$cal->editable) {
continue;
}
if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) {
continue;
}
if (($filter & self::FILTER_PRIVATE) && $cal->subtype != 'private') {
continue;
}
if (($filter & self::FILTER_CONFIDENTIAL) && $cal->subtype != 'confidential') {
continue;
}
if ($personal || $shared) {
$ns = $cal->get_namespace();
if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) {
continue;
}
}
$calendars[$cal->id] = $cal;
}
return $calendars;
}
/**
* Get the kolab_calendar instance for the given calendar ID
*
* @param string Calendar identifier (encoded imap folder name)
*
* @return object kolab_calendar Object nor null if calendar doesn't exist
*/
public function get_calendar($id)
{
$this->_read_calendars();
// create calendar object if necesary
if (!$this->calendars[$id]) {
if (in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
return new kolab_invitation_calendar($id, $this->cal);
}
// for unsubscribed calendar folders
if ($id !== self::BIRTHDAY_CALENDAR_ID) {
$calendar = kolab_calendar::factory($id, $this->cal);
if ($calendar->ready) {
$this->calendars[$calendar->id] = $calendar;
}
}
}
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, $scope = 0, $full = false)
{
if (is_array($event)) {
$id = $event['id'] ?: $event['uid'];
$cal = $event['calendar'];
// we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
if (!$event['id'] && $event['_instance']) {
$id .= '-' . $event['_instance'];
}
}
else {
$id = $event;
}
if ($cal) {
if ($storage = $this->get_calendar($cal)) {
$result = $storage->get_event($id);
return self::to_rcube_event($result);
}
// get event from the address books birthday calendar
else if ($cal == self::BIRTHDAY_CALENDAR_ID) {
return $this->get_birthday_event($id);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->filter_calendars($scope) as $calendar) {
if ($result = $calendar->get_event($id)) {
return self::to_rcube_event($result);
}
}
}
return false;
}
/**
* Add a single event to the database
*
* @see calendar_driver::new_event()
*/
public function new_event($event)
{
if (!$this->validate($event))
return false;
$event = self::from_rcube_event($event);
if (!$event['calendar']) {
$this->_read_calendars();
$event['calendar'] = reset(array_keys($this->calendars));
}
if ($storage = $this->get_calendar($event['calendar'])) {
// if this is a recurrence instance, append as exception to an already existing object for this UID
if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
self::add_exception($master, $event);
$success = $storage->update_event($master);
}
else {
$success = $storage->insert_event($event);
}
if ($success && $this->freebusy_trigger) {
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
$this->freebusy_trigger = false; // disable after first execution (#2355)
}
return $success;
}
return false;
}
/**
* Update an event entry with the given data
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function edit_event($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
}
/**
* Extended event editing with possible changes to the argument
*
* @param array Hash array with event properties
* @param string New participant status
* @param array List of hash arrays with updated attendees
* @return boolean True on success, False on error
*/
public function edit_rsvp(&$event, $status, $attendees)
{
$update_event = $event;
// apply changes to master (and all exceptions)
if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
if ($storage = $this->get_calendar($event['calendar'])) {
$update_event = $storage->get_event($event['recurrence_id']);
$update_event['_savemode'] = $event['_savemode'];
$update_event['id'] = $update_event['uid'];
unset($update_event['recurrence_id']);
calendar::merge_attendee_data($update_event, $attendees);
}
}
if ($ret = $this->update_attendees($update_event, $attendees)) {
// replace with master event (for iTip reply)
$event = self::to_rcube_event($update_event);
// re-assign to the according (virtual) calendar
if ($this->rc->config->get('kolab_invitation_calendars')) {
if (strtoupper($status) == 'DECLINED')
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
else if (strtoupper($status) == 'NEEDS-ACTION')
$event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
else if ($event['_folder_id'])
$event['calendar'] = $event['_folder_id'];
}
}
return $ret;
}
/**
* Update the participant status for the given attendees
*
* @see calendar_driver::update_attendees()
*/
public function update_attendees(&$event, $attendees)
{
// for this-and-future updates, merge the updated attendees onto all exceptions in range
if (($event['_savemode'] == 'future' && $event['recurrence_id']) || (!empty($event['recurrence']) && !$event['recurrence_id'])) {
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// load master event
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
// apply attendee update to each existing exception
if ($master['recurrence'] && !empty($master['recurrence']['EXCEPTIONS'])) {
$saved = false;
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// merge the new event properties onto future exceptions
if ($exception['_instance'] >= strval($event['_instance'])) {
calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
}
// update a specific instance
if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
$saved = true;
}
}
// add the given event as new exception
if (!$saved && $event['id'] != $master['id']) {
$event['thisandfuture'] = true;
$master['recurrence']['EXCEPTIONS'][] = $event;
}
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
return $this->update_event($master);
}
}
// just update the given event (instance)
return $this->update_event($event);
}
/**
* Move a single event
*
* @see calendar_driver::move_event()
* @return boolean True on success, False on error
*/
public function move_event($event)
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
self::clear_attandee_noreply($ev);
return $this->update_event($event + $ev);
}
return false;
}
/**
* Resize a single event
*
* @see calendar_driver::resize_event()
* @return boolean True on success, False on error
*/
public function resize_event($event)
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
self::clear_attandee_noreply($ev);
return $this->update_event($event + $ev);
}
return false;
}
/**
* Remove a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return boolean True on success, False on error
*/
public function remove_event($event, $force = true)
{
$ret = true;
$success = false;
$savemode = $event['_savemode'];
$decline = $event['_decline'];
if (!$force) {
unset($event['attendees']);
$this->rc->session->remove('calendar_event_undo');
$this->rc->session->remove('calendar_restore_event_data');
$sess_data = $event;
}
if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $savemode;
$savemode = 'all';
$master = $event;
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id'] || $event['isexception']) {
$master = $storage->get_event($event['uid']);
$savemode = $event['_savemode'] ?: ($event['_instance'] || $event['isexception'] ? 'current' : 'all');
// force 'current' mode for single occurrences stored as exception
if (!$event['recurrence'] && !$event['recurrence_id'] && $event['isexception'])
$savemode = 'current';
}
// removing an exception instance
if (($event['recurrence_id'] || $event['isexception']) && is_array($master['exceptions'])) {
foreach ($master['exceptions'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
unset($master['exceptions'][$i]);
// set event date back to the actual occurrence
if ($exception['recurrence_date'])
$event['start'] = $exception['recurrence_date'];
}
}
if (is_array($master['recurrence'])) {
$master['recurrence']['EXCEPTIONS'] = &$master['exceptions'];
}
}
switch ($savemode) {
case 'current':
$_SESSION['calendar_restore_event_data'] = $master;
// remove the matching RDATE entry
if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
unset($master['recurrence']['RDATE'][$j]);
break;
}
}
}
// add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
$success = $storage->update_event($master);
break;
case 'future':
$master['_instance'] = libcalendaring::recurrence_instance_identifier($master);
if ($master['_instance'] != $event['_instance']) {
$_SESSION['calendar_restore_event_data'] = $master;
// set until-date on master event
$master['recurrence']['UNTIL'] = clone $event['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']);
// if all future instances are deleted, remove recurrence rule entirely (bug #1677)
if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
$master['recurrence'] = 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);
$ret = $master['uid'];
break;
}
default: // 'all' is default
// removing the master event with loose exceptions (not recurring though)
if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
// make the first exception the new master
$newmaster = array_shift($master['exceptions']);
$newmaster['exceptions'] = $master['exceptions'];
$newmaster['_attachments'] = $master['_attachments'];
$newmaster['_mailbox'] = $master['_mailbox'];
$newmaster['_msguid'] = $master['_msguid'];
$success = $storage->update_event($newmaster);
}
else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
// don't delete but set PARTSTAT=DECLINED
if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
$success = $storage->update_event($master);
}
}
if (!$success)
$success = $storage->delete_event($master, $force);
break;
}
}
if ($success && !$force) {
if ($master['_folder_id'])
$sess_data['_folder_id'] = $master['_folder_id'];
$_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $sess_data);
}
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array(
'action' => 'calendar/push-freebusy',
// _folder_id may be set by invitations calendar
'source' => $master['_folder_id'] ?: $storage->id,
));
return $success ? $ret : false;
}
/**
* Restore a single deleted event
*
* @param array Hash array with event properties:
* id: Event identifier
* calendar: Event calendar
*
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
if ($storage = $this->get_calendar($event['calendar'])) {
if (!empty($_SESSION['calendar_restore_event_data']))
$success = $storage->update_event($event = $_SESSION['calendar_restore_event_data']);
else
$success = $storage->restore_event($event);
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array(
'action' => 'calendar/push-freebusy',
// _folder_id may be set by invitations calendar
'source' => $event['_folder_id'] ?: $storage->id,
));
return $success;
}
return false;
}
/**
* Wrapper to update an event object depending on the given savemode
*/
private function update_event($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
return false;
$old = $fromcalendar->get_event($event['id']);
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
return false;
}
$fromcalendar = $storage;
}
}
else
$fromcalendar = $storage;
$success = false;
$savemode = 'all';
$attachments = array();
$old = $master = $storage->get_event($event['id']);
if (!$old || !$old['start']) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load event object to update: id=" . $event['id']),
true, false);
return false;
}
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) {
$master = $storage->get_event($old['uid']);
$savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all');
// this-and-future on the first instance equals to 'all'
if ($savemode == 'future' && $master['start'] && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master))
$savemode = 'all';
// force 'current' mode for single occurrences stored as exception
else if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception'])
$savemode = 'current';
// Stick to the master timezone for all occurrences (Bifrost#T104637)
$master_tz = $master['start']->getTimezone();
$event_tz = $event['start']->getTimezone();
if ($master_tz->getName() != $event_tz->getName()) {
$event['start']->setTimezone($master_tz);
$event['end']->setTimezone($master_tz);
}
}
// check if update affects scheduling and update attendee status accordingly
$reschedule = $this->check_scheduling($event, $old, true);
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
if (isset($event['recurrence']['EXCEPTIONS']))
$with_exceptions = true; // exceptions already provided (e.g. from iCal import)
else if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
else if ($old['exceptions'])
$event['exceptions'] = $old['exceptions'];
// remove some internal properties which should not be saved
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
$event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']);
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['_copyfrom'] = $master['_msguid'];
$event['_mailbox'] = $master['_mailbox'];
$event['uid'] = $this->cal->generate_uid();
unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
// copy attachment metadata to new event
$event = self::from_rcube_event($event, $master);
self::clear_attandee_noreply($event);
if ($success = $storage->insert_event($event))
$success = $event['uid'];
break;
case 'future':
// create a new recurring event
$event['_copyfrom'] = $master['_msguid'];
$event['_mailbox'] = $master['_mailbox'];
$event['uid'] = $this->cal->generate_uid();
unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
// copy attachment metadata to new event
$event = self::from_rcube_event($event, $master);
// remove recurrence exceptions on re-scheduling
if ($reschedule) {
unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
}
else if (is_array($event['recurrence']['EXCEPTIONS'])) {
// only keep relevant exceptions
$event['recurrence']['EXCEPTIONS'] = array_filter($event['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
return $exception['start'] > $event['start'];
});
if (is_array($event['recurrence']['EXDATE'])) {
$event['recurrence']['EXDATE'] = array_filter($event['recurrence']['EXDATE'], function($exdate) use ($event) {
return $exdate > $event['start'];
});
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
// compute remaining occurrences
if ($event['recurrence']['COUNT']) {
if (!$old['_count'])
$old['_count'] = $this->get_recurrence_count($master, $old['start']);
$event['recurrence']['COUNT'] -= intval($old['_count']);
}
// remove fixed weekday when date changed
if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) {
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
unset($event['recurrence']['BYMONTH']);
}
// set until-date on master event
$master['recurrence']['UNTIL'] = clone $old['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']);
// remove all exceptions after $event['start']
if (is_array($master['recurrence']['EXCEPTIONS'])) {
$master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
return $exception['start'] < $event['start'];
});
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
}
if (is_array($master['recurrence']['EXDATE'])) {
$master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function($exdate) use ($event) {
return $exdate < $event['start'];
});
}
// save new event
if ($success = $storage->insert_event($event)) {
$success = $event['uid'];
// update master event (no rescheduling!)
self::clear_attandee_noreply($master);
$storage->update_event($master);
}
break;
case 'current':
// recurring instances shall not store recurrence rules and attachments
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
unset($event['attachments'], $event['id']);
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
$event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
else if (!isset($event['sequence'])) {
$event['sequence'] = $old['sequence'] ?: $master['sequence'];
}
// save properties to a recurrence exception instance
if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
$success = $storage->update_event($master, $old['id']);
break;
}
}
$add_exception = true;
// adjust matching RDATE entry if dates changed
if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $old_date) {
$master['recurrence']['RDATE'][$j] = $event['start'];
sort($master['recurrence']['RDATE']);
$add_exception = false;
break;
}
}
}
// save as new exception to master event
if ($add_exception) {
self::add_exception($master, $event, $old);
}
$success = $storage->update_event($master);
break;
default: // 'all' is default
$event['id'] = $master['uid'];
$event['uid'] = $master['uid'];
// use start date from master but try to be smart on time or duration changes
$old_start_date = $old['start']->format('Y-m-d');
$old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
$old_duration = self::event_duration($old['start'], $old['end'], $old['allday']);
$new_start_date = $event['start']->format('Y-m-d');
$new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
$new_duration = self::event_duration($event['start'], $event['end'], $event['allday']);
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
$date_shift = $old['start']->diff($event['start']);
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start']->add($date_shift);
$event['end'] = clone $event['start'];
$event['end']->add(new DateInterval($new_duration));
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
if ($old_start_date != $new_start_date && $event['recurrence']) {
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
unset($event['recurrence']['BYMONTH']);
}
}
// dates did not change, use the ones from master
else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
$event['start'] = $master['start'];
$event['end'] = $master['end'];
}
// when saving an instance in 'all' mode, copy recurrence exceptions over
if ($old['recurrence_id']) {
$event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
$event['recurrence']['EXDATE'] = $master['recurrence']['EXDATE'];
}
else if ($master['_instance']) {
$event['_instance'] = $master['_instance'];
$event['recurrence_date'] = $master['recurrence_date'];
}
// TODO: forward changes to exceptions (which do not yet have differing values stored)
if (is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
// determine added and removed attendees
$old_attendees = $current_attendees = $added_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
foreach ((array)$event['attendees'] as $attendee) {
$current_attendees[] = $attendee['email'];
if (!in_array($attendee['email'], $old_attendees)) {
$added_attendees[] = $attendee;
}
}
$removed_attendees = array_diff($old_attendees, $current_attendees);
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
}
// adjust recurrence-id when start changed and therefore the entire recurrence chain changes
if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
$recurrence_id_format = libcalendaring::recurrence_id_format($event);
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
if (is_a($recurrence_id, 'DateTime')) {
$recurrence_id->add($date_shift);
$event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
$event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
}
}
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
// unset _dateonly flags in (cached) date objects
unset($event['start']->_dateonly, $event['end']->_dateonly);
$success = $storage->update_event($event) ? $event['id'] : false; // return master UID
break;
}
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Calculate event duration, returns string in DateInterval format
*/
protected static function event_duration($start, $end, $allday = false)
{
if ($allday) {
$diff = $start->diff($end);
return 'P' . $diff->days . 'D';
}
return 'PT' . ($end->format('U') - $start->format('U')) . 'S';
}
/**
* Determine whether the current change affects scheduling and reset attendee status accordingly
*/
public function check_scheduling(&$event, $old, $update = true)
{
// skip this check when importing iCal/iTip events
if (isset($event['sequence']) || !empty($event['_method'])) {
return false;
}
// iterate through the list of properties considered 'significant' for scheduling
$kolab_event = $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;
}
/**
* Apply the given changes to already existing exceptions
*/
protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
{
$saved = false;
$existing = null;
// determine added and removed attendees
$added_attendees = $removed_attendees = array();
if ($savemode == 'future') {
$old_attendees = $current_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
foreach ((array)$event['attendees'] as $attendee) {
$current_attendees[] = $attendee['email'];
if (!in_array($attendee['email'], $old_attendees)) {
$added_attendees[] = $attendee;
}
}
$removed_attendees = array_diff($old_attendees, $current_attendees);
}
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// update a specific instance
if ($exception['_instance'] == $old['_instance']) {
$existing = $i;
// check savemode against existing exception mode.
// if matches, we can update this existing exception
if ((bool)$exception['thisandfuture'] === ($savemode == 'future')) {
$event['_instance'] = $old['_instance'];
$event['thisandfuture'] = $old['thisandfuture'];
$event['recurrence_date'] = $old['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$saved = true;
}
}
// merge the new event properties onto future exceptions
if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
unset($event['thisandfuture']);
self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, array('attendees'));
if (!empty($added_attendees) || !empty($removed_attendees)) {
calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
}
}
}
/*
// we could not update the existing exception due to savemode mismatch...
if (!$saved && $existing !== null && $master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture']) {
// ... try to move the existing this-and-future exception to the next occurrence
foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
// our old this-and-future exception is obsolete
if ($candidate['thisandfuture']) {
unset($master['recurrence']['EXCEPTIONS'][$existing]);
$saved = true;
break;
}
// this occurrence doesn't yet have an exception
else if (!$candidate['isexception']) {
$event['_instance'] = $candidate['_instance'];
$event['recurrence_date'] = $candidate['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$saved = true;
break;
}
}
}
*/
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
// returning false here will add a new exception
return $saved;
}
/**
* Add or update the given event as an exception to $master
*/
public static function add_exception(&$master, $event, $old = null)
{
if ($old) {
$event['_instance'] = $old['_instance'];
if (!$event['recurrence_date'])
$event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
}
else if (!$event['recurrence_date']) {
$event['recurrence_date'] = $event['start'];
}
if (!$event['_instance'] && is_a($event['recurrence_date'], 'DateTime')) {
$event['_instance'] = libcalendaring::recurrence_instance_identifier($event, $master['allday']);
}
if (!is_array($master['exceptions']) && is_array($master['recurrence']['EXCEPTIONS'])) {
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
}
$existing = false;
foreach ((array)$master['exceptions'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
$master['exceptions'][$i] = $event;
$existing = true;
}
}
if (!$existing) {
$master['exceptions'][] = $event;
}
return true;
}
/**
* Remove the noreply flags from attendees
*/
public static function clear_attandee_noreply(&$event)
{
foreach ((array)$event['attendees'] as $i => $attendee) {
unset($event['attendees'][$i]['noreply']);
}
}
/**
* Merge certain properties from the overlay event to the base event object
*
* @param array The event object to be altered
* @param array The overlay event object to be merged over $event
* @param array List of properties not allowed to be overwritten
*/
public static function merge_exception_data(&$event, $overlay, $blacklist = null)
{
$forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
if (is_array($blacklist))
$forbidden = array_merge($forbidden, $blacklist);
foreach ($overlay as $prop => $value) {
if ($prop == 'start' || $prop == 'end') {
// handled by merge_exception_dates() below
}
else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
$event[$prop] = $value;
}
else if ($prop[0] != '_' && !in_array($prop, $forbidden))
$event[$prop] = $value;
}
self::merge_exception_dates($event, $overlay);
}
/**
* Merge start/end date from the overlay event to the base event object
*
* @param array The event object to be altered
* @param array The overlay event object to be merged over $event
*/
public static function merge_exception_dates(&$event, $overlay)
{
// compute date offset from the exception
if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) {
$date_offset = $overlay['recurrence_date']->diff($overlay['start']);
}
foreach (array('start', 'end') as $prop) {
$value = $overlay[$prop];
if (is_object($event[$prop]) && $event[$prop] instanceof DateTime) {
// set date value if overlay is an exception of the current instance
if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
$event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
}
// apply date offset
else if ($date_offset) {
$event[$prop]->add($date_offset);
}
// adjust time of the recurring event instance
$event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
}
}
}
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Include virtual events (optional)
* @param integer Only list events modified since this time (unix timestamp)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
else if (!$calendars) {
$this->_read_calendars();
$calendars = array_keys($this->calendars);
}
$query = array();
if ($modifiedsince)
$query[] = array('changed', '>=', $modifiedsince);
$events = $categories = array();
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
$categories += $storage->categories;
}
}
// add events from the address books birthday calendar
if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
$events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
array_walk($events, 'kolab_driver::to_rcube_event');
return $events;
}
/**
* Get number of events in the given calendar
*
* @param mixed List of calendar IDs to count events (either as array or comma-separated string)
* @param integer Date range start (unix timestamp)
* @param integer Date range end (unix timestamp)
* @return array Hash array with counts grouped by calendar ID
*/
public function count_events($calendars, $start, $end = null)
{
$counts = array();
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
else if (!$calendars) {
$this->_read_calendars();
$calendars = array_keys($this->calendars);
}
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$counts[$cid] = $storage->count_events($start, $end);
}
}
return $counts;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return array();
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$time = $slot + $interval;
$candidates = array();
$query = array(array('tags', '=', 'x-has-alarms'));
$this->_read_calendars();
foreach ($this->calendars as $cid => $calendar) {
// skip calendars with alarms disabled
if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
continue;
foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($e);
if ($alarm && $alarm['time'] && $alarm['time'] >= $last && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $e['title'],
'location' => $e['location'],
'start' => $e['start'],
'end' => $e['end'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query("SELECT *"
. " FROM " . $this->rc->db->table_name('kolab_alarms', true)
. " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
. " AND `user_id` = ?",
$this->rc->user->ID
);
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['alarm_id']] = $e;
}
}
$alarms = array();
foreach ($candidates as $id => $alarm) {
// skip dismissed alarms
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
if ($notifyat <= $time)
$alarms[] = $alarm;
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($alarm_id, $snooze = 0)
{
$alarms_table = $this->rc->db->table_name('kolab_alarms', true);
// delete old alarm entry
$this->rc->db->query("DELETE FROM $alarms_table"
. " WHERE `alarm_id` = ? AND `user_id` = ?",
$alarm_id,
$this->rc->user->ID
);
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query("INSERT INTO $alarms_table"
. " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
. " VALUES (?, ?, ?, ?)",
$alarm_id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments from the given event
*/
public function list_attachments($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
$event = $storage->get_event($event['id']);
return $event['attachments'];
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// get old revision of event
if ($event['rev']) {
$event = $this->get_event_revison($event, $event['rev'], true);
}
else {
$event = $storage->get_event($event['id']);
}
if ($event) {
$attachments = isset($event['_attachments']) ? $event['_attachments'] : $event['attachments'];
foreach ((array) $attachments as $att) {
if ($att['id'] == $id) {
return $att;
}
}
}
}
/**
* Get attachment body
* @see calendar_driver::get_attachment_body()
*/
public function get_attachment_body($id, $event)
{
if (!($cal = $this->get_calendar($event['calendar'])))
return false;
// get old revision of event
if ($event['rev']) {
if (empty($this->bonnie_api)) {
return false;
}
$cid = substr($id, 4);
// call Bonnie API and get the raw mime message
list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['rev'], $mailbox, $msguid)) {
// parse the message and find the part with the matching content-id
$message = rcube_mime::parse_message($msg_raw);
foreach ((array)$message->parts as $part) {
if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
return $part->body;
}
}
}
return false;
}
return $cal->get_attachment_body($id, $event);
}
/**
* Build a struct representing the given message reference
*
* @see calendar_driver::get_message_reference()
*/
public function get_message_reference($uri_or_headers, $folder = null)
{
if (is_object($uri_or_headers)) {
$uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
}
if (is_string($uri_or_headers)) {
return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
}
return false;
}
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
// FIXME: complete list with categories saved in config objects (KEP:12)
return $this->rc->config->get('calendar_categories', $this->default_categories);
}
/**
* Create instances of a recurring event
*
* @param array Hash array with event properties
* @param object DateTime Start date of the recurrence window
* @param object DateTime End date of the recurrence window
* @return array List of recurring event instances
*/
public function get_recurring_events($event, $start, $end = null)
{
// load the given event data into a libkolabxml container
if (!$event['_formatobj']) {
$event_xml = new kolab_format_event();
$event_xml->set($event);
$event['_formatobj'] = $event_xml;
}
$this->_read_calendars();
$storage = reset($this->calendars);
return $storage->get_recurring_events($event, $start, $end);
}
/**
*
*/
private function get_recurrence_count($event, $dtstart)
{
// load the given event data into a libkolabxml container
if (!$event['_formatobj']) {
$event_xml = new kolab_format_event();
$event_xml->set($event);
$event['_formatobj'] = $event_xml;
}
// use libkolab to compute recurring events
$recurrence = new kolab_date_recurrence($event['_formatobj']);
$count = 0;
while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
$count++;
}
return $count;
}
/**
* Fetch free/busy information from a person within the given range
*/
public function get_freebusy_list($email, $start, $end)
{
if (empty($email)/* || $end < time()*/)
return false;
// map vcalendar fbtypes to internal values
$fbtypemap = array(
'FREE' => calendar::FREEBUSY_FREE,
'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE,
'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF,
'OOF' => calendar::FREEBUSY_OOF);
// ask kolab server first
try {
$request_config = array(
'store_body' => true,
'follow_redirects' => true,
);
$request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config);
$response = $request->send();
// authentication required
if ($response->getStatus() == 401) {
$request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
$response = $request->send();
}
if ($response->getStatus() == 200)
$fbdata = $response->getBody();
unset($request, $response);
}
catch (Exception $e) {
PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage());
}
// get free-busy url from contacts
if (!$fbdata) {
$fburl = null;
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
$abook = $this->rc->get_address_book($book);
if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) {
while ($contact = $result->iterate()) {
if ($fburl = $contact['freebusyurl']) {
$fbdata = @file_get_contents($fburl);
break;
}
}
}
if ($fbdata)
break;
}
}
// parse free-busy information using Horde classes
if ($fbdata) {
$ical = $this->cal->get_ical();
$ical->import($fbdata);
if ($fb = $ical->freebusy) {
$result = array();
foreach ($fb['periods'] as $tuple) {
list($from, $to, $type) = $tuple;
$result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
}
// we take 'dummy' free-busy lists as "unknown"
if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy'))
return false;
// set period from $start till the begin of the free-busy information as 'unknown'
if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
}
// pad period till $end with status 'unknown'
if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
$result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
}
return $result;
}
}
return false;
}
/**
* Handler to push folder triggers when sent from client.
* Used to push free-busy changes asynchronously after updating an event
*/
public function push_freebusy()
{
// make shure triggering completes
set_time_limit(0);
ignore_user_abort(true);
$cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
if (!($cal = $this->get_calendar($cal)))
return false;
// trigger updates on folder
$trigger = $cal->storage->trigger();
if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
true, false);
}
exit;
}
/**
* Convert from driver format to external caledar app data
*/
public static function to_rcube_event(&$record)
{
if (!is_array($record))
return $record;
$record['id'] = $record['uid'];
if ($record['_instance']) {
$record['id'] .= '-' . $record['_instance'];
if (!$record['recurrence_id'] && !empty($record['recurrence']))
$record['recurrence_id'] = $record['uid'];
}
// all-day events go from 12:00 - 13:00
if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = clone $record['start'];
$record['end']->add(new DateInterval('PT1H'));
}
// translate internal '_attachments' to external 'attachments' list
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
unset($attachment['path'], $attachment['content']);
$attachments[] = $attachment;
}
}
$record['attachments'] = $attachments;
}
if (!empty($record['attendees'])) {
foreach ((array)$record['attendees'] as $i => $attendee) {
if (is_array($attendee['delegated-from'])) {
$record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
}
if (is_array($attendee['delegated-to'])) {
$record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
}
}
}
// Roundcube only supports one category assignment
if (is_array($record['categories']))
$record['categories'] = $record['categories'][0];
// the cancelled flag transltes into status=CANCELLED
if ($record['cancelled'])
$record['status'] = 'CANCELLED';
// The web client only supports DISPLAY type of alarms
if (!empty($record['alarms']))
$record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
// remove empty recurrence array
if (empty($record['recurrence']))
unset($record['recurrence']);
// clean up exception data
if (is_array($record['recurrence']['EXCEPTIONS'])) {
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
});
}
unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
$record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']);
return $record;
}
/**
*
*/
public static function from_rcube_event($event, $old = array())
{
kolab_format::merge_attachments($event, $old);
return $event;
}
/**
* Set CSS class according to the event's attendde partstat
*/
public static function add_partstat_class($event, $partstats, $user = null)
{
// set classes according to PARTSTAT
if (is_array($event['attendees'])) {
$user_emails = libcalendaring::get_instance()->get_user_emails($user);
$partstat = 'UNKNOWN';
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails)) {
$partstat = $attendee['status'];
break;
}
}
if (in_array($partstat, $partstats)) {
$event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
}
}
return $event;
}
/**
* Provide a list of revisions for the given event
*
* @param array $event Hash array with event properties
*
* @return array List of changes, each as a hash array
* @see calendar_driver::get_event_changelog()
*/
public function get_event_changelog($event)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
$result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
return $result['changes'];
}
return false;
}
/**
* Get a list of property changes beteen two revisions of an event
*
* @param array $event Hash array with event properties
* @param mixed $rev1 Old Revision
* @param mixed $rev2 New Revision
*
* @return array List of property changes, each as a hash array
* @see calendar_driver::get_event_diff()
*/
public function get_event_diff($event, $rev1, $rev2)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
// get diff for the requested recurrence instance
$instance_id = $event['id'] != $uid ? substr($event['id'], strlen($uid) + 1) : null;
// call Bonnie API
$result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
$keymap = array(
'dtstart' => 'start',
'dtend' => 'end',
'dstamp' => 'changed',
'summary' => 'title',
'alarm' => 'alarms',
'attendee' => 'attendees',
'attach' => 'attachments',
'rrule' => 'recurrence',
'transparency' => 'free_busy',
'classification' => 'sensitivity',
'lastmodified-date' => 'changed',
);
$prop_keymaps = array(
'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
'attendees' => array('partstat' => 'status'),
);
$special_changes = array();
// map kolab event properties to keys the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// translate free_busy values
if ($change['property'] == 'free_busy') {
$change['old'] = $old['old'] ? 'free' : 'busy';
$change['new'] = $old['new'] ? 'free' : 'busy';
}
// map alarms trigger value
if ($change['property'] == 'alarms') {
if (is_array($change['old']) && is_array($change['old']['trigger']))
$change['old']['trigger'] = $change['old']['trigger']['value'];
if (is_array($change['new']) && is_array($change['new']['trigger']))
$change['new']['trigger'] = $change['new']['trigger']['value'];
}
// make all property keys uppercase
if ($change['property'] == 'recurrence') {
$special_changes['recurrence'] = $i;
foreach (array('old','new') as $m) {
if (is_array($change[$m])) {
$props = array();
foreach ($change[$m] as $k => $v)
$props[strtoupper($k)] = $v;
$change[$m] = $props;
}
}
}
// 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, $internal = false)
{
if (empty($this->bonnie_api)) {
return false;
}
$eventid = $event['id'];
$calid = $event['calendar'];
list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
// call Bonnie API
$result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('event');
$format->load($result['xml']);
$event = $format->to_array();
$format->get_attachments($event, true);
// get the right instance from a recurring event
if ($eventid != $event['uid']) {
$instance_id = substr($eventid, strlen($event['uid']) + 1);
// check for recurrence exception first
if ($instance = $format->get_instance($instance_id)) {
$event = $instance;
}
else {
// not a exception, compute recurrence...
$event['_formatobj'] = $format;
$recurrence_date = rcube_utils::anytodatetime($instance_id, $event['start']->getTimezone());
foreach ($this->get_recurring_events($event, $event['start'], $recurrence_date) as $instance) {
if ($instance['id'] == $eventid) {
$event = $instance;
break;
}
}
}
}
if ($format->is_valid()) {
$event['calendar'] = $calid;
$event['rev'] = $result['rev'];
return $internal ? $event : self::to_rcube_event($event);
}
}
return false;
}
/**
* Command the backend to restore a certain revision of an event.
* This shall replace the current event with an older version.
*
* @param mixed UID string or hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
* @param mixed $rev Revision number
*
* @return boolean True on success, False on failure
*/
public function restore_event_revision($event, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
$calendar = $this->get_calendar($event['calendar']);
$success = false;
if ($calendar && $calendar->storage && $calendar->editable) {
if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) {
$imap = $this->rc->get_storage();
// insert $raw_msg as new message
if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) {
$success = true;
// delete old revision from imap and cache
$imap->delete_message($msguid, $calendar->storage->name);
$calendar->storage->cache->set($msguid, false);
}
}
}
return $success;
}
/**
* Helper method to resolved the given event identifier into uid and folder
*
* @return array (uid,folder,msguid) tuple
*/
private function _resolve_event_identity($event)
{
$mailbox = $msguid = null;
if (is_array($event)) {
$uid = $event['uid'] ?: $event['id'];
if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
$mailbox = $cal->get_mailbox_id();
// get event object from storage in order to get the real object uid an msguid
if ($ev = $cal->get_event($event['id'])) {
$msguid = $ev['_msguid'];
$uid = $ev['uid'];
}
}
}
else {
$uid = $event;
// get event object from storage in order to get the real object uid an msguid
if ($ev = $this->get_event($event)) {
$mailbox = $ev['_mailbox'];
$msguid = $ev['_msguid'];
$uid = $ev['uid'];
}
}
return array($uid, $mailbox, $msguid);
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
// 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']);
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => $formfields,
);
return kolab_utils::folder_form($form, '', 'calendar');
}
$this->_read_calendars();
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);
+ $path_imap = implode($delim, $path_imap);
$options = $storage->folder_info($folder);
}
else {
$path_imap = '';
}
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
// 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']['fields']['location'] = $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']['fields']['path'] = array(
'id' => 'calendar-parent',
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// calendar color (default field)
$form['props']['fields']['color'] = $formfields['color'];
$form['props']['fields']['alarms'] = $formfields['showalarms'];
return kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
$db = $this->rc->get_dbh();
foreach (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/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
index 58e09731..710b5562 100644
--- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -1,256 +1,256 @@
<?php
/**
* Kolab address book UI
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_addressbook_ui
{
private $plugin;
private $rc;
/**
* Class constructor
*
* @param kolab_addressbook $plugin Plugin object
*/
public function __construct($plugin)
{
$this->rc = rcube::get_instance();
$this->plugin = $plugin;
$this->init_ui();
}
/**
* Adds folders management functionality to Addressbook UI
*/
private function init_ui()
{
if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action) && $this->rc->action != 'show') {
return;
}
// Include script
$this->plugin->include_script('kolab_addressbook.js');
if (empty($this->rc->action)) {
// Include stylesheet (for directorylist)
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css');
// include kolab folderlist widget if available
if (in_array('libkolab', $this->plugin->api->loaded_plugins())) {
$this->plugin->api->include_script('libkolab/libkolab.js');
}
$this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.search_addon', false, false));
// Add actions on address books
$options = array('book-create', 'book-edit', 'book-delete', 'book-remove');
$idx = 0;
if ($dav_url = $this->rc->config->get('kolab_addressbook_carddav_url')) {
$options[] = 'book-showurl';
$this->rc->output->set_env('kolab_addressbook_carddav_url', true);
// set CardDAV URI for specified ldap addressbook
if ($ldap_abook = $this->rc->config->get('kolab_addressbook_carddav_ldap')) {
$dav_ldap_url = strtr($dav_url, array(
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($this->rc->get_user_name()),
'%i' => 'ldap-directory',
'%n' => '',
));
$this->rc->output->set_env('kolab_addressbook_carddav_ldap', $ldap_abook);
$this->rc->output->set_env('kolab_addressbook_carddav_ldap_url', $dav_ldap_url);
}
}
foreach ($options as $command) {
$content = html::tag('li', $idx ? null : array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'kolab_addressbook.'.str_replace('-', '', $command),
'domain' => $this->ID,
'class' => str_replace('-', ' ', $command) . ' disabled',
'classact' => str_replace('-', ' ', $command) . ' active',
'command' => $command,
'type' => 'link'
)));
$this->plugin->api->add_content($content, 'groupoptions');
$idx++;
}
// Link to Settings/Folders
$content = html::tag('li', array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'managefolders',
'type' => 'link',
'class' => 'folders disabled',
'classact' => 'folders active',
'command' => 'folders',
'task' => 'settings',
)));
$this->plugin->api->add_content($content, 'groupoptions');
$this->rc->output->add_label(
'kolab_addressbook.bookdeleteconfirm',
'kolab_addressbook.bookdeleting',
'kolab_addressbook.carddavurldescription',
'kolab_addressbook.bookdelete',
'kolab_addressbook.bookshowurl',
'kolab_addressbook.bookedit',
'kolab_addressbook.bookcreate',
'kolab_addressbook.nobooknamewarning',
'kolab_addressbook.booksaving',
'kolab_addressbook.findaddressbooks',
'kolab_addressbook.searchterms',
'kolab_addressbook.foldersearchform',
'kolab_addressbook.listsearchresults',
'kolab_addressbook.nraddressbooksfound',
'kolab_addressbook.noaddressbooksfound',
'kolab_addressbook.foldersubscribe',
'resetsearch'
);
if ($this->plugin->bonnie_api) {
$this->rc->output->set_env('kolab_audit_trail', true);
$this->plugin->api->include_script('libkolab/libkolab.js');
$this->rc->output->add_label(
'kolab_addressbook.showhistory',
'kolab_addressbook.objectchangelog',
'kolab_addressbook.objectdiff',
'kolab_addressbook.objectdiffnotavailable',
'kolab_addressbook.objectchangelognotavailable',
'kolab_addressbook.revisionrestoreconfirm'
);
$this->plugin->add_hook('render_page', array($this, 'render_audittrail_page'));
$this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
}
}
// include stylesheet for audit trail
else if ($this->rc->action == 'show' && $this->plugin->bonnie_api) {
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css', true);
$this->rc->output->add_label('kolab_addressbook.showhistory');
}
}
/**
* Handler for address book create/edit action
*/
public function book_edit()
{
$this->rc->output->set_env('pagetitle', $this->plugin->gettext('bookproperties'));
$this->rc->output->add_handler('folderform', array($this, 'book_form'));
$this->rc->output->send('libkolab.folderform');
}
/**
* Handler for 'bookdetails' object returning form content for book create/edit
*
* @param array $attr Object attributes
*
* @return string HTML output
*/
public function book_form($attrib)
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
$folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true)); // UTF8
$hidden_fields[] = array('name' => '_source', 'value' => $folder);
$folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
if ($action == 'edit') {
$path_imap = explode($delim, $folder);
$name = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
- $path_imap = implode($path_imap, $delim);
+ $path_imap = implode($delim, $path_imap);
}
else { // create
$path_imap = $folder;
$name = '';
$folder = '';
}
// Store old name, get folder options
if (strlen($folder)) {
$hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
$options = $storage->folder_info($folder);
}
$form = array();
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$foldername = rcube::Q(str_replace($delim, ' » ', kolab_storage::object_name($folder)));
}
else {
$foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
$foldername = $foldername->show($name);
}
$form['properties']['fields']['name'] = array(
'label' => $this->plugin->gettext('bookname'),
'value' => $foldername,
'id' => '_name',
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_fields[] = array('name' => '_parent', 'value' => $path_imap);
}
else {
$prop = array('name' => '_parent', 'id' => '_parent');
$select = kolab_storage::folder_selector('contact', $prop, $folder);
$form['properties']['fields']['parent'] = array(
'label' => $this->plugin->gettext('parentbook'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
'id' => '_parent',
);
}
$form_html = kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
return html::tag('form', $attrib + array('action' => 'plugin.book-save', 'method' => 'post', 'id' => 'bookpropform'), $form_html);
}
/**
*
*/
public function render_audittrail_page($p)
{
// append audit trail UI elements to contact page
if ($p['template'] === 'addressbook' && !$p['kolab-audittrail']) {
$this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.audittrail', false, false));
$p['kolab-audittrail'] = true;
}
return $p;
}
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index cfc9e693..f6805eb0 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1340 +1,1340 @@
<?php
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the kolab_storage wrapper from libkolab
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011, 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/>.
*
* @see rcube_addressbook
*/
class rcube_kolab_contacts extends rcube_addressbook
{
public $primary_key = 'ID';
public $rights = 'lrs';
public $readonly = true;
public $undelete = true;
public $groups = true;
public $coltypes = array(
'name' => array('limit' => 1),
'firstname' => array('limit' => 1),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => array('home','work','other')),
'phone' => array(),
'address' => array('subtypes' => array('home','work','office')),
'website' => array('subtypes' => array('homepage','blog')),
'im' => array('subtypes' => null),
'gender' => array('limit' => 1),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
'manager' => array('limit' => null),
'assistant' => array('limit' => null),
'spouse' => array('limit' => 1),
'children' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => null,
'label' => 'kolab_addressbook.children', 'category' => 'personal'),
'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'),
'pgppublickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'),
'pkcs7publickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
'label' => 'kolab_addressbook.pkcs7publickey'),
'notes' => array('limit' => 1),
'photo' => array('limit' => 1),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
);
/**
* vCard additional fields mapping
*/
public $vcard_map = array(
'profession' => 'X-PROFESSION',
'officelocation' => 'X-OFFICE-LOCATION',
'initials' => 'X-INITIALS',
'children' => 'X-CHILDREN',
'freebusyurl' => 'X-FREEBUSY-URL',
'pgppublickey' => 'KEY',
);
/**
* List of date type fields
*/
public $date_cols = array('birthday', 'anniversary');
private $gid;
private $storagefolder;
private $dataset;
private $sortindex;
private $contacts;
private $distlists;
private $groupmembers;
private $filter;
private $result;
private $namespace;
private $imap_folder = 'INBOX/Contacts';
private $action;
// list of fields used for searching in "All fields" mode
private $search_fields = array(
'name',
'firstname',
'surname',
'middlename',
'prefix',
'suffix',
'nickname',
'jobtitle',
'organization',
'department',
'email',
'phone',
'address',
'profession',
'manager',
'assistant',
'spouse',
'children',
'notes',
);
public function __construct($imap_folder = null)
{
if ($imap_folder) {
$this->imap_folder = $imap_folder;
}
// extend coltypes configuration
$format = kolab_format::factory('contact');
$this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
$this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
$rcube = rcube::get_instance();
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (is_string($prop['label']))
$this->coltypes[$col]['label'] = $rcube->gettext($prop['label']);
}
// fetch objects from the given IMAP folder
$this->storagefolder = kolab_storage::get_folder($this->imap_folder);
$this->ready = $this->storagefolder && !PEAR::isError($this->storagefolder);
// Set readonly and rights flags according to folder permissions
if ($this->ready) {
if ($this->storagefolder->get_owner() == $_SESSION['username']) {
$this->readonly = false;
$this->rights = 'lrswikxtea';
}
else {
$rights = $this->storagefolder->get_myrights();
if ($rights && !PEAR::isError($rights)) {
$this->rights = $rights;
if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false)
$this->readonly = false;
}
}
}
$this->action = rcube::get_instance()->action;
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
public function get_name()
{
return $this->storagefolder->get_name();
}
/**
* Wrapper for kolab_storage_folder::get_foldername()
*/
public function get_foldername()
{
return $this->storagefolder->get_foldername();
}
/**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
*/
public function get_realname()
{
return $this->imap_folder;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if ($this->namespace === null && $this->ready) {
$this->namespace = $this->storagefolder->get_namespace();
}
return $this->namespace;
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
return $this->storagefolder->get_parent();
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->imap_folder);
}
/**
* Compose an URL for CardDAV access to this address book (if configured)
*/
public function get_carddav_url()
{
$rcmail = rcmail::get_instance();
if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
return strtr($template, array(
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($rcmail->get_user_name()),
'%i' => urlencode($this->storagefolder->get_uid()),
'%n' => urlencode($this->imap_folder),
));
}
return false;
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @param int Search mode. Sum of self::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(mb_strtolower($group['name']), mb_strtolower($search))) {
$groups[$group['ID']] = array('ID' => $group['ID'], 'name' => $group['name']);
}
}
// sort groups by name
uasort($groups, function($a, $b) { return strcoll($a['name'], $b['name']); });
return array_values($groups);
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @param boolean True to skip the count query (select only)
*
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols = null, $subset = 0, $nocount = false)
{
$this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
$fetch_all = false;
$fast_mode = !empty($cols) && is_array($cols);
// list member of the selected group
if ($this->gid) {
$this->_fetch_groups();
$this->sortindex = array();
$this->contacts = array();
$local_sortindex = array();
$uids = array();
// get members with email specified
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (!empty($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false) {
continue;
}
if (!empty($member['uid'])) {
$uids[] = $member['uid'];
}
else if (!empty($member['email'])) {
$this->contacts[$member['ID']] = $member;
$local_sortindex[$member['ID']] = $this->_sort_string($member);
$fetch_all = true;
}
}
// get members by UID
if (!empty($uids)) {
$this->_fetch_contacts($query = array(array('uid', '=', $uids)), $fetch_all ? false : count($uids), $fast_mode);
$this->sortindex = array_merge($this->sortindex, $local_sortindex);
}
}
else if (is_array($this->filter['ids'])) {
$ids = $this->filter['ids'];
if (count($ids)) {
$uids = array_map(array($this, 'id2uid'), $this->filter['ids']);
$this->_fetch_contacts($query = array(array('uid', '=', $uids)), count($ids), $fast_mode);
}
}
else {
$this->_fetch_contacts($query = 'contact', true, $fast_mode);
}
if ($fetch_all) {
// sort results (index only)
asort($this->sortindex, SORT_LOCALE_STRING);
$ids = array_keys($this->sortindex);
// fill contact data into the current result set
$this->result->count = count($ids);
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
for ($i = $start_row; $i < $last_row; $i++) {
if (array_key_exists($i, $ids)) {
$idx = $ids[$i];
$this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
}
}
}
else if (!empty($this->dataset)) {
// get all records count, skip the query if possible
if (!isset($query) || count($this->dataset) < $this->page_size) {
$this->result->count = count($this->dataset) + $this->page_size * ($this->list_page - 1);
}
else {
$this->result->count = $this->storagefolder->count($query);
}
$start_row = $subset < 0 ? $this->page_size + $subset : 0;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->page_size, $this->result->count);
for ($i = $start_row; $i < $last_row; $i++) {
$this->result->add($this->_to_rcube_contact($this->dataset[$i]));
}
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* 4 - include groups (if supported)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = $this->search_fields;
}
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else
$value = mb_strtolower($value);
$scount = count($fields);
// build key name regexp
- $regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
+ $regexp = '/^(' . implode('|', $fields) . ')(?:.*)$/';
// pass query to storage if only indexed cols are involved
// NOTE: this is only some rough pre-filtering but probably includes false positives
$squery = $this->_search_query($fields, $value, $mode);
// add magic selector to select contacts with birthday dates only
if (in_array('birthday', $required)) {
$squery[] = array('tags', '=', 'x-has-birthday');
}
$squery[] = array('type', '=', 'contact');
// get all/matching records
$this->_fetch_contacts($squery);
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
// search by iterating over all records in dataset
foreach ($this->dataset as $record) {
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f) {
// required field might be 'email', but contact might contain 'email:home'
if (!($v = rcube_addressbook::get_col_values($f, $contact, true)) || empty($v)) {
continue 2;
}
}
}
$found = array();
$contents = '';
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
foreach ((array)$contact[$col] as $val) {
if ($advanced) {
$found[$colname] = $this->compare_search_value($colname, $val, $value[array_search($colname, $fields)], $mode);
}
else {
$contents .= ' ' . join(' ', (array)$val);
}
}
}
// compare matches
if (($advanced && count($found) >= $scount) ||
(!$advanced && rcube_utils::words_match(mb_strtolower($contents), $value))) {
$this->filter['ids'][] = $id;
}
}
// dummy result with contacts count
if (!$select) {
return new rcube_result_set(count($this->filter['ids']), ($this->list_page-1) * $this->page_size);
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter)
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['mode']);
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
if ($this->gid) {
$this->_fetch_groups();
$count = count($this->distlists[$this->gid]['member']);
}
else if (is_array($this->filter['ids'])) {
$count = count($this->filter['ids']);
}
else {
$count = $this->storagefolder->count('contact');
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @param boolean True to return record as associative array, otherwise a result set is returned
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc=false)
{
$rec = null;
$uid = $this->id2uid($id);
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
if (strpos($uid, 'mailto:') === 0) {
$this->_fetch_groups(true);
$rec = $this->contacts[$id];
$this->readonly = true; // set source to read-only
}
else if (!empty($rev)) {
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->get_plugin('kolab_addressbook');
if ($plugin && ($object = $plugin->get_revision($id, kolab_storage::id_encode($this->imap_folder), $rev))) {
$rec = $this->_to_rcube_contact($object);
$rec['rev'] = $rev;
}
$this->readonly = true; // set source to read-only
}
else if ($object = $this->storagefolder->get_object($uid)) {
$rec = $this->_to_rcube_contact($object);
}
if ($rec) {
$this->result = new rcube_result_set(1);
$this->result->add($rec);
return $assoc ? $rec : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
foreach ((array)$this->groupmembers[$id] as $gid) {
if ($group = $this->distlists[$gid])
$out[$gid] = $group['name'];
}
return $out;
}
/**
* Create a new contact record
*
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
// remove existing id attributes (#1101)
unset($save_data['ID'], $save_data['uid']);
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$saved = $this->storagefolder->save($object, 'contact');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
}
else {
$insert_id = $this->uid2id($object['uid']);
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @return boolean True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
if ($old = $this->storagefolder->get_object($this->id2uid($id))) {
$object = $this->_from_rcube_contact($save_data, $old);
if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
}
else {
$updated = true;
// TODO: update data in groups this contact is member of
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return int Number of records deleted
*/
public function delete($ids, $force=true)
{
$this->_fetch_groups();
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
foreach ($ids as $id) {
if ($uid = $this->id2uid($id)) {
$is_mailto = strpos($uid, 'mailto:') === 0;
$deleted = $is_mailto || $this->storagefolder->delete($uid, $force);
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object $uid from the Kolab server"),
true, false);
}
else {
// remove from distribution lists
foreach ((array)$this->groupmembers[$id] as $gid) {
if (!$is_mailto || $gid == $this->gid)
$this->remove_from_group($gid, $id);
}
// clear internal cache
unset($this->groupmembers[$id]);
$count++;
}
}
}
return $count;
}
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
* @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
foreach ($ids as $id) {
$uid = $this->id2uid($id);
if ($this->storagefolder->undelete($uid)) {
$count++;
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting a contact object $uid from the Kolab server"),
true, false);
}
}
return $count;
}
/**
* Remove all records from the database
*
* @param bool $with_groups Remove also groups
*/
public function delete_all($with_groups = false)
{
if ($this->storagefolder->delete_all()) {
$this->contacts = array();
$this->sortindex = array();
$this->dataset = null;
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'name' => $name,
'member' => array(),
);
$saved = $this->storagefolder->save($list, 'distribution-list');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"),
true, false);
return false;
}
else {
$id = $this->uid2id($list['uid']);
$this->distlists[$id] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid]) {
$deleted = $this->storagefolder->delete($list['uid']);
}
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server"),
true, false);
}
else {
$result = true;
}
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
*
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname, &$newid)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['name']) {
$list['name'] = $newname;
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"),
true, false);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$this->_fetch_groups(true);
$list = $this->distlists[$gid];
$added = 0;
$uids = array();
$exists = array();
foreach ((array)$list['member'] as $member) {
$exists[] = $member['ID'];
}
// substract existing assignments from list
$ids = array_unique(array_diff($ids, $exists));
// add mailto: members
foreach ($ids as $contact_id) {
$uid = $this->id2uid($contact_id);
if (strpos($uid, 'mailto:') === 0 && ($contact = $this->contacts[$contact_id])) {
$list['member'][] = array(
'email' => $contact['email'],
'name' => $contact['name'],
);
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
else {
$uids[$uid] = $contact_id;
}
}
// add members with UID
if (!empty($uids)) {
foreach ($uids as $uid => $contact_id) {
$list['member'][] = array('uid' => $uid);
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added)
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
else
$saved = true;
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server"),
true, false);
$added = false;
$this->set_error(self::ERROR_SAVING, 'errorsaving');
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$this->_fetch_groups();
if (!($list = $this->distlists[$gid]))
return false;
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids))
$new_member[] = $member;
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"),
true, false);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Associative array with contact data to save
* @param bool Attempt to fix/complete data automatically
*
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
$valid = parent::validate($save_data);
// require at least one e-mail address if there's no name
// (syntax check is already done)
if ($valid) {
if (!strlen($save_data['name'])
&& !strlen($save_data['organization'])
&& !array_filter($this->get_col_values('email', $save_data, true))
) {
$this->set_error('warning', 'kolab_addressbook.noemailnamewarning');
$valid = false;
}
}
return $valid;
}
/**
* Query storage layer and store records in private member var
*/
private function _fetch_contacts($query = array(), $limit = false, $fast_mode = false)
{
if (!isset($this->dataset) || !empty($query)) {
if ($limit) {
$size = is_int($limit) && $limit < $this->page_size ? $limit : $this->page_size;
$this->storagefolder->set_order_and_limit($this->_sort_columns(), $size, ($this->list_page-1) * $this->page_size);
}
$this->sortindex = array();
$this->dataset = $this->storagefolder->select($query, $fast_mode);
foreach ($this->dataset as $idx => $record) {
$contact = $this->_to_rcube_contact($record);
$this->sortindex[$idx] = $this->_sort_string($contact);
}
}
}
/**
* Extract a string for sorting from the given contact record
*/
private function _sort_string($rec)
{
$str = '';
switch ($this->sort_col) {
case 'name':
$str = $rec['name'] . $rec['prefix'];
case 'firstname':
$str .= $rec['firstname'] . $rec['middlename'] . $rec['surname'];
break;
case 'surname':
$str = $rec['surname'] . $rec['firstname'] . $rec['middlename'];
break;
default:
$str = $rec[$this->sort_col];
break;
}
$str .= is_array($rec['email']) ? $rec['email'][0] : $rec['email'];
return mb_strtolower($str);
}
/**
* Return the cache table columns to order by
*/
private function _sort_columns()
{
$sortcols = array();
switch ($this->sort_col) {
case 'name':
$sortcols[] = 'name';
case 'firstname':
$sortcols[] = 'firstname';
break;
case 'surname':
$sortcols[] = 'surname';
break;
}
$sortcols[] = 'email';
return $sortcols;
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups($with_contacts = false)
{
if (!isset($this->distlists)) {
$this->distlists = $this->groupmembers = array();
foreach ($this->storagefolder->select('distribution-list', true) as $record) {
$record['ID'] = $this->uid2id($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = $this->uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);
$record['member'][$i]['ID'] = $mid;
$record['member'][$i]['readonly'] = empty($member['uid']);
$this->groupmembers[$mid][] = $record['ID'];
if ($with_contacts && empty($member['uid'])) {
$this->contacts[$mid] = $record['member'][$i];
}
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Encode object UID into a safe identifier
*/
public function uid2id($uid)
{
return rtrim(strtr(base64_encode($uid), '+/', '-_'), '=');
}
/**
* Convert Roundcube object identifier back into the original UID
*/
public function id2uid($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Build SQL query for fulltext matches
*/
private function _search_query($fields, $value, $mode)
{
$query = array();
$cols = array();
// $fulltext_cols might contain composite field names e.g. 'email:address' while $fields not
foreach (kolab_format_contact::$fulltext_cols as $col) {
if ($pos = strpos($col, ':')) {
$col = substr($col, 0, $pos);
}
if (in_array($col, $fields)) {
$cols[] = $col;
}
}
if (count($cols) == count($fields)) {
if ($mode & rcube_addressbook::SEARCH_STRICT) {
$prefix = '^'; $suffix = '$';
}
else if ($mode & rcube_addressbook::SEARCH_PREFIX) {
$prefix = '^'; $suffix = '';
}
else {
$prefix = ''; $suffix = '';
}
$search_string = is_array($value) ? join(' ', $value) : $value;
foreach (rcube_utils::normalize_string($search_string, true) as $word) {
$query[] = array('words', 'LIKE', $prefix . $word . $suffix);
}
}
return $query;
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$record['ID'] = $this->uid2id($record['uid']);
// convert email, website, phone values
foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
if (is_array($record[$col])) {
$values = $record[$col];
unset($record[$col]);
foreach ((array)$values as $i => $val) {
$key = $col . ($val['type'] ? ':' . $val['type'] : '');
$record[$key][] = $val[$propname];
}
}
}
if (is_array($record['address'])) {
$addresses = $record['address'];
unset($record['address']);
foreach ($addresses as $i => $adr) {
$key = 'address' . ($adr['type'] ? ':' . $adr['type'] : '');
$record[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
// only fetch photo content if requested
if ($this->action == 'photo')
$record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['id']);
}
// truncate publickey value for display
if ($record['pgppublickey'] && $this->action == 'show')
$record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
// remove empty fields
$record = array_filter($record);
// remove kolab_storage internal data
unset($record['_msguid'], $record['_formatobj'], $record['_mailbox'], $record['_type'], $record['_size']);
return $record;
}
/**
* Map fields from Roundcube format to internal kolab_format_contact properties
*/
private function _from_rcube_contact($contact, $old = array())
{
if (!$contact['uid'] && $contact['ID'])
$contact['uid'] = $this->id2uid($contact['ID']);
else if (!$contact['uid'] && $old['uid'])
$contact['uid'] = $old['uid'];
$contact['im'] = array_filter($this->get_col_values('im', $contact, true));
// convert email, website, phone values
foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
$col_values = $this->get_col_values($col, $contact);
$contact[$col] = array();
foreach ($col_values as $type => $values) {
foreach ((array)$values as $val) {
if (!empty($val)) {
$contact[$col][] = array($propname => $val, 'type' => $type);
}
}
unset($contact[$col.':'.$type]);
}
}
$addresses = array();
foreach ($this->get_col_values('address', $contact) as $type => $values) {
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
if (empty($adr))
continue;
$addresses[] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
unset($contact['address:'.$type]);
}
$contact['address'] = $addresses;
// categories are not supported in the web client but should be preserved (#2608)
$contact['categories'] = $old['categories'];
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($contact[$key]) && $key[0] == '_')
$contact[$key] = $val;
}
// convert one-item-array elements into string element
// this is needed e.g. to properly import birthday field
foreach ($this->coltypes as $type => $col_def) {
if ($col_def['limit'] == 1 && is_array($contact[$type])) {
$contact[$type] = array_shift(array_filter($contact[$type]));
}
}
// When importing contacts 'vcard' data is added, we don't need it (Bug #1711)
unset($contact['vcard']);
// add empty values for some fields which can be removed in the UI
return array_filter($contact) + array('nickname' => '', 'birthday' => '', 'anniversary' => '', 'freebusyurl' => '', 'photo' => $contact['photo']);
}
}
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index b39a57f4..dcbb35a6 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -1,347 +1,347 @@
<?php
class kolab_notes_ui
{
private $rc;
private $plugin;
private $ready = false;
function __construct($plugin)
{
$this->plugin = $plugin;
$this->rc = $plugin->rc;
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) // already done
return;
// add taskbar button
$this->plugin->add_button(array(
'command' => 'notes',
'class' => 'button-notes',
'classsel' => 'button-notes button-selected',
'innerclass' => 'button-inner',
'label' => 'kolab_notes.navtitle',
'type' => 'link'
), 'taskbar');
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/notes.css');
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->plugin->register_handler('plugin.notebooks', array($this, 'folders'));
#$this->plugin->register_handler('plugin.folders_select', array($this, 'folders_select'));
$this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
$this->plugin->register_handler('plugin.listing', array($this, 'listing'));
$this->plugin->register_handler('plugin.editform', array($this, 'editform'));
$this->plugin->register_handler('plugin.notetitle', array($this, 'notetitle'));
$this->plugin->register_handler('plugin.detailview', array($this, 'detailview'));
$this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
$this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
$this->rc->output->include_script('list.js');
$this->rc->output->include_script('treelist.js');
$this->plugin->include_script('notes.js');
$this->plugin->api->include_script('libkolab/libkolab.js');
// load config options and user prefs relevant for the UI
$settings = array(
'sort_col' => $this->rc->config->get('kolab_notes_sort_col', 'changed'),
);
if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) {
$settings['selected_list'] = $list;
}
if ($uid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC)) {
$settings['selected_uid'] = $uid;
}
$this->rc->html_editor();
$this->rc->output->set_env('kolab_notes_settings', $settings);
$this->rc->output->add_label('save','cancel','delete','close','listoptionstitle');
}
public function folders($attrib)
{
$attrib += array('id' => 'rcmkolabnotebooks');
if ($attrib['type'] == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
}
$tree = $attrib['type'] != 'select' ? true : null;
$lists = $this->plugin->get_lists($tree);
$jsenv = array();
if (is_object($tree)) {
$html = $this->folder_tree_html($tree, $lists, $jsenv, $attrib);
}
else {
$html = '';
foreach ($lists as $prop) {
$id = $prop['id'];
if (!$prop['virtual']) {
unset($prop['user_id']);
$jsenv[$id] = $prop;
}
if ($attrib['type'] == 'select') {
if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) {
$select->add($prop['name'], $prop['id']);
}
}
else {
$html .= html::tag('li', array('id' => 'rcmliknb' . rcube_utils::html_identifier($id), 'class' => $prop['group']),
$this->folder_list_item($id, $prop, $jsenv)
);
}
}
}
$this->rc->output->set_env('kolab_notebooks', $jsenv);
$this->rc->output->add_gui_object('notebooks', $attrib['id']);
return $attrib['type'] == 'select' ? $select->show() : html::tag('ul', $attrib, $html, html::$common_attrib);
}
/**
* Return html for a structured list <ul> for the folder tree
*/
public function folder_tree_html($node, $data, &$jsenv, $attrib)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$prop = $data[$id];
$is_collapsed = false; // TODO: determine this somehow?
$content = $this->folder_list_item($id, $prop, $jsenv);
if (!empty($folder->children)) {
$content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
$this->folder_tree_html($folder, $data, $jsenv, $attrib));
}
if (strlen($content)) {
$out .= html::tag('li', array(
'id' => 'rcmliknb' . rcube_utils::html_identifier($id),
'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
),
$content);
}
}
return $out;
}
/**
* Helper method to build a tasklist item (HTML content and js data)
*/
public function folder_list_item($id, $prop, &$jsenv, $checkbox = false)
{
if (!$prop['virtual']) {
unset($prop['user_id']);
$jsenv[$id] = $prop;
}
$classes = array('folder');
if ($prop['virtual']) {
$classes[] = 'virtual';
}
else if (!$prop['editable']) {
$classes[] = 'readonly';
}
if ($prop['subscribed']) {
$classes[] = 'subscribed';
}
if ($prop['class']) {
$classes[] = $prop['class'];
}
$title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : '');
$label_id = 'nl:' . $id;
$attr = $prop['virtual'] ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id)));
return html::div(join(' ', $classes),
html::a($attr + array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
($prop['virtual'] ? '' :
($checkbox ?
html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) :
''
) .
html::span('handle', '') .
html::span('actions',
(!$prop['default'] ?
html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') :
''
) .
(isset($prop['subscribed']) ?
html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('foldersubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') :
''
)
)
)
);
return '';
}
public function listing($attrib)
{
$attrib += array('id' => 'rcmkolabnoteslist');
$this->rc->output->add_gui_object('noteslist', $attrib['id']);
return html::tag('table', $attrib, '<tbody></tbody>', html::$common_attrib);
}
public function editform($attrib)
{
$attrib += array('action' => '#', 'id' => 'rcmkolabnoteseditform');
$textarea = new html_textarea(array(
'name' => 'content',
'id' => 'notecontent',
'cols' => 60,
'rows' => 20,
'tabindex' => 0,
'class' => 'mce_editor form-control',
));
$this->rc->output->add_gui_object('noteseditform', $attrib['id']);
return html::tag('form', $attrib, $textarea->show(), array_merge(html::$common_attrib, array('action')));
}
public function detailview($attrib)
{
$attrib += array('id' => 'rcmkolabnotesdetailview');
$this->rc->output->add_gui_object('notesdetailview', $attrib['id']);
return html::div($attrib, '');
}
public function notetitle($attrib)
{
$attrib += array('id' => 'rcmkolabnotestitle');
$this->rc->output->add_gui_object('noteviewtitle', $attrib['id']);
$summary = new html_inputfield(array(
'name' => 'summary',
'class' => 'notetitle inline-edit form-control',
'size' => 60,
'id' => 'notetitleinput',
'tabindex' => 0
));
$html = html::div('form-group row',
html::label(array('class' => 'col-sm-2 col-form-label', 'for' => 'notetitleinput'), $this->plugin->gettext('kolab_notes.title'))
. html::span('col-sm-10', $summary->show())
)
. html::div('form-group row',
html::label(array('class' => 'col-sm-2 col-form-label'), $this->plugin->gettext('kolab_notes.tags'))
. html::div(array('class' => 'tagline tagedit col-sm-10'), ' ')
)
. html::div(array('class' => 'dates text-only', 'style' => 'display:none'),
html::div('form-group row',
html::label(array('class' => 'col-sm-2 col-form-label'), $this->plugin->gettext('created'))
. html::span('col-sm-10', html::span('notecreated form-control-plaintext', ''))
)
. html::div('form-group row',
html::label(array('class' => 'col-sm-2 col-form-label'), $this->plugin->gettext('changed'))
. html::span('col-sm-10', html::span('notechanged form-control-plaintext', ''))
)
);
return html::div($attrib, $html);
}
public function attachments_list($attrib)
{
$attrib += array('id' => 'rcmkolabnotesattachmentslist');
$this->rc->output->add_gui_object('notesattachmentslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
/**
* Render create/edit form for notes lists (folders)
*/
public function list_editform($action, $list, $folder)
{
$this->action = $action;
$this->list = $list;
$this->folder = is_object($folder) ? $folder->name : ''; // UTF7;
$this->rc->output->set_env('pagetitle', $this->plugin->gettext('arialabelnotebookform'));
$this->rc->output->add_handler('folderform', array($this, 'notebookform'));
$this->rc->output->send('libkolab.folderform');
}
/**
* Render create/edit form for notes lists (folders)
*/
public function notebookform($attrib)
{
$folder_name = $this->folder;
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder_name)) {
$options = $storage->folder_info($folder_name);
$path_imap = explode($delim, $folder_name);
array_pop($path_imap); // pop off name part
- $path_imap = implode($path_imap, $delim);
+ $path_imap = implode($delim, $path_imap);
}
else {
$path_imap = '';
$options = array();
}
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
// folder name (default field)
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'noteslist-name', 'size' => 20));
$form['properties']['fields']['name'] = array(
'label' => $this->plugin->gettext('listname'),
'value' => $input_name->show($this->list['editname'], array('disabled' => ($options['norename'] || $options['protected']))),
'id' => 'noteslist-name',
);
// prevent user from moving folder
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = kolab_storage::folder_selector('note', array('name' => 'parent', 'id' => 'parent-folder'), $folder_name);
$form['properties']['fields']['path'] = array(
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show(strlen($folder_name) ? $path_imap : ''),
'id' => 'parent-folder',
);
}
$form_html = kolab_utils::folder_form($form, $folder_name, 'kolab_notes', $hidden_fields);
return html::tag('form', $attrib + array('action' => '#', 'method' => 'post', 'id' => 'noteslistpropform'), $form_html);
}
}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 192a510c..fa21692d 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1,1810 +1,1810 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @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_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
const ERROR_IMAP_CONN = 1;
const ERROR_CACHE_DB = 2;
const ERROR_NO_PERMISSION = 3;
const ERROR_INVALID_FOLDER = 4;
public static $version = '3.0';
public static $last_error;
public static $encode_ids = false;
private static $ready = false;
private static $with_tempsubs = true;
private static $subscriptions;
private static $ldapcache = array();
private static $typedata = array();
private static $ldap = array();
private static $states;
private static $config;
private static $imap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// do nothing
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else if (self::$imap->get_error_code()) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php', 'message' => "IMAP error"
), true);
}
// adjust some configurable settings
if ($event_scheduling_prop = $rcmail->config->get('kolab_event_scheduling_properties', null)) {
kolab_format_event::$scheduling_properties = (array)$event_scheduling_prop;
}
// adjust some configurable settings
if ($task_scheduling_prop = $rcmail->config->get('kolab_task_scheduling_properties', null)) {
kolab_format_task::$scheduling_properties = (array)$task_scheduling_prop;
}
return self::$ready;
}
/**
* Initializes LDAP object to resolve Kolab users
*
* @param string $name Name of the configuration option with LDAP config
*/
public static function ldap($name = 'kolab_users_directory')
{
self::setup();
$config = self::$config->get($name);
if (empty($config)) {
$name = 'kolab_auth_addressbook';
$config = self::$config->get($name);
}
if (self::$ldap[$name]) {
return self::$ldap[$name];
}
if (!is_array($config)) {
$ldap_config = (array)self::$config->get('ldap_public');
$config = $ldap_config[$config];
}
if (empty($config)) {
return null;
}
$ldap = new kolab_ldap($config);
// overwrite filter option
if ($filter = self::$config->get('kolab_users_filter')) {
self::$config->set('kolab_auth_filter', $filter);
}
$user_field = $user_attrib = self::$config->get('kolab_users_id_attrib');
// Fallback to kolab_auth_login, which is not attribute, but field name
if (!$user_field && ($user_field = self::$config->get('kolab_auth_login', 'email'))) {
$user_attrib = $config['fieldmap'][$user_field];
}
if ($user_field && $user_attrib) {
$ldap->extend_fieldmap(array($user_field => $user_attrib));
}
self::$ldap[$name] = $ldap;
return $ldap;
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @param string Expected folder type
*
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder, $type = null)
{
return self::setup() ? new kolab_storage_folder($folder, $type) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
$folder->set_folder($foldername, $type, $folderdata[$foldername]);
if ($object = $folder->get_object($uid))
return $object;
}
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Folder type (contact,event,task,journal,file,note,configuration)
* @param int Expected number of records or limit (for performance reasons)
*
* @return array List of Kolab data objects (each represented as hash array)
* @see kolab_storage_format::select()
*/
public static function select($query, $type, $limit = null)
{
self::setup();
$folder = null;
$result = array();
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($query) as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Returns Free-busy server URL
*/
public static function get_freebusy_server()
{
self::setup();
$url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
$url = self::$config->get('kolab_freebusy_server', $url);
$url = rcube_utils::resolve_url($url);
return unslashify($url);
}
/**
* Compose an URL to query the free/busy status for the given user
*
* @param string Email address of the user to get free/busy data for
* @param object DateTime Start of the query range (optional)
* @param object DateTime End of the query range (optional)
*
* @return string Fully qualified URL to query free/busy data
*/
public static function get_freebusy_url($email, $start = null, $end = null)
{
$query = '';
$param = array();
$utc = new \DateTimeZone('UTC');
if ($start instanceof \DateTime) {
$start->setTimezone($utc);
$param['dtstart'] = $start->format('Ymd\THis\Z');
}
if ($end instanceof \DateTime) {
$end->setTimezone($utc);
$param['dtend'] = $end->format('Ymd\THis\Z');
}
if (!empty($param)) {
$query = '?' . http_build_query($param);
}
return self::get_freebusy_server() . '/' . $email . '.ifb' . $query;
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
* @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
public static function folder_id($folder, $enc = null)
{
return $enc == true || ($enc === null && self::$encode_ids) ?
self::id_encode($folder) :
asciiwords(strtr($folder, '/.-', '___'));
}
/**
* Encode the given ID to a safe ascii representation
*
* @param string $id Arbitrary identifier string
*
* @return string Ascii representation
*/
public static function id_encode($id)
{
return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
}
/**
* Convert the given identifier back to it's raw value
*
* @param string $id Ascii identifier
* @return string Raw identifier string
*/
public static function id_decode($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Return the (first) path of the requested IMAP namespace
*
* @param string Namespace name (personal, shared, other)
* @return string IMAP root path for that namespace
*/
public static function namespace_root($name)
{
self::setup();
foreach ((array)self::$imap->get_namespace($name) as $paths) {
if (strlen($paths[0]) > 1) {
return $paths[0];
}
}
return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
'name' => $name,
'subscribe' => $subscribed,
)));
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_rename', array(
'oldname' => $oldname, 'newname' => $newname));
$oldfolder = self::get_folder($oldname);
$active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
self::set_state($oldname, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array &$prop Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
*
* @return string|false New folder name or False on failure
*
* @see self::set_folder_props() for list of other properties
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else {
$result = true;
}
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* with kolab_custom_display_names support.
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
return self::object_prettyname($folder, $folder_ns);
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
static $_metadata;
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true) && self::setup()) {
if ($_metadata !== null) {
$metadata = $_metadata;
}
else {
// For performance reasons ask for all folders, it will be cached as one cache entry
$metadata = self::$imap->get_metadata("*", array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
// If cache is disabled store result in memory
if (!self::$config->get('imap_cache')) {
$_metadata = $metadata;
}
}
if ($data = $metadata[$folder]) {
if (($name = $data[self::NAME_KEY_PRIVATE]) || ($name = $data[self::NAME_KEY_SHARED])) {
return $name;
}
}
}
return false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_prettyname($folder, &$folder_ns=null)
{
self::setup();
$found = false;
$namespace = self::$imap->get_namespace();
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix and extract username
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username part and map it to user name
$pos = strpos($folder, $delim);
$fid = $pos ? substr($folder, 0, $pos) : $folder;
if ($user = self::folder_id2user($fid, true)) {
$fid = str_replace($delim, '', $user);
}
$prefix = "($fid)";
$folder = $pos ? substr($folder, $pos + 1) : '';
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' » ', $folder);
if ($prefix)
$folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Helper method to generate a truncated folder name to display.
* Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' » ') === 0) {
$length = strlen($names[$i] . ' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$diff = 1;
// check if prefix folder is in other users namespace
for ($n = count($names)-1; $n >= 0; $n--) {
if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
$diff = 0;
break;
}
}
$name = str_repeat(' ', $count - $diff) . '» ' . substr($name, $length);
break;
}
// other users namespace and parent folder exists
else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
$length = strlen('(' . $names[$i] . ') ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count) . '» ' . substr($name, $length);
break;
}
}
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type (sorted)
$folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len) {
if ($name == $current) {
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = self::object_name($parent);
}
continue;
}
if (strpos($name, $current.$delim) === 0) {
continue;
}
}
// always show the parent of current folder
if ($p_len && $name == $parent) {
}
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = $c_folder->get_name();
}
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' » ') === 0) {
$length = strlen($listnames[$i].' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
return $folders;
}
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// get folders types for all folders
$folderdata = self::folders_typedata($prefix);
if (!is_array($folderdata)) {
return array();
}
// In some conditions we can skip LIST command (?)
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders() with optional post-filtering
*/
protected static function _imap_list_folders($root, $mbox)
{
$postfilter = null;
// compose a post-filter expression for the excluded namespaces
if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$excludes = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = self::namespace_root($ns)) {
$excludes[] = $ns_root;
}
}
if (count($excludes)) {
$postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
}
}
// use normal LIST command to return all folders, it's fast enough
$folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
if (!empty($postfilter)) {
$folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
$folders = self::$imap->sort_folder_list($folders);
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders_subscribed()
* with support for temporarily subscribed folders
*/
protected static function _imap_list_subscribed($root, $mbox)
{
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
return $folders;
}
/**
* Search for shared or otherwise not listed groupware folders the user has access
*
* @param string Folder type of folders to search for
* @param string Search string
* @param array Namespace(s) to exclude results from
*
* @return array List of matching kolab_storage_folder objects
*/
public static function search_folders($type, $query, $exclude_ns = array())
{
if (!self::setup()) {
return array();
}
$folders = array();
$query = str_replace('*', '', $query);
// find unsubscribed IMAP folders of the given type
foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
// FIXME: only consider the last part of the folder path for searching?
$realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
if (($query == '' || strpos($realname, $query) !== false) &&
!self::folder_is_subscribed($foldername, true) &&
!in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
) {
$folders[] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
$pad = ' ';
$out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
foreach ($folders as $folder) {
$_folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode »
}
// $folders is a result of get_folders() we can assume folders were already sorted
foreach (array_keys($nsnames) as $ns) {
asort($nsnames[$ns], SORT_LOCALE_STRING);
foreach (array_keys($nsnames[$ns]) as $utf7name) {
$out[] = $_folders[$utf7name];
}
}
return $out;
}
/**
* Check the folder tree and add the missing parents as virtual folders
*
* @param array $folders Folders list
* @param object $tree Reference to the root node of the folder tree
*
* @return array Flat folders list
*/
public static function folder_hierarchy($folders, &$tree = null)
{
if (!self::setup()) {
return array();
}
$_folders = array();
$delim = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delim);
$tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
$refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
$folder->parent = join($delim, $path);
$folder->children = array(); // reset list
// skip top folders or ones with a custom displayname
if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
$tree->children[] = $folder;
}
else {
$parents = array();
$depth = $folder->get_namespace() == 'personal' ? 1 : 2;
while (count($path) >= $depth && ($parent = join($delim, $path))) {
array_pop($path);
$parent_parent = join($delim, $path);
if (!$refs[$parent]) {
if ($folder->type && self::folder_type($parent) == $folder->type) {
$refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type);
$refs[$parent]->parent = $parent_parent;
}
else if ($parent_parent == $other_ns) {
$refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
}
else {
$name = kolab_storage::object_name($parent);
$refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
}
$parents[] = $refs[$parent];
}
}
if (!empty($parents)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$parent_node = $refs[$parent->parent] ?: $tree;
$parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
$parent_node = $refs[$folder->parent] ?: $tree;
$parent_node->children[] = $folder;
}
$refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
// return cached result
if (is_array(self::$typedata[$prefix])) {
return self::$typedata[$prefix];
}
$type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
// fetch metadata from *some* folders only
if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$folderdata = $blacklist = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
$blacklist[] = $ns_root;
}
}
foreach (array('personal','other','shared') as $ns) {
if (!in_array($ns, (array)$skip_ns)) {
$ns_root = rtrim(self::namespace_root($ns), $delimiter);
// list top-level folders and their childs one by one
// GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
if ($ns_root == '') {
foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
if (!in_array($folder, $blacklist)) {
$folderdata[$folder] = $metadata;
$opts = self::$imap->folder_attributes($folder);
if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
$folderdata += $data;
}
}
}
}
else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
$folderdata += $data;
}
}
}
}
else {
$folderdata = self::$imap->get_metadata($prefix, $type_keys);
}
if (!is_array($folderdata)) {
return false;
}
// keep list in memory
self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
return self::$typedata[$prefix];
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
// return in-memory cached result
foreach (self::$typedata as $typedata) {
if (array_key_exists($folder, $typedata)) {
return $typedata[$folder];
}
}
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
public static function set_folder_type($folder, $type='mail')
{
self::setup();
list($ctype, $subtype) = explode('.', $type);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) // fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
return in_array($folder, self::$subscriptions) ||
($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
public static function folder_subscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (self::folder_is_subscribed($folder)) {
return true;
}
else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
$_SESSION['kolab_subscribed_folders'][] = $folder;
return true;
}
}
else if (self::$imap->subscribe($folder)) {
self::$subscriptions = null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
unset($_SESSION['kolab_subscribed_folders'][$i]);
}
return true;
}
else if (self::$imap->unsubscribe($folder)) {
self::$subscriptions = null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
// activation implies temporary subscription
self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
// remove from temp subscriptions, really?
self::folder_unsubscribe($folder, true);
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
- self::$states = self::$subscriptions;
- $folders = implode(self::$states, '**');
+ self::$states = (array) self::$subscriptions;
+ $folders = implode('**', self::$states);
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
- $folders = implode(self::$states, '**');
- $rcube = rcube::get_instance();
- return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
+ $folders = implode('**', self::$states);
+
+ return rcube::get_instance()->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array &$prop Folder properties (color, displayname)
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
/**
* Search users in Kolab LDAP storage
*
* @param mixed $query Search value (or array of field => value pairs)
* @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
* @param array $required List of fields that shall ot be empty
* @param int $limit Maximum number of records
* @param int $count Returns the number of records found
*
* @return array List of users
*/
public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
{
$query = str_replace('*', '', $query);
// requires a working LDAP setup
if (!strlen($query) || !($ldap = self::ldap())) {
return array();
}
$root = self::namespace_root('other');
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
$search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias'));
// search users using the configured attributes
$results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count);
// exclude myself
if ($_SESSION['kolab_dn']) {
unset($results[$_SESSION['kolab_dn']]);
}
// resolve to IMAP folder name
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, ) = explode('@', $user[$user_attrib]);
$user['kolabtargetfolder'] = $root . $localpart;
});
return $results;
}
/**
* Returns a list of IMAP folders shared by the given user
*
* @param array User entry from LDAP
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param int 1 - subscribed folders only, 0 - all folders, 2 - all non-active
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_user_folders($user, $type, $subscribed = 0, &$folderdata = array())
{
self::setup();
$folders = array();
// use localpart of user attribute as root for folder listing
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = self::namespace_root('other');
$prefix = $other_ns . $mbox . $delimiter;
$subscribed = (int) $subscribed;
$subs = $subscribed < 2 ? (bool) $subscribed : false;
$folders = self::list_folders($prefix, '*', $type, $subs, $folderdata);
if ($subscribed === 2 && !empty($folders)) {
$active = self::get_states();
if (!empty($active)) {
$folders = array_diff($folders, $active);
}
}
}
return $folders;
}
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
public static function get_user_folders($type, $subscribed)
{
$folders = $folderdata = array();
if (self::setup()) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
$path = explode($delimiter, $foldername);
// compare folder type if a subfolder is listed
if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
continue;
}
// truncate folder path to top-level folders of the 'other' namespace
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
}
}
// for every (subscribed) user folder, list all (unsubscribed) subfolders
foreach ($folders as $userfolder) {
foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
$userfolder->children[] = $folders[$foldername];
}
}
}
}
return $folders;
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
}
/**
* Get folder METADATA for all supported keys
* Do this in one go for better caching performance
*/
public static function folder_metadata($folder)
{
if (self::setup()) {
$keys = array(
// For better performance we skip displayname here, see (self::custom_displayname())
// self::NAME_KEY_PRIVATE,
// self::NAME_KEY_SHARED,
self::CTYPE_KEY,
self::CTYPE_KEY_PRIVATE,
self::COLOR_KEY_PRIVATE,
self::COLOR_KEY_SHARED,
self::UID_KEY_SHARED,
self::UID_KEY_CYRUS,
);
$metadata = self::$imap->get_metadata($folder, $keys);
return $metadata[$folder];
}
}
/**
* Get user attributes for specified other user (imap) folder identifier.
*
* @param string $folder_id Folder name w/o path (imap user identifier)
* @param bool $as_string Return configured display name attribute value
*
* @return array User attributes
* @see self::ldap()
*/
public static function folder_id2user($folder_id, $as_string = false)
{
static $domain, $cache, $name_attr;
$rcube = rcube::get_instance();
if ($domain === null) {
list(, $domain) = explode('@', $rcube->get_user_name());
}
if ($name_attr === null) {
$name_attr = (array) ($rcube->config->get('kolab_users_name_field', $rcube->config->get('kolab_auth_name')) ?: 'name');
}
$token = $folder_id;
if ($domain && strpos($find, '@') === false) {
$token .= '@' . $domain;
}
if ($cache === null) {
$cache = $rcube->get_cache_shared('kolab_users') ?: false;
}
// use value cached in memory for repeated lookups
if (!$cache && array_key_exists($token, self::$ldapcache)) {
$user = self::$ldapcache[$token];
}
if (empty($user) && $cache) {
$user = $cache->get($token);
}
if (empty($user) && ($ldap = self::ldap())) {
$user = $ldap->get_user_record($token, $_SESSION['imap_host']);
if (!empty($user)) {
$keys = array('displayname', 'name', 'mail'); // supported keys
$user = array_intersect_key($user, array_flip($keys));
if (!empty($user)) {
if ($cache) {
$cache->set($token, $user);
}
else {
self::$ldapcache[$token] = $user;
}
}
}
}
if (!empty($user)) {
if ($as_string) {
foreach ($name_attr as $attr) {
if ($display = $user[$attr]) {
break;
}
}
if (!$display) {
$display = $user['displayname'] ?: $user['name'];
}
if ($display && $display != $folder_id) {
$display = "$display ($folder_id)";
}
return $display;
}
return $user;
}
}
/**
* Chwala's 'folder_mod' hook handler for mapping other users folder names
*/
public static function folder_mod($args)
{
static $roots;
if ($roots === null) {
self::setup();
$roots = self::$imap->get_namespace('other');
}
// Note: We're working with UTF7-IMAP encoding here
if ($args['dir'] == 'in') {
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// compare first (user) part with a regexp, it's supposed
// to look like this: "Doe, Jane (uid)", so we can extract the uid
// and replace the folder with it
if (preg_match('~^[^/]+ \(([^)]+)\)$~', $folder[0], $m)) {
$folder[0] = $m[1];
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
else { // dir == 'out'
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// Replace uid with "Doe, Jane (uid)"
if ($user = self::folder_id2user($folder[0], true)) {
$user = str_replace($delim, '', $user);
$folder[0] = rcube_charset::convert($user, RCUBE_CHARSET, 'UTF7-IMAP');
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
return $args;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Mar 19, 9:05 AM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
456423
Default Alt Text
(215 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment