diff --git a/plugins/libkolab/SQL/mysql.initial.sql b/plugins/libkolab/SQL/mysql.initial.sql index 0213c45b..80da02fa 100644 --- a/plugins/libkolab/SQL/mysql.initial.sql +++ b/plugins/libkolab/SQL/mysql.initial.sql @@ -1,191 +1,183 @@ /** * libkolab database schema * * @version 1.2 * @author Thomas Bruederli * @licence GNU AGPL **/ /*!40014 SET FOREIGN_KEY_CHECKS=0 */; DROP TABLE IF EXISTS `kolab_folders`; CREATE TABLE `kolab_folders` ( `folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `resource` VARCHAR(255) NOT NULL, `type` VARCHAR(32) NOT NULL, `synclock` INT(10) NOT NULL DEFAULT '0', `ctag` VARCHAR(40) DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `objectcount` BIGINT DEFAULT NULL, PRIMARY KEY(`folder_id`), INDEX `resource_type` (`resource`, `type`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache`; DROP TABLE IF EXISTS `kolab_cache_contact`; CREATE TABLE `kolab_cache_contact` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `type` VARCHAR(32) CHARACTER SET ascii NOT NULL, `name` VARCHAR(255) NOT NULL, `firstname` VARCHAR(255) NOT NULL, `surname` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `contact_type` (`folder_id`,`type`), INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_event`; CREATE TABLE `kolab_cache_event` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `dtstart` DATETIME, `dtend` DATETIME, CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_task`; CREATE TABLE `kolab_cache_task` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `dtstart` DATETIME, `dtend` DATETIME, CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_journal`; CREATE TABLE `kolab_cache_journal` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `dtstart` DATETIME, `dtend` DATETIME, CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_note`; CREATE TABLE `kolab_cache_note` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_file`; CREATE TABLE `kolab_cache_file` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `filename` varchar(255) DEFAULT NULL, CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `folder_filename` (`folder_id`, `filename`), INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_configuration`; CREATE TABLE `kolab_cache_configuration` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `type` VARCHAR(32) CHARACTER SET ascii NOT NULL, CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `configuration_type` (`folder_id`,`type`), INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; DROP TABLE IF EXISTS `kolab_cache_freebusy`; CREATE TABLE `kolab_cache_freebusy` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, - `xml` LONGBLOB NOT NULL, `tags` TEXT NOT NULL, `words` TEXT NOT NULL, `dtstart` DATETIME, `dtend` DATETIME, CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`) REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY(`folder_id`,`msguid`), INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; /*!40014 SET FOREIGN_KEY_CHECKS=1 */; -REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2018021300'); +REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2018122700'); diff --git a/plugins/libkolab/SQL/mysql/2018122700.sql b/plugins/libkolab/SQL/mysql/2018122700.sql new file mode 100644 index 00000000..08030dd0 --- /dev/null +++ b/plugins/libkolab/SQL/mysql/2018122700.sql @@ -0,0 +1,10 @@ +-- remove xml column, and change data format (clear cache needed) +DELETE FROM `kolab_folders`; +ALTER TABLE `kolab_cache_contact` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_event` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_task` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_journal` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_note` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_file` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_configuration` DROP COLUMN `xml`; +ALTER TABLE `kolab_cache_freebusy` DROP COLUMN `xml`; diff --git a/plugins/libkolab/SQL/oracle.initial.sql b/plugins/libkolab/SQL/oracle.initial.sql index 78675783..ebba60dc 100644 --- a/plugins/libkolab/SQL/oracle.initial.sql +++ b/plugins/libkolab/SQL/oracle.initial.sql @@ -1,186 +1,178 @@ /** * libkolab database schema * * @version 1.2 * @author Aleksander Machniak * @licence GNU AGPL **/ CREATE TABLE "kolab_folders" ( "folder_id" number NOT NULL PRIMARY KEY, "resource" VARCHAR(255) NOT NULL, "type" VARCHAR(32) NOT NULL, "synclock" integer DEFAULT 0 NOT NULL, "ctag" VARCHAR(40) DEFAULT NULL, "changed" timestamp DEFAULT NULL, "objectcount" number DEFAULT NULL ); CREATE INDEX "kolab_folders_resource_idx" ON "kolab_folders" ("resource", "type"); CREATE SEQUENCE "kolab_folders_seq" START WITH 1 INCREMENT BY 1 NOMAXVALUE; CREATE TRIGGER "kolab_folders_seq_trig" BEFORE INSERT ON "kolab_folders" FOR EACH ROW BEGIN :NEW."folder_id" := "kolab_folders_seq".nextval; END; / CREATE TABLE "kolab_cache_contact" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "type" varchar(32) NOT NULL, "name" varchar(255) DEFAULT NULL, "firstname" varchar(255) DEFAULT NULL, "surname" varchar(255) DEFAULT NULL, "email" varchar(255) DEFAULT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_contact_type_idx" ON "kolab_cache_contact" ("folder_id", "type"); CREATE INDEX "kolab_cache_contact_uid2msguid" ON "kolab_cache_contact" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_event" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "dtstart" timestamp DEFAULT NULL, "dtend" timestamp DEFAULT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_event_uid2msguid" ON "kolab_cache_event" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_task" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "dtstart" timestamp DEFAULT NULL, "dtend" timestamp DEFAULT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_task_uid2msguid" ON "kolab_cache_task" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_journal" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "dtstart" timestamp DEFAULT NULL, "dtend" timestamp DEFAULT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_journal_uid2msguid" ON "kolab_cache_journal" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_note" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_note_uid2msguid" ON "kolab_cache_note" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_file" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "filename" varchar(255) DEFAULT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_file_filename" ON "kolab_cache_file" ("folder_id", "filename"); CREATE INDEX "kolab_cache_file_uid2msguid" ON "kolab_cache_file" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_configuration" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "type" varchar(32) NOT NULL, PRIMARY KEY ("folder_id", "msguid") ); CREATE INDEX "kolab_cache_config_type" ON "kolab_cache_configuration" ("folder_id", "type"); CREATE INDEX "kolab_cache_config_uid2msguid" ON "kolab_cache_configuration" ("folder_id", "uid", "msguid"); CREATE TABLE "kolab_cache_freebusy" ( "folder_id" number NOT NULL REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE, "msguid" number NOT NULL, "uid" varchar(512) NOT NULL, "created" timestamp DEFAULT NULL, "changed" timestamp DEFAULT NULL, "data" clob NOT NULL, - "xml" clob NOT NULL, "tags" clob DEFAULT NULL, "words" clob DEFAULT NULL, "dtstart" timestamp DEFAULT NULL, "dtend" timestamp DEFAULT NULL, PRIMARY KEY("folder_id", "msguid") ); CREATE INDEX "kolab_cache_fb_uid2msguid" ON "kolab_cache_freebusy" ("folder_id", "uid", "msguid"); -INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2018021300'); +INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2018122700'); diff --git a/plugins/libkolab/SQL/oracle/2018122700.sql b/plugins/libkolab/SQL/oracle/2018122700.sql new file mode 100644 index 00000000..45fccb74 --- /dev/null +++ b/plugins/libkolab/SQL/oracle/2018122700.sql @@ -0,0 +1,10 @@ +-- remove xml column, and change data format (clear cache needed) +DELETE FROM "kolab_folders"; +ALTER TABLE "kolab_cache_contact" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_event" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_task" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_journal" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_note" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_file" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_configuration" DROP COLUMN "xml"; +ALTER TABLE "kolab_cache_freebusy" DROP COLUMN "xml"; diff --git a/plugins/libkolab/SQL/sqlite.initial.sql b/plugins/libkolab/SQL/sqlite.initial.sql index 909188ca..815316dc 100644 --- a/plugins/libkolab/SQL/sqlite.initial.sql +++ b/plugins/libkolab/SQL/sqlite.initial.sql @@ -1,159 +1,151 @@ /** * libkolab database schema * * @version 1.2 * @author Thomas Bruederli * @licence GNU AGPL **/ CREATE TABLE kolab_folders ( folder_id INTEGER NOT NULL PRIMARY KEY, resource VARCHAR(255) NOT NULL, type VARCHAR(32) NOT NULL, synclock INTEGER NOT NULL DEFAULT '0', ctag VARCHAR(40) DEFAULT NULL, changed DATETIME DEFAULT NULL, objectcount INTEGER DEFAULT NULL ); CREATE INDEX ix_resource_type ON kolab_folders(resource, type); CREATE TABLE kolab_cache_contact ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, type VARCHAR(32) NOT NULL, name VARCHAR(255) NOT NULL, firstname VARCHAR(255) NOT NULL, surname VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_contact_type ON kolab_cache_contact(folder_id,type); CREATE INDEX ix_contact_uid2msguid ON kolab_cache_contact(folder_id,uid,msguid); CREATE TABLE kolab_cache_event ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_event_uid2msguid ON kolab_cache_event(folder_id,uid,msguid); CREATE TABLE kolab_cache_task ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_task_uid2msguid ON kolab_cache_task(folder_id,uid,msguid); CREATE TABLE kolab_cache_journal ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_journal_uid2msguid ON kolab_cache_journal(folder_id,uid,msguid); CREATE TABLE kolab_cache_note ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_note_uid2msguid ON kolab_cache_note(folder_id,uid,msguid); CREATE TABLE kolab_cache_file ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, filename varchar(255) DEFAULT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_folder_filename ON kolab_cache_file(folder_id,filename); CREATE INDEX ix_file_uid2msguid ON kolab_cache_file(folder_id,uid,msguid); CREATE TABLE kolab_cache_configuration ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, type VARCHAR(32) NOT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_configuration_type ON kolab_cache_configuration(folder_id,type); CREATE INDEX ix_configuration_uid2msguid ON kolab_cache_configuration(folder_id,uid,msguid); CREATE TABLE kolab_cache_freebusy ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_freebusy_uid2msguid ON kolab_cache_freebusy(folder_id,uid,msguid); -INSERT INTO system (name, value) VALUES ('libkolab-version', '2018021300'); +INSERT INTO system (name, value) VALUES ('libkolab-version', '2018122700'); diff --git a/plugins/libkolab/SQL/sqlite.initial.sql b/plugins/libkolab/SQL/sqlite/2018122700.sql similarity index 83% copy from plugins/libkolab/SQL/sqlite.initial.sql copy to plugins/libkolab/SQL/sqlite/2018122700.sql index 909188ca..ee24c1a6 100644 --- a/plugins/libkolab/SQL/sqlite.initial.sql +++ b/plugins/libkolab/SQL/sqlite/2018122700.sql @@ -1,159 +1,137 @@ -/** - * libkolab database schema - * - * @version 1.2 - * @author Thomas Bruederli - * @licence GNU AGPL - **/ - -CREATE TABLE kolab_folders ( - folder_id INTEGER NOT NULL PRIMARY KEY, - resource VARCHAR(255) NOT NULL, - type VARCHAR(32) NOT NULL, - synclock INTEGER NOT NULL DEFAULT '0', - ctag VARCHAR(40) DEFAULT NULL, - changed DATETIME DEFAULT NULL, - objectcount INTEGER DEFAULT NULL -); - -CREATE INDEX ix_resource_type ON kolab_folders(resource, type); - +DROP TABLE kolab_cache_contact; CREATE TABLE kolab_cache_contact ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, type VARCHAR(32) NOT NULL, name VARCHAR(255) NOT NULL, firstname VARCHAR(255) NOT NULL, surname VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_contact_type ON kolab_cache_contact(folder_id,type); CREATE INDEX ix_contact_uid2msguid ON kolab_cache_contact(folder_id,uid,msguid); +DROP TABLE kolab_cache_event; CREATE TABLE kolab_cache_event ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_event_uid2msguid ON kolab_cache_event(folder_id,uid,msguid); +DROP TABLE kolab_cache_task; CREATE TABLE kolab_cache_task ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_task_uid2msguid ON kolab_cache_task(folder_id,uid,msguid); +DROP TABLE kolab_cache_journal; CREATE TABLE kolab_cache_journal ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_journal_uid2msguid ON kolab_cache_journal(folder_id,uid,msguid); +DROP TABLE kolab_cache_note; CREATE TABLE kolab_cache_note ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_note_uid2msguid ON kolab_cache_note(folder_id,uid,msguid); +DROP TABLE kolab_cache_file; CREATE TABLE kolab_cache_file ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, filename varchar(255) DEFAULT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_folder_filename ON kolab_cache_file(folder_id,filename); CREATE INDEX ix_file_uid2msguid ON kolab_cache_file(folder_id,uid,msguid); +DROP TABLE kolab_cache_configuration; CREATE TABLE kolab_cache_configuration ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, type VARCHAR(32) NOT NULL, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_configuration_type ON kolab_cache_configuration(folder_id,type); CREATE INDEX ix_configuration_uid2msguid ON kolab_cache_configuration(folder_id,uid,msguid); +DROP TABLE kolab_cache_freebusy; CREATE TABLE kolab_cache_freebusy ( folder_id INTEGER NOT NULL, msguid INTEGER NOT NULL, uid VARCHAR(512) NOT NULL, created DATETIME DEFAULT NULL, changed DATETIME DEFAULT NULL, data TEXT NOT NULL, - xml TEXT NOT NULL, tags TEXT NOT NULL, words TEXT NOT NULL, dtstart DATETIME, dtend DATETIME, PRIMARY KEY(folder_id,msguid) ); CREATE INDEX ix_freebusy_uid2msguid ON kolab_cache_freebusy(folder_id,uid,msguid); - -INSERT INTO system (name, value) VALUES ('libkolab-version', '2018021300'); diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php index df851d6d..a0d360ed 100644 --- a/plugins/libkolab/lib/kolab_storage_cache.php +++ b/plugins/libkolab/lib/kolab_storage_cache.php @@ -1,1289 +1,1268 @@ * * Copyright (C) 2012-2013, Kolab Systems AG * * 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 . */ class kolab_storage_cache { const DB_DATE_FORMAT = 'Y-m-d H:i:s'; const MAX_RECORDS = 500; public $sync_complete = false; protected $db; protected $imap; protected $folder; protected $uid2msg; protected $objects; protected $metadata = array(); protected $folder_id; protected $resource_uri; protected $enabled = true; protected $synched = false; protected $synclock = false; protected $ready = false; protected $cache_table; protected $cache_refresh = 3600; protected $folders_table; protected $max_sql_packet; protected $max_sync_lock_time = 600; - protected $binary_items = array(); protected $extra_cols = array(); protected $data_props = array(); protected $order_by = null; protected $limit = null; protected $error = 0; protected $server_timezone; /** * Factory constructor */ public static function factory(kolab_storage_folder $storage_folder) { $subclass = 'kolab_storage_cache_' . $storage_folder->type; if (class_exists($subclass)) { return new $subclass($storage_folder); } else { rcube::raise_error(array( 'code' => 900, 'type' => 'php', '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); } } /** * Default constructor */ public function __construct(kolab_storage_folder $storage_folder = null) { $rcmail = rcube::get_instance(); $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'); $this->cache_refresh = get_offset_sec($rcmail->config->get('kolab_cache_refresh', '12h')); $this->server_timezone = new DateTimeZone(date_default_timezone_get()); if ($this->enabled) { // always read folder cache and lock state from DB master $this->db->set_table_dsn('kolab_folders', 'w'); // remove sync-lock on script termination $rcmail->add_shutdown_function(array($this, '_sync_unlock')); } if ($storage_folder) $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 * * @param kolab_storage_folder The storage folder instance to connect with */ public function set_folder(kolab_storage_folder $storage_folder) { $this->folder = $storage_folder; if (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->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type); $this->ready = $this->enabled && !empty($this->folder->type); $this->folder_id = null; } /** * Returns true if this cache supports query by type */ public function has_type_col() { return in_array('type', $this->extra_cols); } /** * 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() { // only sync once per request cycle if ($this->synched) return; // increase time limit @set_time_limit($this->max_sync_lock_time - 60); // get effective time limit we have for synchronization (~70% of the execution time) $time_limit = ini_get('max_execution_time') * 0.7; $sync_start = time(); // assume sync will be completed $this->sync_complete = true; if (!$this->ready) { // kolab cache is disabled, synchronize IMAP mailbox cache only $this->imap_mode(true); $this->imap->folder_sync($this->folder->name); $this->imap_mode(false); } else { // read cached folder metadata $this->_read_folder_data(); // check cache status ($this->metadata is set in _read_folder_data()) if ( empty($this->metadata['ctag']) || empty($this->metadata['changed']) || $this->metadata['objectcount'] === null || $this->metadata['changed'] < date(self::DB_DATE_FORMAT, time() - $this->cache_refresh) || $this->metadata['ctag'] != $this->folder->get_ctag() || intval($this->metadata['objectcount']) !== $this->count() - ) { + ) { // lock synchronization for this folder or wait if locked $this->_sync_lock(); // disable messages cache if configured to do so $this->imap_mode(true); // synchronize IMAP mailbox cache $this->imap->folder_sync($this->folder->name); // compare IMAP index with object cache index $imap_index = $this->imap->index($this->folder->name, null, null, true, true); $this->imap_mode(false); // determine objects to fetch or to invalidate if (!$imap_index->is_error()) { $imap_index = $imap_index->get(); $old_index = array(); $del_index = array(); // read cache index $sql_result = $this->db->query( "SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?" . " ORDER BY `msguid` DESC", $this->folder_id ); while ($sql_arr = $this->db->fetch_assoc($sql_result)) { // Mark all duplicates for removal (note sorting order above) // Duplicates here should not happen, but they do sometimes if (isset($old_index[$sql_arr['uid']])) { $del_index[] = $sql_arr['msguid']; } else { $old_index[$sql_arr['uid']] = $sql_arr['msguid']; } } // fetch new objects from imap $i = 0; foreach (array_diff($imap_index, $old_index) as $msguid) { if ($object = $this->folder->read_object($msguid, '*')) { // Deduplication: remove older objects with the same UID // Here we do not resolve conflicts, we just make sure // the most recent version of the object will be used if ($old_msguid = $old_index[$object['uid']]) { if ($old_msguid < $msguid) { $del_index[] = $old_msguid; } else { $del_index[] = $msguid; continue; } } $old_index[$object['uid']] = $msguid; $this->_extended_insert($msguid, $object); // check time limit and abort sync if running too long if (++$i % 50 == 0 && time() - $sync_start > $time_limit) { $this->sync_complete = false; break; } } } $this->_extended_insert(0, null); $del_index = array_unique($del_index); // delete duplicate entries from IMAP $rem_index = array_intersect($del_index, $imap_index); if (!empty($rem_index)) { $this->imap_mode(true); $this->imap->delete_message($rem_index, $this->folder->name); $this->imap_mode(false); } // delete old/invalid entries from the cache $del_index += array_diff($old_index, $imap_index); if (!empty($del_index)) { $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index)); $this->db->query( "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)", $this->folder_id ); } // update ctag value (will be written to database in _sync_unlock()) if ($this->sync_complete) { $this->metadata['ctag'] = $this->folder->get_ctag(); $this->metadata['changed'] = date(self::DB_DATE_FORMAT, time()); // remember the number of cache entries linked to this folder $this->metadata['objectcount'] = $this->count(); } } // remove lock $this->_sync_unlock(); } } $this->check_error(); $this->synched = time(); } /** * Read a single entry from cache or from IMAP directly * * @param string Related IMAP message UID * @param string Object type to read * @param string IMAP folder name the entry relates to * @param array Hash array with object properties or null if not found */ public function get($msguid, $type = null, $foldername = null) { // delegate to another cache instance if ($foldername && $foldername != $this->folder->name) { $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 if (!isset($this->objects[$msguid])) { if ($this->ready) { $this->_read_folder_data(); $sql_result = $this->db->query( "SELECT * FROM `{$this->cache_table}` ". "WHERE `folder_id` = ? AND `msguid` = ?", $this->folder_id, $msguid ); if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $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])) { if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) { $this->objects = array($msguid => $object); $this->set($msguid, $object); } } } $this->check_error(); return $this->objects[$msguid]; } /** * Getter for a single Kolab object identified by its UID * * @param string $uid Object UID * * @return array The Kolab object represented as hash array */ public function get_by_uid($uid) { $old_order_by = $this->order_by; $old_limit = $this->limit; // set order to make sure we get most recent object version // set limit to skip count query $this->order_by = '`msguid` DESC'; $this->limit = array(1, 0); $list = $this->select(array(array('uid', '=', $uid))); // set the order/limit back to defined value $this->order_by = $old_order_by; $this->limit = $old_limit; if (!empty($list) && !empty($list[0])) { return $list[0]; } } /** * Insert/Update a cache entry * * @param string Related IMAP message UID * @param mixed Hash array with object properties to save or false to delete the cache entry * @param string IMAP folder name the entry relates to */ public function set($msguid, $object, $foldername = null) { if (!$msguid) { return; } // delegate to another cache instance if ($foldername && $foldername != $this->folder->name) { if ($targetfolder = kolab_storage::get_folder($foldername)) { $targetfolder->cache->set($msguid, $object); $this->error = $targetfolder->cache->get_error(); } return; } // remove old entry if ($this->ready) { $this->_read_folder_data(); $this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?", $this->folder_id, $msguid); } if ($object) { // insert new object data... $this->save($msguid, $object); } else { // ...or set in-memory cache to false $this->objects[$msguid] = $object; } $this->check_error(); } /** * Insert (or update) a cache entry * * @param int Related IMAP message UID * @param mixed Hash array with object properties to save or false to delete the cache entry * @param int Optional old message UID (for update) */ public function save($msguid, $object, $olduid = null) { // write to cache if ($this->ready) { $this->_read_folder_data(); $sql_data = $this->_serialize($object); $sql_data['folder_id'] = $this->folder_id; $sql_data['msguid'] = $msguid; $sql_data['uid'] = $object['uid']; $args = array(); - $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words'); + $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'tags', 'words'); $cols = array_merge($cols, $this->extra_cols); foreach ($cols as $idx => $col) { $cols[$idx] = $this->db->quote_identifier($col); $args[] = $sql_data[$col]; } if ($olduid) { foreach ($cols as $idx => $col) { $cols[$idx] = "$col = ?"; } $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols) . " WHERE `folder_id` = ? AND `msguid` = ?"; $args[] = $this->folder_id; $args[] = $olduid; } else { $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols) . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")"; } $result = $this->db->query($query, $args); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } } // keep a copy in memory for fast access $this->objects = array($msguid => $object); $this->uid2msg = array($object['uid'] => $msguid); $this->check_error(); } /** * Move an existing cache entry to a new resource * * @param string Entry's IMAP message UID * @param string Entry's Object UID * @param kolab_storage_folder Target storage folder instance * @param string Target entry's IMAP message UID */ public function move($msguid, $uid, $target, $new_msguid = null) { if ($this->ready && $target) { // clear cached uid mapping and force new lookup unset($target->cache->uid2msg[$uid]); // resolve new message UID in target folder if (!$new_msguid) { $new_msguid = $target->cache->uid2msguid($uid); } if ($new_msguid) { $this->_read_folder_data(); $this->db->query( "UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ". "WHERE `folder_id` = ? AND `msguid` = ?", $target->cache->get_folder_id(), $new_msguid, $this->folder_id, $msguid ); $result = $this->db->affected_rows(); } } if (empty($result)) { // just clear cache entry $this->set($msguid, false); } unset($this->uid2msg[$uid]); $this->check_error(); } /** * Remove all objects from local cache */ public function purge() { if (!$this->ready) { return true; } $this->_read_folder_data(); $result = $this->db->query( "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?", $this->folder_id ); return $this->db->affected_rows($result); } /** * Update resource URI for existing cache entries * * @param string Target IMAP folder to move it to */ public function rename($new_folder) { if (!$this->ready) { return; } 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 ); $this->check_error(); } else { $this->error = kolab_storage::ERROR_IMAP_CONN; } } /** * Select Kolab objects filtered by the given query * * @param array Pseudo-SQL query as list of filter parameter triplets * triplet: array('', '', '') * @param boolean Set true to only return UIDs instead of complete objects * @param boolean Use fast mode to fetch only minimal set of information * (no xml fetching and parsing, etc.) * * @return array List of Kolab data objects (each represented as hash array) or UIDs */ public function select($query = array(), $uids = false, $fast = false) { $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(); // fetch full object data on one query if a small result set is expected $fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS; // skip SELECT if we know it will return nothing if ($count === 0) { return $result; } $sql_query = "SELECT " . ($fetchall ? '*' : "`msguid` AS `_msguid`, `uid`") . " FROM `{$this->cache_table}` WHERE `folder_id` = ?" . $this->_sql_where($query) . (!empty($this->order_by) ? " ORDER BY " . $this->order_by : ''); $sql_result = $this->limit ? $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) : $this->db->query($sql_query, $this->folder_id); if ($this->db->is_error($sql_result)) { if ($uids) { return null; } $result->set_error(true); return $result; } while ($sql_arr = $this->db->fetch_assoc($sql_result)) { if ($fast) { $sql_arr['fast-mode'] = true; } if ($uids) { $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid']; $result[] = $sql_arr['uid']; } else if ($fetchall && ($object = $this->_unserialize($sql_arr))) { $result[] = $object; } else if (!$fetchall) { // only add msguid to dataset index $result[] = $sql_arr; } } } // use IMAP else { $filter = $this->_query2assoc($query); $this->imap_mode(true); if ($filter['type']) { $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type']; $index = $this->imap->search_once($this->folder->name, $search); } else { $index = $this->imap->index($this->folder->name, null, null, true, true); } $this->imap_mode(false); if ($index->is_error()) { $this->check_error(); if ($uids) { return null; } $result->set_error(true); return $result; } $index = $index->get(); $result = $uids ? $index : $this->_fetch($index, $filter['type']); // TODO: post-filter result according to query } // We don't want to cache big results in-memory, however // if we select only one object here, there's a big chance we will need it later if (!$uids && count($result) == 1) { if ($msguid = $result[0]['_msguid']) { $this->uid2msg[$result[0]['uid']] = $msguid; $this->objects = array($msguid => $result[0]); } } $this->check_error(); return $result; } /** * Get number of objects mathing the given query * * @param array $query Pseudo-SQL query as list of filter parameter triplets * @return integer The number of objects of the given type */ public function count($query = array()) { // read from local cache DB (assume it to be synchronized) if ($this->ready) { $this->_read_folder_data(); $sql_result = $this->db->query( "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ". "WHERE `folder_id` = ?" . $this->_sql_where($query), $this->folder_id ); if ($this->db->is_error($sql_result)) { return null; } $sql_arr = $this->db->fetch_assoc($sql_result); $count = intval($sql_arr['numrows']); } // use IMAP else { $filter = $this->_query2assoc($query); $this->imap_mode(true); if ($filter['type']) { $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type']; $index = $this->imap->search_once($this->folder->name, $search); } else { $index = $this->imap->index($this->folder->name, null, null, true, true); } $this->imap_mode(false); if ($index->is_error()) { $this->check_error(); return null; } // TODO: post-filter result according to query $count = $index->count(); } $this->check_error(); return $count; } /** * Define ORDER BY clause for cache queries */ public function set_order_by($sortcols) { if (!empty($sortcols)) { $sortcols = array_map(function($v) { list($column, $order) = explode(' ', $v, 2); return "`$column`" . ($order ? " $order" : ''); }, (array) $sortcols); $this->order_by = join(', ', $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 */ protected function _sql_where($query) { $sql_where = ''; foreach ((array) $query as $param) { if (is_array($param[0])) { $subq = array(); foreach ($param[0] as $q) { $subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q))); } if (!empty($subq)) { $sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')'; } continue; } else if ($param[1] == '=' && is_array($param[2])) { $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')'; $param[1] = 'IN'; } else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') { $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : ''; $param[1] = $not . 'LIKE'; $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%'); } else if ($param[1] == '~*' || $param[1] == '!~*') { $not = $param[1][1] == '!' ? 'NOT ' : ''; $param[1] = $not . 'LIKE'; $qvalue = $this->db->quote(preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%'); } else if ($param[0] == 'tags') { $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE'; $qvalue = $this->db->quote('% '.$param[2].' %'); } else { $qvalue = $this->db->quote($param[2]); } $sql_where .= sprintf(' AND %s %s %s', $this->db->quote_identifier($param[0]), $param[1], $qvalue ); } return $sql_where; } /** * Helper method to convert the given pseudo-query triplets into * an associative filter array with 'equals' values only */ protected function _query2assoc($query) { // extract object type from query parameter $filter = array(); foreach ($query as $param) { if ($param[1] == '=') $filter[$param[0]] = $param[2]; } return $filter; } /** * Fetch messages from IMAP * * @param array List of message UIDs to fetch * @param string Requested object type or * for all * @param string IMAP folder to read from * @return array List of parsed Kolab objects */ protected function _fetch($index, $type = null, $folder = null) { $results = new kolab_storage_dataset($this); foreach ((array)$index as $msguid) { if ($object = $this->folder->read_object($msguid, $type, $folder)) { $results[] = $object; $this->set($msguid, $object); } } return $results; } /** * Helper method to convert the given Kolab object into a dataset to be written to cache */ protected function _serialize($object) { - $sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => ''); + $data = array(); + $sql_data = array('changed' => null, 'tags' => '', 'words' => ''); if ($object['changed']) { $sql_data['changed'] = date(self::DB_DATE_FORMAT, is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']); } if ($object['_formatobj']) { - $xml = (string) $object['_formatobj']->write(3.0); - $size = strlen($xml); + $xml = (string) $object['_formatobj']->write(3.0); - $sql_data['xml'] = preg_replace('!()[\n\r\t\s]+!ms', '$1', $xml); + $data['_size'] = strlen($xml); $sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' '; } - // TODO: get rid of xml column - // TODO: store only small subset of properties in data column, i.e. properties that are - // needed for fast-mode only (use $data_props) - // TODO: store data in JSON format and no base64-encoding - - // extract object data - $data = array(); - foreach ($object as $key => $val) { - // skip empty properties - if ($val === "" || $val === null) { - continue; - } - // mark binary data to be extracted from xml on unserialize() - if (isset($this->binary_items[$key])) { - $data[$key] = true; - } - else if ($key[0] != '_') { - $data[$key] = $val; - } - else if ($key == '_attachments') { - foreach ($val as $k => $att) { - unset($att['content'], $att['path']); - if ($att['id']) - $data[$key][$k] = $att; + // Store only minimal set of object properties + foreach ($this->data_props as $prop) { + if (isset($object[$prop])) { + $data[$prop] = $object[$prop]; + if ($data[$prop] instanceof DateTime) { + $data[$prop] = array( + 'cl' => 'DateTime', + 'dt' => $data[$prop]->format('Y-m-d H:i:s'), + 'tz' => $data[$prop]->getTimezone()->getName(), + ); } } } - if ($size) { - $data['_size'] = $size; - } - - // use base64 encoding (Bug #1912, #2662) - $sql_data['data'] = base64_encode(serialize($data)); + $sql_data['data'] = json_encode(rcube_charset::clean($data)); return $sql_data; } /** * Helper method to turn stored cache data into a valid storage object */ protected function _unserialize($sql_arr) { - if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) - && ($object = unserialize(base64_decode($sql_arr['data']))) - ) { + if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) { $object['uid'] = $sql_arr['uid']; foreach ($this->data_props as $prop) { - if (!isset($object[$prop]) && isset($sql_arr[$prop])) { + if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') { + $object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz'])); + } + else if (!isset($object[$prop]) && isset($sql_arr[$prop])) { $object[$prop] = $sql_arr[$prop]; } } if ($sql_arr['created'] && empty($object['created'])) { $object['created'] = new DateTime($sql_arr['created']); } if ($sql_arr['changed'] && empty($object['changed'])) { $object['changed'] = new DateTime($sql_arr['changed']); } $object['_type'] = $sql_arr['type'] ?: $this->folder->type; $object['_msguid'] = $sql_arr['msguid']; $object['_mailbox'] = $this->folder->name; } // Fetch object xml - else if ($object = $this->folder->read_object($sql_arr['msguid'])) { - // additional meta data - $object['_size'] = strlen($xml); + else { + $object = $this->folder->read_object($sql_arr['msguid']); } return $object; } /** * Write records into cache using extended inserts to reduce the number of queries to be executed * * @param int Message UID. Set 0 to commit buffered inserts * @param array Kolab object to cache */ protected function _extended_insert($msguid, $object) { static $buffer = ''; $line = ''; if ($object) { $sql_data = $this->_serialize($object); // Skip multifolder insert for Oracle, we can't put long data inline if ($this->db->db_provider == 'oracle') { $extra_cols = ''; if ($this->extra_cols) { $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols); $extra_cols = ', ' . join(', ', $extra_cols); $extra_args = str_repeat(', ?', count($this->extra_cols)); } $params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'], - $sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']); + $sql_data['data'], $sql_data['tags'], $sql_data['words']); foreach ($this->extra_cols as $col) { $params[] = $sql_data[$col]; } $result = $this->db->query( "INSERT INTO `{$this->cache_table}` " - . " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)" - . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)", + . " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `tags`, `words`$extra_cols)" + . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ? $extra_args)", $params ); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } return; } $values = array( $this->db->quote($this->folder_id), $this->db->quote($msguid), $this->db->quote($object['uid']), $this->db->now(), $this->db->quote($sql_data['changed']), $this->db->quote($sql_data['data']), - $this->db->quote($sql_data['xml']), $this->db->quote($sql_data['tags']), $this->db->quote($sql_data['words']), ); foreach ($this->extra_cols as $col) { $values[] = $this->db->quote($sql_data[$col]); } $line = '(' . join(',', $values) . ')'; } if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) { $extra_cols = ''; if ($this->extra_cols) { $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols); $extra_cols = ', ' . join(', ', $extra_cols); } $result = $this->db->query( "INSERT INTO `{$this->cache_table}` ". - " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)". + " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `tags`, `words`$extra_cols)". " VALUES $buffer" ); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } $buffer = ''; } $buffer .= ($buffer ? ',' : '') . $line; } /** * Returns max_allowed_packet from mysql config */ protected function max_sql_packet() { if (!$this->max_sql_packet) { // mysql limit or max 4 MB $value = $this->db->get_variable('max_allowed_packet', 1048500); $this->max_sql_packet = min($value, 4*1024*1024) - 2000; } return $this->max_sql_packet; } /** * Read this folder's ID and cache metadata */ protected function _read_folder_data() { // already done if (!empty($this->folder_id) || !$this->ready) return; $sql_arr = $this->db->fetch_assoc($this->db->query( "SELECT `folder_id`, `synclock`, `ctag`, `changed`, `objectcount`" . " FROM `{$this->folders_table}` WHERE `resource` = ?", $this->resource_uri )); if ($sql_arr) { $this->metadata = $sql_arr; $this->folder_id = $sql_arr['folder_id']; } else { $this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)" . " VALUES (?, ?)", $this->resource_uri, $this->folder->type); $this->folder_id = $this->db->insert_id('kolab_folders'); $this->metadata = array(); } } /** * Check lock record for this folder and wait if locked or set lock */ protected function _sync_lock() { if (!$this->ready) return; $this->_read_folder_data(); // abort if database is not set-up if ($this->db->is_error()) { $this->check_error(); $this->ready = false; return; } $read_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?"; $write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?"; // wait if locked (expire locks after 10 minutes) ... // ... or if setting lock fails (another process meanwhile set it) while ( (intval($this->metadata['synclock']) + $this->max_sync_lock_time > time()) || (($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock']))) && !($affected = $this->db->affected_rows($res))) ) { usleep(500000); $this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id)); } $this->synclock = $affected > 0; } /** * Remove lock for this folder */ public function _sync_unlock() { if (!$this->ready || !$this->synclock) return; $this->db->query( "UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ?, `changed` = ?, `objectcount` = ? WHERE `folder_id` = ?", $this->metadata['ctag'], $this->metadata['changed'], $this->metadata['objectcount'], $this->folder_id ); $this->synclock = false; } /** * 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 * @param boolean Include deleted objects * @return int The resolved IMAP message UID */ public function uid2msguid($uid, $deleted = false) { // query local database if available if (!isset($this->uid2msg[$uid]) && $this->ready) { $this->_read_folder_data(); $sql_result = $this->db->query( "SELECT `msguid` FROM `{$this->cache_table}` ". "WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC", $this->folder_id, $uid ); if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $this->uid2msg[$uid] = $sql_arr['msguid']; } } if (!isset($this->uid2msg[$uid])) { // use IMAP SEARCH to get the right message $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . rcube_imap_generic::escape($uid)); $results = $index->get(); $this->uid2msg[$uid] = end($results); } return $this->uid2msg[$uid]; } /** * Getter for protected member variables */ public function __get($name) { if ($name == 'folder_id') { $this->_read_folder_data(); } return $this->$name; } /** * Set Roundcube storage options and bypass messages/indexes cache. * * We use skip_deleted and threading settings specific to Kolab, * we have to change these global settings only temporarily. * Roundcube cache duplicates information already stored in kolab_cache, * that's why we can disable it for better performance. * * @param bool $force True to start Kolab mode, False to stop it. */ public function imap_mode($force = false) { // remember current IMAP settings if ($force) { $this->imap_options = array( 'skip_deleted' => $this->imap->get_option('skip_deleted'), 'threading' => $this->imap->get_threading(), ); } // re-set IMAP settings $this->imap->set_threading($force ? false : $this->imap_options['threading']); $this->imap->set_options(array( 'skip_deleted' => $force ? true : $this->imap_options['skip_deleted'], )); // if kolab cache is disabled do nothing if (!$this->enabled) { return; } static $messages_cache, $cache_bypass; if ($messages_cache === null) { $rcmail = rcube::get_instance(); $messages_cache = (bool) $rcmail->config->get('messages_cache'); $cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass'); } if ($messages_cache) { // handle recurrent (multilevel) bypass() calls if ($force) { $this->cache_bypassed += 1; if ($this->cache_bypassed > 1) { return; } } else { $this->cache_bypassed -= 1; if ($this->cache_bypassed > 0) { return; } } switch ($cache_bypass) { case 2: // Disable messages and index cache completely $this->imap->set_messages_caching(!$force); break; case 3: case 1: // We'll disable messages cache, but keep index cache (1) or vice-versa (3) // Default mode is both (MODE_INDEX | MODE_MESSAGE) $mode = $cache_bypass == 3 ? rcube_imap_cache::MODE_MESSAGE : rcube_imap_cache::MODE_INDEX; if (!$force) { $mode |= $cache_bypass == 3 ? rcube_imap_cache::MODE_INDEX : rcube_imap_cache::MODE_MESSAGE; } $this->imap->set_messages_caching(true, $mode); } } } /** * Converts DateTime or unix timestamp into sql date format * using server timezone. */ protected function _convert_datetime($datetime) { if (is_object($datetime)) { $dt = clone $datetime; $dt->setTimeZone($this->server_timezone); return $dt->format(self::DB_DATE_FORMAT); } else if ($datetime) { return date(self::DB_DATE_FORMAT, $datetime); } } } diff --git a/plugins/libkolab/lib/kolab_storage_cache_contact.php b/plugins/libkolab/lib/kolab_storage_cache_contact.php index 5dfb16b7..22d0a330 100644 --- a/plugins/libkolab/lib/kolab_storage_cache_contact.php +++ b/plugins/libkolab/lib/kolab_storage_cache_contact.php @@ -1,73 +1,68 @@ * * Copyright (C) 2013, Kolab Systems AG * * 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 . */ class kolab_storage_cache_contact extends kolab_storage_cache { protected $extra_cols_max = 255; protected $extra_cols = array('type', 'name', 'firstname', 'surname', 'email'); protected $data_props = array('type', 'name', 'firstname', 'middlename', 'prefix', 'suffix', 'surname', 'email', 'organization', 'member'); - protected $binary_items = array( - 'photo' => '|[^;]+;base64,([^<]+)|i', - 'pgppublickey' => '|data:application/pgp-keys;base64,([^<]+)|i', - 'pkcs7publickey' => '|data:application/pkcs7-mime;base64,([^<]+)|i', - ); /** * Helper method to convert the given Kolab object into a dataset to be written to cache * * @override */ protected function _serialize($object) { $sql_data = parent::_serialize($object); $sql_data['type'] = $object['_type']; // columns for sorting $sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']); $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']); $sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']); $sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']); if (is_array($sql_data['email'])) { $sql_data['email'] = $sql_data['email']['address']; } // avoid value being null if (empty($sql_data['email'])) { $sql_data['email'] = ''; } // use organization if name is empty if (empty($sql_data['name']) && !empty($object['organization'])) { $sql_data['name'] = rcube_charset::clean($object['organization']); } // make sure some data is not longer that database limit (#5291) foreach ($this->extra_cols as $col) { if (strlen($sql_data[$col]) > $this->extra_cols_max) { $sql_data[$col] = rcube_charset::clean(substr($sql_data[$col], 0, $this->extra_cols_max)); } } return $sql_data; } } diff --git a/plugins/libkolab/lib/kolab_storage_cache_mongodb.php b/plugins/libkolab/lib/kolab_storage_cache_mongodb.php deleted file mode 100644 index 8ae95e4b..00000000 --- a/plugins/libkolab/lib/kolab_storage_cache_mongodb.php +++ /dev/null @@ -1,561 +0,0 @@ - - * - * Copyright (C) 2012, Kolab Systems AG - * - * 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 . - */ - -class kolab_storage_cache_mongodb -{ - private $db; - private $imap; - private $folder; - private $uid2msg; - private $objects; - private $index = array(); - private $resource_uri; - private $enabled = true; - private $synched = false; - private $synclock = false; - private $ready = false; - private $max_sql_packet = 1046576; // 1 MB - 2000 bytes - private $binary_cols = array('photo','pgppublickey','pkcs7publickey'); - - - /** - * Default constructor - */ - public function __construct(kolab_storage_folder $storage_folder = null) - { - $rcmail = rcube::get_instance(); - $mongo = new Mongo(); - $this->db = $mongo->kolab_cache; - $this->imap = $rcmail->get_storage(); - $this->enabled = $rcmail->config->get('kolab_cache', false); - - if ($this->enabled) { - // remove sync-lock on script termination - $rcmail->add_shutdown_function(array($this, '_sync_unlock')); - } - - if ($storage_folder) - $this->set_folder($storage_folder); - } - - - /** - * Connect cache with a storage folder - * - * @param kolab_storage_folder The storage folder instance to connect with - */ - public function set_folder(kolab_storage_folder $storage_folder) - { - $this->folder = $storage_folder; - - if (empty($this->folder->name)) { - $this->ready = false; - return; - } - - // compose fully qualified ressource uri for this instance - $this->resource_uri = $this->folder->get_resource_uri(); - $this->ready = $this->enabled; - } - - - /** - * Synchronize local cache data with remote - */ - public function synchronize() - { - // only sync once per request cycle - if ($this->synched) - return; - - // increase time limit - @set_time_limit(500); - - // lock synchronization for this folder or wait if locked - $this->_sync_lock(); - - // synchronize IMAP mailbox cache - $this->imap->folder_sync($this->folder->name); - - // compare IMAP index with object cache index - $imap_index = $this->imap->index($this->folder->name); - $this->index = $imap_index->get(); - - // determine objects to fetch or to invalidate - if ($this->ready) { - // read cache index - $old_index = array(); - $cursor = $this->db->cache->find(array('resource' => $this->resource_uri), array('msguid' => 1, 'uid' => 1)); - foreach ($cursor as $doc) { - $old_index[] = $doc['msguid']; - $this->uid2msg[$doc['uid']] = $doc['msguid']; - } - - // fetch new objects from imap - foreach (array_diff($this->index, $old_index) as $msguid) { - if ($object = $this->folder->read_object($msguid, '*')) { - try { - $this->db->cache->insert($this->_serialize($object, $msguid)); - } - catch (Exception $e) { - rcmail::raise_error(array( - 'code' => 900, 'type' => 'php', - 'message' => "Failed to write to mongodb cache: " . $e->getMessage(), - ), true); - } - } - } - - // delete invalid entries from local DB - $del_index = array_diff($old_index, $this->index); - if (!empty($del_index)) { - $this->db->cache->remove(array('resource' => $this->resource_uri, 'msguid' => array('$in' => $del_index))); - } - } - - // remove lock - $this->_sync_unlock(); - - $this->synched = time(); - } - - - /** - * Read a single entry from cache or from IMAP directly - * - * @param string Related IMAP message UID - * @param string Object type to read - * @param string IMAP folder name the entry relates to - * @param array Hash array with object properties or null if not found - */ - public function get($msguid, $type = null, $foldername = null) - { - // delegate to another cache instance - if ($foldername && $foldername != $this->folder->name) { - return kolab_storage::get_folder($foldername)->cache->get($msguid, $object); - } - - // load object if not in memory - if (!isset($this->objects[$msguid])) { - if ($this->ready && ($doc = $this->db->cache->findOne(array('resource' => $this->resource_uri, 'msguid' => $msguid)))) - $this->objects[$msguid] = $this->_unserialize($doc); - - // 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]; - } - } - - return $this->objects[$msguid]; - } - - - /** - * Insert/Update a cache entry - * - * @param string Related IMAP message UID - * @param mixed Hash array with object properties to save or false to delete the cache entry - * @param string IMAP folder name the entry relates to - */ - public function set($msguid, $object, $foldername = null) - { - // delegate to another cache instance - if ($foldername && $foldername != $this->folder->name) { - kolab_storage::get_folder($foldername)->cache->set($msguid, $object); - return; - } - - // write to cache - if ($this->ready) { - // remove old entry - $this->db->cache->remove(array('resource' => $this->resource_uri, 'msguid' => $msguid)); - - // write new object data if not false (wich means deleted) - if ($object) { - try { - $this->db->cache->insert($this->_serialize($object, $msguid)); - } - catch (Exception $e) { - rcmail::raise_error(array( - 'code' => 900, 'type' => 'php', - 'message' => "Failed to write to mongodb cache: " . $e->getMessage(), - ), true); - } - } - } - - // keep a copy in memory for fast access - $this->objects[$msguid] = $object; - - if ($object) - $this->uid2msg[$object['uid']] = $msguid; - } - - /** - * Move an existing cache entry to a new resource - * - * @param string Entry's IMAP message UID - * @param string Entry's Object UID - * @param string Target IMAP folder to move it to - */ - public function move($msguid, $objuid, $target_folder) - { - $target = kolab_storage::get_folder($target_folder); - - // resolve new message UID in target folder - if ($new_msguid = $target->cache->uid2msguid($objuid)) { -/* - $this->db->query( - "UPDATE kolab_cache SET resource=?, msguid=? ". - "WHERE resource=? AND msguid=? AND type<>?", - $target->get_resource_uri(), - $new_msguid, - $this->resource_uri, - $msguid, - 'lock' - ); -*/ - } - else { - // just clear cache entry - $this->set($msguid, false); - } - - unset($this->uid2msg[$uid]); - } - - - /** - * Remove all objects from local cache - */ - public function purge($type = null) - { - return $this->db->cache->remove(array(), array('safe' => true)); - } - - - /** - * Select Kolab objects filtered by the given query - * - * @param array Pseudo-SQL query as list of filter parameter triplets - * triplet: array('', '', '') - * @return array List of Kolab data objects (each represented as hash array) - */ - public function select($query = array()) - { - $result = array(); - - // read from local cache DB (assume it to be synchronized) - if ($this->ready) { - $cursor = $this->db->cache->find(array('resource' => $this->resource_uri) + $this->_mongo_filter($query)); - foreach ($cursor as $doc) { - if ($object = $this->_unserialize($doc)) - $result[] = $object; - } - } - else { - // extract object type from query parameter - $filter = $this->_query2assoc($query); - - // use 'list' for folder's default objects - if ($filter['type'] == $this->type) { - $index = $this->index; - } - else { // search by object type - $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_storage_folder::KTYPE_PREFIX . $filter['type']; - $index = $this->imap->search_once($this->folder->name, $search)->get(); - } - - // fetch all messages in $index from IMAP - $result = $this->_fetch($index, $filter['type']); - - // TODO: post-filter result according to query - } - - return $result; - } - - - /** - * Get number of objects mathing the given query - * - * @param array $query Pseudo-SQL query as list of filter parameter triplets - * @return integer The number of objects of the given type - */ - public function count($query = array()) - { - $count = 0; - - // cache is in sync, we can count records in local DB - if ($this->synched) { - $cursor = $this->db->cache->find(array('resource' => $this->resource_uri) + $this->_mongo_filter($query)); - $count = $cursor->valid() ? $cursor->count() : 0; - } - else { - // search IMAP by object type - $filter = $this->_query2assoc($query); - $ctype = kolab_storage_folder::KTYPE_PREFIX . $filter['type']; - $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype); - $count = $index->count(); - } - - return $count; - } - - /** - * Helper method to convert the pseudo-SQL query into a valid mongodb filter - */ - private function _mongo_filter($query) - { - $filters = array(); - foreach ($query as $param) { - $filter = array(); - if ($param[1] == '=' && is_array($param[2])) { - $filter[$param[0]] = array('$in' => $param[2]); - $filters[] = $filter; - } - else if ($param[1] == '=') { - $filters[] = array($param[0] => $param[2]); - } - else if ($param[1] == 'LIKE' || $param[1] == '~') { - $filter[$param[0]] = array('$regex' => preg_quote($param[2]), '$options' => 'i'); - $filters[] = $filter; - } - else if ($param[1] == '!~' || $param[1] == '!LIKE') { - $filter[$param[0]] = array('$not' => '/' . preg_quote($param[2]) . '/i'); - $filters[] = $filter; - } - else { - $op = ''; - switch ($param[1]) { - case '>': $op = '$gt'; break; - case '>=': $op = '$gte'; break; - case '<': $op = '$lt'; break; - case '<=': $op = '$lte'; break; - case '!=': - case '<>': $op = '$gte'; break; - } - if ($op) { - $filter[$param[0]] = array($op => $param[2]); - $filters[] = $filter; - } - } - } - - return array('$and' => $filters); - } - - /** - * Helper method to convert the given pseudo-query triplets into - * an associative filter array with 'equals' values only - */ - private function _query2assoc($query) - { - // extract object type from query parameter - $filter = array(); - foreach ($query as $param) { - if ($param[1] == '=') - $filter[$param[0]] = $param[2]; - } - return $filter; - } - - /** - * Fetch messages from IMAP - * - * @param array List of message UIDs to fetch - * @return array List of parsed Kolab objects - */ - private function _fetch($index, $type = null, $folder = null) - { - $results = array(); - foreach ((array)$index as $msguid) { - if ($object = $this->folder->read_object($msguid, $type, $folder)) { - $results[] = $object; - $this->set($msguid, $object); - } - } - - return $results; - } - - - /** - * Helper method to convert the given Kolab object into a dataset to be written to cache - */ - private function _serialize($object, $msguid) - { - $bincols = array_flip($this->binary_cols); - $doc = array( - 'resource' => $this->resource_uri, - 'type' => $object['_type'] ? $object['_type'] : $this->folder->type, - 'msguid' => $msguid, - 'uid' => $object['uid'], - 'xml' => '', - 'tags' => array(), - 'words' => array(), - 'objcols' => array(), - ); - - // set type specific values - if ($this->folder->type == 'event') { - // database runs in server's timezone so using date() is what we want - $doc['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']); - $doc['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']); - - // extend date range for recurring events - if ($object['recurrence']) { - $doc['dtend'] = date('Y-m-d H:i:s', $object['recurrence']['UNTIL'] ?: strtotime('now + 2 years')); - } - } - - if ($object['_formatobj']) { - $doc['xml'] = preg_replace('!()[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write()); - $doc['tags'] = $object['_formatobj']->get_tags(); - $doc['words'] = $object['_formatobj']->get_words(); - } - - // extract object data - $data = array(); - foreach ($object as $key => $val) { - if ($val === "" || $val === null) { - // skip empty properties - continue; - } - if (isset($bincols[$key])) { - $data[$key] = base64_encode($val); - } - else if (is_object($val)) { - if (is_a($val, 'DateTime')) { - $data[$key] = array('_class' => 'DateTime', 'date' => $val->format('Y-m-d H:i:s'), 'timezone' => $val->getTimezone()->getName()); - $doc['objcols'][] = $key; - } - } - else if ($key[0] != '_') { - $data[$key] = $val; - } - else if ($key == '_attachments') { - foreach ($val as $k => $att) { - unset($att['content'], $att['path']); - if ($att['id']) - $data[$key][$k] = $att; - } - } - } - - $doc['data'] = $data; - return $doc; - } - - /** - * Helper method to turn stored cache data into a valid storage object - */ - private function _unserialize($doc) - { - $object = $doc['data']; - - // decode binary properties - foreach ($this->binary_cols as $key) { - if (!empty($object[$key])) - $object[$key] = base64_decode($object[$key]); - } - - // restore serialized objects - foreach ((array)$doc['objcols'] as $key) { - switch ($object[$key]['_class']) { - case 'DateTime': - $val = new DateTime($object[$key]['date'], new DateTimeZone($object[$key]['timezone'])); - $object[$key] = $val; - break; - } - } - - // add meta data - $object['_type'] = $doc['type']; - $object['_msguid'] = $doc['msguid']; - $object['_mailbox'] = $this->folder->name; - $object['_formatobj'] = kolab_format::factory($doc['type'], $doc['xml']); - - return $object; - } - - /** - * Check lock record for this folder and wait if locked or set lock - */ - private function _sync_lock() - { - if (!$this->ready) - return; - - $this->synclock = true; - $lock = $this->db->locks->findOne(array('resource' => $this->resource_uri)); - - // create lock record if not exists - if (!$lock) { - $this->db->locks->insert(array('resource' => $this->resource_uri, 'created' => time())); - } - // wait if locked (expire locks after 10 minutes) - else if ((time() - $lock['created']) < 600) { - usleep(500000); - return $this->_sync_lock(); - } - // set lock - else { - $lock['created'] = time(); - $this->db->locks->update(array('_id' => $lock['_id']), $lock, array('safe' => true)); - } - } - - /** - * Remove lock for this folder - */ - public function _sync_unlock() - { - if (!$this->ready || !$this->synclock) - return; - - $this->db->locks->remove(array('resource' => $this->resource_uri)); - } - - /** - * Resolve an object UID into an IMAP message UID - * - * @param string Kolab object UID - * @param boolean Include deleted objects - * @return int The resolved IMAP message UID - */ - public function uid2msguid($uid, $deleted = false) - { - if (!isset($this->uid2msg[$uid])) { - // use IMAP SEARCH to get the right message - $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid); - $results = $index->get(); - $this->uid2msg[$uid] = $results[0]; - } - - return $this->uid2msg[$uid]; - } - -} diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index 1ffbb457..d0695cb2 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -1,1146 +1,1147 @@ * @author Aleksander Machniak * * Copyright (C) 2012-2013, Kolab Systems AG * * 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 . */ class kolab_storage_folder extends kolab_storage_folder_api { /** * The kolab_storage_cache instance for caching operations * @var object */ public $cache; /** * Indicate validity status * @var boolean */ public $valid = false; protected $error = 0; protected $resource_uri; /** * Default constructor * * @param string The folder name/path * @param string Expected folder type */ function __construct($name, $type = null, $type_annotation = null) { parent::__construct($name); $this->set_folder($name, $type, $type_annotation); } /** * Set the IMAP folder this instance connects to * * @param string The folder name/path * @param string Expected folder type * @param string Optional folder type if known */ public function set_folder($name, $type = null, $type_annotation = null) { $this->name = $name; if (empty($type_annotation)) { $type_annotation = $this->get_type(); } $oldtype = $this->type; list($this->type, $suffix) = explode('.', $type_annotation); $this->default = $suffix == 'default'; $this->subtype = $this->default ? '' : $suffix; $this->id = kolab_storage::folder_id($name); $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 if folder type changed 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); } /** * Returns code of last error * * @return int Error code */ public function get_error() { return $this->error ?: $this->cache->get_error(); } /** * Check IMAP connection error state */ public 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; } } return $this->error; } /** * Compose a unique resource URI for this IMAP folder */ public function get_resource_uri() { if (!empty($this->resource_uri)) { return $this->resource_uri; } // strip namespace prefix from folder name $ns = $this->get_namespace(); $nsdata = $this->imap->get_namespace($ns); if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) { $subpath = substr($this->name, strlen($nsdata[0][0])); if ($ns == 'other') { list($user, $suffix) = explode($nsdata[0][1], $subpath, 2); $subpath = $suffix; } } else { $subpath = $this->name; } // compose fully qualified ressource uri for this instance $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath; return $this->resource_uri; } /** * Helper method to extract folder UID metadata * * @return string Folder's UID */ public function get_uid() { // UID is defined in folder METADATA $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_CYRUS); $metadata = $this->get_metadata(); if ($metadata !== null) { foreach ($metakeys as $key) { if ($uid = $metadata[$key]) { return $uid; } } // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if ($this->set_uid($uid)) { return $uid; } } $this->check_error(); // create hash from folder name if we can't write the UID metadata return md5($this->name . $this->get_owner()); } /** * Helper method to set an UID value to the given IMAP folder instance * * @param string Folder's UID * @return boolean True on succes, False on failure */ public function set_uid($uid) { $success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)); $this->check_error(); return $success; } /** * Compose a folder Etag identifier */ public function get_ctag() { $fdata = $this->get_imap_data(); $this->check_error(); return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']); } /** * Check activation status of this folder * * @return boolean True if enabled, false if not */ public function is_active() { return kolab_storage::folder_is_active($this->name); } /** * Change activation status of this folder * * @param boolean The desired subscription status: true = active, false = not active * * @return True on success, false on error */ public function activate($active) { return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name); } /** * Check subscription status of this folder * * @return boolean True if subscribed, false if not */ public function is_subscribed() { return kolab_storage::folder_is_subscribed($this->name); } /** * Change subscription status of this folder * * @param boolean The desired subscription status: true = subscribed, false = not subscribed * * @return True on success, false on error */ public function subscribe($subscribed) { return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name); } /** * Get number of objects stored in this folder * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * * @return integer The number of objects of the given type * @see self::select() */ public function count($query = null) { if (!$this->valid) { return 0; } // synchronize cache first $this->cache->synchronize(); return $this->cache->count($this->_prepare_query($query)); } /** * List Kolab objects matching the given query * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * * @return array List of Kolab data objects (each represented as hash array) * @deprecated Use select() */ public function get_objects($query = array()) { return $this->select($query); } /** * Select Kolab objects matching the given query * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * @param boolean Use fast mode to fetch only minimal set of information * (no xml fetching and parsing, etc.) * * @return array List of Kolab data objects (each represented as hash array) */ public function select($query = array(), $fast = false) { if (!$this->valid) { return array(); } // synchronize caches $this->cache->synchronize(); // fetch objects from cache return $this->cache->select($this->_prepare_query($query), false, $fast); } /** * Getter for object UIDs only * * @param array Pseudo-SQL query as list of filter parameter triplets * @return array List of Kolab object UIDs */ public function get_uids($query = array()) { if (!$this->valid) { return array(); } // synchronize caches $this->cache->synchronize(); // fetch UIDs from cache 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 */ private function _prepare_query($query) { // string equals type query // FIXME: should not be called this way! if (is_string($query)) { return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array(); } foreach ((array)$query as $i => $param) { if ($param[0] == 'type' && !$this->cache->has_type_col()) { unset($query[$i]); } else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) { if (is_object($param[2]) && is_a($param[2], 'DateTime')) $param[2] = $param[2]->format('U'); if (is_numeric($param[2])) $query[$i][2] = date('Y-m-d H:i:s', $param[2]); } } return $query; } /** * Getter for a single Kolab object identified by its UID * * @param string $uid Object UID * * @return array The Kolab object represented as hash array */ public function get_object($uid) { if (!$this->valid || !$uid) { return false; } // synchronize caches $this->cache->synchronize(); return $this->cache->get_by_uid($uid); } /** * Fetch a Kolab object attachment which is stored in a separate part * of the mail MIME message that represents the Kolab record. * * @param string Object's UID * @param string The attachment's mime number * @param string IMAP folder where message is stored; * If set, that also implies that the given UID is an IMAP UID * @param bool True to print the part content * @param resource File pointer to save the message part * @param boolean Disables charset conversion * * @return mixed The attachment content as binary string */ public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false) { if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) { $this->imap->set_folder($mailbox ? $mailbox : $this->name); if (substr($part, 0, 2) == 'i:') { // attachment data is stored in XML if ($object = $this->cache->get($msguid)) { // load data from XML (attachment content is not stored in cache) if ($object['_formatobj'] && isset($object['_size'])) { $object['_attachments'] = array(); $object['_formatobj']->get_attachments($object); } foreach ($object['_attachments'] as $attach) { if ($attach['id'] == $part) { if ($print) echo $attach['content']; else if ($fp) fwrite($fp, $attach['content']); else return $attach['content']; return true; } } } } else { // return message part from IMAP directly // TODO: We could improve performance if we cache part's encoding // without 3rd argument get_message_part() will request BODYSTRUCTURE from IMAP return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv); } } return null; } /** * Fetch the mime message from the storage server and extract * the Kolab groupware object from it * * @param string The IMAP message UID to fetch * @param string The object type expected (use wildcard '*' to accept all types) * @param string The folder name where the message is stored * * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found */ public function read_object($msguid, $type = null, $folder = null) { if (!$this->valid) { return false; } if (!$type) $type = $this->type; if (!$folder) $folder = $this->name; $this->imap->set_folder($folder); $this->cache->imap_mode(true); $message = new rcube_message($msguid); $this->cache->imap_mode(false); // Message doesn't exist? if (empty($message->headers)) { return false; } // extract the X-Kolab-Type header from the XML attachment part if missing if (empty($message->headers->others['x-kolab-type'])) { foreach ((array)$message->attachments as $part) { if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) { $message->headers->others['x-kolab-type'] = $part->mimetype; break; } } } // fix buggy messages stating the X-Kolab-Type header twice else if (is_array($message->headers->others['x-kolab-type'])) { $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']); } // no object type header found: abort if (empty($message->headers->others['x-kolab-type'])) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "No X-Kolab-Type information found in message $msguid ($this->name).", ), true); return false; } $object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']); $content_type = kolab_format::KTYPE_PREFIX . $object_type; // check object type header and abort on mismatch if ($type != '*' && strpos($object_type, $type) !== 0 && !($object_type == 'distribution-list' && $type == 'contact')) { return false; } $attachments = array(); // get XML part foreach ((array)$message->attachments as $part) { if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!i', $part->mimetype))) { $xml = $message->get_part_body($part->mime_id, true); } else if ($part->filename || $part->content_id) { $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename; $size = null; // Use Content-Disposition 'size' as for the Kolab Format spec. if (isset($part->d_parameters['size'])) { $size = $part->d_parameters['size']; } // we can trust part size only if it's not encoded else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') { $size = $part->size; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'size' => $size, ); } } if (!$xml) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not find Kolab data part in message $msguid ($this->name).", ), true); return false; } // check kolab format version $format_version = $message->headers->others['x-kolab-mime-version']; if (empty($format_version)) { list($xmltype, $subtype) = explode('.', $object_type); $xmlhead = substr($xml, 0, 512); // detect old Kolab 2.0 format if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false) $format_version = '2.0'; else $format_version = '3.0'; // assume 3.0 } // get Kolab format handler for the given type $format = kolab_format::factory($object_type, $format_version); if (is_a($format, 'PEAR_Error')) return false; // load Kolab object from XML part $format->load($xml); if ($format->is_valid()) { $object = $format->to_array(array('_attachments' => $attachments)); $object['_type'] = $object_type; $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; $object['_formatobj'] = $format; + $object['_size'] = strlen($xml); return $object; } else { // try to extract object UID from XML block if (preg_match('!(.+)!Uims', $xml, $m)) $msgadd = " UID = " . trim(strip_tags($m[1])); rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd, ), true); } return false; } /** * Save an object in this folder. * * @param array $object The array that holds the data of the object. * @param string $type The type of the kolab object. * @param string $uid The UID of the old object if it existed before * * @return mixed False on error or IMAP message UID on success */ public function save(&$object, $type = null, $uid = null) { if (!$this->valid) { return false; } if (!$type) $type = $this->type; // copy attachments from old message $copyfrom = $object['_copyfrom'] ?: $object['_msguid']; if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) { foreach ((array)$old['_attachments'] as $key => $att) { if (!isset($object['_attachments'][$key])) { $object['_attachments'][$key] = $old['_attachments'][$key]; } // unset deleted attachment entries if ($object['_attachments'][$key] == false) { unset($object['_attachments'][$key]); } // load photo.attachment from old Kolab2 format to be directly embedded in xcard block else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) { if (!isset($object['photo'])) $object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']); unset($object['_attachments'][$key]); } } } // save contact photo to attachment for Kolab2 format if (kolab_storage::$version == '2.0' && $object['photo']) { $attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp $object['_attachments'][$attkey] = array( 'mimetype'=> rcube_mime::image_content_type($object['photo']), 'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']), ); } // process attachments if (is_array($object['_attachments'])) { $numatt = count($object['_attachments']); foreach ($object['_attachments'] as $key => $attachment) { // FIXME: kolab_storage and Roundcube attachment hooks use different fields! if (empty($attachment['content']) && !empty($attachment['data'])) { $attachment['content'] = $attachment['data']; unset($attachment['data'], $object['_attachments'][$key]['data']); } // make sure size is set, so object saved in cache contains this info if (!isset($attachment['size'])) { if (!empty($attachment['content'])) { if (is_resource($attachment['content'])) { // this need to be a seekable resource, otherwise // fstat() failes and we're unable to determine size // here nor in rcube_imap_generic before IMAP APPEND $stat = fstat($attachment['content']); $attachment['size'] = $stat ? $stat['size'] : 0; } else { $attachment['size'] = strlen($attachment['content']); } } else if (!empty($attachment['path'])) { $attachment['size'] = filesize($attachment['path']); } $object['_attachments'][$key] = $attachment; } // generate unique keys (used as content-id) for attachments if (is_numeric($key) && $key < $numatt) { // derrive content-id from attachment file name $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null; $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii if (!$basename) $basename = 'noname'; $cid = $basename . '.' . microtime(true) . $key . $ext; $object['_attachments'][$cid] = $attachment; unset($object['_attachments'][$key]); } } } // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) { $this->save_recurrence_exceptions($object, $type); } // check IMAP BINARY extension support for 'file' objects // allow configuration to workaround bug in Cyrus < 2.4.17 $rcmail = rcube::get_instance(); $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY'); // generate and save object message if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) { // resolve old msguid before saving if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) { $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; } $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary); // update cache with new UID if ($result) { $old_uid = $object['_msguid']; $object['_msguid'] = $result; $object['_mailbox'] = $this->name; if ($old_uid) { // delete old message $this->cache->imap_mode(true); $this->imap->delete_message($old_uid, $object['_mailbox']); $this->cache->imap_mode(false); } // insert/update message in cache $this->cache->save($result, $object, $old_uid); } // remove temp file if ($body_file) { @unlink($body_file); } } return $result; } /** * Save recurrence exceptions as individual objects. * The Kolab v2 format doesn't allow us to save fully embedded exception objects. * * @param array Hash array with event properties * @param string Object type */ private function save_recurrence_exceptions(&$object, $type = null) { if ($object['recurrence']['EXCEPTIONS']) { $exdates = array(); foreach ((array)$object['recurrence']['EXDATE'] as $exdate) { $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate); $exdates[$key] = 1; } // save every exception as individual object foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) { $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd')); $exception['sequence'] = $object['sequence'] + 1; if ($exception['thisandfuture']) { $exception['recurrence'] = $object['recurrence']; // adjust the recurrence duration of the exception if ($object['recurrence']['COUNT']) { $recurrence = new kolab_date_recurrence($object['_formatobj']); if ($end = $recurrence->end()) { unset($exception['recurrence']['COUNT']); $exception['recurrence']['UNTIL'] = $end; } } // set UNTIL date if we have a thisandfuture exception $untildate = clone $exception['start']; $untildate->sub(new DateInterval('P1D')); $object['recurrence']['UNTIL'] = $untildate; unset($object['recurrence']['COUNT']); } else { if (!$exdates[$exception['start']->format('Y-m-d')]) $object['recurrence']['EXDATE'][] = clone $exception['start']; unset($exception['recurrence']); } unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']); $this->save($exception, $type, $exception['uid']); } unset($object['recurrence']['EXCEPTIONS']); } } /** * Generate an object UID with the given recurrence-ID in a way that it is * unique (the original UID is not a substring) but still recoverable. */ private static function recurrence_exception_uid($uid, $recurrence_id) { $offset = -2; return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset); } /** * Delete the specified object from this folder. * * @param mixed $object The Kolab object to delete or object UID * @param boolean $expunge Should the folder be expunged? * * @return boolean True if successful, false on error */ public function delete($object, $expunge = true) { if (!$this->valid) { return false; } $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object); $success = false; $this->cache->imap_mode(true); if ($msguid && $expunge) { $success = $this->imap->delete_message($msguid, $this->name); } else if ($msguid) { $success = $this->imap->set_flag($msguid, 'DELETED', $this->name); } $this->cache->imap_mode(false); if ($success) { $this->cache->set($msguid, false); } return $success; } /** * */ public function delete_all() { if (!$this->valid) { return false; } $this->cache->purge(); $this->cache->imap_mode(true); $result = $this->imap->clear_folder($this->name); $this->cache->imap_mode(false); return $result; } /** * Restore a previously deleted object * * @param string Object UID * @return mixed Message UID on success, false on error */ public function undelete($uid) { if (!$this->valid) { return false; } if ($msguid = $this->cache->uid2msguid($uid, true)) { $this->cache->imap_mode(true); $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name); $this->cache->imap_mode(false); if ($result) { return $msguid; } } return false; } /** * Move a Kolab object message to another IMAP folder * * @param string Object UID * @param string IMAP folder to move object to * @return boolean True on success, false on failure */ 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->imap_mode(true); $result = $this->imap->move_message($msguid, $target_folder->name, $this->name); $this->cache->imap_mode(false); if ($result) { $new_uid = ($copyuid = $this->imap->conn->data['COPYUID']) ? $copyuid[1] : null; $this->cache->move($msguid, $uid, $target_folder, $new_uid); return true; } else { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(), ), true); } } return false; } /** * Creates source of the configuration object message * * @param array $object The array that holds the data of the object. * @param string $type The type of the kolab object. * @param bool $binary Enables use of binary encoding of attachment(s) * @param string $body_file Reference to filename of message body * * @return mixed Message as string or array with two elements * (one for message file path, second for message headers) */ private function build_message(&$object, $type, $binary, &$body_file) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) $format = $object['_formatobj']; else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) $format = $old['_formatobj']; // create new kolab_format instance if (!$format) $format = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) return false; $format->set($object); $xml = $format->write(kolab_storage::$version); $object['uid'] = $format->uid; // read UID from format $object['_formatobj'] = $format; if (empty($xml) || !$format->is_valid() || empty($object['uid'])) { return false; } $mime = new Mail_mime("\r\n"); $rcmail = rcube::get_instance(); $headers = array(); $files = array(); $part_id = 1; $encoding = $binary ? 'binary' : 'base64'; if ($user_email = $rcmail->get_user_email()) { $headers['From'] = $user_email; $headers['To'] = $user_email; } $headers['Date'] = date('r'); $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type; $headers['X-Kolab-Mime-Version'] = kolab_storage::$version; $headers['Subject'] = $object['uid']; // $headers['Message-ID'] = $rcmail->gen_message_id(); $headers['User-Agent'] = $rcmail->config->get('useragent'); // Check if we have enough memory to handle the message in it // It's faster than using files, so we'll do this if we only can if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) { $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB foreach ($object['_attachments'] as $attachment) { $memory += $attachment['size']; } // 1.33 is for base64, we need at least 4x more memory than the message size if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) { $marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%'; $is_file = true; $temp_dir = unslashify($rcmail->config->get('temp_dir')); $mime->setParam('delay_file_io', true); } } $mime->headers($headers); $mime->setTXTBody("This is a Kolab Groupware object. " . "To view this object you will need an email client that understands the Kolab Groupware format. " . "For a list of such email clients please visit http://www.kolab.org/\n\n"); $ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE; // Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines" // when APPENDing from temp file $xml = preg_replace('/\r?\n/', "\r\n", $xml); $mime->addAttachment($xml, // file $ctype, // content-type 'kolab.xml', // filename false, // is_file '8bit', // encoding 'attachment', // disposition RCUBE_CHARSET // charset ); $part_id++; // save object attachments as separate parts foreach ((array)$object['_attachments'] as $key => $att) { if (empty($att['content']) && !empty($att['id'])) { // @TODO: use IMAP CATENATE to skip attachment fetch+push operation $msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']); if ($is_file) { $att['path'] = tempnam($temp_dir, 'rcmAttmnt'); if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) { fclose($fp); } else { return false; } } else { $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true); } } $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable')); $name = !empty($att['name']) ? $att['name'] : $key; // To store binary files we can use faster method // without writting full message content to a temporary file but // directly to IMAP, see rcube_imap_generic::append(). // I.e. use file handles where possible if (!empty($att['path'])) { if ($is_file && $binary) { $files[] = fopen($att['path'], 'r'); $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } else { $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } } else { if (is_resource($att['content']) && $is_file && $binary) { $files[] = $att['content']; $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } else { if (is_resource($att['content'])) { @rewind($att['content']); $att['content'] = stream_get_contents($att['content']); } $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } } $object['_attachments'][$key]['id'] = ++$part_id; } if (!$is_file || !empty($files)) { $message = $mime->getMessage(); } // parse message and build message array with // attachment file pointers in place of file markers if (!empty($files)) { $message = explode($marker, $message); $tmp = array(); foreach ($message as $msg_part) { $tmp[] = $msg_part; if ($file = array_shift($files)) { $tmp[] = $file; } } $message = $tmp; } // write complete message body into temp file else if ($is_file) { // use common temp dir $body_file = tempnam($temp_dir, 'rcmMsg'); if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) { rcube::raise_error(array('code' => 650, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not create message: ".$mime_result->getMessage()), true, false); return false; } $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r')); } return $message; } /** * Triggers any required updates after changes within the * folder. This is currently only required for handling free/busy * information with Kolab. * * @return boolean|PEAR_Error True if successfull. */ public function trigger() { $owner = $this->get_owner(); $result = false; switch($this->type) { case 'event': if ($this->get_namespace() == 'personal') { $result = $this->trigger_url( sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), urlencode($owner), urlencode($this->imap->mod_folder($this->name)) ), $this->imap->options['user'], $this->imap->options['password'] ); } break; default: return true; } if ($result && is_object($result) && is_a($result, 'PEAR_Error')) { return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s", $this->name, $result->getMessage())); } return $result; } /** * Triggers a URL. * * @param string $url The URL to be triggered. * @param string $auth_user Username to authenticate with * @param string $auth_passwd Password for basic auth * @return boolean|PEAR_Error True if successfull. */ private function trigger_url($url, $auth_user = null, $auth_passwd = null) { try { $request = libkolab::http_request($url); // set authentication credentials if ($auth_user && $auth_passwd) $request->setAuth($auth_user, $auth_passwd); $result = $request->send(); // rcube::write_log('trigger', $result->getBody()); } catch (Exception $e) { return PEAR::raiseError($e->getMessage()); } return true; } }