Branch 'dev/new-foldernav' - 2 commits - plugins/calendar plugins/kolab_auth plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Wed May 14 20:42:17 CEST 2014


 plugins/calendar/calendar_ui.js                        |  113 +------
 plugins/calendar/drivers/kolab/kolab_calendar.php      |   37 +-
 plugins/calendar/drivers/kolab/kolab_driver.php        |   72 +++-
 plugins/calendar/drivers/kolab/kolab_user_calendar.php |  219 +++++++++++++++
 plugins/calendar/lib/calendar_ui.php                   |   32 +-
 plugins/calendar/lib/js/folderlist.js                  |    1 
 plugins/calendar/skins/larry/calendar.css              |    8 
 plugins/kolab_auth/kolab_auth_ldap.php                 |    1 
 plugins/libkolab/js/folderlist.js                      |  158 ++++++++++
 plugins/libkolab/lib/kolab_storage.php                 |  245 +++++++++++++----
 plugins/libkolab/lib/kolab_storage_user_folder.php     |  123 ++++++++
 plugins/libkolab/lib/kolab_storage_virtual_folder.php  |   86 +++++
 12 files changed, 899 insertions(+), 196 deletions(-)

New commits:
commit 701c3391fe149a4e82535ae7d48fedd30ce70a77
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 14 20:37:06 2014 +0200

    Search in LDAP and collect accessible folders (#3041)
    - Add LDAP user search capabilities to kolab_storage class (using kolab_auth plugin classes)
    - Introduce virtual 'user' folder objects and add methods to list them
    - New 'user calendar' class in calendar (kolab driver)
    - Render folder search results as hierarchical list

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 52baf11..a1a60b1 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2739,7 +2739,9 @@ function rcube_calendar_ui(settings)
       selectable: true,
       save_state: true,
       searchbox: '#calendarlistsearch',
-      search_action: 'calendar/calendar'
+      search_action: 'calendar/calendar',
+      search_sources: [ 'folders', 'users' ],
+      search_title: rcmail.gettext('calsearchresults','calendar')
     });
     calendars_list.addEventListener('select', function(node) {
       me.select_calendar(node.id);
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 5feed23..97f6a96 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -7,7 +7,7 @@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  * @author Aleksander Machniak <machniak 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
@@ -35,11 +35,30 @@ class kolab_calendar
   public $storage;
   public $name;
 
-  private $cal;
-  private $events = array();
-  private $imap_folder = 'INBOX/Calendar';
-  private $search_fields = array('title', 'description', 'location', 'attendees');
+  protected $cal;
+  protected $events = array();
+  protected $imap_folder = 'INBOX/Calendar';
+  protected $search_fields = array('title', 'description', 'location', 'attendees');
 
+  /**
+   * Factory method to instantiate a kolab_calendar object
+   *
+   * @param string  Calendar ID (encoded IMAP folder name)
+   * @param object  calendar plugin object
+   * @return object kolab_calendar instance
+   */
+  public static function factory($id, $calendar)
+  {
+    $imap = $calendar->rc->get_storage();
+    $imap_folder = kolab_storage::id_decode($id);
+    $info = $imap->folder_info($imap_folder, true);
+    if (empty($info) || $info['noselect'] || kolab_storage::folder_type($imap_folder) != 'event') {
+      return new kolab_user_calendar($imap_folder, $calendar);
+    }
+    else {
+      return new kolab_calendar($imap_folder, $calendar);
+    }
+  }
 
   /**
    * Default constructor
@@ -177,14 +196,6 @@ class kolab_calendar
     return false;
   }
 
-  /**
-   * Return the corresponding kolab_storage_folder instance
-   */
-  public function get_folder()
-  {
-    return $this->storage;
-  }
-
 
   /**
    * Getter for a single event object
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 9c45eb6..12e4258 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -24,6 +24,7 @@
  */
 
 require_once(dirname(__FILE__) . '/kolab_calendar.php');
+require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
 
 class kolab_driver extends calendar_driver
 {
@@ -78,14 +79,20 @@ class kolab_driver extends calendar_driver
       return $this->calendars;
 
     // get all folders that have "event" type, sorted by namespace/name
-    $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event'));
+    $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders(true));
     $this->calendars = array();
 
     foreach ($folders as $folder) {
-      $calendar = new kolab_calendar($folder->name, $this->cal);
-      $this->calendars[$calendar->id] = $calendar;
-      if (!$calendar->readonly)
-        $this->has_writeable = true;
+      if ($folder instanceof kolab_storage_user_folder)
+        $calendar = new kolab_user_calendar($folder->name, $this->cal);
+      else
+        $calendar = new kolab_calendar($folder->name, $this->cal);
+
+      if ($calendar->ready) {
+        $this->calendars[$calendar->id] = $calendar;
+        if (!$calendar->readonly)
+          $this->has_writeable = true;
+      }
     }
 
     return $this->calendars;
@@ -114,18 +121,32 @@ class kolab_driver extends calendar_driver
     $calendars = $names = array();
 
     // include virtual folders for a full folder tree
-    if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
+    if (!is_null($tree))
       $folders = kolab_storage::folder_hierarchy($folders, $tree);
 
     foreach ($folders as $id => $cal) {
       $fullname = $cal->get_name();
-      $listname = kolab_storage::folder_displayname($fullname, $names);
+      $listname = $cal->get_foldername();
       $imap_path = explode('/', $cal->name);
       $topname = array_pop($imap_path);
       $parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
 
-      // special handling for virtual folders
-      if ($cal->virtual) {
+      // special handling for user or virtual folders
+      if ($cal instanceof kolab_storage_user_folder) {
+        $calendars[$cal->id] = array(
+          'id' => $cal->id,
+          'name' => kolab_storage::object_name($fullname),
+          'listname' => $listname,
+          'editname' => $cal->get_foldername(),
+          'color'    => $cal->get_color(),
+          'active'   => $cal->is_active(),
+          'owner'    => $cal->get_owner(),
+          'virtual' => false,
+          'readonly' => true,
+          'class_name' => 'user',
+        );
+      }
+      else if ($cal->virtual) {
         $calendars[$cal->id] = array(
           'id' => $cal->id,
           'name' => $fullname,
@@ -230,8 +251,8 @@ class kolab_driver extends calendar_driver
   {
     // create calendar object if necesary
     if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
-      $foldername = kolab_storage::id_decode($id);
-      $calendar = new kolab_calendar($foldername, $this->cal);
+      $calendar = kolab_calendar::factory($id, $this->cal);
+      console($id, $calendar->id, $calendar->ready);
       if ($calendar->ready)
         $this->calendars[$calendar->id] = $calendar;
     }
@@ -389,8 +410,20 @@ class kolab_driver extends calendar_driver
         $this->calendars[$calendar->id] = $calendar;
       }
     }
+    // find other user's virtual calendars
     else if ($source == 'users') {
-      // TODO: implement this
+      foreach (kolab_storage::search_users($query, 0) as $user) {
+        $calendar = new kolab_user_calendar($user, $this->cal);
+        $this->calendars[$calendar->id] = $calendar;
+
+        // search for calendar folders shared by this user
+        foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
+          if (1 || !kolab_storage::folder_is_subscribed($foldername, true)) {
+            $cal = new kolab_calendar($foldername, $this->cal);
+            $this->calendars[$cal->id] = $cal;
+          }
+        }
+      }
     }
 
     // don't list the birthday calendar
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
new file mode 100644
index 0000000..a5795a4
--- /dev/null
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual user calendar
+ *
+ * @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_user_calendar extends kolab_calendar
+{
+  public $id = 'unknown';
+  public $ready = false;
+  public $readonly = true;
+  public $attachments = false;
+  public $name;
+
+  protected $userdata = array();
+
+
+  /**
+   * Default constructor
+   */
+  public function __construct($user_or_folder, $calendar)
+  {
+    $this->cal = $calendar;
+
+    // full user record is provided
+    if (is_array($user_or_folder)) {
+      $this->userdata = $user_or_folder;
+      $this->storage = new kolab_storage_user_folder($this->userdata['kolabtargetfolder'], '', $this->userdata);
+    }
+    else {  // get user record from LDAP
+      $this->storage = new kolab_storage_user_folder($user_or_folder);
+      $this->userdata = $this->storage->ldaprec;
+    }
+
+    $this->ready = !empty($this->userdata['kolabtargetfolder']);
+
+    if ($this->ready) {
+      // ID is derrived from the user's kolabtargetfolder attribute
+      $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
+      $this->imap_folder = $this->userdata['kolabtargetfolder'];
+      $this->name = $this->storage->get_name();
+
+      // user-specific alarms settings win
+      $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+      if (isset($prefs[$this->id]['showalarms']))
+        $this->alarms = $prefs[$this->id]['showalarms'];
+    }
+  }
+
+
+  /**
+   * Getter for a nice and human readable name for this calendar
+   *
+   * @return string Name of this calendar
+   */
+  public function get_name()
+  {
+    return $this->userdata['name'] ?: $this->userdata['mail'];
+  }
+
+
+  /**
+   * Getter for the IMAP folder owner
+   *
+   * @return string Name of the folder owner
+   */
+  public function get_owner()
+  {
+    return $this->userdata['mail'];
+  }
+
+
+  /**
+   * Getter for the name of the namespace to which the IMAP folder belongs
+   *
+   * @return string Name of the namespace (personal, other, shared)
+   */
+  public function get_namespace()
+  {
+    return 'user';
+  }
+
+
+  /**
+   * Getter for the top-end calendar folder name (not the entire path)
+   *
+   * @return string Name of this calendar
+   */
+  public function get_foldername()
+  {
+    return $this->get_name();
+  }
+
+  /**
+   * Return color to display this calendar
+   */
+  public function get_color()
+  {
+    // calendar color is stored in local user prefs
+    $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+    if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+      return $prefs[$this->id]['color'];
+
+    return 'cc0000';
+  }
+
+  /**
+   * Compose an URL for CalDAV access to this calendar (if configured)
+   */
+  public function get_caldav_url()
+  {
+    return false;
+  }
+
+  /**
+   * Getter for a single event object
+   */
+  public function get_event($id)
+  {
+    // TODO: implement this
+    return $this->events[$id];
+  }
+
+
+  /**
+   * @param  integer Event's new start (unix timestamp)
+   * @param  integer Event's new end (unix timestamp)
+   * @param  string  Search query (optional)
+   * @param  boolean Include virtual events (optional)
+   * @param  array   Additional parameters to query storage
+   * @return array A list of event records
+   */
+  public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+  {
+    // TODO: implement this
+    console('kolab_user_calendar::list_events()');
+    return array();
+  }
+
+
+  /**
+   * Create a new event record
+   *
+   * @see calendar_driver::new_event()
+   * 
+   * @return mixed The created record ID on success, False on error
+   */
+  public function insert_event($event)
+  {
+    return false;
+  }
+
+  /**
+   * Update a specific event record
+   *
+   * @see calendar_driver::new_event()
+   * @return boolean True on success, False on error
+   */
+
+  public function update_event($event, $exception_id = null)
+  {
+    return false;
+  }
+
+  /**
+   * Delete an event record
+   *
+   * @see calendar_driver::remove_event()
+   * @return boolean True on success, False on error
+   */
+  public function delete_event($event, $force = true)
+  {
+    return false;
+  }
+
+  /**
+   * Restore deleted event record
+   *
+   * @see calendar_driver::undelete_event()
+   * @return boolean True on success, False on error
+   */
+  public function restore_event($event)
+  {
+    return false;
+  }
+
+
+  /**
+   * Convert from Kolab_Format to internal representation
+   */
+  private function _to_rcube_event($record)
+  {
+    $record['id'] = $record['uid'];
+    $record['calendar'] = $this->id;
+
+    // TODO: implement this
+
+    return $record;
+  }
+
+}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 0f5091a..a3ed443 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -205,6 +205,7 @@ class calendar_ui
   {
     $html = '';
     $jsenv = array();
+    $tree = true;
     $calendars = $this->cal->driver->list_calendars(false, false, $tree);
 
     // walk folder tree
@@ -273,19 +274,22 @@ class calendar_ui
    */
   public function calendar_list_item($id, $prop, &$jsenv)
   {
-    unset($prop['user_id']);
-    $prop['alarms']      = $this->cal->driver->alarms;
-    $prop['attendees']   = $this->cal->driver->attendees;
-    $prop['freebusy']    = $this->cal->driver->freebusy;
-    $prop['attachments'] = $this->cal->driver->attachments;
-    $prop['undelete']    = $this->cal->driver->undelete;
-    $prop['feedurl']     = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+    // enrich calendar properties with settings from the driver
+    if (!$prop['virtual']) {
+      unset($prop['user_id']);
+      $prop['alarms']      = $this->cal->driver->alarms;
+      $prop['attendees']   = $this->cal->driver->attendees;
+      $prop['freebusy']    = $this->cal->driver->freebusy;
+      $prop['attachments'] = $this->cal->driver->attachments;
+      $prop['undelete']    = $this->cal->driver->undelete;
+      $prop['feedurl']     = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
 
-    if (!$prop['virtual'])
       $jsenv[$id] = $prop;
+    }
 
     $class = 'calendar cal-'  . asciiwords($id, true);
-    $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+    $title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+      html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
     $is_collapsed = false; // TODO: determine this somehow?
 
     if ($prop['virtual'])
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 01a4c3d..a44899b 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -269,12 +269,12 @@ pre {
 #calendars .treelist div.readonly span.calname {
 	background-position: right -20px;
 }
-/*
-#calendars .treelist div.other span.calname {
+
+#calendars .treelist div.user span.calname {
 	background-position: right -38px;
 }
-
-#calendars .treelist div.other.readonly span.calname {
+/*
+#calendars .treelist div.user.readonly span.calname {
 	background-position: right -56px;
 }
 
diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/kolab_auth/kolab_auth_ldap.php
index d529b73..7044ebf 100644
--- a/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/plugins/kolab_auth/kolab_auth_ldap.php
@@ -213,7 +213,6 @@ class kolab_auth_ldap extends rcube_ldap_generic
      *                          0 - partial (*abc*),
      *                          1 - strict (=),
      *                          2 - prefix (abc*)
-     * @param boolean $select   True if results are requested, False if count only
      * @param array   $required List of fields that cannot be empty
      * @param int     $limit    Number of records
      *
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index eed8b50..e2119a8 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -46,7 +46,7 @@ function kolab_folderlist(node, p)
           // create treelist widget to present the search results
           if (!search_results_widget) {
               search_results_container = $('<div class="searchresults"></div>')
-                  .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+                  .html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
                   .insertAfter(me.container);
 
               search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
@@ -63,36 +63,64 @@ function kolab_folderlist(node, p)
 
                       var li = $(this).closest('li'),
                           id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+                          node = search_results_widget.get_node(id),
                           prop = search_results[id],
-                          parent_id = prop.parent || null;
+                          parent_id = prop.parent || null,
+                          has_children = node.children && node.children.length,
+                          dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
 
                       // find parent node and insert at the right place
                       if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
                           prop.listname = prop.editname;
-                          li.children().first().children('span,a').first().html(Q(prop.listname));
+                          dom_node.children('span,a').first().html(Q(prop.listname));
                       }
 
-                      // move this result item to the main list widget
-                      me.insert({
-                          id: id,
-                          classes: [],
-                          html: li.children().first()
-                      }, parent_id, parent_id ? true : false);
+                      // TODO: copy parent tree too
+
+                      // replace virtual node with a real one
+                      if (me.get_node(id)) {
+                          $(me.get_item(id, true)).children().first()
+                              .replaceWith(dom_node)
+                              .removeClass('virtual');
+                      }
+                      else {
+                          // move this result item to the main list widget
+                          me.insert({
+                              id: id,
+                              classes: [],
+                              virtual: prop.virtual,
+                              html: dom_node,
+                          }, parent_id, parent_id ? true : false);
+                      }
 
                       delete prop.html;
                       me.triggerEvent('insert-item', { id: id, data: prop, item: li });
-                      li.remove();
+
+                      if (has_children) {
+                          li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+                      }
+                      else {
+                          li.remove();
+                      }
                   });
           }
 
           // add results to list
-          for (var prop, i=0; i < results.length; i++) {
+          for (var prop, item, i=0; i < results.length; i++) {
               prop = results[i];
+              item = $(prop.html);
               search_results[prop.id] = prop;
-              $('<li>')
-                  .attr('id', p.id_prefix + prop.id)
-                  .html(prop.html)
-                  .appendTo(search_results_widget.container);
+              search_results_widget.insert({
+                  id: prop.id,
+                  classes: prop.class_name ? String(prop.class_name).split(' ') : [],
+                  html: item,
+                  collapsed: true
+              }, prop.parent);
+
+              // disable checkbox if item already exists in main list
+              if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
+                  item.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+              }
           }
 
           search_results_container.show();
@@ -110,7 +138,7 @@ function kolab_folderlist(node, p)
 
         // send search request(s) to server
         if (search.query && search.execute) {
-            var sources = [ 'folders' /*, 'users'*/ ];
+            var sources = p.search_sources || [ 'folders' ];
             var reqid = rcmail.multi_thread_http_request({
                 items: sources,
                 threads: rcmail.env.autocomplete_threads || 1,
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 970316e..872ce29 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -44,6 +44,7 @@ class kolab_storage
     private static $states;
     private static $config;
     private static $imap;
+    private static $ldap;
 
     // Default folder names
     private static $default_folders = array(
@@ -101,6 +102,40 @@ class kolab_storage
         return self::$ready;
     }
 
+    /**
+     * Initializes LDAP object to resolve Kolab users
+     */
+    public static function ldap()
+    {
+        if (self::$ldap) {
+            return self::$ldap;
+        }
+
+        $rcmail = rcube::get_instance();
+        $config = $rcmail->config->get('kolab_users_directory', $rcmail->config->get('kolab_auth_addressbook'));
+
+        if (!is_array($config)) {
+            $ldap_config = (array)$rcmail->config->get('ldap_public');
+            $config = $ldap_config[$config];
+        }
+
+        if (empty($config)) {
+            return null;
+        }
+
+        // overwrite filter option
+        if ($filter = $rcmail->config->get('kolab_users_filter')) {
+            $rcmail->config->set('kolab_auth_filter', $filter);
+        }
+
+        // re-use the LDAP wrapper class from kolab_auth plugin
+        require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
+
+        self::$ldap = new kolab_auth_ldap($config);
+
+        return self::$ldap;
+    }
+
 
     /**
      * Get a list of storage folders for the given data type
@@ -490,7 +525,7 @@ class kolab_storage
                         $folder = substr($folder, $pos+1);
                     }
                     else {
-                        $prefix = $folder;
+                        $prefix = '('.$folder.')';
                         $folder = '';
                     }
 
@@ -811,7 +846,7 @@ class kolab_storage
 
         // $folders is a result of get_folders() we can assume folders were already sorted
         foreach (array_keys($nsnames) as $ns) {
-            // asort($nsnames[$ns], SORT_LOCALE_STRING);
+            asort($nsnames[$ns], SORT_LOCALE_STRING);
             foreach (array_keys($nsnames[$ns]) as $utf7name) {
                 $out[] = $folders[$utf7name];
             }
@@ -829,13 +864,18 @@ class kolab_storage
      *
      * @return array Flat folders list
      */
-    public static function folder_hierarchy($folders, &$tree)
+    public static function folder_hierarchy($folders, &$tree = null)
     {
         $_folders = array();
-        $delim    = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
-        $tree     = new virtual_kolab_storage_folder('', '<root>', '');  // create tree root
+        $delim    = self::$imap->get_hierarchy_delimiter();
+        $other_ns = self::$imap->get_namespace('other');
+        $tree     = new kolab_storage_virtual_folder('', '<root>', '');  // create tree root
         $refs     = array('' => $tree);
 
+        if (is_array($other_ns)) {
+            $other_ns = rtrim($other_ns[0][0], '/');
+        }
+
         foreach ($folders as $idx => $folder) {
             $path = explode($delim, $folder->name);
             array_pop($path);
@@ -852,9 +892,15 @@ class kolab_storage
 
                 while (count($path) >= $depth && ($parent = join($delim, $path))) {
                     array_pop($path);
-                    $name = kolab_storage::object_name($parent, $folder->get_namespace());
+                    $parent_parent = join($delim, $path);
                     if (!$refs[$parent]) {
-                        $refs[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace(), join($delim, $path));
+                        if ($parent_parent == $other_ns) {
+                            $refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
+                        }
+                        else {
+                            $name = kolab_storage::object_name($parent, $folder->get_namespace());
+                            $refs[$parent] = new kolab_storage_virtual_folder($parent, $name, $folder->get_namespace(), $parent_parent);
+                        }
                         $parents[] = $refs[$parent];
                     }
                 }
@@ -1023,14 +1069,22 @@ class kolab_storage
      * Change subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Only remove temporary subscription
      *
      * @return True on success, false on error
      */
-    public static function folder_unsubscribe($folder)
+    public static function folder_unsubscribe($folder, $temp = false)
     {
         self::setup();
 
-        if (self::$imap->unsubscribe($folder)) {
+        // temporary/session subscription
+        if ($temp) {
+            if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+                unset($_SESSION['kolab_subscribed_folders'][$i]);
+            }
+            return true;
+        }
+        else if (self::$imap->unsubscribe($folder)) {
             self::$subscriptions === null;
             return true;
         }
@@ -1078,10 +1132,8 @@ class kolab_storage
      */
     public static function folder_deactivate($folder)
     {
-        // remove from temp subscriptions
-        if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
-            unset($_SESSION['kolab_subscribed_folders'][$i]);
-        }
+        // remove from temp subscriptions, really?
+        self::folder_unsubscribe($folder, true);
 
         return self::set_state($folder, false);
     }
@@ -1241,57 +1293,117 @@ class kolab_storage
         }
     }
 
+
     /**
-     * Handler for user_delete plugin hooks
      *
-     * Remove all cache data from the local database related to the given user.
+     * @param mixed   $query    Search value (or array of field => value pairs)
+     * @param int     $mode     Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
+     * @param array   $required List of fields that shall ot be empty
+     * @param int     $limit    Number of records
+     *
+     * @return array List or false on error
      */
-    public static function delete_user_folders($args)
+    public static function search_users($query, $mode = 1, $required = array(), $limit = 0)
     {
-        $db = rcmail::get_instance()->get_dbh();
-        $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
-        $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
-    }
+        // requires a working LDAP setup
+        if (!self::ldap()) {
+            return array();
+        }
 
-}
+        // FIXME: make search attributes configurable
+        $results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
 
-/**
- * Helper class that represents a virtual IMAP folder
- * with a subset of the kolab_storage_folder API.
- */
-class virtual_kolab_storage_folder
-{
-    public $id;
-    public $name;
-    public $namespace;
-    public $parent = '';
-    public $children = array();
-    public $virtual = true;
-    protected $displayname;
-
-    public function __construct($name, $dispname, $ns, $parent = '')
-    {
-        $this->id        = kolab_storage::folder_id($name);
-        $this->name      = $name;
-        $this->namespace = $ns;
-        $this->parent    = $parent;
-        $this->displayname = $dispname;
+        // resolve to IMAP folder name
+        $other_ns = self::$imap->get_namespace('other');
+        $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+
+        array_walk($results, function(&$user, $dn) use ($other_ns, $user_attrib) {
+            list($localpart, $domain) = explode('@', $user[$user_attrib]);
+            $root = $other_ns[0][0];
+            $user['kolabtargetfolder'] = $root . $localpart;
+        });
+
+        return $results;
     }
 
-    public function get_namespace()
+
+    /**
+     * Returns a list of IMAP folders shared by the given user
+     *
+     * @param array   User entry from LDAP
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+     * @param boolean Return subscribed folders only (null to use configured subscription mode)
+     * @param array   Will be filled with folder-types data
+     *
+     * @return array List of folders
+     */
+    public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
     {
-        return $this->namespace;
+        $folders = array();
+
+        // use localpart of user attribute as root for folder listing
+        $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+        if (!empty($user[$user_attrib])) {
+            list($mbox) = explode('@', $user[$user_attrib]);
+
+            $other_ns = self::$imap->get_namespace('other');
+            if (is_array($other_ns)) {
+                $other_ns = $other_ns[0][0];
+            }
+
+            $folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
+        }
+
+        return $folders;
     }
 
-    public function get_name()
+
+    /**
+     * Get a list of (virtual) top-level folders from the other users namespace
+     *
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
+     *
+     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+     */
+    public static function get_user_folders($subscribed)
     {
-        // this is already kolab_storage::object_name() result
-        return $this->displayname;
+        $folders = $folderdata = array();
+
+        if (self::setup()) {
+            $delimiter = self::$imap->get_hierarchy_delimiter();
+            $other_ns = self::$imap->get_namespace('other');
+            if (is_array($other_ns)) {
+                $other_ns = rtrim($other_ns[0][0], $delimiter);
+                $other_depth = count(explode($delimiter, $other_ns));
+            }
+
+            foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+                $path = explode($delimiter, $foldername);
+                $depth = count($path) - $other_depth;
+                array_pop($path);
+
+                // only list top-level folders of the 'other' namespace
+                if ($depth == 1) {
+                    $folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
+                }
+            }
+        }
+
+        return $folders;
     }
 
-    public function get_foldername()
+
+    /**
+     * Handler for user_delete plugin hooks
+     *
+     * Remove all cache data from the local database related to the given user.
+     */
+    public static function delete_user_folders($args)
     {
-        $parts = explode('/', $this->name);
-        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+        $db = rcmail::get_instance()->get_dbh();
+        $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
+        $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
     }
+
 }
+
diff --git a/plugins/libkolab/lib/kolab_storage_user_folder.php b/plugins/libkolab/lib/kolab_storage_user_folder.php
new file mode 100644
index 0000000..55e38a0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_user_folder.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @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_user_folder extends kolab_storage_virtual_folder
+{
+    protected static $ldapcache = array();
+
+    public $ldaprec;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($name, $parent = '', $ldaprec = null)
+    {
+        parent::__construct($name, $name, 'other', $parent);
+
+        if (!empty($ldaprec)) {
+            self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+        }
+        // use value cached in memory for repeated lookups
+        else if (array_key_exists($name, self::$ldapcache)) {
+            $this->ldaprec = self::$ldapcache[$name];
+        }
+        // lookup user in LDAP and set $this->ldaprec
+        else if ($ldap = kolab_storage::ldap()) {
+            // get domain from current user
+            list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+            $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+            if (!empty($this->ldaprec)) {
+                $this->ldaprec['kolabtargetfolder'] = $name;
+            }
+            self::$ldapcache[$name] = $this->ldaprec;
+        }
+    }
+
+    /**
+     * Getter for the top-end folder name to be displayed
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+            parent::get_foldername();
+    }
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        return $this->ldaprec['mail'];
+    }
+
+    /**
+     * Check activation status of this folder
+     *
+     * @return boolean True if enabled, false if not
+     */
+    public function is_active()
+    {
+        return kolab_storage::folder_is_active($this->name);
+    }
+
+    /**
+     * Change activation status of this folder
+     *
+     * @param boolean The desired subscription status: true = active, false = not active
+     *
+     * @return True on success, false on error
+     */
+    public function activate($active)
+    {
+        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed()
+    {
+        return kolab_storage::folder_is_subscribed($this->name, true);
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     *
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed)
+    {
+        return $subscribed ?
+            kolab_storage::folder_subscribe($this->name, true) :
+            kolab_storage::folder_unsubscribe($this->name, true);
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_virtual_folder.php b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
new file mode 100644
index 0000000..61d0fe0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @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_virtual_folder
+{
+    public $id;
+    public $name;
+    public $namespace;
+    public $parent = '';
+    public $children = array();
+    public $virtual = true;
+
+    protected $displayname;
+
+    public function __construct($name, $dispname, $ns, $parent = '')
+    {
+        $this->id        = kolab_storage::folder_id($name);
+        $this->name      = $name;
+        $this->namespace = $ns;
+        $this->parent    = $parent;
+        $this->displayname = $dispname;
+    }
+
+    /**
+     * Getter for the name of the namespace to which the IMAP folder belongs
+     *
+     * @return string Name of the namespace (personal, other, shared)
+     */
+    public function get_namespace()
+    {
+        return $this->namespace;
+    }
+
+    /**
+     * Get the display name value of this folder
+     *
+     * @return string Folder name
+     */
+    public function get_name()
+    {
+        // this is already kolab_storage::object_name() result
+        return $this->displayname;
+    }
+
+    /**
+     * Getter for the top-end folder name (not the entire path)
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        $parts = explode('/', $this->name);
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+    }
+
+    /**
+     * Get the color value stored in metadata
+     *
+     * @param string Default color value to return if not set
+     * @return mixed Color value from IMAP metadata or $default is not set
+     */
+    public function get_color($default = null)
+    {
+        return $default;
+    }
+}
\ No newline at end of file


commit 8a47c676d52645f424a5234c9c6d2fdfa7536f49
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 13 19:14:08 2014 +0200

    Move new calendar list widget and folder searching to libkolab for shared use

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 4db8593..52baf11 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1548,7 +1548,8 @@ function rcube_calendar_ui(settings)
           id_prefix: 'rcres',
           id_encode: rcmail.html_identifier_encode,
           id_decode: rcmail.html_identifier_decode,
-          selectable: true
+          selectable: true,
+          save_state: true
         });
         resources_treelist.addEventListener('select', function(node) {
           if (resources_data[node.id]) {
@@ -2671,72 +2672,6 @@ function rcube_calendar_ui(settings)
       this.selected_calendar = id;
     };
 
-    // render the results for calendar list search
-    var calendar_search_results = function(results)
-    {
-      if (results.length) {
-        // create treelist widget to present the search results
-        if (!calenders_search_list) {
-          calenders_search_container = $('<div class="searchresults"></div>')
-            .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
-            .insertAfter(rcmail.gui_objects.calendarslist);
-
-          calenders_search_list = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
-            id_prefix: 'rcmlical',
-            selectable: false
-          });
-
-          // register click handler on search result's checkboxes to select the given calendar for listing
-          calenders_search_list.container
-            .appendTo(calenders_search_container)
-            .on('click', 'input[type=checkbox]', function(e){
-              var li = $(this).closest('li'),
-                id = li.attr('id').replace(/^rcmlical/, ''),
-                prop = search_calendars[id],
-                parent_id = prop.parent || null;
-
-              if (!this.checked)
-                return;
-
-              // find parent node and insert at the right place
-              if (parent_id && $('#rcmlical'+parent_id, rcmail.gui_objects.calendarslist).length) {
-                prop.listname = prop.editname;
-                li.children().first().find('.calname').html(Q(prop.listname));
-              }
-
-              // move this calendar to the calendars_list widget
-              calendars_list.insert({
-                id: id,
-                classes: [],
-                html: li.children().first()
-              }, parent_id, parent_id ? true : false);
-
-              search_calendars[id].active = true;
-              add_calendar_source(prop);
-              li.remove();
-
-              // add css classes related to this calendar to document
-              if (cal.css) {
-                $('<style type="text/css"></style>')
-                  .html(cal.css)
-                  .appendTo('head');
-              }
-            });
-        }
-
-        for (var cal, i=0; i < results.length; i++) {
-          cal = results[i];
-          search_calendars[cal.id] = cal;
-          $('<li>')
-            .attr('id', 'rcmlical' + cal.id)
-            .html(cal.html)
-            .appendTo(calenders_search_list.container);
-        }
-
-        calenders_search_container.show();
-      }
-    };
-
     // register the given calendar to the current view
     var add_calendar_source = function(cal)
     {
@@ -2798,37 +2733,31 @@ function rcube_calendar_ui(settings)
     }
 
     // initialize treelist widget that controls the calendars list
-    calendars_list = new rcube_treelist_widget(rcmail.gui_objects.calendarslist, {
+    var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+    calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
       id_prefix: 'rcmlical',
       selectable: true,
-      searchbox: '#calendarlistsearch'
+      save_state: true,
+      searchbox: '#calendarlistsearch',
+      search_action: 'calendar/calendar'
     });
-    calendars_list.addEventListener('select', function(node){
+    calendars_list.addEventListener('select', function(node) {
       me.select_calendar(node.id);
       rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
       rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
     });
-    calendars_list.addEventListener('search', function(search){
-      // hide search results
-      if (calenders_search_list) {
-        calenders_search_container.hide();
-        calenders_search_list.reset();
-      }
-      search_calendars = {};
-
-      // send search request(s) to server
-      if (search.query && search.execute) {
-        var sources = [ 'folders' /*, 'users'*/ ];
-        var reqid = rcmail.multi_thread_http_request({
-          items: sources,
-          threads: rcmail.env.autocomplete_threads || 1,
-          action:  'calendar/calendar',
-          postdata: { action:'search', q:search.query, source:'%s' },
-          lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
-          onresponse: calendar_search_results
-        });
-
-        listsearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
+    calendars_list.addEventListener('insert-item', function(p) {
+      var cal = p.data;
+      if (cal && cal.id) {
+        cal.active = true;
+        add_calendar_source(cal);
+
+        // add css classes related to this calendar to document
+        if (cal.css) {
+          $('<style type="text/css"></style>')
+            .html(cal.css)
+            .appendTo('head');
+        }
       }
     });
 
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 786556f..9c45eb6 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -381,23 +381,10 @@ class kolab_driver extends calendar_driver
       return array();
 
     $this->calendars = array();
-    $imap = $this->rc->get_storage();
 
     // find unsubscribed IMAP folders that have "event" type
     if ($source == 'folders') {
-      $folders = array();
-      foreach ((array)kolab_storage::list_folders('', '*', 'event', false, $folderdata) as $foldername) {
-        // FIXME: only consider the last part of the folder path for searching?
-        $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
-        if (strpos($realname, $query) !== false &&
-            !kolab_storage::folder_is_subscribed($foldername, true) &&
-            $imap->folder_namespace($foldername) != 'other'
-          ) {
-          $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
-        }
-      }
-
-      foreach ($folders as $folder) {
+      foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
         $calendar = new kolab_calendar($folder->name, $this->cal);
         $this->calendars[$calendar->id] = $calendar;
       }
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 69ba9c2..0f5091a 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -117,6 +117,12 @@ class calendar_ui
     $this->cal->include_script('calendar_ui.js');
     $this->cal->include_script('lib/js/fullcalendar.js');
     $this->rc->output->include_script('treelist.js');
+
+    // include kolab folderlist widget if available
+    if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
+      $this->cal->include_script('lib/js/folderlist.js');
+    }
+
     jqueryui::miniColors();
   }
 
@@ -292,9 +298,9 @@ class calendar_ui
     $content = '';
     if (!$attrib['activeonly'] || $prop['active']) {
       $content = html::div($class,
+        html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-        html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')) .
-        html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
+        html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' '))
       );
     }
 
diff --git a/plugins/calendar/lib/js/folderlist.js b/plugins/calendar/lib/js/folderlist.js
new file mode 120000
index 0000000..c49706b
--- /dev/null
+++ b/plugins/calendar/lib/js/folderlist.js
@@ -0,0 +1 @@
+../../../libkolab/js/folderlist.js
\ No newline at end of file
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
new file mode 100644
index 0000000..eed8b50
--- /dev/null
+++ b/plugins/libkolab/js/folderlist.js
@@ -0,0 +1,130 @@
+/**
+ * Kolab groupware folders treelist widget
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * @licstart  The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * 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/>.
+ *
+ * @licend  The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+function kolab_folderlist(node, p)
+{
+    // extends treelist.js
+    rcube_treelist_widget.call(this, node, p);
+
+    // private vars
+    var me = this;
+    var search_results;
+    var search_results_widget;
+    var search_results_container;
+    var listsearch_request;
+
+    var Q = rcmail.quote_html;
+
+    // render the results for folderlist search
+    function render_search_results(results)
+    {
+        if (results.length) {
+          // create treelist widget to present the search results
+          if (!search_results_widget) {
+              search_results_container = $('<div class="searchresults"></div>')
+                  .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+                  .insertAfter(me.container);
+
+              search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+                  id_prefix: p.id_prefix,
+                  selectable: false
+              });
+
+              // register click handler on search result's checkboxes to select the given item for listing
+              search_results_widget.container
+                  .appendTo(search_results_container)
+                  .on('click', 'input[type=checkbox]', function(e) {
+                      if (!this.checked)
+                          return;
+
+                      var li = $(this).closest('li'),
+                          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+                          prop = search_results[id],
+                          parent_id = prop.parent || null;
+
+                      // find parent node and insert at the right place
+                      if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
+                          prop.listname = prop.editname;
+                          li.children().first().children('span,a').first().html(Q(prop.listname));
+                      }
+
+                      // move this result item to the main list widget
+                      me.insert({
+                          id: id,
+                          classes: [],
+                          html: li.children().first()
+                      }, parent_id, parent_id ? true : false);
+
+                      delete prop.html;
+                      me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+                      li.remove();
+                  });
+          }
+
+          // add results to list
+          for (var prop, i=0; i < results.length; i++) {
+              prop = results[i];
+              search_results[prop.id] = prop;
+              $('<li>')
+                  .attr('id', p.id_prefix + prop.id)
+                  .html(prop.html)
+                  .appendTo(search_results_widget.container);
+          }
+
+          search_results_container.show();
+        }
+    }
+
+    // do some magic when search is performed on the widget
+    this.addEventListener('search', function(search) {
+        // hide search results
+        if (search_results_widget) {
+            search_results_container.hide();
+            search_results_widget.reset();
+        }
+        search_results = {};
+
+        // send search request(s) to server
+        if (search.query && search.execute) {
+            var sources = [ 'folders' /*, 'users'*/ ];
+            var reqid = rcmail.multi_thread_http_request({
+                items: sources,
+                threads: rcmail.env.autocomplete_threads || 1,
+                action:  p.search_action || 'listsearch',
+                postdata: { action:'search', q:search.query, source:'%s' },
+                lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+                onresponse: render_search_results
+            });
+
+            listsearch_request = { id:reqid, sources:sources.slice(), num:sources.length };
+        }
+    });
+
+}
+
+// link prototype from base class
+kolab_folderlist.prototype = rcube_treelist_widget.prototype;
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 55526fd..970316e 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -759,6 +759,39 @@ class kolab_storage
 
 
     /**
+     * Search for shared or otherwise not listed groupware folders the user has access
+     *
+     * @param string Folder type of folders to search for
+     * @param string Search string
+     * @param array  Namespace(s) to exclude results from
+     *
+     * @return array List of matching kolab_storage_folder objects
+     */
+    public static function search_folders($type, $query, $exclude_ns = array())
+    {
+        if (!self::setup()) {
+            return array();
+        }
+
+        $folders = array();
+
+        // find unsubscribed IMAP folders of the given type
+        foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+            // FIXME: only consider the last part of the folder path for searching?
+            $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+            if (strpos($realname, $query) !== false &&
+                !self::folder_is_subscribed($foldername, true) &&
+                !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
+              ) {
+                $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+            }
+        }
+
+        return $folders;
+    }
+
+
+    /**
      * Sort the given list of kolab folders by namespace/name
      *
      * @param array List of kolab_storage_folder objects




More information about the commits mailing list