Branch 'roundcubemail-plugins-kolab-3.1' - 13 commits - plugins/calendar plugins/kolab_addressbook plugins/kolab_config plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Mon Feb 10 16:55:33 CET 2014


 plugins/calendar/drivers/kolab/kolab_calendar.php        |    2 
 plugins/kolab_addressbook/lib/rcube_kolab_contacts.php   |  156 +++++++-----
 plugins/kolab_config/kolab_config.php                    |    2 
 plugins/libkolab/SQL/mysql/2014021000.sql                |    9 
 plugins/libkolab/bin/modcache.sh                         |   29 ++
 plugins/libkolab/bin/randomcontacts.sh                   |  181 +++++++++++++++
 plugins/libkolab/lib/kolab_storage_cache.php             |   79 +++++-
 plugins/libkolab/lib/kolab_storage_cache_contact.php     |   16 +
 plugins/libkolab/lib/kolab_storage_dataset.php           |  154 ++++++++++++
 plugins/libkolab/lib/kolab_storage_folder.php            |   16 +
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |    8 
 11 files changed, 559 insertions(+), 93 deletions(-)

New commits:
commit dca5f2bef6829871e73a55d464f002983faad536
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 10 16:01:49 2014 +0100

    Avoid null values for empty fields

diff --git a/plugins/libkolab/lib/kolab_storage_cache_contact.php b/plugins/libkolab/lib/kolab_storage_cache_contact.php
index b70b37f..9666a39 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -49,6 +49,10 @@ class kolab_storage_cache_contact extends kolab_storage_cache
         if (is_array($sql_data['email'])) {
             $sql_data['email'] = $sql_data['email']['address'];
         }
+        // avoid value being null
+        if (empty($sql_data['email'])) {
+            $sql_data['email'] = '';
+        }
 
         return $sql_data;
     }


commit cfd38ccde982b230dfad1426c5d9d1fc85417cb5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 10 14:10:36 2014 +0100

    Remove non-unicode characters from sort cols

diff --git a/plugins/libkolab/lib/kolab_storage_cache_contact.php b/plugins/libkolab/lib/kolab_storage_cache_contact.php
index fde27bc..b70b37f 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -41,10 +41,10 @@ class kolab_storage_cache_contact extends kolab_storage_cache
         $sql_data['type'] = $object['_type'];
 
         // columns for sorting
-        $sql_data['name']      = $object['name'] . $object['prefix'];
-        $sql_data['firstname'] = $object['firstname'] . $object['middlename'] . $object['surname'];
-        $sql_data['surname']   = $object['surname']   . $object['firstname']  . $object['middlename'];
-        $sql_data['email']     = is_array($object['email']) ? $object['email'][0] : $object['email'];
+        $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'];


commit d9e9db7c6f188aed1be28bfa7d44b56e2b68c9c6
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Mon Feb 10 11:46:50 2014 +0100

    Optimize access to kolab contacts using a sorted and limited query (#2828)
    - Add columns for sorting in kolab_cache_contact
    - Extend bin/modcache.sh script to update existing cache records
    - Add setters for ORDER BY and LIMIT clauses
    - Adapt the kolab_addressbook plugin to fetch contacts page-wise
    
    ATTENTION: This changeset contains database schema changes!
    Run `bin/updatedb.sh --dir plugins/libkolab/SQL --package libkolab`
    
    Afterwards, the cached data needs to be updated. To do so, either run
      `plugins/libkolab/bin/modcache.sh update --type=contact`
    or execute the following query
      DELETE FROM `kolab_folders` WHERE `type`='contact';

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 6f74e30..c8642fc 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -275,6 +275,7 @@ class rcube_kolab_contacts extends rcube_addressbook
     public function list_records($cols = null, $subset = 0, $nocount = false)
     {
         $this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
+        $fetch_all = false;
 
         // list member of the selected group
         if ($this->gid) {
@@ -298,12 +299,13 @@ class rcube_kolab_contacts extends rcube_addressbook
                 else if (!empty($member['email'])) {
                     $this->contacts[$member['ID']] = $member;
                     $local_sortindex[$member['ID']] = $this->_sort_string($member);
+                    $fetch_all = true;
                 }
             }
 
             // get members by UID
             if (!empty($uids)) {
-                $this->_fetch_contacts(array(array('uid', '=', $uids)));
+                $this->_fetch_contacts($query = array(array('uid', '=', $uids)), !$fetch_all);
                 $this->sortindex = array_merge($this->sortindex, $local_sortindex);
             }
         }
@@ -311,26 +313,34 @@ class rcube_kolab_contacts extends rcube_addressbook
             $ids = $this->filter['ids'];
             if (count($ids)) {
                 $uids = array_map(array($this, 'id2uid'), $this->filter['ids']);
-                $this->_fetch_contacts(array(array('uid', '=', $uids)));
+                $this->_fetch_contacts($query = array(array('uid', '=', $uids)), true);
             }
         }
         else {
-            $this->_fetch_contacts();
+            $this->_fetch_contacts($query = array(), true);
         }
 
-        // sort results (index only)
-        asort($this->sortindex, SORT_LOCALE_STRING);
-        $ids = array_keys($this->sortindex);
+        if ($fetch_all) {
+            // sort results (index only)
+            asort($this->sortindex, SORT_LOCALE_STRING);
+            $ids = array_keys($this->sortindex);
 
-        // fill contact data into the current result set
-        $this->result->count = count($ids);
-        $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
-        $last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
+            // fill contact data into the current result set
+            $this->result->count = count($ids);
+            $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
+            $last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
 
-        for ($i = $start_row; $i < $last_row; $i++) {
-            if (array_key_exists($i, $ids)) {
-                $idx = $ids[$i];
-                $this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
+            for ($i = $start_row; $i < $last_row; $i++) {
+                if (array_key_exists($i, $ids)) {
+                    $idx = $ids[$i];
+                    $this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
+                }
+            }
+        }
+        else {
+            $this->result->count = $this->storagefolder->count($query);
+            foreach ($this->dataset as $idx => $record) {
+                $this->result->add($this->_to_rcube_contact($record));
             }
         }
 
@@ -966,9 +976,12 @@ class rcube_kolab_contacts extends rcube_addressbook
     /**
      * Query storage layer and store records in private member var
      */
-    private function _fetch_contacts($query = array())
+    private function _fetch_contacts($query = array(), $limit = false)
     {
         if (!isset($this->dataset) || !empty($query)) {
+            if ($limit) {
+                $this->storagefolder->set_order_and_limit($this->_sort_columns(), $this->page_size, ($this->list_page-1) * $this->page_size);
+            }
             $this->sortindex = array();
             $this->dataset = $this->storagefolder->select($query);
             foreach ($this->dataset as $idx => $record) {
@@ -1006,6 +1019,29 @@ class rcube_kolab_contacts extends rcube_addressbook
     }
 
     /**
+     * Return the cache table columns to order by
+     */
+    private function _sort_columns()
+    {
+        $sortcols = array();
+
+        switch ($this->sort_col) {
+        case 'name':
+            $sortcols[] = 'name';
+        case 'firstname':
+            $sortcols[] = 'firstname';
+            break;
+
+        case 'surname':
+            $sortcols[] = 'surname';
+            break;
+        }
+
+        $sortcols[] = 'email';
+        return $sortcols;
+    }
+
+    /**
      * Read distribution-lists AKA groups from server
      */
     private function _fetch_groups($with_contacts = false)
diff --git a/plugins/libkolab/SQL/mysql/2014021000.sql b/plugins/libkolab/SQL/mysql/2014021000.sql
new file mode 100644
index 0000000..31ce699
--- /dev/null
+++ b/plugins/libkolab/SQL/mysql/2014021000.sql
@@ -0,0 +1,9 @@
+ALTER TABLE `kolab_cache_contact` ADD `name` VARCHAR(255) NOT NULL,
+  ADD `firstname` VARCHAR(255) NOT NULL,
+  ADD `surname` VARCHAR(255) NOT NULL,
+  ADD `email` VARCHAR(255) NOT NULL;
+
+-- updating or clearing all contacts caches is required.
+-- either run `bin/modcache.sh update --type=contact` or execute the following query:
+--   DELETE FROM `kolab_folders` WHERE `type`='contact';
+
diff --git a/plugins/libkolab/bin/modcache.sh b/plugins/libkolab/bin/modcache.sh
index da6e4f8..550a7d6 100755
--- a/plugins/libkolab/bin/modcache.sh
+++ b/plugins/libkolab/bin/modcache.sh
@@ -7,7 +7,7 @@
  * @version 3.1
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -65,6 +65,7 @@ $db->db_connect('w');
 if (!$db->is_connected() || $db->is_error())
     die("No DB connection\n");
 
+ini_set('display_errors', 1);
 
 /*
  * Script controller
@@ -142,6 +143,32 @@ case 'prewarm':
         die("Authentication failed for " . $opts['user']);
     break;
 
+/**
+ * Update the cache meta columns from the serialized/xml data
+ * (might be run after a schema update)
+ */
+case 'update':
+    // make sure libkolab classes are loaded
+    $rcmail->plugins->load_plugin('libkolab');
+
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
+    foreach ($folder_types as $type) {
+        $class = 'kolab_storage_cache_' . $type;
+        $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
+        while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+            $folder = new $class;
+            $folder->select_by_id($sql_arr['folder_id']);
+            echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
+            foreach ($folder->select() as $object) {
+                $object['_formatobj']->to_array();  // load data
+                $folder->save($object['_msguid'], $object, $object['_msguid']);
+                echo ".";
+            }
+            echo "done.\n";
+        }
+    }
+    break;
+
 
 /*
  * Unknown action => show usage
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index cb47dd4..35775d1 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -43,6 +43,8 @@ class kolab_storage_cache
     protected $max_sync_lock_time = 600;
     protected $binary_items = array();
     protected $extra_cols = array();
+    protected $order_by = null;
+    protected $limit = null;
 
 
     /**
@@ -87,6 +89,24 @@ class kolab_storage_cache
             $this->set_folder($storage_folder);
     }
 
+    /**
+     * Direct access to cache by folder_id
+     * (only for internal use)
+     */
+    public function select_by_id($folder_id)
+    {
+        $folders_table = $this->db->table_name('kolab_folders');
+        $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM $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
@@ -416,12 +436,15 @@ class kolab_storage_cache
             $this->_read_folder_data();
 
             // fetch full object data on one query if a small result set is expected
-            $fetchall = !$uids && $this->count($query) < 500;
-            $sql_result = $this->db->query(
-                "SELECT " . ($fetchall ? '*' : 'msguid AS _msguid, uid') . " FROM $this->cache_table ".
-                "WHERE folder_id=? " . $this->_sql_where($query),
-                $this->folder_id
-            );
+            $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
+            $sql_query = "SELECT " . ($fetchall ? '*' : 'msguid AS _msguid, uid') . " FROM $this->cache_table ".
+                         "WHERE folder_id=? " . $this->_sql_where($query);
+            if (!empty($this->order_by)) {
+                $sql_query .= ' ORDER BY ' . $this->order_by;
+            }
+            $sql_result = $this->limit ?
+                $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
+                $this->db->query($sql_query, $this->folder_id);
 
             if ($this->db->is_error($sql_result)) {
                 if ($uids) {
@@ -518,6 +541,26 @@ class kolab_storage_cache
         return $count;
     }
 
+    /**
+     * Define ORDER BY clause for cache queries
+     */
+    public function set_order_by($sortcols)
+    {
+        if (!empty($sortcols)) {
+            $this->order_by = join(', ', (array)$sortcols);
+        }
+        else {
+            $this->order_by = null;
+        }
+    }
+
+    /**
+     * Define LIMIT clause for cache queries
+     */
+    public function set_limit($length, $offset = 0)
+    {
+        $this->limit = array($length, $offset);
+    }
 
     /**
      * Helper method to compose a valid SQL query from pseudo filter triplets
diff --git a/plugins/libkolab/lib/kolab_storage_cache_contact.php b/plugins/libkolab/lib/kolab_storage_cache_contact.php
index e17923d..fde27bc 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -23,7 +23,7 @@
 
 class kolab_storage_cache_contact extends kolab_storage_cache
 {
-    protected $extra_cols = array('type');
+    protected $extra_cols = array('type','name','firstname','surname','email');
     protected $binary_items = array(
         'photo'          => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
         'pgppublickey'   => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
@@ -40,6 +40,16 @@ class kolab_storage_cache_contact extends kolab_storage_cache
         $sql_data = parent::_serialize($object);
         $sql_data['type'] = $object['_type'];
 
+        // columns for sorting
+        $sql_data['name']      = $object['name'] . $object['prefix'];
+        $sql_data['firstname'] = $object['firstname'] . $object['middlename'] . $object['surname'];
+        $sql_data['surname']   = $object['surname']   . $object['firstname']  . $object['middlename'];
+        $sql_data['email']     = is_array($object['email']) ? $object['email'][0] : $object['email'];
+
+        if (is_array($sql_data['email'])) {
+            $sql_data['email'] = $sql_data['email']['address'];
+        }
+
         return $sql_data;
     }
 }
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 12da5e9..24602a4 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -92,7 +92,6 @@ class kolab_storage_folder
         $this->cache->set_folder($this);
     }
 
-
     /**
      *
      */
@@ -424,6 +423,21 @@ class kolab_storage_folder
         return $this->cache->select($this->_prepare_query($query), true);
     }
 
+    /**
+     * Setter for ORDER BY and LIMIT parameters for cache queries
+     *
+     * @param array   List of columns to order by
+     * @param integer Limit result set to this length
+     * @param integer Offset row
+     */
+    public function set_order_and_limit($sortcols, $length = null, $offset = 0)
+    {
+        $this->cache->set_order_by($sortcols);
+
+        if ($length !== null) {
+            $this->cache->set_limit($length, $offset);
+        }
+    }
 
     /**
      * Helper method to sanitize query arguments


commit 8d3ade4b44944dd5814dced672a57a254808db10
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Mon Feb 10 11:38:41 2014 +0100

    Return null on error for UID queries (to remain backwards compatible)
    
    Conflicts:
    	plugins/libkolab/lib/kolab_storage_cache.php

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index ac2430e..cb47dd4 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -424,7 +424,10 @@ class kolab_storage_cache
             );
 
             if ($this->db->is_error($sql_result)) {
-                if (!$uids) $result->set_error(true);
+                if ($uids) {
+                    return null;
+                }
+                $result->set_error(true);
                 return $result;
             }
 


commit 2bac2f2285b087655e4a8b2a70967087955484e6
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Mon Feb 10 09:27:16 2014 +0100

    Don't set error state on simple arrays; simplify query
    
    Conflicts:
    	plugins/libkolab/lib/kolab_storage_cache.php

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index d64cb61..ac2430e 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -418,19 +418,19 @@ class kolab_storage_cache
             // fetch full object data on one query if a small result set is expected
             $fetchall = !$uids && $this->count($query) < 500;
             $sql_result = $this->db->query(
-                "SELECT " . (!$fetchall ? 'msguid, msguid AS _msguid, uid' : '*') . " FROM $this->cache_table ".
+                "SELECT " . ($fetchall ? '*' : 'msguid AS _msguid, uid') . " FROM $this->cache_table ".
                 "WHERE folder_id=? " . $this->_sql_where($query),
                 $this->folder_id
             );
 
             if ($this->db->is_error($sql_result)) {
-                $result->set_error(true);
+                if (!$uids) $result->set_error(true);
                 return $result;
             }
 
             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                 if ($uids) {
-                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
                     $result[] = $sql_arr['uid'];
                 }
                 else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {


commit d88ee3016c8f5a5b038251de287dc511312d2340
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Mon Feb 10 08:51:51 2014 +0100

    Save error state in kolab_storage_cache::select() return value
    
    Conflicts:
    	plugins/libkolab/lib/kolab_storage_cache.php

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 890f2fd..d64cb61 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -424,6 +424,7 @@ class kolab_storage_cache
             );
 
             if ($this->db->is_error($sql_result)) {
+                $result->set_error(true);
                 return $result;
             }
 
diff --git a/plugins/libkolab/lib/kolab_storage_dataset.php b/plugins/libkolab/lib/kolab_storage_dataset.php
index 23dfb86..9ddf3f9 100644
--- a/plugins/libkolab/lib/kolab_storage_dataset.php
+++ b/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -32,6 +32,7 @@ class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
     private $index = array();
     private $data = array();
     private $iteratorkey = 0;
+    private $error = null;
 
     /**
      * Default constructor
@@ -49,6 +50,22 @@ class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
         }
     }
 
+    /**
+     * Return error state
+     */
+    public function is_error()
+    {
+        return !empty($this->error);
+    }
+
+    /**
+     * Set error state
+     */
+    public function set_error($err)
+    {
+        $this->error = $err;
+    }
+
 
     /*** Implement PHP Countable interface ***/
 


commit 0ada6bd8d2abe3a1957adee7e87e9ca75e76414e
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Fri Feb 7 13:24:11 2014 +0100

    Always return a valid array/iterator
    
    Conflicts:
    	plugins/libkolab/lib/kolab_storage_cache.php

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 85576cf..890f2fd 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -424,7 +424,7 @@ class kolab_storage_cache
             );
 
             if ($this->db->is_error($sql_result)) {
-                return null;
+                return $result;
             }
 
             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {


commit 888b88117c8d79558bc72d5ce3e6feee40f14390
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Fri Feb 7 09:30:08 2014 +0100

    Init iterator key with 0

diff --git a/plugins/libkolab/lib/kolab_storage_dataset.php b/plugins/libkolab/lib/kolab_storage_dataset.php
index d23ba69..23dfb86 100644
--- a/plugins/libkolab/lib/kolab_storage_dataset.php
+++ b/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -31,7 +31,7 @@ class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
     private $buffer = false;
     private $index = array();
     private $data = array();
-    private $iteratorkey = -1;
+    private $iteratorkey = 0;
 
     /**
      * Default constructor


commit 0c2f85a4690e21343272c5213968a8ebc2a27965
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Fri Feb 7 09:29:11 2014 +0100

    Also implement the Countable interface for full array-like compatibility

diff --git a/plugins/libkolab/lib/kolab_storage_dataset.php b/plugins/libkolab/lib/kolab_storage_dataset.php
index 17e66de..d23ba69 100644
--- a/plugins/libkolab/lib/kolab_storage_dataset.php
+++ b/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -24,7 +24,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-class kolab_storage_dataset implements Iterator, ArrayAccess
+class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
 {
     private $cache;  // kolab_storage_cache instance to use for fetching data
     private $memlimit = 0;
@@ -50,6 +50,14 @@ class kolab_storage_dataset implements Iterator, ArrayAccess
     }
 
 
+    /*** Implement PHP Countable interface ***/
+
+    public function count()
+    {
+        return count($this->index);
+    }
+
+
     /*** Implement PHP ArrayAccess interface ***/
 
     public function offsetSet($offset, $value)


commit 795d16b8b578c8fb193fdc6a8ebafd8aaa6b343d
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Feb 6 17:38:19 2014 +0100

    New utility script tp generate random contact data

diff --git a/plugins/libkolab/bin/randomcontacts.sh b/plugins/libkolab/bin/randomcontacts.sh
new file mode 100755
index 0000000..e4a820c
--- /dev/null
+++ b/plugins/libkolab/bin/randomcontacts.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * Generate a number contacts with random data
+ *
+ * @version 3.1
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALL_PATH', realpath('.') . '/' );
+ini_set('display_errors', 1);
+
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
+    die("Execute this from the Roundcube installation dir!\n\n");
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+function print_usage()
+{
+    print "Usage:  randomcontacts.sh [OPTIONS] USERNAME FOLDER\n";
+    print "Create random contact that for then given user in the specified folder.\n";
+    print "-n, --num      Number of contacts to be created, defaults to 50\n";
+    print "-h, --host     IMAP host name\n";
+    print "-p, --password IMAP user password\n";
+}
+
+// read arguments
+$opts = get_opt(array(
+    'n' => 'num',
+    'h' => 'host',
+    'u' => 'user',
+    'p' => 'pass',
+    'v' => 'verbose',
+));
+
+$opts['username'] = !empty($opts[0]) ? $opts[0] : $opts['user'];
+$opts['folder'] = $opts[1];
+
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+$rcmail->plugins->load_plugins(array('libkolab'));
+ini_set('display_errors', 1);
+
+
+if (empty($opts['host'])) {
+    $opts['host'] = $rcmail->config->get('default_host');
+    if (is_array($opts['host']))  // not unique
+        $opts['host'] = null;
+}
+
+if (empty($opts['username']) || empty($opts['folder']) || empty($opts['host'])) {
+    print_usage();
+    exit;
+}
+
+// prompt for password
+if (empty($opts['pass'])) {
+    $opts['pass'] = rcube_utils::prompt_silent("Password: ");
+}
+
+// parse $host URL
+$a_host = parse_url($opts['host']);
+if ($a_host['host']) {
+    $host = $a_host['host'];
+    $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
+    $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+}
+else {
+    $host = $opts['host'];
+    $imap_port = 143;
+}
+
+// instantiate IMAP class
+$IMAP = $rcmail->get_storage();
+
+// try to connect to IMAP server
+if ($IMAP->connect($host, $opts['username'], $opts['pass'], $imap_port, $imap_ssl)) {
+    print "IMAP login successful.\n";
+    $user = rcube_user::query($opts['username'], $host);
+    $rcmail->user = $user ?: new rcube_user(null, array('username' => $opts['username'], 'host' => $host));
+}
+else {
+    die("IMAP login failed for user " . $opts['username'] . " @ $host\n");
+}
+
+// get contacts folder
+$folder = kolab_storage::get_folder($opts['folder']);
+if (!$folder || empty($folder->type)) {
+    die("Invalid Address Book " . $opts['folder'] . "\n");
+}
+
+$format = new kolab_format_contact;
+
+$num = $opts['num'] ? intval($opts['num']) : 50;
+echo "Creating $num contacts in " . $folder->get_resource_uri() . "\n";
+
+for ($i=0; $i < $num; $i++) {
+    // generate random names
+    $contact = array(
+        'surname' => random_string(rand(1,2)),
+        'firstname' => random_string(rand(1,2)),
+        'organization' => random_string(rand(0,2)),
+        'profession' => random_string(rand(1,2)),
+        'email' => array(),
+        'phone' => array(),
+        'address' => array(),
+        'notes' => random_string(rand(10,200)),
+    );
+
+    // randomly add email addresses
+    $em = rand(1,3);
+    for ($e=0; $e < $em; $e++) {
+        $type = array_rand($format->emailtypes);
+        $contact['email'][] = array(
+            'address' => strtolower(random_string(1) . '@' . random_string(1) . '.tld'),
+            'type' => $type,
+        );
+    }
+
+    // randomly add phone numbers
+    $ph = rand(1,4);
+    for ($p=0; $p < $ph; $p++) {
+        $type = array_rand($format->phonetypes);
+        $contact['phone'][] = array(
+            'number' => '+'.rand(2,8).rand(1,9).rand(1,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9),
+            'type' => $type,
+        );
+    }
+
+    // randomly add addresses
+    $ad = rand(0,2);
+    for ($a=0; $a < $ad; $a++) {
+        $type = array_rand($format->addresstypes);
+        $contact['address'][] = array(
+            'street' => random_string(rand(1,3)),
+            'locality' => random_string(rand(1,2)),
+            'code' => rand(1000, 89999),
+            'country' => random_string(1),
+            'type' => $type,
+        );
+    }
+
+    $contact['name'] = $contact['firstname'] . ' ' . $contact['surname'];
+
+    if ($folder->save($contact, 'contact')) {
+        echo ".";
+    }
+    else {
+        echo "x";
+        break;  // abort on error
+    }
+}
+
+echo " done.\n";
+
+
+
+function random_string($len)
+{
+    $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a features is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform hereafter referred to without the classical prefix retains many applications, as most manufac- tured parts and many anatomical parts investigated in medical imagery contain feature boundaries which can be described by regular curve
 s. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
+    for ($i = 0; $i < $len; $i++) {
+        $str .= $words[rand(0,count($words)-1)] . " ";
+    }
+
+    return rtrim($str);
+}


commit f7dcf5a5f6310ddaef375663b811d324f453519a
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Feb 6 17:33:05 2014 +0100

    Refactor access to storage backend to avoid memory limit errors (#2828):
    1. query backend and read contact names for sorting
    2. sort index according to UI settings and fetched names
    3. select the subset for the current page
    4. fetch contacts for current page

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 674f859..6f74e30 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -89,6 +89,8 @@ class rcube_kolab_contacts extends rcube_addressbook
 
     private $gid;
     private $storagefolder;
+    private $dataset;
+    private $sortindex;
     private $contacts;
     private $distlists;
     private $groupmembers;
@@ -266,10 +268,11 @@ class rcube_kolab_contacts extends rcube_addressbook
      *
      * @param array List of cols to show
      * @param  int  Only return this number of records, use negative values for tail
+     * @param  boolean True to skip the count query (select only)
      *
      * @return array  Indexed list of contact records, each a hash array
      */
-    public function list_records($cols = null, $subset = 0)
+    public function list_records($cols = null, $subset = 0, $nocount = false)
     {
         $this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
 
@@ -277,8 +280,10 @@ class rcube_kolab_contacts extends rcube_addressbook
         if ($this->gid) {
             $this->_fetch_groups();
 
-            $this->contacts = array();
-            $uids           = array();
+            $this->sortindex = array();
+            $this->contacts  = array();
+            $local_sortindex = array();
+            $uids            = array();
 
             // get members with email specified
             foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
@@ -287,23 +292,20 @@ class rcube_kolab_contacts extends rcube_addressbook
                     continue;
                 }
 
-                if (!empty($member['email'])) {
-                    $this->contacts[$member['ID']] = $member;
-                }
                 if (!empty($member['uid'])) {
                     $uids[] = $member['uid'];
                 }
+                else if (!empty($member['email'])) {
+                    $this->contacts[$member['ID']] = $member;
+                    $local_sortindex[$member['ID']] = $this->_sort_string($member);
+                }
             }
 
             // get members by UID
             if (!empty($uids)) {
-                foreach ((array)$this->storagefolder->select(array(array('uid', '=', $uids))) as $record) {
-                    $member = $this->_to_rcube_contact($record);
-                    $this->contacts[$member['ID']] = $member;
-                }
+                $this->_fetch_contacts(array(array('uid', '=', $uids)));
+                $this->sortindex = array_merge($this->sortindex, $local_sortindex);
             }
-
-            $ids = array_keys($this->contacts);
         }
         else if (is_array($this->filter['ids'])) {
             $ids = $this->filter['ids'];
@@ -311,34 +313,25 @@ class rcube_kolab_contacts extends rcube_addressbook
                 $uids = array_map(array($this, 'id2uid'), $this->filter['ids']);
                 $this->_fetch_contacts(array(array('uid', '=', $uids)));
             }
-            else {
-                $this->contacts = array();
-            }
         }
         else {
             $this->_fetch_contacts();
-            $ids = array_keys($this->contacts);
         }
 
-        // sort data arrays according to desired list sorting
-        if ($count = count($ids)) {
-            uasort($this->contacts, array($this, '_sort_contacts_comp'));
-            // get sorted IDs
-            if ($count != count($this->contacts))
-                $ids = array_values(array_intersect(array_keys($this->contacts), $ids));
-            else
-                $ids = array_keys($this->contacts);
-
-            $this->result->count = count($ids);
-        }
+        // sort results (index only)
+        asort($this->sortindex, SORT_LOCALE_STRING);
+        $ids = array_keys($this->sortindex);
 
         // fill contact data into the current result set
+        $this->result->count = count($ids);
         $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
-        $last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $count);
+        $last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
 
         for ($i = $start_row; $i < $last_row; $i++) {
-            if ($id = $ids[$i])
-                $this->result->add($this->contacts[$id]);
+            if (array_key_exists($i, $ids)) {
+                $idx = $ids[$i];
+                $this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
+            }
         }
 
         return $this->result;
@@ -406,8 +399,11 @@ class rcube_kolab_contacts extends rcube_addressbook
         // save searching conditions
         $this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
 
-        // search be iterating over all records in memory
-        foreach ($this->contacts as $id => $contact) {
+        // search by iterating over all records in dataset
+        foreach ($this->dataset as $i => $record) {
+            $contact = $this->_to_rcube_contact($record);
+            $id = $contact['ID'];
+
             // check if current contact has required values, otherwise skip it
             if ($required) {
                 foreach ($required as $f) {
@@ -588,10 +584,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                 true, false);
             }
             else {
-                $contact = $this->_to_rcube_contact($object);
-                $id = $contact['ID'];
-                $this->contacts[$id] = $contact;
-                $insert_id = $id;
+                $insert_id = $this->uid2id($object['uid']);
             }
         }
 
@@ -622,7 +615,6 @@ class rcube_kolab_contacts extends rcube_addressbook
                 true, false);
             }
             else {
-                $this->contacts[$id] = $this->_to_rcube_contact($object);
                 $updated = true;
 
                 // TODO: update data in groups this contact is member of
@@ -669,7 +661,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                     }
 
                     // clear internal cache
-                    unset($this->contacts[$id], $this->groupmembers[$id]);
+                    unset($this->groupmembers[$id]);
                     $count++;
                 }
             }
@@ -718,6 +710,8 @@ class rcube_kolab_contacts extends rcube_addressbook
     {
         if ($this->storagefolder->delete_all()) {
             $this->contacts = array();
+            $this->sortindex = array();
+            $this->dataset = null;
             $this->result = null;
         }
     }
@@ -974,57 +968,41 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     private function _fetch_contacts($query = array())
     {
-        if (!isset($this->contacts)) {
-            $this->contacts = array();
-            foreach ((array)$this->storagefolder->select($query) as $record) {
+        if (!isset($this->dataset) || !empty($query)) {
+            $this->sortindex = array();
+            $this->dataset = $this->storagefolder->select($query);
+            foreach ($this->dataset as $idx => $record) {
                 $contact = $this->_to_rcube_contact($record);
-                $id = $contact['ID'];
-                $this->contacts[$id] = $contact;
+                $this->sortindex[$idx] = $this->_sort_string($contact);
             }
         }
     }
 
     /**
-     * Callback function for sorting contacts
+     * Extract a string for sorting from the given contact record
      */
-    private function _sort_contacts_comp($a, $b)
+    private function _sort_string($rec)
     {
-        $a_value = $b_value = '';
+        $str = '';
 
         switch ($this->sort_col) {
         case 'name':
-            $a_value = $a['name'] . $a['prefix'];
-            $b_value = $b['name'] . $b['prefix'];
+            $str = $rec['name'] . $rec['prefix'];
         case 'firstname':
-            $a_value .= $a['firstname'] . $a['middlename'] . $a['surname'];
-            $b_value .= $b['firstname'] . $b['middlename'] . $b['surname'];
+            $str .= $rec['firstname'] . $rec['middlename'] . $rec['surname'];
             break;
 
         case 'surname':
-            $a_value = $a['surname'] . $a['firstname'] . $a['middlename'];
-            $b_value = $b['surname'] . $b['firstname'] . $b['middlename'];
+            $str = $rec['surname'] . $rec['firstname'] . $rec['middlename'];
             break;
 
         default:
-            $a_value = $a[$this->sort_col];
-            $b_value = $b[$this->sort_col];
+            $str = $rec[$this->sort_col];
             break;
         }
 
-        $a_value .= is_array($a['email']) ? $a['email'][0] : $a['email'];
-        $b_value .= is_array($b['email']) ? $b['email'][0] : $b['email'];
-
-        $a_value = mb_strtolower($a_value);
-        $b_value = mb_strtolower($b_value);
-
-        // return strcasecmp($a_value, $b_value);
-        // make sorting unicode-safe and locale-dependent
-        if ($a_value == $b_value)
-            return 0;
-
-        $arr = array($a_value, $b_value);
-        sort($arr, SORT_LOCALE_STRING);
-        return $a_value == $arr[0] ? -1 : 1;
+        $str .= is_array($rec['email']) ? $rec['email'][0] : $rec['email'];
+        return mb_strtolower($str);
     }
 
     /**
@@ -1034,7 +1012,7 @@ class rcube_kolab_contacts extends rcube_addressbook
     {
         if (!isset($this->distlists)) {
             $this->distlists = $this->groupmembers = array();
-            foreach ((array)$this->storagefolder->get_objects('distribution-list') as $record) {
+            foreach ($this->storagefolder->get_objects('distribution-list') as $record) {
                 $record['ID'] = $this->uid2id($record['uid']);
                 foreach ((array)$record['member'] as $i => $member) {
                     $mid = $this->uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);


commit 24e67acdb1ae1c96f0fafeff74e0fb5b2b22f2e6
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Feb 6 17:30:40 2014 +0100

    Adapt to kolab_storage_folder optimizations: don't cast resultset into array (#2828)

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 9427b1c..2fe072a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -233,7 +233,7 @@ class kolab_calendar
     }
 
     $events = array();
-    foreach ((array)$this->storage->select($query) as $record) {
+    foreach ($this->storage->select($query) as $record) {
       $event = $this->_to_rcube_event($record);
       $this->events[$event['id']] = $event;
 
diff --git a/plugins/kolab_config/kolab_config.php b/plugins/kolab_config/kolab_config.php
index e5f07ad..d4a4753 100644
--- a/plugins/kolab_config/kolab_config.php
+++ b/plugins/kolab_config/kolab_config.php
@@ -167,7 +167,7 @@ class kolab_config extends rcube_plugin
             if ($default && !$folder->default)
                 continue;
 
-            foreach ((array)$folder->select($query) as $object) {
+            foreach ($folder->select($query) as $object) {
                 if ($object['type'] == 'dictionary' && ($object['language'] == $lang || $object['language'] == 'XX')) {
                     if (is_array($this->dicts[$lang]))
                         $this->dicts[$lang]['e'] = array_merge((array)$this->dicts[$lang]['e'], $object['e']);
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 3142e10..2101ed4 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -293,7 +293,7 @@ class tasklist_kolab_driver extends tasklist_driver
         $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
         foreach ($lists as $list_id) {
             $folder = $this->folders[$list_id];
-            foreach ((array)$folder->select(array(array('tags','!~','x-complete'))) as $record) {
+            foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) {
                 $rec = $this->_to_rcube_task($record);
 
                 if ($rec['complete'] >= 1.0)  // don't count complete tasks
@@ -357,7 +357,7 @@ class tasklist_kolab_driver extends tasklist_driver
 
         foreach ($lists as $list_id) {
             $folder = $this->folders[$list_id];
-            foreach ((array)$folder->select($query) as $record) {
+            foreach ($folder->select($query) as $record) {
                 $task = $this->_to_rcube_task($record);
                 $task['list'] = $list_id;
 
@@ -420,7 +420,7 @@ class tasklist_kolab_driver extends tasklist_driver
             $query_ids = array();
             foreach ($task_ids as $task_id) {
                 $query = array(array('tags','=','x-parent:' . $task_id));
-                foreach ((array)$folder->select($query) as $record) {
+                foreach ($folder->select($query) as $record) {
                     // don't rely on kolab_storage_folder filtering
                     if ($record['parent_id'] == $task_id) {
                         $childs[] = $record['uid'];
@@ -474,7 +474,7 @@ class tasklist_kolab_driver extends tasklist_driver
                 continue;
 
             $folder = $this->folders[$lid];
-            foreach ((array)$folder->select($query) as $record) {
+            foreach ($folder->select($query) as $record) {
                 if (!$record['alarms'])  // don't trust query :-)
                     continue;
 


commit a5617d3d5994a42f8b598f902269fe9a22a33fb7
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Feb 6 17:25:16 2014 +0100

    Return a kolab_storage_dataset itertor object from kolab_storage_cache::select()
    to manage memory usage for large result sets (#2828).
    
    Attention!
    Do not cast the return value of kolab_storage_folder::select() calls into an array anymore.

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 98f32a8..85576cf 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -231,14 +231,14 @@ class kolab_storage_cache
                 );
 
                 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                    $this->objects[$msguid] = $this->_unserialize($sql_arr);
+                    $this->objects = array($msguid => $this->_unserialize($sql_arr));  // store only this object in memory (#2827)
                 }
             }
 
             // fetch from IMAP if not present in cache
             if (empty($this->objects[$msguid])) {
                 $result = $this->_fetch(array($msguid), $type, $foldername);
-                $this->objects[$msguid] = $result[0];
+                $this->objects = array($msguid => $result[0]);  // store only this object in memory (#2827)
             }
         }
 
@@ -409,14 +409,16 @@ class kolab_storage_cache
      */
     public function select($query = array(), $uids = false)
     {
-        $result = array();
+        $result = $uids ? array() : new kolab_storage_dataset($this);
 
         // read from local cache DB (assume it to be synchronized)
         if ($this->ready) {
             $this->_read_folder_data();
 
+            // fetch full object data on one query if a small result set is expected
+            $fetchall = !$uids && $this->count($query) < 500;
             $sql_result = $this->db->query(
-                "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
+                "SELECT " . (!$fetchall ? 'msguid, msguid AS _msguid, uid' : '*') . " FROM $this->cache_table ".
                 "WHERE folder_id=? " . $this->_sql_where($query),
                 $this->folder_id
             );
@@ -430,9 +432,13 @@ class kolab_storage_cache
                     $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
                     $result[] = $sql_arr['uid'];
                 }
-                else if ($object = $this->_unserialize($sql_arr)) {
+                else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
                     $result[] = $object;
                 }
+                else {
+                    // only add msguid to dataset index
+                    $result[] = $sql_arr;
+                }
             }
         }
         else {
@@ -459,7 +465,7 @@ class kolab_storage_cache
         if (!$uids && count($result) == 1) {
             if ($msguid = $result[0]['_msguid']) {
                 $this->uid2msg[$result[0]['uid']] = $msguid;
-                $this->objects[$msguid] = $result[0];
+                $this->objects = array($msguid => $result[0]);
             }
         }
 
@@ -578,7 +584,7 @@ class kolab_storage_cache
      */
     protected function _fetch($index, $type = null, $folder = null)
     {
-        $results = array();
+        $results = new kolab_storage_dataset($this);
         foreach ((array)$index as $msguid) {
             if ($object = $this->folder->read_object($msguid, $type, $folder)) {
                 $results[] = $object;
diff --git a/plugins/libkolab/lib/kolab_storage_dataset.php b/plugins/libkolab/lib/kolab_storage_dataset.php
new file mode 100644
index 0000000..17e66de
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * Dataset class providing the results of a select operation on a kolab_storage_folder.
+ *
+ * Can be used as a normal array as well as an iterator in foreach() loops.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_dataset implements Iterator, ArrayAccess
+{
+    private $cache;  // kolab_storage_cache instance to use for fetching data
+    private $memlimit = 0;
+    private $buffer = false;
+    private $index = array();
+    private $data = array();
+    private $iteratorkey = -1;
+
+    /**
+     * Default constructor
+     *
+     * @param object kolab_storage_cache instance to be used for fetching objects upon access
+     */
+    public function __construct($cache)
+    {
+        $this->cache = $cache;
+
+        // enable in-memory buffering up until 1/5 of the available memory
+        if (function_exists('memory_get_usage')) {
+            $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
+            $this->buffer = true;
+        }
+    }
+
+
+    /*** Implement PHP ArrayAccess interface ***/
+
+    public function offsetSet($offset, $value)
+    {
+        $uid = $value['_msguid'];
+
+        if (is_null($offset)) {
+            $offset = count($this->index);
+            $this->index[] = $uid;
+        }
+        else {
+            $this->index[$offset] = $uid;
+        }
+
+        // keep full payload data in memory if possible
+        if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
+            $this->data[$offset] = $value;
+
+            // check memory usage and stop buffering
+            if ($offset % 10 == 0) {
+                $this->buffer = memory_get_usage() < $this->memlimit;
+            }
+        }
+    }
+
+    public function offsetExists($offset)
+    {
+        return isset($this->index[$offset]);
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->index[$offset]);
+    }
+
+    public function offsetGet($offset)
+    {
+        if (isset($this->data[$offset])) {
+            return $this->data[$offset];
+        }
+        else if ($msguid = $this->index[$offset]) {
+            return $this->cache->get($msguid);
+        }
+
+        return null;
+    }
+
+
+    /*** Implement PHP Iterator interface ***/
+
+    public function current()
+    {
+        return $this->offsetGet($this->iteratorkey);
+    }
+
+    public function key()
+    {
+        return $this->iteratorkey;
+    }
+
+    public function next()
+    {
+        $this->iteratorkey++;
+        return $this->valid();
+    }
+
+    public function rewind()
+    {
+        $this->iteratorkey = 0;
+    }
+
+    public function valid()
+    {
+        return !empty($this->index[$this->iteratorkey]);
+    }
+
+}




More information about the commits mailing list