From 18d8fec133bba66bc177221b0727dbdf9424935c Mon Sep 17 00:00:00 2001 From: Thomas B Date: Wed, 2 May 2012 17:41:02 +0200 Subject: [PATCH] First implementation of a caching layer for kolab_storage; - Caching is disabled by default (until fully functional and tested) - Attention: database initialization required for cache) Silently ignore old Kolab2 objects if no Horde classes found to parse them. --- plugins/libkolab/SQL/mysql.sql | 20 + plugins/libkolab/lib/kolab_format.php | 26 +- plugins/libkolab/lib/kolab_format_contact.php | 28 +- .../lib/kolab_format_distributionlist.php | 21 +- plugins/libkolab/lib/kolab_format_event.php | 23 +- plugins/libkolab/lib/kolab_storage_cache.php | 443 ++++++++++++++++++ plugins/libkolab/lib/kolab_storage_folder.php | 124 ++--- 7 files changed, 603 insertions(+), 82 deletions(-) create mode 100644 plugins/libkolab/SQL/mysql.sql create mode 100644 plugins/libkolab/lib/kolab_storage_cache.php diff --git a/plugins/libkolab/SQL/mysql.sql b/plugins/libkolab/SQL/mysql.sql new file mode 100644 index 00000000..376eb4fd --- /dev/null +++ b/plugins/libkolab/SQL/mysql.sql @@ -0,0 +1,20 @@ +/** + * libkolab database schema + * + * @version @package_version@ + * @author Thomas Bruederli + * @licence GNU AGPL + **/ + +CREATE TABLE `kolab_cache` ( + `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL, + `type` VARCHAR(32) CHARACTER SET ascii NOT NULL, + `msguid` BIGINT UNSIGNED NOT NULL, + `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL, + `data` TEXT NOT NULL, + `xml` TEXT NOT NULL, + `dtstart` DATETIME, + `dtend` DATETIME, + `tags` VARCHAR(255) NOT NULL, + PRIMARY KEY(`resource`,`type`,`msguid`) +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php index 42ecaab8..134df856 100644 --- a/plugins/libkolab/lib/kolab_format.php +++ b/plugins/libkolab/lib/kolab_format.php @@ -31,11 +31,17 @@ abstract class kolab_format protected $obj; protected $data; + protected $xmldata; + protected $loaded = false; /** * Factory method to instantiate a kolab_format object of the given type + * + * @param string Object type to instantiate + * @param string Cached xml data to initialize with + * @return object kolab_format */ - public static function factory($type) + public static function factory($type, $xmldata = null) { if (!isset(self::$timezone)) self::$timezone = new DateTimeZone('UTC'); @@ -43,7 +49,7 @@ abstract class kolab_format $suffix = preg_replace('/[^a-z]+/', '', $type); $classname = 'kolab_format_' . $suffix; if (class_exists($classname)) - return new $classname(); + return new $classname($xmldata); return PEAR::raiseError(sprintf("Failed to load Kolab Format wrapper for type %s", $type)); } @@ -165,10 +171,24 @@ abstract class kolab_format } } + /** + * Initialize libkolabxml object with cached xml data + */ + protected function init() + { + if (!$this->loaded) { + if ($this->xmldata) { + $this->load($this->xmldata); + $this->xmldata = null; + } + $this->loaded = true; + } + } + /** * Direct getter for object properties */ - function __get($var) + public function __get($var) { return $this->data[$var]; } diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php index 69db2d15..59a6e0a0 100644 --- a/plugins/libkolab/lib/kolab_format_contact.php +++ b/plugins/libkolab/lib/kolab_format_contact.php @@ -101,9 +101,10 @@ class kolab_format_contact extends kolab_format /** * Default constructor */ - function __construct() + function __construct($xmldata = null) { $this->obj = new Contact; + $this->xmldata = $xmldata; // complete phone types $this->phonetypes['homefax'] |= Telephone::Home; @@ -118,6 +119,7 @@ class kolab_format_contact extends kolab_format public function load($xml) { $this->obj = kolabformat::readContact($xml, false); + $this->loaded = true; } /** @@ -127,9 +129,14 @@ class kolab_format_contact extends kolab_format */ public function write() { - $xml = kolabformat::writeContact($this->obj); - parent::update_uid(); - return $xml; + $this->init(); + + if ($this->obj->isValid()) { + $this->xmldata = kolabformat::writeContact($this->obj); + parent::update_uid(); + } + + return $this->xmldata; } /** @@ -139,6 +146,8 @@ class kolab_format_contact extends kolab_format */ public function set(&$object) { + $this->init(); + // set some automatic values if missing if (false && !$this->obj->created()) { if (!empty($object['created'])) @@ -255,9 +264,10 @@ class kolab_format_contact extends kolab_format if ($type = rc_image_content_type($object['photo'])) $this->obj->setPhoto($object['photo'], $type); } - else if (isset($object['photo'])) { + else if (isset($object['photo'])) $this->obj->setPhoto('',''); - } + else if ($this->obj->photoMimetype()) // load saved photo for caching + $object['photo'] = $this->obj->photo(); // spouse and children are relateds $rels = new vectorrelated; @@ -299,8 +309,8 @@ class kolab_format_contact extends kolab_format // cache this data - unset($object['_formatobj']); $this->data = $object; + unset($this->data['_formatobj']); } /** @@ -308,7 +318,7 @@ class kolab_format_contact extends kolab_format */ public function is_valid() { - return $this->data || (is_object($this->obj) && true /*$this->obj->isValid()*/); + return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/); } /** @@ -322,6 +332,8 @@ class kolab_format_contact extends kolab_format if (!empty($this->data)) return $this->data; + $this->init(); + // read object properties into local data object $object = array( 'uid' => $this->obj->uid(), diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php index 3c5047c3..b8d22085 100644 --- a/plugins/libkolab/lib/kolab_format_distributionlist.php +++ b/plugins/libkolab/lib/kolab_format_distributionlist.php @@ -26,9 +26,10 @@ class kolab_format_distributionlist extends kolab_format { public $CTYPE = 'application/vcard+xml'; - function __construct() + function __construct($xmldata = null) { $this->obj = new DistList; + $this->xmldata = $xmldata; } /** @@ -39,6 +40,7 @@ class kolab_format_distributionlist extends kolab_format public function load($xml) { $this->obj = kolabformat::readDistlist($xml, false); + $this->loaded = true; } /** @@ -48,13 +50,20 @@ class kolab_format_distributionlist extends kolab_format */ public function write() { - $xml = kolabformat::writeDistlist($this->obj); - parent::update_uid(); - return $xml; + $this->init(); + + if ($this->obj->isValid()) { + $this->xmldata = kolabformat::writeDistlist($this->obj); + parent::update_uid(); + } + + return $this->xmldata; } public function set(&$object) { + $this->init(); + // set some automatic values if missing if (!empty($object['uid'])) $this->obj->setUid($object['uid']); @@ -79,8 +88,8 @@ class kolab_format_distributionlist extends kolab_format $this->obj->setMembers($members); // cache this data - unset($object['_formatobj']); $this->data = $object; + unset($this->data['_formatobj']); } public function is_valid() @@ -122,6 +131,8 @@ class kolab_format_distributionlist extends kolab_format if (!empty($this->data)) return $this->data; + $this->init(); + // read object properties $object = array( 'uid' => $this->obj->uid(), diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php index 78e51ca4..0552025e 100644 --- a/plugins/libkolab/lib/kolab_format_event.php +++ b/plugins/libkolab/lib/kolab_format_event.php @@ -91,9 +91,10 @@ class kolab_format_event extends kolab_format /** * Default constructor */ - function __construct() + function __construct($xmldata = null) { $this->obj = new Event; + $this->xmldata = $xmldata; } /** @@ -104,6 +105,7 @@ class kolab_format_event extends kolab_format public function load($xml) { $this->obj = kolabformat::readEvent($xml, false); + $this->loaded = true; } /** @@ -113,9 +115,14 @@ class kolab_format_event extends kolab_format */ public function write() { - $xml = kolabformat::writeEvent($this->obj); - parent::update_uid(); - return $xml; + $this->init(); + + if ($this->obj->isValid()) { + $this->xmldata = kolabformat::writeEvent($this->obj); + parent::update_uid(); + } + + return $this->xmldata; } /** @@ -125,6 +132,8 @@ class kolab_format_event extends kolab_format */ public function set(&$object) { + $this->init(); + // set some automatic values if missing if (!$this->obj->created()) { if (!empty($object['created'])) @@ -293,8 +302,8 @@ class kolab_format_event extends kolab_format $this->obj->setAttachments($vattach); // cache this data - unset($object['_formatobj']); $this->data = $object; + unset($this->data['_formatobj']); } /** @@ -302,7 +311,7 @@ class kolab_format_event extends kolab_format */ public function is_valid() { - return $this->data || (is_object($this->obj) && $this->obj->isValid()); + return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()); } /** @@ -316,6 +325,8 @@ class kolab_format_event extends kolab_format if (!empty($this->data)) return $this->data; + $this->init(); + $sensitivity_map = array_flip($this->sensitivity_map); // read object properties diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php new file mode 100644 index 00000000..cfcacf51 --- /dev/null +++ b/plugins/libkolab/lib/kolab_storage_cache.php @@ -0,0 +1,443 @@ + + * + * 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 +{ + private $db; + private $imap; + private $folder; + private $uid2msg; + private $objects; + private $resource_uri; + private $enabled = true; + private $synched = false; + private $ready = false; + + private $binary_cols = array('photo','pgppublickey','pkcs7publickey'); + + + /** + * 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); + + 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; + } + + // strip namespace prefix from folder name + $ns = $this->folder->get_namespace(); + $nsdata = $this->imap->get_namespace($ns); + if (is_array($nsdata[0]) && strpos($this->folder->name, $nsdata[0][0]) === 0) { + $subpath = substr($this->folder->name, strlen($nsdata[0][0])); + if ($ns == 'other') { + list($user, $suffix) = explode($nsdata[0][1], $subpath); + $subpath = $suffix; + } + } + else { + $subpath = $this->folder->name; + } + + // compose fully qualified ressource uri for this instance + $this->resource_uri = 'imap://' . urlencode($this->folder->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath; + $this->ready = $this->enabled; + } + + + /** + * Synchronize local cache data with remote + */ + public function synchronize() + { + // only sync once per request cycle + if ($this->synched) + return; + + // 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 + $sql_result = $this->db->query( + "SELECT msguid, uid FROM kolab_cache WHERE resource=?", + $this->resource_uri + ); + + $old_index = array(); + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $old_index[] = $sql_arr['msguid']; + $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid']; + } + + // fetch new objects from imap + $fetch_index = array_diff($this->index, $old_index); + foreach ($this->_fetch($fetch_index, '*') as $object) { + $msguid = $object['_msguid']; + $this->set($msguid, $object); + } + + // delete invalid entries from local DB + $del_index = array_diff($old_index, $this->index); + if (!empty($del_index)) { + $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index)); + $this->db->query( + "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)", + $this->resource_uri + ); + } + } + + $this->synched = time(); + } + + + /** + * Read a single entry from cache or + */ + public function get($msguid, $type = null, $folder = null) + { + // load object if not in memory + if (!isset($this->objects[$msguid])) { + if ($this->ready) { + // TODO: handle $folder != $this->folder->name situations + + $sql_result = $this->db->query( + "SELECT * FROM kolab_cache ". + "WHERE resource=? AND msguid=?", + $this->resource_uri, + $msguid + ); + + if ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $this->objects[$msguid] = $this->_unserialize($sql_arr); + } + } + + // fetch from IMAP if not present in cache + if (empty($this->objects[$msguid])) { + $result = $this->_fetch(array($msguid), $type, $folder); + $this->objects[$msguid] = $result[0]; + } + } + + return $this->objects[$msguid]; + } + + + /** + * + */ + public function set($msguid, $object, $folder = null) + { + // write to cache + if ($this->ready) { + // TODO: handle $folder != $this->folder->name situations + + // remove old entry + $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=?", + $this->resource_uri, $msguid); + + // write new object data if not false (wich means deleted) + if ($object) { + $sql_data = $this->_serialize($object); + $objtype = $object['_type'] ? $object['_type'] : $this->folder->type; + + $result = $this->db->query( + "INSERT INTO kolab_cache ". + " (resource, type, msguid, uid, data, xml, dtstart, dtend)". + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + $this->resource_uri, + $objtype, + $msguid, + $object['uid'], + $sql_data['data'], + $sql_data['xml'], + $sql_data['dtstart'], + $sql_data['dtend'] + ); + + if (!$this->db->affected_rows($result)) { + rcmail::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[$msguid] = $object; + + if ($object) + $this->uid2msg[$object['uid']] = $msguid; + } + + + /** + * Remove all objects from local cache + */ + public function purge($type = null) + { + $result = $this->db->query( + "DELETE FROM kolab_cache WHERE resource=?". + ($type ? ' AND type=?' : ''), + $this->resource_uri, + $type + ); + return $this->db->affected_rows($result); + } + + + /** + * 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) { + $sql_result = $this->db->query( + "SELECT * FROM kolab_cache ". + "WHERE resource=? " . $this->_sql_where($query), + $this->resource_uri + ); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + if ($object = $this->_unserialize($sql_arr)) + $result[] = $object; + } + } + else { + // extract object type from query parameter + $filter = $this->_query2assoc($query); + $result = $this->_fetch($this->index, $filter['type']); + + // TODO: post-filter result according to query + } + + return $result; + } + + + /** + * Get number of objects mathing the given query + * + * @param string $type Object type (e.g. contact, event, todo, journal, note, configuration) + * @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) { + $sql_result = $this->db->query( + "SELECT COUNT(*) AS NUMROWS FROM kolab_cache ". + "WHERE resource=? " . $this->_sql_where($query), + $this->resource_uri + ); + + $sql_arr = $this->db->fetch_assoc($sql_result); + $count = intval($sql_arr['NUMROWS']); + } + 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 compose a valid SQL query from pseudo filter triplets + */ + private function _sql_where($query) + { + $sql_where = ''; + foreach ($query as $param) { + $sql_where .= sprintf(' AND %s%s%s', + $this->db->quote_identifier($param[0]), + $param[1], + $this->db->quote($param[2]) + ); + } + return $sql_where; + } + + /** + * 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->uid2msg[$object['uid']] = $msguid; + } + } + + return $results; + } + + + /** + * Helper method to convert the given Kolab object into a dataset to be written to cache + */ + private function _serialize($object) + { + $bincols = array_flip($this->binary_cols); + $sql_data = array('dtstart' => null, 'dtend' => null, 'xml' => ''); + + // set type specific values + if ($this->folder->type == 'event') { + // database runs in server's timetone so using date() is safe + $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']); + $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']); + } + + if ($object['_formatobj']) + $sql_data['xml'] = (string)$object['_formatobj']->write(); + + // 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 ($key[0] != '_') { + $data[$key] = $val; + } + else if ($key == '_attachments') { + foreach ($val as $k => $att) { + unset($att['content']); + if ($att['id']) + $data[$key][$k] = $att; + } + } + } + + $sql_data['data'] = serialize($data); + return $sql_data; + } + + /** + * Helper method to turn stored cache data into a valid storage object + */ + private function _unserialize($sql_arr) + { + $object = unserialize($sql_arr['data']); + + // decode binary properties + foreach ($this->binary_cols as $key) { + if (!empty($object[$key])) + $object[$key] = base64_decode($object[$key]); + } + + // add meta data + $object['_type'] = $sql_arr['type']; + $object['_msguid'] = $sql_arr['msguid']; + $object['_mailbox'] = $this->folder->name; + $object['_formatobj'] = kolab_format::factory($sql_arr['type'], $sql_arr['xml']); + + return $object; + } + + /** + * 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 73b6dbba..246e58bb 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -40,11 +40,10 @@ class kolab_storage_folder public $type; private $type_annotation; - private $subpath; private $imap; private $info; private $owner; - private $objcache = array(); + private $cache; private $uid2msg = array(); @@ -54,7 +53,8 @@ class kolab_storage_folder function __construct($name, $imap = null) { $this->imap = is_object($imap) ? $imap : rcube::get_instance()->get_storage(); - $this->imap->set_options(array('skip_deleted' => false)); + $this->imap->set_options(array('skip_deleted' => true)); + $this->cache = new kolab_storage_cache($this); $this->set_folder($name); } @@ -72,6 +72,8 @@ class kolab_storage_folder $metadata = $this->imap->get_metadata($this->name, array(kolab_storage::CTYPE_KEY)); $this->type_annotation = $metadata[$this->name][kolab_storage::CTYPE_KEY]; $this->type = reset(explode('.', $this->type_annotation)); + + $this->cache->set_folder($this); } @@ -232,13 +234,12 @@ class kolab_storage_folder { if (!$type) $type = $this->type; - // search by object type - $ctype = self::KTYPE_PREFIX . $type; - $index = $this->imap->search_once($this->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype); + // TODO: synchronize cache first? - return $index->count(); + return $this->cache->count(array(array('type','=',$type))); } + /** * List all Kolab objects of the given type * @@ -249,8 +250,15 @@ class kolab_storage_folder { if (!$type) $type = $this->type; - $ctype = self::KTYPE_PREFIX . $type; + // synchronize caches + $this->cache->synchronize(); + + // fetch objects from cache + return $this->cache->select(array(array('type','=',$type))); + +/* $results = array(); + $ctype = self::KTYPE_PREFIX . $type; // use 'list' for folder's default objects if ($type == $this->type) { @@ -272,6 +280,7 @@ class kolab_storage_folder // TODO: write $this->uid2msg to cache return $results; +*/ } @@ -283,8 +292,11 @@ class kolab_storage_folder */ public function get_object($uid) { - $msguid = $this->uid2msguid($uid); - if ($msguid && ($object = $this->read_object($msguid))) + // synchronize caches + $this->cache->synchronize(); + + $msguid = $this->cache->uid2msguid($uid); + if ($msguid && ($object = $this->cache->get($msguid))) return $object; return false; @@ -303,10 +315,8 @@ class kolab_storage_folder */ public function get_attachment($uid, $part, $mailbox = null) { - if ($msguid = ($mailbox ? $uid : $this->uid2msguid($uid))) { - if ($mailbox) - $this->imap->set_folder($mailbox); - + if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) { + $this->imap->set_folder($mailbox ? $mailbox : $this->name); return $this->imap->get_message_part($msguid, $part); } @@ -319,25 +329,23 @@ class kolab_storage_folder * the Kolab groupware object from it * * @param string The IMAP message UID to fetch - * @param string The object type expected + * @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 */ - private function read_object($msguid, $type = null, $folder = null) + public function read_object($msguid, $type = null, $folder = null) { if (!$type) $type = $this->type; if (!$folder) $folder = $this->name; - $ctype = self::KTYPE_PREFIX . $type; - - // requested message in local cache - if ($this->objcache[$msguid]) - return $this->objcache[$msguid]; $this->imap->set_folder($folder); - // check ctype header and abort on mismatch $headers = $this->imap->get_message_headers($msguid); - if ($headers->others['x-kolab-type'] != $ctype) + $object_type = substr($headers->others['x-kolab-type'], strlen(self::KTYPE_PREFIX)); + $content_type = self::KTYPE_PREFIX . $object_type; + + // check object type header and abort on mismatch + if ($type != '*' && $object_type != $type) return false; $message = new rcube_message($msguid); @@ -345,7 +353,7 @@ class kolab_storage_folder // get XML part foreach ((array)$message->attachments as $part) { - if (!$xml && ($part->mimetype == $ctype || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) { + if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) { $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id); } else if ($part->filename) { @@ -368,14 +376,17 @@ class kolab_storage_folder return false; } - $format = kolab_format::factory($type); + $format = kolab_format::factory($object_type); + + if (is_a($format, 'PEAR_Error')) + return false; // check kolab format version - if (strpos($xml, '<' . $type) !== false) { + if (strpos($xml, '<' . $object_type) !== false) { // old Kolab 2.0 format detected - $handler = Horde_Kolab_Format::factory('XML', $type); - if (is_object($handler) && is_a($handler, 'PEAR_Error')) { - continue; + $handler = class_exists('Horde_Kolab_Format') ? Horde_Kolab_Format::factory('XML', $object_type) : null; + if (!is_object($handler) || is_a($handler, 'PEAR_Error')) { + return false; } // XML-to-array @@ -389,12 +400,12 @@ class kolab_storage_folder if ($format->is_valid()) { $object = $format->to_array(); + $object['_type'] = $object_type; $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; $object['_attachments'] = array_merge((array)$object['_attachments'], $attachments); $object['_formatobj'] = $format; - $this->objcache[$msguid] = $object; return $object; } @@ -416,8 +427,8 @@ class kolab_storage_folder $type = $this->type; // copy attachments from old message - if (!empty($object['_msguid']) && ($old = $this->read_object($object['_msguid'], $type, $object['_mailbox']))) { - foreach ($old['_attachments'] as $name => $att) { + if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) { + foreach ((array)$old['_attachments'] as $name => $att) { if (!isset($object['_attachments'][$name])) { $object['_attachments'][$name] = $old['_attachments'][$name]; } @@ -435,13 +446,18 @@ class kolab_storage_folder // delete old message if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) { $this->imap->delete_message($object['_msguid'], $object['_mailbox']); + $this->cache->set($object['_msguid'], false, $object['_mailbox']); } - else if ($result && $uid && ($msguid = $this->uid2msguid($uid))) { + else if ($result && $uid && ($msguid = $this->cache->uid2msguid($uid))) { $this->imap->delete_message($msguid, $this->name); + $this->cache->set($object['_msguid'], false); } - // TODO: update cache with new UID - $this->uid2msg[$object['uid']] = $result; + // update cache with new UID + if ($result) { + $object['_msguid'] = $result; + $this->cache->set($result, $object); + } } return $result; @@ -459,16 +475,21 @@ class kolab_storage_folder */ public function delete($object, $expunge = true, $trigger = true) { - $msguid = is_array($object) ? $object['_msguid'] : $this->uid2msguid($object); + $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object); + $success = false; if ($msguid && $expunge) { - return $this->imap->delete_message($msguid, $this->name); + $success = $this->imap->delete_message($msguid, $this->name); } else if ($msguid) { - return $this->imap->set_flag($msguid, 'DELETED', $this->name); + $success = $this->imap->set_flag($msguid, 'DELETED', $this->name); } - return false; + if ($success) { + $this->cache->set($result, false); + } + + return $success; } @@ -477,6 +498,7 @@ class kolab_storage_folder */ public function delete_all() { + $this->cache->purge(); return $this->imap->clear_folder($this->name); } @@ -489,7 +511,7 @@ class kolab_storage_folder */ public function undelete($uid) { - if ($msguid = $this->uid2msguid($uid, true)) { + if ($msguid = $this->cache->uid2msguid($uid, true)) { if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) { return $msguid; } @@ -508,7 +530,7 @@ class kolab_storage_folder */ public function move($uid, $target_folder) { - if ($msguid = $this->uid2msguid($uid)) { + if ($msguid = $this->cache->uid2msguid($uid)) { if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) { // TODO: update cache return true; @@ -526,24 +548,6 @@ class kolab_storage_folder } - /** - * Resolve an object UID into an IMAP message UID - */ - private function uid2msguid($uid, $deleted = false) - { - if (!isset($this->uid2msg[$uid])) { - // use IMAP SEARCH to get the right message - $index = $this->imap->search_once($this->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid); - $results = $index->get(); - $this->uid2msg[$uid] = $results[0]; - - // TODO: cache this lookup - } - - return $this->uid2msg[$uid]; - } - - /** * Creates source of the configuration object message */ @@ -552,7 +556,7 @@ class kolab_storage_folder // 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->read_object($object['_msguid'], $type, $object['_mailbox']))) + else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) $format = $old['_formatobj']; // create new kolab_format instance