Branch 'kolab-syncroton-2.2' - 2 commits - lib/kolab_sync_backend.php lib/kolab_sync_data.php lib/plugins
Aleksander Machniak
machniak at kolabsys.com
Thu Feb 5 11:56:27 CET 2015
lib/kolab_sync_backend.php | 9
lib/kolab_sync_data.php | 48
lib/plugins/libkolab/SQL/mysql.initial.sql | 38
lib/plugins/libkolab/SQL/mysql/2013121100.sql | 13
lib/plugins/libkolab/SQL/mysql/2014021000.sql | 9
lib/plugins/libkolab/SQL/mysql/2014040900.sql | 8
lib/plugins/libkolab/SQL/mysql/2014112700.sql | 2
lib/plugins/libkolab/bin/modcache.sh | 29
lib/plugins/libkolab/lib/kolab_date_recurrence.php | 2
lib/plugins/libkolab/lib/kolab_format.php | 145 +
lib/plugins/libkolab/lib/kolab_format_configuration.php | 59
lib/plugins/libkolab/lib/kolab_format_contact.php | 43
lib/plugins/libkolab/lib/kolab_format_distributionlist.php | 26
lib/plugins/libkolab/lib/kolab_format_event.php | 2
lib/plugins/libkolab/lib/kolab_format_task.php | 7
lib/plugins/libkolab/lib/kolab_format_xcal.php | 163 +
lib/plugins/libkolab/lib/kolab_storage.php | 189 +-
lib/plugins/libkolab/lib/kolab_storage_cache.php | 229 ++
lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php | 50
lib/plugins/libkolab/lib/kolab_storage_cache_contact.php | 16
lib/plugins/libkolab/lib/kolab_storage_config.php | 840 ++++++++++
lib/plugins/libkolab/lib/kolab_storage_dataset.php | 154 +
lib/plugins/libkolab/lib/kolab_storage_folder.php | 143 +
23 files changed, 1998 insertions(+), 226 deletions(-)
New commits:
commit 7b5e7fbbb86c13b54ab12cde3fd162d8f35a2d13
Author: Aleksander Machniak <machniak at kolabsys.com>
Date: Thu Feb 5 05:56:07 2015 -0500
Handle kolab_storage errors correctly (#4418)
As we already found out (in #4378) kolab_storage did't prevent from sql errors
if some imap connection error happened. This is already fixed, however
syncroton need to use 'valid' flag implemented in kolab_storage_folder
to pass correct error codes to activesync client and to prevent from more issues.
Conflicts:
lib/kolab_sync_data.php
diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index b4bd3ca..f11f631 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -789,7 +789,7 @@ class kolab_sync_backend
// get all folders of specified type
$folderdata = $this->folder_meta();
- if (!is_array($folderdata)) {
+ if (!is_array($folderdata) || $id === null) {
return null;
}
@@ -801,10 +801,11 @@ class kolab_sync_backend
continue;
}
- $uid = self::folder_id($folder);
- $this->folder_uids[$folder] = $uid;
+ if ($uid = self::folder_id($folder)) {
+ $this->folder_uids[$folder] = $uid;
+ }
- if ($uid == $id) {
+ if ($uid === $id) {
$name = $folder;
}
}
diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php
index 3579a12..2e6a1e2 100644
--- a/lib/kolab_sync_data.php
+++ b/lib/kolab_sync_data.php
@@ -375,13 +375,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
+ $folder = $this->getFolderObject($foldername);
- if ($foldername === null) {
- continue;
+ if (!$folder || !$folder->valid) {
+ throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
- $folder = $this->getFolderObject($foldername);
-
// Remove all entries
$folder->delete_all();
@@ -390,13 +389,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
$list = $this->listFolders($folderid);
foreach ($list as $folderid => $folder) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
+ $folder = $this->getFolderObject($foldername);
- if ($foldername === null) {
- continue;
+ if (!$folder || !$folder->valid) {
+ throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
- $folder = $this->getFolderObject($foldername);
-
// Remove all entries
$folder->delete_all();
}
@@ -538,17 +536,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
$result = $result_type == self::RESULT_COUNT ? 0 : array();
$found = 0;
- foreach ($folders as $folderid) {
- $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
-
- if ($foldername === null) {
- continue;
- }
+ foreach ($folders as $folder_id) {
+ $foldername = $this->backend->folder_id2name($folder_id, $this->device->deviceid);
+ $folder = $this->getFolderObject($foldername);
- $folder = $this->getFolderObject($foldername);
-
- if (!$folder) {
- continue;
+ if (!$folder || !$folder->valid) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$found++;
@@ -753,14 +746,9 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
+ $folder = $this->getFolderObject($foldername);
- if ($foldername === null) {
- continue;
- }
-
- $folder = $this->getFolderObject($foldername);
-
- if ($folder && ($object = $folder->get_object($entryid))) {
+ if ($folder && $folder->valid && ($object = $folder->get_object($entryid))) {
$object['_folderid'] = $folderid;
return $object;
@@ -786,7 +774,7 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
- if ($folder && $folder->save($data)) {
+ if ($folder && $folder->valid && $folder->save($data)) {
return $data;
}
}
@@ -801,7 +789,7 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
if ($object) {
$folder = $this->getFolderObject($object['_mailbox']);
- if ($folder && $folder->save($data)) {
+ if ($folder && $folder->valid && $folder->save($data)) {
return $data;
}
}
@@ -816,7 +804,7 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
if ($object) {
$folder = $this->getFolderObject($object['_mailbox']);
- return $folder && $folder->delete($entryid);
+ return $folder && $folder->valid && $folder->delete($entryid);
}
// object doesn't exist, confirm deletion
@@ -891,12 +879,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
*/
protected function getFolderObject($name)
{
- if ($name === null) {
+ if ($name === null || $name === '') {
return null;
}
if (!isset($this->folders[$name])) {
- $this->folders[$name] = kolab_storage::get_folder($name);
+ $this->folders[$name] = kolab_storage::get_folder($name, $this->modelName);
}
return $this->folders[$name];
commit 354fd67871e30c554f609515a6755dc6236d164c
Author: Aleksander Machniak <machniak at kolabsys.com>
Date: Thu Feb 5 05:51:56 2015 -0500
Update libkolab plugin (from roundcubemail-plugins-kolab-3.1 branch)
diff --git a/lib/plugins/libkolab/SQL/mysql.initial.sql b/lib/plugins/libkolab/SQL/mysql.initial.sql
index fcb51b0..cc83ad3 100644
--- a/lib/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/plugins/libkolab/SQL/mysql.initial.sql
@@ -29,11 +29,15 @@ CREATE TABLE `kolab_cache_contact` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` LONGTEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) 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_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
@@ -48,8 +52,8 @@ CREATE TABLE `kolab_cache_event` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
@@ -67,8 +71,8 @@ CREATE TABLE `kolab_cache_task` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
@@ -86,8 +90,8 @@ CREATE TABLE `kolab_cache_journal` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
@@ -105,8 +109,8 @@ CREATE TABLE `kolab_cache_note` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
@@ -122,8 +126,8 @@ CREATE TABLE `kolab_cache_file` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`filename` varchar(255) DEFAULT NULL,
@@ -141,8 +145,8 @@ CREATE TABLE `kolab_cache_configuration` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
@@ -160,8 +164,8 @@ CREATE TABLE `kolab_cache_freebusy` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
@@ -172,4 +176,4 @@ CREATE TABLE `kolab_cache_freebusy` (
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013110400');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2014021000');
diff --git a/lib/plugins/libkolab/SQL/mysql/2013121100.sql b/lib/plugins/libkolab/SQL/mysql/2013121100.sql
new file mode 100644
index 0000000..8cab5ef
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2013121100.sql
@@ -0,0 +1,13 @@
+-- well, these deletes are really optional
+-- we can clear all caches or only contacts/events/tasks
+-- the issue we're fixing here was about contacts (Bug #2662)
+DELETE FROM `kolab_folders` WHERE `type` IN ('contact', 'event', 'task');
+
+ALTER TABLE `kolab_cache_contact` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `xml` `xml` LONGBLOB NOT NULL;
diff --git a/lib/plugins/libkolab/SQL/mysql/2014021000.sql b/lib/plugins/libkolab/SQL/mysql/2014021000.sql
new file mode 100644
index 0000000..31ce699
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014021000.sql
@@ -0,0 +1,9 @@
+ALTER TABLE `kolab_cache_contact` ADD `name` VARCHAR(255) NOT NULL,
+ ADD `firstname` VARCHAR(255) NOT NULL,
+ ADD `surname` VARCHAR(255) NOT NULL,
+ ADD `email` VARCHAR(255) NOT NULL;
+
+-- updating or clearing all contacts caches is required.
+-- either run `bin/modcache.sh update --type=contact` or execute the following query:
+-- DELETE FROM `kolab_folders` WHERE `type`='contact';
+
diff --git a/lib/plugins/libkolab/SQL/mysql/2014040900.sql b/lib/plugins/libkolab/SQL/mysql/2014040900.sql
new file mode 100644
index 0000000..61649c1
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014040900.sql
@@ -0,0 +1,8 @@
+ALTER TABLE `kolab_cache_contact` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;
diff --git a/lib/plugins/libkolab/SQL/mysql/2014112700.sql b/lib/plugins/libkolab/SQL/mysql/2014112700.sql
new file mode 100644
index 0000000..90c77b8
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014112700.sql
@@ -0,0 +1,2 @@
+-- delete cache entries for old folder identifiers
+DELETE FROM `kolab_folders` WHERE `resource` LIKE 'imap://anonymous@%';
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
index da6e4f8..550a7d6 100755
--- a/lib/plugins/libkolab/bin/modcache.sh
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -7,7 +7,7 @@
* @version 3.1
* @author Thomas Bruederli <bruederli at kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at 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
@@ -65,6 +65,7 @@ $db->db_connect('w');
if (!$db->is_connected() || $db->is_error())
die("No DB connection\n");
+ini_set('display_errors', 1);
/*
* Script controller
@@ -142,6 +143,32 @@ case 'prewarm':
die("Authentication failed for " . $opts['user']);
break;
+/**
+ * Update the cache meta columns from the serialized/xml data
+ * (might be run after a schema update)
+ */
+case 'update':
+ // make sure libkolab classes are loaded
+ $rcmail->plugins->load_plugin('libkolab');
+
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
+ foreach ($folder_types as $type) {
+ $class = 'kolab_storage_cache_' . $type;
+ $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
+ while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+ $folder = new $class;
+ $folder->select_by_id($sql_arr['folder_id']);
+ echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
+ foreach ($folder->select() as $object) {
+ $object['_formatobj']->to_array(); // load data
+ $folder->save($object['_msguid'], $object, $object['_msguid']);
+ echo ".";
+ }
+ echo "done.\n";
+ }
+ }
+ break;
+
/*
* Unknown action => show usage
diff --git a/lib/plugins/libkolab/lib/kolab_date_recurrence.php b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
index 85ffd91..1dc63be 100644
--- a/lib/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -118,7 +118,7 @@ class kolab_date_recurrence
}
// determine a reasonable end date if none given
- if (!$event['recurrence']['COUNT']) {
+ if (!$event['recurrence']['COUNT'] && $event['start'] instanceof DateTime) {
switch ($event['recurrence']['FREQ']) {
case 'YEARLY': $intvl = 'P100Y'; break;
case 'MONTHLY': $intvl = 'P20Y'; break;
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 9dc8a31..fa85eec 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -47,6 +47,117 @@ abstract class kolab_format
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
const PRODUCT_ID = 'Roundcube-libkolab-0.9';
+ // mapping table for valid PHP timezones not supported by libkolabxml
+ // basically the entire list of ftp://ftp.iana.org/tz/data/backward
+ protected static $timezone_map = array(
+ 'Africa/Asmera' => 'Africa/Asmara',
+ 'Africa/Timbuktu' => 'Africa/Abidjan',
+ 'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
+ 'America/Atka' => 'America/Adak',
+ 'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
+ 'America/Catamarca' => 'America/Argentina/Catamarca',
+ 'America/Coral_Harbour' => 'America/Atikokan',
+ 'America/Cordoba' => 'America/Argentina/Cordoba',
+ 'America/Ensenada' => 'America/Tijuana',
+ 'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
+ 'America/Indianapolis' => 'America/Indiana/Indianapolis',
+ 'America/Jujuy' => 'America/Argentina/Jujuy',
+ 'America/Knox_IN' => 'America/Indiana/Knox',
+ 'America/Louisville' => 'America/Kentucky/Louisville',
+ 'America/Mendoza' => 'America/Argentina/Mendoza',
+ 'America/Porto_Acre' => 'America/Rio_Branco',
+ 'America/Rosario' => 'America/Argentina/Cordoba',
+ 'America/Virgin' => 'America/Port_of_Spain',
+ 'Asia/Ashkhabad' => 'Asia/Ashgabat',
+ 'Asia/Calcutta' => 'Asia/Kolkata',
+ 'Asia/Chungking' => 'Asia/Shanghai',
+ 'Asia/Dacca' => 'Asia/Dhaka',
+ 'Asia/Katmandu' => 'Asia/Kathmandu',
+ 'Asia/Macao' => 'Asia/Macau',
+ 'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
+ 'Asia/Tel_Aviv' => 'Asia/Jerusalem',
+ 'Asia/Thimbu' => 'Asia/Thimphu',
+ 'Asia/Ujung_Pandang' => 'Asia/Makassar',
+ 'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
+ 'Atlantic/Faeroe' => 'Atlantic/Faroe',
+ 'Atlantic/Jan_Mayen' => 'Europe/Oslo',
+ 'Australia/ACT' => 'Australia/Sydney',
+ 'Australia/Canberra' => 'Australia/Sydney',
+ 'Australia/LHI' => 'Australia/Lord_Howe',
+ 'Australia/NSW' => 'Australia/Sydney',
+ 'Australia/North' => 'Australia/Darwin',
+ 'Australia/Queensland' => 'Australia/Brisbane',
+ 'Australia/South' => 'Australia/Adelaide',
+ 'Australia/Tasmania' => 'Australia/Hobart',
+ 'Australia/Victoria' => 'Australia/Melbourne',
+ 'Australia/West' => 'Australia/Perth',
+ 'Australia/Yancowinna' => 'Australia/Broken_Hill',
+ 'Brazil/Acre' => 'America/Rio_Branco',
+ 'Brazil/DeNoronha' => 'America/Noronha',
+ 'Brazil/East' => 'America/Sao_Paulo',
+ 'Brazil/West' => 'America/Manaus',
+ 'Canada/Atlantic' => 'America/Halifax',
+ 'Canada/Central' => 'America/Winnipeg',
+ 'Canada/East-Saskatchewan' => 'America/Regina',
+ 'Canada/Eastern' => 'America/Toronto',
+ 'Canada/Mountain' => 'America/Edmonton',
+ 'Canada/Newfoundland' => 'America/St_Johns',
+ 'Canada/Pacific' => 'America/Vancouver',
+ 'Canada/Saskatchewan' => 'America/Regina',
+ 'Canada/Yukon' => 'America/Whitehorse',
+ 'Chile/Continental' => 'America/Santiago',
+ 'Chile/EasterIsland' => 'Pacific/Easter',
+ 'Cuba' => 'America/Havana',
+ 'Egypt' => 'Africa/Cairo',
+ 'Eire' => 'Europe/Dublin',
+ 'Europe/Belfast' => 'Europe/London',
+ 'Europe/Tiraspol' => 'Europe/Chisinau',
+ 'GB' => 'Europe/London',
+ 'GB-Eire' => 'Europe/London',
+ 'Greenwich' => 'Etc/GMT',
+ 'Hongkong' => 'Asia/Hong_Kong',
+ 'Iceland' => 'Atlantic/Reykjavik',
+ 'Iran' => 'Asia/Tehran',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jamaica' => 'America/Jamaica',
+ 'Japan' => 'Asia/Tokyo',
+ 'Kwajalein' => 'Pacific/Kwajalein',
+ 'Libya' => 'Africa/Tripoli',
+ 'Mexico/BajaNorte' => 'America/Tijuana',
+ 'Mexico/BajaSur' => 'America/Mazatlan',
+ 'Mexico/General' => 'America/Mexico_City',
+ 'NZ' => 'Pacific/Auckland',
+ 'NZ-CHAT' => 'Pacific/Chatham',
+ 'Navajo' => 'America/Denver',
+ 'PRC' => 'Asia/Shanghai',
+ 'Pacific/Ponape' => 'Pacific/Pohnpei',
+ 'Pacific/Samoa' => 'Pacific/Pago_Pago',
+ 'Pacific/Truk' => 'Pacific/Chuuk',
+ 'Pacific/Yap' => 'Pacific/Chuuk',
+ 'Poland' => 'Europe/Warsaw',
+ 'Portugal' => 'Europe/Lisbon',
+ 'ROC' => 'Asia/Taipei',
+ 'ROK' => 'Asia/Seoul',
+ 'Singapore' => 'Asia/Singapore',
+ 'Turkey' => 'Europe/Istanbul',
+ 'UCT' => 'Etc/UCT',
+ 'US/Alaska' => 'America/Anchorage',
+ 'US/Aleutian' => 'America/Adak',
+ 'US/Arizona' => 'America/Phoenix',
+ 'US/Central' => 'America/Chicago',
+ 'US/East-Indiana' => 'America/Indiana/Indianapolis',
+ 'US/Eastern' => 'America/New_York',
+ 'US/Hawaii' => 'Pacific/Honolulu',
+ 'US/Indiana-Starke' => 'America/Indiana/Knox',
+ 'US/Michigan' => 'America/Detroit',
+ 'US/Mountain' => 'America/Denver',
+ 'US/Pacific' => 'America/Los_Angeles',
+ 'US/Samoa' => 'Pacific/Pago_Pago',
+ 'Universal' => 'Etc/UTC',
+ 'W-SU' => 'Europe/Moscow',
+ 'Zulu' => 'Etc/UTC',
+ );
+
/**
* Factory method to instantiate a kolab_format object of the given type and version
*
@@ -63,7 +174,7 @@ abstract class kolab_format
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
- $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
+ $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
@@ -123,10 +234,15 @@ abstract class kolab_format
if (!$dateonly)
$result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
- if ($tz && in_array($tz->getName(), array('UTC','GMT','+00:00')))
+ if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z'))) {
$result->setUTC(true);
- else if ($tz !== false)
- $result->setTimezone($tz->getName());
+ }
+ else if ($tz !== false) {
+ $tzid = $tz->getName();
+ if (array_key_exists($tzid, self::$timezone_map))
+ $tzid = self::$timezone_map[$tzid];
+ $result->setTimezone($tzid);
+ }
}
return $result;
@@ -208,7 +324,11 @@ abstract class kolab_format
*/
public static function mime2object_type($x_kolab_type)
{
- return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
+ return preg_replace(
+ array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
+ array( 'dictionary', 'distribution-list'),
+ substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
+ );
}
@@ -410,7 +530,7 @@ abstract class kolab_format
$this->obj->setLastModified(self::get_datetime($object['changed']));
// Save custom properties of the given object
- if (isset($object['x-custom'])) {
+ if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
$vcustom = new vectorcs;
foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
@@ -418,7 +538,8 @@ abstract class kolab_format
}
$this->obj->setCustomProperties($vcustom);
}
- else { // load custom properties from XML for caching (#2238)
+ // load custom properties from XML for caching (#2238) if method exists (#3125)
+ else if (method_exists($this->obj, 'customProperties')) {
$object['x-custom'] = array();
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
@@ -451,10 +572,12 @@ abstract class kolab_format
}
// read custom properties
- $vcustom = $this->obj->customProperties();
- for ($i=0; $i < $vcustom->size(); $i++) {
- $cp = $vcustom->get($i);
- $object['x-custom'][] = array($cp->identifier, $cp->value);
+ if (method_exists($this->obj, 'customProperties')) {
+ $vcustom = $this->obj->customProperties();
+ for ($i=0; $i < $vcustom->size(); $i++) {
+ $cp = $vcustom->get($i);
+ $object['x-custom'][] = array($cp->identifier, $cp->value);
+ }
}
// merge with additional data, e.g. attachments from the message
diff --git a/lib/plugins/libkolab/lib/kolab_format_configuration.php b/lib/plugins/libkolab/lib/kolab_format_configuration.php
index 5a8d3ff..b555484 100644
--- a/lib/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/lib/plugins/libkolab/lib/kolab_format_configuration.php
@@ -32,10 +32,12 @@ class kolab_format_configuration extends kolab_format
protected $write_func = 'writeConfiguration';
private $type_map = array(
- 'dictionary' => Configuration::TypeDictionary,
- 'category' => Configuration::TypeCategoryColor,
+ 'category' => Configuration::TypeCategoryColor,
+ 'dictionary' => Configuration::TypeDictionary,
+ 'file_driver' => Configuration::TypeFileDriver,
);
+ private $driver_settings_fields = array('host', 'port', 'username', 'password');
/**
* Set properties to the kolabformat object
@@ -60,6 +62,22 @@ class kolab_format_configuration extends kolab_format
$categories = new vectorcategorycolor;
$this->obj = new Configuration($categories);
break;
+
+ case 'file_driver':
+ $driver = new FileDriver($object['driver'], $object['title']);
+
+ $driver->setEnabled((bool) $object['enabled']);
+
+ foreach ($this->driver_settings_fields as $field) {
+ $value = $object[$field];
+ if ($value !== null) {
+ $driver->{'set' . ucfirst($field)}($value);
+ }
+ }
+
+ $this->obj = new Configuration($driver);
+ break;
+
default:
return false;
}
@@ -111,6 +129,19 @@ class kolab_format_configuration extends kolab_format
case 'category':
// TODO: implement this
break;
+
+ case 'file_driver':
+ $driver = $this->obj->fileDriver();
+
+ $object['driver'] = $driver->driver();
+ $object['title'] = $driver->title();
+ $object['enabled'] = $driver->enabled();
+
+ foreach ($this->driver_settings_fields as $field) {
+ $object[$field] = $driver->{$field}();
+ }
+
+ break;
}
// adjust content-type string
@@ -136,4 +167,28 @@ class kolab_format_configuration extends kolab_format
return $tags;
}
+ /**
+ * Callback for kolab_storage_cache to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words()
+ {
+ $words = array();
+
+ foreach ((array)$this->data['members'] as $url) {
+ $member = kolab_storage_config::parse_member_url($url);
+
+ if (empty($member)) {
+ if (strpos($url, 'urn:uuid:') === 0) {
+ $words[] = substr($url, 9);
+ }
+ }
+ else if (!empty($member['params']['message-id'])) {
+ $words[] = $member['params']['message-id'];
+ }
+ }
+
+ return $words;
+ }
}
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 0d0bc75..a96312a 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -107,8 +107,8 @@ class kolab_format_contact extends kolab_format
if (isset($object['nickname']))
$this->obj->setNickNames(self::array2vector($object['nickname']));
- if (isset($object['profession']))
- $this->obj->setTitles(self::array2vector($object['profession']));
+ if (isset($object['jobtitle']))
+ $this->obj->setTitles(self::array2vector($object['jobtitle']));
// organisation related properties (affiliation)
$org = new Affiliation;
@@ -117,17 +117,17 @@ class kolab_format_contact extends kolab_format
$org->setOrganisation($object['organization']);
if ($object['department'])
$org->setOrganisationalUnits(self::array2vector($object['department']));
- if ($object['jobtitle'])
- $org->setRoles(self::array2vector($object['jobtitle']));
+ if ($object['profession'])
+ $org->setRoles(self::array2vector($object['profession']));
$rels = new vectorrelated;
- if ($object['manager']) {
- foreach ((array)$object['manager'] as $manager)
- $rels->push(new Related(Related::Text, $manager, Related::Manager));
- }
- if ($object['assistant']) {
- foreach ((array)$object['assistant'] as $assistant)
- $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+ foreach (array('manager','assistant') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
$org->setRelateds($rels);
@@ -219,12 +219,13 @@ class kolab_format_contact extends kolab_format
// spouse and children are relateds
$rels = new vectorrelated;
- if ($object['spouse']) {
- $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
- }
- if ($object['children']) {
- foreach ((array)$object['children'] as $child)
- $rels->push(new Related(Related::Text, $child, Related::Child));
+ foreach (array('spouse','children') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
$this->obj->setRelateds($rels);
@@ -296,7 +297,7 @@ class kolab_format_contact extends kolab_format
$object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
$object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
$object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
- $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+ $object['jobtitle'] = join(' ', self::vector2array($this->obj->titles()));
$object['categories'] = self::vector2array($this->obj->categories());
// organisation related properties (affiliation)
@@ -304,7 +305,7 @@ class kolab_format_contact extends kolab_format
if ($orgs->size()) {
$org = $orgs->get(0);
$object['organization'] = $org->organisation();
- $object['jobtitle'] = join(' ', self::vector2array($org->roles()));
+ $object['profession'] = join(' ', self::vector2array($org->roles()));
$object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
$this->read_relateds($org->relateds(), $object);
}
@@ -347,10 +348,10 @@ class kolab_format_contact extends kolab_format
$object['freebusyurl'] = $this->obj->freeBusyUrl();
if ($bday = self::php_datetime($this->obj->bDay()))
- $object['birthday'] = $bday->format('c');
+ $object['birthday'] = $bday;
if ($anniversary = self::php_datetime($this->obj->anniversary()))
- $object['anniversary'] = $anniversary->format('c');
+ $object['anniversary'] = $anniversary;
$gendermap = array_flip($this->gendermap);
if (($g = $this->obj->gender()) && $gendermap[$g])
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
index 46dda01..88c6f7b 100644
--- a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -44,17 +44,29 @@ class kolab_format_distributionlist extends kolab_format
$this->obj->setName($object['name']);
+ $seen = array();
$members = new vectorcontactref;
- foreach ((array)$object['member'] as $member) {
- if ($member['uid'])
+ foreach ((array)$object['member'] as $i => $member) {
+ if ($member['uid']) {
+ $key = 'uid:' . $member['uid'];
$m = new ContactReference(ContactReference::UidReference, $member['uid']);
- else if ($member['email'])
+ }
+ else if ($member['email']) {
+ $key = 'mailto:' . $member['email'];
$m = new ContactReference(ContactReference::EmailReference, $member['email']);
- else
+ $m->setName($member['name']);
+ }
+ else {
continue;
-
- $m->setName($member['name']);
- $members->push($m);
+ }
+
+ if (!$seen[$key]++) {
+ $members->push($m);
+ }
+ else {
+ // remove dupes for caching
+ unset($object['member'][$i]);
+ }
}
$this->obj->setMembers($members);
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
index 6a8c3ae..7d1d53c 100644
--- a/lib/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -87,6 +87,8 @@ class kolab_format_event extends kolab_format_xcal
$status = kolabformat::StatusTentative;
if ($object['cancelled'])
$status = kolabformat::StatusCancelled;
+ else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+ $status = $this->status_map[$object['status']];
$this->obj->setStatus($status);
// save recurrence exceptions
diff --git a/lib/plugins/libkolab/lib/kolab_format_task.php b/lib/plugins/libkolab/lib/kolab_format_task.php
index 465ba90..555e77f 100644
--- a/lib/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/plugins/libkolab/lib/kolab_format_task.php
@@ -42,6 +42,8 @@ class kolab_format_task extends kolab_format_xcal
parent::set($object);
$this->obj->setPercentComplete(intval($object['complete']));
+ $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
+ $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
$status = kolabformat::StatusUndefined;
if ($object['complete'] == 100)
@@ -50,9 +52,6 @@ class kolab_format_task extends kolab_format_xcal
$status = $this->status_map[$object['status']];
$this->obj->setStatus($status);
- $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
- $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
-
$related = new vectors;
if (!empty($object['parent_id']))
$related->push($object['parent_id']);
@@ -119,7 +118,7 @@ class kolab_format_task extends kolab_format_xcal
if ($this->data['priority'] == 1)
$tags[] = 'x-flagged';
- if (!empty($this->data['valarms']))
+ if (!empty($this->data['alarms']))
$tags[] = 'x-has-alarms';
if ($this->data['parent_id'])
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index ef423aa..76b7b3c 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -81,6 +81,10 @@ abstract class kolab_format_xcal extends kolab_format
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
'CANCELLED' => kolabformat::StatusCancelled,
+ 'TENTATIVE' => kolabformat::StatusTentative,
+ 'CONFIRMED' => kolabformat::StatusConfirmed,
+ 'DRAFT' => kolabformat::StatusDraft,
+ 'FINAL' => kolabformat::StatusFinal,
);
protected $part_status_map = array(
@@ -191,30 +195,77 @@ abstract class kolab_format_xcal extends kolab_format
}
}
+ if ($rdates = $this->obj->recurrenceDates()) {
+ for ($i=0; $i < $rdates->size(); $i++) {
+ if ($rdate = self::php_datetime($rdates->get($i)))
+ $object['recurrence']['RDATE'][] = $rdate;
+ }
+ }
+
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
+ $object['valarms'] = array();
for ($i=0; $i < $valarms->size(); $i++) {
$alarm = $valarms->get($i);
$type = $alarm_types[$alarm->type()];
if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported
+ $valarm = array(
+ 'action' => $type,
+ 'summary' => $alarm->summary(),
+ 'description' => $alarm->description(),
+ );
+
+ if ($type == 'EMAIL') {
+ $valarm['attendees'] = array();
+ $attvec = $this->obj->attendees();
+ for ($j=0; $j < $attvec->size(); $j++) {
+ $cr = $attvec->get($j);
+ $valarm['attendees'][] = $cr->email();
+ }
+ }
+
if ($start = self::php_datetime($alarm->start())) {
- $object['alarms'] = '@' . $start->format('U');
+ $trigger = '@' . $start->format('U');
+ $valarm['trigger'] = $start;
}
else if ($offset = $alarm->relativeStart()) {
- $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ $value = $time = '';
if ($w = $offset->weeks()) $value .= $w . 'W';
else if ($d = $offset->days()) $value .= $d . 'D';
- else if ($h = $offset->hours()) $value .= $h . 'H';
- else if ($m = $offset->minutes()) $value .= $m . 'M';
- else if ($s = $offset->seconds()) $value .= $s . 'S';
- else continue;
+ else if ($h = $offset->hours()) $time .= $h . 'H';
+ else if ($m = $offset->minutes()) $time .= $m . 'M';
+ else if ($s = $offset->seconds()) $time .= $s . 'S';
+
+ // assume 'at event time'
+ if (empty($value) && empty($time)) {
+ $prefix = '';
+ $time = '0S';
+ }
+
+ $trigger = $prefix . $value . $time;
+ $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
+ }
- $object['alarms'] = $value;
+ // read alarm duration and repeat properties
+ if (($duration = $alarm->duration()) && $duration->isValid()) {
+ $value = $time = '';
+ if ($w = $duration->weeks()) $value .= $w . 'W';
+ else if ($d = $duration->days()) $value .= $d . 'D';
+ else if ($h = $duration->hours()) $time .= $h . 'H';
+ else if ($m = $duration->minutes()) $time .= $m . 'M';
+ else if ($s = $duration->seconds()) $time .= $s . 'S';
+ $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
+ $valarm['repeat'] = $alarm->numrepeat();
+ }
+
+ $object['valarms'][] = array_filter($valarm);
+
+ if (!$object['alarms']) {
+ $object['alarms'] = $trigger . ':' . $type; // legacy property
}
- $object['alarms'] .= ':' . $type;
- break;
}
}
@@ -311,7 +362,7 @@ abstract class kolab_format_xcal extends kolab_format
$rr = new RecurrenceRule;
$rr->setFrequency(RecurrenceRule::FreqNone);
- if ($object['recurrence']) {
+ if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
$rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
if ($object['recurrence']['INTERVAL'])
@@ -368,9 +419,71 @@ abstract class kolab_format_xcal extends kolab_format
$this->obj->setRecurrenceRule($rr);
+ // save recurrence dates (aka RDATE)
+ if (!empty($object['recurrence']['RDATE'])) {
+ $rdates = new vectordatetime;
+ foreach ((array)$object['recurrence']['RDATE'] as $rdate)
+ $rdates->push(self::get_datetime($rdate, null, true));
+ $this->obj->setRecurrenceDates($rdates);
+ }
+
// save alarm
$valarms = new vectoralarm;
- if ($object['alarms']) {
+ if ($object['valarms']) {
+ foreach ($object['valarms'] as $valarm) {
+ if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
+ continue; // skip unknown alarm types
+ }
+
+ if ($valarm['action'] == 'EMAIL') {
+ $recipients = new vectorcontactref;
+ foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
+ $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
+ }
+ $alarm = new Alarm(
+ strval($valarm['summary'] ?: $object['title']),
+ strval($valarm['description'] ?: $object['description']),
+ $recipients
+ );
+ }
+ else {
+ $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
+ }
+
+ if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
+ $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
+ }
+ else {
+ try {
+ $prefix = $valarm['trigger'][0];
+ $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
+ $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
+ }
+ catch (Exception $e) {
+ // skip alarm with invalid trigger values
+ rcube::raise_error($e, true);
+ continue;
+ }
+
+ $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
+ }
+
+ if ($valarm['duration']) {
+ try {
+ $d = new DateInterval($valarm['duration']);
+ $duration = new Duration($d->d, $d->h, $d->i, $d->s);
+ $alarm->setDuration($duration, intval($valarm['repeat']));
+ }
+ catch (Exception $e) {
+ // ignore
+ }
+ }
+
+ $valarms->push($alarm);
+ }
+ }
+ // legacy support
+ else if ($object['alarms']) {
list($offset, $type) = explode(":", $object['alarms']);
if ($type == 'EMAIL' && !empty($object['_owner'])) { // email alarms implicitly go to event owner
@@ -398,6 +511,19 @@ abstract class kolab_format_xcal extends kolab_format
}
$valarms->push($alarm);
+
+ // preserve additional alarm entries
+ $oldvalarms = $this->obj->alarms();
+ for ($i=1; $i < $oldvalarms->size(); $i++) {
+ $valarms->push($oldvalarms->get($i));
+ }
+
+ // HACK: set and read back alarms to store the correct 'valarms' value in cache
+ if ($i > 1) {
+ $this->obj->setAlarms($valarms);
+ $update = $this->to_array();
+ $object['valarms'] = $update['valarms'];
+ }
}
$this->obj->setAlarms($valarms);
@@ -408,8 +534,19 @@ abstract class kolab_format_xcal extends kolab_format
continue;
$attach = new Attachment;
$attach->setLabel((string)$attr['name']);
- $attach->setUri('cid:' . $cid, $attr['mimetype']);
- $vattach->push($attach);
+ $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
+ if ($attach->isValid()) {
+ $vattach->push($attach);
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 660,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
+ ), true);
+ }
}
foreach ((array)$object['links'] as $link) {
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index 5f8b9c6..9ef3057 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -35,6 +35,11 @@ class kolab_storage
const UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
+ const ERROR_IMAP_CONN = 1;
+ const ERROR_CACHE_DB = 2;
+ const ERROR_NO_PERMISSION = 3;
+ const ERROR_INVALID_FOLDER = 4;
+
public static $version = '3.0';
public static $last_error;
@@ -101,7 +106,6 @@ class kolab_storage
return self::$ready;
}
-
/**
* Get a list of storage folders for the given data type
*
@@ -116,7 +120,7 @@ class kolab_storage
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
- $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
@@ -133,26 +137,26 @@ class kolab_storage
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
- return new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return null;
}
-
/**
* Getter for a specific storage folder
*
- * @param string IMAP folder to access (UTF7-IMAP)
+ * @param string IMAP folder to access (UTF7-IMAP)
+ * @param string Expected folder type
+ *
* @return object kolab_storage_folder The folder object
*/
- public static function get_folder($folder)
+ public static function get_folder($folder, $type = null)
{
- return self::setup() ? new kolab_storage_folder($folder) : null;
+ return self::setup() ? new kolab_storage_folder($folder, $type) : null;
}
-
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
@@ -165,11 +169,11 @@ class kolab_storage
{
self::setup();
$folder = null;
- foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+ foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
- $folder = new kolab_storage_folder($foldername);
+ $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
- $folder->set_folder($foldername);
+ $folder->set_folder($foldername, $type, $folderdata[$foldername]);
if ($object = $folder->get_object($uid, '*'))
return $object;
@@ -433,13 +437,14 @@ class kolab_storage
// get username
$pos = strpos($folder, $delim);
if ($pos) {
- $prefix = '('.substr($folder, 0, $pos).') ';
+ $prefix = '('.substr($folder, 0, $pos).')';
$folder = substr($folder, $pos+1);
}
else {
- $prefix = '('.$folder.')';
+ $prefix = $folder;
$folder = '';
}
+
$found = true;
$folder_ns = 'other';
break;
@@ -467,7 +472,7 @@ class kolab_storage
$folder = str_replace(html::quote($delim), ' » ', $folder);
if ($prefix)
- $folder = html::quote($prefix) . ' ' . $folder;
+ $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
@@ -492,7 +497,8 @@ class kolab_storage
}
/**
- * Helper method to generate a truncated folder name to display
+ * Helper method to generate a truncated folder name to display.
+ * Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
@@ -504,10 +510,29 @@ class kolab_storage
$length = strlen($names[$i] . ' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
- $name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
+ $diff = 1;
+
+ // check if prefix folder is in other users namespace
+ for ($n = count($names)-1; $n >= 0; $n--) {
+ if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
+ $diff = 0;
+ break;
+ }
+ }
+
+ $name = str_repeat(' ', $count - $diff) . '» ' . substr($name, $length);
+ break;
+ }
+ // other users namespace and parent folder exists
+ else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
+ $length = strlen('(' . $names[$i] . ') ');
+ $prefix = substr($name, 0, $length);
+ $count = count(explode(' » ', $prefix));
+ $name = str_repeat(' ', $count) . '» ' . substr($name, $length);
break;
}
}
+
$names[] = $origname;
return $name;
@@ -525,8 +550,8 @@ class kolab_storage
*/
public static function folder_selector($type, $attrs, $current = '')
{
- // get all folders of specified type
- $folders = self::get_folders($type, false);
+ // get all folders of specified type (sorted)
+ $folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
@@ -540,13 +565,24 @@ class kolab_storage
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
+
// skip current folder and it's subfolders
- if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
- continue;
+ if ($len) {
+ if ($name == $current) {
+ // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+ if ($p_len && !isset($names[$parent])) {
+ $names[$parent] = self::object_name($parent);
+ }
+ continue;
+ }
+ if (strpos($name, $current.$delim) === 0) {
+ continue;
+ }
}
// always show the parent of current folder
- if ($p_len && $name == $parent) { }
+ if ($p_len && $name == $parent) {
+ }
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
@@ -558,14 +594,6 @@ class kolab_storage
$names[$name] = self::object_name($name);
}
- // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
- if ($p_len && !isset($names[$parent])) {
- $names[$parent] = self::object_name($parent);
- }
-
- // Sort folders list
- asort($names, SORT_LOCALE_STRING);
-
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
@@ -643,7 +671,8 @@ class kolab_storage
unset($folderdata[$folder]);
}
}
- return array_keys($folderdata);
+
+ return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
@@ -683,26 +712,74 @@ class kolab_storage
*/
public static function sort_folders($folders)
{
- $pad = ' ';
+ $pad = ' ';
+ $out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
+
foreach ($folders as $folder) {
$folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode »
}
- $names = array();
- foreach ($nsnames as $ns => $dummy) {
- asort($nsnames[$ns], SORT_LOCALE_STRING);
- $names += $nsnames[$ns];
+ // $folders is a result of get_folders() we can assume folders were already sorted
+ foreach (array_keys($nsnames) as $ns) {
+ // asort($nsnames[$ns], SORT_LOCALE_STRING);
+ foreach (array_keys($nsnames[$ns]) as $utf7name) {
+ $out[] = $folders[$utf7name];
+ }
}
- $out = array();
- foreach ($names as $utf7name => $name) {
- $out[] = $folders[$utf7name];
+ return $out;
+ }
+
+
+ /**
+ * Check the folder tree and add the missing parents as virtual folders
+ *
+ * @param array $folders Folders list
+ *
+ * @return array Folders list
+ */
+ public static function folder_hierarchy($folders)
+ {
+ $_folders = array();
+ $existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
+ $delim = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
+
+ foreach ($folders as $idx => $folder) {
+ $path = explode($delim, $folder->name);
+ array_pop($path);
+
+ // skip top folders or ones with a custom displayname
+ if (count($path) <= 1 || kolab_storage::custom_displayname($folder->name)) {
+ }
+ else {
+ $parents = array();
+
+ while (count($path) > 1 && ($parent = join($delim, $path))) {
+ $name = kolab_storage::object_name($parent, $folder->get_namespace());
+ if (!in_array($name, $existing)) {
+ $parents[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace());
+ $existing[] = $name;
+ }
+
+ array_pop($path);
+ }
+
+ if (!empty($parents)) {
+ $parents = array_reverse(array_values($parents));
+ foreach ($parents as $parent) {
+ $_folders[] = $parent;
+ }
+ }
+ }
+
+ $_folders[] = $folder;
+ unset($folders[$idx]);
}
- return $out;
+ return $_folders;
}
@@ -823,7 +900,7 @@ class kolab_storage
self::setup();
if (self::$imap->subscribe($folder)) {
- self::$subscriptions === null;
+ self::$subscriptions = null;
return true;
}
@@ -843,7 +920,7 @@ class kolab_storage
self::setup();
if (self::$imap->unsubscribe($folder)) {
- self::$subscriptions === null;
+ self::$subscriptions = null;
return true;
}
@@ -1047,3 +1124,33 @@ class kolab_storage
}
}
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ */
+class virtual_kolab_storage_folder
+{
+ public $id;
+ public $name;
+ public $namespace;
+ public $virtual = true;
+
+ public function __construct($realname, $name, $ns)
+ {
+ $this->id = kolab_storage::folder_id($realname);
+ $this->name = $name;
+ $this->namespace = $ns;
+ }
+
+ public function get_namespace()
+ {
+ return $this->namespace;
+ }
+
+ public function get_name()
+ {
+ // this is already kolab_storage::object_name() result
+ return $this->name;
+ }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index 54047ba..b31cd74 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -29,7 +29,7 @@ class kolab_storage_cache
protected $folder;
protected $uid2msg;
protected $objects;
- protected $index = array();
+ protected $index = null;
protected $metadata = array();
protected $folder_id;
protected $resource_uri;
@@ -43,6 +43,9 @@ class kolab_storage_cache
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
+ protected $order_by = null;
+ protected $limit = null;
+ protected $error = 0;
/**
@@ -58,8 +61,10 @@ class kolab_storage_cache
rcube::raise_error(array(
'code' => 900,
'type' => 'php',
- 'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+ 'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
), true);
+
+ return new kolab_storage_cache($storage_folder);
}
}
@@ -73,6 +78,7 @@ class kolab_storage_cache
$this->db = $rcmail->get_dbh();
$this->imap = $rcmail->get_storage();
$this->enabled = $rcmail->config->get('kolab_cache', false);
+ $this->folders_table = $this->db->table_name('kolab_folders');
if ($this->enabled) {
// always read folder cache and lock state from DB master
@@ -85,6 +91,23 @@ class kolab_storage_cache
$this->set_folder($storage_folder);
}
+ /**
+ * Direct access to cache by folder_id
+ * (only for internal use)
+ */
+ 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) {
+ $this->metadata = $sql_arr;
+ $this->folder_id = $sql_arr['folder_id'];
+ $this->folder = new StdClass;
+ $this->folder->type = $sql_arr['type'];
+ $this->resource_uri = $sql_arr['resource'];
+ $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
+ $this->ready = true;
+ }
+ }
/**
* Connect cache with a storage folder
@@ -95,16 +118,15 @@ class kolab_storage_cache
{
$this->folder = $storage_folder;
- if (empty($this->folder->name)) {
+ if (empty($this->folder->name) || !$this->folder->valid) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
- $this->folders_table = $this->db->table_name('kolab_folders');
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
- $this->ready = $this->enabled;
+ $this->ready = $this->enabled && !empty($this->folder->type);
$this->folder_id = null;
}
@@ -117,6 +139,25 @@ class kolab_storage_cache
}
/**
+ * Getter for the numeric ID used in cache tables
+ */
+ public function get_folder_id()
+ {
+ $this->_read_folder_data();
+ return $this->folder_id;
+ }
+
+ /**
+ * Returns code of last error
+ *
+ * @return int Error code
+ */
+ public function get_error()
+ {
+ return $this->error;
+ }
+
+ /**
* Synchronize local cache data with remote
*/
public function synchronize()
@@ -188,6 +229,7 @@ class kolab_storage_cache
$this->_sync_unlock();
}
+ $this->check_error();
$this->synched = time();
}
@@ -204,7 +246,12 @@ class kolab_storage_cache
{
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
- return kolab_storage::get_folder($foldername)->cache->get($msguid, $type);
+ $success = false;
+ if ($targetfolder = kolab_storage::get_folder($foldername)) {
+ $success = $targetfolder->cache->get($msguid, $type);
+ $this->error = $targetfolder->cache->get_error();
+ }
+ return $success;
}
// load object if not in memory
@@ -220,17 +267,18 @@ class kolab_storage_cache
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $this->objects[$msguid] = $this->_unserialize($sql_arr);
+ $this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827)
}
}
// fetch from IMAP if not present in cache
if (empty($this->objects[$msguid])) {
$result = $this->_fetch(array($msguid), $type, $foldername);
- $this->objects[$msguid] = $result[0];
+ $this->objects = array($msguid => $result[0]); // store only this object in memory (#2827)
}
}
+ $this->check_error();
return $this->objects[$msguid];
}
@@ -250,8 +298,11 @@ class kolab_storage_cache
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
- kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
- return;
+ if ($targetfolder = kolab_storage::get_folder($foldername)) {
+ $targetfolder->cache->set($msguid, $object);
+ $this->error = $targetfolder->cache->get_error();
+ }
+ return;
}
// remove old entry
@@ -269,6 +320,8 @@ class kolab_storage_cache
// ...or set in-memory cache to false
$this->objects[$msguid] = $object;
}
+
+ $this->check_error();
}
@@ -320,6 +373,8 @@ class kolab_storage_cache
// keep a copy in memory for fast access
$this->objects = array($msguid => $object);
$this->uid2msg = array($object['uid'] => $msguid);
+
+ $this->check_error();
}
@@ -328,20 +383,21 @@ class kolab_storage_cache
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
- * @param string Target IMAP folder to move it to
+ * @param object kolab_storage_folder Target storage folder instance
*/
- public function move($msguid, $uid, $target_folder)
+ public function move($msguid, $uid, $target)
{
- $target = kolab_storage::get_folder($target_folder);
+ // clear cached uid mapping and force new lookup
+ unset($target->cache->uid2msg[$uid]);
// resolve new message UID in target folder
- if ($new_msguid = $target->cache->uid2msguid($uid)) {
+ if ($this->ready && ($new_msguid = $target->cache->uid2msguid($uid))) {
$this->_read_folder_data();
$this->db->query(
"UPDATE $this->cache_table SET folder_id=?, msguid=? ".
"WHERE folder_id=? AND msguid=?",
- $target->folder_id,
+ $target->cache->get_folder_id(),
$new_msguid,
$this->folder_id,
$msguid
@@ -353,6 +409,7 @@ class kolab_storage_cache
}
unset($this->uid2msg[$uid]);
+ $this->check_error();
}
@@ -367,6 +424,7 @@ class kolab_storage_cache
"DELETE FROM $this->cache_table WHERE folder_id=?",
$this->folder_id
);
+
return $this->db->affected_rows($result);
}
@@ -377,15 +435,20 @@ class kolab_storage_cache
*/
public function rename($new_folder)
{
- $target = kolab_storage::get_folder($new_folder);
+ if ($target = kolab_storage::get_folder($new_folder)) {
+ // resolve new message UID in target folder
+ $this->db->query(
+ "UPDATE $this->folders_table SET resource=? ".
+ "WHERE resource=?",
+ $target->get_resource_uri(),
+ $this->resource_uri
+ );
- // resolve new message UID in target folder
- $this->db->query(
- "UPDATE $this->folders_table SET resource=? ".
- "WHERE resource=?",
- $target->get_resource_uri(),
- $this->resource_uri
- );
+ $this->check_error();
+ }
+ else {
+ $this->error = kolab_storage::ERROR_IMAP_CONN;
+ }
}
/**
@@ -398,30 +461,43 @@ class kolab_storage_cache
*/
public function select($query = array(), $uids = false)
{
- $result = array();
+ $result = $uids ? array() : new kolab_storage_dataset($this);
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
- $sql_result = $this->db->query(
- "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
- "WHERE folder_id=? " . $this->_sql_where($query),
- $this->folder_id
- );
+ // fetch full object data on one query if a small result set is expected
+ $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
+ $sql_query = "SELECT " . ($fetchall ? '*' : 'msguid AS _msguid, uid') . " FROM $this->cache_table ".
+ "WHERE folder_id=? " . $this->_sql_where($query);
+ if (!empty($this->order_by)) {
+ $sql_query .= ' 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)) {
- return null;
+ if ($uids) {
+ return null;
+ }
+ $result->set_error(true);
+ return $result;
}
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($uids) {
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
$result[] = $sql_arr['uid'];
}
- else if ($object = $this->_unserialize($sql_arr)) {
+ else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
+ else if (!$fetchall) {
+ // only add msguid to dataset index
+ $result[] = $sql_arr;
+ }
}
}
else {
@@ -429,7 +505,7 @@ class kolab_storage_cache
$filter = $this->_query2assoc($query);
// use 'list' for folder's default objects
- if ($filter['type'] == $this->type) {
+ if (is_array($this->index) && $filter['type'] == $this->type) {
$index = $this->index;
}
else { // search by object type
@@ -437,10 +513,6 @@ class kolab_storage_cache
$index = $this->imap->search_once($this->folder->name, $search)->get();
}
- if ($index->is_error()) {
- return null;
- }
-
// fetch all messages in $index from IMAP
$result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
@@ -452,10 +524,12 @@ class kolab_storage_cache
if (!$uids && count($result) == 1) {
if ($msguid = $result[0]['_msguid']) {
$this->uid2msg[$result[0]['uid']] = $msguid;
- $this->objects[$msguid] = $result[0];
+ $this->objects = array($msguid => $result[0]);
}
}
+ $this->check_error();
+
return $result;
}
@@ -469,7 +543,7 @@ class kolab_storage_cache
public function count($query = array())
{
// cache is in sync, we can count records in local DB
- if ($this->synched) {
+ if ($this->synched && $this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
@@ -492,15 +566,37 @@ class kolab_storage_cache
$index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
if ($index->is_error()) {
+ $this->check_error();
return null;
}
$count = $index->count();
}
+ $this->check_error();
return $count;
}
+ /**
+ * Define ORDER BY clause for cache queries
+ */
+ public function set_order_by($sortcols)
+ {
+ if (!empty($sortcols)) {
+ $this->order_by = join(', ', (array)$sortcols);
+ }
+ else {
+ $this->order_by = null;
+ }
+ }
+
+ /**
+ * Define LIMIT clause for cache queries
+ */
+ public function set_limit($length, $offset = 0)
+ {
+ $this->limit = array($length, $offset);
+ }
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
@@ -529,7 +625,7 @@ class kolab_storage_cache
$qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
}
else if ($param[0] == 'tags') {
- $param[1] = 'LIKE';
+ $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
$qvalue = $this->db->quote('% '.$param[2].' %');
}
else {
@@ -571,7 +667,7 @@ class kolab_storage_cache
*/
protected function _fetch($index, $type = null, $folder = null)
{
- $results = array();
+ $results = new kolab_storage_dataset($this);
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
@@ -659,7 +755,9 @@ class kolab_storage_cache
}
}
- $sql_data['data'] = serialize($data);
+ // use base64 encoding (Bug #1912, #2662)
+ $sql_data['data'] = base64_encode(serialize($data));
+
return $sql_data;
}
@@ -668,8 +766,23 @@ class kolab_storage_cache
*/
protected function _unserialize($sql_arr)
{
+ // check if data is a base64-encoded string, for backward compat.
+ if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
+ $sql_arr['data'] = base64_decode($sql_arr['data']);
+ }
+
$object = unserialize($sql_arr['data']);
+ // de-serialization failed
+ if ($object === false) {
+ rcube::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
+ ), true);
+
+ return null;
+ }
+
// decode binary properties
foreach ($this->binary_items as $key => $regexp) {
if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
@@ -677,12 +790,15 @@ class kolab_storage_cache
}
}
+ $object_type = $sql_arr['type'] ?: $this->folder->type;
+ $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
+
// add meta data
- $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
- $object['_msguid'] = $sql_arr['msguid'];
- $object['_mailbox'] = $this->folder->name;
- $object['_size'] = strlen($sql_arr['xml']);
- $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
+ $object['_type'] = $object_type;
+ $object['_msguid'] = $sql_arr['msguid'];
+ $object['_mailbox'] = $this->folder->name;
+ $object['_size'] = strlen($sql_arr['xml']);
+ $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
return $object;
}
@@ -785,6 +901,7 @@ class kolab_storage_cache
// abort if database is not set-up
if ($this->db->is_error()) {
+ $this->check_error();
$this->ready = false;
return;
}
@@ -819,6 +936,22 @@ class kolab_storage_cache
}
/**
+ * Check IMAP connection error state
+ */
+ protected function check_error()
+ {
+ if (($err_code = $this->imap->get_error_code()) < 0) {
+ $this->error = kolab_storage::ERROR_IMAP_CONN;
+ if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
+ $this->error = kolab_storage::ERROR_NO_PERMISSION;
+ }
+ }
+ else if ($this->db->is_error()) {
+ $this->error = kolab_storage::ERROR_CACHE_DB;
+ }
+ }
+
+ /**
* Resolve an object UID into an IMAP message UID
*
* @param string Kolab object UID
@@ -832,7 +965,7 @@ class kolab_storage_cache
$index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
$results = $index->get();
- $this->uid2msg[$uid] = $results[0];
+ $this->uid2msg[$uid] = end($results);
}
return $this->uid2msg[$uid];
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
index 8380aa8..c3c7ac4 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -37,4 +37,52 @@ class kolab_storage_cache_configuration extends kolab_storage_cache
return $sql_data;
}
-}
\ No newline at end of file
+
+ /**
+ * Select Kolab objects filtered by the given query
+ *
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * @param boolean Set true to only return UIDs instead of complete objects
+ * @return array List of Kolab data objects (each represented as hash array) or UIDs
+ */
+ public function select($query = array(), $uids = false)
+ {
+ // modify query for IMAP search: query param 'type' is actually a subtype
+ if (!$this->ready) {
+ foreach ($query as $i => $tuple) {
+ if ($tuple[0] == 'type') {
+ $tuple[2] = 'configuration.' . $tuple[2];
+ $query[$i] = $tuple;
+ }
+ }
+ }
+
+ return parent::select($query, $uids);
+ }
+
+ /**
+ * Helper method to compose a valid SQL query from pseudo filter triplets
+ */
+ protected function _sql_where($query)
+ {
+ if (is_array($query)) {
+ foreach ($query as $idx => $param) {
+ // convert category filter
+ if ($param[0] == 'category') {
+ $param[2] = array_map(function($n) { return 'category:' . $n; }, (array) $param[2]);
+
+ $query[$idx][0] = 'tags';
+ $query[$idx][2] = count($param[2]) > 1 ? $param[2] : $param[2][0];
+ }
+ // convert member filter (we support only = operator with single value)
+ else if ($param[0] == 'member') {
+ $query[$idx][0] = 'words';
+ $query[$idx][1] = '~';
+ $query[$idx][2] = '^' . $param[2] . '$';
+ }
+ }
+ }
+
+ return parent::_sql_where($query);
+ }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
index e17923d..9666a39 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -23,7 +23,7 @@
class kolab_storage_cache_contact extends kolab_storage_cache
{
- protected $extra_cols = array('type');
+ protected $extra_cols = array('type','name','firstname','surname','email');
protected $binary_items = array(
'photo' => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
'pgppublickey' => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
@@ -40,6 +40,20 @@ class kolab_storage_cache_contact extends kolab_storage_cache
$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'] = '';
+ }
+
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_config.php b/lib/plugins/libkolab/lib/kolab_storage_config.php
new file mode 100644
index 0000000..d58e3c0
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_config.php
@@ -0,0 +1,840 @@
+<?php
+
+/**
+ * Kolab storage class providing access to configuration objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at 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_config
+{
+ const FOLDER_TYPE = 'configuration';
+
+
+ /**
+ * Singleton instace of kolab_storage_config
+ *
+ * @var kolab_storage_config
+ */
+ static protected $instance;
+
+ private $folders;
+ private $default;
+ private $enabled;
+
+
+ /**
+ * This implements the 'singleton' design pattern
+ *
+ * @return kolab_storage_config The one and only instance
+ */
+ static function get_instance()
+ {
+ if (!self::$instance) {
+ self::$instance = new kolab_storage_config();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Private constructor (finds default configuration folder as a config source)
+ */
+ private function __construct()
+ {
+ // get all configuration folders
+ $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
+
+ foreach ($this->folders as $folder) {
+ if ($folder->default) {
+ $this->default = $folder;
+ break;
+ }
+ }
+
+ // if no folder is set as default, choose the first one
+ if (!$this->default) {
+ $this->default = reset($this->folders);
+ }
+
+ // attempt to create a default folder if it does not exist
+ if (!$this->default) {
+ $folder_name = 'Configuration';
+ $folder_type = self::FOLDER_TYPE . '.default';
+
+ if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
+ $this->default = new kolab_storage_folder($folder_name, $folder_type);
+ }
+ }
+
+ // check if configuration folder exist
+ if ($this->default && $this->default->name) {
+ $this->enabled = true;
+ }
+ }
+
+ /**
+ * Check wether any configuration storage (folder) exists
+ *
+ * @return bool
+ */
+ public function is_enabled()
+ {
+ return $this->enabled;
+ }
+
+ /**
+ * Get configuration objects
+ *
+ * @param array $filter Search filter
+ * @param bool $default Enable to get objects only from default folder
+ * @param int $limit Max. number of records (per-folder)
+ *
+ * @return array List of objects
+ */
+ public function get_objects($filter = array(), $default = false, $limit = 0)
+ {
+ $list = array();
+
+ foreach ($this->folders as $folder) {
+ // we only want to read from default folder
+ if ($default && !$folder->default) {
+ continue;
+ }
+
+ // for better performance it's good to assume max. number of records
+ if ($limit) {
+ $folder->set_order_and_limit(null, $limit);
+ }
+
+ foreach ($folder->select($filter) as $object) {
+ unset($object['_formatobj']);
+ $list[] = $object;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Get configuration object
+ *
+ * @param string $uid Object UID
+ * @param bool $default Enable to get objects only from default folder
+ *
+ * @return array Object data
+ */
+ public function get_object($uid, $default = false)
+ {
+ foreach ($this->folders as $folder) {
+ // we only want to read from default folder
+ if ($default && !$folder->default) {
+ continue;
+ }
+
+ if ($object = $folder->get_object($uid)) {
+ return $object;
+ }
+ }
+ }
+
+ /**
+ * Create/update configuration object
+ *
+ * @param array $object Object data
+ * @param string $type Object type
+ *
+ * @return bool True on success, False on failure
+ */
+ public function save(&$object, $type)
+ {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ $folder = $this->find_folder($object);
+
+ if ($type) {
+ $object['type'] = $type;
+ }
+
+ return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
+ }
+
+ /**
+ * Remove configuration object
+ *
+ * @param string $uid Object UID
+ *
+ * @return bool True on success, False on failure
+ */
+ public function delete($uid)
+ {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ // fetch the object to find folder
+ $object = $this->get_object($uid);
+
+ if (!$object) {
+ return false;
+ }
+
+ $folder = $this->find_folder($object);
+
+ return $folder->delete($uid);
+ }
+
+ /**
+ * Find folder
+ */
+ public function find_folder($object = array())
+ {
+ // find folder object
+ if ($object['_mailbox']) {
+ foreach ($this->folders as $folder) {
+ if ($folder->name == $object['_mailbox']) {
+ break;
+ }
+ }
+ }
+ else {
+ $folder = $this->default;
+ }
+
+ return $folder;
+ }
+
+ /**
+ * Builds relation member URI
+ *
+ * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
+ *
+ * @return string $url Member URI
+ */
+ public static function build_member_url($params)
+ {
+ // param is object UUID
+ if (is_string($params) && !empty($params)) {
+ return 'urn:uuid:' . $params;
+ }
+
+ if (empty($params) || !strlen($params['folder'])) {
+ return null;
+ }
+
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+
+ // modify folder spec. according to namespace
+ $folder = $params['folder'];
+ $ns = $storage->folder_namespace($folder);
+
+ if ($ns == 'shared') {
+ // Note: this assumes there's only one shared namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = 'shared' . substr($folder, strlen($prefix));
+ }
+ }
+ }
+ else {
+ if ($ns == 'other') {
+ // Note: this assumes there's only one other users namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = 'user' . substr($folder, strlen($prefix));
+ }
+ }
+ }
+ else {
+ $folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
+ }
+ }
+
+ $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
+
+ // build URI
+ $url = 'imap:///' . $folder;
+
+ // UID is optional here because sometimes we want
+ // to build just a member uri prefix
+ if ($params['uid']) {
+ $url .= '/' . $params['uid'];
+ }
+
+ unset($params['folder']);
+ unset($params['uid']);
+
+ if (!empty($params)) {
+ $url .= '?' . http_build_query($params, '', '&');
+ }
+
+ return $url;
+ }
+
+ /**
+ * Parses relation member string
+ *
+ * @param string $url Member URI
+ *
+ * @return array Message folder, UID, Search headers (Message-Id, Date)
+ */
+ public static function parse_member_url($url)
+ {
+ // Look for IMAP URI:
+ // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
+ if (strpos($url, 'imap:///') === 0) {
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+
+ // parse_url does not work with imap:/// prefix
+ $url = parse_url(substr($url, 8));
+ $path = explode('/', $url['path']);
+ parse_str($url['query'], $params);
+
+ $uid = array_pop($path);
+ $ns = array_shift($path);
+ $path = array_map('rawurldecode', $path);
+
+ // resolve folder name
+ if ($ns == 'shared') {
+ $folder = implode('/', $path);
+ // Note: this assumes there's only one shared namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = $prefix . '/' . $folder;
+ }
+ }
+ }
+ else if ($ns == 'user') {
+ $username = array_shift($path);
+ $folder = implode('/', $path);
+
+ if ($username != $rcube->get_user_name()) {
+ // Note: this assumes there's only one other users namespace root
+ if ($ns = $storage->get_namespace('other')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = $prefix . '/' . $username . '/' . $folder;
+ }
+ }
+ }
+ else if (!strlen($folder)) {
+ $folder = 'INBOX';
+ }
+ }
+ else {
+ return;
+ }
+
+ return array(
+ 'folder' => $folder,
+ 'uid' => $uid,
+ 'params' => $params,
+ );
+ }
+
+ return false;
+ }
+
+ /**
+ * Build array of member URIs from set of messages
+ *
+ * @param string $folder Folder name
+ * @param array $messages Array of rcube_message objects
+ *
+ * @return array List of members (IMAP URIs)
+ */
+ public static function build_members($folder, $messages)
+ {
+ $members = array();
+
+ foreach ((array) $messages as $msg) {
+ $params = array(
+ 'folder' => $folder,
+ 'uid' => $msg->uid,
+ );
+
+ // add search parameters:
+ // we don't want to build "invalid" searches e.g. that
+ // will return false positives (more or wrong messages)
+ if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
+ $params['message-id'] = $messageid;
+ $params['date'] = $date;
+
+ if ($subject = $msg->get('subject', false)) {
+ $params['subject'] = substr($subject, 0, 256);
+ }
+ }
+
+ $members[] = self::build_member_url($params);
+ }
+
+ return $members;
+ }
+
+ /**
+ * Resolve/validate/update members (which are IMAP URIs) of relation object.
+ *
+ * @param array $tag Tag object
+ * @param bool $force Force members list update
+ *
+ * @return array Folder/UIDs list
+ */
+ public static function resolve_members(&$tag, $force = true)
+ {
+ $result = array();
+
+ foreach ((array) $tag['members'] as $member) {
+ // IMAP URI members
+ if ($url = self::parse_member_url($member)) {
+ $folder = $url['folder'];
+
+ if (!$force) {
+ $result[$folder][] = $url['uid'];
+ }
+ else {
+ $result[$folder]['uid'][] = $url['uid'];
+ $result[$folder]['params'][] = $url['params'];
+ $result[$folder]['member'][] = $member;
+ }
+ }
+ }
+
+ if (empty($result) || !$force) {
+ return $result;
+ }
+
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+ $search = array();
+ $missing = array();
+
+ // first we search messages by Folder+UID
+ foreach ($result as $folder => $data) {
+ // @FIXME: maybe better use index() which is cached?
+ // @TODO: consider skip_deleted option
+ $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
+ $uids = $index->get();
+
+ // messages that were not found need to be searched by search parameters
+ $not_found = array_diff($data['uid'], $uids);
+ if (!empty($not_found)) {
+ foreach ($not_found as $uid) {
+ $idx = array_search($uid, $data['uid']);
+
+ if ($p = $data['params'][$idx]) {
+ $search[] = $p;
+ }
+
+ $missing[] = $result[$folder]['member'][$idx];
+
+ unset($result[$folder]['uid'][$idx]);
+ unset($result[$folder]['params'][$idx]);
+ unset($result[$folder]['member'][$idx]);
+ }
+ }
+
+ $result[$folder] = $uids;
+ }
+
+ // search in all subscribed mail folders using search parameters
+ if (!empty($search)) {
+ // remove not found members from the members list
+ $tag['members'] = array_diff($tag['members'], $missing);
+
+ // get subscribed folders
+ $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
+
+ // @TODO: do this search in chunks (for e.g. 10 messages)?
+ $search_str = '';
+
+ foreach ($search as $p) {
+ $search_params = array();
+ foreach ($p as $key => $val) {
+ $key = strtoupper($key);
+ // don't search by subject, we don't want false-positives
+ if ($key != 'SUBJECT') {
+ $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
+ }
+ }
+
+ $search_str .= ' (' . implode(' ', $search_params) . ')';
+ }
+
+ $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
+
+ // search
+ $search = $storage->search_once($folders, $search_str);
+
+ // handle search result
+ $folders = (array) $search->get_parameters('MAILBOX');
+
+ foreach ($folders as $folder) {
+ $set = $search->get_set($folder);
+ $uids = $set->get();
+
+ if (!empty($uids)) {
+ $msgs = $storage->fetch_headers($folder, $uids, false);
+ $members = self::build_members($folder, $msgs);
+
+ // merge new members into the tag members list
+ $tag['members'] = array_merge($tag['members'], $members);
+
+ // add UIDs into the result
+ $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
+ }
+ }
+
+ // update tag object with new members list
+ $tag['members'] = array_unique($tag['members']);
+ kolab_storage_config::get_instance()->save($tag, 'relation', false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Assign tags to kolab objects
+ *
+ * @param array $records List of kolab objects
+ *
+ * @return array List of tags
+ */
+ public function apply_tags(&$records)
+ {
+ // first convert categories into tags
+ foreach ($records as $i => $rec) {
+ if (!empty($rec['categories'])) {
+ $folder = new kolab_storage_folder($rec['_mailbox']);
+ if ($object = $folder->get_object($rec['uid'])) {
+ $tags = $rec['categories'];
+
+ unset($object['categories']);
+ unset($records[$i]['categories']);
+
+ $this->save_tags($rec['uid'], $tags);
+ $folder->save($object, $rec['_type'], $rec['uid']);
+ }
+ }
+ }
+
+ $tags = array();
+
+ // assign tags to objects
+ foreach ($this->get_tags() as $tag) {
+ foreach ($records as $idx => $rec) {
+ $uid = self::build_member_url($rec['uid']);
+ if (in_array($uid, (array) $tag['members'])) {
+ $records[$idx]['tags'][] = $tag['name'];
+ }
+ }
+
+ $tags[] = $tag['name'];
+ }
+
+ $tags = array_unique($tags);
+
+ return $tags;
+ }
+
+ /**
+ * Update object tags
+ *
+ * @param string $uid Kolab object UID
+ * @param array $tags List of tag names
+ */
+ public function save_tags($uid, $tags)
+ {
+ $url = self::build_member_url($uid);
+ $relations = $this->get_tags();
+
+ foreach ($relations as $idx => $relation) {
+ $selected = !empty($tags) && in_array($relation['name'], $tags);
+ $found = !empty($relation['members']) && in_array($url, $relation['members']);
+ $update = false;
+
+ // remove member from the relation
+ if ($found && !$selected) {
+ $relation['members'] = array_diff($relation['members'], (array) $url);
+ $update = true;
+ }
+ // add member to the relation
+ else if (!$found && $selected) {
+ $relation['members'][] = $url;
+ $update = true;
+ }
+
+ if ($update) {
+ if ($this->save($relation, 'relation')) {
+ $this->tags[$idx] = $relation; // update in-memory cache
+ }
+ }
+
+ if ($selected) {
+ $tags = array_diff($tags, (array)$relation['name']);
+ }
+ }
+
+ // create new relations
+ if (!empty($tags)) {
+ foreach ($tags as $tag) {
+ $relation = array(
+ 'name' => $tag,
+ 'members' => (array) $url,
+ 'category' => 'tag',
+ );
+
+ if ($this->save($relation, 'relation')) {
+ $this->tags[] = $relation; // update in-memory cache
+ }
+ }
+ }
+ }
+
+ /**
+ * Get tags (all or referring to specified object)
+ *
+ * @param string $uid Optional object UID
+ *
+ * @return array List of Relation objects
+ */
+ public function get_tags($uid = '*')
+ {
+ if (!isset($this->tags)) {
+ $default = true;
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'tag')
+ );
+
+ // use faster method
+ if ($uid && $uid != '*') {
+ $filter[] = array('member', '=', $uid);
+ $tags = $this->get_objects($filter, $default);
+ }
+ else {
+ $this->tags = $tags = $this->get_objects($filter, $default);
+ }
+ }
+ else {
+ $tags = $this->tags;
+ }
+
+ if ($uid === '*') {
+ return $tags;
+ }
+
+ $result = array();
+ $search = self::build_member_url($uid);
+
+ foreach ($tags as $tag) {
+ if (in_array($search, (array) $tag['members'])) {
+ $result[] = $tag;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Find objects linked with the given groupware object through a relation
+ *
+ * @param string Object UUID
+ * @param array List of related URIs
+ */
+ public function get_object_links($uid)
+ {
+ $links = array();
+ $object_uri = self::build_member_url($uid);
+
+ foreach ($this->get_relations_for_member($uid) as $relation) {
+ if (in_array($object_uri, (array) $relation['members'])) {
+ // make relation members up-to-date
+ kolab_storage_config::resolve_members($relation);
+
+ foreach ($relation['members'] as $member) {
+ if ($member != $object_uri) {
+ $links[] = $member;
+ }
+ }
+ }
+ }
+
+ return array_unique($links);
+ }
+
+ /**
+ *
+ */
+ public function save_object_links($uid, $links, $remove = array())
+ {
+ $object_uri = self::build_member_url($uid);
+ $relations = $this->get_relations_for_member($uid);
+ $done = false;
+
+ foreach ($relations as $relation) {
+ // make relation members up-to-date
+ kolab_storage_config::resolve_members($relation);
+
+ // remove and add links
+ $members = array_diff($relation['members'], (array)$remove);
+ $members = array_unique(array_merge($members, $links));
+
+ // make sure the object_uri is still a member
+ if (!in_array($object_uri, $members)) {
+ $members[$object_uri];
+ }
+
+ // remove relation if no other members remain
+ if (count($members) <= 1) {
+ $done = $this->delete($relation['uid']);
+ }
+ // update relation object if members changed
+ else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
+ $relation['members'] = $members;
+ $done = $this->save($relation, 'relation');
+ $links = array();
+ }
+ // no changes, we're happy
+ else {
+ $done = true;
+ $links = array();
+ }
+ }
+
+ // create a new relation
+ if (!$done && !empty($links)) {
+ $relation = array(
+ 'members' => array_merge($links, array($object_uri)),
+ 'category' => 'generic',
+ );
+
+ $ret = $this->save($relation, 'relation');
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Find relation objects referring to specified note
+ */
+ public function get_relations_for_member($uid, $reltype = 'generic')
+ {
+ $default = true;
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', $reltype),
+ array('member', '=', $uid),
+ );
+
+ return $this->get_objects($filter, $default, 100);
+ }
+
+ /**
+ * Find kolab objects assigned to specified e-mail message
+ *
+ * @param rcube_message $message E-mail message
+ * @param string $folder Folder name
+ * @param string $type Result objects type
+ *
+ * @return array List of kolab objects
+ */
+ public function get_message_relations($message, $folder, $type)
+ {
+ static $_cache = array();
+
+ $result = array();
+ $uids = array();
+ $default = true;
+ $uri = self::get_message_uri($message, $folder);
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'generic'),
+ );
+
+ // query by message-id
+ $member_id = $message->get('message-id', false);
+ if (empty($member_id)) {
+ // derive message identifier from URI
+ $member_id = md5($uri);
+ }
+ $filter[] = array('member', '=', $member_id);
+
+ if (!isset($_cache[$uri])) {
+ // get UIDs of related groupware objects
+ foreach ($this->get_objects($filter, $default) as $relation) {
+ // we don't need to update members if the URI is found
+ if (!in_array($uri, $relation['members'])) {
+ // update members...
+ $messages = kolab_storage_config::resolve_members($relation);
+ // ...and check again
+ if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
+ continue;
+ }
+ }
+
+ // find groupware object UID(s)
+ foreach ($relation['members'] as $member) {
+ if (strpos($member, 'urn:uuid:') === 0) {
+ $uids[] = substr($member, 9);
+ }
+ }
+ }
+
+ // remember this lookup
+ $_cache[$uri] = $uids;
+ }
+ else {
+ $uids = $_cache[$uri];
+ }
+
+ // get kolab objects of specified type
+ if (!empty($uids)) {
+ $query = array(array('uid', '=', array_unique($uids)));
+ $result = kolab_storage::select($query, $type);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Build a URI representing the given message reference
+ */
+ public static function get_message_uri($headers, $folder)
+ {
+ $params = array(
+ 'folder' => $headers->folder ?: $folder,
+ 'uid' => $headers->uid,
+ );
+
+ if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
+ $params['message-id'] = $messageid;
+ $params['date'] = $date;
+
+ if ($subject = $headers->get('subject')) {
+ $params['subject'] = $subject;
+ }
+ }
+
+ return self::build_member_url($params);
+ }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_dataset.php b/lib/plugins/libkolab/lib/kolab_storage_dataset.php
new file mode 100644
index 0000000..9ddf3f9
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * Dataset class providing the results of a select operation on a kolab_storage_folder.
+ *
+ * Can be used as a normal array as well as an iterator in foreach() loops.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at 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_dataset implements Iterator, ArrayAccess, Countable
+{
+ private $cache; // kolab_storage_cache instance to use for fetching data
+ private $memlimit = 0;
+ private $buffer = false;
+ private $index = array();
+ private $data = array();
+ private $iteratorkey = 0;
+ private $error = null;
+
+ /**
+ * Default constructor
+ *
+ * @param object kolab_storage_cache instance to be used for fetching objects upon access
+ */
+ public function __construct($cache)
+ {
+ $this->cache = $cache;
+
+ // enable in-memory buffering up until 1/5 of the available memory
+ if (function_exists('memory_get_usage')) {
+ $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
+ $this->buffer = true;
+ }
+ }
+
+ /**
+ * Return error state
+ */
+ public function is_error()
+ {
+ return !empty($this->error);
+ }
+
+ /**
+ * Set error state
+ */
+ public function set_error($err)
+ {
+ $this->error = $err;
+ }
+
+
+ /*** Implement PHP Countable interface ***/
+
+ public function count()
+ {
+ return count($this->index);
+ }
+
+
+ /*** Implement PHP ArrayAccess interface ***/
+
+ public function offsetSet($offset, $value)
+ {
+ $uid = $value['_msguid'];
+
+ if (is_null($offset)) {
+ $offset = count($this->index);
+ $this->index[] = $uid;
+ }
+ else {
+ $this->index[$offset] = $uid;
+ }
+
+ // keep full payload data in memory if possible
+ if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
+ $this->data[$offset] = $value;
+
+ // check memory usage and stop buffering
+ if ($offset % 10 == 0) {
+ $this->buffer = memory_get_usage() < $this->memlimit;
+ }
+ }
+ }
+
+ public function offsetExists($offset)
+ {
+ return isset($this->index[$offset]);
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->index[$offset]);
+ }
+
+ public function offsetGet($offset)
+ {
+ if (isset($this->data[$offset])) {
+ return $this->data[$offset];
+ }
+ else if ($msguid = $this->index[$offset]) {
+ return $this->cache->get($msguid);
+ }
+
+ return null;
+ }
+
+
+ /*** Implement PHP Iterator interface ***/
+
+ public function current()
+ {
+ return $this->offsetGet($this->iteratorkey);
+ }
+
+ public function key()
+ {
+ return $this->iteratorkey;
+ }
+
+ public function next()
+ {
+ $this->iteratorkey++;
+ return $this->valid();
+ }
+
+ public function rewind()
+ {
+ $this->iteratorkey = 0;
+ }
+
+ public function valid()
+ {
+ return !empty($this->index[$this->iteratorkey]);
+ }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 74d95b7..f7370bb 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -48,7 +48,13 @@ class kolab_storage_folder
*/
public $cache;
- private $type_annotation;
+ /**
+ * Indicate validity status
+ * @var boolean
+ */
+ public $valid = false;
+
+ private $error = 0;
private $namespace;
private $imap;
private $info;
@@ -59,12 +65,15 @@ class kolab_storage_folder
/**
* Default constructor
+ *
+ * @param string The folder name/path
+ * @param string Expected folder type
*/
- function __construct($name, $type = null)
+ function __construct($name, $type = null, $type_annotation = null)
{
$this->imap = rcube::get_instance()->get_storage();
$this->imap->set_options(array('skip_deleted' => true));
- $this->set_folder($name, $type);
+ $this->set_folder($name, $type, $type_annotation);
}
@@ -74,24 +83,43 @@ class kolab_storage_folder
* @param string The folder name/path
* @param string Optional folder type if known
*/
- public function set_folder($name, $ftype = null)
+ public function set_folder($name, $type = null, $type_annotation = null)
{
- $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
+ if (empty($type_annotation)) {
+ $type_annotation = kolab_storage::folder_type($name);
+ }
$oldtype = $this->type;
- list($this->type, $suffix) = explode('.', $this->type_annotation);
+ list($this->type, $suffix) = explode('.', $type_annotation);
$this->default = $suffix == 'default';
$this->name = $name;
- $this->resource_uri = null;
+ $this->valid = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);
+
+ if (!$this->valid) {
+ $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
+ }
+
+ // reset cached object properties
+ $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
// get a new cache instance of folder type changed
- if (!$this->cache || $type != $oldtype)
+ if (!$this->cache || $this->type != $oldtype)
$this->cache = kolab_storage_cache::factory($this);
+ else
+ $this->cache->set_folder($this);
$this->imap->set_folder($this->name);
- $this->cache->set_folder($this);
}
+ /**
+ * Returns code of last error
+ *
+ * @return int Error code
+ */
+ public function get_error()
+ {
+ return $this->error ?: $this->cache->get_error();
+ }
/**
*
@@ -143,9 +171,10 @@ class kolab_storage_folder
/**
* Returns the owner of the folder.
*
+ * @param boolean Return a fully qualified owner name (i.e. including domain for shared folders)
* @return string The owner of this folder.
*/
- public function get_owner()
+ public function get_owner($fully_qualified = false)
{
// return cached value
if (isset($this->owner))
@@ -164,16 +193,21 @@ class kolab_storage_folder
break;
default:
- list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
- if (strpos($user, '@') === false) {
- $domain = strstr($rcmail->get_user_name(), '@');
- if (!empty($domain))
- $user .= $domain;
- }
- $this->owner = $user;
+ list($prefix, $this->owner) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+ $fully_qualified = true; // enforce email addresses (backwards compatibility)
break;
}
+ if ($fully_qualified && strpos($this->owner, '@') === false) {
+ // extract domain from current user name
+ $domain = strstr($rcmail->get_user_name(), '@');
+ // fall back to mail_domain config option
+ if (empty($domain) && ($mdomain = $rcmail->config->mail_domain($this->imap->options['host']))) {
+ $domain = '@' . $mdomain;
+ }
+ $this->owner .= $domain;
+ }
+
return $this->owner;
}
@@ -259,7 +293,7 @@ class kolab_storage_folder
}
// compose fully qualified ressource uri for this instance
- $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+ $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
return $this->resource_uri;
}
@@ -280,7 +314,7 @@ class kolab_storage_folder
}
// generate a folder UID and set it to IMAP
- $uid = rtrim(chunk_split(md5($this->name . $this->get_owner()), 12, '-'), '-');
+ $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
$this->set_uid($uid);
return $uid;
@@ -363,6 +397,10 @@ class kolab_storage_folder
*/
public function count($query = null)
{
+ if (!$this->valid) {
+ return 0;
+ }
+
// synchronize cache first
$this->cache->synchronize();
@@ -380,6 +418,10 @@ class kolab_storage_folder
{
if (!$type) $type = $this->type;
+ if (!$this->valid) {
+ return array();
+ }
+
// synchronize caches
$this->cache->synchronize();
@@ -397,9 +439,14 @@ class kolab_storage_folder
*/
public function select($query = array())
{
+ if (!$this->valid) {
+ return array();
+ }
+
// check query argument
- if (empty($query))
+ if (empty($query)) {
return $this->get_objects();
+ }
// synchronize caches
$this->cache->synchronize();
@@ -417,6 +464,10 @@ class kolab_storage_folder
*/
public function get_uids($query = array())
{
+ if (!$this->valid) {
+ return array();
+ }
+
// synchronize caches
$this->cache->synchronize();
@@ -424,6 +475,21 @@ class kolab_storage_folder
return $this->cache->select($this->_prepare_query($query), true);
}
+ /**
+ * Setter for ORDER BY and LIMIT parameters for cache queries
+ *
+ * @param array List of columns to order by
+ * @param integer Limit result set to this length
+ * @param integer Offset row
+ */
+ public function set_order_and_limit($sortcols, $length = null, $offset = 0)
+ {
+ $this->cache->set_order_by($sortcols);
+
+ if ($length !== null) {
+ $this->cache->set_limit($length, $offset);
+ }
+ }
/**
* Helper method to sanitize query arguments
@@ -463,6 +529,10 @@ class kolab_storage_folder
*/
public function get_object($uid, $type = null)
{
+ if (!$this->valid) {
+ return false;
+ }
+
// synchronize caches
$this->cache->synchronize();
@@ -492,7 +562,7 @@ class kolab_storage_folder
*/
public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
{
- if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
+ if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
}
@@ -513,6 +583,10 @@ class kolab_storage_folder
*/
public function read_object($msguid, $type = null, $folder = null)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if (!$type) $type = $this->type;
if (!$folder) $folder = $this->name;
@@ -658,6 +732,10 @@ class kolab_storage_folder
*/
public function save(&$object, $type = null, $uid = null)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if (!$type)
$type = $this->type;
@@ -850,6 +928,10 @@ class kolab_storage_folder
*/
public function delete($object, $expunge = true)
{
+ if (!$this->valid) {
+ return false;
+ }
+
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
@@ -877,6 +959,10 @@ class kolab_storage_folder
*/
public function delete_all()
{
+ if (!$this->valid) {
+ return false;
+ }
+
$this->cache->purge();
$this->cache->bypass(true);
$result = $this->imap->clear_folder($this->name);
@@ -894,6 +980,10 @@ class kolab_storage_folder
*/
public function undelete($uid)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if ($msguid = $this->cache->uid2msguid($uid, true)) {
$this->cache->bypass(true);
$result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
@@ -917,9 +1007,16 @@ class kolab_storage_folder
*/
public function move($uid, $target_folder)
{
+ if (!$this->valid) {
+ return false;
+ }
+
+ if (is_string($target_folder))
+ $target_folder = kolab_storage::get_folder($target_folder);
+
if ($msguid = $this->cache->uid2msguid($uid)) {
$this->cache->bypass(true);
- $result = $this->imap->move_message($msguid, $target_folder, $this->name);
+ $result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
$this->cache->bypass(false);
if ($result) {
@@ -1169,8 +1266,6 @@ class kolab_storage_folder
*/
private function trigger_url($url, $auth_user = null, $auth_passwd = null)
{
- require_once('HTTP/Request2.php');
-
try {
$request = libkolab::http_request($url);
More information about the commits
mailing list