CalDAV driver
This commit is contained in:
parent
b06f1e3941
commit
af5461eb76
13 changed files with 3689 additions and 47 deletions
904
plugins/calendar/drivers/caldav/caldav_calendar.php
Normal file
904
plugins/calendar/drivers/caldav/caldav_calendar.php
Normal file
|
@ -0,0 +1,904 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* CalDAV calendar storage class
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2012-2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* 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 caldav_calendar extends kolab_storage_dav_folder
|
||||
{
|
||||
public $ready = false;
|
||||
public $rights = 'lrs';
|
||||
public $editable = false;
|
||||
public $attachments = false; // TODO
|
||||
public $alarms = false;
|
||||
public $history = false;
|
||||
public $subscriptions = false;
|
||||
public $categories = [];
|
||||
public $storage;
|
||||
|
||||
public $type = 'event';
|
||||
|
||||
protected $cal;
|
||||
protected $events = [];
|
||||
protected $search_fields = ['title', 'description', 'location', 'attendees', 'categories'];
|
||||
|
||||
/**
|
||||
* Factory method to instantiate a caldav_calendar object
|
||||
*
|
||||
* @param string $id Calendar ID (encoded IMAP folder name)
|
||||
* @param object $calendar Calendar plugin object
|
||||
*
|
||||
* @return caldav_calendar Self instance
|
||||
*/
|
||||
public static function factory($id, $calendar)
|
||||
{
|
||||
return new caldav_calendar($id, $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct($folder_or_id, $calendar)
|
||||
{
|
||||
if ($folder_or_id instanceof kolab_storage_dav_folder) {
|
||||
$this->storage = $folder_or_id;
|
||||
}
|
||||
else {
|
||||
// $this->storage = kolab_storage_dav::get_folder($folder_or_id);
|
||||
}
|
||||
|
||||
$this->cal = $calendar;
|
||||
$this->id = $this->storage->id;
|
||||
$this->attributes = $this->storage->attributes;
|
||||
$this->ready = true;
|
||||
|
||||
// Set writeable and alarms flags according to folder permissions
|
||||
if ($this->ready) {
|
||||
if ($this->storage->get_namespace() == 'personal') {
|
||||
$this->editable = true;
|
||||
$this->rights = 'lrswikxteav';
|
||||
$this->alarms = true;
|
||||
}
|
||||
else {
|
||||
$rights = $this->storage->get_myrights();
|
||||
if ($rights && !PEAR::isError($rights)) {
|
||||
$this->rights = $rights;
|
||||
if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
|
||||
$this->editable = strpos($rights, 'i');;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// user-specific alarms settings win
|
||||
$prefs = $this->cal->rc->config->get('kolab_calendars', []);
|
||||
if (isset($prefs[$this->id]['showalarms'])) {
|
||||
$this->alarms = $prefs[$this->id]['showalarms'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->default = $this->storage->default;
|
||||
$this->subtype = $this->storage->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the folder name
|
||||
*
|
||||
* @return string Name of the folder
|
||||
*/
|
||||
public function get_realname()
|
||||
{
|
||||
return $this->get_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return color to display this calendar
|
||||
*/
|
||||
public function get_color($default = null)
|
||||
{
|
||||
if ($color = $this->storage->get_color()) {
|
||||
return $color;
|
||||
}
|
||||
|
||||
return $default ?: 'cc0000';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an URL for CalDAV access to this calendar (if configured)
|
||||
*/
|
||||
public function get_caldav_url()
|
||||
{
|
||||
/*
|
||||
if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
|
||||
return strtr($template, [
|
||||
'%h' => $_SERVER['HTTP_HOST'],
|
||||
'%u' => urlencode($this->cal->rc->get_user_name()),
|
||||
'%i' => urlencode($this->storage->get_uid()),
|
||||
'%n' => urlencode($this->name),
|
||||
]);
|
||||
}
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties of this calendar folder
|
||||
*
|
||||
* @see caldav_driver::edit_calendar()
|
||||
*/
|
||||
public function update(&$prop)
|
||||
{
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for a single event object
|
||||
*/
|
||||
public function get_event($id)
|
||||
{
|
||||
// remove our occurrence identifier if it's there
|
||||
$master_id = preg_replace('/-\d{8}(T\d{6})?$/', '', $id);
|
||||
|
||||
// directly access storage object
|
||||
if (empty($this->events[$id]) && $master_id == $id && ($record = $this->storage->get_object($id))) {
|
||||
$this->events[$id] = $this->_to_driver_event($record, true);
|
||||
}
|
||||
|
||||
// maybe a recurring instance is requested
|
||||
if (empty($this->events[$id]) && $master_id != $id) {
|
||||
$instance_id = substr($id, strlen($master_id) + 1);
|
||||
|
||||
if ($record = $this->storage->get_object($master_id)) {
|
||||
$master = $this->_to_driver_event($record);
|
||||
}
|
||||
|
||||
if ($master) {
|
||||
// check for match in top-level exceptions (aka loose single occurrences)
|
||||
if (!empty($master['_formatobj']) && ($instance = $master['_formatobj']->get_instance($instance_id))) {
|
||||
$this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
|
||||
}
|
||||
// check for match on the first instance already
|
||||
else if (!empty($master['_instance']) && $master['_instance'] == $instance_id) {
|
||||
$this->events[$id] = $master;
|
||||
}
|
||||
else if (!empty($master['recurrence'])) {
|
||||
$start_date = $master['start'];
|
||||
// For performance reasons we'll get only the specific instance
|
||||
if (($date = substr($id, strlen($master_id) + 1, 8)) && strlen($date) == 8 && is_numeric($date)) {
|
||||
$start_date = new DateTime($date . 'T000000', $master['start']->getTimezone());
|
||||
}
|
||||
|
||||
$this->get_recurring_events($record, $start_date, null, $id, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->events[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment body
|
||||
* @see calendar_driver::get_attachment_body()
|
||||
*/
|
||||
public function get_attachment_body($id, $event)
|
||||
{
|
||||
if (!$this->ready) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->storage->get_attachment($event['id'], $id);
|
||||
|
||||
if ($data == null) {
|
||||
// try again with master UID
|
||||
$uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
|
||||
if ($uid != $event['id']) {
|
||||
$data = $this->storage->get_attachment($uid, $id);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int Event's new start (unix timestamp)
|
||||
* @param int Event's new end (unix timestamp)
|
||||
* @param string Search query (optional)
|
||||
* @param bool Include virtual events (optional)
|
||||
* @param array Additional parameters to query storage
|
||||
* @param array Additional query to filter events
|
||||
*
|
||||
* @return array A list of event records
|
||||
*/
|
||||
public function list_events($start, $end, $search = null, $virtual = 1, $query = [], $filter_query = null)
|
||||
{
|
||||
// convert to DateTime for comparisons
|
||||
// #5190: make the range a little bit wider
|
||||
// to workaround possible timezone differences
|
||||
try {
|
||||
$start = new DateTime('@' . ($start - 12 * 3600));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$start = new DateTime('@0');
|
||||
}
|
||||
try {
|
||||
$end = new DateTime('@' . ($end + 12 * 3600));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$end = new DateTime('today +10 years');
|
||||
}
|
||||
|
||||
// get email addresses of the current user
|
||||
$user_emails = $this->cal->get_user_emails();
|
||||
|
||||
// query Kolab storage
|
||||
$query[] = ['dtstart', '<=', $end];
|
||||
$query[] = ['dtend', '>=', $start];
|
||||
|
||||
if (is_array($filter_query)) {
|
||||
$query = array_merge($query, $filter_query);
|
||||
}
|
||||
|
||||
$words = [];
|
||||
$partstat_exclude = [];
|
||||
$events = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$search = mb_strtolower($search);
|
||||
$words = rcube_utils::tokenize_string($search, 1);
|
||||
foreach (rcube_utils::normalize_string($search, true) as $word) {
|
||||
$query[] = ['words', 'LIKE', $word];
|
||||
}
|
||||
}
|
||||
|
||||
// set partstat filter to skip pending and declined invitations
|
||||
if (empty($filter_query)
|
||||
&& $this->cal->rc->config->get('kolab_invitation_calendars')
|
||||
&& $this->get_namespace() != 'other'
|
||||
) {
|
||||
$partstat_exclude = ['NEEDS-ACTION', 'DECLINED'];
|
||||
}
|
||||
|
||||
foreach ($this->storage->select($query) as $record) {
|
||||
$event = $this->_to_driver_event($record, !$virtual, false);
|
||||
|
||||
// remember seen categories
|
||||
if (!empty($event['categories'])) {
|
||||
$cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
|
||||
$this->categories[$cat]++;
|
||||
}
|
||||
|
||||
// list events in requested time window
|
||||
if ($event['start'] <= $end && $event['end'] >= $start) {
|
||||
unset($event['_attendees']);
|
||||
$add = true;
|
||||
|
||||
// skip the first instance of a recurring event if listed in exdate
|
||||
if ($virtual && !empty($event['recurrence']['EXDATE'])) {
|
||||
$event_date = $event['start']->format('Ymd');
|
||||
$event_tz = $event['start']->getTimezone();
|
||||
|
||||
foreach ((array) $event['recurrence']['EXDATE'] as $exdate) {
|
||||
$ex = clone $exdate;
|
||||
$ex->setTimezone($event_tz);
|
||||
|
||||
if ($ex->format('Ymd') == $event_date) {
|
||||
$add = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find and merge exception for the first instance
|
||||
if ($virtual && !empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS'])) {
|
||||
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
|
||||
if ($event['_instance'] == $exception['_instance']) {
|
||||
unset($exception['calendar'], $exception['className'], $exception['_folder_id']);
|
||||
// clone date objects from main event before adjusting them with exception data
|
||||
if (is_object($event['start'])) {
|
||||
$event['start'] = clone $record['start'];
|
||||
}
|
||||
if (is_object($event['end'])) {
|
||||
$event['end'] = clone $record['end'];
|
||||
}
|
||||
kolab_driver::merge_exception_data($event, $exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($add) {
|
||||
$events[] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
// resolve recurring events
|
||||
if (!empty($event['recurrence']) && $virtual == 1) {
|
||||
$events = array_merge($events, $this->get_recurring_events($event, $start, $end));
|
||||
}
|
||||
// add top-level exceptions (aka loose single occurrences)
|
||||
else if (!empty($record['exceptions'])) {
|
||||
foreach ($record['exceptions'] as $ex) {
|
||||
$component = $this->_to_driver_event($ex, false, false, $record);
|
||||
if ($component['start'] <= $end && $component['end'] >= $start) {
|
||||
$events[] = $component;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// post-filter all events by fulltext search and partstat values
|
||||
$me = $this;
|
||||
$events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
|
||||
// fulltext search
|
||||
if (count($words)) {
|
||||
$hits = 0;
|
||||
foreach ($words as $word) {
|
||||
$hits += $me->fulltext_match($event, $word, false);
|
||||
}
|
||||
if ($hits < count($words)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// partstat filter
|
||||
if (count($partstat_exclude) && !empty($event['attendees'])) {
|
||||
foreach ($event['attendees'] as $attendee) {
|
||||
if (
|
||||
in_array($attendee['email'], $user_emails)
|
||||
&& in_array($attendee['status'], $partstat_exclude)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Apply event-to-mail relations
|
||||
$config = kolab_storage_config::get_instance();
|
||||
$config->apply_links($events);
|
||||
|
||||
// avoid session race conditions that will loose temporary subscriptions
|
||||
$this->cal->rc->session->nowrite = true;
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of events in the given calendar
|
||||
*
|
||||
* @param int Date range start (unix timestamp)
|
||||
* @param int Date range end (unix timestamp)
|
||||
* @param array Additional query to filter events
|
||||
*
|
||||
* @return int Number of events
|
||||
*/
|
||||
public function count_events($start, $end = null, $filter_query = null)
|
||||
{
|
||||
// convert to DateTime for comparisons
|
||||
try {
|
||||
$start = new DateTime('@'.$start);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$start = new DateTime('@0');
|
||||
}
|
||||
if ($end) {
|
||||
try {
|
||||
$end = new DateTime('@'.$end);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$end = null;
|
||||
}
|
||||
}
|
||||
|
||||
// query Kolab storage
|
||||
$query[] = ['dtend', '>=', $start];
|
||||
|
||||
if ($end) {
|
||||
$query[] = ['dtstart', '<=', $end];
|
||||
}
|
||||
|
||||
// add query to exclude pending/declined invitations
|
||||
if (empty($filter_query)) {
|
||||
foreach ($this->cal->get_user_emails() as $email) {
|
||||
$query[] = ['tags', '!=', 'x-partstat:' . $email . ':needs-action'];
|
||||
$query[] = ['tags', '!=', 'x-partstat:' . $email . ':declined'];
|
||||
}
|
||||
}
|
||||
else if (is_array($filter_query)) {
|
||||
$query = array_merge($query, $filter_query);
|
||||
}
|
||||
|
||||
return $this->storage->count($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event record
|
||||
*
|
||||
* @see calendar_driver::new_event()
|
||||
*
|
||||
* @return array|false The created record ID on success, False on error
|
||||
*/
|
||||
public function insert_event($event)
|
||||
{
|
||||
if (!is_array($event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// email links are stored separately
|
||||
$links = !empty($event['links']) ? $event['links'] : [];
|
||||
unset($event['links']);
|
||||
|
||||
// generate new event from RC input
|
||||
$object = $this->_from_driver_event($event);
|
||||
$saved = $this->storage->save($object, 'event');
|
||||
|
||||
if (!$saved) {
|
||||
rcube::raise_error([
|
||||
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Error saving event object to DAV server"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// save links in configuration.relation object
|
||||
if ($this->save_links($event['uid'], $links)) {
|
||||
$object['links'] = $links;
|
||||
}
|
||||
|
||||
$this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific event record
|
||||
*
|
||||
* @return bool True on success, False on error
|
||||
*/
|
||||
public function update_event($event, $exception_id = null)
|
||||
{
|
||||
$updated = false;
|
||||
$old = $this->storage->get_object(!empty($event['uid']) ? $event['uid'] : $event['id']);
|
||||
|
||||
if (!$old || PEAR::isError($old)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// email links are stored separately
|
||||
$links = !empty($event['links']) ? $event['links'] : [];
|
||||
unset($event['links']);
|
||||
|
||||
$object = $this->_from_driver_event($event, $old);
|
||||
$saved = $this->storage->save($object, 'event', $old['uid']);
|
||||
|
||||
if (!$saved) {
|
||||
rcube::raise_error([
|
||||
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Error saving event object to CalDAV server"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
}
|
||||
else {
|
||||
// save links in configuration.relation object
|
||||
if ($this->save_links($event['uid'], $links)) {
|
||||
$object['links'] = $links;
|
||||
}
|
||||
|
||||
$updated = true;
|
||||
$this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
|
||||
|
||||
// refresh local cache with recurring instances
|
||||
if ($exception_id) {
|
||||
$this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event record
|
||||
*
|
||||
* @see calendar_driver::remove_event()
|
||||
*
|
||||
* @return bool True on success, False on error
|
||||
*/
|
||||
public function delete_event($event, $force = true)
|
||||
{
|
||||
$uid = !empty($event['uid']) ? $event['uid'] : $event['id'];
|
||||
$deleted = $this->storage->delete($uid, $force);
|
||||
|
||||
if (!$deleted) {
|
||||
rcube::raise_error([
|
||||
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Error deleting event '{$uid}' from CalDAV server"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore deleted event record
|
||||
*
|
||||
* @see calendar_driver::undelete_event()
|
||||
*
|
||||
* @return bool True on success, False on error
|
||||
*/
|
||||
public function restore_event($event)
|
||||
{
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find messages linked with an event
|
||||
*/
|
||||
protected function get_links($uid)
|
||||
{
|
||||
return []; // TODO
|
||||
$storage = kolab_storage_config::get_instance();
|
||||
return $storage->get_object_links($uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save message references (links) to an event
|
||||
*/
|
||||
protected function save_links($uid, $links)
|
||||
{
|
||||
return false; // TODO
|
||||
$storage = kolab_storage_config::get_instance();
|
||||
return $storage->save_object_links($uid, (array) $links);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instances of a recurring event
|
||||
*
|
||||
* @param array $event Hash array with event properties
|
||||
* @param DateTime $start Start date of the recurrence window
|
||||
* @param DateTime $end End date of the recurrence window
|
||||
* @param string $event_id ID of a specific recurring event instance
|
||||
* @param int $limit Max. number of instances to return
|
||||
*
|
||||
* @return array List of recurring event instances
|
||||
*/
|
||||
public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
|
||||
{
|
||||
$object = $event['_formatobj'];
|
||||
|
||||
if (!is_object($object)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// determine a reasonable end date if none given
|
||||
if (!$end) {
|
||||
$end = clone $event['start'];
|
||||
$end->add(new DateInterval('P100Y'));
|
||||
}
|
||||
|
||||
// read recurrence exceptions first
|
||||
$events = [];
|
||||
$exdata = [];
|
||||
$futuredata = [];
|
||||
$recurrence_id_format = libcalendaring::recurrence_id_format($event);
|
||||
|
||||
if (!empty($event['recurrence'])) {
|
||||
// copy the recurrence rule from the master event (to be used in the UI)
|
||||
$recurrence_rule = $event['recurrence'];
|
||||
unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
|
||||
|
||||
if (!empty($event['recurrence']['EXCEPTIONS'])) {
|
||||
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
|
||||
if (empty($exception['_instance'])) {
|
||||
$exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception, !empty($event['allday']));
|
||||
}
|
||||
|
||||
$rec_event = $this->_to_driver_event($exception, false, false, $event);
|
||||
$rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
|
||||
$rec_event['isexception'] = 1;
|
||||
|
||||
// found the specifically requested instance: register exception (single occurrence wins)
|
||||
if (
|
||||
$rec_event['id'] == $event_id
|
||||
&& (empty($this->events[$event_id]) || !empty($this->events[$event_id]['thisandfuture']))
|
||||
) {
|
||||
$rec_event['recurrence'] = $recurrence_rule;
|
||||
$rec_event['recurrence_id'] = $event['uid'];
|
||||
$this->events[$rec_event['id']] = $rec_event;
|
||||
}
|
||||
|
||||
// remember this exception's date
|
||||
$exdate = substr($exception['_instance'], 0, 8);
|
||||
if (empty($exdata[$exdate]) || !empty($exdata[$exdate]['thisandfuture'])) {
|
||||
$exdata[$exdate] = $rec_event;
|
||||
}
|
||||
if (!empty($rec_event['thisandfuture'])) {
|
||||
$futuredata[$exdate] = $rec_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// found the specifically requested instance, exiting...
|
||||
if ($event_id && !empty($this->events[$event_id])) {
|
||||
return [$this->events[$event_id]];
|
||||
}
|
||||
|
||||
// Check first occurrence, it might have been moved
|
||||
if ($first = $exdata[$event['start']->format('Ymd')]) {
|
||||
// return it only if not already in the result, but in the requested period
|
||||
if (!($event['start'] <= $end && $event['end'] >= $start)
|
||||
&& ($first['start'] <= $end && $first['end'] >= $start)
|
||||
) {
|
||||
$events[] = $first;
|
||||
}
|
||||
}
|
||||
|
||||
if ($limit && count($events) >= $limit) {
|
||||
return $events;
|
||||
}
|
||||
|
||||
// use libkolab to compute recurring events
|
||||
$recurrence = new kolab_date_recurrence($object);
|
||||
|
||||
$i = 0;
|
||||
while ($next_event = $recurrence->next_instance()) {
|
||||
$datestr = $next_event['start']->format('Ymd');
|
||||
$instance_id = $next_event['start']->format($recurrence_id_format);
|
||||
|
||||
// use this event data for future recurring instances
|
||||
if (!empty($futuredata[$datestr])) {
|
||||
$overlay_data = $futuredata[$datestr];
|
||||
}
|
||||
|
||||
$rec_id = $event['uid'] . '-' . $instance_id;
|
||||
$exception = !empty($exdata[$datestr]) ? $exdata[$datestr] : $overlay_data;
|
||||
$event_start = $next_event['start'];
|
||||
$event_end = $next_event['end'];
|
||||
|
||||
// copy some event from exception to get proper start/end dates
|
||||
if ($exception) {
|
||||
$event_copy = $next_event;
|
||||
caldav_driver::merge_exception_dates($event_copy, $exception);
|
||||
$event_start = $event_copy['start'];
|
||||
$event_end = $event_copy['end'];
|
||||
}
|
||||
|
||||
// add to output if in range
|
||||
if (($event_start <= $end && $event_end >= $start) || ($event_id && $rec_id == $event_id)) {
|
||||
$rec_event = $this->_to_driver_event($next_event, false, false, $event);
|
||||
$rec_event['_instance'] = $instance_id;
|
||||
$rec_event['_count'] = $i + 1;
|
||||
|
||||
if ($exception) {
|
||||
// copy data from exception
|
||||
colab_driver::merge_exception_data($rec_event, $exception);
|
||||
}
|
||||
|
||||
$rec_event['id'] = $rec_id;
|
||||
$rec_event['recurrence_id'] = $event['uid'];
|
||||
$rec_event['recurrence'] = $recurrence_rule;
|
||||
unset($rec_event['_attendees']);
|
||||
$events[] = $rec_event;
|
||||
|
||||
if ($rec_id == $event_id) {
|
||||
$this->events[$rec_id] = $rec_event;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($limit && count($events) >= $limit) {
|
||||
return $events;
|
||||
}
|
||||
}
|
||||
else if ($next_event['start'] > $end) {
|
||||
// stop loop if out of range
|
||||
break;
|
||||
}
|
||||
|
||||
// avoid endless recursion loops
|
||||
if (++$i > 100000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from storage format to internal representation
|
||||
*/
|
||||
private function _to_driver_event($record, $noinst = false, $links = true, $master_event = null)
|
||||
{
|
||||
$record['calendar'] = $this->id;
|
||||
|
||||
// remove (possibly outdated) cached parameters
|
||||
unset($record['_folder_id'], $record['className']);
|
||||
|
||||
if ($links && !array_key_exists('links', $record)) {
|
||||
$record['links'] = $this->get_links($record['uid']);
|
||||
}
|
||||
|
||||
$ns = $this->get_namespace();
|
||||
|
||||
if ($ns == 'other') {
|
||||
$record['className'] = 'fc-event-ns-other';
|
||||
}
|
||||
|
||||
if ($ns == 'other' || !$this->cal->rc->config->get('kolab_invitation_calendars')) {
|
||||
$record = caldav_driver::add_partstat_class($record, ['NEEDS-ACTION', 'DECLINED'], $this->get_owner());
|
||||
|
||||
// Modify invitation status class name, when invitation calendars are disabled
|
||||
// we'll use opacity only for declined/needs-action events
|
||||
$record['className'] = str_replace('-invitation', '', $record['className']);
|
||||
}
|
||||
|
||||
// add instance identifier to first occurrence (master event)
|
||||
$recurrence_id_format = libcalendaring::recurrence_id_format($master_event ? $master_event : $record);
|
||||
if (!$noinst && !empty($record['recurrence']) && empty($record['recurrence_id']) && empty($record['_instance'])) {
|
||||
$record['_instance'] = $record['start']->format($recurrence_id_format);
|
||||
}
|
||||
else if (isset($record['recurrence_date']) && is_a($record['recurrence_date'], 'DateTime')) {
|
||||
$record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
|
||||
}
|
||||
|
||||
// clean up exception data
|
||||
if (!empty($record['recurrence']) && !empty($record['recurrence']['EXCEPTIONS'])) {
|
||||
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
|
||||
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
|
||||
});
|
||||
}
|
||||
|
||||
// Load the given event data into a libkolabxml container
|
||||
// it's needed for recurrence resolving, which uses libcalendaring
|
||||
// TODO: Drop dependency on libkolabxml?
|
||||
$event_xml = new kolab_format_event();
|
||||
$event_xml->set($record);
|
||||
$event['_formatobj'] = $event_xml;
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given event record into a data structure that can be passed to the storage backend for saving
|
||||
* (opposite of self::_to_driver_event())
|
||||
*/
|
||||
private function _from_driver_event($event, $old = [])
|
||||
{
|
||||
// set current user as ORGANIZER
|
||||
if ($identity = $this->cal->rc->user->list_emails(true)) {
|
||||
$event['attendees'] = !empty($event['attendees']) ? $event['attendees'] : [];
|
||||
$found = false;
|
||||
|
||||
// there can be only resources on attendees list (T1484)
|
||||
// let's check the existence of an organizer
|
||||
foreach ($event['attendees'] as $attendee) {
|
||||
if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$event['attendees'][] = ['role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']];
|
||||
}
|
||||
|
||||
$event['_owner'] = $identity['email'];
|
||||
}
|
||||
|
||||
// remove EXDATE values if RDATE is given
|
||||
if (!empty($event['recurrence']['RDATE'])) {
|
||||
$event['recurrence']['EXDATE'] = [];
|
||||
}
|
||||
|
||||
// remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
|
||||
if (!empty($event['recurrence']) && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
|
||||
$event['recurrence'] = [];
|
||||
}
|
||||
|
||||
// keep 'comment' from initial itip invitation
|
||||
if (!empty($old['comment'])) {
|
||||
$event['comment'] = $old['comment'];
|
||||
}
|
||||
|
||||
// remove some internal properties which should not be cached
|
||||
$cleanup_fn = function(&$event) {
|
||||
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
|
||||
$event['calendar'], $event['className'], $event['recurrence_id'],
|
||||
$event['attachments'], $event['deleted_attachments']);
|
||||
};
|
||||
|
||||
$cleanup_fn($event);
|
||||
|
||||
// clean up exception data
|
||||
if (!empty($event['exceptions'])) {
|
||||
array_walk($event['exceptions'], function(&$exception) use ($cleanup_fn) {
|
||||
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj']);
|
||||
$cleanup_fn($exception);
|
||||
});
|
||||
}
|
||||
|
||||
// copy meta data (starting with _) from old object
|
||||
foreach ((array) $old as $key => $val) {
|
||||
if (!isset($event[$key]) && $key[0] == '_') {
|
||||
$event[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the given word in the event contents
|
||||
*/
|
||||
public function fulltext_match($event, $word, $recursive = true)
|
||||
{
|
||||
$hits = 0;
|
||||
foreach ($this->search_fields as $col) {
|
||||
if (empty($event[$col])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
|
||||
if (empty($sval)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// do a simple substring matching (to be improved)
|
||||
$val = mb_strtolower($sval);
|
||||
if (strpos($val, $word) !== false) {
|
||||
$hits++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a complex event attribute to a string value
|
||||
*/
|
||||
private static function _complex2string($prop)
|
||||
{
|
||||
static $ignorekeys = ['role', 'status', 'rsvp'];
|
||||
|
||||
$out = '';
|
||||
if (is_array($prop)) {
|
||||
foreach ($prop as $key => $val) {
|
||||
if (is_numeric($key)) {
|
||||
$out .= self::_complex2string($val);
|
||||
}
|
||||
else if (!in_array($key, $ignorekeys)) {
|
||||
$out .= $val . ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (is_string($prop) || is_numeric($prop)) {
|
||||
$out .= $prop . ' ';
|
||||
}
|
||||
|
||||
return rtrim($out);
|
||||
}
|
||||
}
|
527
plugins/calendar/drivers/caldav/caldav_driver.php
Normal file
527
plugins/calendar/drivers/caldav/caldav_driver.php
Normal file
|
@ -0,0 +1,527 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* CalDAV driver for the Calendar plugin.
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2012-2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../kolab/kolab_driver.php');
|
||||
|
||||
class caldav_driver extends kolab_driver
|
||||
{
|
||||
// features this backend supports
|
||||
public $alarms = true;
|
||||
public $attendees = true;
|
||||
public $freebusy = true;
|
||||
public $attachments = false; // TODO
|
||||
public $undelete = false; // TODO
|
||||
public $alarm_types = ['DISPLAY', 'AUDIO'];
|
||||
public $categoriesimmutable = true;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct($cal)
|
||||
{
|
||||
$cal->require_plugin('libkolab');
|
||||
|
||||
// load helper classes *after* libkolab has been loaded (#3248)
|
||||
require_once(__DIR__ . '/caldav_calendar.php');
|
||||
// require_once(__DIR__ . '/kolab_user_calendar.php');
|
||||
// require_once(__DIR__ . '/caldav_invitation_calendar.php');
|
||||
|
||||
$this->cal = $cal;
|
||||
$this->rc = $cal->rc;
|
||||
|
||||
// Initialize the CalDAV storage
|
||||
$url = $this->rc->config->get('calendar_caldav_server', 'http://localhost');
|
||||
$this->storage = new kolab_storage_dav($url);
|
||||
|
||||
$this->cal->register_action('push-freebusy', [$this, 'push_freebusy']);
|
||||
$this->cal->register_action('calendar-acl', [$this, 'calendar_acl']);
|
||||
|
||||
// $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
|
||||
|
||||
// TODO: get configuration for the Bonnie API
|
||||
// $this->bonnie_api = libkolab::get_bonnie_api();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read available calendars from server
|
||||
*/
|
||||
protected function _read_calendars()
|
||||
{
|
||||
// already read sources
|
||||
if (isset($this->calendars)) {
|
||||
return $this->calendars;
|
||||
}
|
||||
|
||||
// get all folders that support VEVENT, sorted by namespace/name
|
||||
$folders = $this->storage->get_folders('event');
|
||||
// + $this->storage->get_user_folders('event', true);
|
||||
|
||||
$this->calendars = [];
|
||||
|
||||
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 caldav_calendar
|
||||
*/
|
||||
protected function _to_calendar($folder)
|
||||
{
|
||||
if ($folder instanceof caldav_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 caldav_calendar($folder, $this->cal);
|
||||
}
|
||||
|
||||
return $calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of available calendars from this source.
|
||||
*
|
||||
* @param int $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();
|
||||
|
||||
$folders = $this->filter_calendars($filter);
|
||||
$calendars = [];
|
||||
|
||||
// include virtual folders for a full folder tree
|
||||
/*
|
||||
if (!is_null($tree)) {
|
||||
$folders = $this->storage->folder_hierarchy($folders, $tree);
|
||||
}
|
||||
*/
|
||||
$parents = array_keys($this->calendars);
|
||||
|
||||
foreach ($folders as $id => $cal) {
|
||||
/*
|
||||
$path = explode('/', $cal->name);
|
||||
|
||||
// find parent
|
||||
do {
|
||||
array_pop($path);
|
||||
$parent_id = $this->storage->folder_id(implode('/', $path));
|
||||
}
|
||||
while (count($path) > 1 && !in_array($parent_id, $parents));
|
||||
|
||||
// restore "real" parent ID
|
||||
if ($parent_id && !in_array($parent_id, $parents)) {
|
||||
$parent_id = $this->storage->folder_id($cal->get_parent());
|
||||
}
|
||||
|
||||
$parents[] = $cal->id;
|
||||
|
||||
if ($cal->virtual) {
|
||||
$calendars[$cal->id] = [
|
||||
'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_dav::folder_hierarchy() above
|
||||
// make sure we deal with caldav_calendar instances
|
||||
$cal = $this->_to_calendar($cal);
|
||||
$this->calendars[$cal->id] = $cal;
|
||||
|
||||
$is_user = ($cal instanceof caldav_user_calendar);
|
||||
|
||||
$calendars[$cal->id] = [
|
||||
'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] += [
|
||||
'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 ([self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED] as $id) {
|
||||
$cal = new caldav_invitation_calendar($id, $this->cal);
|
||||
if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
|
||||
$calendars[$id] = [
|
||||
'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,
|
||||
'counts' => $id == self::INVITATIONS_CALENDAR_PENDING,
|
||||
];
|
||||
|
||||
|
||||
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', []); // read local prefs
|
||||
|
||||
if (!($filter & self::FILTER_ACTIVE) || !empty($prefs[$id]['active'])) {
|
||||
$calendars[$id] = [
|
||||
'id' => $id,
|
||||
'name' => $this->cal->gettext('birthdays'),
|
||||
'listname' => $this->cal->gettext('birthdays'),
|
||||
'color' => !empty($prefs[$id]['color']) ? $prefs[$id]['color'] : '87CEFA',
|
||||
'active' => !empty($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 the caldav_calendar instance for the given calendar ID
|
||||
*
|
||||
* @param string Calendar identifier (encoded imap folder name)
|
||||
*
|
||||
* @return ?caldav_calendar Object nor null if calendar doesn't exist
|
||||
*/
|
||||
public function get_calendar($id)
|
||||
{
|
||||
$this->_read_calendars();
|
||||
|
||||
// create calendar object if necesary
|
||||
if (empty($this->calendars[$id])) {
|
||||
if (in_array($id, [self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED])) {
|
||||
return new caldav_invitation_calendar($id, $this->cal);
|
||||
}
|
||||
|
||||
// for unsubscribed calendar folders
|
||||
if ($id !== self::BIRTHDAY_CALENDAR_ID) {
|
||||
$calendar = caldav_calendar::factory($id, $this->cal);
|
||||
if ($calendar->ready) {
|
||||
$this->calendars[$calendar->id] = $calendar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($this->calendars[$id]) ? $this->calendars[$id] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$this->calendars = [];
|
||||
$this->search_more_results = false;
|
||||
/*
|
||||
// find unsubscribed IMAP folders that have "event" type
|
||||
if ($source == 'folders') {
|
||||
foreach ((array) $this->storage->search_folders('event', $query, ['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') {
|
||||
// we have slightly more space, so display twice the number
|
||||
$limit = $this->rc->config->get('autocomplete_max', 15) * 2;
|
||||
|
||||
foreach ($this->storage->search_users($query, 0, [], $limit, $count) as $user) {
|
||||
$calendar = new caldav_user_calendar($user, $this->cal);
|
||||
$this->calendars[$calendar->id] = $calendar;
|
||||
|
||||
// search for calendar folders shared by this user
|
||||
foreach ($this->storage->list_user_folders($user, 'event', false) as $foldername) {
|
||||
$cal = new caldav_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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events from source.
|
||||
*
|
||||
* @param int Event's new start (unix timestamp)
|
||||
* @param int 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 bool Include virtual events (optional)
|
||||
* @param int 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 = [];
|
||||
$events = [];
|
||||
$categories = [];
|
||||
|
||||
if ($modifiedsince) {
|
||||
$query[] = ['changed', '>=', $modifiedsince];
|
||||
}
|
||||
|
||||
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);
|
||||
$newcats = array_udiff(
|
||||
array_keys($categories),
|
||||
array_keys($old_categories),
|
||||
function($a, $b) { return strcasecmp($a, $b); }
|
||||
);
|
||||
|
||||
if (!empty($newcats)) {
|
||||
foreach ($newcats as $category) {
|
||||
$old_categories[$category] = ''; // no color set yet
|
||||
}
|
||||
$this->rc->user->save_prefs(['calendar_categories' => $old_categories]);
|
||||
}
|
||||
|
||||
array_walk($events, 'caldav_driver::to_rcube_event');
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instances of a recurring event
|
||||
*
|
||||
* @param array Hash array with event properties
|
||||
* @param DateTime Start date of the recurrence window
|
||||
* @param 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
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function get_recurrence_count($event, $dtstart)
|
||||
{
|
||||
// load the given event data into a libkolabxml container
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$special_calendars = [
|
||||
self::BIRTHDAY_CALENDAR_ID,
|
||||
self::INVITATIONS_CALENDAR_PENDING,
|
||||
self::INVITATIONS_CALENDAR_DECLINED
|
||||
];
|
||||
|
||||
// show default dialog for birthday calendar
|
||||
if (in_array($calendar['id'], $special_calendars)) {
|
||||
if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID) {
|
||||
unset($formfields['showalarms']);
|
||||
}
|
||||
|
||||
// General tab
|
||||
$form['props'] = [
|
||||
'name' => $this->rc->gettext('properties'),
|
||||
'fields' => $formfields,
|
||||
];
|
||||
|
||||
return kolab_utils::folder_form($form, '', 'calendar');
|
||||
}
|
||||
|
||||
$this->_read_calendars();
|
||||
|
||||
if (!empty($calendar['id']) && ($cal = $this->calendars[$calendar['id']])) {
|
||||
$folder = $cal->get_realname(); // UTF7
|
||||
$color = $cal->get_color();
|
||||
}
|
||||
else {
|
||||
$folder = '';
|
||||
$color = '';
|
||||
}
|
||||
|
||||
$hidden_fields[] = ['name' => 'oldname', 'value' => $folder];
|
||||
|
||||
$form = [];
|
||||
$protected = false; // TODO
|
||||
|
||||
// Disable folder name input
|
||||
if ($protected) {
|
||||
$input_name = new html_hiddenfield(['name' => 'name', 'id' => 'calendar-name']);
|
||||
$formfields['name']['value'] = $this->storage->object_name($folder)
|
||||
. $input_name->show($folder);
|
||||
}
|
||||
|
||||
// calendar name (default field)
|
||||
$form['props']['fields']['location'] = $formfields['name'];
|
||||
|
||||
if ($protected) {
|
||||
// prevent user from moving folder
|
||||
$hidden_fields[] = ['name' => 'parent', 'value' => '']; // TODO
|
||||
}
|
||||
else {
|
||||
$select = $this->storage->folder_selector('event', ['name' => 'parent', 'id' => 'calendar-parent'], $folder);
|
||||
|
||||
$form['props']['fields']['path'] = [
|
||||
'id' => 'calendar-parent',
|
||||
'label' => $this->cal->gettext('parentcalendar'),
|
||||
'value' => $select->show(strlen($folder) ? '' : ''), // TODO
|
||||
];
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* CalDAV calendar storage class simulating a virtual calendar listing pedning/declined invitations
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2014-2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../kolab/kolab_driver.php');
|
||||
require_once(__DIR__ . '/../kolab/kolab_invitation_calendar.php');
|
||||
|
||||
class caldav_invitation_calendar extends kolab_invitation_calendar
|
||||
{
|
||||
public $id = '__caldav_invitation__';
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct($id, $calendar)
|
||||
{
|
||||
$this->cal = $calendar;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an URL for CalDAV access to this calendar (if configured)
|
||||
*/
|
||||
public function get_caldav_url()
|
||||
{
|
||||
return false; // TODO
|
||||
}
|
||||
}
|
|
@ -37,12 +37,13 @@ class kolab_driver extends calendar_driver
|
|||
public $alarm_types = ['DISPLAY', 'AUDIO'];
|
||||
public $categoriesimmutable = true;
|
||||
|
||||
private $rc;
|
||||
private $cal;
|
||||
private $calendars;
|
||||
private $has_writeable = false;
|
||||
private $freebusy_trigger = false;
|
||||
private $bonnie_api = false;
|
||||
protected $rc;
|
||||
protected $cal;
|
||||
protected $calendars;
|
||||
protected $storage;
|
||||
protected $has_writeable = false;
|
||||
protected $freebusy_trigger = false;
|
||||
protected $bonnie_api = false;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
|
@ -56,8 +57,9 @@ class kolab_driver extends calendar_driver
|
|||
require_once(__DIR__ . '/kolab_user_calendar.php');
|
||||
require_once(__DIR__ . '/kolab_invitation_calendar.php');
|
||||
|
||||
$this->cal = $cal;
|
||||
$this->rc = $cal->rc;
|
||||
$this->cal = $cal;
|
||||
$this->rc = $cal->rc;
|
||||
$this->storage = new kolab_storage();
|
||||
|
||||
$this->cal->register_action('push-freebusy', [$this, 'push_freebusy']);
|
||||
$this->cal->register_action('calendar-acl', [$this, 'calendar_acl']);
|
||||
|
@ -79,7 +81,7 @@ class kolab_driver extends calendar_driver
|
|||
/**
|
||||
* Read available calendars from server
|
||||
*/
|
||||
private function _read_calendars()
|
||||
protected function _read_calendars()
|
||||
{
|
||||
// already read sources
|
||||
if (isset($this->calendars)) {
|
||||
|
@ -87,8 +89,8 @@ class kolab_driver extends calendar_driver
|
|||
}
|
||||
|
||||
// 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)
|
||||
$folders = $this->storage->sort_folders(
|
||||
$this->storage->get_folders('event') + kolab_storage::get_user_folders('event', true)
|
||||
);
|
||||
|
||||
$this->calendars = [];
|
||||
|
@ -109,7 +111,7 @@ class kolab_driver extends calendar_driver
|
|||
/**
|
||||
* Convert kolab_storage_folder into kolab_calendar
|
||||
*/
|
||||
private function _to_calendar($folder)
|
||||
protected function _to_calendar($folder)
|
||||
{
|
||||
if ($folder instanceof kolab_calendar) {
|
||||
return $folder;
|
||||
|
@ -152,7 +154,7 @@ class kolab_driver extends calendar_driver
|
|||
|
||||
// include virtual folders for a full folder tree
|
||||
if (!is_null($tree)) {
|
||||
$folders = kolab_storage::folder_hierarchy($folders, $tree);
|
||||
$folders = $this->storage->folder_hierarchy($folders, $tree);
|
||||
}
|
||||
|
||||
$parents = array_keys($this->calendars);
|
||||
|
@ -163,13 +165,13 @@ class kolab_driver extends calendar_driver
|
|||
// find parent
|
||||
do {
|
||||
array_pop($imap_path);
|
||||
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
|
||||
$parent_id = $this->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());
|
||||
$parent_id = $this->storage->folder_id($cal->get_parent());
|
||||
}
|
||||
|
||||
$parents[] = $cal->id;
|
||||
|
@ -385,15 +387,15 @@ class kolab_driver extends calendar_driver
|
|||
$prop['active'] = true;
|
||||
$prop['subscribed'] = true;
|
||||
|
||||
$folder = kolab_storage::folder_update($prop);
|
||||
$folder = $this->storage->folder_update($prop);
|
||||
|
||||
if ($folder === false) {
|
||||
$this->last_error = $this->cal->gettext(kolab_storage::$last_error);
|
||||
$this->last_error = $this->cal->gettext($this->storage->last_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// create ID
|
||||
$id = kolab_storage::folder_id($folder);
|
||||
$id = $this->storage->folder_id($folder);
|
||||
|
||||
// save color in user prefs (temp. solution)
|
||||
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
|
||||
|
@ -467,22 +469,22 @@ class kolab_driver extends calendar_driver
|
|||
|
||||
// apply to child folders, too
|
||||
if (!empty($prop['recursive'])) {
|
||||
foreach ((array) kolab_storage::list_folders($cal->storage->name, '*', 'event') as $subfolder) {
|
||||
foreach ((array) $this->storage->list_folders($cal->storage->name, '*', 'event') as $subfolder) {
|
||||
if (isset($prop['permanent'])) {
|
||||
if ($prop['permanent']) {
|
||||
kolab_storage::folder_subscribe($subfolder);
|
||||
$this->storage->folder_subscribe($subfolder);
|
||||
}
|
||||
else {
|
||||
kolab_storage::folder_unsubscribe($subfolder);
|
||||
$this->storage->folder_unsubscribe($subfolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($prop['active'])) {
|
||||
if ($prop['active']) {
|
||||
kolab_storage::folder_activate($subfolder);
|
||||
$this->storage->folder_activate($subfolder);
|
||||
}
|
||||
else {
|
||||
kolab_storage::folder_deactivate($subfolder);
|
||||
$this->storage->folder_deactivate($subfolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +513,7 @@ class kolab_driver extends calendar_driver
|
|||
$folder = $cal->get_realname();
|
||||
|
||||
// TODO: unsubscribe if no admin rights
|
||||
if (kolab_storage::folder_delete($folder)) {
|
||||
if ($this->storage->folder_delete($folder)) {
|
||||
// remove color in user prefs (temp. solution)
|
||||
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
|
||||
unset($prefs['kolab_calendars'][$prop['id']]);
|
||||
|
@ -520,7 +522,7 @@ class kolab_driver extends calendar_driver
|
|||
return true;
|
||||
}
|
||||
else {
|
||||
$this->last_error = kolab_storage::$last_error;
|
||||
$this->last_error = $this->storage->last_error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -537,7 +539,7 @@ class kolab_driver extends calendar_driver
|
|||
*/
|
||||
public function search_calendars($query, $source)
|
||||
{
|
||||
if (!kolab_storage::setup()) {
|
||||
if (!$this->storage->setup()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -546,7 +548,7 @@ class kolab_driver extends calendar_driver
|
|||
|
||||
// find unsubscribed IMAP folders that have "event" type
|
||||
if ($source == 'folders') {
|
||||
foreach ((array) kolab_storage::search_folders('event', $query, ['other']) as $folder) {
|
||||
foreach ((array) $this->storage->search_folders('event', $query, ['other']) as $folder) {
|
||||
$calendar = new kolab_calendar($folder->name, $this->cal);
|
||||
$this->calendars[$calendar->id] = $calendar;
|
||||
}
|
||||
|
@ -556,12 +558,12 @@ class kolab_driver extends calendar_driver
|
|||
// we have slightly more space, so display twice the number
|
||||
$limit = $this->rc->config->get('autocomplete_max', 15) * 2;
|
||||
|
||||
foreach (kolab_storage::search_users($query, 0, [], $limit, $count) as $user) {
|
||||
foreach ($this->storage->search_users($query, 0, [], $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) {
|
||||
foreach ($this->storage->list_user_folders($user, 'event', false) as $foldername) {
|
||||
$cal = new kolab_calendar($foldername, $this->cal);
|
||||
$this->calendars[$cal->id] = $cal;
|
||||
$calendar->subscriptions = true;
|
||||
|
@ -1001,7 +1003,7 @@ class kolab_driver extends calendar_driver
|
|||
/**
|
||||
* Wrapper to update an event object depending on the given savemode
|
||||
*/
|
||||
private function update_event($event)
|
||||
protected function update_event($event)
|
||||
{
|
||||
if (!($storage = $this->get_calendar($event['calendar']))) {
|
||||
return false;
|
||||
|
@ -1970,7 +1972,7 @@ class kolab_driver extends calendar_driver
|
|||
/**
|
||||
*
|
||||
*/
|
||||
private function get_recurrence_count($event, $dtstart)
|
||||
protected function get_recurrence_count($event, $dtstart)
|
||||
{
|
||||
// load the given event data into a libkolabxml container
|
||||
if (empty($event['_formatobj'])) {
|
||||
|
@ -2014,7 +2016,7 @@ class kolab_driver extends calendar_driver
|
|||
'follow_redirects' => true,
|
||||
];
|
||||
|
||||
$request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config);
|
||||
$request = libkolab::http_request($this->storage->get_freebusy_url($email), 'GET', $request_config);
|
||||
$response = $request->send();
|
||||
|
||||
// authentication required
|
||||
|
@ -2501,7 +2503,7 @@ class kolab_driver extends calendar_driver
|
|||
*
|
||||
* @return array (uid,folder,msguid) tuple
|
||||
*/
|
||||
private function _resolve_event_identity($event)
|
||||
protected function _resolve_event_identity($event)
|
||||
{
|
||||
$mailbox = $msguid = null;
|
||||
|
||||
|
@ -2602,7 +2604,7 @@ class kolab_driver extends calendar_driver
|
|||
// Disable folder name input
|
||||
if ($protected) {
|
||||
$input_name = new html_hiddenfield(['name' => 'name', 'id' => 'calendar-name']);
|
||||
$formfields['name']['value'] = kolab_storage::object_name($folder)
|
||||
$formfields['name']['value'] = $this->storage->object_name($folder)
|
||||
. $input_name->show($folder);
|
||||
}
|
||||
|
||||
|
@ -2614,7 +2616,7 @@ class kolab_driver extends calendar_driver
|
|||
$hidden_fields[] = ['name' => 'parent', 'value' => $path_imap];
|
||||
}
|
||||
else {
|
||||
$select = kolab_storage::folder_selector('event', ['name' => 'parent', 'id' => 'calendar-parent'], $folder);
|
||||
$select = $this->storage->folder_selector('event', ['name' => 'parent', 'id' => 'calendar-parent'], $folder);
|
||||
|
||||
$form['props']['fields']['path'] = [
|
||||
'id' => 'calendar-parent',
|
||||
|
|
|
@ -177,6 +177,23 @@ CREATE TABLE `kolab_cache_freebusy` (
|
|||
INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
|
||||
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `kolab_cache_dav_event`;
|
||||
|
||||
CREATE TABLE `kolab_cache_dav_event` (
|
||||
`folder_id` BIGINT UNSIGNED NOT NULL,
|
||||
`uid` VARCHAR(512) NOT NULL,
|
||||
`created` DATETIME DEFAULT NULL,
|
||||
`changed` DATETIME DEFAULT NULL,
|
||||
`data` LONGTEXT NOT NULL,
|
||||
`tags` TEXT NOT NULL,
|
||||
`words` TEXT NOT NULL,
|
||||
`dtstart` DATETIME,
|
||||
`dtend` DATETIME,
|
||||
CONSTRAINT `fk_kolab_cache_dav_event_folder` FOREIGN KEY (`folder_id`)
|
||||
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY(`folder_id`,`uid`)
|
||||
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2021101100');
|
||||
REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2022100500');
|
||||
|
|
450
plugins/libkolab/lib/kolab_dav_client.php
Normal file
450
plugins/libkolab/lib/kolab_dav_client.php
Normal file
|
@ -0,0 +1,450 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A *DAV client.
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* 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_dav_client
|
||||
{
|
||||
public $url;
|
||||
|
||||
protected $rc;
|
||||
protected $responseHeaders = [];
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*/
|
||||
public function __construct($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->rc = rcube::get_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute HTTP request to a DAV server
|
||||
*/
|
||||
protected function request($path, $method, $body = '', $headers = [])
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
$debug = (array) $rcube->config->get('dav_debug');
|
||||
|
||||
$request_config = [
|
||||
'store_body' => true,
|
||||
'follow_redirects' => true,
|
||||
];
|
||||
|
||||
$this->responseHeaders = [];
|
||||
|
||||
if ($path && ($rootPath = parse_url($this->url, PHP_URL_PATH)) && strpos($path, $rootPath) === 0) {
|
||||
$path = substr($path, strlen($rootPath));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$request = $this->initRequest($this->url . $path, $method, $request_config);
|
||||
|
||||
$request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
|
||||
|
||||
if ($body) {
|
||||
$request->setBody($body);
|
||||
$request->setHeader(['Content-Type' => 'application/xml; charset=utf-8']);
|
||||
}
|
||||
|
||||
if (!empty($headers)) {
|
||||
$request->setHeader($headers);
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
rcube::write_log('dav', "C: {$method}: " . (string) $request->getUrl()
|
||||
. "\n" . $this->debugBody($body, $request->getHeaders()));
|
||||
}
|
||||
|
||||
$response = $request->send();
|
||||
|
||||
$body = $response->getBody();
|
||||
$code = $response->getStatus();
|
||||
|
||||
if ($debug) {
|
||||
rcube::write_log('dav', "S: [{$code}]\n" . $this->debugBody($body, $response->getHeader()));
|
||||
}
|
||||
|
||||
if ($code >= 300) {
|
||||
throw new Exception("DAV Error ($code):\n{$body}");
|
||||
}
|
||||
|
||||
$this->responseHeaders = $response->getHeader();
|
||||
|
||||
return $this->parseXML($body);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover DAV folders of specified type on the server
|
||||
*/
|
||||
public function discover($component = 'VEVENT')
|
||||
{
|
||||
/*
|
||||
$path = parse_url($this->url, PHP_URL_PATH);
|
||||
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?>'
|
||||
. '<d:propfind xmlns:d="DAV:">'
|
||||
. '<d:prop>'
|
||||
. '<d:current-user-principal />'
|
||||
. '</d:prop>'
|
||||
. '</d:propfind>';
|
||||
|
||||
$response = $this->request('/calendars', 'PROPFIND', $body);
|
||||
|
||||
$elements = $response->getElementsByTagName('response');
|
||||
|
||||
foreach ($elements as $element) {
|
||||
foreach ($element->getElementsByTagName('prop') as $prop) {
|
||||
$principal_href = $prop->nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($path && strpos($principal_href, $path) === 0) {
|
||||
$principal_href = substr($principal_href, strlen($path));
|
||||
}
|
||||
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?>'
|
||||
. '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
|
||||
. '<d:prop>'
|
||||
. '<c:calendar-home-set />'
|
||||
. '</d:prop>'
|
||||
. '</d:propfind>';
|
||||
|
||||
$response = $this->request($principal_href, 'PROPFIND', $body);
|
||||
*/
|
||||
$roots = [
|
||||
'VEVENT' => 'calendars',
|
||||
'VTODO' => 'calendars',
|
||||
'VCARD' => 'addressbooks',
|
||||
];
|
||||
|
||||
$principal_href = '/' . $roots[$component] . '/' . $this->rc->user->get_username();
|
||||
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?>'
|
||||
. '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/">'
|
||||
. '<d:prop>'
|
||||
. '<d:resourcetype />'
|
||||
. '<d:displayname />'
|
||||
. '<cs:getctag />'
|
||||
. '<c:supported-calendar-component-set />'
|
||||
. '<a:calendar-color />'
|
||||
. '</d:prop>'
|
||||
. '</d:propfind>';
|
||||
|
||||
$response = $this->request($principal_href, 'PROPFIND', $body);
|
||||
|
||||
if (empty($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$folders = [];
|
||||
|
||||
foreach ($response->getElementsByTagName('response') as $element) {
|
||||
$folder = $this->getFolderPropertiesFromResponse($element);
|
||||
if ($folder['type'] === $component) {
|
||||
$folders[] = $folder;
|
||||
}
|
||||
}
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DAV object in a folder
|
||||
*/
|
||||
public function create($location, $content)
|
||||
{
|
||||
$response = $this->request($location, 'PUT', $content, ['Content-Type' => 'text/calendar; charset=utf-8']);
|
||||
|
||||
if ($response !== false) {
|
||||
$etag = $this->responseHeaders['etag'];
|
||||
|
||||
if (preg_match('|^".*"$|', $etag)) {
|
||||
$etag = substr($etag, 1, -1);
|
||||
}
|
||||
|
||||
return $etag;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete DAV object from a folder
|
||||
*/
|
||||
public function delete($location)
|
||||
{
|
||||
$response = $this->request($location, 'DELETE', '', ['Depth' => 1, 'Prefer' => 'return-minimal']);
|
||||
|
||||
return $response !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch DAV objects metadata (ETag, href) a folder
|
||||
*/
|
||||
public function getIndex($location, $component = 'VEVENT')
|
||||
{
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?>'
|
||||
.' <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
|
||||
. '<d:prop>'
|
||||
. '<d:getetag />'
|
||||
. '</d:prop>'
|
||||
. '<c:filter>'
|
||||
. '<c:comp-filter name="VCALENDAR">'
|
||||
. '<c:comp-filter name="' . $component . '" />'
|
||||
. '</c:comp-filter>'
|
||||
. '</c:filter>'
|
||||
. '</c:calendar-query>';
|
||||
|
||||
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
|
||||
|
||||
if (empty($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$objects = [];
|
||||
|
||||
foreach ($response->getElementsByTagName('response') as $element) {
|
||||
$objects[] = $this->getObjectPropertiesFromResponse($element);
|
||||
}
|
||||
|
||||
return $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch DAV objects data from a folder
|
||||
*/
|
||||
public function getData($location, $hrefs = [])
|
||||
{
|
||||
if (empty($hrefs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = '';
|
||||
foreach ($hrefs as $href) {
|
||||
$body .= '<d:href>' . $href . '</d:href>';
|
||||
}
|
||||
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?>'
|
||||
.' <c:calendar-multiget xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
|
||||
. '<d:prop>'
|
||||
. '<d:getetag />'
|
||||
. '<c:calendar-data />'
|
||||
. '</d:prop>'
|
||||
. $body
|
||||
. '</c:calendar-multiget>';
|
||||
|
||||
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
|
||||
|
||||
if (empty($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$objects = [];
|
||||
|
||||
foreach ($response->getElementsByTagName('response') as $element) {
|
||||
$objects[] = $this->getObjectPropertiesFromResponse($element);
|
||||
}
|
||||
|
||||
return $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse XML content
|
||||
*/
|
||||
protected function parseXML($xml)
|
||||
{
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
if (stripos($xml, '<?xml') === 0) {
|
||||
if (!$doc->loadXML($xml)) {
|
||||
throw new Exception("Failed to parse XML");
|
||||
}
|
||||
|
||||
$doc->formatOutput = true;
|
||||
}
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse request/response body for debug purposes
|
||||
*/
|
||||
protected function debugBody($body, $headers)
|
||||
{
|
||||
$head = '';
|
||||
foreach ($headers as $header_name => $header_value) {
|
||||
$head .= "{$header_name}: {$header_value}\n";
|
||||
}
|
||||
|
||||
if (stripos($body, '<?xml') === 0) {
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
if (!$doc->loadXML($body)) {
|
||||
throw new Exception("Failed to parse XML");
|
||||
}
|
||||
|
||||
$doc->formatOutput = true;
|
||||
|
||||
$body = $doc->saveXML();
|
||||
}
|
||||
|
||||
return $head . "\n" . rtrim($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract folder properties from a server 'response' element
|
||||
*/
|
||||
protected function getFolderPropertiesFromResponse(DOMNode $element)
|
||||
{
|
||||
|
||||
if ($href = $element->getElementsByTagName('href')->item(0)) {
|
||||
$href = $href->nodeValue;
|
||||
/*
|
||||
$path = parse_url($this->url, PHP_URL_PATH);
|
||||
if ($path && strpos($href, $path) === 0) {
|
||||
$href = substr($href, strlen($path));
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
if ($color = $element->getElementsByTagName('calendar-color')->item(0)) {
|
||||
if (preg_match('/^#[0-9A-F]{8}$/', $color->nodeValue)) {
|
||||
$color = substr($color->nodeValue, 1, -2);
|
||||
} else {
|
||||
$color = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($name = $element->getElementsByTagName('displayname')->item(0)) {
|
||||
$name = $name->nodeValue;
|
||||
}
|
||||
|
||||
if ($ctag = $element->getElementsByTagName('getctag')->item(0)) {
|
||||
$ctag = $ctag->nodeValue;
|
||||
}
|
||||
|
||||
$component = null;
|
||||
if ($set_element = $element->getElementsByTagName('supported-calendar-component-set')->item(0)) {
|
||||
if ($comp_element = $set_element->getElementsByTagName('comp')->item(0)) {
|
||||
$component = $comp_element->attributes->getNamedItem('name')->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'href' => $href,
|
||||
'name' => $name,
|
||||
'ctag' => $ctag,
|
||||
'color' => $color,
|
||||
'type' => $component,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract object properties from a server 'response' element
|
||||
*/
|
||||
protected function getObjectPropertiesFromResponse(DOMNode $element)
|
||||
{
|
||||
$uid = null;
|
||||
if ($href = $element->getElementsByTagName('href')->item(0)) {
|
||||
$href = $href->nodeValue;
|
||||
/*
|
||||
$path = parse_url($this->url, PHP_URL_PATH);
|
||||
if ($path && strpos($href, $path) === 0) {
|
||||
$href = substr($href, strlen($path));
|
||||
}
|
||||
*/
|
||||
// Extract UID from the URL
|
||||
$href_parts = explode('/', $href);
|
||||
$uid = preg_replace('/\.[a-z]+$/', '', $href_parts[count($href_parts)-1]);
|
||||
}
|
||||
|
||||
if ($data = $element->getElementsByTagName('calendar-data')->item(0)) {
|
||||
$data = $data->nodeValue;
|
||||
}
|
||||
|
||||
if ($etag = $element->getElementsByTagName('getetag')->item(0)) {
|
||||
$etag = $etag->nodeValue;
|
||||
if (preg_match('|^".*"$|', $etag)) {
|
||||
$etag = substr($etag, 1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'href' => $href,
|
||||
'data' => $data,
|
||||
'etag' => $etag,
|
||||
'uid' => $uid,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize HTTP request object
|
||||
*/
|
||||
protected function initRequest($url = '', $method = 'GET', $config = array())
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
$http_config = (array) $rcube->config->get('kolab_http_request');
|
||||
|
||||
// deprecated configuration options
|
||||
if (empty($http_config)) {
|
||||
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
|
||||
$value = $rcube->config->get('kolab_' . $option, true);
|
||||
if (is_bool($value)) {
|
||||
$http_config[$option] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($config)) {
|
||||
$http_config = array_merge($http_config, $config);
|
||||
}
|
||||
|
||||
// load HTTP_Request2
|
||||
require_once 'HTTP/Request2.php';
|
||||
|
||||
try {
|
||||
$request = new HTTP_Request2();
|
||||
$request->setConfig($http_config);
|
||||
|
||||
// proxy User-Agent string
|
||||
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
|
||||
|
||||
// cleanup
|
||||
$request->setBody('');
|
||||
$request->setUrl($url);
|
||||
$request->setMethod($method);
|
||||
|
||||
return $request;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -101,8 +101,8 @@ class kolab_storage_cache
|
|||
*/
|
||||
public function select_by_id($folder_id)
|
||||
{
|
||||
$sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id));
|
||||
if ($sql_arr) {
|
||||
$query = $this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id);
|
||||
if ($sql_arr = $this->db->fetch_assoc($query)) {
|
||||
$this->metadata = $sql_arr;
|
||||
$this->folder_id = $sql_arr['folder_id'];
|
||||
$this->folder = new StdClass;
|
||||
|
|
|
@ -79,18 +79,16 @@ class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
|
|||
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
$uid = $value['_msguid'];
|
||||
$uid = !empty($value['_msguid']) ? $value['_msguid'] : $value['uid'];
|
||||
|
||||
if (is_null($offset)) {
|
||||
$offset = count($this->index);
|
||||
$this->index[] = $uid;
|
||||
}
|
||||
else {
|
||||
$this->index[$offset] = $uid;
|
||||
}
|
||||
|
||||
$this->index[$offset] = $uid;
|
||||
|
||||
// keep full payload data in memory if possible
|
||||
if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
|
||||
if ($this->memlimit && $this->buffer) {
|
||||
$this->data[$offset] = $value;
|
||||
|
||||
// check memory usage and stop buffering
|
||||
|
@ -115,8 +113,9 @@ class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
|
|||
if (isset($this->data[$offset])) {
|
||||
return $this->data[$offset];
|
||||
}
|
||||
else if ($msguid = $this->index[$offset]) {
|
||||
return $this->cache->get($msguid);
|
||||
|
||||
if ($uid = $this->index[$offset]) {
|
||||
return $this->cache->get($uid);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
476
plugins/libkolab/lib/kolab_storage_dav.php
Normal file
476
plugins/libkolab/lib/kolab_storage_dav.php
Normal file
|
@ -0,0 +1,476 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Kolab storage class providing access to groupware objects on a *DAV server.
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* 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_dav
|
||||
{
|
||||
const ERROR_DAV_CONN = 1;
|
||||
const ERROR_CACHE_DB = 2;
|
||||
const ERROR_NO_PERMISSION = 3;
|
||||
const ERROR_INVALID_FOLDER = 4;
|
||||
|
||||
protected $dav;
|
||||
protected $url;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*/
|
||||
public function __construct($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the environment
|
||||
*/
|
||||
public function setup()
|
||||
{
|
||||
$rcmail = rcube::get_instance();
|
||||
|
||||
$this->config = $rcmail->config;
|
||||
$this->dav = new kolab_dav_client($this->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @return array List of kolab_storage_dav_folder objects
|
||||
*/
|
||||
public function get_folders($type)
|
||||
{
|
||||
// TODO: This should be cached
|
||||
$folders = $this->dav->discover();
|
||||
|
||||
if (is_array($folders)) {
|
||||
foreach ($folders as $idx => $folder) {
|
||||
$folders[$idx] = new kolab_storage_dav_folder($this->dav, $folder, $type);
|
||||
}
|
||||
}
|
||||
|
||||
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_dav_folder The folder object
|
||||
*/
|
||||
public function get_default_folder($type)
|
||||
{
|
||||
// TODO: Not used
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for a specific storage folder
|
||||
*
|
||||
* @param string Folder to access (UTF7-IMAP)
|
||||
* @param string Expected folder type
|
||||
*
|
||||
* @return object kolab_storage_folder The folder object
|
||||
*/
|
||||
public function get_folder($folder, $type = null)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 function get_object($uid, $type)
|
||||
{
|
||||
// TODO
|
||||
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)
|
||||
*/
|
||||
public function select($query, $type, $limit = null)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->get_folders($type) as $folder) {
|
||||
if ($limit) {
|
||||
$folder->set_order_and_limit(null, $limit);
|
||||
}
|
||||
|
||||
foreach ($folder->select($query) as $object) {
|
||||
$result[] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return kolab_storage::get_freebusy_url($email, $start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a folder
|
||||
*
|
||||
* @param string $name Folder name
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function folder_delete($name)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 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 function folder_create($name, $type = null, $subscribed = false, $active = false)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames DAV 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 function folder_rename($oldname, $newname)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename or Create a new 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
|
||||
*/
|
||||
public function folder_update(&$prop)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for human-readable name of a folder
|
||||
*
|
||||
* @param string $folder 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 function object_name($folder, &$folder_ns = null)
|
||||
{
|
||||
// TODO: Shared folders
|
||||
$folder_ns = 'personal';
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 function folder_selector($type, $attrs, $current = '')
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool 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 function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 function search_folders($type, $query, $exclude_ns = [])
|
||||
{
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the given list of folders by namespace/name
|
||||
*
|
||||
* @param array List of kolab_storage_dav_folder objects
|
||||
*
|
||||
* @return array Sorted list of folders
|
||||
*/
|
||||
public static function sort_folders($folders)
|
||||
{
|
||||
// TODO
|
||||
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 function folders_typedata($prefix = '*')
|
||||
{
|
||||
// TODO: Used by kolab_folders, kolab_activesync, kolab_delegation
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns type of a DAV folder
|
||||
*
|
||||
* @param string $folder Folder name (UTF7-IMAP)
|
||||
*
|
||||
* @return string Folder type
|
||||
*/
|
||||
public function folder_type($folder)
|
||||
{
|
||||
// TODO: Used by kolab_folders, kolab_activesync, kolab_delegation
|
||||
return 'event';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets folder content-type.
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
* @param string $type Content type
|
||||
*
|
||||
* @return bool True on success, False otherwise
|
||||
*/
|
||||
public function set_folder_type($folder, $type = 'mail')
|
||||
{
|
||||
// NOP: Used by kolab_folders, kolab_activesync, kolab_delegation
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check subscription status of this folder
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
* @param bool $temp Include temporary/session subscriptions
|
||||
*
|
||||
* @return bool True if subscribed, false if not
|
||||
*/
|
||||
public function folder_is_subscribed($folder, $temp = false)
|
||||
{
|
||||
// NOP
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change subscription status of this folder
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
* @param bool $temp Only subscribe temporarily for the current session
|
||||
*
|
||||
* @return True on success, false on error
|
||||
*/
|
||||
public function folder_subscribe($folder, $temp = false)
|
||||
{
|
||||
// NOP
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change subscription status of this folder
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
* @param bool $temp Only remove temporary subscription
|
||||
*
|
||||
* @return True on success, false on error
|
||||
*/
|
||||
public function folder_unsubscribe($folder, $temp = false)
|
||||
{
|
||||
// NOP
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check activation status of this folder
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
*
|
||||
* @return bool True if active, false if not
|
||||
*/
|
||||
public function folder_is_active($folder)
|
||||
{
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change activation status of this folder
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
*
|
||||
* @return True on success, false on error
|
||||
*/
|
||||
public function folder_activate($folder)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change activation status of this folder
|
||||
*
|
||||
* @param string $folder Folder name
|
||||
*
|
||||
* @return True on success, false on error
|
||||
*/
|
||||
public function folder_deactivate($folder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 function create_default_folder($type, $props = [])
|
||||
{
|
||||
// TODO: For kolab_addressbook??
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 function list_user_folders($user, $type, $subscribed = 0, &$folderdata = [])
|
||||
{
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool Enable to return subscribed folders only (null to use configured subscription mode)
|
||||
*
|
||||
* @return array List of kolab_storage_folder_user objects
|
||||
*/
|
||||
public function get_user_folders($type, $subscribed)
|
||||
{
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$table = $db->table_name('kolab_folders', true);
|
||||
$prefix = 'dav://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
|
||||
|
||||
$db->query("DELETE FROM $table WHERE `resource` LIKE ?", $prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get folder METADATA for all supported keys
|
||||
* Do this in one go for better caching performance
|
||||
*/
|
||||
public function folder_metadata($folder)
|
||||
{
|
||||
// TODO ?
|
||||
return [];
|
||||
}
|
||||
}
|
622
plugins/libkolab/lib/kolab_storage_dav_cache.php
Normal file
622
plugins/libkolab/lib/kolab_storage_dav_cache.php
Normal file
|
@ -0,0 +1,622 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
|
||||
*
|
||||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
|
||||
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* 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_dav_cache extends kolab_storage_cache
|
||||
{
|
||||
/**
|
||||
* Factory constructor
|
||||
*/
|
||||
public static function factory(kolab_storage_folder $storage_folder)
|
||||
{
|
||||
$subclass = 'kolab_storage_dav_cache_' . $storage_folder->type;
|
||||
if (class_exists($subclass)) {
|
||||
return new $subclass($storage_folder);
|
||||
}
|
||||
|
||||
rcube::raise_error(
|
||||
['code' => 900, 'message' => "No {$subclass} class found for folder '{$storage_folder->name}'"],
|
||||
true
|
||||
);
|
||||
|
||||
return new kolab_storage_dav_cache($storage_folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect cache with a storage folder
|
||||
*
|
||||
* @param kolab_storage_folder The storage folder instance to connect with
|
||||
*/
|
||||
public function set_folder(kolab_storage_folder $storage_folder)
|
||||
{
|
||||
$this->folder = $storage_folder;
|
||||
|
||||
if (!$this->folder->valid) {
|
||||
$this->ready = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// compose fully qualified ressource uri for this instance
|
||||
$this->resource_uri = $this->folder->get_resource_uri();
|
||||
$this->cache_table = $this->db->table_name('kolab_cache_dav_' . $this->folder->type);
|
||||
$this->ready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize local cache data with remote
|
||||
*/
|
||||
public function synchronize()
|
||||
{
|
||||
// only sync once per request cycle
|
||||
if ($this->synched) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sync_start = time();
|
||||
|
||||
// read cached folder metadata
|
||||
$this->_read_folder_data();
|
||||
|
||||
$ctag = $this->folder->get_ctag();
|
||||
|
||||
// check cache status ($this->metadata is set in _read_folder_data())
|
||||
if (
|
||||
empty($this->metadata['ctag'])
|
||||
|| empty($this->metadata['changed'])
|
||||
|| $this->metadata['ctag'] !== $ctag
|
||||
) {
|
||||
// lock synchronization for this folder and wait if already locked
|
||||
$this->_sync_lock();
|
||||
|
||||
$result = $this->synchronize_worker();
|
||||
|
||||
// update ctag value (will be written to database in _sync_unlock())
|
||||
if ($result) {
|
||||
$this->metadata['ctag'] = $ctag;
|
||||
$this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
|
||||
}
|
||||
|
||||
// remove lock
|
||||
$this->_sync_unlock();
|
||||
}
|
||||
|
||||
$this->synched = time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform cache synchronization
|
||||
*/
|
||||
protected function synchronize_worker()
|
||||
{
|
||||
// get effective time limit we have for synchronization (~70% of the execution time)
|
||||
$time_limit = $this->_max_sync_lock_time() * 0.7;
|
||||
|
||||
if (time() - $this->sync_start > $time_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Implement synchronization with use of WebDAV-Sync (RFC 6578)
|
||||
|
||||
// Get the objects from the DAV server
|
||||
$dav_index = $this->folder->dav->getIndex($this->folder->href, $this->folder->get_dav_type());
|
||||
|
||||
if (!is_array($dav_index)) {
|
||||
rcube::raise_error([
|
||||
'code' => 900,
|
||||
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
|
||||
], true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// WARNING: For now we assume object's href is <calendar-href>/<uid>.ics,
|
||||
// which would mean there are no duplicates (objects with the same uid).
|
||||
// With DAV protocol we can't get UID without fetching the whole object.
|
||||
// Also the folder_id + uid is a unique index in the database.
|
||||
// In the future we maybe should store the href in database.
|
||||
|
||||
// Determine objects to fetch or delete
|
||||
$new_index = [];
|
||||
$update_index = [];
|
||||
$old_index = $this->current_index(); // uid -> etag
|
||||
$chunk_size = 20; // max numer of objects per DAV request
|
||||
|
||||
foreach ($dav_index as $object) {
|
||||
$uid = $object['uid'];
|
||||
if (isset($old_index[$uid])) {
|
||||
$old_etag = $old_index[$uid];
|
||||
$old_index[$uid] = null;
|
||||
|
||||
if ($old_etag === $object['etag']) {
|
||||
// the object didn't change
|
||||
continue;
|
||||
}
|
||||
|
||||
$update_index[$uid] = $object['href'];
|
||||
}
|
||||
else {
|
||||
$new_index[$uid] = $object['href'];
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new objects and store in DB
|
||||
if (!empty($new_index)) {
|
||||
foreach (array_chunk($new_index, $chunk_size, true) as $chunk) {
|
||||
$objects = $this->folder->dav->getData($this->folder->href, $chunk);
|
||||
|
||||
if (!is_array($objects)) {
|
||||
rcube::raise_error([
|
||||
'code' => 900,
|
||||
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
|
||||
], true);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($objects as $object) {
|
||||
if ($object = $this->folder->from_dav($object)) {
|
||||
$this->_extended_insert(false, $object);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_extended_insert(true, null);
|
||||
|
||||
// check time limit and abort sync if running too long
|
||||
if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch updated objects and store in DB
|
||||
if (!empty($update_index)) {
|
||||
foreach (array_chunk($update_index, $chunk_size, true) as $chunk) {
|
||||
$objects = $this->folder->dav->getData($this->folder->href, $chunk);
|
||||
|
||||
if (!is_array($objects)) {
|
||||
rcube::raise_error([
|
||||
'code' => 900,
|
||||
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
|
||||
], true);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($objects as $object) {
|
||||
if ($object = $this->folder->from_dav($object)) {
|
||||
$this->save($object, $object['uid']);
|
||||
}
|
||||
}
|
||||
|
||||
// check time limit and abort sync if running too long
|
||||
if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove deleted objects
|
||||
$old_index = array_filter($old_index);
|
||||
if (!empty($old_index)) {
|
||||
$quoted_uids = join(',', array_map(array($this->db, 'quote'), $old_index));
|
||||
$this->db->query(
|
||||
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` IN ($quoted_uids)",
|
||||
$this->folder_id
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current folder index (uid -> etag)
|
||||
*/
|
||||
protected function current_index()
|
||||
{
|
||||
// read cache index
|
||||
$sql_result = $this->db->query(
|
||||
"SELECT `uid`, `data` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
|
||||
$this->folder_id
|
||||
);
|
||||
|
||||
$index = [];
|
||||
|
||||
// TODO: Store etag as a separate column
|
||||
|
||||
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
||||
if ($object = json_decode($sql_arr['data'], true)) {
|
||||
$index[$sql_arr['uid']] = $object['etag'];
|
||||
}
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single entry from cache or from server directly
|
||||
*
|
||||
* @param string Object UID
|
||||
* @param string Object type to read
|
||||
*/
|
||||
public function get($uid, $type = null)
|
||||
{
|
||||
if ($this->ready) {
|
||||
$this->_read_folder_data();
|
||||
|
||||
$sql_result = $this->db->query(
|
||||
"SELECT * FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
|
||||
$this->folder_id,
|
||||
$uid
|
||||
);
|
||||
|
||||
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
||||
$object = $this->_unserialize($sql_arr);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch from DAV if not present in cache
|
||||
if (empty($object)) {
|
||||
if ($object = $this->folder->read_object($uid, $type ?: '*')) {
|
||||
$this->save($object);
|
||||
}
|
||||
}
|
||||
|
||||
return $object ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert/Update a cache entry
|
||||
*
|
||||
* @param string Object UID
|
||||
* @param array|false Hash array with object properties to save or false to delete the cache entry
|
||||
*/
|
||||
public function set($uid, $object)
|
||||
{
|
||||
// remove old entry
|
||||
if ($this->ready) {
|
||||
$this->_read_folder_data();
|
||||
|
||||
$this->db->query(
|
||||
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
|
||||
$this->folder_id,
|
||||
$uid
|
||||
);
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$this->save($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert (or update) a cache entry
|
||||
*
|
||||
* @param mixed Hash array with object properties to save or false to delete the cache entry
|
||||
* @param string Optional old message UID (for update)
|
||||
*/
|
||||
public function save($object, $olduid = null)
|
||||
{
|
||||
// write to cache
|
||||
if ($this->ready) {
|
||||
$this->_read_folder_data();
|
||||
|
||||
$sql_data = $this->_serialize($object);
|
||||
$sql_data['folder_id'] = $this->folder_id;
|
||||
$sql_data['uid'] = $object['uid'];
|
||||
|
||||
$args = [];
|
||||
$cols = ['folder_id', 'uid', 'changed', 'data', 'tags', 'words'];
|
||||
$cols = array_merge($cols, $this->extra_cols);
|
||||
|
||||
foreach ($cols as $idx => $col) {
|
||||
$cols[$idx] = $this->db->quote_identifier($col);
|
||||
$args[] = $sql_data[$col];
|
||||
}
|
||||
|
||||
if ($olduid) {
|
||||
foreach ($cols as $idx => $col) {
|
||||
$cols[$idx] = "$col = ?";
|
||||
}
|
||||
|
||||
$query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
|
||||
. " WHERE `folder_id` = ? AND `uid` = ?";
|
||||
$args[] = $this->folder_id;
|
||||
$args[] = $olduid;
|
||||
}
|
||||
else {
|
||||
$query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
|
||||
. ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
|
||||
}
|
||||
|
||||
$result = $this->db->query($query, $args);
|
||||
|
||||
if (!$this->db->affected_rows($result)) {
|
||||
rcube::raise_error([
|
||||
'code' => 900,
|
||||
'message' => "Failed to write to kolab cache"
|
||||
], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an existing cache entry to a new resource
|
||||
*
|
||||
* @param string Entry's UID
|
||||
* @param kolab_storage_folder Target storage folder instance
|
||||
*/
|
||||
public function move($uid, $target)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Update resource URI for existing folder
|
||||
*
|
||||
* @param string Target DAV folder to move it to
|
||||
*/
|
||||
public function rename($new_folder)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Select Kolab objects filtered by the given query
|
||||
*
|
||||
* @param array Pseudo-SQL query as list of filter parameter triplets
|
||||
* triplet: ['<colname>', '<comparator>', '<value>']
|
||||
* @param bool Set true to only return UIDs instead of complete objects
|
||||
* @param bool Use fast mode to fetch only minimal set of information
|
||||
* (no xml fetching and parsing, etc.)
|
||||
*
|
||||
* @return array|null|kolab_storage_dataset List of Kolab data objects (each represented as hash array) or UIDs
|
||||
*/
|
||||
public function select($query = [], $uids = false, $fast = false)
|
||||
{
|
||||
$result = $uids ? [] : new kolab_storage_dataset($this);
|
||||
|
||||
$this->_read_folder_data();
|
||||
|
||||
// fetch full object data on one query if a small result set is expected
|
||||
$fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS;
|
||||
|
||||
// skip SELECT if we know it will return nothing
|
||||
if ($count === 0) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$sql_query = "SELECT " . ($fetchall ? '*' : "`uid`")
|
||||
. " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
|
||||
. $this->_sql_where($query)
|
||||
. (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
|
||||
|
||||
$sql_result = $this->limit ?
|
||||
$this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
|
||||
$this->db->query($sql_query, $this->folder_id);
|
||||
|
||||
if ($this->db->is_error($sql_result)) {
|
||||
if ($uids) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result->set_error(true);
|
||||
return $result;
|
||||
}
|
||||
|
||||
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
||||
if ($fast) {
|
||||
$sql_arr['fast-mode'] = true;
|
||||
}
|
||||
if ($uids) {
|
||||
$result[] = $sql_arr['uid'];
|
||||
}
|
||||
else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
|
||||
$result[] = $object;
|
||||
}
|
||||
else if (!$fetchall) {
|
||||
$result[] = $sql_arr;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of objects mathing the given query
|
||||
*
|
||||
* @param array $query Pseudo-SQL query as list of filter parameter triplets
|
||||
*
|
||||
* @return int The number of objects of the given type
|
||||
*/
|
||||
public function count($query = [])
|
||||
{
|
||||
// read from local cache DB (assume it to be synchronized)
|
||||
$this->_read_folder_data();
|
||||
|
||||
$sql_result = $this->db->query(
|
||||
"SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
|
||||
"WHERE `folder_id` = ?" . $this->_sql_where($query),
|
||||
$this->folder_id
|
||||
);
|
||||
|
||||
if ($this->db->is_error($sql_result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql_arr = $this->db->fetch_assoc($sql_result);
|
||||
$count = intval($sql_arr['numrows']);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for a single Kolab object identified by its UID
|
||||
*
|
||||
* @param string $uid Object UID
|
||||
*
|
||||
* @return array|null The Kolab object represented as hash array
|
||||
*/
|
||||
public function get_by_uid($uid)
|
||||
{
|
||||
$old_limit = $this->limit;
|
||||
|
||||
// set limit to skip count query
|
||||
$this->limit = [1, 0];
|
||||
|
||||
$list = $this->select([['uid', '=', $uid]]);
|
||||
|
||||
// set the limit back to defined value
|
||||
$this->limit = $old_limit;
|
||||
|
||||
if (!empty($list) && !empty($list[0])) {
|
||||
return $list[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check DAV connection error state
|
||||
*/
|
||||
protected function check_error()
|
||||
{
|
||||
// TODO ?
|
||||
}
|
||||
|
||||
/**
|
||||
* Write records into cache using extended inserts to reduce the number of queries to be executed
|
||||
*
|
||||
* @param bool Set to false to commit buffered insert, true to force an insert
|
||||
* @param array Kolab object to cache
|
||||
*/
|
||||
protected function _extended_insert($force, $object)
|
||||
{
|
||||
static $buffer = '';
|
||||
|
||||
$line = '';
|
||||
$cols = ['folder_id', 'uid', 'created', 'changed', 'data', 'tags', 'words'];
|
||||
if ($this->extra_cols) {
|
||||
$cols = array_merge($cols, $this->extra_cols);
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$sql_data = $this->_serialize($object);
|
||||
|
||||
// Skip multi-folder insert for all databases but MySQL
|
||||
// In Oracle we can't put long data inline, others we don't support yet
|
||||
if (strpos($this->db->db_provider, 'mysql') !== 0) {
|
||||
$extra_args = [];
|
||||
$params = [$this->folder_id, $object['uid'], $sql_data['changed'],
|
||||
$sql_data['data'], $sql_data['tags'], $sql_data['words']];
|
||||
|
||||
foreach ($this->extra_cols as $col) {
|
||||
$params[] = $sql_data[$col];
|
||||
$extra_args[] = '?';
|
||||
}
|
||||
|
||||
$cols = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
|
||||
$extra_args = count($extra_args) ? ', ' . implode(', ', $extra_args) : '';
|
||||
|
||||
$result = $this->db->query(
|
||||
"INSERT INTO `{$this->cache_table}` ($cols)"
|
||||
. " VALUES (?, ?, " . $this->db->now() . ", ?, ?, ?, ?$extra_args)",
|
||||
$params
|
||||
);
|
||||
|
||||
if (!$this->db->affected_rows($result)) {
|
||||
rcube::raise_error(array(
|
||||
'code' => 900, 'message' => "Failed to write to kolab cache"
|
||||
), true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$values = array(
|
||||
$this->db->quote($this->folder_id),
|
||||
$this->db->quote($object['uid']),
|
||||
$this->db->now(),
|
||||
$this->db->quote($sql_data['changed']),
|
||||
$this->db->quote($sql_data['data']),
|
||||
$this->db->quote($sql_data['tags']),
|
||||
$this->db->quote($sql_data['words']),
|
||||
);
|
||||
foreach ($this->extra_cols as $col) {
|
||||
$values[] = $this->db->quote($sql_data[$col]);
|
||||
}
|
||||
$line = '(' . join(',', $values) . ')';
|
||||
}
|
||||
|
||||
if ($buffer && ($force || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
|
||||
$columns = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
|
||||
$update = implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
|
||||
|
||||
$result = $this->db->query(
|
||||
"INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer"
|
||||
. " ON DUPLICATE KEY UPDATE $update"
|
||||
);
|
||||
|
||||
if (!$this->db->affected_rows($result)) {
|
||||
rcube::raise_error(array(
|
||||
'code' => 900, 'message' => "Failed to write to kolab cache"
|
||||
), true);
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
$buffer .= ($buffer ? ',' : '') . $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to turn stored cache data into a valid storage object
|
||||
*/
|
||||
protected function _unserialize($sql_arr)
|
||||
{
|
||||
if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
|
||||
$object['uid'] = $sql_arr['uid'];
|
||||
|
||||
foreach ($this->data_props as $prop) {
|
||||
if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') {
|
||||
$object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
|
||||
}
|
||||
else if (!isset($object[$prop]) && isset($sql_arr[$prop])) {
|
||||
$object[$prop] = $sql_arr[$prop];
|
||||
}
|
||||
}
|
||||
|
||||
if ($sql_arr['created'] && empty($object['created'])) {
|
||||
$object['created'] = new DateTime($sql_arr['created']);
|
||||
}
|
||||
|
||||
if ($sql_arr['changed'] && empty($object['changed'])) {
|
||||
$object['changed'] = new DateTime($sql_arr['changed']);
|
||||
}
|
||||
|
||||
$object['_type'] = $sql_arr['type'] ?: $this->folder->type;
|
||||
}
|
||||
// Fetch a complete object from the server
|
||||
else {
|
||||
// TODO: Fetching objects one-by-one from DAV server is slow
|
||||
$object = $this->folder->read_object($sql_arr['uid'], '*');
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
}
|
68
plugins/libkolab/lib/kolab_storage_dav_cache_event.php
Normal file
68
plugins/libkolab/lib/kolab_storage_dav_cache_event.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Kolab storage cache class for calendar event objects
|
||||
*
|
||||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class kolab_storage_dav_cache_event extends kolab_storage_dav_cache
|
||||
{
|
||||
protected $extra_cols = array('dtstart','dtend');
|
||||
protected $data_props = array('categories', 'status', 'attendees', 'etag');
|
||||
|
||||
/**
|
||||
* Helper method to convert the given Kolab object into a dataset to be written to cache
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
protected function _serialize($object)
|
||||
{
|
||||
$sql_data = parent::_serialize($object);
|
||||
|
||||
$sql_data['dtstart'] = $this->_convert_datetime($object['start']);
|
||||
$sql_data['dtend'] = $this->_convert_datetime($object['end']);
|
||||
|
||||
// extend date range for recurring events
|
||||
if (!empty($object['recurrence']) && !empty($object['_formatobj'])) {
|
||||
$recurrence = new kolab_date_recurrence($object['_formatobj']);
|
||||
$dtend = $recurrence->end() ?: new DateTime('now +100 years');
|
||||
$sql_data['dtend'] = $this->_convert_datetime($dtend);
|
||||
}
|
||||
|
||||
// extend start/end dates to spawn all exceptions
|
||||
if (is_array($object['exceptions'])) {
|
||||
foreach ($object['exceptions'] as $exception) {
|
||||
if (is_a($exception['start'], 'DateTime')) {
|
||||
$exstart = $this->_convert_datetime($exception['start']);
|
||||
if ($exstart < $sql_data['dtstart']) {
|
||||
$sql_data['dtstart'] = $exstart;
|
||||
}
|
||||
}
|
||||
if (is_a($exception['end'], 'DateTime')) {
|
||||
$exend = $this->_convert_datetime($exception['end']);
|
||||
if ($exend > $sql_data['dtend']) {
|
||||
$sql_data['dtend'] = $exend;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sql_data;
|
||||
}
|
||||
}
|
529
plugins/libkolab/lib/kolab_storage_dav_folder.php
Normal file
529
plugins/libkolab/lib/kolab_storage_dav_folder.php
Normal file
|
@ -0,0 +1,529 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A class representing a DAV folder object.
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2014-2022, Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* 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_dav_folder extends kolab_storage_folder
|
||||
{
|
||||
public $dav;
|
||||
public $href;
|
||||
public $attributes;
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*/
|
||||
public function __construct($dav, $attributes, $type_annotation = '')
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
$this->href = $this->attributes['href'];
|
||||
|
||||
// Here we assume the last element of the folder path is the folder ID
|
||||
// if that's not the case, we should consider generating an ID
|
||||
$href = explode('/', unslashify($this->href));
|
||||
$this->id = $href[count($href) - 1];
|
||||
$this->dav = $dav;
|
||||
$this->valid = true;
|
||||
|
||||
list($this->type, $suffix) = explode('.', $type_annotation);
|
||||
$this->default = $suffix == 'default';
|
||||
$this->subtype = $this->default ? '' : $suffix;
|
||||
|
||||
// Init cache
|
||||
$this->cache = kolab_storage_dav_cache::factory($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner of the folder.
|
||||
*
|
||||
* @param bool Return a fully qualified owner name (i.e. including domain for shared folders)
|
||||
*
|
||||
* @return string The owner of this folder.
|
||||
*/
|
||||
public function get_owner($fully_qualified = false)
|
||||
{
|
||||
// return cached value
|
||||
if (isset($this->owner)) {
|
||||
return $this->owner;
|
||||
}
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
$this->owner = $rcube->get_user_name();
|
||||
$this->valid = true;
|
||||
|
||||
// TODO: Support shared folders
|
||||
|
||||
return $this->owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a folder Etag identifier
|
||||
*/
|
||||
public function get_ctag()
|
||||
{
|
||||
return $this->attributes['ctag'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the name of the namespace to which the folder belongs
|
||||
*
|
||||
* @return string Name of the namespace (personal, other, shared)
|
||||
*/
|
||||
public function get_namespace()
|
||||
{
|
||||
// TODO: Support shared folders
|
||||
return 'personal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name value of this folder
|
||||
*
|
||||
* @return string Folder name
|
||||
*/
|
||||
public function get_name()
|
||||
{
|
||||
return kolab_storage_dav::object_name($this->attributes['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the top-end folder name (not the entire path)
|
||||
*
|
||||
* @return string Name of this folder
|
||||
*/
|
||||
public function get_foldername()
|
||||
{
|
||||
return $this->attributes['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for parent folder path
|
||||
*
|
||||
* @return string Full path to parent folder
|
||||
*/
|
||||
public function get_parent()
|
||||
{
|
||||
// TODO
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a unique resource URI for this folder
|
||||
*/
|
||||
public function get_resource_uri()
|
||||
{
|
||||
if (!empty($this->resource_uri)) {
|
||||
return $this->resource_uri;
|
||||
}
|
||||
|
||||
// compose fully qualified ressource uri for this instance
|
||||
$host = preg_replace('|^https?://|', 'dav://' . urlencode($this->get_owner(true)) . '@', $this->dav->url);
|
||||
$path = $this->href[0] == '/' ? $this->href : "/{$this->href}";
|
||||
|
||||
$this->resource_uri = unslashify($host) . $path;
|
||||
|
||||
return $this->resource_uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the Cyrus mailbox identifier corresponding to this folder
|
||||
* (e.g. user/john.doe/Calendar/Personal@example.org)
|
||||
*
|
||||
* @return string Mailbox ID
|
||||
*/
|
||||
public function get_mailbox_id()
|
||||
{
|
||||
// TODO: This is used with Bonnie related features
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color value stored in metadata
|
||||
*
|
||||
* @param string Default color value to return if not set
|
||||
*
|
||||
* @return mixed Color value from the folder metadata or $default if not set
|
||||
*/
|
||||
public function get_color($default = null)
|
||||
{
|
||||
return !empty($this->attributes['color']) ? $this->attributes['color'] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ACL information for this folder
|
||||
*
|
||||
* @return string Permissions as string
|
||||
*/
|
||||
public function get_myrights()
|
||||
{
|
||||
// TODO
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract folder UID
|
||||
*
|
||||
* @return string Folder's UID
|
||||
*/
|
||||
public function get_uid()
|
||||
{
|
||||
// TODO ???
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check activation status of this folder
|
||||
*
|
||||
* @return bool True if enabled, false if not
|
||||
*/
|
||||
public function is_active()
|
||||
{
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change activation status of this folder
|
||||
*
|
||||
* @param bool The desired subscription status: true = active, false = not active
|
||||
*
|
||||
* @return bool True on success, false on error
|
||||
*/
|
||||
public function activate($active)
|
||||
{
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check subscription status of this folder
|
||||
*
|
||||
* @return bool True if subscribed, false if not
|
||||
*/
|
||||
public function is_subscribed()
|
||||
{
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change subscription status of this folder
|
||||
*
|
||||
* @param bool The desired subscription status: true = subscribed, false = not subscribed
|
||||
*
|
||||
* @return True on success, false on error
|
||||
*/
|
||||
public function subscribe($subscribed)
|
||||
{
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specified object from this folder.
|
||||
*
|
||||
* @param array|string $object The Kolab object to delete or object UID
|
||||
* @param bool $expunge Should the folder be expunged?
|
||||
*
|
||||
* @return bool True if successful, false on error
|
||||
*/
|
||||
public function delete($object, $expunge = true)
|
||||
{
|
||||
if (!$this->valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uid = is_array($object) ? $object['uid'] : $object;
|
||||
|
||||
$success = $this->dav->delete($this->object_location($uid), $content);
|
||||
|
||||
if ($success) {
|
||||
$this->cache->set($uid, false);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function delete_all()
|
||||
{
|
||||
if (!$this->valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: This method is used by kolab_addressbook plugin only
|
||||
|
||||
$this->cache->purge();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a previously deleted object
|
||||
*
|
||||
* @param string $uid Object UID
|
||||
*
|
||||
* @return mixed Message UID on success, false on error
|
||||
*/
|
||||
public function undelete($uid)
|
||||
{
|
||||
if (!$this->valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a Kolab object message to another IMAP folder
|
||||
*
|
||||
* @param string Object UID
|
||||
* @param string IMAP folder to move object to
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function move($uid, $target_folder)
|
||||
{
|
||||
if (!$this->valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an object in this folder.
|
||||
*
|
||||
* @param array $object The array that holds the data of the object.
|
||||
* @param string $type The type of the kolab object.
|
||||
* @param string $uid The UID of the old object if it existed before
|
||||
*
|
||||
* @return mixed False on error or object UID on success
|
||||
*/
|
||||
public function save(&$object, $type = null, $uid = null)
|
||||
{
|
||||
if (!$this->valid || empty($object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
$type = $this->type;
|
||||
}
|
||||
/*
|
||||
// copy attachments from old message
|
||||
$copyfrom = $object['_copyfrom'] ?: $object['_msguid'];
|
||||
if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
|
||||
foreach ((array)$old['_attachments'] as $key => $att) {
|
||||
if (!isset($object['_attachments'][$key])) {
|
||||
$object['_attachments'][$key] = $old['_attachments'][$key];
|
||||
}
|
||||
// unset deleted attachment entries
|
||||
if ($object['_attachments'][$key] == false) {
|
||||
unset($object['_attachments'][$key]);
|
||||
}
|
||||
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
|
||||
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
|
||||
if (!isset($object['photo']))
|
||||
$object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
|
||||
unset($object['_attachments'][$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process attachments
|
||||
if (is_array($object['_attachments'])) {
|
||||
$numatt = count($object['_attachments']);
|
||||
foreach ($object['_attachments'] as $key => $attachment) {
|
||||
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
|
||||
if (empty($attachment['content']) && !empty($attachment['data'])) {
|
||||
$attachment['content'] = $attachment['data'];
|
||||
unset($attachment['data'], $object['_attachments'][$key]['data']);
|
||||
}
|
||||
|
||||
// make sure size is set, so object saved in cache contains this info
|
||||
if (!isset($attachment['size'])) {
|
||||
if (!empty($attachment['content'])) {
|
||||
if (is_resource($attachment['content'])) {
|
||||
// this need to be a seekable resource, otherwise
|
||||
// fstat() failes and we're unable to determine size
|
||||
// here nor in rcube_imap_generic before IMAP APPEND
|
||||
$stat = fstat($attachment['content']);
|
||||
$attachment['size'] = $stat ? $stat['size'] : 0;
|
||||
}
|
||||
else {
|
||||
$attachment['size'] = strlen($attachment['content']);
|
||||
}
|
||||
}
|
||||
else if (!empty($attachment['path'])) {
|
||||
$attachment['size'] = filesize($attachment['path']);
|
||||
}
|
||||
$object['_attachments'][$key] = $attachment;
|
||||
}
|
||||
|
||||
// generate unique keys (used as content-id) for attachments
|
||||
if (is_numeric($key) && $key < $numatt) {
|
||||
// derrive content-id from attachment file name
|
||||
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
|
||||
$basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii
|
||||
if (!$basename) $basename = 'noname';
|
||||
$cid = $basename . '.' . microtime(true) . $key . $ext;
|
||||
|
||||
$object['_attachments'][$cid] = $attachment;
|
||||
unset($object['_attachments'][$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
$rcmail = rcube::get_instance();
|
||||
$result = false;
|
||||
|
||||
// generate and save object message
|
||||
if ($content = $this->to_dav($object)) {
|
||||
$result = $this->dav->create($this->object_location($object['uid']), $content);
|
||||
|
||||
if ($result !== false) {
|
||||
// insert/update object in the cache
|
||||
$object['etag'] = $result;
|
||||
$this->cache->save($object, $uid);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the object the DAV server and convert to internal format
|
||||
*
|
||||
* @param string The object UID to fetch
|
||||
* @param string The object type expected (use wildcard '*' to accept all types)
|
||||
*
|
||||
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
|
||||
*/
|
||||
public function read_object($uid, $type = null)
|
||||
{
|
||||
if (!$this->valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$href = $this->object_location($uid);
|
||||
$objects = $this->dav->getData($this->href, [$href]);
|
||||
|
||||
if (!is_array($objects) || count($objects) != 1) {
|
||||
rcube::raise_error([
|
||||
'code' => 900,
|
||||
'message' => "Failed to fetch {$href}"
|
||||
], true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->from_dav($objects[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DAV object into PHP array
|
||||
*
|
||||
* @param array Object data in kolab_dav_client::fetchData() format
|
||||
*
|
||||
* @return array Object properties
|
||||
*/
|
||||
public function from_dav($object)
|
||||
{
|
||||
if ($this->type == 'event') {
|
||||
$ical = libcalendaring::get_ical();
|
||||
$events = $ical->import($object['data']);
|
||||
|
||||
if (!count($events) || empty($events[0]['uid'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $events[0];
|
||||
}
|
||||
|
||||
$result['etag'] = $object['etag'];
|
||||
$result['href'] = $object['href'];
|
||||
$result['uid'] = $object['uid'] ?: $result['uid'];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Kolab object into DAV format (iCalendar)
|
||||
*/
|
||||
public function to_dav($object)
|
||||
{
|
||||
$result = '';
|
||||
|
||||
if ($this->type == 'event') {
|
||||
$ical = libcalendaring::get_ical();
|
||||
// TODO: Attachments?
|
||||
$result = $ical->export([$object]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function object_location($uid)
|
||||
{
|
||||
return unslashify($this->href) . '/' . urlencode($uid) . '.' . $this->get_dav_ext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a folder DAV content type
|
||||
*/
|
||||
public function get_dav_type()
|
||||
{
|
||||
$types = [
|
||||
'event' => 'VEVENT',
|
||||
'task' => 'VTODO',
|
||||
'contact' => 'VCARD',
|
||||
];
|
||||
|
||||
return $types[$this->type];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a DAV file extension for specified Kolab type
|
||||
*/
|
||||
public function get_dav_ext()
|
||||
{
|
||||
$types = [
|
||||
'event' => 'ics',
|
||||
'task' => 'ics',
|
||||
'contact' => 'vcf',
|
||||
];
|
||||
|
||||
return $types[$this->type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return folder name as string representation of this object
|
||||
*
|
||||
* @return string Full IMAP folder name
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->attributes['name'];
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
|
|||
*
|
||||
* @param string The folder name/path
|
||||
* @param string Expected folder type
|
||||
* @param string Optional folder type if known
|
||||
*/
|
||||
function __construct($name, $type = null, $type_annotation = null)
|
||||
{
|
||||
|
@ -596,7 +597,7 @@ class kolab_storage_folder extends kolab_storage_folder_api
|
|||
*/
|
||||
public function save(&$object, $type = null, $uid = null)
|
||||
{
|
||||
if (!$this->valid && empty($object)) {
|
||||
if (!$this->valid || empty($object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue