CalDAV driver

This commit is contained in:
Aleksander Machniak 2022-10-11 15:27:59 +02:00
parent af5461eb76
commit 5c6a7a2d6f
16 changed files with 377 additions and 52 deletions

View file

@ -3658,9 +3658,10 @@ function rcube_calendar_ui(settings)
}); });
// register dbl-click handler to open calendar edit dialog // register dbl-click handler to open calendar edit dialog
$(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){ $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e) {
var id = $(this).closest('li').attr('id').replace(/^rcmlical/, ''); var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
me.calendar_edit_dialog(me.calendars[id]); if (me.calendars[id] && me.calendars[id].driver != 'caldav')
me.calendar_edit_dialog(me.calendars[id]);
}); });
// Make Elastic checkboxes pretty // Make Elastic checkboxes pretty

View file

@ -25,7 +25,7 @@
+-------------------------------------------------------------------------+ +-------------------------------------------------------------------------+
*/ */
// backend type (database, kolab) // backend type (database, kolab, caldav)
$config['calendar_driver'] = "database"; $config['calendar_driver'] = "database";
// default calendar view (agendaDay, agendaWeek, month) // default calendar view (agendaDay, agendaWeek, month)

View file

@ -158,7 +158,7 @@ class caldav_calendar extends kolab_storage_dav_folder
// directly access storage object // directly access storage object
if (empty($this->events[$id]) && $master_id == $id && ($record = $this->storage->get_object($id))) { if (empty($this->events[$id]) && $master_id == $id && ($record = $this->storage->get_object($id))) {
$this->events[$id] = $this->_to_driver_event($record, true); $this->events[$id] = $record = $this->_to_driver_event($record, true);
} }
// maybe a recurring instance is requested // maybe a recurring instance is requested
@ -166,10 +166,10 @@ class caldav_calendar extends kolab_storage_dav_folder
$instance_id = substr($id, strlen($master_id) + 1); $instance_id = substr($id, strlen($master_id) + 1);
if ($record = $this->storage->get_object($master_id)) { if ($record = $this->storage->get_object($master_id)) {
$master = $this->_to_driver_event($record); $master = $record = $this->_to_driver_event($record);
} }
if ($master) { if (!empty($master)) {
// check for match in top-level exceptions (aka loose single occurrences) // check for match in top-level exceptions (aka loose single occurrences)
if (!empty($master['_formatobj']) && ($instance = $master['_formatobj']->get_instance($instance_id))) { if (!empty($master['_formatobj']) && ($instance = $master['_formatobj']->get_instance($instance_id))) {
$this->events[$id] = $this->_to_driver_event($instance, false, true, $master); $this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
@ -695,7 +695,7 @@ class caldav_calendar extends kolab_storage_dav_folder
if ($exception) { if ($exception) {
// copy data from exception // copy data from exception
colab_driver::merge_exception_data($rec_event, $exception); caldav_driver::merge_exception_data($rec_event, $exception);
} }
$rec_event['id'] = $rec_id; $rec_event['id'] = $rec_id;
@ -776,7 +776,7 @@ class caldav_calendar extends kolab_storage_dav_folder
// TODO: Drop dependency on libkolabxml? // TODO: Drop dependency on libkolabxml?
$event_xml = new kolab_format_event(); $event_xml = new kolab_format_event();
$event_xml->set($record); $event_xml->set($record);
$event['_formatobj'] = $event_xml; $record['_formatobj'] = $event_xml;
return $record; return $record;
} }

View file

@ -184,6 +184,9 @@ class caldav_driver extends kolab_driver
'active' => $cal->is_active(), 'active' => $cal->is_active(),
'owner' => $cal->get_owner(), 'owner' => $cal->get_owner(),
'removable' => !$cal->default, 'removable' => !$cal->default,
// extras to hide some elements in the UI
'subscriptions' => false,
'driver' => 'caldav',
]; ];
if (!$is_user) { if (!$is_user) {
@ -274,7 +277,7 @@ class caldav_driver extends kolab_driver
{ {
$this->_read_calendars(); $this->_read_calendars();
// create calendar object if necesary // create calendar object if necessary
if (empty($this->calendars[$id])) { if (empty($this->calendars[$id])) {
if (in_array($id, [self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED])) { if (in_array($id, [self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED])) {
return new caldav_invitation_calendar($id, $this->cal); return new caldav_invitation_calendar($id, $this->cal);

View file

@ -381,15 +381,17 @@ class calendar_ui
); );
} }
$content .= html::tag('input', [ if (!isset($prop['subscriptions']) || $prop['subscriptions'] !== false) {
'type' => 'checkbox', $content .= html::tag('input', [
'name' => '_cal[]', 'type' => 'checkbox',
'value' => $id, 'name' => '_cal[]',
'checked' => !empty($prop['active']), 'value' => $id,
'aria-labelledby' => $label_id 'checked' => !empty($prop['active']),
]) 'aria-labelledby' => $label_id
. html::span('actions', $actions) ])
. html::span(['class' => 'handle', 'style' => "background-color: #$color"], ' '); . html::span('actions', $actions)
. html::span(['class' => 'handle', 'style' => "background-color: #$color"], ' ');
}
} }
$content = html::div(join(' ', $classes), $content); $content = html::div(join(' ', $classes), $content);

View file

@ -141,9 +141,11 @@
<div id="calendaractions-menu" class="popupmenu"> <div id="calendaractions-menu" class="popupmenu">
<h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3> <h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
<ul class="menu listing" role="menu" aria-labelledby="aria-label-calendaroptions"> <ul class="menu listing" role="menu" aria-labelledby="aria-label-calendaroptions">
<roundcube:if condition="env:calendar_driver != 'caldav'" />
<roundcube:button type="link-menuitem" command="calendar-create" label="calendar.addcalendar" class="create disabled" classAct="create active" /> <roundcube:button type="link-menuitem" command="calendar-create" label="calendar.addcalendar" class="create disabled" classAct="create active" />
<roundcube:button type="link-menuitem" command="calendar-edit" label="calendar.editcalendar" class="edit disabled" classAct="edit active" /> <roundcube:button type="link-menuitem" command="calendar-edit" label="calendar.editcalendar" class="edit disabled" classAct="edit active" />
<roundcube:button type="link-menuitem" command="calendar-delete" label="calendar.deletecalendar" class="delete disabled" classAct="delete active" /> <roundcube:button type="link-menuitem" command="calendar-delete" label="calendar.deletecalendar" class="delete disabled" classAct="delete active" />
<roundcube:endif />
<roundcube:if condition="env:calendar_driver == 'kolab'" /> <roundcube:if condition="env:calendar_driver == 'kolab'" />
<roundcube:button type="link-menuitem" command="calendar-remove" label="calendar.removelist" class="remove disabled" classAct="remove active" /> <roundcube:button type="link-menuitem" command="calendar-remove" label="calendar.removelist" class="remove disabled" classAct="remove active" />
<roundcube:endif /> <roundcube:endif />

View file

@ -48,13 +48,14 @@ class kolab_addressbook extends rcube_plugin
*/ */
public function init() public function init()
{ {
require_once(dirname(__FILE__) . '/lib/rcube_kolab_contacts.php');
$this->rc = rcube::get_instance(); $this->rc = rcube::get_instance();
// load required plugin // load required plugin
$this->require_plugin('libkolab'); $this->require_plugin('libkolab');
$driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
require_once(dirname(__FILE__) . '/lib/rcube_' . $driver . '_contacts.php');
// register hooks // register hooks
$this->add_hook('addressbooks_list', array($this, 'address_sources')); $this->add_hook('addressbooks_list', array($this, 'address_sources'));
$this->add_hook('addressbook_get', array($this, 'get_address_book')); $this->add_hook('addressbook_get', array($this, 'get_address_book'));

View file

@ -177,11 +177,34 @@ CREATE TABLE `kolab_cache_freebusy` (
INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`) INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_dav_contact`;
CREATE TABLE `kolab_cache_dav_contact` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`etag` VARCHAR(128) DEFAULT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
`name` VARCHAR(255) NOT NULL,
`firstname` VARCHAR(255) NOT NULL,
`surname` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
CONSTRAINT `fk_kolab_cache_dav_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`uid`),
INDEX `contact_type` (`folder_id`,`type`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_dav_event`; DROP TABLE IF EXISTS `kolab_cache_dav_event`;
CREATE TABLE `kolab_cache_dav_event` ( CREATE TABLE `kolab_cache_dav_event` (
`folder_id` BIGINT UNSIGNED NOT NULL, `folder_id` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL, `uid` VARCHAR(512) NOT NULL,
`etag` VARCHAR(128) DEFAULT NULL,
`created` DATETIME DEFAULT NULL, `created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL, `data` LONGTEXT NOT NULL,

View file

@ -0,0 +1,39 @@
DROP TABLE IF EXISTS `kolab_cache_dav_contact`;
CREATE TABLE `kolab_cache_dav_contact` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`etag` VARCHAR(128) DEFAULT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
`name` VARCHAR(255) NOT NULL,
`firstname` VARCHAR(255) NOT NULL,
`surname` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
CONSTRAINT `fk_kolab_cache_dav_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`uid`),
INDEX `contact_type` (`folder_id`,`type`)
) 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,
`etag` VARCHAR(128) DEFAULT 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;

View file

@ -176,7 +176,7 @@ class kolab_dav_client
} }
/** /**
* Create DAV object in a folder * Create a DAV object in a folder
*/ */
public function create($location, $content) public function create($location, $content)
{ {
@ -196,7 +196,15 @@ class kolab_dav_client
} }
/** /**
* Delete DAV object from a folder * Update a DAV object in a folder
*/
public function update($location, $content)
{
return $this->create($location, $content);
}
/**
* Delete a DAV object from a folder
*/ */
public function delete($location) public function delete($location)
{ {
@ -210,17 +218,32 @@ class kolab_dav_client
*/ */
public function getIndex($location, $component = 'VEVENT') public function getIndex($location, $component = 'VEVENT')
{ {
$queries = [
'VEVENT' => 'calendar-query',
'VTODO' => 'calendar-query',
'VCARD' => 'addressbook-query',
];
$ns = [
'VEVENT' => 'caldav',
'VTODO' => 'caldav',
'VCARD' => 'carddav',
];
$filter = '';
if ($component != 'VCARD') {
$filter = '<c:comp-filter name="VCALENDAR">'
. '<c:comp-filter name="' . $component . '" />'
. '</c:comp-filter>';
}
$body = '<?xml version="1.0" encoding="utf-8"?>' $body = '<?xml version="1.0" encoding="utf-8"?>'
.' <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">' .' <c:' . $queries[$component] . ' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component]. '">'
. '<d:prop>' . '<d:prop>'
. '<d:getetag />' . '<d:getetag />'
. '</d:prop>' . '</d:prop>'
. '<c:filter>' . ($filter ? "<c:filter>$filter</c:filter>" : '')
. '<c:comp-filter name="VCALENDAR">' . '</c:' . $queries[$component] . '>';
. '<c:comp-filter name="' . $component . '" />'
. '</c:comp-filter>'
. '</c:filter>'
. '</c:calendar-query>';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']); $response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
@ -240,7 +263,7 @@ class kolab_dav_client
/** /**
* Fetch DAV objects data from a folder * Fetch DAV objects data from a folder
*/ */
public function getData($location, $hrefs = []) public function getData($location, $component = 'VEVENT', $hrefs = [])
{ {
if (empty($hrefs)) { if (empty($hrefs)) {
return []; return [];
@ -251,14 +274,32 @@ class kolab_dav_client
$body .= '<d:href>' . $href . '</d:href>'; $body .= '<d:href>' . $href . '</d:href>';
} }
$queries = [
'VEVENT' => 'calendar-multiget',
'VTODO' => 'calendar-multiget',
'VCARD' => 'addressbook-multiget',
];
$ns = [
'VEVENT' => 'caldav',
'VTODO' => 'caldav',
'VCARD' => 'carddav',
];
$types = [
'VEVENT' => 'calendar-data',
'VTODO' => 'calendar-data',
'VCARD' => 'address-data',
];
$body = '<?xml version="1.0" encoding="utf-8"?>' $body = '<?xml version="1.0" encoding="utf-8"?>'
.' <c:calendar-multiget xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">' .' <c:' . $queries[$component] . ' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component] . '">'
. '<d:prop>' . '<d:prop>'
. '<d:getetag />' . '<d:getetag />'
. '<c:calendar-data />' . '<c:' . $types[$component]. ' />'
. '</d:prop>' . '</d:prop>'
. $body . $body
. '</c:calendar-multiget>'; . '</c:' . $queries[$component] . '>';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']); $response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
@ -388,6 +429,9 @@ class kolab_dav_client
if ($data = $element->getElementsByTagName('calendar-data')->item(0)) { if ($data = $element->getElementsByTagName('calendar-data')->item(0)) {
$data = $data->nodeValue; $data = $data->nodeValue;
} }
else if ($data = $element->getElementsByTagName('address-data')->item(0)) {
$data = $data->nodeValue;
}
if ($etag = $element->getElementsByTagName('getetag')->item(0)) { if ($etag = $element->getElementsByTagName('getetag')->item(0)) {
$etag = $etag->nodeValue; $etag = $etag->nodeValue;

View file

@ -944,7 +944,7 @@ class kolab_storage
* Wrapper for rcube_imap::list_folders_subscribed() * Wrapper for rcube_imap::list_folders_subscribed()
* with support for temporarily subscribed folders * with support for temporarily subscribed folders
*/ */
protected static function _imap_list_subscribed($root, $mbox) protected static function _imap_list_subscribed($root, $mbox, $filter = null)
{ {
$folders = self::$imap->list_folders_subscribed($root, $mbox); $folders = self::$imap->list_folders_subscribed($root, $mbox);

View file

@ -61,8 +61,14 @@ class kolab_storage_dav
*/ */
public function get_folders($type) public function get_folders($type)
{ {
$davTypes = [
'event' => 'VEVENT',
'task' => 'VTODO',
'contact' => 'VCARD',
];
// TODO: This should be cached // TODO: This should be cached
$folders = $this->dav->discover(); $folders = $this->dav->discover($davTypes[$type]);
if (is_array($folders)) { if (is_array($folders)) {
foreach ($folders as $idx => $folder) { foreach ($folders as $idx => $folder) {
@ -88,7 +94,7 @@ class kolab_storage_dav
/** /**
* Getter for a specific storage folder * Getter for a specific storage folder
* *
* @param string Folder to access (UTF7-IMAP) * @param string Folder to access
* @param string Expected folder type * @param string Expected folder type
* *
* @return object kolab_storage_folder The folder object * @return object kolab_storage_folder The folder object

View file

@ -233,18 +233,14 @@ class kolab_storage_dav_cache extends kolab_storage_cache
{ {
// read cache index // read cache index
$sql_result = $this->db->query( $sql_result = $this->db->query(
"SELECT `uid`, `data` FROM `{$this->cache_table}` WHERE `folder_id` = ?", "SELECT `uid`, `etag` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id $this->folder_id
); );
$index = []; $index = [];
// TODO: Store etag as a separate column
while ($sql_arr = $this->db->fetch_assoc($sql_result)) { while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($object = json_decode($sql_arr['data'], true)) { $index[$sql_arr['uid']] = $sql_arr['etag'];
$index[$sql_arr['uid']] = $object['etag'];
}
} }
return $index; return $index;
@ -321,9 +317,10 @@ class kolab_storage_dav_cache extends kolab_storage_cache
$sql_data = $this->_serialize($object); $sql_data = $this->_serialize($object);
$sql_data['folder_id'] = $this->folder_id; $sql_data['folder_id'] = $this->folder_id;
$sql_data['uid'] = $object['uid']; $sql_data['uid'] = $object['uid'];
$sql_data['etag'] = $object['etag'];
$args = []; $args = [];
$cols = ['folder_id', 'uid', 'changed', 'data', 'tags', 'words']; $cols = ['folder_id', 'uid', 'etag', 'changed', 'data', 'tags', 'words'];
$cols = array_merge($cols, $this->extra_cols); $cols = array_merge($cols, $this->extra_cols);
foreach ($cols as $idx => $col) { foreach ($cols as $idx => $col) {
@ -510,7 +507,7 @@ class kolab_storage_dav_cache extends kolab_storage_cache
static $buffer = ''; static $buffer = '';
$line = ''; $line = '';
$cols = ['folder_id', 'uid', 'created', 'changed', 'data', 'tags', 'words']; $cols = ['folder_id', 'uid', 'etag', 'created', 'changed', 'data', 'tags', 'words'];
if ($this->extra_cols) { if ($this->extra_cols) {
$cols = array_merge($cols, $this->extra_cols); $cols = array_merge($cols, $this->extra_cols);
} }
@ -522,7 +519,7 @@ class kolab_storage_dav_cache extends kolab_storage_cache
// In Oracle we can't put long data inline, others we don't support yet // In Oracle we can't put long data inline, others we don't support yet
if (strpos($this->db->db_provider, 'mysql') !== 0) { if (strpos($this->db->db_provider, 'mysql') !== 0) {
$extra_args = []; $extra_args = [];
$params = [$this->folder_id, $object['uid'], $sql_data['changed'], $params = [$this->folder_id, $object['uid'], $object['etag'], $sql_data['changed'],
$sql_data['data'], $sql_data['tags'], $sql_data['words']]; $sql_data['data'], $sql_data['tags'], $sql_data['words']];
foreach ($this->extra_cols as $col) { foreach ($this->extra_cols as $col) {
@ -551,6 +548,7 @@ class kolab_storage_dav_cache extends kolab_storage_cache
$values = array( $values = array(
$this->db->quote($this->folder_id), $this->db->quote($this->folder_id),
$this->db->quote($object['uid']), $this->db->quote($object['uid']),
$this->db->quote($object['etag']),
$this->db->now(), $this->db->now(),
$this->db->quote($sql_data['changed']), $this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']), $this->db->quote($sql_data['data']),
@ -590,8 +588,6 @@ class kolab_storage_dav_cache extends kolab_storage_cache
protected function _unserialize($sql_arr) protected function _unserialize($sql_arr)
{ {
if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) { 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) { foreach ($this->data_props as $prop) {
if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') { if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') {
$object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz'])); $object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
@ -610,6 +606,8 @@ class kolab_storage_dav_cache extends kolab_storage_cache
} }
$object['_type'] = $sql_arr['type'] ?: $this->folder->type; $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
$object['uid'] = $sql_arr['uid'];
$object['etag'] = $sql_arr['etag'];
} }
// Fetch a complete object from the server // Fetch a complete object from the server
else { else {

View file

@ -0,0 +1,116 @@
<?php
/**
* Kolab storage cache class for contact objects
*
* @author Aleksander Machniak <machniak@apcheleia-it.ch>
*
* Copyright (C) 2013-2022, Apheleia IT AG <contact@apcheleia-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_cache_contact extends kolab_storage_cache
{
protected $extra_cols_max = 255;
protected $extra_cols = ['type', 'name', 'firstname', 'surname', 'email'];
protected $data_props = ['type', 'name', 'firstname', 'middlename', 'prefix', 'suffix', 'surname', 'email', 'organization', 'member'];
protected $fulltext_cols = ['name', 'firstname', 'surname', 'middlename', 'email:address'];
/**
* 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['type'] = $object['_type'];
// columns for sorting
$sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']);
$sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
$sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']);
$sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
if (is_array($sql_data['email'])) {
$sql_data['email'] = $sql_data['email']['address'];
}
// avoid value being null
if (empty($sql_data['email'])) {
$sql_data['email'] = '';
}
// use organization if name is empty
if (empty($sql_data['name']) && !empty($object['organization'])) {
$sql_data['name'] = rcube_charset::clean($object['organization']);
}
// make sure some data is not longer that database limit (#5291)
foreach ($this->extra_cols as $col) {
if (strlen($sql_data[$col]) > $this->extra_cols_max) {
$sql_data[$col] = rcube_charset::clean(substr($sql_data[$col], 0, $this->extra_cols_max));
}
}
$sql_data['tags'] = ' ' . join(' ', $this->get_tags($object)) . ' '; // pad with spaces for strict/prefix search
$sql_data['words'] = ' ' . join(' ', $this->get_words($object)) . ' ';
return $sql_data;
}
/**
* Callback to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words($object)
{
$data = '';
foreach ($this->fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = [];
foreach ((array)$object[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
/**
* Callback to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags($object)
{
$tags = [];
if (!empty($object['birthday'])) {
$tags[] = 'x-has-birthday';
}
return $tags;
}
}

View file

@ -23,8 +23,9 @@
class kolab_storage_dav_cache_event extends kolab_storage_dav_cache class kolab_storage_dav_cache_event extends kolab_storage_dav_cache
{ {
protected $extra_cols = array('dtstart','dtend'); protected $extra_cols = ['dtstart','dtend'];
protected $data_props = array('categories', 'status', 'attendees', 'etag'); protected $data_props = ['categories', 'status', 'attendees'];
protected $fulltext_cols = ['title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories'];
/** /**
* Helper method to convert the given Kolab object into a dataset to be written to cache * Helper method to convert the given Kolab object into a dataset to be written to cache
@ -63,6 +64,83 @@ class kolab_storage_dav_cache_event extends kolab_storage_dav_cache
} }
} }
$sql_data['tags'] = ' ' . join(' ', $this->get_tags($object)) . ' '; // pad with spaces for strict/prefix search
$sql_data['words'] = ' ' . join(' ', $this->get_words($object)) . ' ';
return $sql_data; return $sql_data;
} }
/**
* Callback to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words($object = [])
{
$data = '';
foreach ($this->fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = [];
foreach ((array) $object[$col] as $attr) {
$a[] = $attr[$field];
}
$val = join(' ', $a);
}
else {
$val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
$words = rcube_utils::normalize_string($data, true);
// collect words from recurrence exceptions
if (is_array($object['exceptions'])) {
foreach ($object['exceptions'] as $exception) {
$words = array_merge($words, $this->get_words($exception));
}
}
return array_unique($words);
}
/**
* Callback to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags($object)
{
$tags = [];
if (!empty($object['valarms'])) {
$tags[] = 'x-has-alarms';
}
// create tags reflecting participant status
if (is_array($object['attendees'])) {
foreach ($object['attendees'] as $attendee) {
if (!empty($attendee['email']) && !empty($attendee['status']))
$tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
}
}
// collect tags from recurrence exceptions
if (is_array($object['exceptions'])) {
foreach ($object['exceptions'] as $exception) {
$tags = array_merge($tags, $this->get_tags($exception));
}
}
if (!empty($object['status'])) {
$tags[] = 'x-status:' . strtolower($object['status']);
}
return array_unique($tags);
}
} }

View file

@ -111,6 +111,11 @@ class kolab_storage_dav_folder extends kolab_storage_folder
return $this->attributes['name']; return $this->attributes['name'];
} }
public function get_folder_info()
{
return []; // todo ?
}
/** /**
* Getter for parent folder path * Getter for parent folder path
* *
@ -400,12 +405,15 @@ class kolab_storage_dav_folder extends kolab_storage_folder
// generate and save object message // generate and save object message
if ($content = $this->to_dav($object)) { if ($content = $this->to_dav($object)) {
$result = $this->dav->create($this->object_location($object['uid']), $content); $method = $uid ? 'update' : 'create';
$result = $this->dav->{$method}($this->object_location($object['uid']), $content);
// Note: $result can be NULL if the request was successful, but ETag wasn't returned
if ($result !== false) { if ($result !== false) {
// insert/update object in the cache // insert/update object in the cache
$object['etag'] = $result; $object['etag'] = $result;
$this->cache->save($object, $uid); $this->cache->save($object, $uid);
$result = true;
} }
} }
@ -427,7 +435,7 @@ class kolab_storage_dav_folder extends kolab_storage_folder
} }
$href = $this->object_location($uid); $href = $this->object_location($uid);
$objects = $this->dav->getData($this->href, [$href]); $objects = $this->dav->getData($this->href, $this->get_dav_type(), [$href]);
if (!is_array($objects) || count($objects) != 1) { if (!is_array($objects) || count($objects) != 1) {
rcube::raise_error([ rcube::raise_error([
@ -476,7 +484,11 @@ class kolab_storage_dav_folder extends kolab_storage_folder
if ($this->type == 'event') { if ($this->type == 'event') {
$ical = libcalendaring::get_ical(); $ical = libcalendaring::get_ical();
// TODO: Attachments?
if (!empty($object['exceptions'])) {
$object['recurrence']['EXCEPTIONS'] = $object['exceptions'];
}
$result = $ical->export([$object]); $result = $ical->export([$object]);
} }