Branch 'dev/kolab3' - plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Wed May 2 17:42:48 CEST 2012


 plugins/libkolab/SQL/mysql.sql                         |   20 
 plugins/libkolab/lib/kolab_format.php                  |   26 
 plugins/libkolab/lib/kolab_format_contact.php          |   28 -
 plugins/libkolab/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(-)

New commits:
commit 18d8fec133bba66bc177221b0727dbdf9424935c
Author: Thomas B <roundcube at gmail.com>
Date:   Wed May 2 17:41:02 2012 +0200

    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.

diff --git a/plugins/libkolab/SQL/mysql.sql b/plugins/libkolab/SQL/mysql.sql
new file mode 100644
index 0000000..376eb4f
--- /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 42ecaab..134df85 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));
     }
@@ -166,9 +172,23 @@ 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 69db2d1..59a6e0a 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 3c5047c..b8d2208 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 78e51ca..0552025 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 0000000..cfcacf5
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -0,0 +1,443 @@
+<?php
+
+/**
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_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('<colname>', '<comparator>', '<value>')
+     * @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 73b6dbb..246e58b 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;
@@ -527,24 +549,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
      */
     private function build_message(&$object, $type)
@@ -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





More information about the commits mailing list