4 commits - plugins/calendar plugins/kolab_addressbook plugins/kolab_config plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Thu Feb 6 18:32:53 CET 2014


 plugins/calendar/drivers/kolab/kolab_calendar.php        |    2 
 plugins/kolab_addressbook/lib/rcube_kolab_contacts.php   |  116 +++------
 plugins/kolab_config/kolab_config.php                    |    2 
 plugins/libkolab/bin/randomcontacts.sh                   |  181 +++++++++++++++
 plugins/libkolab/lib/kolab_storage_cache.php             |   20 +
 plugins/libkolab/lib/kolab_storage_dataset.php           |  129 ++++++++++
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |    8 
 7 files changed, 376 insertions(+), 82 deletions(-)

New commits:
commit c2b87d4ad5d4ca0f25b79f3f050c54a9dc846f1b
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 ea131a84e65481048a3e865aa8f0756f3f055b42
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 3fc8527..858ad17 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;
@@ -411,8 +404,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) {
@@ -593,10 +589,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']);
             }
         }
 
@@ -627,7 +620,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
@@ -674,7 +666,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                     }
 
                     // clear internal cache
-                    unset($this->contacts[$id], $this->groupmembers[$id]);
+                    unset($this->groupmembers[$id]);
                     $count++;
                 }
             }
@@ -723,6 +715,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;
         }
     }
@@ -979,57 +973,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);
     }
 
     /**
@@ -1039,7 +1017,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 69b7803f25c35c2b1c0d48cd47ada4e214a0528e
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 edb60e6..99971f0 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -294,7 +294,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
@@ -358,7 +358,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;
 
@@ -421,7 +421,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'];
@@ -475,7 +475,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 2ade247a0cef2cce7177cf6d68b87d1c3f19cedd
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 efd61ef..f261220 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -239,14 +239,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)
             }
         }
 
@@ -438,14 +438,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
             );
@@ -459,9 +461,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;
+                }
             }
         }
         // use IMAP
@@ -491,7 +497,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]);
             }
         }
 
@@ -618,7 +624,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