108 commits - plugins/calendar plugins/kolab_addressbook plugins/kolab_auth plugins/kolab_config plugins/kolab_core plugins/kolab_folders plugins/kolab_zpush plugins/libkolab plugins/odfviewer

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Wed May 23 11:24:46 CEST 2012


 plugins/calendar/.gitignore                                   |    3 
 plugins/calendar/README                                       |   18 
 plugins/calendar/TODO                                         |   11 
 plugins/calendar/calendar.php                                 |  105 
 plugins/calendar/calendar_ui.js                               |   17 
 plugins/calendar/drivers/database/database_driver.php         |   28 
 plugins/calendar/drivers/kolab/SQL/mysql.sql                  |    5 
 plugins/calendar/drivers/kolab/kolab_calendar.php             |  509 -
 plugins/calendar/drivers/kolab/kolab_driver.php               |  109 
 plugins/calendar/lib/Horde_Date.php                           |  774 ++
 plugins/calendar/lib/Horde_Date_Recurrence.php                |  773 --
 plugins/calendar/lib/Horde_iCalendar.php                      | 3289 ++++++++++
 plugins/calendar/lib/calendar_ical.php                        |    5 
 plugins/calendar/lib/calendar_itip.php                        |    5 
 plugins/calendar/lib/calendar_ui.php                          |   27 
 plugins/calendar/lib/get_horde_icalendar.sh                   |   31 
 plugins/calendar/package.xml                                  |   10 
 plugins/calendar/skins/larry/calendar.css                     |   46 
 plugins/calendar/skins/larry/templates/attachment.html        |    2 
 plugins/kolab_addressbook/kolab_addressbook.php               |   26 
 plugins/kolab_addressbook/lib/kolab_addressbook_ui.php        |   10 
 plugins/kolab_addressbook/lib/rcube_kolab_contacts.php        |  620 -
 plugins/kolab_addressbook/localization/de_CH.inc              |    3 
 plugins/kolab_addressbook/localization/de_DE.inc              |    3 
 plugins/kolab_addressbook/localization/en_US.inc              |    3 
 plugins/kolab_addressbook/package.xml                         |    2 
 plugins/kolab_addressbook/skins/larry/kolab_addressbook.css   |   28 
 plugins/kolab_addressbook/skins/larry/templates/bookedit.html |   24 
 plugins/kolab_auth/package.xml                                |    2 
 plugins/kolab_config/kolab_config.php                         |   11 
 plugins/kolab_config/package.xml                              |    2 
 plugins/kolab_core/package.xml                                |    2 
 plugins/kolab_folders/kolab_folders.php                       |  227 
 plugins/kolab_folders/package.xml                             |    8 
 plugins/kolab_zpush/kolab_zpush.php                           |   53 
 plugins/kolab_zpush/kolab_zpush_ui.php                        |   18 
 plugins/kolab_zpush/package.xml                               |    9 
 plugins/libkolab/README                                       |   43 
 plugins/libkolab/SQL/mysql.sql                                |   22 
 plugins/libkolab/lib/kolab_format.php                         |  289 
 plugins/libkolab/lib/kolab_format_contact.php                 |  532 +
 plugins/libkolab/lib/kolab_format_distributionlist.php        |  160 
 plugins/libkolab/lib/kolab_format_event.php                   |  638 +
 plugins/libkolab/lib/kolab_storage.php                        |  462 +
 plugins/libkolab/lib/kolab_storage_cache.php                  |  568 +
 plugins/libkolab/lib/kolab_storage_folder.php                 |  835 ++
 plugins/libkolab/libkolab.php                                 |   74 
 plugins/odfviewer/odfviewer.php                               |   23 
 plugins/odfviewer/webodf.js                                   |  382 -
 49 files changed, 8712 insertions(+), 2134 deletions(-)

New commits:
commit 2cf2cbcca88677239aa35ed186528e4825498d28
Merge: 8674399 f5e96dc
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed May 23 10:24:27 2012 +0100

    Merge branch 'dev/kolab3'
    
    Conflicts:
    	plugins/calendar/drivers/kolab/kolab_driver.php



commit f5e96dcabb86e378bf5f921af31645523c694550
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun May 20 19:20:03 2012 +0200

    Adapt to new roundcube framework API (#787)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 5f7d1fd..83993ac 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2220,15 +2220,15 @@ class calendar extends rcube_plugin
     $charset = RCMAIL_CHARSET;
     
     // establish imap connection
-    $this->rc->imap_connect();
-    $this->rc->imap->set_mailbox($mbox);
+    $imap = $this->rc->get_storage();
+    $imap->set_mailbox($mbox);
 
     if ($uid && $mime_id) {
       list($mime_id, $index) = explode(':', $mime_id);
-      $part = $this->rc->imap->get_message_part($uid, $mime_id);
+      $part = $imap->get_message_part($uid, $mime_id);
       if ($part->ctype_parameters['charset'])
         $charset = $part->ctype_parameters['charset'];
-      $headers = $this->rc->imap->get_headers($uid);
+      $headers = $imap->get_message_headers($uid);
     }
 
     $events = $this->get_ical()->import($part, $charset);
@@ -2365,8 +2365,8 @@ class calendar extends rcube_plugin
     $event = array();
     
     // establish imap connection
-    $this->rc->imap_connect();
-    $this->rc->imap->set_mailbox($mbox);
+    $imap = $this->rc->get_storage();
+    $imap->set_mailbox($mbox);
     $message = new rcube_message($uid);
 
     if ($message->headers) {
@@ -2384,7 +2384,7 @@ class calendar extends rcube_plugin
 
         foreach ((array)$message->attachments as $part) {
           $attachment = array(
-            'data' => $this->rc->imap->get_message_part($uid, $part->mime_id, $part),
+            'data' => $imap->get_message_part($uid, $part->mime_id, $part),
             'size' => $part->size,
             'name' => $part->filename,
             'mimetype' => $part->mimetype,


commit cb61bf76a1084b13b5e2d8ebb1c6aed7b986a3d8
Author: Aleksander Machniak <alec at alec.pl>
Date:   Thu May 17 15:57:23 2012 +0200

    Improved folder_create() method

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index b97d2e1..c00f45f 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -307,7 +307,7 @@ class kolab_driver extends calendar_driver
     }
     // create new folder
     else {
-      if (!($result = kolab_storage::folder_create($folder, 'event', false)))
+      if (!($result = kolab_storage::folder_create($folder, 'event')))
         $this->last_error = kolab_storage::$last_error;
     }
 
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 3e1d200..5fd45b7 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -497,7 +497,7 @@ class kolab_addressbook extends rcube_plugin
                 $folder = $plugin['name'];
 
                 if (!$plugin['abort']) {
-                    $result = kolab_storage::folder_create($folder, 'contact', false);
+                    $result = kolab_storage::folder_create($folder, 'contact');
                 }
                 else {
                     $result = $plugin['result'];
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 62b7796..5924530 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -173,25 +173,30 @@ class kolab_storage
     /**
      * Creates IMAP folder
      *
-     * @param string $name    Folder name (UTF7-IMAP)
-     * @param string $type    Folder type
-     * @param bool   $default True if older is default (for specified type)
+     * @param string $name        Folder name (UTF7-IMAP)
+     * @param string $type        Folder type
+     * @param bool   $subscribed  Sets folder subscription
      *
      * @return bool True on success, false on failure
      */
-    public static function folder_create($name, $type=null, $default=false)
+    public static function folder_create($name, $type = null, $subscribed = false)
     {
         self::setup();
 
-        if (self::$imap->create_folder($name)) {
+        if ($saved = self::$imap->create_folder($name, $subscribed)) {
             // set metadata for folder type
-            $ctype = $type . ($default ? '.default' : '');
-            $saved = self::$imap->set_metadata($name, array(self::CTYPE_KEY => $ctype));
+            if ($type) {
+                $saved = self::$imap->set_metadata($name, array(self::CTYPE_KEY => $type));
+
+                // revert if metadata could not be set
+                if (!$saved) {
+                    self::$imap->delete_folder($name);
+                }
+            }
+        }
 
-            if ($saved)
-                return true;
-            else  // revert if metadata could not be set
-                self::$imap->delete_folder($name);
+        if ($saved) {
+            return true;
         }
 
         self::$last_error = self::$imap->get_error_str();


commit d91fe846171ed0b5f1bcfb424c98793b200c808a
Author: Aleksander Machniak <alec at alec.pl>
Date:   Thu May 17 10:51:45 2012 +0200

    Remove dependency on kolab_folders

diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index bf54569..5d64dfb 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -35,9 +35,6 @@ class libkolab extends rcube_plugin
         // load local config
         $this->load_config();
 
-        // require kolab_folders plugin for listing folders by type (annotation)
-        $this->require_plugin('kolab_folders');
-
         $this->add_hook('storage_init', array($this, 'storage_init'));
 
         // extend include path to load bundled lib classes


commit 394fb8f56dc4f4cb755f19acf3495c60db43489b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 18:58:57 2012 +0200

    Store event alarm status by event uid + user id. Attention: database schema changed!

diff --git a/plugins/calendar/TODO b/plugins/calendar/TODO
index 35d5e0a..b1a08d7 100644
--- a/plugins/calendar/TODO
+++ b/plugins/calendar/TODO
@@ -13,7 +13,7 @@
 + View: 3.3: Display modes (agenda / day / week / month)
   + Day / Week / Month
   + List (Agenda) view
-    - Add selection for date range
+    + Add selection for date range
   - Individual days selection
 + Show list of calendars in a (hideable) drawer
   + View: 3.1: Folder list
@@ -39,6 +39,7 @@
 + Colors for calendars should be user-configurable
 + ICS parser/generator (http://code.google.com/p/qcal/)
 
+- Script to send event alarms by email (in cronjob)
 - Export *with* attachments
 - Remember last visited view
 - Create/manage invdividual views
diff --git a/plugins/calendar/drivers/kolab/SQL/mysql.sql b/plugins/calendar/drivers/kolab/SQL/mysql.sql
index e64413a..7a93d0a 100644
--- a/plugins/calendar/drivers/kolab/SQL/mysql.sql
+++ b/plugins/calendar/drivers/kolab/SQL/mysql.sql
@@ -8,9 +8,12 @@
 
 CREATE TABLE IF NOT EXISTS `kolab_alarms` (
   `event_id` VARCHAR(255) NOT NULL,
+  `user_id` int(10) UNSIGNED NOT NULL,
   `notifyat` DATETIME DEFAULT NULL,
   `dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
-  PRIMARY KEY(`event_id`)
+  PRIMARY KEY(`event_id`),
+  CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
 ) /*!40000 ENGINE=INNODB */;
 
 CREATE TABLE IF NOT EXISTS `itipinvitations` (
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 5d1906b..b97d2e1 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -786,11 +786,13 @@ class kolab_driver extends calendar_driver
     if (!empty($events)) {
       $event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events));
       $result = $this->rc->db->query(sprintf(
-        "SELECT * FROM kolab_alarms
-         WHERE event_id IN (%s)",
-         join(',', $event_ids),
-         $this->rc->db->now()
-       ));
+          "SELECT * FROM kolab_alarms
+           WHERE event_id IN (%s) AND user_id=?",
+           join(',', $event_ids),
+           $this->rc->db->now()
+          ),
+          $this->rc->user->ID
+       );
 
       while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
         $dbdata[$e['event_id']] = $e;
@@ -820,16 +822,22 @@ class kolab_driver extends calendar_driver
   public function dismiss_alarm($event_id, $snooze = 0)
   {
     // delete old alarm entry
-    $this->rc->db->query("DELETE FROM kolab_alarms WHERE event_id=?", $event_id);
+    $this->rc->db->query(
+      "DELETE FROM kolab_alarms
+       WHERE event_id=? AND user_id=?",
+       $event_id,
+       $this->rc->user->ID
+    );
 
     // set new notifyat time or unset if not snoozed
     $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
 
     $query = $this->rc->db->query(
       "INSERT INTO kolab_alarms
-       (event_id, dismissed, notifyat)
-       VALUES(?, ?, ?)",
+       (event_id, user_id, dismissed, notifyat)
+       VALUES(?, ?, ?, ?)",
       $event_id,
+      $this->rc->user->ID,
       $snooze > 0 ? 0 : 1,
       $notifyat
     );


commit 839adb2c267ab8f562c7daf87659d9a5e6bece03
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 18:36:03 2012 +0200

    - Adapt event alarms to new storage format
    - Query objects by x-has-alarm tag
    - Re-use code to compute absolute times for alarms

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index b808323..5f7d1fd 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1264,7 +1264,47 @@ class calendar extends rcube_plugin
     
     return false;
   }
-  
+
+  /**
+   * Get the next alarm (time & action) for the given event
+   *
+   * @param array Event data
+   * @return array Hash array with alarm time/type or null if no alarms are configured
+   */
+  public static function get_next_alarm($event)
+  {
+      if (!$event['alarms'])
+        return null;
+
+      // TODO: handle multiple alarms (currently not supported)
+      list($trigger, $action) = explode(':', $event['alarms'], 2);
+
+      $notify = self::parse_alaram_value($trigger);
+      if (!empty($notify[1])){  // offset
+        $mult = 1;
+        switch ($notify[1]) {
+          case '-S': $mult =     -1; break;
+          case '+S': $mult =      1; break;
+          case '-M': $mult =    -60; break;
+          case '+M': $mult =     60; break;
+          case '-H': $mult =  -3600; break;
+          case '+H': $mult =   3600; break;
+          case '-D': $mult = -86400; break;
+          case '+D': $mult =  86400; break;
+          case '-W': $mult = -604800; break;
+          case '+W': $mult =  604800; break;
+        }
+        $offset = $notify[0] * $mult;
+        $refdate = $mult > 0 ? $event['end'] : $event['start'];
+        $notify_at = $refdate + $offset;
+      }
+      else {  // absolute timestamp
+        $notify_at = $notify[0];
+      }
+
+      return array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY');
+  }
+
   /**
    * Convert the internal structured data into a vcalendar rrule 2.0 string
    */
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index ba7f1e7..b871d51 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -396,31 +396,13 @@ class database_driver extends calendar_driver
    */
   private function _get_notification($event)
   {
-    if ($event['alarms']) {
-      list($trigger, $action) = explode(':', $event['alarms']);
-      $notify = calendar::parse_alaram_value($trigger);
-      if (!empty($notify[1])){  // offset
-        $mult = 1;
-        switch ($notify[1]) {
-          case '-M': $mult =    -60; break;
-          case '+M': $mult =     60; break;
-          case '-H': $mult =  -3600; break;
-          case '+H': $mult =   3600; break;
-          case '-D': $mult = -86400; break;
-          case '+D': $mult =  86400; break;
-        }
-        $offset = $notify[0] * $mult;
-        $refdate = $mult > 0 ? $event['end'] : $event['start'];
-        $notify_at = $refdate + $offset;
-      }
-      else {  // absolute timestamp
-        $notify_at = $notify[0];
-      }
+    if ($event['alarms'] && $event['start'] > time()) {
+      $alarm = calendar::get_next_alarm($event);
 
-      if ($event['start'] > time())
-        return date('Y-m-d H:i:s', $notify_at);
+      if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
+        return date('Y-m-d H:i:s', $alarm['time']);
     }
-    
+
     return null;
   }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 6104972..d036658 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -197,16 +197,16 @@ class kolab_calendar
    * @param  integer Event's new start (unix timestamp)
    * @param  integer Event's new end (unix timestamp)
    * @param  string  Search query (optional)
-   * @param  boolean Strip virtual events (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)
+  public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
   {
     // query Kolab storage
-    $query = array(
-      array('dtstart', '<=', $end),
-      array('dtend',   '>=', $start),
-    );
+    $query[] = array('dtstart', '<=', $end);
+    $query[] = array('dtend',   '>=', $start);
+
     foreach ((array)$this->storage->select($query) as $record) {
       $event = $this->_to_rcube_event($record);
       $this->events[$event['id']] = $event;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 6ce7a84..5d1906b 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -765,17 +765,19 @@ class kolab_driver extends calendar_driver
     $time = $slot + $interval;
     
     $events = array();
+    $query = array(array('tags', 'LIKE', '% x-has-alarms %'));
     foreach ($this->calendars as $cid => $calendar) {
       // skip calendars with alarms disabled
       if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
         continue;
 
-      foreach ($calendar->list_events($time, $time + 86400 * 365) as $e) {
+      foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
         // add to list if alarm is set
-        if ($e['_alarm'] && ($notifyat = $e['start'] - $e['_alarm'] * 60) <= $time) {
+        $alarm = calendar::get_next_alarm($e);
+        if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
           $id = $e['id'];
           $events[$id] = $e;
-          $events[$id]['notifyat'] = $notifyat;
+          $events[$id]['notifyat'] = $alarm['time'];
         }
       }
     }


commit 3f5712a117302e60a7610e99d7c6f4b3a94acb7a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 18:34:21 2012 +0200

    Save event categories and 'has alarm' information as tags in cache

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index c2c0ddf..699cfb8 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,6 +26,8 @@ class kolab_format_event extends kolab_format
 {
     public $CTYPE = 'application/calendar+xml';
 
+    public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email');
+
     private $sensitivity_map = array(
         'public'       => kolabformat::ClassPublic,
         'private'      => kolabformat::ClassPrivate,
@@ -469,6 +471,54 @@ class kolab_format_event extends kolab_format
     }
 
     /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        foreach ((array)$this->data['categories'] as $cat) {
+            $tags[] = rcube_utils::normalize_string($cat);
+        }
+
+        if (!empty($this->data['alarms'])) {
+            $tags[] = 'x-has-alarms';
+        }
+
+        return $tags;
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $data = '';
+        foreach (self::$fulltext_cols as $colname) {
+            list($col, $field) = explode(':', $colname);
+
+            if ($field) {
+                $a = array();
+                foreach ((array)$this->data[$col] as $attr)
+                    $a[] = $attr[$field];
+                $val = join(' ', $a);
+            }
+            else {
+                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+            }
+
+            if (strlen($val))
+                $data .= $val . ' ';
+        }
+
+        return array_unique(rcube_utils::normalize_string($data, true));
+    }
+
+    /**
      * Load data from old Kolab2 format
      */
     public function fromkolab2($rec)


commit 3ec99f89a6555e3d1ba9256c06ef2b968d3b93a8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 17:13:49 2012 +0200

    Add option to disable SSL certificate checks when triggering Free/Busy

diff --git a/plugins/libkolab/README b/plugins/libkolab/README
index b44d4d2..0a3c0ce 100644
--- a/plugins/libkolab/README
+++ b/plugins/libkolab/README
@@ -38,3 +38,6 @@ $rcmail_config['kolab_cache'] = true;
 // Defaults to https://<imap-server->/freebusy
 $rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
 
+// Set this option to disable SSL certificate checks when triggering Free/Busy (enabled by default)
+$rcmail_config['kolab_ssl_verify_peer'] = false;
+
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 5f6d289..da0718a 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -737,7 +737,9 @@ class kolab_storage_folder
         require_once('HTTP/Request2.php');
 
         try {
+            $rcmail = rcube::get_instance();
             $request = new HTTP_Request2($url);
+            $request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
 
             // set authentication credentials
             if ($auth_user && $auth_passwd)


commit e50d35c1a3202c50ac3690e3a31eb61b462fcbd5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 17:12:54 2012 +0200

    Set and read Content-ID header of MIME message parts as suggested by the spec; Uses the latest (not yet released) version of the PEAR Mail_Mime package

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 1fc84cf..5f6d289 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -403,8 +403,9 @@ class kolab_storage_folder
             if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
                 $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
             }
-            else if ($part->filename) {
-                $attachments[$part->filename] = array(
+            else if ($part->filename || $part->content_id) {
+                $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
+                $attachments[$key] = array(
                     'id' => $part->mime_id,
                     'mimetype' => $part->mimetype,
                     'size' => $part->size,
@@ -625,7 +626,7 @@ class kolab_storage_folder
 
         $format->set($object);
         $xml = $format->write();
-        $object['uid'] = $format->uid;  // get read UID from format
+        $object['uid'] = $format->uid;  // read UID from format
         $object['_formatobj'] = $format;
 
         if (!$format->is_valid() || empty($object['uid'])) {
@@ -652,13 +653,13 @@ class kolab_storage_folder
             . 'To view this object you will need an email client that understands the Kolab Groupware format. '
             . "For a list of such email clients please visit http://www.kolab.org/\n\n");
 
-        $mime->addAttachment($xml,
-            $format->CTYPE,
-            'kolab.xml',
-            false, '8bit', 'attachment', RCMAIL_CHARSET, '', '',
-            $rcmail->config->get('mime_param_folding') ? 'quoted-printable' : null,
-            $rcmail->config->get('mime_param_folding') == 2 ? 'quoted-printable' : null,
-            '', RCMAIL_CHARSET
+        $mime->addAttachment($xml,  // file
+            $format->CTYPE,         // content-type
+            'kolab.xml',            // filename
+            false,                  // is_file
+            '8bit',                 // encoding
+            'attachment',           // disposition
+            RCMAIL_CHARSET          // charset
         );
         $part_id++;
 
@@ -669,12 +670,15 @@ class kolab_storage_folder
                 $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
                 $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox']);
             }
+
+            $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $name . '>', RCMAIL_CHARSET, 'quoted-printable'));
+
             if (!empty($att['content'])) {
-                $mime->addAttachment($att['content'], $att['mimetype'], $name, false);
+                $mime->addAttachment($att['content'], $att['mimetype'], $name, false, 'base64', 'attachment', '', '', '', null, null, '', null, $headers);
                 $part_id++;
             }
             else if (!empty($att['path'])) {
-                $mime->addAttachment($att['path'], $att['mimetype'], $name, true);
+                $mime->addAttachment($att['path'], $att['mimetype'], $name, true, 'base64', 'attachment', '', '', '', null, null, '', null, $headers);
                 $part_id++;
             }
 


commit f7f6c10c4ac432255e4ed39dee9eb50dc5c6989d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 16:33:07 2012 +0200

    Add some documentation

diff --git a/plugins/calendar/lib/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh
index 07b7608..1992bf2 100755
--- a/plugins/calendar/lib/get_horde_icalendar.sh
+++ b/plugins/calendar/lib/get_horde_icalendar.sh
@@ -16,6 +16,7 @@ echo "<?php
 /**
  * This is a concatenated copy of the following files:
  *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
  */
 
 require_once(dirname(__FILE__) . '/Horde_Date.php');"
diff --git a/plugins/libkolab/README b/plugins/libkolab/README
index a60d548..b44d4d2 100644
--- a/plugins/libkolab/README
+++ b/plugins/libkolab/README
@@ -18,3 +18,23 @@ REQUIREMENTS
 * Optional for old format support:
   Horde Kolab_Format package and all of its dependencies
   which are at least Horde_(Browser,DOM,NLS,String,Utils)
+
+
+INSTALLATION
+------------
+To use local cache you need to create a dedicated table in Roundcube's database.
+To do so, execute the SQL commands in SQL/<yourdatabase>.sql
+
+
+CONFIGURATION
+-------------
+The following options can be configured in Roundcube's main config file
+or a local config file (config.inc.php) located in the plugin folder.
+
+// Enable caching of Kolab objects in local database
+$rcmail_config['kolab_cache'] = true;
+
+// Optional override of the URL to read and trigger Free/Busy information of Kolab users
+// Defaults to https://<imap-server->/freebusy
+$rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
+


commit b95a4d19b74ef98f0299c5fcecfe025719704d15
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 16:32:49 2012 +0200

    Catch errors in user's timezone settings

diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index fd911be..bf54569 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -45,7 +45,13 @@ class libkolab extends rcube_plugin
         set_include_path($include_path);
 
         $rcmail = rcmail::get_instance();
-        kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
+        try {
+            kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
+        }
+        catch (Exception $e) {
+            raise_error($e, true);
+            kolab_format::$timezone = new DateTimeZone('GMT');
+        }
 
         // load (old) dependencies if available
         if (@include_once('Horde/Util.php')) {


commit e7ea756cdb81f45dd3dc0cb99a42f0c735ab440c
Merge: e379aaa 3428a64
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 14:16:38 2012 +0200

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit e379aaafc12202f5314c7e8f7295148b721a9948
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 14:16:29 2012 +0200

    Code cleanup: define private members and default DB values; set kolab_format reference to new objects

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 4c822f6..be94bb8 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -29,6 +29,7 @@ class kolab_storage_cache
     private $folder;
     private $uid2msg;
     private $objects;
+    private $index = array();
     private $resource_uri;
     private $enabled = true;
     private $synched = false;
@@ -422,7 +423,7 @@ class kolab_storage_cache
     private function _serialize($object)
     {
         $bincols = array_flip($this->binary_cols);
-        $sql_data = array('dtstart' => null, 'dtend' => null, 'xml' => '');
+        $sql_data = array('dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
 
         // set type specific values
         if ($this->folder->type == 'event') {
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 7ae1a1e..6c4b401 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -271,7 +271,8 @@ class kolab_storage_folder
     {
         if (!$type) $type = $this->type;
 
-        // TODO: synchronize cache first?
+        // synchronize cache first
+        $this->cache->synchronize();
 
         return $this->cache->count(array(array('type','=',$type)));
     }
@@ -625,6 +626,7 @@ class kolab_storage_folder
         $format->set($object);
         $xml = $format->write();
         $object['uid'] = $format->uid;  // get read UID from format
+        $object['_formatobj'] = $format;
 
         if (!$format->is_valid() || empty($object['uid'])) {
             return false;


commit 5cc7fc616fee050806d015d1b13fe67e1a5945d8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 14:15:05 2012 +0200

    Better process of writing Kolab objects: don't use isValid() but check kolabformat::error() after writing. Fixes #769

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 4c8e363..a7b1e48 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -159,6 +159,40 @@ abstract class kolab_format
     }
 
     /**
+     * Check for format errors after calling kolabformat::write*()
+     *
+     * @return boolean True if there were errors, False if OK
+     */
+    protected function format_errors()
+    {
+        $ret = $log = false;
+        switch (kolabformat::error()) {
+            case kolabformat.NoError:
+                $ret = false;
+                break;
+            case kolabformat.Warning:
+                $ret = false;
+                $log = "Warning";
+                break;
+            default:
+                $ret = true;
+                $log = "Error";
+        }
+
+        if ($log) {
+            raise_error(array(
+                'code' => 660,
+                'type' => 'php',
+                'file' => __FILE__,
+                'line' => __LINE__,
+                'message' => "kolabformat write $log: " . kolabformat::errorMessage(),
+            ), true);
+        }
+
+        return $ret;
+    }
+
+    /**
      * Save the last generated UID to the object properties.
      * Should be called after kolabformat::writeXXXX();
      */
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 1f20e3e..d6da235 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -132,11 +132,12 @@ class kolab_format_contact extends kolab_format
     public function write()
     {
         $this->init();
+        $this->xmldata = kolabformat::writeContact($this->obj);
 
-        if ($this->obj->isValid()) {
-            $this->xmldata = kolabformat::writeContact($this->obj);
+        if (!parent::format_errors())
             parent::update_uid();
-        }
+        else
+            $this->xmldata = null;
 
         return $this->xmldata;
     }
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index b8d2208..592387e 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -51,11 +51,12 @@ class kolab_format_distributionlist extends kolab_format
     public function write()
     {
         $this->init();
+        $this->xmldata = kolabformat::writeDistlist($this->obj);
 
-        if ($this->obj->isValid()) {
-            $this->xmldata = kolabformat::writeDistlist($this->obj);
+        if (!parent::format_errors())
             parent::update_uid();
-        }
+        else
+            $this->xmldata = null;
 
         return $this->xmldata;
     }
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 42c323c..c2c0ddf 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -116,11 +116,12 @@ class kolab_format_event extends kolab_format
     public function write()
     {
         $this->init();
+        $this->xmldata = kolabformat::writeEvent($this->obj);
 
-        if ($this->obj->isValid()) {
-            $this->xmldata = kolabformat::writeEvent($this->obj);
+        if (!parent::format_errors())
             parent::update_uid();
-        }
+        else
+            $this->xmldata = null;
 
         return $this->xmldata;
     }


commit 3428a64c0d793ae2c06ac029c6f6f03dac306b18
Author: Aleksander Machniak <alec at alec.pl>
Date:   Wed May 16 13:43:01 2012 +0200

    Migrate to HTTP/Request2

diff --git a/plugins/libkolab/README b/plugins/libkolab/README
index a16250f..a60d548 100644
--- a/plugins/libkolab/README
+++ b/plugins/libkolab/README
@@ -12,5 +12,9 @@ REQUIREMENTS
 * libkolabxml PHP bindings
   - kolabformat.so loaded into PHP
   - kolabformat.php placed somewhere in the include_path
-* Horde Kolab_Format package and all of its dependencies
+* PEAR: HTTP/Request2
+* PEAR: Net/URL2
 
+* Optional for old format support:
+  Horde Kolab_Format package and all of its dependencies
+  which are at least Horde_(Browser,DOM,NLS,String,Utils)
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 7ae1a1e..18f66a7 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -710,7 +710,7 @@ class kolab_storage_folder
             return true;
         }
 
-        if ($result && is_a($result, 'PEAR_Error')) {
+        if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
             return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
                                             $this->name, $result->getMessage()));
         }
@@ -728,18 +728,23 @@ class kolab_storage_folder
      */
     private function trigger_url($url, $auth_user = null, $auth_passwd = null)
     {
-        require_once('HTTP/Request.php');
+        require_once('HTTP/Request2.php');
 
-        $request = new HTTP_Request($url);
+        try {
+            $request = new HTTP_Request2($url);
 
-        // set authentication credentials
-        if ($auth_user && $auth_passwd)
-            $request->setBasicAuth($auth_user, $auth_passwd);
+            // set authentication credentials
+            if ($auth_user && $auth_passwd)
+                $request->setAuth($auth_user, $auth_passwd);
 
-        $result = $request->sendRequest(true);
-        // rcube::write_log('trigger', $request->getResponseBody());
+            $result = $request->send();
+            // rcube::write_log('trigger', $result->getBody());
+        }
+        catch (Exception $e) {
+            return PEAR::raiseError($e->getMessage());
+        }
 
-        return $result;
+        return true;
     }
 
 


commit c3cfc084a7309bd63c38f90b37fed95ffc71132b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 10:49:53 2012 +0200

    Improve fetching contacts with new kolab_storage/cache backend

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 5b7fd8d..5702a0c 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -246,13 +246,12 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function list_records($cols=null, $subset=0)
     {
-        $this->result = $this->count();
+        $this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);;
 
         // list member of the selected group
         if ($this->gid) {
             $this->_fetch_groups();
             $seen = array();
-            $this->result->count = 0;
             foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
                 // skip member that don't match the search filter
                 if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
@@ -269,9 +268,15 @@ class rcube_kolab_contacts extends rcube_addressbook
             }
             $ids = array_keys($seen);
         }
+        else if (is_array($this->filter['ids'])) {
+            $ids = $this->filter['ids'];
+            if ($this->result->count = count($ids))
+                $this->_fetch_contacts(array(array('uid', '=', $ids)));
+        }
         else {
             $this->_fetch_contacts();
-            $ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
+            $ids = array_keys($this->contacts);
+            $this->result->count = count($ids);
         }
 
         // sort data arrays according to desired list sorting
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index afe4dc1..4c822f6 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -326,7 +326,7 @@ class kolab_storage_cache
     /**
      * Get number of objects mathing the given query
      *
-     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
+     * @param array  $query Pseudo-SQL query as list of filter parameter triplets
      * @return integer The number of objects of the given type
      */
     public function count($query = array())
@@ -363,10 +363,18 @@ class kolab_storage_cache
     {
         $sql_where = '';
         foreach ($query as $param) {
+            if ($param[1] == '=' && is_array($param[2])) {
+                $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
+                $param[1] = 'IN';
+            }
+            else {
+                $qvalue = $this->db->quote($param[2]);
+            }
+
             $sql_where .= sprintf(' AND %s %s %s',
                 $this->db->quote_identifier($param[0]),
                 $param[1],
-                $this->db->quote($param[2])
+                $qvalue
             );
         }
 


commit 3a1f2c207c42c956a010f6fa9e575e11ae7f8475
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 16 10:12:22 2012 +0200

    Revert commit d08cb111 "Fixed working with disabled kolab_cache"
    
    This reverts commit d08cb11137116a7f3a98adb48750b6edccf68abe.

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 7978411..afe4dc1 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -82,8 +82,7 @@ class kolab_storage_cache
             return;
 
         // lock synchronization for this folder or wait if locked
-        if (!$this->_sync_lock())
-            return;
+        $this->_sync_lock();
 
         // synchronize IMAP mailbox cache
         $this->imap->folder_sync($this->folder->name);
@@ -187,7 +186,7 @@ class kolab_storage_cache
             kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
             return;
         }
-
+        
         // write to cache
         if ($this->ready) {
             // remove old entry
@@ -306,7 +305,7 @@ class kolab_storage_cache
             $filter = $this->_query2assoc($query);
 
             // use 'list' for folder's default objects
-            if ($filter['type'] == $this->type && !empty($this->index)) {
+            if ($filter['type'] == $this->type) {
                 $index = $this->index;
             }
             else {  // search by object type
@@ -489,7 +488,7 @@ class kolab_storage_cache
     private function _sync_lock()
     {
         if (!$this->ready)
-            return false;
+            return;
 
         $sql_arr = $this->db->fetch_assoc($this->db->query(
             "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
@@ -523,8 +522,6 @@ class kolab_storage_cache
                 'lock'
             );
         }
-
-        return true;
     }
 
     /**


commit 4b5c2ce98551c8dbff40e679793c6901466132dc
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Wed May 16 10:10:51 2012 +0200

    Add some fulltext searching capabilities to kolab_storage_cache and use them for search (autocompletion) in Kolab address books

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 37ba1f1..5b7fd8d 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -314,8 +314,6 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
     {
-        $this->_fetch_contacts();
-
         // search by ID
         if ($fields == $this->primary_key) {
             $ids    = !is_array($value) ? explode(',', $value) : $value;
@@ -350,6 +348,25 @@ class rcube_kolab_contacts extends rcube_addressbook
         // build key name regexp
         $regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
 
+        // pass query to storage if only indexed cols are involved
+        // NOTE: this is only some rough pre-filtering but probably includes false positives
+        $squery = array();
+        if (count(array_intersect(kolab_format_contact::$fulltext_cols, $fields)) == $scount) {
+            switch ($mode) {
+                case 1:  $prefix = ' '; $suffix = ' '; break;  // strict
+                case 2:  $prefix = ' '; $suffix = '';  break;  // prefix
+                default: $prefix = '';  $suffix = '';  break;  // substring
+            }
+
+            $search_string = is_array($value) ? join(' ', $value) : $value;
+            foreach (rcube_utils::normalize_string($search_string, true) as $word) {
+                $squery[] = array('words', 'LIKE', '%' . $prefix . $word . $suffix . '%');
+            }
+        }
+
+        // get all/matching records
+        $this->_fetch_contacts($squery);
+
         // save searching conditions
         $this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
 
@@ -374,17 +391,19 @@ class rcube_kolab_contacts extends rcube_addressbook
                 }
 
                 foreach ((array)$contact[$col] as $val) {
-                    $val = mb_strtolower($val);
-                    switch ($mode) {
-                    case 1:
-                        $got = ($val == $search);
-                        break;
-                    case 2:
-                        $got = ($search == substr($val, 0, strlen($search)));
-                        break;
-                    default:
-                        $got = (strpos($val, $search) !== false);
-                        break;
+                    foreach ((array)$val as $str) {
+                        $str = mb_strtolower($str);
+                        switch ($mode) {
+                        case 1:
+                            $got = ($str == $search);
+                            break;
+                        case 2:
+                            $got = ($search == substr($str, 0, strlen($search)));
+                            break;
+                        default:
+                            $got = (strpos($str, $search) !== false);
+                            break;
+                        }
                     }
 
                     if ($got) {
@@ -916,17 +935,13 @@ class rcube_kolab_contacts extends rcube_addressbook
     }
 
     /**
-     * Simply fetch all records and store them in private member vars
+     * Query storage layer and store records in private member var
      */
-    private function _fetch_contacts()
+    private function _fetch_contacts($query = array())
     {
         if (!isset($this->contacts)) {
             $this->contacts = array();
-            foreach ((array)$this->storagefolder->get_objects() as $record) {
-                // Because of a bug, sometimes group records are returned
-                if ($record['__type'] == 'Group')
-                    continue;
-
+            foreach ((array)$this->storagefolder->select($query) as $record) {
                 $contact = $this->_to_rcube_contact($record);
                 $id = $contact['ID'];
                 $this->contacts[$id] = $contact;
diff --git a/plugins/libkolab/SQL/mysql.sql b/plugins/libkolab/SQL/mysql.sql
index 5f49e92..55d0dbc 100644
--- a/plugins/libkolab/SQL/mysql.sql
+++ b/plugins/libkolab/SQL/mysql.sql
@@ -11,10 +11,12 @@ CREATE TABLE IF NOT EXISTS `kolab_cache` (
   `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
   `msguid` BIGINT UNSIGNED NOT NULL,
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
   `data` TEXT NOT NULL,
   `xml` TEXT NOT NULL,
   `dtstart` DATETIME,
   `dtend` DATETIME,
   `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
   PRIMARY KEY(`resource`,`type`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 646f6ca..1f20e3e 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -427,7 +427,7 @@ class kolab_format_contact extends kolab_format
     {
         $data = '';
         foreach (self::$fulltext_cols as $col) {
-            $val = is_array($this->data[$col]) ? join(" ", $this->data[$col]) : $this->data[$col];
+            $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
             if (strlen($val))
                 $data .= $val . ' ';
         }
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 72373e1..7978411 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -364,7 +364,7 @@ class kolab_storage_cache
     {
         $sql_where = '';
         foreach ($query as $param) {
-            $sql_where .= sprintf(' AND %s%s%s',
+            $sql_where .= sprintf(' AND %s %s %s',
                 $this->db->quote_identifier($param[0]),
                 $param[1],
                 $this->db->quote($param[2])
@@ -431,8 +431,8 @@ class kolab_storage_cache
 
         if ($object['_formatobj']) {
             $sql_data['xml'] = (string)$object['_formatobj']->write();
-            $sql_data['tags'] = join(' ', $object['_formatobj']->get_tags());
-            $sql_data['words'] = join(' ', $object['_formatobj']->get_words());
+            $sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' ';  // pad with spaces for strict/prefix search
+            $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
         }
 
         // extract object data
@@ -492,7 +492,7 @@ class kolab_storage_cache
             return false;
 
         $sql_arr = $this->db->fetch_assoc($this->db->query(
-            "SELECT msguid AS locked FROM kolab_cache ".
+            "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
             "WHERE resource=? AND type=?",
             $this->resource_uri,
             'lock'
@@ -501,24 +501,24 @@ class kolab_storage_cache
         // create lock record if not exists
         if (!$sql_arr) {
             $this->db->query(
-                "INSERT INTO kolab_cache (resource, type, msguid, uid, data, xml)".
-                " VALUES (?, ?, ?, '', '', '')",
+                "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml)".
+                " VALUES (?, ?, 1, ?, '', '', '')",
                 $this->resource_uri,
                 'lock',
-                time()
+                date('Y-m-d H:i:s')
             );
         }
         // wait if locked (expire locks after 10 minutes)
-        else if (intval($sql_arr['locked']) > 0 && (time() - $sql_arr['locked']) < 600) {
+        else if (intval($sql_arr['locked']) > 0 && (time() - $sql_arr['created']) < 600) {
             usleep(500000);
             return $this->_sync_lock();
         }
         // set lock
         else {
             $this->db->query(
-                "UPDATE kolab_cache SET msguid=? ".
+                "UPDATE kolab_cache SET msguid=1, created=? ".
                 "WHERE resource=? AND type=?",
-                time(),
+                date('Y-m-d H:i:s'),
                 $this->resource_uri,
                 'lock'
             );
@@ -533,7 +533,7 @@ class kolab_storage_cache
     private function _sync_unlock()
     {
         $this->db->query(
-            "UPDATE kolab_cache SET msguid=0 ".
+            "UPDATE kolab_cache SET msguid=0, created='' ".
             "WHERE resource=? AND type=?",
             $this->resource_uri,
             'lock'


commit dadf00f1a646bb3a5142d14b2365788477d3d4bf
Merge: e602722 d08cb11
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Tue May 15 19:08:16 2012 +0200

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit e602722064cb28a0f1e7ea42aa009581d924adad
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Tue May 15 19:08:01 2012 +0200

    Save object words for fulltext searching in caching table

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 9f9431c..a9e688b 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -200,8 +200,8 @@ class kolab_storage_cache
 
                 $result = $this->db->query(
                     "INSERT INTO kolab_cache ".
-                    " (resource, type, msguid, uid, data, xml, dtstart, dtend)".
-                    " VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+                    " (resource, type, msguid, uid, data, xml, dtstart, dtend, tags, words)".
+                    " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                     $this->resource_uri,
                     $objtype,
                     $msguid,
@@ -209,7 +209,9 @@ class kolab_storage_cache
                     $sql_data['data'],
                     $sql_data['xml'],
                     $sql_data['dtstart'],
-                    $sql_data['dtend']
+                    $sql_data['dtend'],
+                    $sql_data['tags'],
+                    $sql_data['words']
                 );
 
                 if (!$this->db->affected_rows($result)) {
@@ -426,8 +428,11 @@ class kolab_storage_cache
             }
         }
 
-        if ($object['_formatobj'])
+        if ($object['_formatobj']) {
             $sql_data['xml'] = (string)$object['_formatobj']->write();
+            $sql_data['tags'] = join(' ', $object['_formatobj']->get_tags());
+            $sql_data['words'] = join(' ', $object['_formatobj']->get_words());
+        }
 
         // extract object data
         $data = array();


commit ab69f4909165d20fcf09cb1d361370a26c6ad987
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Tue May 15 19:05:46 2012 +0200

    Prepare for fulltext indexing in kolab_storage_cache; fix saving contacts with no address data (#769)

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 134df85..4c8e363 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -232,4 +232,24 @@ abstract class kolab_format
      * @param array Hash array with object properties (produced by Horde Kolab_Format classes)
      */
     abstract public function fromkolab2($object);
+
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        return array();
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        return array();
+    }
 }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 59a6e0a..646f6ca 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -26,6 +26,8 @@ class kolab_format_contact extends kolab_format
 {
     public $CTYPE = 'application/vcard+xml';
 
+    public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email');
+
     public $phonetypes = array(
         'home'    => Telephone::Home,
         'work'    => Telephone::Work,
@@ -207,7 +209,7 @@ class kolab_format_contact extends kolab_format
 
         // addresses
         $adrs = new vectoraddress;
-        foreach ($object['address'] as $address) {
+        foreach ((array)$object['address'] as $address) {
             $adr = new Address;
             $type = $this->addresstypes[$address['type']];
             if (isset($type))
@@ -417,6 +419,23 @@ class kolab_format_contact extends kolab_format
     }
 
     /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $data = '';
+        foreach (self::$fulltext_cols as $col) {
+            $val = is_array($this->data[$col]) ? join(" ", $this->data[$col]) : $this->data[$col];
+            if (strlen($val))
+                $data .= $val . ' ';
+        }
+
+        return array_unique(rcube_utils::normalize_string($data, true));
+    }
+
+    /**
      * Load data from old Kolab2 format
      *
      * @param array Hash array with object properties


commit d08cb11137116a7f3a98adb48750b6edccf68abe
Author: Aleksander Machniak <alec at alec.pl>
Date:   Tue May 15 13:57:27 2012 +0200

    Fixed working with disabled kolab_cache

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 9f9431c..19a4815 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -82,7 +82,8 @@ class kolab_storage_cache
             return;
 
         // lock synchronization for this folder or wait if locked
-        $this->_sync_lock();
+        if (!$this->_sync_lock())
+            return;
 
         // synchronize IMAP mailbox cache
         $this->imap->folder_sync($this->folder->name);
@@ -186,7 +187,7 @@ class kolab_storage_cache
             kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
             return;
         }
-        
+
         // write to cache
         if ($this->ready) {
             // remove old entry
@@ -303,7 +304,7 @@ class kolab_storage_cache
             $filter = $this->_query2assoc($query);
 
             // use 'list' for folder's default objects
-            if ($filter['type'] == $this->type) {
+            if ($filter['type'] == $this->type && !empty($this->index)) {
                 $index = $this->index;
             }
             else {  // search by object type
@@ -483,7 +484,7 @@ class kolab_storage_cache
     private function _sync_lock()
     {
         if (!$this->ready)
-            return;
+            return false;
 
         $sql_arr = $this->db->fetch_assoc($this->db->query(
             "SELECT msguid AS locked FROM kolab_cache ".
@@ -517,6 +518,8 @@ class kolab_storage_cache
                 'lock'
             );
         }
+
+        return true;
     }
 
     /**


commit d9924e675dc8ffaaad7bfaddacd4a6de515b293e
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Tue May 15 09:56:52 2012 +0200

    Fix broken folder type detection after refactorings

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index ffbbb5d..7ae1a1e 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -73,7 +73,7 @@ class kolab_storage_folder
     {
         if (!$type) {
             $metadata = $this->imap->get_metadata($name, array(kolab_storage::CTYPE_KEY));
-            $type     = $metadata[$this->name][kolab_storage::CTYPE_KEY];
+            $type     = $metadata[$name][kolab_storage::CTYPE_KEY];
         }
 
         $this->name            = $name;


commit 55513b2a19173b7f40761f99598adcf832b1b5db
Author: Aleksander Machniak <alec at alec.pl>
Date:   Mon May 14 15:45:21 2012 +0200

    Ported to libkolab.
    @TODO: remove caching, metadata is already cached by Roundcube storage

diff --git a/plugins/kolab_zpush/kolab_zpush.php b/plugins/kolab_zpush/kolab_zpush.php
index 4ef242c..b65f39c 100644
--- a/plugins/kolab_zpush/kolab_zpush.php
+++ b/plugins/kolab_zpush/kolab_zpush.php
@@ -26,7 +26,7 @@ class kolab_zpush extends rcube_plugin
 {
     public $task = 'settings';
     public $urlbase;
-    
+
     private $rc;
     private $ui;
     private $cache;
@@ -34,7 +34,7 @@ class kolab_zpush extends rcube_plugin
     private $folders;
     private $folders_meta;
     private $root_meta;
-    
+
     const ROOT_MAILBOX = 'INBOX';
     const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
     const ACTIVESYNC_KEY = '/private/vendor/kolab/activesync';
@@ -45,17 +45,18 @@ class kolab_zpush extends rcube_plugin
     public function init()
     {
         $this->rc = rcmail::get_instance();
-        
+
         $this->require_plugin('jqueryui');
         $this->add_texts('localization/', true);
-        
+
         $this->include_script('kolab_zpush.js');
-        
+
         $this->register_action('plugin.zpushconfig', array($this, 'config_view'));
         $this->register_action('plugin.zpushjson', array($this, 'json_command'));
-        
-        if ($this->rc->action == 'plugin.zpushconfig')
-          $this->require_plugin('kolab_core');
+
+        if ($this->rc->action == 'plugin.zpushconfig') {
+            $this->require_plugin('libkolab');
+        }
     }
 
 
@@ -66,6 +67,8 @@ class kolab_zpush extends rcube_plugin
     {
         $storage = $this->rc->get_storage();
 
+        // @TODO: Metadata is already cached by rcube storage, get rid of cache here
+
         $this->cache = $this->rc->get_cache('zpush', 'db', 900);
         $this->cache->expunge();
 
@@ -120,7 +123,7 @@ class kolab_zpush extends rcube_plugin
             $laxpic = intval(get_input_value('laxpic', RCUBE_INPUT_POST));
             $subsciptions = get_input_value('subscribed', RCUBE_INPUT_POST);
             $err = false;
-            
+
             if ($device = $devices[$imei]) {
                 // update device config if changed
                 if ($devicealias != $this->root_meta['DEVICE'][$imei]['ALIAS']  ||
@@ -146,12 +149,12 @@ class kolab_zpush extends rcube_plugin
                     // skip root folder (already handled above)
                     if ($folder == self::ROOT_MAILBOX)
                         continue;
-                    
+
                     if ($subsciptions[$folder] != $meta[$imei]['S']) {
                         $meta[$imei]['S'] = intval($subsciptions[$folder]);
                         $this->folders_meta[$folder] = $meta;
                         unset($meta['TYPE']);
-                        
+
                         // read metadata first
                         $folderdata = $storage->get_metadata($folder, array(self::ACTIVESYNC_KEY));
                         if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
@@ -161,24 +164,24 @@ class kolab_zpush extends rcube_plugin
                         $err |= !$storage->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)));
                     }
                 }
-                
+
                 // update cache
                 $this->cache->remove('folders');
                 $this->cache->write('folders', $this->folders_meta);
-                
+
                 $this->rc->output->command('plugin.zpush_save_complete', array('success' => !$err, 'id' => $imei, 'devicealias' => Q($devicealias)));
             }
-            
+
             if ($err)
                 $this->rc->output->show_message($this->gettext('savingerror'), 'error');
             else
                 $this->rc->output->show_message($this->gettext('successfullysaved'), 'confirmation');
-            
+
             break;
 
         case 'delete':
             $devices = $this->list_devices();
-            
+
             if ($device = $devices[$imei]) {
                 unset($this->root_meta['DEVICE'][$imei], $this->root_meta['FOLDER'][$imei]);
 
@@ -236,20 +239,20 @@ class kolab_zpush extends rcube_plugin
     public function config_view()
     {
         require_once $this->home . '/kolab_zpush_ui.php';
-        
+
         $storage = $this->rc->get_storage();
-        
+
         // checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
         if (!($storage->get_capability('METADATA') || $storage->get_capability('ANNOTATEMORE') || $storage->get_capability('ANNOTATEMORE2'))) {
             $this->rc->output->show_message($this->gettext('notsupported'), 'error');
         }
-        
+
         $this->ui = new kolab_zpush_ui($this);
-        
+
         $this->register_handler('plugin.devicelist', array($this->ui, 'device_list'));
         $this->register_handler('plugin.deviceconfigform', array($this->ui, 'device_config_form'));
         $this->register_handler('plugin.foldersubscriptions', array($this->ui, 'folder_subscriptions'));
-        
+
         $this->rc->output->set_env('devicecount', count($this->list_devices()));
         $this->rc->output->send('kolab_zpush.config');
     }
@@ -266,7 +269,7 @@ class kolab_zpush extends rcube_plugin
             $this->init_imap();
             $this->devices = (array)$this->root_meta['DEVICE'];
         }
-        
+
         return $this->devices;
     }
 
@@ -299,7 +302,7 @@ class kolab_zpush extends rcube_plugin
                     }
                     $this->folders_meta[$folder]['TYPE'] = !empty($foldertype[0]) ? $foldertype[0] : 'mail';
                 }
-                
+
                 // cache it!
                 $this->cache->write('folders', $this->folders_meta);
             }
@@ -317,7 +320,7 @@ class kolab_zpush extends rcube_plugin
     {
         if (!isset($this->folders_meta))
             $this->list_folders();
-        
+
         return $this->folders_meta;
     }
 
diff --git a/plugins/kolab_zpush/kolab_zpush_ui.php b/plugins/kolab_zpush/kolab_zpush_ui.php
index e651e98..4c99cf7 100644
--- a/plugins/kolab_zpush/kolab_zpush_ui.php
+++ b/plugins/kolab_zpush/kolab_zpush_ui.php
@@ -67,18 +67,18 @@ class kolab_zpush_ui
         $input = new html_inputfield(array('name' => 'devicealias', 'id' => $field_id, 'size' => 40));
         $table->add('title', html::label($field_id, $this->config->gettext('devicealias')));
         $table->add(null, $input->show());
-        
+
         $field_id = 'config-device-mode';
         $select = new html_select(array('name' => 'syncmode', 'id' => $field_id));
         $select->add(array($this->config->gettext('modeauto'), $this->config->gettext('modeflat'), $this->config->gettext('modefolder')), array('-1', '0', '1'));
         $table->add('title', html::label($field_id, $this->config->gettext('syncmode')));
         $table->add(null, $select->show('-1'));
-        
+
         $field_id = 'config-device-laxpic';
         $checkbox = new html_checkbox(array('name' => 'laxpic', 'value' => '1', 'id' => $field_id));
         $table->add('title', $this->config->gettext('imageformat'));
         $table->add(null, html::label($field_id, $checkbox->show() . ' ' . $this->config->gettext('laxpiclabel')));
-        
+
         if ($attrib['form'])
             $this->rc->output->add_gui_object('editform', $attrib['form']);
 
@@ -90,7 +90,7 @@ class kolab_zpush_ui
     {
         if (!$attrib['id'])
             $attrib['id'] = 'foldersubscriptions';
-        
+
         // group folders by type (show only known types)
         $folder_groups = array('mail' => array(), 'contact' => array(), 'event' => array(), 'task' => array());
         $folder_meta = $this->config->folders_meta();
@@ -99,7 +99,7 @@ class kolab_zpush_ui
             if (is_array($folder_groups[$type]))
                 $folder_groups[$type][] = $folder;
         }
-        
+
         // build block for every folder type
         foreach ($folder_groups as $type => $group) {
             if (empty($group))
@@ -111,14 +111,14 @@ class kolab_zpush_ui
         }
         
         $this->rc->output->add_gui_object('subscriptionslist', $attrib['id']);
-        
+
         return html::div($attrib, $html);
     }
 
     public function folder_subscriptions_block($a_folders, $attrib)
     {
         $alarms = ($attrib['type'] == 'event' || $attrib['type'] == 'task');
-        
+
         $table = new html_table(array('cellspacing' => 0));
         $table->add_header('subscription', $attrib['syncicon'] ? html::img(array('src' => $this->skin_path . $attrib['syncicon'], 'title' => $this->config->gettext('synchronize'))) : '');
         $table->add_header('alarm', $alarms && $attrib['alarmicon'] ? html::img(array('src' => $this->skin_path . $attrib['alarmicon'], 'title' => $this->config->gettext('withalarms'))) : '');
@@ -129,7 +129,7 @@ class kolab_zpush_ui
 
         $names = array();
         foreach ($a_folders as $folder) {
-            $foldername = $origname = preg_replace('/^INBOX »\s+/', '', rcube_kolab::object_name($folder));
+            $foldername = $origname = preg_replace('/^INBOX »\s+/', '', kolab_storage::object_name($folder));
 
             // find folder prefix to truncate (the same code as in kolab_addressbook plugin)
             for ($i = count($names)-1; $i >= 0; $i--) {
@@ -161,7 +161,7 @@ class kolab_zpush_ui
                 $table->add('alarm', $checkbox_alarm->show('', array('value' => $folder, 'id' => $folder_id.'_alarm')));
             else
                 $table->add('alarm', '');
-            
+
             $table->add(join(' ', $classes), html::label($folder_id, $padding . Q($foldername)));
         }
 
diff --git a/plugins/kolab_zpush/package.xml b/plugins/kolab_zpush/package.xml
index 6147879..ef6dfd7 100644
--- a/plugins/kolab_zpush/package.xml
+++ b/plugins/kolab_zpush/package.xml
@@ -13,10 +13,10 @@
 		<email>bruederli at kolabsys.com</email>
 		<active>yes</active>
 	</lead>
-	<date>2011-11-14</date>
-	<time>12:12:00</time>
+	<date>2012-05-14</date>
 	<version>
-		<release>0.3</release>
+		<release>1.0</release>
+		<api>1.0</api>
 	</version>
 	<stability>
 		<release>stable</release>
@@ -29,6 +29,7 @@
 			<file name="kolab_zpush_ui.php" role="php"></file>
 			<file name="kolab_zpush.js" role="data"></file>
 			<file name="localization/de_CH.inc" role="data"></file>
+			<file name="localization/de_DE.inc" role="data"></file>
 			<file name="localization/en_US.inc" role="data"></file>
 			<file name="localization/pl_PL.inc" role="data"></file>
 			<file name="skins/default/templates/config.html" role="data"></file>


commit a2e191d631a7d018db6e5d9cea777e8eb18afc69
Author: Aleksander Machniak <alec at alec.pl>
Date:   Mon May 14 13:41:33 2012 +0200

    Improve performance of kolab_storage_folder object creation when
    folder type is already known (e.g. in kolab_storage::get_folders())

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 43f9c72..62b7796 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -72,11 +72,11 @@ class kolab_storage
      */
     public static function get_folders($type)
     {
-        $folders = array();
+        $folders = $folderdata = array();
 
         if (self::setup()) {
-            foreach ((array)self::list_folders('', '*', $type) as $foldername) {
-                $folders[$foldername] = new kolab_storage_folder($foldername, self::$imap);
+            foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+                $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
             }
         }
 
@@ -92,7 +92,7 @@ class kolab_storage
      */
     public static function get_folder($folder)
     {
-        return self::setup() ? new kolab_storage_folder($folder, self::$imap) : null;
+        return self::setup() ? new kolab_storage_folder($folder) : null;
     }
 
 
@@ -110,7 +110,7 @@ class kolab_storage
         $folder = null;
         foreach ((array)self::list_folders('', '*', $type) as $foldername) {
             if (!$folder)
-                $folder = new kolab_storage_folder($foldername, self::$imap);
+                $folder = new kolab_storage_folder($foldername);
             else
                 $folder->set_folder($foldername);
 
@@ -384,10 +384,11 @@ class kolab_storage
      * @param string  Optional name pattern
      * @param string  Data type to list folders for (contact,distribution-list,event,task,note,mail)
      * @param string  Enable to return subscribed folders only
+     * @param array   Will be filled with folder-types data
      *
      * @return array List of folders
      */
-    public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false)
+    public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false, &$folderdata = array())
     {
         if (!self::setup()) {
             return null;
@@ -412,14 +413,14 @@ class kolab_storage
             return array();
         }
 
-        $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
+        $folderdata = array_map('implode', $folderdata);
+        $regexp     = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
 
         // In some conditions we can skip LIST command (?)
         if ($subscribed == false && $filter != 'mail' && $prefix == '*') {
-            foreach ($folderdata as $idx => $folder) {
-                $type = $folder[self::CTYPE_KEY];
+            foreach ($folderdata as $folder => $type) {
                 if (!preg_match($regexp, $type)) {
-                    unset($folderdata[$idx]);
+                    unset($folderdata[$folder]);
                 }
             }
             return array_keys($folderdata);
@@ -440,7 +441,7 @@ class kolab_storage
 
         // Filter folders list
         foreach ($folders as $idx => $folder) {
-            $type = !empty($folderdata[$folder]) ? $folderdata[$folder][self::CTYPE_KEY] : null;
+            $type = $folderdata[$folder];
 
             if ($filter == 'mail' && empty($type)) {
                 continue;
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index d691906..ffbbb5d 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -54,30 +54,34 @@ class kolab_storage_folder
     /**
      * Default constructor
      */
-    function __construct($name, $imap = null)
+    function __construct($name, $type = null)
     {
-        $this->imap = is_object($imap) ? $imap : rcube::get_instance()->get_storage();
+        $this->imap = rcube::get_instance()->get_storage();
         $this->imap->set_options(array('skip_deleted' => true));
         $this->cache = new kolab_storage_cache($this);
-        $this->set_folder($name);
+        $this->set_folder($name, $type);
     }
 
 
     /**
-     * Set the IMAP folder name this instance connects to
+     * Set the IMAP folder this instance connects to
      *
      * @param string The folder name/path
+     * @param string Optional folder type if known
      */
-    public function set_folder($name)
+    public function set_folder($name, $type = null)
     {
-        $this->name = $name;
-        $this->imap->set_folder($this->name);
+        if (!$type) {
+            $metadata = $this->imap->get_metadata($name, array(kolab_storage::CTYPE_KEY));
+            $type     = $metadata[$this->name][kolab_storage::CTYPE_KEY];
+        }
 
-        $metadata = $this->imap->get_metadata($this->name, array(kolab_storage::CTYPE_KEY));
-        $this->type_annotation = $metadata[$this->name][kolab_storage::CTYPE_KEY];
-        $this->type = reset(explode('.', $this->type_annotation));
-        $this->resource_uri = null;
+        $this->name            = $name;
+        $this->type_annotation = $type;
+        $this->type            = reset(explode('.', $type));
+        $this->resource_uri    = null;
 
+        $this->imap->set_folder($this->name);
         $this->cache->set_folder($this);
     }
 


commit 4ed6758112fdab4a6696b33b25fe87ebb16ce421
Author: Aleksander Machniak <alec at alec.pl>
Date:   Mon May 14 13:04:24 2012 +0200

    Integrate folders listing functionality from kolab_folders plugin
    into kolab_storage class - added public list_folders() method.
    Now we can use kolab_storage out of Roundcube application.

diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index 29bcd2f..3e01061 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -30,8 +30,6 @@ class kolab_folders extends rcube_plugin
     public $mail_types = array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail');
     private $rc;
 
-    const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
-
 
     /**
      * Plugin initialization.
@@ -40,6 +38,9 @@ class kolab_folders extends rcube_plugin
     {
         $this->rc = rcmail::get_instance();
 
+        // load required plugin
+        $this->require_plugin('libkolab');
+
         // Folder listing hooks
         $this->add_hook('storage_folders', array($this, 'mailboxes_list'));
 
@@ -57,68 +58,32 @@ class kolab_folders extends rcube_plugin
      */
     function mailboxes_list($args)
     {
-        if (!$this->metadata_support()) {
+        // infinite loop prevention
+        if ($this->is_processing) {
             return $args;
         }
 
-        $filter = $args['filter'];
-
-        // all-folders request, use core method
-        if (!$filter) {
+        if (!$this->metadata_support()) {
             return $args;
         }
 
-        // get folders types
-        $folderdata = $this->get_folder_type_list($args['root'].$args['name'], true);
+        $this->is_processing = true;
 
-        if (!is_array($folderdata)) {
-            return $args;
-        }
+        // get folders
+        $folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB');
 
-        $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
+        $this->is_processing = false;
 
-        // In some conditions we can skip LIST command (?)
-        if ($args['mode'] == 'LIST' && $filter != 'mail'
-            && $args['root'] == '' && $args['name'] == '*'
-        ) {
-            foreach ($folderdata as $folder => $type) {
-                if (!preg_match($regexp, $type)) {
-                    unset($folderdata[$folder]);
-                }
-            }
-            $args['folders'] = array_keys($folderdata);
+        if (!is_array($folders)) {
             return $args;
         }
 
-        $storage = $this->rc->get_storage();
-
-        // Get folders list
-        if ($args['mode'] == 'LIST') {
-            if (!$storage->check_connection()) {
-                return $args;        
-            }
-            $args['folders'] = $storage->conn->listMailboxes($args['root'], $args['name']);
-        }
-        else {
-            $args['folders'] = $this->list_subscribed($args['root'], $args['name']);
+        // Create default folders
+        if ($args['root'] == '' && $args['name'] = '*') {
+            $this->create_default_folders($folders, $args['filter']);
         }
 
-        // In case of an error, return empty list
-        if (!is_array($args['folders'])) {
-            $args['folders'] = array();
-            return $args;
-        }
-
-        // Filter folders list
-        foreach ($args['folders'] as $idx => $folder) {
-            $type = $folderdata[$folder];
-            if ($filter == 'mail' && empty($type)) {
-                continue;
-            }
-            if (empty($type) || !preg_match($regexp, $type)) {
-                unset($args['folders'][$idx]);
-            }
-        }
+        $args['folders'] = $folders;
 
         return $args;
     }
@@ -132,10 +97,11 @@ class kolab_folders extends rcube_plugin
             return $args;
         }
 
-        $table = $args['table'];
+        $table   = $args['table'];
+        $storage = $this->rc->get_storage();
 
         // get folders types
-        $folderdata = $this->get_folder_type_list('*');
+        $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
 
         if (!is_array($folderdata)) {
             return $args;
@@ -146,7 +112,7 @@ class kolab_folders extends rcube_plugin
         for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
             $attrib = $table->get_row_attribs($i);
             $folder = $attrib['foldername']; // UTF7-IMAP
-            $type   = $folderdata[$folder];
+            $type   = !empty($folderdata[$folder]) ? $folderdata[$folder][kolab_storage::CTYPE_KEY] : null;
 
             if (!$type)
                 $type = 'mail';
@@ -266,8 +232,6 @@ class kolab_folders extends rcube_plugin
     {
         // Folder actions from folders list
         if (empty($args['record'])) {
-            // Just clear Horde folders cache and return
-            $this->clear_folders_cache();
             return $args;
         }
 
@@ -340,11 +304,6 @@ class kolab_folders extends rcube_plugin
             }
         }
 
-        // Clear Horde folders cache
-        if ($result) {
-            $this->clear_folders_cache();
-        }
-
         $args['record']['class'] = self::folder_class_name($ctype);
         $args['record']['subscribe'] = $subscribe;
         $args['result'] = $result;
@@ -355,7 +314,7 @@ class kolab_folders extends rcube_plugin
     /**
      * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
      *
-     * @return boolean 
+     * @return boolean
      */
     function metadata_support()
     {
@@ -376,9 +335,9 @@ class kolab_folders extends rcube_plugin
     function get_folder_type($folder)
     {
         $storage    = $this->rc->get_storage();
-        $folderdata = $storage->get_metadata($folder, array(kolab_folders::CTYPE_KEY));
+        $folderdata = $storage->get_metadata($folder, kolab_storage::CTYPE_KEY);
 
-        return explode('.', $folderdata[$folder][kolab_folders::CTYPE_KEY]);
+        return explode('.', $folderdata[$folder][kolab_storage::CTYPE_KEY]);
     }
 
     /**
@@ -393,112 +352,7 @@ class kolab_folders extends rcube_plugin
     {
         $storage = $this->rc->get_storage();
 
-        return $storage->set_metadata($folder, array(kolab_folders::CTYPE_KEY => $type));
-    }
-
-    /**
-     * Returns list of subscribed folders (directly from IMAP server)
-     *
-     * @param string $root Optional root folder
-     * @param string $name Optional name pattern
-     *
-     * @return array List of mailboxes/folders
-     */
-    private function list_subscribed($root='', $name='*')
-    {
-        $storage = $this->rc->get_storage();
-
-        if (!$storage->check_connection()) {
-            return null;
-        }
-
-        // Code copied from rcube_imap::_list_mailboxes()
-        // Server supports LIST-EXTENDED, we can use selection options
-        // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
-        if (!$this->rc->config->get('imap_force_lsub') && $storage->get_capability('LIST-EXTENDED')) {
-            // This will also set mailbox options, LSUB doesn't do that
-            $a_folders = $storage->conn->listMailboxes($root, $name,
-                NULL, array('SUBSCRIBED'));
-
-            // remove non-existent folders
-            if (is_array($a_folders) && $name = '*' && !empty($storage->conn->data['LIST'])) {
-                foreach ($a_folders as $idx => $folder) {
-                    if (($opts = $storage->conn->data['LIST'][$folder])
-                        && in_array('\\NonExistent', $opts)
-                    ) {
-                        $storage->conn->unsubscribe($folder);
-                        unset($a_folders[$idx]);
-                    }
-                }
-            }
-        }
-        // retrieve list of folders from IMAP server using LSUB
-        else {
-            $a_folders = $storage->conn->listSubscribed($root, $name);
-
-            // unsubscribe non-existent folders, remove from the list
-            if (is_array($a_folders) && $name == '*' && !empty($storage->conn->data['LIST'])) {
-                foreach ($a_folders as $idx => $folder) {
-                    if (!isset($storage->conn->data['LIST'][$folder])
-                        || in_array('\\Noselect', $storage->conn->data['LIST'][$folder])
-                    ) {
-                        // Some servers returns \Noselect for existing folders
-                        if (!$storage->folder_exists($folder)) {
-                            $storage->conn->unsubscribe($folder);
-                            unset($a_folders[$idx]);
-                        }
-                    }
-                }
-            }
-        }
-
-        return $a_folders;
-    }
-
-    /**
-     * Returns list of folder(s) type(s)
-     *
-     * @param string $mbox     Folder name or pattern
-     * @param bool   $defaults Enables creation of configured default folders
-     *
-     * @return array List of folders data, indexed by folder name
-     */
-    function get_folder_type_list($mbox, $create_defaults = false)
-    {
-        $storage = $this->rc->get_storage();
-
-        // Use mailboxes. prefix so the cache will be cleared by core
-        // together with other mailboxes-related cache data
-        $cache_key = 'mailboxes.folder-type.'.$mbox;
-
-        // get cached metadata
-        $metadata = $storage->get_cache($cache_key);
-
-        if (!is_array($metadata)) {
-            $metadata = $storage->get_metadata($mbox, kolab_folders::CTYPE_KEY);
-            $need_update = true;
-        }
-
-        if (!is_array($metadata)) {
-            return false;
-        }
-
-        // make the result more flat
-        if ($need_update) {
-            $metadata = array_map('implode', $metadata);
-        }
-
-        // create default folders if needed
-        if ($create_defaults) {
-            $this->create_default_folders($metadata, $cache_key);
-        }
-
-        // write mailboxlist to cache
-        if ($need_update) {
-            $storage->update_cache($cache_key, $metadata);
-        }
-
-        return $metadata;
+        return $storage->set_metadata($folder, array(kolab_storage::CTYPE_KEY => $type));
     }
 
     /**
@@ -511,7 +365,7 @@ class kolab_folders extends rcube_plugin
     function get_default_folder($type)
     {
         $storage    = $this->rc->get_storage();
-        $folderdata = $this->get_folder_type_list('*');
+        $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
 
         if (!is_array($folderdata)) {
             return null;
@@ -521,7 +375,8 @@ class kolab_folders extends rcube_plugin
         $namespace = $storage->get_namespace();
 
         // get all folders of specified type
-        $folderdata = array_intersect($folderdata, array($type)); 
+        $folderdata = array_map('implode', $folderdata);
+        $folderdata = array_intersect($folderdata, array($type));
         unset($folders[0]);
 
         foreach ($folderdata as $folder => $data) {
@@ -563,23 +418,23 @@ class kolab_folders extends rcube_plugin
     }
 
     /**
-     * Clear Horde's folder cache. See Kolab_List::singleton().
-     */
-    private function clear_folders_cache()
-    {
-        unset($_SESSION['horde_session_objects']['kolab_folderlist']);
-    }
-
-    /**
      * Creates default folders if they doesn't exist
      */
-    private function create_default_folders(&$folderdata, $cache_key = null)
+    private function create_default_folders(&$folders, $filter)
     {
         $storage     = $this->rc->get_storage();
         $namespace   = $storage->get_namespace();
+        $folderdata  = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
         $defaults    = array();
         $need_update = false;
 
+        if (!is_array($folderdata)) {
+            return;
+        }
+
+        // "Flattenize" metadata array to become a name->type hash
+        $folderdata = array_map('implode', $folderdata);
+
         // Find personal namespace prefix
         if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
             $prefix = $namespace['personal'][0][0];
@@ -621,7 +476,7 @@ class kolab_folders extends rcube_plugin
             }
 
             // get all folders of specified type
-            $folders = array_intersect($folderdata, array($type)); 
+            $folders = array_intersect($folderdata, array($type));
             unset($folders[0]);
 
             // find folders in personal namespace
@@ -653,16 +508,10 @@ class kolab_folders extends rcube_plugin
             $result = $this->set_folder_type($foldername, $type);
 
             // add new folder to the result
-            if ($result) {
-                $folderdata[$foldername] = $type;
-                $need_update = true;
+            if ($result && (!$filter || $filter == $type1)) {
+                $folders[] = $foldername;
             }
         }
-
-        // update cache
-        if ($need_update && $cache_key) {
-            $storage->update_cache($cache_key, $folderdata);
-        }
     }
 
 }
diff --git a/plugins/kolab_folders/package.xml b/plugins/kolab_folders/package.xml
index 8042ba2..875d614 100644
--- a/plugins/kolab_folders/package.xml
+++ b/plugins/kolab_folders/package.xml
@@ -21,10 +21,10 @@
 		<email>machniak at kolabsys.com</email>
 		<active>yes</active>
 	</lead>
-	<date>2011-11-01</date>
+	<date>2012-05-14</date>
 	<version>
-		<release>1.0</release>
-		<api>1.0</api>
+		<release>2.0</release>
+		<api>2.0</api>
 	</version>
 	<stability>
 		<release>stable</release>
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 175bb02..43f9c72 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -75,7 +75,7 @@ class kolab_storage
         $folders = array();
 
         if (self::setup()) {
-            foreach ((array)self::$imap->list_folders('', '*', $type) as $foldername) {
+            foreach ((array)self::list_folders('', '*', $type) as $foldername) {
                 $folders[$foldername] = new kolab_storage_folder($foldername, self::$imap);
             }
         }
@@ -108,7 +108,7 @@ class kolab_storage
     {
         self::setup();
         $folder = null;
-        foreach ((array)self::$imap->list_folders('', '*', $type) as $foldername) {
+        foreach ((array)self::list_folders('', '*', $type) as $foldername) {
             if (!$folder)
                 $folder = new kolab_storage_folder($foldername, self::$imap);
             else
@@ -375,4 +375,82 @@ class kolab_storage
 
         return $select;
     }
+
+
+    /**
+     * Returns a list of folder names
+     *
+     * @param string  Optional root folder
+     * @param string  Optional name pattern
+     * @param string  Data type to list folders for (contact,distribution-list,event,task,note,mail)
+     * @param string  Enable to return subscribed folders only
+     *
+     * @return array List of folders
+     */
+    public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false)
+    {
+        if (!self::setup()) {
+            return null;
+        }
+
+        if (!$filter) {
+            // Get ALL folders list, standard way
+            if ($subscribed) {
+                return self::$imap->list_folders_subscribed($root, $mbox);
+            }
+            else {
+                return self::$imap->list_folders($root, $mbox);
+            }
+        }
+
+        $prefix = $root . $mbox;
+
+        // get folders types
+        $folderdata = self::$imap->get_metadata($prefix, self::CTYPE_KEY);
+
+        if (!is_array($folderdata)) {
+            return array();
+        }
+
+        $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
+
+        // In some conditions we can skip LIST command (?)
+        if ($subscribed == false && $filter != 'mail' && $prefix == '*') {
+            foreach ($folderdata as $idx => $folder) {
+                $type = $folder[self::CTYPE_KEY];
+                if (!preg_match($regexp, $type)) {
+                    unset($folderdata[$idx]);
+                }
+            }
+            return array_keys($folderdata);
+        }
+
+        // Get folders list
+        if ($subscribed) {
+            $folders = self::$imap->list_folders_subscribed_direct($root, $mbox);
+        }
+        else {
+            $folders = self::$imap->list_folders_direct($root, $mbox);
+        }
+
+        // In case of an error, return empty list (?)
+        if (!is_array($folders)) {
+            return array();
+        }
+
+        // Filter folders list
+        foreach ($folders as $idx => $folder) {
+            $type = !empty($folderdata[$folder]) ? $folderdata[$folder][self::CTYPE_KEY] : null;
+
+            if ($filter == 'mail' && empty($type)) {
+                continue;
+            }
+            if (empty($type) || !preg_match($regexp, $type)) {
+                unset($folders[$idx]);
+            }
+        }
+
+        return $folders;
+    }
+
 }


commit dd516006db8b662dac24bded8bd5c129358d6f6c
Author: Aleksander Machniak <machniak at hosted05.klab.cc>
Date:   Mon May 14 10:04:59 2012 +0200

    Fix argument count for kolab_storage_folder constructor

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 983a268..175bb02 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -92,7 +92,7 @@ class kolab_storage
      */
     public static function get_folder($folder)
     {
-        return self::setup() ? new kolab_storage_folder($folder, null, self::$imap) : null;
+        return self::setup() ? new kolab_storage_folder($folder, self::$imap) : null;
     }
 
 


commit 7de0aa468c32e570087f6f02bf6ad41bf1b5928b
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu May 10 21:25:57 2012 +0200

    Increment sequence property when updating an event

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 0552025..42c323c 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -144,8 +144,8 @@ class kolab_format_event extends kolab_format
         if (!empty($object['uid']))
             $this->obj->setUid($object['uid']);
 
-        // TODO: increase sequence
-        // $this->obj->setSequence($this->obj->sequence()+1);
+        // increment sequence
+        $this->obj->setSequence($this->obj->sequence()+1);
 
         // do the hard work of setting object values
         $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));


commit 6973bbcaee2d9591aa3a7864aae61948ee8876f2
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu May 10 21:24:48 2012 +0200

    Implement undelete with new storage backend; remove cruft

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 53a8d83..6104972 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -352,7 +352,17 @@ class kolab_calendar
    */
   public function restore_event($event)
   {
-    // TODO: re-implement this with new kolab_storege backend
+    if ($this->storage->undelete($event['id'])) {
+        return true;
+    }
+    else {
+        raise_error(array(
+          'code' => 600, 'type' => 'php',
+          'file' => __FILE__, 'line' => __LINE__,
+          'message' => "Error undeleting a contact object $uid from the Kolab server"),
+        true, false);
+    }
+
     return false;
   }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index aea6783..6ce7a84 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -401,7 +401,6 @@ class kolab_driver extends calendar_driver
         }
       }
 
-      $GLOBALS['conf']['kolab']['no_triggering'] = true;
       $success = $storage->insert_event($event);
       
       if ($success)
@@ -472,7 +471,6 @@ class kolab_driver extends calendar_driver
       $master = $event;
 
       $this->rc->session->remove('calendar_restore_event_data');
-      $GLOBALS['conf']['kolab']['no_triggering'] = true;
 
       // read master if deleting a recurring event
       if ($event['recurrence'] || $event['recurrence_id']) {
@@ -614,8 +612,6 @@ class kolab_driver extends calendar_driver
     if ($old['recurrence']['EXDATE'])
       $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
 
-    $GLOBALS['conf']['kolab']['no_triggering'] = true;
-
     switch ($savemode) {
       case 'new':
         // save submitted data as new (non-recurring) event


commit ff90fa64f92ab0ca9e311eac94853d5a2357fb3f
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu May 10 20:45:30 2012 +0200

    Implement missing lib function

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 689b438..983a268 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -306,13 +306,72 @@ class kolab_storage
      */
     public static function folder_selector($type, $attrs, $current = '')
     {
-        // TODO: implement this
+        // get all folders of specified type
+        $folders = self::get_folders($type);
 
+        $delim = self::$imap->get_hierarchy_delimiter();
+        $names = array();
+        $len   = strlen($current);
+
+        if ($len && ($rpos = strrpos($current, $delim))) {
+            $parent = substr($current, 0, $rpos);
+            $p_len  = strlen($parent);
+        }
+
+        // Filter folders list
+        foreach ($folders as $c_folder) {
+            $name = $c_folder->name;
+            // skip current folder and it's subfolders
+            if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
+                continue;
+            }
+
+            // always show the parent of current folder
+            if ($p_len && $name == $parent) { }
+            // skip folders where user have no rights to create subfolders
+            else if ($c_folder->get_owner() != $_SESSION['username']) {
+                $rights = $c_folder->get_myrights();
+                if (!preg_match('/[ck]/', $rights)) {
+                    continue;
+                }
+            }
+
+            $names[$name] = rcube_charset::convert($name, 'UTF7-IMAP');
+        }
+
+        // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+        if ($p_len && !isset($names[$parent])) {
+            $names[$parent] = rcube_charset::convert($parent, 'UTF7-IMAP');
+        }
+
+        // Sort folders list
+        asort($names, SORT_LOCALE_STRING);
+
+        $folders = array_keys($names);
+        $names   = array();
 
         // Build SELECT field of parent folder
         $select = new html_select($attrs);
         $select->add('---', '');
 
+        foreach ($folders as $name) {
+            $imap_name = $name;
+            $name      = $origname = self::object_name($name);
+
+            // find folder prefix to truncate
+            for ($i = count($names)-1; $i >= 0; $i--) {
+                if (strpos($name, $names[$i].' » ') === 0) {
+                    $length = strlen($names[$i].' » ');
+                    $prefix = substr($name, 0, $length);
+                    $count  = count(explode(' » ', $prefix));
+                    $name   = str_repeat('  ', $count-1) . '» ' . substr($name, $length);
+                    break;
+                }
+            }
+
+            $names[] = $origname;
+            $select->add($name, $imap_name);
+        }
 
         return $select;
     }


commit 418e6540f5c614a593d11e7ecbd0f6c275187065
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu May 10 20:45:06 2012 +0200

    Update roundcube core API calls

diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
index 2dabd6c..980df05 100644
--- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -144,8 +144,8 @@ class kolab_addressbook_ui
         if (strlen($folder)) {
             $hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
 
-            $this->rc->imap_connect();
-            $options = $this->rc->imap->mailbox_info($folder);
+            $this->rc->storage_connect();
+            $options = $this->rc->get_storage()->mailbox_info($folder);
         }
 
         $form   = array();


commit 0131c8aa513cbea8ae78abca654c18ddd0499b70
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 9 19:12:26 2012 +0200

    Improve object fetching when cache is disabled

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 65aa4cb..9f9431c 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -301,7 +301,18 @@ class kolab_storage_cache
         else {
             // extract object type from query parameter
             $filter = $this->_query2assoc($query);
-            $result = $this->_fetch($this->index, $filter['type']);
+
+            // use 'list' for folder's default objects
+            if ($filter['type'] == $this->type) {
+                $index = $this->index;
+            }
+            else {  // search by object type
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
+                $index = $this->imap->search_once($this->folder->name, $search)->get();
+            }
+
+            // fetch all messages in $index from IMAP
+            $result = $this->_fetch($index, $filter['type']);
 
             // TODO: post-filter result according to query
         }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index eed7b08..d691906 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -288,32 +288,6 @@ class kolab_storage_folder
 
         // fetch objects from cache
         return $this->cache->select(array(array('type','=',$type)));
-
-/*
-        $results = array();
-        $ctype  = self::KTYPE_PREFIX . $type;
-
-        // use 'list' for folder's default objects
-        if ($type == $this->type) {
-            $index = $this->imap->index($this->name);
-        }
-        else {  // search by object type
-            $search = 'UNDELETED HEADER X-Kolab-Type ' . $ctype;
-            $index = $this->imap->search_once($this->name, $search);
-        }
-
-        // fetch all messages from IMAP
-        foreach ($index->get() as $msguid) {
-            if ($object = $this->read_object($msguid, $type)) {
-                $results[] = $object;
-                $this->uid2msg[$object['uid']] = $msguid;
-            }
-        }
-
-        // TODO: write $this->uid2msg to cache
-
-        return $results;
-*/
     }
 
 


commit b37784bb3ba3b3e89b54b542242d568c4d5af20a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 9 19:02:46 2012 +0200

    Query objects directly from the storage layer; no need to fetch all for every operation

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 7763159..53a8d83 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -38,8 +38,7 @@ class kolab_calendar
   public $storage;
 
   private $cal;
-  private $events;
-  private $id2uid;
+  private $events = array();
   private $imap_folder = 'INBOX/Calendar';
   private $search_fields = array('title', 'description', 'location', '_attendees');
   private $sensitivity_map = array('public', 'private', 'confidential');
@@ -175,17 +174,21 @@ class kolab_calendar
    */
   public function get_event($id)
   {
-    $this->_fetch_events();
-    
+    // directly access storage object
+    if (!$this->events[$id] && ($record = $this->storage->get_object($id)))
+        $this->events[$id] = $this->_to_rcube_event($record);
+
     // event not found, maybe a recurring instance is requested
     if (!$this->events[$id]) {
       $master_id = preg_replace('/-\d+$/', '', $id);
-      if ($this->events[$master_id] && $this->events[$master_id]['recurrence']) {
-        $master = $this->events[$master_id];
+      if ($record = $this->storage->get_object($master_id))
+        $this->events[$master_id] = $this->_to_rcube_event($record);
+
+      if (($master = $this->events[$master_id]) && $master['recurrence']) {
         $this->_get_recurring_events($master, $master['start'], $master['start'] + 86400 * 365 * 10, $id);
       }
     }
-    
+
     return $this->events[$id];
   }
 
@@ -199,8 +202,16 @@ class kolab_calendar
    */
   public function list_events($start, $end, $search = null, $virtual = 1)
   {
-    $this->_fetch_events();
-    
+    // query Kolab storage
+    $query = array(
+      array('dtstart', '<=', $end),
+      array('dtend',   '>=', $start),
+    );
+    foreach ((array)$this->storage->select($query) as $record) {
+      $event = $this->_to_rcube_event($record);
+      $this->events[$event['id']] = $event;
+    }
+
     if (!empty($search))
       $search =  mb_strtolower($search);
     
@@ -345,22 +356,6 @@ class kolab_calendar
     return false;
   }
 
-  /**
-   * Simply fetch all records and store them in private member vars
-   * We thereby rely on cahcing done by the Horde classes
-   */
-  private function _fetch_events()
-  {
-    if (!isset($this->events)) {
-      $this->events = array();
-
-      foreach ((array)$this->storage->get_objects() as $record) {
-        $event = $this->_to_rcube_event($record);
-        $this->events[$event['id']] = $event;
-      }
-    }
-  }
-
 
   /**
    * Create instances of a recurring event


commit 86c1d510f1c192880920085fd7b77d65f3f848ad
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 9 19:01:51 2012 +0200

    Add select() method to query objects from cache; Use locks to avoid multiple threads synching the same folder simultaneously

diff --git a/plugins/libkolab/SQL/mysql.sql b/plugins/libkolab/SQL/mysql.sql
index 376eb4f..5f49e92 100644
--- a/plugins/libkolab/SQL/mysql.sql
+++ b/plugins/libkolab/SQL/mysql.sql
@@ -6,7 +6,7 @@
  * @licence GNU AGPL
  **/
 
-CREATE TABLE `kolab_cache` (
+CREATE TABLE IF NOT EXISTS `kolab_cache` (
   `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
   `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
   `msguid` BIGINT UNSIGNED NOT NULL,
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 9f80691..65aa4cb 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -81,6 +81,9 @@ class kolab_storage_cache
         if ($this->synched)
             return;
 
+        // lock synchronization for this folder or wait if locked
+        $this->_sync_lock();
+
         // synchronize IMAP mailbox cache
         $this->imap->folder_sync($this->folder->name);
 
@@ -92,8 +95,9 @@ class kolab_storage_cache
         if ($this->ready) {
             // read cache index
             $sql_result = $this->db->query(
-                "SELECT msguid, uid FROM kolab_cache WHERE resource=?",
-                $this->resource_uri
+                "SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
+                $this->resource_uri,
+                'lock'
             );
 
             $old_index = array();
@@ -120,6 +124,9 @@ class kolab_storage_cache
             }
         }
 
+        // remove lock
+        $this->_sync_unlock();
+
         $this->synched = time();
     }
 
@@ -349,6 +356,7 @@ class kolab_storage_cache
                 $this->db->quote($param[2])
             );
         }
+
         return $sql_where;
     }
 
@@ -400,6 +408,11 @@ class kolab_storage_cache
             // database runs in server's timezone so using date() is what we want
             $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
             $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
+
+            // extend date range for recurring events
+            if ($object['recurrence']) {
+                $sql_data['dtend'] = date('Y-m-d H:i:s', $object['recurrence']['UNTIL'] ?: strtotime('now + 2 years'));
+            }
         }
 
         if ($object['_formatobj'])
@@ -454,6 +467,61 @@ class kolab_storage_cache
     }
 
     /**
+     * Check lock record for this folder and wait if locked or set lock
+     */
+    private function _sync_lock()
+    {
+        if (!$this->ready)
+            return;
+
+        $sql_arr = $this->db->fetch_assoc($this->db->query(
+            "SELECT msguid AS locked FROM kolab_cache ".
+            "WHERE resource=? AND type=?",
+            $this->resource_uri,
+            'lock'
+        ));
+
+        // create lock record if not exists
+        if (!$sql_arr) {
+            $this->db->query(
+                "INSERT INTO kolab_cache (resource, type, msguid, uid, data, xml)".
+                " VALUES (?, ?, ?, '', '', '')",
+                $this->resource_uri,
+                'lock',
+                time()
+            );
+        }
+        // wait if locked (expire locks after 10 minutes)
+        else if (intval($sql_arr['locked']) > 0 && (time() - $sql_arr['locked']) < 600) {
+            usleep(500000);
+            return $this->_sync_lock();
+        }
+        // set lock
+        else {
+            $this->db->query(
+                "UPDATE kolab_cache SET msguid=? ".
+                "WHERE resource=? AND type=?",
+                time(),
+                $this->resource_uri,
+                'lock'
+            );
+        }
+    }
+
+    /**
+     * Remove lock for this folder
+     */
+    private function _sync_unlock()
+    {
+        $this->db->query(
+            "UPDATE kolab_cache SET msguid=0 ".
+            "WHERE resource=? AND type=?",
+            $this->resource_uri,
+            'lock'
+        );
+    }
+
+    /**
      * Resolve an object UID into an IMAP message UID
      *
      * @param string  Kolab object UID
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index c55a34f..eed7b08 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -318,6 +318,41 @@ class kolab_storage_folder
 
 
     /**
+     * Select *some* Kolab objects matching the given query
+     *
+     * @param array Pseudo-SQL query as list of filter parameter triplets
+     *   triplet: array('<colname>', '<comparator>', '<value>')
+     * @return array List of Kolab data objects (each represented as hash array)
+     */
+    public function select($query = array())
+    {
+        // check query argument
+        if (empty($query))
+            return $this->get_objects();
+
+        $type = null;
+        foreach ($query as $i => $param) {
+            if ($param[0] == 'type') {
+                $type = $param[2];
+            }
+            else if (($param[0] == 'dtstart' || $param[0] == 'dtend') && is_numeric($param[2])) {
+              $query[$i][2] = date('Y-m-d H:i:s', $param[2]);
+            }
+        }
+
+        // add type selector if not in $query
+        if (!$type)
+            $query[] = array('type','=',$this->type);
+
+        // synchronize caches
+        $this->cache->synchronize();
+
+        // fetch objects from cache
+        return $this->cache->select($query);
+    }
+
+
+    /**
      * Getter for a single Kolab object, identified by its UID
      *
      * @param string Object UID
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 3d0709b..fd911be 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -63,9 +63,7 @@ class libkolab extends rcube_plugin
      */
     function storage_init($p)
     {
-        $rcmail = rcmail::get_instance();
         $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE');
-
         return $p;
     }
 


commit ed467e73f910f56e50759ad35f9a6bacf030bbe0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 9 17:20:39 2012 +0200

    Disable kolab_config plugin for now

diff --git a/plugins/kolab_config/kolab_config.php b/plugins/kolab_config/kolab_config.php
index 24d569e..b785306 100644
--- a/plugins/kolab_config/kolab_config.php
+++ b/plugins/kolab_config/kolab_config.php
@@ -61,16 +61,9 @@ class kolab_config extends rcube_plugin
         if ($this->config)
             return;
 
-        $this->require_plugin('kolab_folders');
+        return;  // CURRENTLY DISABLED until libkolabxml has support for config objects
 
-        // load dependencies
-        require_once 'Horde/Util.php';
-        require_once 'Horde/Kolab/Format.php';
-        require_once 'Horde/Kolab/Format/XML.php';
-        require_once $this->home . '/lib/configuration.php';
-        require_once $this->home . '/lib/kolab_configuration.php';
-
-        String::setDefaultCharset('UTF-8');
+        $this->require_plugin('libkolab');
 
         $this->config = new kolab_configuration();
 


commit 3a05fb691ff0a1da69bc2714286de2459af9a419
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 9 14:10:02 2012 +0200

    Make Horde library optional

diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 23816a6..3d0709b 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -47,14 +47,15 @@ class libkolab extends rcube_plugin
         $rcmail = rcmail::get_instance();
         kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
 
-        // load (old) dependencies
-        require_once 'Horde/Util.php';
-        require_once 'Horde/Kolab/Format.php';
-        require_once 'Horde/Kolab/Format/XML.php';
-        require_once 'Horde/Kolab/Format/XML/contact.php';
-        require_once 'Horde/Kolab/Format/XML/event.php';
+        // load (old) dependencies if available
+        if (@include_once('Horde/Util.php')) {
+            include_once 'Horde/Kolab/Format.php';
+            include_once 'Horde/Kolab/Format/XML.php';
+            include_once 'Horde/Kolab/Format/XML/contact.php';
+            include_once 'Horde/Kolab/Format/XML/event.php';
 
-        String::setDefaultCharset('UTF-8');
+            String::setDefaultCharset('UTF-8');
+        }
     }
 
     /**


commit 7cb7f5316e9af9b45fa771c728e53cd8e0b9a54f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon May 7 12:07:12 2012 +0200

    Fix namespace prefix stripping

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index c93881b..c55a34f 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -196,7 +196,7 @@ class kolab_storage_folder
         // strip namespace prefix from folder name
         $ns = $this->get_namespace();
         $nsdata = $this->imap->get_namespace($ns);
-        if (is_array($nsdata[0]) && strpos($this->name, $nsdata[0][0]) === 0) {
+        if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
             $subpath = substr($this->name, strlen($nsdata[0][0]));
             if ($ns == 'other') {
                 list($user, $suffix) = explode($nsdata[0][1], $subpath);


commit 9db0e9b3d58b783fc3f95876e4bcef993f802f9a
Author: Thomas B <roundcube at gmail.com>
Date:   Thu May 3 11:01:23 2012 +0200

    Log Kolab object parsing failures

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index a0c12c4..c93881b 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -441,6 +441,19 @@ class kolab_storage_folder
 
             return $object;
         }
+        else {
+            // try to extract object UID from XML block
+            if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
+                $msgadd = " UID = " . trim(strip_tags($m[1]));
+
+            raise_error(array(
+                'code' => 600,
+                'type' => 'php',
+                'file' => __FILE__,
+                'line' => __LINE__,
+                'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
+            ), true);
+        }
 
         return false;
     }


commit c9963d279c0b58348b54d72f004af06f48b5dda7
Author: Thomas B <roundcube at gmail.com>
Date:   Thu May 3 10:28:42 2012 +0200

    Correctly handle object moving in kolab cache

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index ff11d63..9f80691 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -66,22 +66,8 @@ class kolab_storage_cache
             return;
         }
 
-        // strip namespace prefix from folder name
-        $ns = $this->folder->get_namespace();
-        $nsdata = $this->imap->get_namespace($ns);
-        if (is_array($nsdata[0]) && strpos($this->folder->name, $nsdata[0][0]) === 0) {
-            $subpath = substr($this->folder->name, strlen($nsdata[0][0]));
-            if ($ns == 'other') {
-                list($user, $suffix) = explode($nsdata[0][1], $subpath);
-                $subpath = $suffix;
-            }
-        }
-        else {
-            $subpath = $this->folder->name;
-        }
-
         // compose fully qualified ressource uri for this instance
-        $this->resource_uri = 'imap://' . urlencode($this->folder->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+        $this->resource_uri = $this->folder->get_resource_uri();
         $this->ready = $this->enabled;
     }
 
@@ -140,14 +126,22 @@ class kolab_storage_cache
 
     /**
      * Read a single entry from cache or
+     *
+     * @param string Related IMAP message UID
+     * @param string Object type to read
+     * @param string IMAP folder name the entry relates to
+     * @param array  Hash array with object properties or null if not found
      */
-    public function get($msguid, $type = null, $folder = null)
+    public function get($msguid, $type = null, $foldername = null)
     {
+        // delegate to another cache instance
+        if ($foldername && $foldername != $this->folder->name) {
+            return kolab_storage::get_folder($foldername)->cache->get($msguid, $object);
+        }
+
         // load object if not in memory
         if (!isset($this->objects[$msguid])) {
             if ($this->ready) {
-                // TODO: handle $folder != $this->folder->name situations
-                
                 $sql_result = $this->db->query(
                     "SELECT * FROM kolab_cache ".
                     "WHERE resource=? AND msguid=?",
@@ -162,7 +156,7 @@ class kolab_storage_cache
 
             // fetch from IMAP if not present in cache
             if (empty($this->objects[$msguid])) {
-                $result = $this->_fetch(array($msguid), $type, $folder);
+                $result = $this->_fetch(array($msguid), $type, $foldername);
                 $this->objects[$msguid] = $result[0];
             }
         }
@@ -172,14 +166,22 @@ class kolab_storage_cache
 
 
     /**
+     * Insert/Update a cache entry
      *
+     * @param string Related IMAP message UID
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
+     * @param string IMAP folder name the entry relates to
      */
-    public function set($msguid, $object, $folder = null)
+    public function set($msguid, $object, $foldername = null)
     {
+        // delegate to another cache instance
+        if ($foldername && $foldername != $this->folder->name) {
+            kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
+            return;
+        }
+        
         // write to cache
         if ($this->ready) {
-            // TODO: handle $folder != $this->folder->name situations
-
             // remove old entry
             $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=?",
                 $this->resource_uri, $msguid);
@@ -219,6 +221,36 @@ class kolab_storage_cache
             $this->uid2msg[$object['uid']] = $msguid;
     }
 
+    /**
+     * Move an existing cache entry to a new resource
+     *
+     * @param string Entry's IMAP message UID
+     * @param string Entry's Object UID
+     * @param string Target IMAP folder to move it to
+     */
+    public function move($msguid, $objuid, $target_folder)
+    {
+        $target = kolab_storage::get_folder($target_folder);
+
+        // resolve new message UID in target folder
+        if ($new_msguid = $target->cache->uid2msguid($objuid)) {
+            $this->db->query(
+                "UPDATE kolab_cache SET resource=?, msguid=? ".
+                "WHERE resource=? AND msguid=?",
+                $target->get_resource_uri(),
+                $new_msguid,
+                $this->resource_uri,
+                $msguid
+            );
+        }
+        else {
+            // just clear cache entry
+            $this->set($msguid, false);
+        }
+
+        unset($this->uid2msg[$uid]);
+    }
+
 
     /**
      * Remove all objects from local cache
@@ -365,7 +397,7 @@ class kolab_storage_cache
 
         // set type specific values
         if ($this->folder->type == 'event') {
-            // database runs in server's timetone so using date() is safe
+            // database runs in server's timezone so using date() is what we want
             $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
             $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
         }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 69770ab..a0c12c4 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -27,23 +27,27 @@ class kolab_storage_folder
 
     /**
      * The folder name.
-     *
      * @var string
      */
     public $name;
 
     /**
      * The type of this folder.
-     *
      * @var string
      */
     public $type;
 
+    /**
+    * The attached cache object
+    * @var kolab_storage_cache
+     */
+    public $cache;
+
     private $type_annotation;
     private $imap;
     private $info;
     private $owner;
-    private $cache;
+    private $resource_uri;
     private $uid2msg = array();
 
 
@@ -72,6 +76,7 @@ class kolab_storage_folder
         $metadata = $this->imap->get_metadata($this->name, array(kolab_storage::CTYPE_KEY));
         $this->type_annotation = $metadata[$this->name][kolab_storage::CTYPE_KEY];
         $this->type = reset(explode('.', $this->type_annotation));
+        $this->resource_uri = null;
 
         $this->cache->set_folder($this);
     }
@@ -181,6 +186,34 @@ class kolab_storage_folder
 
 
     /**
+     * Compose a unique resource URI for this IMAP folder
+     */
+    public function get_resource_uri()
+    {
+        if (!empty($this->resource_uri))
+            return $this->resource_uri;
+
+        // strip namespace prefix from folder name
+        $ns = $this->get_namespace();
+        $nsdata = $this->imap->get_namespace($ns);
+        if (is_array($nsdata[0]) && strpos($this->name, $nsdata[0][0]) === 0) {
+            $subpath = substr($this->name, strlen($nsdata[0][0]));
+            if ($ns == 'other') {
+                list($user, $suffix) = explode($nsdata[0][1], $subpath);
+                $subpath = $suffix;
+            }
+        }
+        else {
+            $subpath = $this->name;
+        }
+
+        // compose fully qualified ressource uri for this instance
+        $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+        return $this->resource_uri;
+    }
+
+
+    /**
      * Check subscription status of this folder
      *
      * @param string Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
@@ -532,7 +565,7 @@ class kolab_storage_folder
     {
         if ($msguid = $this->cache->uid2msguid($uid)) {
             if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) {
-                // TODO: update cache
+                $this->cache->move($msguid, $uid, $target_folder);
                 return true;
             }
             else {


commit 3fc78aa4096ae5976908a460cc6c32470bd57c89
Author: Thomas B <roundcube at gmail.com>
Date:   Wed May 2 18:04:42 2012 +0200

    Set mime-part ID when adding attachments

diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index cfcacf5..ff11d63 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -388,7 +388,7 @@ class kolab_storage_cache
             }
             else if ($key == '_attachments') {
                 foreach ($val as $k => $att) {
-                    unset($att['content']);
+                    unset($att['content'], $att['path']);
                     if ($att['id'])
                         $data[$key][$k] = $att;
                 }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 246e58b..69770ab 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -574,6 +574,7 @@ class kolab_storage_folder
         $mime = new Mail_mime("\r\n");
         $rcmail = rcube::get_instance();
         $headers = array();
+        $part_id = 1;
 
         if ($ident = $rcmail->user->get_identity()) {
             $headers['From'] = $ident['email'];
@@ -598,6 +599,7 @@ class kolab_storage_folder
             $rcmail->config->get('mime_param_folding') == 2 ? 'quoted-printable' : null,
             '', RCMAIL_CHARSET
         );
+        $part_id++;
 
         // save object attachments as separate parts
         // TODO: optimize memory consumption by using tempfiles for transfer
@@ -608,10 +610,14 @@ class kolab_storage_folder
             }
             if (!empty($att['content'])) {
                 $mime->addAttachment($att['content'], $att['mimetype'], $name, false);
+                $part_id++;
             }
             else if (!empty($att['path'])) {
                 $mime->addAttachment($att['path'], $att['mimetype'], $name, true);
+                $part_id++;
             }
+
+            $object['_attachments'][$name]['id'] = $part_id;
         }
 
         return $mime->getMessage();


commit d2f8ae44d2e8d367ba9a4b2d8dad60e56bfd69bb
Author: Thomas B <roundcube at gmail.com>
Date:   Wed May 2 18:02:33 2012 +0200

    Remove debug code

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 063cbf5..b888825 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -268,8 +268,6 @@ function rcube_calendar_ui(settings)
         }
       }
 
-      return;
-
       rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
     };
 


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

    First implementation of a caching layer for kolab_storage;
    - Caching is disabled by default (until fully functional and tested)
    - Attention: database initialization required for cache)
    
    Silently ignore old Kolab2 objects if no Horde classes found to parse them.

diff --git a/plugins/libkolab/SQL/mysql.sql b/plugins/libkolab/SQL/mysql.sql
new file mode 100644
index 0000000..376eb4f
--- /dev/null
+++ b/plugins/libkolab/SQL/mysql.sql
@@ -0,0 +1,20 @@
+/**
+ * libkolab database schema
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ **/
+
+CREATE TABLE `kolab_cache` (
+  `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `dtstart` DATETIME,
+  `dtend` DATETIME,
+  `tags` VARCHAR(255) NOT NULL,
+  PRIMARY KEY(`resource`,`type`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 42ecaab..134df85 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -31,11 +31,17 @@ abstract class kolab_format
 
     protected $obj;
     protected $data;
+    protected $xmldata;
+    protected $loaded = false;
 
     /**
      * Factory method to instantiate a kolab_format object of the given type
+     *
+     * @param string Object type to instantiate
+     * @param string Cached xml data to initialize with
+     * @return object kolab_format
      */
-    public static function factory($type)
+    public static function factory($type, $xmldata = null)
     {
         if (!isset(self::$timezone))
             self::$timezone = new DateTimeZone('UTC');
@@ -43,7 +49,7 @@ abstract class kolab_format
         $suffix = preg_replace('/[^a-z]+/', '', $type);
         $classname = 'kolab_format_' . $suffix;
         if (class_exists($classname))
-            return new $classname();
+            return new $classname($xmldata);
 
         return PEAR::raiseError(sprintf("Failed to load Kolab Format wrapper for type %s", $type));
     }
@@ -166,9 +172,23 @@ abstract class kolab_format
     }
 
     /**
+     * Initialize libkolabxml object with cached xml data
+     */
+    protected function init()
+    {
+        if (!$this->loaded) {
+            if ($this->xmldata) {
+                $this->load($this->xmldata);
+                $this->xmldata = null;
+            }
+            $this->loaded = true;
+        }
+    }
+
+    /**
      * Direct getter for object properties
      */
-    function __get($var)
+    public function __get($var)
     {
         return $this->data[$var];
     }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 69db2d1..59a6e0a 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -101,9 +101,10 @@ class kolab_format_contact extends kolab_format
     /**
      * Default constructor
      */
-    function __construct()
+    function __construct($xmldata = null)
     {
         $this->obj = new Contact;
+        $this->xmldata = $xmldata;
 
         // complete phone types
         $this->phonetypes['homefax'] |= Telephone::Home;
@@ -118,6 +119,7 @@ class kolab_format_contact extends kolab_format
     public function load($xml)
     {
         $this->obj = kolabformat::readContact($xml, false);
+        $this->loaded = true;
     }
 
     /**
@@ -127,9 +129,14 @@ class kolab_format_contact extends kolab_format
      */
     public function write()
     {
-        $xml = kolabformat::writeContact($this->obj);
-        parent::update_uid();
-        return $xml;
+        $this->init();
+
+        if ($this->obj->isValid()) {
+            $this->xmldata = kolabformat::writeContact($this->obj);
+            parent::update_uid();
+        }
+
+        return $this->xmldata;
     }
 
     /**
@@ -139,6 +146,8 @@ class kolab_format_contact extends kolab_format
      */
     public function set(&$object)
     {
+        $this->init();
+
         // set some automatic values if missing
         if (false && !$this->obj->created()) {
             if (!empty($object['created']))
@@ -255,9 +264,10 @@ class kolab_format_contact extends kolab_format
             if ($type = rc_image_content_type($object['photo']))
                 $this->obj->setPhoto($object['photo'], $type);
         }
-        else if (isset($object['photo'])) {
+        else if (isset($object['photo']))
             $this->obj->setPhoto('','');
-        }
+        else if ($this->obj->photoMimetype())  // load saved photo for caching
+            $object['photo'] = $this->obj->photo();
 
         // spouse and children are relateds
         $rels = new vectorrelated;
@@ -299,8 +309,8 @@ class kolab_format_contact extends kolab_format
 
 
         // cache this data
-        unset($object['_formatobj']);
         $this->data = $object;
+        unset($this->data['_formatobj']);
     }
 
     /**
@@ -308,7 +318,7 @@ class kolab_format_contact extends kolab_format
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && true /*$this->obj->isValid()*/);
+        return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
     }
 
     /**
@@ -322,6 +332,8 @@ class kolab_format_contact extends kolab_format
         if (!empty($this->data))
             return $this->data;
 
+        $this->init();
+
         // read object properties into local data object
         $object = array(
             'uid'       => $this->obj->uid(),
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index 3c5047c..b8d2208 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -26,9 +26,10 @@ class kolab_format_distributionlist extends kolab_format
 {
     public $CTYPE = 'application/vcard+xml';
 
-    function __construct()
+    function __construct($xmldata = null)
     {
         $this->obj = new DistList;
+        $this->xmldata = $xmldata;
     }
 
     /**
@@ -39,6 +40,7 @@ class kolab_format_distributionlist extends kolab_format
     public function load($xml)
     {
         $this->obj = kolabformat::readDistlist($xml, false);
+        $this->loaded = true;
     }
 
     /**
@@ -48,13 +50,20 @@ class kolab_format_distributionlist extends kolab_format
      */
     public function write()
     {
-        $xml = kolabformat::writeDistlist($this->obj);
-        parent::update_uid();
-        return $xml;
+        $this->init();
+
+        if ($this->obj->isValid()) {
+            $this->xmldata = kolabformat::writeDistlist($this->obj);
+            parent::update_uid();
+        }
+
+        return $this->xmldata;
     }
 
     public function set(&$object)
     {
+        $this->init();
+
         // set some automatic values if missing
         if (!empty($object['uid']))
             $this->obj->setUid($object['uid']);
@@ -79,8 +88,8 @@ class kolab_format_distributionlist extends kolab_format
         $this->obj->setMembers($members);
 
         // cache this data
-        unset($object['_formatobj']);
         $this->data = $object;
+        unset($this->data['_formatobj']);
     }
 
     public function is_valid()
@@ -122,6 +131,8 @@ class kolab_format_distributionlist extends kolab_format
         if (!empty($this->data))
             return $this->data;
 
+        $this->init();
+
         // read object properties
         $object = array(
             'uid'       => $this->obj->uid(),
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 78e51ca..0552025 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -91,9 +91,10 @@ class kolab_format_event extends kolab_format
     /**
      * Default constructor
      */
-    function __construct()
+    function __construct($xmldata = null)
     {
         $this->obj = new Event;
+        $this->xmldata = $xmldata;
     }
 
     /**
@@ -104,6 +105,7 @@ class kolab_format_event extends kolab_format
     public function load($xml)
     {
         $this->obj = kolabformat::readEvent($xml, false);
+        $this->loaded = true;
     }
 
     /**
@@ -113,9 +115,14 @@ class kolab_format_event extends kolab_format
      */
     public function write()
     {
-        $xml = kolabformat::writeEvent($this->obj);
-        parent::update_uid();
-        return $xml;
+        $this->init();
+
+        if ($this->obj->isValid()) {
+            $this->xmldata = kolabformat::writeEvent($this->obj);
+            parent::update_uid();
+        }
+
+        return $this->xmldata;
     }
 
     /**
@@ -125,6 +132,8 @@ class kolab_format_event extends kolab_format
      */
     public function set(&$object)
     {
+        $this->init();
+
         // set some automatic values if missing
         if (!$this->obj->created()) {
             if (!empty($object['created']))
@@ -293,8 +302,8 @@ class kolab_format_event extends kolab_format
         $this->obj->setAttachments($vattach);
 
         // cache this data
-        unset($object['_formatobj']);
         $this->data = $object;
+        unset($this->data['_formatobj']);
     }
 
     /**
@@ -302,7 +311,7 @@ class kolab_format_event extends kolab_format
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+        return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid());
     }
 
     /**
@@ -316,6 +325,8 @@ class kolab_format_event extends kolab_format
         if (!empty($this->data))
             return $this->data;
 
+        $this->init();
+
         $sensitivity_map = array_flip($this->sensitivity_map);
 
         // read object properties
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
new file mode 100644
index 0000000..cfcacf5
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -0,0 +1,443 @@
+<?php
+
+/**
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache
+{
+    private $db;
+    private $imap;
+    private $folder;
+    private $uid2msg;
+    private $objects;
+    private $resource_uri;
+    private $enabled = true;
+    private $synched = false;
+    private $ready = false;
+
+    private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
+
+
+    /**
+     * Default constructor
+     */
+    public function __construct(kolab_storage_folder $storage_folder = null)
+    {
+        $rcmail = rcube::get_instance();
+        $this->db = $rcmail->get_dbh();
+        $this->imap = $rcmail->get_storage();
+        $this->enabled = $rcmail->config->get('kolab_cache', false);
+
+        if ($storage_folder)
+            $this->set_folder($storage_folder);
+    }
+
+
+    /**
+     * Connect cache with a storage folder
+     *
+     * @param kolab_storage_folder The storage folder instance to connect with
+     */
+    public function set_folder(kolab_storage_folder $storage_folder)
+    {
+        $this->folder = $storage_folder;
+
+        if (empty($this->folder->name)) {
+            $this->ready = false;
+            return;
+        }
+
+        // strip namespace prefix from folder name
+        $ns = $this->folder->get_namespace();
+        $nsdata = $this->imap->get_namespace($ns);
+        if (is_array($nsdata[0]) && strpos($this->folder->name, $nsdata[0][0]) === 0) {
+            $subpath = substr($this->folder->name, strlen($nsdata[0][0]));
+            if ($ns == 'other') {
+                list($user, $suffix) = explode($nsdata[0][1], $subpath);
+                $subpath = $suffix;
+            }
+        }
+        else {
+            $subpath = $this->folder->name;
+        }
+
+        // compose fully qualified ressource uri for this instance
+        $this->resource_uri = 'imap://' . urlencode($this->folder->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+        $this->ready = $this->enabled;
+    }
+
+
+    /**
+     * Synchronize local cache data with remote
+     */
+    public function synchronize()
+    {
+        // only sync once per request cycle
+        if ($this->synched)
+            return;
+
+        // synchronize IMAP mailbox cache
+        $this->imap->folder_sync($this->folder->name);
+
+        // compare IMAP index with object cache index
+        $imap_index = $this->imap->index($this->folder->name);
+        $this->index = $imap_index->get();
+
+        // determine objects to fetch or to invalidate
+        if ($this->ready) {
+            // read cache index
+            $sql_result = $this->db->query(
+                "SELECT msguid, uid FROM kolab_cache WHERE resource=?",
+                $this->resource_uri
+            );
+
+            $old_index = array();
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $old_index[] = $sql_arr['msguid'];
+                $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+            }
+
+            // fetch new objects from imap
+            $fetch_index = array_diff($this->index, $old_index);
+            foreach ($this->_fetch($fetch_index, '*') as $object) {
+                $msguid = $object['_msguid'];
+                $this->set($msguid, $object);
+            }
+
+            // delete invalid entries from local DB
+            $del_index = array_diff($old_index, $this->index);
+            if (!empty($del_index)) {
+                $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+                $this->db->query(
+                    "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
+                    $this->resource_uri
+                );
+            }
+        }
+
+        $this->synched = time();
+    }
+
+
+    /**
+     * Read a single entry from cache or
+     */
+    public function get($msguid, $type = null, $folder = null)
+    {
+        // load object if not in memory
+        if (!isset($this->objects[$msguid])) {
+            if ($this->ready) {
+                // TODO: handle $folder != $this->folder->name situations
+                
+                $sql_result = $this->db->query(
+                    "SELECT * FROM kolab_cache ".
+                    "WHERE resource=? AND msguid=?",
+                    $this->resource_uri,
+                    $msguid
+                );
+
+                if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                    $this->objects[$msguid] = $this->_unserialize($sql_arr);
+                }
+            }
+
+            // fetch from IMAP if not present in cache
+            if (empty($this->objects[$msguid])) {
+                $result = $this->_fetch(array($msguid), $type, $folder);
+                $this->objects[$msguid] = $result[0];
+            }
+        }
+
+        return $this->objects[$msguid];
+    }
+
+
+    /**
+     *
+     */
+    public function set($msguid, $object, $folder = null)
+    {
+        // write to cache
+        if ($this->ready) {
+            // TODO: handle $folder != $this->folder->name situations
+
+            // remove old entry
+            $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=?",
+                $this->resource_uri, $msguid);
+
+            // write new object data if not false (wich means deleted)
+            if ($object) {
+                $sql_data = $this->_serialize($object);
+                $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
+
+                $result = $this->db->query(
+                    "INSERT INTO kolab_cache ".
+                    " (resource, type, msguid, uid, data, xml, dtstart, dtend)".
+                    " VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+                    $this->resource_uri,
+                    $objtype,
+                    $msguid,
+                    $object['uid'],
+                    $sql_data['data'],
+                    $sql_data['xml'],
+                    $sql_data['dtstart'],
+                    $sql_data['dtend']
+                );
+
+                if (!$this->db->affected_rows($result)) {
+                    rcmail::raise_error(array(
+                        'code' => 900, 'type' => 'php',
+                        'message' => "Failed to write to kolab cache"
+                    ), true);
+                }
+            }
+        }
+
+        // keep a copy in memory for fast access
+        $this->objects[$msguid] = $object;
+
+        if ($object)
+            $this->uid2msg[$object['uid']] = $msguid;
+    }
+
+
+    /**
+     * Remove all objects from local cache
+     */
+    public function purge($type = null)
+    {
+        $result = $this->db->query(
+            "DELETE FROM kolab_cache WHERE resource=?".
+            ($type ? ' AND type=?' : ''),
+            $this->resource_uri,
+            $type
+        );
+        return $this->db->affected_rows($result);
+    }
+
+
+    /**
+     * Select Kolab objects filtered by the given query
+     *
+     * @param array Pseudo-SQL query as list of filter parameter triplets
+     *   triplet: array('<colname>', '<comparator>', '<value>')
+     * @return array List of Kolab data objects (each represented as hash array)
+     */
+    public function select($query = array())
+    {
+        $result = array();
+
+        // read from local cache DB (assume it to be synchronized)
+        if ($this->ready) {
+            $sql_result = $this->db->query(
+                "SELECT * FROM kolab_cache ".
+                "WHERE resource=? " . $this->_sql_where($query),
+                $this->resource_uri
+            );
+
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                if ($object = $this->_unserialize($sql_arr))
+                    $result[] = $object;
+            }
+        }
+        else {
+            // extract object type from query parameter
+            $filter = $this->_query2assoc($query);
+            $result = $this->_fetch($this->index, $filter['type']);
+
+            // TODO: post-filter result according to query
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Get number of objects mathing the given query
+     *
+     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
+     * @return integer The number of objects of the given type
+     */
+    public function count($query = array())
+    {
+        $count = 0;
+
+        // cache is in sync, we can count records in local DB
+        if ($this->synched) {
+            $sql_result = $this->db->query(
+                "SELECT COUNT(*) AS NUMROWS FROM kolab_cache ".
+                "WHERE resource=? " . $this->_sql_where($query),
+                $this->resource_uri
+            );
+
+            $sql_arr = $this->db->fetch_assoc($sql_result);
+            $count = intval($sql_arr['NUMROWS']);
+        }
+        else {
+            // search IMAP by object type
+            $filter = $this->_query2assoc($query);
+            $ctype  = kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
+            $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+            $count = $index->count();
+        }
+
+        return $count;
+    }
+
+
+    /**
+     * Helper method to compose a valid SQL query from pseudo filter triplets
+     */
+    private function _sql_where($query)
+    {
+        $sql_where = '';
+        foreach ($query as $param) {
+            $sql_where .= sprintf(' AND %s%s%s',
+                $this->db->quote_identifier($param[0]),
+                $param[1],
+                $this->db->quote($param[2])
+            );
+        }
+        return $sql_where;
+    }
+
+    /**
+     * Helper method to convert the given pseudo-query triplets into
+     * an associative filter array with 'equals' values only
+     */
+    private function _query2assoc($query)
+    {
+        // extract object type from query parameter
+        $filter = array();
+        foreach ($query as $param) {
+            if ($param[1] == '=')
+                $filter[$param[0]] = $param[2];
+        }
+        return $filter;
+    }
+
+    /**
+     * Fetch messages from IMAP
+     *
+     * @param array List of message UIDs to fetch
+     * @return array List of parsed Kolab objects
+     */
+    private function _fetch($index, $type = null, $folder = null)
+    {
+        $results = array();
+        foreach ((array)$index as $msguid) {
+            if ($object = $this->folder->read_object($msguid, $type, $folder)) {
+                $results[] = $object;
+                $this->uid2msg[$object['uid']] = $msguid;
+            }
+        }
+
+        return $results;
+    }
+
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     */
+    private function _serialize($object)
+    {
+        $bincols = array_flip($this->binary_cols);
+        $sql_data = array('dtstart' => null, 'dtend' => null, 'xml' => '');
+
+        // set type specific values
+        if ($this->folder->type == 'event') {
+            // database runs in server's timetone so using date() is safe
+            $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+            $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
+        }
+
+        if ($object['_formatobj'])
+            $sql_data['xml'] = (string)$object['_formatobj']->write();
+
+        // extract object data
+        $data = array();
+        foreach ($object as $key => $val) {
+            if ($val === "" || $val === null) {
+                // skip empty properties
+                continue;
+            }
+            if (isset($bincols[$key])) {
+                $data[$key] = base64_encode($val);
+            }
+            else if ($key[0] != '_') {
+                $data[$key] = $val;
+            }
+            else if ($key == '_attachments') {
+                foreach ($val as $k => $att) {
+                    unset($att['content']);
+                    if ($att['id'])
+                        $data[$key][$k] = $att;
+                }
+            }
+        }
+
+        $sql_data['data'] = serialize($data);
+        return $sql_data;
+    }
+
+    /**
+     * Helper method to turn stored cache data into a valid storage object
+     */
+    private function _unserialize($sql_arr)
+    {
+        $object = unserialize($sql_arr['data']);
+
+        // decode binary properties
+        foreach ($this->binary_cols as $key) {
+            if (!empty($object[$key]))
+                $object[$key] = base64_decode($object[$key]);
+        }
+
+        // add meta data
+        $object['_type'] = $sql_arr['type'];
+        $object['_msguid'] = $sql_arr['msguid'];
+        $object['_mailbox'] = $this->folder->name;
+        $object['_formatobj'] = kolab_format::factory($sql_arr['type'], $sql_arr['xml']);
+
+        return $object;
+    }
+
+    /**
+     * Resolve an object UID into an IMAP message UID
+     *
+     * @param string  Kolab object UID
+     * @param boolean Include deleted objects
+     * @return int The resolved IMAP message UID
+     */
+    public function uid2msguid($uid, $deleted = false)
+    {
+        if (!isset($this->uid2msg[$uid])) {
+            // use IMAP SEARCH to get the right message
+            $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
+            $results = $index->get();
+            $this->uid2msg[$uid] = $results[0];
+        }
+
+        return $this->uid2msg[$uid];
+    }
+
+}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 73b6dbb..246e58b 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -40,11 +40,10 @@ class kolab_storage_folder
     public $type;
 
     private $type_annotation;
-    private $subpath;
     private $imap;
     private $info;
     private $owner;
-    private $objcache = array();
+    private $cache;
     private $uid2msg = array();
 
 
@@ -54,7 +53,8 @@ class kolab_storage_folder
     function __construct($name, $imap = null)
     {
         $this->imap = is_object($imap) ? $imap : rcube::get_instance()->get_storage();
-        $this->imap->set_options(array('skip_deleted' => false));
+        $this->imap->set_options(array('skip_deleted' => true));
+        $this->cache = new kolab_storage_cache($this);
         $this->set_folder($name);
     }
 
@@ -72,6 +72,8 @@ class kolab_storage_folder
         $metadata = $this->imap->get_metadata($this->name, array(kolab_storage::CTYPE_KEY));
         $this->type_annotation = $metadata[$this->name][kolab_storage::CTYPE_KEY];
         $this->type = reset(explode('.', $this->type_annotation));
+
+        $this->cache->set_folder($this);
     }
 
 
@@ -232,13 +234,12 @@ class kolab_storage_folder
     {
         if (!$type) $type = $this->type;
 
-        // search by object type
-        $ctype  = self::KTYPE_PREFIX . $type;
-        $index = $this->imap->search_once($this->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+        // TODO: synchronize cache first?
 
-        return $index->count();
+        return $this->cache->count(array(array('type','=',$type)));
     }
 
+
     /**
      * List all Kolab objects of the given type
      *
@@ -249,8 +250,15 @@ class kolab_storage_folder
     {
         if (!$type) $type = $this->type;
 
-        $ctype  = self::KTYPE_PREFIX . $type;
+        // synchronize caches
+        $this->cache->synchronize();
+
+        // fetch objects from cache
+        return $this->cache->select(array(array('type','=',$type)));
+
+/*
         $results = array();
+        $ctype  = self::KTYPE_PREFIX . $type;
 
         // use 'list' for folder's default objects
         if ($type == $this->type) {
@@ -272,6 +280,7 @@ class kolab_storage_folder
         // TODO: write $this->uid2msg to cache
 
         return $results;
+*/
     }
 
 
@@ -283,8 +292,11 @@ class kolab_storage_folder
      */
     public function get_object($uid)
     {
-        $msguid = $this->uid2msguid($uid);
-        if ($msguid && ($object = $this->read_object($msguid)))
+        // synchronize caches
+        $this->cache->synchronize();
+
+        $msguid = $this->cache->uid2msguid($uid);
+        if ($msguid && ($object = $this->cache->get($msguid)))
             return $object;
 
         return false;
@@ -303,10 +315,8 @@ class kolab_storage_folder
      */
     public function get_attachment($uid, $part, $mailbox = null)
     {
-        if ($msguid = ($mailbox ? $uid : $this->uid2msguid($uid))) {
-            if ($mailbox)
-                $this->imap->set_folder($mailbox);
-
+        if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
+            $this->imap->set_folder($mailbox ? $mailbox : $this->name);
             return $this->imap->get_message_part($msguid, $part);
         }
 
@@ -319,25 +329,23 @@ class kolab_storage_folder
      * the Kolab groupware object from it
      *
      * @param string The IMAP message UID to fetch
-     * @param string The object type expected
+     * @param string The object type expected (use wildcard '*' to accept all types)
      * @param string The folder name where the message is stored
      * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
      */
-    private function read_object($msguid, $type = null, $folder = null)
+    public function read_object($msguid, $type = null, $folder = null)
     {
         if (!$type) $type = $this->type;
         if (!$folder) $folder = $this->name;
-        $ctype = self::KTYPE_PREFIX . $type;
-
-        // requested message in local cache
-        if ($this->objcache[$msguid])
-            return $this->objcache[$msguid];
 
         $this->imap->set_folder($folder);
 
-        // check ctype header and abort on mismatch
         $headers = $this->imap->get_message_headers($msguid);
-        if ($headers->others['x-kolab-type'] != $ctype)
+        $object_type = substr($headers->others['x-kolab-type'], strlen(self::KTYPE_PREFIX));
+        $content_type  = self::KTYPE_PREFIX . $object_type;
+
+        // check object type header and abort on mismatch
+        if ($type != '*' && $object_type != $type)
             return false;
 
         $message = new rcube_message($msguid);
@@ -345,7 +353,7 @@ class kolab_storage_folder
 
         // get XML part
         foreach ((array)$message->attachments as $part) {
-            if (!$xml && ($part->mimetype == $ctype || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
+            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
                 $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
             }
             else if ($part->filename) {
@@ -368,14 +376,17 @@ class kolab_storage_folder
             return false;
         }
 
-        $format = kolab_format::factory($type);
+        $format = kolab_format::factory($object_type);
+
+        if (is_a($format, 'PEAR_Error'))
+            return false;
 
         // check kolab format version
-        if (strpos($xml, '<' . $type) !== false) {
+        if (strpos($xml, '<' . $object_type) !== false) {
             // old Kolab 2.0 format detected
-            $handler = Horde_Kolab_Format::factory('XML', $type);
-            if (is_object($handler) && is_a($handler, 'PEAR_Error')) {
-                continue;
+            $handler = class_exists('Horde_Kolab_Format') ? Horde_Kolab_Format::factory('XML', $object_type) : null;
+            if (!is_object($handler) || is_a($handler, 'PEAR_Error')) {
+                return false;
             }
 
             // XML-to-array
@@ -389,12 +400,12 @@ class kolab_storage_folder
 
         if ($format->is_valid()) {
             $object = $format->to_array();
+            $object['_type'] = $object_type;
             $object['_msguid'] = $msguid;
             $object['_mailbox'] = $this->name;
             $object['_attachments'] = array_merge((array)$object['_attachments'], $attachments);
             $object['_formatobj'] = $format;
 
-            $this->objcache[$msguid] = $object;
             return $object;
         }
 
@@ -416,8 +427,8 @@ class kolab_storage_folder
             $type = $this->type;
 
         // copy attachments from old message
-        if (!empty($object['_msguid']) && ($old = $this->read_object($object['_msguid'], $type, $object['_mailbox']))) {
-            foreach ($old['_attachments'] as $name => $att) {
+        if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
+            foreach ((array)$old['_attachments'] as $name => $att) {
                 if (!isset($object['_attachments'][$name])) {
                     $object['_attachments'][$name] = $old['_attachments'][$name];
                 }
@@ -435,13 +446,18 @@ class kolab_storage_folder
             // delete old message
             if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
                 $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
+                $this->cache->set($object['_msguid'], false, $object['_mailbox']);
             }
-            else if ($result && $uid && ($msguid = $this->uid2msguid($uid))) {
+            else if ($result && $uid && ($msguid = $this->cache->uid2msguid($uid))) {
                 $this->imap->delete_message($msguid, $this->name);
+                $this->cache->set($object['_msguid'], false);
             }
 
-            // TODO: update cache with new UID
-            $this->uid2msg[$object['uid']] = $result;
+            // update cache with new UID
+            if ($result) {
+                $object['_msguid'] = $result;
+                $this->cache->set($result, $object);
+            }
         }
         
         return $result;
@@ -459,16 +475,21 @@ class kolab_storage_folder
      */
     public function delete($object, $expunge = true, $trigger = true)
     {
-        $msguid = is_array($object) ? $object['_msguid'] : $this->uid2msguid($object);
+        $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
+        $success = false;
 
         if ($msguid && $expunge) {
-            return $this->imap->delete_message($msguid, $this->name);
+            $success = $this->imap->delete_message($msguid, $this->name);
         }
         else if ($msguid) {
-            return $this->imap->set_flag($msguid, 'DELETED', $this->name);
+            $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
         }
 
-        return false;
+        if ($success) {
+            $this->cache->set($result, false);
+        }
+
+        return $success;
     }
 
 
@@ -477,6 +498,7 @@ class kolab_storage_folder
      */
     public function delete_all()
     {
+        $this->cache->purge();
         return $this->imap->clear_folder($this->name);
     }
 
@@ -489,7 +511,7 @@ class kolab_storage_folder
      */
     public function undelete($uid)
     {
-        if ($msguid = $this->uid2msguid($uid, true)) {
+        if ($msguid = $this->cache->uid2msguid($uid, true)) {
             if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
                 return $msguid;
             }
@@ -508,7 +530,7 @@ class kolab_storage_folder
      */
     public function move($uid, $target_folder)
     {
-        if ($msguid = $this->uid2msguid($uid)) {
+        if ($msguid = $this->cache->uid2msguid($uid)) {
             if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) {
                 // TODO: update cache
                 return true;
@@ -527,24 +549,6 @@ class kolab_storage_folder
 
 
     /**
-     * Resolve an object UID into an IMAP message UID
-     */
-    private function uid2msguid($uid, $deleted = false)
-    {
-        if (!isset($this->uid2msg[$uid])) {
-            // use IMAP SEARCH to get the right message
-            $index = $this->imap->search_once($this->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
-            $results = $index->get();
-            $this->uid2msg[$uid] = $results[0];
-
-            // TODO: cache this lookup
-        }
-
-        return $this->uid2msg[$uid];
-    }
-
-
-    /**
      * Creates source of the configuration object message
      */
     private function build_message(&$object, $type)
@@ -552,7 +556,7 @@ class kolab_storage_folder
         // load old object to preserve data we don't understand/process
         if (is_object($object['_formatobj']))
             $format = $object['_formatobj'];
-        else if ($object['_msguid'] && ($old = $this->read_object($object['_msguid'], $type, $object['_mailbox'])))
+        else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
             $format = $old['_formatobj'];
 
         // create new kolab_format instance


commit 63015a802d16d6085972b98745cc904cd9cf0ede
Author: Thomas B <roundcube at gmail.com>
Date:   Wed May 2 17:13:06 2012 +0200

    Adapt contact saving code to new kolab_storage backend

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 7695402..37ba1f1 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1087,6 +1087,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                     $contact['website'][] = array('url' => $url, 'type' => $type);
                 }
             }
+            unset($contact['website:'.$type]);
         }
 
         foreach ($this->get_col_values('phone', $contact) as $type => $values) {
@@ -1095,6 +1096,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                     $contact['phone'][] = array('number' => $phone, 'type' => $type);
                 }
             }
+            unset($contact['phone:'.$type]);
         }
 
         $addresses = array();
@@ -1119,20 +1121,6 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
         $contact['address'] = $addresses;
 
-        // save new photo as attachment
-        if ($contact['photo']) {
-          $attkey = 'photo.attachment';
-          $contact['_attachments'][$attkey] = array(
-            'mimetype' => rc_image_content_type($contact['photo']),
-            'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
-          );
-          $contact['photo'] = $attkey;
-        }
-        else if (isset($contact['photo']) && empty($contact['photo'])) {
-            // unset photo attachment
-            $contact['_attachments']['photo.attachment'] = false;
-        }
-
         // copy meta data (starting with _) from old object
         foreach ((array)$old as $key => $val) {
             if (!isset($contact[$key]) && $key[0] == '_')
@@ -1140,7 +1128,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
 
         // add empty values for some fields which can be removed in the UI
-        return $contact + array('nickname' => '', 'birthday' => '', 'anniversary' => '', 'freebusyurl' => '');
+        return array_filter($contact) + array('nickname' => '', 'birthday' => '', 'anniversary' => '', 'freebusyurl' => '');
     }
 
 }


commit 8c90898a061fe7b58736325eb884e0e8213d36de
Author: Thomas B <roundcube at gmail.com>
Date:   Wed May 2 17:10:53 2012 +0200

    Try loading Horde classes from default location first

diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
index dd61372..5aa5195 100644
--- a/plugins/calendar/lib/calendar_ical.php
+++ b/plugins/calendar/lib/calendar_ical.php
@@ -124,7 +124,10 @@ class calendar_ical
   private function get_parser()
   {
     // use Horde:iCalendar to parse vcalendar file format
-    require_once($this->cal->home . '/lib/Horde_iCalendar.php');
+    @include_once('Horde/iCalendar.php');
+
+    if (!class_exists('Horde_iCalendar'))
+      require_once($this->cal->home . '/lib/Horde_iCalendar.php');
 
     // set target charset for parsed events
     $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;


commit f21b94107030e55a3f14cb3bc7269972e72f9418
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 1 22:34:51 2012 +0200

    Fix comments in Horde_* file headers

diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
index 18709d9..d710d72 100644
--- a/plugins/calendar/lib/Horde_Date.php
+++ b/plugins/calendar/lib/Horde_Date.php
@@ -1,8 +1,7 @@
 <?php
 
 /**
- * This is a concatenated copy of the following files:
- *   Horde/Date.php, PEAR/Date/Calc.php, Horde/Date/Recurrence.php
+ * This is a copy of the Horde/Date.php class from the Horde framework
  */
 
 define('HORDE_DATE_SUNDAY',    0);
diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php
index fbf1d1e..379d54a 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -1,5 +1,10 @@
 <?php
 
+/**
+ * This is a concatenated copy of the following files:
+ *   PEAR/Date/Calc.php, Horde/Date/Recurrence.php
+ */
+
 require_once(dirname(__FILE__) . '/Horde_Date.php');
 
 // {{{ Header
diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php
index 5ba0f4c..f898170 100644
--- a/plugins/calendar/lib/Horde_iCalendar.php
+++ b/plugins/calendar/lib/Horde_iCalendar.php
@@ -1,5 +1,10 @@
 <?php
 
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ */
+
 require_once(dirname(__FILE__) . '/Horde_Date.php');
 
 
diff --git a/plugins/calendar/lib/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh
index d076af5..07b7608 100755
--- a/plugins/calendar/lib/get_horde_icalendar.sh
+++ b/plugins/calendar/lib/get_horde_icalendar.sh
@@ -11,8 +11,14 @@ if [ ! -d "$SRCDIR" ]; then
   exit 1
 fi
 
-echo "<?php\n"
-echo "require_once(dirname(__FILE__) . '/Horde_Date.php');"
+echo "<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ */
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');"
 
 sed 's/<?php//; s/?>//' $SRCDIR/String.php
 echo "\n"


commit d4ce6a729abb12efa924f7fbd8f763b35c46cb48
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 1 22:28:33 2012 +0200

    Added README file

diff --git a/plugins/calendar/README b/plugins/calendar/README
new file mode 100644
index 0000000..e5a38ee
--- /dev/null
+++ b/plugins/calendar/README
@@ -0,0 +1,18 @@
+A calendar module for Roundcube
+-------------------------------
+
+This plugin currently supports a local database as well as a Kolab groupware
+server as backends for calendar and event storage. For both drivers, some
+initialization of the local database is necessary. To do so, execute the
+SQL commands in drivers/<yourchoice>/SQL/<yourdatabase>.sql
+
+The client-side calendar UI relies on the 'fullcalenda'r project by Adam Arshaw
+with extensions made for the use in Roundcube. All changes are published in
+an official fork at https://github.com/roundcube/fullcalendar
+
+For recurring event computation, some utility classes from the Horde project
+are used. They are packaged in a slightly modified version with this plugin.
+
+iCalendar parsing is done with the help of the Horde_iCalendar class. A copy
+of that class with all its dependencies is part of this package. In order
+to update it, execute lib/get_horde_icalendar.sh > lib/Horde_iCalendar.php


commit 584c4c9bc456eb99eebe24410d10e3169abb04cd
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 1 22:27:29 2012 +0200

    Drop dependency to external Horde libs. Add a local and independent copy of these files to the plugin package

diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
new file mode 100644
index 0000000..18709d9
--- /dev/null
+++ b/plugins/calendar/lib/Horde_Date.php
@@ -0,0 +1,775 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/Date.php, PEAR/Date/Calc.php, Horde/Date/Recurrence.php
+ */
+
+define('HORDE_DATE_SUNDAY',    0);
+define('HORDE_DATE_MONDAY',    1);
+define('HORDE_DATE_TUESDAY',   2);
+define('HORDE_DATE_WEDNESDAY', 3);
+define('HORDE_DATE_THURSDAY',  4);
+define('HORDE_DATE_FRIDAY',    5);
+define('HORDE_DATE_SATURDAY',  6);
+
+define('HORDE_DATE_MASK_SUNDAY',    1);
+define('HORDE_DATE_MASK_MONDAY',    2);
+define('HORDE_DATE_MASK_TUESDAY',   4);
+define('HORDE_DATE_MASK_WEDNESDAY', 8);
+define('HORDE_DATE_MASK_THURSDAY', 16);
+define('HORDE_DATE_MASK_FRIDAY',   32);
+define('HORDE_DATE_MASK_SATURDAY', 64);
+define('HORDE_DATE_MASK_WEEKDAYS', 62);
+define('HORDE_DATE_MASK_WEEKEND',  65);
+define('HORDE_DATE_MASK_ALLDAYS', 127);
+
+define('HORDE_DATE_MASK_SECOND',    1);
+define('HORDE_DATE_MASK_MINUTE',    2);
+define('HORDE_DATE_MASK_HOUR',      4);
+define('HORDE_DATE_MASK_DAY',       8);
+define('HORDE_DATE_MASK_MONTH',    16);
+define('HORDE_DATE_MASK_YEAR',     32);
+define('HORDE_DATE_MASK_ALLPARTS', 63);
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * $Horde: framework/Date/Date.php,v 1.8.10.18 2008/09/17 08:46:04 jan Exp $
+ *
+ * @package Horde_Date
+ */
+class Horde_Date {
+
+    /**
+     * Year
+     *
+     * @var integer
+     */
+    var $year;
+
+    /**
+     * Month
+     *
+     * @var integer
+     */
+    var $month;
+
+    /**
+     * Day
+     *
+     * @var integer
+     */
+    var $mday;
+
+    /**
+     * Hour
+     *
+     * @var integer
+     */
+    var $hour = 0;
+
+    /**
+     * Minute
+     *
+     * @var integer
+     */
+    var $min = 0;
+
+    /**
+     * Second
+     *
+     * @var integer
+     */
+    var $sec = 0;
+
+    /**
+     * Internally supported strftime() specifiers.
+     *
+     * @var string
+     */
+    var $_supportedSpecs = '%CdDeHImMnRStTyY';
+
+    /**
+     * Build a new date object. If $date contains date parts, use them to
+     * initialize the object.
+     *
+     * Recognized formats:
+     * - arrays with keys 'year', 'month', 'mday', 'day' (since Horde 3.2),
+     *   'hour', 'min', 'minute' (since Horde 3.2), 'sec'
+     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+     * - yyyy-mm-dd hh:mm:ss (since Horde 3.1)
+     * - yyyymmddhhmmss (since Horde 3.1)
+     * - yyyymmddThhmmssZ (since Horde 3.1.4)
+     * - unix timestamps
+     */
+    function Horde_Date($date = null)
+    {
+        if (function_exists('nl_langinfo')) {
+            $this->_supportedSpecs .= 'bBpxX';
+        }
+
+        if (is_array($date) || is_object($date)) {
+            foreach ($date as $key => $val) {
+                if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+                    $this->$key = (int)$val;
+                }
+            }
+
+            // If $date['day'] is present and numeric we may have been passed
+            // a Horde_Form_datetime array.
+            if (is_array($date) && isset($date['day']) &&
+                is_numeric($date['day'])) {
+                $this->mday = (int)$date['day'];
+            }
+            // 'minute' key also from Horde_Form_datetime
+            if (is_array($date) && isset($date['minute'])) {
+                $this->min = $date['minute'];
+            }
+        } elseif (!is_null($date)) {
+            // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'.
+            if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})Z?/', $date, $parts)) {
+                $this->year = (int)$parts[1];
+                $this->month = (int)$parts[2];
+                $this->mday = (int)$parts[3];
+                $this->hour = (int)$parts[4];
+                $this->min = (int)$parts[5];
+                $this->sec = (int)$parts[6];
+            } else {
+                // Try as a timestamp.
+                $parts = @getdate($date);
+                if ($parts) {
+                    $this->year = $parts['year'];
+                    $this->month = $parts['mon'];
+                    $this->mday = $parts['mday'];
+                    $this->hour = $parts['hours'];
+                    $this->min = $parts['minutes'];
+                    $this->sec = $parts['seconds'];
+                }
+            }
+        }
+    }
+
+    /**
+     * @static
+     */
+    function isLeapYear($year)
+    {
+        if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+            return false;
+        }
+
+        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+    }
+
+    /**
+     * Returns the day of the year (1-366) that corresponds to the
+     * first day of the given week.
+     *
+     * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php
+     *
+     * @param integer $week  The week of the year to find the first day of.
+     * @param integer $year  The year to calculate for.
+     *
+     * @return integer  The day of the year of the first day of the given week.
+     */
+    function firstDayOfWeek($week, $year)
+    {
+        $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1));
+        $start = $jan1->dayOfWeek();
+        if ($start > HORDE_DATE_THURSDAY) {
+            $start -= 7;
+        }
+        return (($week * 7) - (7 + $start)) + 1;
+    }
+
+    /**
+     * @static
+     */
+    function daysInMonth($month, $year)
+    {
+        if ($month == 2) {
+            if (Horde_Date::isLeapYear($year)) {
+                return 29;
+            } else {
+                return 28;
+            }
+        } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
+            return 30;
+        } else {
+            return 31;
+        }
+    }
+
+    /**
+     * Return the day of the week (0 = Sunday, 6 = Saturday) of this
+     * object's date.
+     *
+     * @return integer  The day of the week.
+     */
+    function dayOfWeek()
+    {
+        if ($this->month > 2) {
+            $month = $this->month - 2;
+            $year = $this->year;
+        } else {
+            $month = $this->month + 10;
+            $year = $this->year - 1;
+        }
+
+        $day = (floor((13 * $month - 1) / 5) +
+                $this->mday + ($year % 100) +
+                floor(($year % 100) / 4) +
+                floor(($year / 100) / 4) - 2 *
+                floor($year / 100) + 77);
+
+        return (int)($day - 7 * floor($day / 7));
+    }
+
+    /**
+     * Returns the day number of the year (1 to 365/366).
+     *
+     * @return integer  The day of the year.
+     */
+    function dayOfYear()
+    {
+        $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
+        $dayOfYear = $this->mday + $monthTotals[$this->month - 1];
+        if (Horde_Date::isLeapYear($this->year) && $this->month > 2) {
+            ++$dayOfYear;
+        }
+
+        return $dayOfYear;
+    }
+
+    /**
+     * Returns the week of the month.
+     *
+     * @since Horde 3.2
+     *
+     * @return integer  The week number.
+     */
+    function weekOfMonth()
+    {
+        return ceil($this->mday / 7);
+    }
+
+    /**
+     * Returns the week of the year, first Monday is first day of first week.
+     *
+     * @return integer  The week number.
+     */
+    function weekOfYear()
+    {
+        return $this->format('W');
+    }
+
+    /**
+     * Return the number of weeks in the given year (52 or 53).
+     *
+     * @static
+     *
+     * @param integer $year  The year to count the number of weeks in.
+     *
+     * @return integer $numWeeks   The number of weeks in $year.
+     */
+    function weeksInYear($year)
+    {
+        // Find the last Thursday of the year.
+        $day = 31;
+        $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0));
+        while ($date->dayOfWeek() != HORDE_DATE_THURSDAY) {
+            --$date->mday;
+        }
+        return $date->weekOfYear();
+    }
+
+    /**
+     * Set the date of this object to the $nth weekday of $weekday.
+     *
+     * @param integer $weekday  The day of the week (0 = Sunday, etc).
+     * @param integer $nth      The $nth $weekday to set to (defaults to 1).
+     */
+    function setNthWeekday($weekday, $nth = 1)
+    {
+        if ($weekday < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) {
+            return false;
+        }
+
+        $this->mday = 1;
+        $first = $this->dayOfWeek();
+        if ($weekday < $first) {
+            $this->mday = 8 + $weekday - $first;
+        } else {
+            $this->mday = $weekday - $first + 1;
+        }
+        $this->mday += 7 * $nth - 7;
+
+        $this->correct();
+
+        return true;
+    }
+
+    function dump($prefix = '')
+    {
+        echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "<br />\n";
+    }
+
+    /**
+     * Is the date currently represented by this object a valid date?
+     *
+     * @return boolean  Validity, counting leap years, etc.
+     */
+    function isValid()
+    {
+        if ($this->year < 0 || $this->year > 9999) {
+            return false;
+        }
+        return checkdate($this->month, $this->mday, $this->year);
+    }
+
+    /**
+     * Correct any over- or underflows in any of the date's members.
+     *
+     * @param integer $mask  We may not want to correct some overflows.
+     */
+    function correct($mask = HORDE_DATE_MASK_ALLPARTS)
+    {
+        if ($mask & HORDE_DATE_MASK_SECOND) {
+            while ($this->sec < 0) {
+                --$this->min;
+                $this->sec += 60;
+            }
+            while ($this->sec > 59) {
+                ++$this->min;
+                $this->sec -= 60;
+            }
+        }
+
+        if ($mask & HORDE_DATE_MASK_MINUTE) {
+            while ($this->min < 0) {
+                --$this->hour;
+                $this->min += 60;
+            }
+            while ($this->min > 59) {
+                ++$this->hour;
+                $this->min -= 60;
+            }
+        }
+
+        if ($mask & HORDE_DATE_MASK_HOUR) {
+            while ($this->hour < 0) {
+                --$this->mday;
+                $this->hour += 24;
+            }
+            while ($this->hour > 23) {
+                ++$this->mday;
+                $this->hour -= 24;
+            }
+        }
+
+        if ($mask & HORDE_DATE_MASK_MONTH) {
+            while ($this->month > 12) {
+                ++$this->year;
+                $this->month -= 12;
+            }
+            while ($this->month < 1) {
+                --$this->year;
+                $this->month += 12;
+            }
+        }
+
+        if ($mask & HORDE_DATE_MASK_DAY) {
+            while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) {
+                $this->mday -= Horde_Date::daysInMonth($this->month, $this->year);
+                ++$this->month;
+                $this->correct(HORDE_DATE_MASK_MONTH);
+            }
+            while ($this->mday < 1) {
+                --$this->month;
+                $this->correct(HORDE_DATE_MASK_MONTH);
+                $this->mday += Horde_Date::daysInMonth($this->month, $this->year);
+            }
+        }
+    }
+
+    /**
+     * Compare this date to another date object to see which one is
+     * greater (later). Assumes that the dates are in the same
+     * timezone.
+     *
+     * @param mixed $date  The date to compare to.
+     *
+     * @return integer  ==  0 if the dates are equal
+     *                  >=  1 if this date is greater (later)
+     *                  <= -1 if the other date is greater (later)
+     */
+    function compareDate($date)
+    {
+        if (!is_object($date) || !is_a($date, 'Horde_Date')) {
+            $date = new Horde_Date($date);
+        }
+
+        if ($this->year != $date->year) {
+            return $this->year - $date->year;
+        }
+        if ($this->month != $date->month) {
+            return $this->month - $date->month;
+        }
+
+        return $this->mday - $date->mday;
+    }
+
+    /**
+     * Compare this to another date object by time, to see which one
+     * is greater (later). Assumes that the dates are in the same
+     * timezone.
+     *
+     * @param mixed $date  The date to compare to.
+     *
+     * @return integer  ==  0 if the dates are equal
+     *                  >=  1 if this date is greater (later)
+     *                  <= -1 if the other date is greater (later)
+     */
+    function compareTime($date)
+    {
+        if (!is_object($date) || !is_a($date, 'Horde_Date')) {
+            $date = new Horde_Date($date);
+        }
+
+        if ($this->hour != $date->hour) {
+            return $this->hour - $date->hour;
+        }
+        if ($this->min != $date->min) {
+            return $this->min - $date->min;
+        }
+
+        return $this->sec - $date->sec;
+    }
+
+    /**
+     * Compare this to another date object, including times, to see
+     * which one is greater (later). Assumes that the dates are in the
+     * same timezone.
+     *
+     * @param mixed $date  The date to compare to.
+     *
+     * @return integer  ==  0 if the dates are equal
+     *                  >=  1 if this date is greater (later)
+     *                  <= -1 if the other date is greater (later)
+     */
+    function compareDateTime($date)
+    {
+        if (!is_object($date) || !is_a($date, 'Horde_Date')) {
+            $date = new Horde_Date($date);
+        }
+
+        if ($diff = $this->compareDate($date)) {
+            return $diff;
+        }
+
+        return $this->compareTime($date);
+    }
+
+    /**
+     * Get the time offset for local time zone.
+     *
+     * @param boolean $colon  Place a colon between hours and minutes?
+     *
+     * @return string  Timezone offset as a string in the format +HH:MM.
+     */
+    function tzOffset($colon = true)
+    {
+        $secs = $this->format('Z');
+
+        if ($secs < 0) {
+            $sign = '-';
+            $secs = -$secs;
+        } else {
+            $sign = '+';
+        }
+        $colon = $colon ? ':' : '';
+        $mins = intval(($secs + 30) / 60);
+        return sprintf('%s%02d%s%02d',
+                       $sign, $mins / 60, $colon, $mins % 60);
+    }
+
+    /**
+     * Return the unix timestamp representation of this date.
+     *
+     * @return integer  A unix timestamp.
+     */
+    function timestamp()
+    {
+        if (class_exists('DateTime')) {
+            return $this->format('U');
+        } else {
+            return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year);
+        }
+    }
+
+    /**
+     * Return the unix timestamp representation of this date, 12:00am.
+     *
+     * @return integer  A unix timestamp.
+     */
+    function datestamp()
+    {
+        if (class_exists('DateTime')) {
+            $dt = new DateTime();
+            $dt->setDate($this->year, $this->month, $this->mday);
+            $dt->setTime(0, 0, 0);
+            return $dt->format('U');
+        } else {
+            return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year);
+        }
+    }
+
+    /**
+     * Format time using the specifiers available in date() or in the DateTime
+     * class' format() method.
+     *
+     * @since Horde 3.3
+     *
+     * @param string $format
+     *
+     * @return string  Formatted time.
+     */
+    function format($format)
+    {
+        if (class_exists('DateTime')) {
+            $dt = new DateTime();
+            $dt->setDate($this->year, $this->month, $this->mday);
+            $dt->setTime($this->hour, $this->min, $this->sec);
+            return $dt->format($format);
+        } else {
+            return date($format, $this->timestamp());
+        }
+    }
+
+    /**
+     * Format time in ISO-8601 format. Works correctly since Horde 3.2.
+     *
+     * @return string  Date and time in ISO-8601 format.
+     */
+    function iso8601DateTime()
+    {
+        return $this->rfc3339DateTime() . $this->tzOffset();
+    }
+
+    /**
+     * Format time in RFC 2822 format.
+     *
+     * @return string  Date and time in RFC 2822 format.
+     */
+    function rfc2822DateTime()
+    {
+        return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false);
+    }
+
+    /**
+     * Format time in RFC 3339 format.
+     *
+     * @since Horde 3.1
+     *
+     * @return string  Date and time in RFC 3339 format. The seconds part has
+     *                 been added with Horde 3.2.
+     */
+    function rfc3339DateTime()
+    {
+        return $this->format('Y-m-d\TH:i:s');
+    }
+
+    /**
+     * Format time to standard 'ctime' format.
+     *
+     * @return string  Date and time.
+     */
+    function cTime()
+    {
+        return $this->format('D M j H:i:s Y');
+    }
+
+    /**
+     * Format date and time using strftime() format.
+     *
+     * @since Horde 3.1
+     *
+     * @return string  strftime() formatted date and time.
+     */
+    function strftime($format)
+    {
+        if (preg_match('/%[^' . $this->_supportedSpecs . ']/', $format)) {
+            return strftime($format, $this->timestamp());
+        } else {
+            return $this->_strftime($format);
+        }
+    }
+
+    /**
+     * Format date and time using a limited set of the strftime() format.
+     *
+     * @return string  strftime() formatted date and time.
+     */
+    function _strftime($format)
+    {
+        if (preg_match('/%[bBpxX]/', $format)) {
+            require_once 'Horde/NLS.php';
+        }
+
+        return preg_replace(
+            array('/%b/e',
+                  '/%B/e',
+                  '/%C/e',
+                  '/%d/e',
+                  '/%D/e',
+                  '/%e/e',
+                  '/%H/e',
+                  '/%I/e',
+                  '/%m/e',
+                  '/%M/e',
+                  '/%n/',
+                  '/%p/e',
+                  '/%R/e',
+                  '/%S/e',
+                  '/%t/',
+                  '/%T/e',
+                  '/%x/e',
+                  '/%X/e',
+                  '/%y/e',
+                  '/%Y/',
+                  '/%%/'),
+            array('$this->_strftime(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->month)))',
+                  '$this->_strftime(NLS::getLangInfo(constant(\'MON_\' . (int)$this->month)))',
+                  '(int)($this->year / 100)',
+                  'sprintf(\'%02d\', $this->mday)',
+                  '$this->_strftime(\'%m/%d/%y\')',
+                  'sprintf(\'%2d\', $this->mday)',
+                  'sprintf(\'%02d\', $this->hour)',
+                  'sprintf(\'%02d\', $this->hour == 0 ? 12 : ($this->hour > 12 ? $this->hour - 12 : $this->hour))',
+                  'sprintf(\'%02d\', $this->month)',
+                  'sprintf(\'%02d\', $this->min)',
+                  "\n",
+                  '$this->_strftime(NLS::getLangInfo($this->hour < 12 ? AM_STR : PM_STR))',
+                  '$this->_strftime(\'%H:%M\')',
+                  'sprintf(\'%02d\', $this->sec)',
+                  "\t",
+                  '$this->_strftime(\'%H:%M:%S\')',
+                  '$this->_strftime(NLS::getLangInfo(D_FMT))',
+                  '$this->_strftime(NLS::getLangInfo(T_FMT))',
+                  'substr(sprintf(\'%04d\', $this->year), -2)',
+                  (int)$this->year,
+                  '%'),
+            $format);
+    }
+
+    /**
+     * mktime() implementation that supports dates outside of 1970-2038,
+     * from http://phplens.com/phpeverywhere/adodb_date_library.
+     *
+     * @TODO remove in Horde 4
+     *
+     * This does NOT work with pre-1970 daylight saving times.
+     *
+     * @static
+     */
+    function _mktime($hr, $min, $sec, $mon = false, $day = false,
+                     $year = false, $is_dst = false, $is_gmt = false)
+    {
+        if ($mon === false) {
+            return $is_gmt
+                ? @gmmktime($hr, $min, $sec)
+                : @mktime($hr, $min, $sec);
+        }
+
+        if ($year > 1901 && $year < 2038 &&
+            ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) {
+            return $is_gmt
+                ? @gmmktime($hr, $min, $sec, $mon, $day, $year)
+                : @mktime($hr, $min, $sec, $mon, $day, $year);
+        }
+
+        $gmt_different = $is_gmt
+            ? 0
+            : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0));
+
+        $mon = intval($mon);
+        $day = intval($day);
+        $year = intval($year);
+
+        if ($mon > 12) {
+            $y = floor($mon / 12);
+            $year += $y;
+            $mon -= $y * 12;
+        } elseif ($mon < 1) {
+            $y = ceil((1 - $mon) / 12);
+            $year -= $y;
+            $mon += $y * 12;
+        }
+
+        $_day_power = 86400;
+        $_hour_power = 3600;
+        $_min_power = 60;
+
+        $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+        $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+
+        $_total_date = 0;
+        if ($year >= 1970) {
+            for ($a = 1970; $a <= $year; $a++) {
+                $leaf = Horde_Date::isLeapYear($a);
+                if ($leaf == true) {
+                    $loop_table = $_month_table_leaf;
+                    $_add_date = 366;
+                } else {
+                    $loop_table = $_month_table_normal;
+                    $_add_date = 365;
+                }
+                if ($a < $year) {
+                    $_total_date += $_add_date;
+                } else {
+                    for ($b = 1; $b < $mon; $b++) {
+                        $_total_date += $loop_table[$b];
+                    }
+                }
+            }
+
+            return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different;
+        }
+
+        for ($a = 1969 ; $a >= $year; $a--) {
+            $leaf = Horde_Date::isLeapYear($a);
+            if ($leaf == true) {
+                $loop_table = $_month_table_leaf;
+                $_add_date = 366;
+            } else {
+                $loop_table = $_month_table_normal;
+                $_add_date = 365;
+            }
+            if ($a > $year) {
+                $_total_date += $_add_date;
+            } else {
+                for ($b = 12; $b > $mon; $b--) {
+                    $_total_date += $loop_table[$b];
+                }
+            }
+        }
+
+        $_total_date += $loop_table[$mon] - $day;
+        $_day_time = $hr * $_hour_power + $min * $_min_power + $sec;
+        $_day_time = $_day_power - $_day_time;
+        $ret = -($_total_date * $_day_power + $_day_time - $gmt_different);
+        if ($ret < -12220185600) {
+            // If earlier than 5 Oct 1582 - gregorian correction.
+            return $ret + 10 * 86400;
+        } elseif ($ret < -12219321600) {
+            // If in limbo, reset to 15 Oct 1582.
+            return -12219321600;
+        } else {
+            return $ret;
+        }
+    }
+
+}
+
diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php
index 68340ba..fbf1d1e 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -1,780 +1,6 @@
 <?php
 
-/**
- * This is a concatenated copy of the following files:
- *   Horde/Date.php, PEAR/Date/Calc.php, Horde/Date/Recurrence.php
- */
-
-define('HORDE_DATE_SUNDAY',    0);
-define('HORDE_DATE_MONDAY',    1);
-define('HORDE_DATE_TUESDAY',   2);
-define('HORDE_DATE_WEDNESDAY', 3);
-define('HORDE_DATE_THURSDAY',  4);
-define('HORDE_DATE_FRIDAY',    5);
-define('HORDE_DATE_SATURDAY',  6);
-
-define('HORDE_DATE_MASK_SUNDAY',    1);
-define('HORDE_DATE_MASK_MONDAY',    2);
-define('HORDE_DATE_MASK_TUESDAY',   4);
-define('HORDE_DATE_MASK_WEDNESDAY', 8);
-define('HORDE_DATE_MASK_THURSDAY', 16);
-define('HORDE_DATE_MASK_FRIDAY',   32);
-define('HORDE_DATE_MASK_SATURDAY', 64);
-define('HORDE_DATE_MASK_WEEKDAYS', 62);
-define('HORDE_DATE_MASK_WEEKEND',  65);
-define('HORDE_DATE_MASK_ALLDAYS', 127);
-
-define('HORDE_DATE_MASK_SECOND',    1);
-define('HORDE_DATE_MASK_MINUTE',    2);
-define('HORDE_DATE_MASK_HOUR',      4);
-define('HORDE_DATE_MASK_DAY',       8);
-define('HORDE_DATE_MASK_MONTH',    16);
-define('HORDE_DATE_MASK_YEAR',     32);
-define('HORDE_DATE_MASK_ALLPARTS', 63);
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * $Horde: framework/Date/Date.php,v 1.8.10.18 2008/09/17 08:46:04 jan Exp $
- *
- * @package Horde_Date
- */
-class Horde_Date {
-
-    /**
-     * Year
-     *
-     * @var integer
-     */
-    var $year;
-
-    /**
-     * Month
-     *
-     * @var integer
-     */
-    var $month;
-
-    /**
-     * Day
-     *
-     * @var integer
-     */
-    var $mday;
-
-    /**
-     * Hour
-     *
-     * @var integer
-     */
-    var $hour = 0;
-
-    /**
-     * Minute
-     *
-     * @var integer
-     */
-    var $min = 0;
-
-    /**
-     * Second
-     *
-     * @var integer
-     */
-    var $sec = 0;
-
-    /**
-     * Internally supported strftime() specifiers.
-     *
-     * @var string
-     */
-    var $_supportedSpecs = '%CdDeHImMnRStTyY';
-
-    /**
-     * Build a new date object. If $date contains date parts, use them to
-     * initialize the object.
-     *
-     * Recognized formats:
-     * - arrays with keys 'year', 'month', 'mday', 'day' (since Horde 3.2),
-     *   'hour', 'min', 'minute' (since Horde 3.2), 'sec'
-     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
-     * - yyyy-mm-dd hh:mm:ss (since Horde 3.1)
-     * - yyyymmddhhmmss (since Horde 3.1)
-     * - yyyymmddThhmmssZ (since Horde 3.1.4)
-     * - unix timestamps
-     */
-    function Horde_Date($date = null)
-    {
-        if (function_exists('nl_langinfo')) {
-            $this->_supportedSpecs .= 'bBpxX';
-        }
-
-        if (is_array($date) || is_object($date)) {
-            foreach ($date as $key => $val) {
-                if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
-                    $this->$key = (int)$val;
-                }
-            }
-
-            // If $date['day'] is present and numeric we may have been passed
-            // a Horde_Form_datetime array.
-            if (is_array($date) && isset($date['day']) &&
-                is_numeric($date['day'])) {
-                $this->mday = (int)$date['day'];
-            }
-            // 'minute' key also from Horde_Form_datetime
-            if (is_array($date) && isset($date['minute'])) {
-                $this->min = $date['minute'];
-            }
-        } elseif (!is_null($date)) {
-            // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'.
-            if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})Z?/', $date, $parts)) {
-                $this->year = (int)$parts[1];
-                $this->month = (int)$parts[2];
-                $this->mday = (int)$parts[3];
-                $this->hour = (int)$parts[4];
-                $this->min = (int)$parts[5];
-                $this->sec = (int)$parts[6];
-            } else {
-                // Try as a timestamp.
-                $parts = @getdate($date);
-                if ($parts) {
-                    $this->year = $parts['year'];
-                    $this->month = $parts['mon'];
-                    $this->mday = $parts['mday'];
-                    $this->hour = $parts['hours'];
-                    $this->min = $parts['minutes'];
-                    $this->sec = $parts['seconds'];
-                }
-            }
-        }
-    }
-
-    /**
-     * @static
-     */
-    function isLeapYear($year)
-    {
-        if (strlen($year) != 4 || preg_match('/\D/', $year)) {
-            return false;
-        }
-
-        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
-    }
-
-    /**
-     * Returns the day of the year (1-366) that corresponds to the
-     * first day of the given week.
-     *
-     * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php
-     *
-     * @param integer $week  The week of the year to find the first day of.
-     * @param integer $year  The year to calculate for.
-     *
-     * @return integer  The day of the year of the first day of the given week.
-     */
-    function firstDayOfWeek($week, $year)
-    {
-        $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1));
-        $start = $jan1->dayOfWeek();
-        if ($start > HORDE_DATE_THURSDAY) {
-            $start -= 7;
-        }
-        return (($week * 7) - (7 + $start)) + 1;
-    }
-
-    /**
-     * @static
-     */
-    function daysInMonth($month, $year)
-    {
-        if ($month == 2) {
-            if (Horde_Date::isLeapYear($year)) {
-                return 29;
-            } else {
-                return 28;
-            }
-        } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
-            return 30;
-        } else {
-            return 31;
-        }
-    }
-
-    /**
-     * Return the day of the week (0 = Sunday, 6 = Saturday) of this
-     * object's date.
-     *
-     * @return integer  The day of the week.
-     */
-    function dayOfWeek()
-    {
-        if ($this->month > 2) {
-            $month = $this->month - 2;
-            $year = $this->year;
-        } else {
-            $month = $this->month + 10;
-            $year = $this->year - 1;
-        }
-
-        $day = (floor((13 * $month - 1) / 5) +
-                $this->mday + ($year % 100) +
-                floor(($year % 100) / 4) +
-                floor(($year / 100) / 4) - 2 *
-                floor($year / 100) + 77);
-
-        return (int)($day - 7 * floor($day / 7));
-    }
-
-    /**
-     * Returns the day number of the year (1 to 365/366).
-     *
-     * @return integer  The day of the year.
-     */
-    function dayOfYear()
-    {
-        $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
-        $dayOfYear = $this->mday + $monthTotals[$this->month - 1];
-        if (Horde_Date::isLeapYear($this->year) && $this->month > 2) {
-            ++$dayOfYear;
-        }
-
-        return $dayOfYear;
-    }
-
-    /**
-     * Returns the week of the month.
-     *
-     * @since Horde 3.2
-     *
-     * @return integer  The week number.
-     */
-    function weekOfMonth()
-    {
-        return ceil($this->mday / 7);
-    }
-
-    /**
-     * Returns the week of the year, first Monday is first day of first week.
-     *
-     * @return integer  The week number.
-     */
-    function weekOfYear()
-    {
-        return $this->format('W');
-    }
-
-    /**
-     * Return the number of weeks in the given year (52 or 53).
-     *
-     * @static
-     *
-     * @param integer $year  The year to count the number of weeks in.
-     *
-     * @return integer $numWeeks   The number of weeks in $year.
-     */
-    function weeksInYear($year)
-    {
-        // Find the last Thursday of the year.
-        $day = 31;
-        $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0));
-        while ($date->dayOfWeek() != HORDE_DATE_THURSDAY) {
-            --$date->mday;
-        }
-        return $date->weekOfYear();
-    }
-
-    /**
-     * Set the date of this object to the $nth weekday of $weekday.
-     *
-     * @param integer $weekday  The day of the week (0 = Sunday, etc).
-     * @param integer $nth      The $nth $weekday to set to (defaults to 1).
-     */
-    function setNthWeekday($weekday, $nth = 1)
-    {
-        if ($weekday < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) {
-            return false;
-        }
-
-        $this->mday = 1;
-        $first = $this->dayOfWeek();
-        if ($weekday < $first) {
-            $this->mday = 8 + $weekday - $first;
-        } else {
-            $this->mday = $weekday - $first + 1;
-        }
-        $this->mday += 7 * $nth - 7;
-
-        $this->correct();
-
-        return true;
-    }
-
-    function dump($prefix = '')
-    {
-        echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "<br />\n";
-    }
-
-    /**
-     * Is the date currently represented by this object a valid date?
-     *
-     * @return boolean  Validity, counting leap years, etc.
-     */
-    function isValid()
-    {
-        if ($this->year < 0 || $this->year > 9999) {
-            return false;
-        }
-        return checkdate($this->month, $this->mday, $this->year);
-    }
-
-    /**
-     * Correct any over- or underflows in any of the date's members.
-     *
-     * @param integer $mask  We may not want to correct some overflows.
-     */
-    function correct($mask = HORDE_DATE_MASK_ALLPARTS)
-    {
-        if ($mask & HORDE_DATE_MASK_SECOND) {
-            while ($this->sec < 0) {
-                --$this->min;
-                $this->sec += 60;
-            }
-            while ($this->sec > 59) {
-                ++$this->min;
-                $this->sec -= 60;
-            }
-        }
-
-        if ($mask & HORDE_DATE_MASK_MINUTE) {
-            while ($this->min < 0) {
-                --$this->hour;
-                $this->min += 60;
-            }
-            while ($this->min > 59) {
-                ++$this->hour;
-                $this->min -= 60;
-            }
-        }
-
-        if ($mask & HORDE_DATE_MASK_HOUR) {
-            while ($this->hour < 0) {
-                --$this->mday;
-                $this->hour += 24;
-            }
-            while ($this->hour > 23) {
-                ++$this->mday;
-                $this->hour -= 24;
-            }
-        }
-
-        if ($mask & HORDE_DATE_MASK_MONTH) {
-            while ($this->month > 12) {
-                ++$this->year;
-                $this->month -= 12;
-            }
-            while ($this->month < 1) {
-                --$this->year;
-                $this->month += 12;
-            }
-        }
-
-        if ($mask & HORDE_DATE_MASK_DAY) {
-            while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) {
-                $this->mday -= Horde_Date::daysInMonth($this->month, $this->year);
-                ++$this->month;
-                $this->correct(HORDE_DATE_MASK_MONTH);
-            }
-            while ($this->mday < 1) {
-                --$this->month;
-                $this->correct(HORDE_DATE_MASK_MONTH);
-                $this->mday += Horde_Date::daysInMonth($this->month, $this->year);
-            }
-        }
-    }
-
-    /**
-     * Compare this date to another date object to see which one is
-     * greater (later). Assumes that the dates are in the same
-     * timezone.
-     *
-     * @param mixed $date  The date to compare to.
-     *
-     * @return integer  ==  0 if the dates are equal
-     *                  >=  1 if this date is greater (later)
-     *                  <= -1 if the other date is greater (later)
-     */
-    function compareDate($date)
-    {
-        if (!is_object($date) || !is_a($date, 'Horde_Date')) {
-            $date = new Horde_Date($date);
-        }
-
-        if ($this->year != $date->year) {
-            return $this->year - $date->year;
-        }
-        if ($this->month != $date->month) {
-            return $this->month - $date->month;
-        }
-
-        return $this->mday - $date->mday;
-    }
-
-    /**
-     * Compare this to another date object by time, to see which one
-     * is greater (later). Assumes that the dates are in the same
-     * timezone.
-     *
-     * @param mixed $date  The date to compare to.
-     *
-     * @return integer  ==  0 if the dates are equal
-     *                  >=  1 if this date is greater (later)
-     *                  <= -1 if the other date is greater (later)
-     */
-    function compareTime($date)
-    {
-        if (!is_object($date) || !is_a($date, 'Horde_Date')) {
-            $date = new Horde_Date($date);
-        }
-
-        if ($this->hour != $date->hour) {
-            return $this->hour - $date->hour;
-        }
-        if ($this->min != $date->min) {
-            return $this->min - $date->min;
-        }
-
-        return $this->sec - $date->sec;
-    }
-
-    /**
-     * Compare this to another date object, including times, to see
-     * which one is greater (later). Assumes that the dates are in the
-     * same timezone.
-     *
-     * @param mixed $date  The date to compare to.
-     *
-     * @return integer  ==  0 if the dates are equal
-     *                  >=  1 if this date is greater (later)
-     *                  <= -1 if the other date is greater (later)
-     */
-    function compareDateTime($date)
-    {
-        if (!is_object($date) || !is_a($date, 'Horde_Date')) {
-            $date = new Horde_Date($date);
-        }
-
-        if ($diff = $this->compareDate($date)) {
-            return $diff;
-        }
-
-        return $this->compareTime($date);
-    }
-
-    /**
-     * Get the time offset for local time zone.
-     *
-     * @param boolean $colon  Place a colon between hours and minutes?
-     *
-     * @return string  Timezone offset as a string in the format +HH:MM.
-     */
-    function tzOffset($colon = true)
-    {
-        $secs = $this->format('Z');
-
-        if ($secs < 0) {
-            $sign = '-';
-            $secs = -$secs;
-        } else {
-            $sign = '+';
-        }
-        $colon = $colon ? ':' : '';
-        $mins = intval(($secs + 30) / 60);
-        return sprintf('%s%02d%s%02d',
-                       $sign, $mins / 60, $colon, $mins % 60);
-    }
-
-    /**
-     * Return the unix timestamp representation of this date.
-     *
-     * @return integer  A unix timestamp.
-     */
-    function timestamp()
-    {
-        if (class_exists('DateTime')) {
-            return $this->format('U');
-        } else {
-            return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year);
-        }
-    }
-
-    /**
-     * Return the unix timestamp representation of this date, 12:00am.
-     *
-     * @return integer  A unix timestamp.
-     */
-    function datestamp()
-    {
-        if (class_exists('DateTime')) {
-            $dt = new DateTime();
-            $dt->setDate($this->year, $this->month, $this->mday);
-            $dt->setTime(0, 0, 0);
-            return $dt->format('U');
-        } else {
-            return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year);
-        }
-    }
-
-    /**
-     * Format time using the specifiers available in date() or in the DateTime
-     * class' format() method.
-     *
-     * @since Horde 3.3
-     *
-     * @param string $format
-     *
-     * @return string  Formatted time.
-     */
-    function format($format)
-    {
-        if (class_exists('DateTime')) {
-            $dt = new DateTime();
-            $dt->setDate($this->year, $this->month, $this->mday);
-            $dt->setTime($this->hour, $this->min, $this->sec);
-            return $dt->format($format);
-        } else {
-            return date($format, $this->timestamp());
-        }
-    }
-
-    /**
-     * Format time in ISO-8601 format. Works correctly since Horde 3.2.
-     *
-     * @return string  Date and time in ISO-8601 format.
-     */
-    function iso8601DateTime()
-    {
-        return $this->rfc3339DateTime() . $this->tzOffset();
-    }
-
-    /**
-     * Format time in RFC 2822 format.
-     *
-     * @return string  Date and time in RFC 2822 format.
-     */
-    function rfc2822DateTime()
-    {
-        return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false);
-    }
-
-    /**
-     * Format time in RFC 3339 format.
-     *
-     * @since Horde 3.1
-     *
-     * @return string  Date and time in RFC 3339 format. The seconds part has
-     *                 been added with Horde 3.2.
-     */
-    function rfc3339DateTime()
-    {
-        return $this->format('Y-m-d\TH:i:s');
-    }
-
-    /**
-     * Format time to standard 'ctime' format.
-     *
-     * @return string  Date and time.
-     */
-    function cTime()
-    {
-        return $this->format('D M j H:i:s Y');
-    }
-
-    /**
-     * Format date and time using strftime() format.
-     *
-     * @since Horde 3.1
-     *
-     * @return string  strftime() formatted date and time.
-     */
-    function strftime($format)
-    {
-        if (preg_match('/%[^' . $this->_supportedSpecs . ']/', $format)) {
-            return strftime($format, $this->timestamp());
-        } else {
-            return $this->_strftime($format);
-        }
-    }
-
-    /**
-     * Format date and time using a limited set of the strftime() format.
-     *
-     * @return string  strftime() formatted date and time.
-     */
-    function _strftime($format)
-    {
-        if (preg_match('/%[bBpxX]/', $format)) {
-            require_once 'Horde/NLS.php';
-        }
-
-        return preg_replace(
-            array('/%b/e',
-                  '/%B/e',
-                  '/%C/e',
-                  '/%d/e',
-                  '/%D/e',
-                  '/%e/e',
-                  '/%H/e',
-                  '/%I/e',
-                  '/%m/e',
-                  '/%M/e',
-                  '/%n/',
-                  '/%p/e',
-                  '/%R/e',
-                  '/%S/e',
-                  '/%t/',
-                  '/%T/e',
-                  '/%x/e',
-                  '/%X/e',
-                  '/%y/e',
-                  '/%Y/',
-                  '/%%/'),
-            array('$this->_strftime(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->month)))',
-                  '$this->_strftime(NLS::getLangInfo(constant(\'MON_\' . (int)$this->month)))',
-                  '(int)($this->year / 100)',
-                  'sprintf(\'%02d\', $this->mday)',
-                  '$this->_strftime(\'%m/%d/%y\')',
-                  'sprintf(\'%2d\', $this->mday)',
-                  'sprintf(\'%02d\', $this->hour)',
-                  'sprintf(\'%02d\', $this->hour == 0 ? 12 : ($this->hour > 12 ? $this->hour - 12 : $this->hour))',
-                  'sprintf(\'%02d\', $this->month)',
-                  'sprintf(\'%02d\', $this->min)',
-                  "\n",
-                  '$this->_strftime(NLS::getLangInfo($this->hour < 12 ? AM_STR : PM_STR))',
-                  '$this->_strftime(\'%H:%M\')',
-                  'sprintf(\'%02d\', $this->sec)',
-                  "\t",
-                  '$this->_strftime(\'%H:%M:%S\')',
-                  '$this->_strftime(NLS::getLangInfo(D_FMT))',
-                  '$this->_strftime(NLS::getLangInfo(T_FMT))',
-                  'substr(sprintf(\'%04d\', $this->year), -2)',
-                  (int)$this->year,
-                  '%'),
-            $format);
-    }
-
-    /**
-     * mktime() implementation that supports dates outside of 1970-2038,
-     * from http://phplens.com/phpeverywhere/adodb_date_library.
-     *
-     * @TODO remove in Horde 4
-     *
-     * This does NOT work with pre-1970 daylight saving times.
-     *
-     * @static
-     */
-    function _mktime($hr, $min, $sec, $mon = false, $day = false,
-                     $year = false, $is_dst = false, $is_gmt = false)
-    {
-        if ($mon === false) {
-            return $is_gmt
-                ? @gmmktime($hr, $min, $sec)
-                : @mktime($hr, $min, $sec);
-        }
-
-        if ($year > 1901 && $year < 2038 &&
-            ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) {
-            return $is_gmt
-                ? @gmmktime($hr, $min, $sec, $mon, $day, $year)
-                : @mktime($hr, $min, $sec, $mon, $day, $year);
-        }
-
-        $gmt_different = $is_gmt
-            ? 0
-            : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0));
-
-        $mon = intval($mon);
-        $day = intval($day);
-        $year = intval($year);
-
-        if ($mon > 12) {
-            $y = floor($mon / 12);
-            $year += $y;
-            $mon -= $y * 12;
-        } elseif ($mon < 1) {
-            $y = ceil((1 - $mon) / 12);
-            $year -= $y;
-            $mon += $y * 12;
-        }
-
-        $_day_power = 86400;
-        $_hour_power = 3600;
-        $_min_power = 60;
-
-        $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
-        $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
-
-        $_total_date = 0;
-        if ($year >= 1970) {
-            for ($a = 1970; $a <= $year; $a++) {
-                $leaf = Horde_Date::isLeapYear($a);
-                if ($leaf == true) {
-                    $loop_table = $_month_table_leaf;
-                    $_add_date = 366;
-                } else {
-                    $loop_table = $_month_table_normal;
-                    $_add_date = 365;
-                }
-                if ($a < $year) {
-                    $_total_date += $_add_date;
-                } else {
-                    for ($b = 1; $b < $mon; $b++) {
-                        $_total_date += $loop_table[$b];
-                    }
-                }
-            }
-
-            return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different;
-        }
-
-        for ($a = 1969 ; $a >= $year; $a--) {
-            $leaf = Horde_Date::isLeapYear($a);
-            if ($leaf == true) {
-                $loop_table = $_month_table_leaf;
-                $_add_date = 366;
-            } else {
-                $loop_table = $_month_table_normal;
-                $_add_date = 365;
-            }
-            if ($a > $year) {
-                $_total_date += $_add_date;
-            } else {
-                for ($b = 12; $b > $mon; $b--) {
-                    $_total_date += $loop_table[$b];
-                }
-            }
-        }
-
-        $_total_date += $loop_table[$mon] - $day;
-        $_day_time = $hr * $_hour_power + $min * $_min_power + $sec;
-        $_day_time = $_day_power - $_day_time;
-        $ret = -($_total_date * $_day_power + $_day_time - $gmt_different);
-        if ($ret < -12220185600) {
-            // If earlier than 5 Oct 1582 - gregorian correction.
-            return $ret + 10 * 86400;
-        } elseif ($ret < -12219321600) {
-            // If in limbo, reset to 15 Oct 1582.
-            return -12219321600;
-        } else {
-            return $ret;
-        }
-    }
-
-}
-
-
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+require_once(dirname(__FILE__) . '/Horde_Date.php');
 
 // {{{ Header
 
diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php
new file mode 100644
index 0000000..5ba0f4c
--- /dev/null
+++ b/plugins/calendar/lib/Horde_iCalendar.php
@@ -0,0 +1,3284 @@
+<?php
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+
+$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
+
+/**
+ * The String:: class provides static methods for charset and locale safe
+ * string manipulation.
+ *
+ * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan at horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Util
+ */
+class String {
+
+    /**
+     * Caches the result of extension_loaded() calls.
+     *
+     * @param string $ext  The extension name.
+     *
+     * @return boolean  Is the extension loaded?
+     *
+     * @see Util::extensionExists()
+     */
+    function extensionExists($ext)
+    {
+        static $cache = array();
+
+        if (!isset($cache[$ext])) {
+            $cache[$ext] = extension_loaded($ext);
+        }
+
+        return $cache[$ext];
+    }
+
+    /**
+     * Sets a default charset that the String:: methods will use if none is
+     * explicitly specified.
+     *
+     * @param string $charset  The charset to use as the default one.
+     */
+    function setDefaultCharset($charset)
+    {
+        $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
+        if (String::extensionExists('mbstring') &&
+            function_exists('mb_regex_encoding')) {
+            $old_error = error_reporting(0);
+            mb_regex_encoding(String::_mbstringCharset($charset));
+            error_reporting($old_error);
+        }
+    }
+
+    /**
+     * Converts a string from one charset to another.
+     *
+     * Works only if either the iconv or the mbstring extension
+     * are present and best if both are available.
+     * The original string is returned if conversion failed or none
+     * of the extensions were available.
+     *
+     * @param mixed $input  The data to be converted. If $input is an an array,
+     *                      the array's values get converted recursively.
+     * @param string $from  The string's current charset.
+     * @param string $to    The charset to convert the string to. If not
+     *                      specified, the global variable
+     *                      $_HORDE_STRING_CHARSET will be used.
+     *
+     * @return mixed  The converted input data.
+     */
+    function convertCharset($input, $from, $to = null)
+    {
+        /* Don't bother converting numbers. */
+        if (is_numeric($input)) {
+            return $input;
+        }
+
+        /* Get the user's default character set if none passed in. */
+        if (is_null($to)) {
+            $to = $GLOBALS['_HORDE_STRING_CHARSET'];
+        }
+
+        /* If the from and to character sets are identical, return now. */
+        if ($from == $to) {
+            return $input;
+        }
+        $from = String::lower($from);
+        $to = String::lower($to);
+        if ($from == $to) {
+            return $input;
+        }
+
+        if (is_array($input)) {
+            $tmp = array();
+            reset($input);
+            while (list($key, $val) = each($input)) {
+                $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
+            }
+            return $tmp;
+        }
+        if (is_object($input)) {
+            // PEAR_Error objects are almost guaranteed to contain recursion,
+            // which will cause a segfault in PHP.  We should never reach
+            // this line, but add a check and a log message to help the devs
+            // track down and fix this issue.
+            if (is_a($input, 'PEAR_Error')) {
+                Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                return '';
+            }
+            $vars = get_object_vars($input);
+            while (list($key, $val) = each($vars)) {
+                $input->$key = String::convertCharset($val, $from, $to);
+            }
+            return $input;
+        }
+
+        if (!is_string($input)) {
+            return $input;
+        }
+
+        return String::_convertCharset($input, $from, $to);
+    }
+
+    /**
+     * Internal function used to do charset conversion.
+     *
+     * @access private
+     *
+     * @param string $input  See String::convertCharset().
+     * @param string $from   See String::convertCharset().
+     * @param string $to     See String::convertCharset().
+     *
+     * @return string  The converted string.
+     */
+    function _convertCharset($input, $from, $to)
+    {
+        $output = '';
+        $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
+        $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
+
+        /* Use utf8_[en|de]code() if possible and if the string isn't too
+         * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
+         * functions use more memory. */
+        if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
+            if ($from_check && ($to == 'utf-8')) {
+                return utf8_encode($input);
+            }
+
+            if (($from == 'utf-8') && $to_check) {
+                return utf8_decode($input);
+            }
+        }
+
+        /* First try iconv with transliteration. */
+        if (($from != 'utf7-imap') &&
+            ($to != 'utf7-imap') &&
+            String::extensionExists('iconv')) {
+            /* We need to tack an extra character temporarily because of a bug
+             * in iconv() if the last character is not a 7 bit ASCII
+             * character. */
+            $oldTrackErrors = ini_set('track_errors', 1);
+            unset($php_errormsg);
+            $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
+            $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
+            ini_set('track_errors', $oldTrackErrors);
+        }
+
+        /* Next try mbstring. */
+        if (!$output && String::extensionExists('mbstring')) {
+            $old_error = error_reporting(0);
+            $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
+            error_reporting($old_error);
+        }
+
+        /* At last try imap_utf7_[en|de]code if appropriate. */
+        if (!$output && String::extensionExists('imap')) {
+            if ($from_check && ($to == 'utf7-imap')) {
+                return @imap_utf7_encode($input);
+            }
+            if (($from == 'utf7-imap') && $to_check) {
+                return @imap_utf7_decode($input);
+            }
+        }
+
+        return (!$output) ? $input : $output;
+    }
+
+    /**
+     * Makes a string lowercase.
+     *
+     * @param string  $string   The string to be converted.
+     * @param boolean $locale   If true the string will be converted based on a
+     *                          given charset, locale independent else.
+     * @param string  $charset  If $locale is true, the charset to use when
+     *                          converting. If not provided the current charset.
+     *
+     * @return string  The string with lowercase characters
+     */
+    function lower($string, $locale = false, $charset = null)
+    {
+        static $lowers;
+
+        if ($locale) {
+            /* The existence of mb_strtolower() depends on the platform. */
+            if (String::extensionExists('mbstring') &&
+                function_exists('mb_strtolower')) {
+                if (is_null($charset)) {
+                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+                }
+                $old_error = error_reporting(0);
+                $ret = mb_strtolower($string, String::_mbstringCharset($charset));
+                error_reporting($old_error);
+                if (!empty($ret)) {
+                    return $ret;
+                }
+            }
+            return strtolower($string);
+        }
+
+        if (!isset($lowers)) {
+            $lowers = array();
+        }
+        if (!isset($lowers[$string])) {
+            $language = setlocale(LC_CTYPE, 0);
+            setlocale(LC_CTYPE, 'C');
+            $lowers[$string] = strtolower($string);
+            setlocale(LC_CTYPE, $language);
+        }
+
+        return $lowers[$string];
+    }
+
+    /**
+     * Makes a string uppercase.
+     *
+     * @param string  $string   The string to be converted.
+     * @param boolean $locale   If true the string will be converted based on a
+     *                          given charset, locale independent else.
+     * @param string  $charset  If $locale is true, the charset to use when
+     *                          converting. If not provided the current charset.
+     *
+     * @return string  The string with uppercase characters
+     */
+    function upper($string, $locale = false, $charset = null)
+    {
+        static $uppers;
+
+        if ($locale) {
+            /* The existence of mb_strtoupper() depends on the
+             * platform. */
+            if (function_exists('mb_strtoupper')) {
+                if (is_null($charset)) {
+                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+                }
+                $old_error = error_reporting(0);
+                $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
+                error_reporting($old_error);
+                if (!empty($ret)) {
+                    return $ret;
+                }
+            }
+            return strtoupper($string);
+        }
+
+        if (!isset($uppers)) {
+            $uppers = array();
+        }
+        if (!isset($uppers[$string])) {
+            $language = setlocale(LC_CTYPE, 0);
+            setlocale(LC_CTYPE, 'C');
+            $uppers[$string] = strtoupper($string);
+            setlocale(LC_CTYPE, $language);
+        }
+
+        return $uppers[$string];
+    }
+
+    /**
+     * Returns a string with the first letter capitalized if it is
+     * alphabetic.
+     *
+     * @param string  $string   The string to be capitalized.
+     * @param boolean $locale   If true the string will be converted based on a
+     *                          given charset, locale independent else.
+     * @param string  $charset  The charset to use, defaults to current charset.
+     *
+     * @return string  The capitalized string.
+     */
+    function ucfirst($string, $locale = false, $charset = null)
+    {
+        if ($locale) {
+            $first = String::substr($string, 0, 1, $charset);
+            if (String::isAlpha($first, $charset)) {
+                $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
+            }
+        } else {
+            $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
+        }
+        return $string;
+    }
+
+    /**
+     * Returns part of a string.
+     *
+     * @param string $string   The string to be converted.
+     * @param integer $start   The part's start position, zero based.
+     * @param integer $length  The part's length.
+     * @param string $charset  The charset to use when calculating the part's
+     *                         position and length, defaults to current
+     *                         charset.
+     *
+     * @return string  The string's part.
+     */
+    function substr($string, $start, $length = null, $charset = null)
+    {
+        if (is_null($length)) {
+            $length = String::length($string, $charset) - $start;
+        }
+
+        if ($length == 0) {
+            return '';
+        }
+
+        /* Try iconv. */
+        if (function_exists('iconv_substr')) {
+            if (is_null($charset)) {
+                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+            }
+
+            $old_error = error_reporting(0);
+            $ret = iconv_substr($string, $start, $length, $charset);
+            error_reporting($old_error);
+            /* iconv_substr() returns false on failure. */
+            if ($ret !== false) {
+                return $ret;
+            }
+        }
+
+        /* Try mbstring. */
+        if (String::extensionExists('mbstring')) {
+            if (is_null($charset)) {
+                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+            }
+            $old_error = error_reporting(0);
+            $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
+            error_reporting($old_error);
+            /* mb_substr() returns empty string on failure. */
+            if (strlen($ret)) {
+                return $ret;
+            }
+        }
+
+        return substr($string, $start, $length);
+    }
+
+    /**
+     * Returns the character (not byte) length of a string.
+     *
+     * @param string $string  The string to return the length of.
+     * @param string $charset The charset to use when calculating the string's
+     *                        length.
+     *
+     * @return string  The string's part.
+     */
+    function length($string, $charset = null)
+    {
+        if (is_null($charset)) {
+            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+        }
+        $charset = String::lower($charset);
+        if ($charset == 'utf-8' || $charset == 'utf8') {
+            return strlen(utf8_decode($string));
+        }
+        if (String::extensionExists('mbstring')) {
+            $old_error = error_reporting(0);
+            $ret = mb_strlen($string, String::_mbstringCharset($charset));
+            error_reporting($old_error);
+            if (!empty($ret)) {
+                return $ret;
+            }
+        }
+        return strlen($string);
+    }
+
+    /**
+     * Returns the numeric position of the first occurrence of $needle
+     * in the $haystack string.
+     *
+     * @param string $haystack  The string to search through.
+     * @param string $needle    The string to search for.
+     * @param integer $offset   Allows to specify which character in haystack
+     *                          to start searching.
+     * @param string $charset   The charset to use when searching for the
+     *                          $needle string.
+     *
+     * @return integer  The position of first occurrence.
+     */
+    function pos($haystack, $needle, $offset = 0, $charset = null)
+    {
+        if (String::extensionExists('mbstring')) {
+            if (is_null($charset)) {
+                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+            }
+            $track_errors = ini_set('track_errors', 1);
+            $old_error = error_reporting(0);
+            $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
+            error_reporting($old_error);
+            ini_set('track_errors', $track_errors);
+            if (!isset($php_errormsg)) {
+                return $ret;
+            }
+        }
+        return strpos($haystack, $needle, $offset);
+    }
+
+    /**
+     * Returns a string padded to a certain length with another string.
+     *
+     * This method behaves exactly like str_pad but is multibyte safe.
+     *
+     * @param string $input    The string to be padded.
+     * @param integer $length  The length of the resulting string.
+     * @param string $pad      The string to pad the input string with. Must
+     *                         be in the same charset like the input string.
+     * @param const $type      The padding type. One of STR_PAD_LEFT,
+     *                         STR_PAD_RIGHT, or STR_PAD_BOTH.
+     * @param string $charset  The charset of the input and the padding
+     *                         strings.
+     *
+     * @return string  The padded string.
+     */
+    function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
+                 $charset = null)
+    {
+        $mb_length = String::length($input, $charset);
+        $sb_length = strlen($input);
+        $pad_length = String::length($pad, $charset);
+
+        /* Return if we already have the length. */
+        if ($mb_length >= $length) {
+            return $input;
+        }
+
+        /* Shortcut for single byte strings. */
+        if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
+            return str_pad($input, $length, $pad, $type);
+        }
+
+        switch ($type) {
+        case STR_PAD_LEFT:
+            $left = $length - $mb_length;
+            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
+            break;
+        case STR_PAD_BOTH:
+            $left = floor(($length - $mb_length) / 2);
+            $right = ceil(($length - $mb_length) / 2);
+            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
+                $input .
+                String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+            break;
+        case STR_PAD_RIGHT:
+            $right = $length - $mb_length;
+            $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+            break;
+        }
+
+        return $output;
+    }
+
+    /**
+     * Wraps the text of a message.
+     *
+     * @since Horde 3.2
+     *
+     * @param string $string         String containing the text to wrap.
+     * @param integer $width         Wrap the string at this number of
+     *                               characters.
+     * @param string $break          Character(s) to use when breaking lines.
+     * @param boolean $cut           Whether to cut inside words if a line
+     *                               can't be wrapped.
+     * @param string $charset        Character set to use when breaking lines.
+     * @param boolean $line_folding  Whether to apply line folding rules per
+     *                               RFC 822 or similar. The correct break
+     *                               characters including leading whitespace
+     *                               have to be specified too.
+     *
+     * @return string  String containing the wrapped text.
+     */
+    function wordwrap($string, $width = 75, $break = "\n", $cut = false,
+                      $charset = null, $line_folding = false)
+    {
+        /* Get the user's default character set if none passed in. */
+        if (is_null($charset)) {
+            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+        }
+        $charset = String::_mbstringCharset($charset);
+        $string = String::convertCharset($string, $charset, 'utf-8');
+        $wrapped = '';
+
+        while (String::length($string, 'utf-8') > $width) {
+            $line = String::substr($string, 0, $width, 'utf-8');
+            $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
+            // Make sure didn't cut a word, unless we want hard breaks anyway.
+            if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
+                $line .= $match[1];
+                $string = $match[2];
+            }
+            // Wrap at existing line breaks.
+            if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
+                $wrapped .= $match[1] . $match[2];
+                $string = $match[3] . $string;
+                continue;
+            }
+            // Wrap at the last colon or semicolon followed by a whitespace if
+            // doing line folding.
+            if ($line_folding &&
+                preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
+                $wrapped .= $match[1] . $match[2] . $break;
+                $string = $match[3] . $string;
+                continue;
+            }
+            // Wrap at the last whitespace of $line.
+            if ($line_folding) {
+                $sub = '(.+[^\s])';
+            } else {
+                $sub = '(.*)';
+            }
+            if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
+                $wrapped .= $match[1] . $break;
+                $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
+                continue;
+            }
+            // Hard wrap if necessary.
+            if ($cut) {
+                $wrapped .= $line . $break;
+                continue;
+            }
+            $wrapped .= $line;
+        }
+
+        return String::convertCharset($wrapped . $string, 'utf-8', $charset);
+    }
+
+    /**
+     * Wraps the text of a message.
+     *
+     * @param string $text        String containing the text to wrap.
+     * @param integer $length     Wrap $text at this number of characters.
+     * @param string $break_char  Character(s) to use when breaking lines.
+     * @param string $charset     Character set to use when breaking lines.
+     * @param boolean $quote      Ignore lines that are wrapped with the '>'
+     *                            character (RFC 2646)? If true, we don't
+     *                            remove any padding whitespace at the end of
+     *                            the string.
+     *
+     * @return string  String containing the wrapped text.
+     */
+    function wrap($text, $length = 80, $break_char = "\n", $charset = null,
+                  $quote = false)
+    {
+        $paragraphs = array();
+
+        foreach (preg_split('/\r?\n/', $text) as $input) {
+            if ($quote && (strpos($input, '>') === 0)) {
+                $line = $input;
+            } else {
+                /* We need to handle the Usenet-style signature line
+                 * separately; since the space after the two dashes is
+                 * REQUIRED, we don't want to trim the line. */
+                if ($input != '-- ') {
+                    $input = rtrim($input);
+                }
+                $line = String::wordwrap($input, $length, $break_char, false, $charset);
+            }
+
+            $paragraphs[] = $line;
+        }
+
+        return implode($break_char, $paragraphs);
+    }
+
+    /**
+     * Returns true if the every character in the parameter is an alphabetic
+     * character.
+     *
+     * @param $string   The string to test.
+     * @param $charset  The charset to use when testing the string.
+     *
+     * @return boolean  True if the parameter was alphabetic only.
+     */
+    function isAlpha($string, $charset = null)
+    {
+        if (!String::extensionExists('mbstring')) {
+            return ctype_alpha($string);
+        }
+
+        $charset = String::_mbstringCharset($charset);
+        $old_charset = mb_regex_encoding();
+        $old_error = error_reporting(0);
+
+        if ($charset != $old_charset) {
+            mb_regex_encoding($charset);
+        }
+        $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
+        if ($charset != $old_charset) {
+            mb_regex_encoding($old_charset);
+        }
+
+        error_reporting($old_error);
+
+        return $alpha;
+    }
+
+    /**
+     * Returns true if ever character in the parameter is a lowercase letter in
+     * the current locale.
+     *
+     * @param $string   The string to test.
+     * @param $charset  The charset to use when testing the string.
+     *
+     * @return boolean  True if the parameter was lowercase.
+     */
+    function isLower($string, $charset = null)
+    {
+        return ((String::lower($string, true, $charset) === $string) &&
+                String::isAlpha($string, $charset));
+    }
+
+    /**
+     * Returns true if every character in the parameter is an uppercase letter
+     * in the current locale.
+     *
+     * @param string $string   The string to test.
+     * @param string $charset  The charset to use when testing the string.
+     *
+     * @return boolean  True if the parameter was uppercase.
+     */
+    function isUpper($string, $charset = null)
+    {
+        return ((String::upper($string, true, $charset) === $string) &&
+                String::isAlpha($string, $charset));
+    }
+
+    /**
+     * Performs a multibyte safe regex match search on the text provided.
+     *
+     * @since Horde 3.1
+     *
+     * @param string $text     The text to search.
+     * @param array $regex     The regular expressions to use, without perl
+     *                         regex delimiters (e.g. '/' or '|').
+     * @param string $charset  The character set of the text.
+     *
+     * @return array  The matches array from the first regex that matches.
+     */
+    function regexMatch($text, $regex, $charset = null)
+    {
+        if (!empty($charset)) {
+            $regex = String::convertCharset($regex, $charset, 'utf-8');
+            $text = String::convertCharset($text, $charset, 'utf-8');
+        }
+
+        $matches = array();
+        foreach ($regex as $val) {
+            if (preg_match('/' . $val . '/u', $text, $matches)) {
+                break;
+            }
+        }
+
+        if (!empty($charset)) {
+            $matches = String::convertCharset($matches, 'utf-8', $charset);
+        }
+
+        return $matches;
+    }
+
+    /**
+     * Workaround charsets that don't work with mbstring functions.
+     *
+     * @access private
+     *
+     * @param string $charset  The original charset.
+     *
+     * @return string  The charset to use with mbstring functions.
+     */
+    function _mbstringCharset($charset)
+    {
+        /* mbstring functions do not handle the 'ks_c_5601-1987' &
+         * 'ks_c_5601-1989' charsets. However, these charsets are used, for
+         * example, by various versions of Outlook to send Korean characters.
+         * Use UHC (CP949) encoding instead. See, e.g.,
+         * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
+        if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
+            $charset = 'UHC';
+        }
+
+        return $charset;
+    }
+
+}
+
+
+
+/**
+ * @package Horde_iCalendar
+ */
+
+/**
+ * String package
+ */
+
+
+
+/**
+ * Class representing iCalendar files.
+ *
+ * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar {
+
+    /**
+     * The parent (containing) iCalendar object.
+     *
+     * @var Horde_iCalendar
+     */
+    var $_container = false;
+
+    /**
+     * The name/value pairs of attributes for this object (UID,
+     * DTSTART, etc.). Which are present depends on the object and on
+     * what kind of component it is.
+     *
+     * @var array
+     */
+    var $_attributes = array();
+
+    /**
+     * Any children (contained) iCalendar components of this object.
+     *
+     * @var array
+     */
+    var $_components = array();
+
+    /**
+     * According to RFC 2425, we should always use CRLF-terminated lines.
+     *
+     * @var string
+     */
+    var $_newline = "\r\n";
+
+    /**
+     * iCalendar format version (different behavior for 1.0 and 2.0
+     * especially with recurring events).
+     *
+     * @var string
+     */
+    var $_version;
+
+    function Horde_iCalendar($version = '2.0')
+    {
+        $this->_version = $version;
+        $this->setAttribute('VERSION', $version);
+    }
+
+    /**
+     * Return a reference to a new component.
+     *
+     * @param string          $type       The type of component to return
+     * @param Horde_iCalendar $container  A container that this component
+     *                                    will be associated with.
+     *
+     * @return object  Reference to a Horde_iCalendar_* object as specified.
+     *
+     * @static
+     */
+    function &newComponent($type, &$container)
+    {
+        $type = String::lower($type);
+        $class = 'Horde_iCalendar_' . $type;
+        if (!class_exists($class)) {
+            include 'Horde/iCalendar/' . $type . '.php';
+        }
+        if (class_exists($class)) {
+            $component = new $class();
+            if ($container !== false) {
+                $component->_container = &$container;
+                // Use version of container, not default set by component
+                // constructor.
+                $component->_version = $container->_version;
+            }
+        } else {
+            // Should return an dummy x-unknown type class here.
+            $component = false;
+        }
+
+        return $component;
+    }
+
+    /**
+     * Sets the value of an attribute.
+     *
+     * @param string $name     The name of the attribute.
+     * @param string $value    The value of the attribute.
+     * @param array $params    Array containing any addition parameters for
+     *                         this attribute.
+     * @param boolean $append  True to append the attribute, False to replace
+     *                         the first matching attribute found.
+     * @param array $values    Array representation of $value.  For
+     *                         comma/semicolon seperated lists of values.  If
+     *                         not set use $value as single array element.
+     */
+    function setAttribute($name, $value, $params = array(), $append = true,
+                          $values = false)
+    {
+        // Make sure we update the internal format version if
+        // setAttribute('VERSION', ...) is called.
+        if ($name == 'VERSION') {
+            $this->_version = $value;
+            if ($this->_container !== false) {
+                $this->_container->_version = $value;
+            }
+        }
+
+        if (!$values) {
+            $values = array($value);
+        }
+        $found = false;
+        if (!$append) {
+            foreach (array_keys($this->_attributes) as $key) {
+                if ($this->_attributes[$key]['name'] == String::upper($name)) {
+                    $this->_attributes[$key]['params'] = $params;
+                    $this->_attributes[$key]['value'] = $value;
+                    $this->_attributes[$key]['values'] = $values;
+                    $found = true;
+                    break;
+                }
+            }
+        }
+
+        if ($append || !$found) {
+            $this->_attributes[] = array(
+                'name'      => String::upper($name),
+                'params'    => $params,
+                'value'     => $value,
+                'values'    => $values
+            );
+        }
+    }
+
+    /**
+     * Sets parameter(s) for an (already existing) attribute.  The
+     * parameter set is merged into the existing set.
+     *
+     * @param string $name   The name of the attribute.
+     * @param array $params  Array containing any additional parameters for
+     *                       this attribute.
+     * @return boolean  True on success, false if no attribute $name exists.
+     */
+    function setParameter($name, $params = array())
+    {
+        $keys = array_keys($this->_attributes);
+        foreach ($keys as $key) {
+            if ($this->_attributes[$key]['name'] == $name) {
+                $this->_attributes[$key]['params'] =
+                    array_merge($this->_attributes[$key]['params'], $params);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the value of an attribute.
+     *
+     * @param string $name     The name of the attribute.
+     * @param boolean $params  Return the parameters for this attribute instead
+     *                         of its value.
+     *
+     * @return mixed (object)  PEAR_Error if the attribute does not exist.
+     *               (string)  The value of the attribute.
+     *               (array)   The parameters for the attribute or
+     *                         multiple values for an attribute.
+     */
+    function getAttribute($name, $params = false)
+    {
+        $result = array();
+        foreach ($this->_attributes as $attribute) {
+            if ($attribute['name'] == $name) {
+                if ($params) {
+                    $result[] = $attribute['params'];
+                } else {
+                    $result[] = $attribute['value'];
+                }
+            }
+        }
+        if (!count($result)) {
+            require_once 'PEAR.php';
+            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+        } if (count($result) == 1 && !$params) {
+            return $result[0];
+        } else {
+            return $result;
+        }
+    }
+
+    /**
+     * Gets the values of an attribute as an array.  Multiple values
+     * are possible due to:
+     *
+     *  a) multiplce occurences of 'name'
+     *  b) (unsecapd) comma seperated lists.
+     *
+     * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
+     * will return array('a', 'b', 'c').
+     *
+     * @param string  $name    The name of the attribute.
+     * @return mixed (object)  PEAR_Error if the attribute does not exist.
+     *               (array)   Multiple values for an attribute.
+     */
+    function getAttributeValues($name)
+    {
+        $result = array();
+        foreach ($this->_attributes as $attribute) {
+            if ($attribute['name'] == $name) {
+                $result = array_merge($attribute['values'], $result);
+            }
+        }
+        if (!count($result)) {
+            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the value of an attribute, or a specified default value
+     * if the attribute does not exist.
+     *
+     * @param string $name    The name of the attribute.
+     * @param mixed $default  What to return if the attribute specified by
+     *                        $name does not exist.
+     *
+     * @return mixed (string) The value of $name.
+     *               (mixed)  $default if $name does not exist.
+     */
+    function getAttributeDefault($name, $default = '')
+    {
+        $value = $this->getAttribute($name);
+        return is_a($value, 'PEAR_Error') ? $default : $value;
+    }
+
+    /**
+     * Remove all occurences of an attribute.
+     *
+     * @param string $name  The name of the attribute.
+     */
+    function removeAttribute($name)
+    {
+        $keys = array_keys($this->_attributes);
+        foreach ($keys as $key) {
+            if ($this->_attributes[$key]['name'] == $name) {
+                unset($this->_attributes[$key]);
+            }
+        }
+    }
+
+    /**
+     * Get attributes for all tags or for a given tag.
+     *
+     * @param string $tag  Return attributes for this tag, or all attributes if
+     *                     not given.
+     *
+     * @return array  An array containing all the attributes and their types.
+     */
+    function getAllAttributes($tag = false)
+    {
+        if ($tag === false) {
+            return $this->_attributes;
+        }
+        $result = array();
+        foreach ($this->_attributes as $attribute) {
+            if ($attribute['name'] == $tag) {
+                $result[] = $attribute;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Add a vCalendar component (eg vEvent, vTimezone, etc.).
+     *
+     * @param Horde_iCalendar $component  Component (subclass) to add.
+     */
+    function addComponent($component)
+    {
+        if (is_a($component, 'Horde_iCalendar')) {
+            $component->_container = &$this;
+            $this->_components[] = &$component;
+        }
+    }
+
+    /**
+     * Retrieve all the components.
+     *
+     * @return array  Array of Horde_iCalendar objects.
+     */
+    function getComponents()
+    {
+        return $this->_components;
+    }
+
+    function getType()
+    {
+        return 'vcalendar';
+    }
+
+    /**
+     * Return the classes (entry types) we have.
+     *
+     * @return array  Hash with class names Horde_iCalendar_xxx as keys
+     *                and number of components of this class as value.
+     */
+    function getComponentClasses()
+    {
+        $r = array();
+        foreach ($this->_components as $c) {
+            $cn = strtolower(get_class($c));
+            if (empty($r[$cn])) {
+                $r[$cn] = 1;
+            } else {
+                $r[$cn]++;
+            }
+        }
+
+        return $r;
+    }
+
+    /**
+     * Number of components in this container.
+     *
+     * @return integer  Number of components in this container.
+     */
+    function getComponentCount()
+    {
+        return count($this->_components);
+    }
+
+    /**
+     * Retrieve a specific component.
+     *
+     * @param integer $idx  The index of the object to retrieve.
+     *
+     * @return mixed    (boolean) False if the index does not exist.
+     *                  (Horde_iCalendar_*) The requested component.
+     */
+    function getComponent($idx)
+    {
+        if (isset($this->_components[$idx])) {
+            return $this->_components[$idx];
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Locates the first child component of the specified class, and returns a
+     * reference to it.
+     *
+     * @param string $type  The type of component to find.
+     *
+     * @return boolean|Horde_iCalendar_*  False if no subcomponent of the
+     *                                    specified class exists or a reference
+     *                                    to the requested component.
+     */
+    function &findComponent($childclass)
+    {
+        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+        $keys = array_keys($this->_components);
+        foreach ($keys as $key) {
+            if (is_a($this->_components[$key], $childclass)) {
+                return $this->_components[$key];
+            }
+        }
+
+        $component = false;
+        return $component;
+    }
+
+    /**
+     * Locates the first matching child component of the specified class, and
+     * returns a reference to it.
+     *
+     * @param string $childclass  The type of component to find.
+     * @param string $attribute   This attribute must be set in the component
+     *                            for it to match.
+     * @param string $value       Optional value that $attribute must match.
+     *
+     * @return boolean|Horde_iCalendar_*  False if no matching subcomponent of
+     *                                    the specified class exists, or a
+     *                                    reference to the requested component.
+     */
+    function &findComponentByAttribute($childclass, $attribute, $value = null)
+    {
+        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+        $keys = array_keys($this->_components);
+        foreach ($keys as $key) {
+            if (is_a($this->_components[$key], $childclass)) {
+                $attr = $this->_components[$key]->getAttribute($attribute);
+                if (is_a($attr, 'PEAR_Error')) {
+                    continue;
+                }
+                if ($value !== null && $value != $attr) {
+                    continue;
+                }
+                return $this->_components[$key];
+            }
+        }
+
+        $component = false;
+        return $component;
+    }
+
+    /**
+     * Clears the iCalendar object (resets the components and attributes
+     * arrays).
+     */
+    function clear()
+    {
+        $this->_components = array();
+        $this->_attributes = array();
+    }
+
+    /**
+     * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
+     *
+     * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
+     * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
+     * respectively.
+     *
+     * @since Horde 3.1.2
+     */
+    function isOldFormat()
+    {
+        if ($this->getType() == 'vcard') {
+            return ($this->_version < 3);
+        }
+        if ($this->getType() == 'vNote') {
+            return ($this->_version < 2);
+        }
+        if ($this->_version >= 2) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Export as vCalendar format.
+     */
+    function exportvCalendar()
+    {
+        // Default values.
+        $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
+        $requiredAttributes['METHOD'] = 'PUBLISH';
+
+        foreach ($requiredAttributes as $name => $default_value) {
+            if (is_a($this->getattribute($name), 'PEAR_Error')) {
+                $this->setAttribute($name, $default_value);
+            }
+        }
+
+        return $this->_exportvData('VCALENDAR');
+    }
+
+    /**
+     * Export this entry as a hash array with tag names as keys.
+     *
+     * @param boolean $paramsInKeys
+     *                If false, the operation can be quite lossy as the
+     *                parameters are ignored when building the array keys.
+     *                So if you export a vcard with
+     *                LABEL;TYPE=WORK:foo
+     *                LABEL;TYPE=HOME:bar
+     *                the resulting hash contains only one label field!
+     *                If set to true, array keys look like 'LABEL;TYPE=WORK'
+     * @return array  A hash array with tag names as keys.
+     */
+    function toHash($paramsInKeys = false)
+    {
+        $hash = array();
+        foreach ($this->_attributes as $a)  {
+            $k = $a['name'];
+            if ($paramsInKeys && is_array($a['params'])) {
+                foreach ($a['params'] as $p => $v) {
+                    $k .= ";$p=$v";
+                }
+            }
+            $hash[$k] = $a['value'];
+        }
+
+        return $hash;
+    }
+
+    /**
+     * Parses a string containing vCalendar data.
+     *
+     * @todo This method doesn't work well at all, if $base is VCARD.
+     *
+     * @param string $text     The data to parse.
+     * @param string $base     The type of the base object.
+     * @param string $charset  The encoding charset for $text. Defaults to
+     *                         utf-8 for new format, iso-8859-1 for old format.
+     * @param boolean $clear   If true clears the iCal object before parsing.
+     *
+     * @return boolean  True on successful import, false otherwise.
+     */
+    function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
+                            $clear = true)
+    {
+        if ($clear) {
+            $this->clear();
+        }
+        if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
+            $container = true;
+            $vCal = $matches[1];
+        } else {
+            // Text isn't enclosed in BEGIN:VCALENDAR
+            // .. END:VCALENDAR. We'll try to parse it anyway.
+            $container = false;
+            $vCal = $text;
+        }
+        $vCal = trim($vCal);
+
+        // Extract all subcomponents.
+        $matches = $components = null;
+        if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
+            foreach ($components[0] as $key => $data) {
+                // Remove from the vCalendar data.
+                $vCal = str_replace($data, '', $vCal);
+            }
+        } elseif (!$container) {
+            return false;
+        }
+
+        // Unfold "quoted printable" folded lines like:
+        //  BODY;ENCODING=QUOTED-PRINTABLE:=
+        //  another=20line=
+        //  last=20line
+        while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
+            foreach ($matches[1] as $s) {
+                $r = preg_replace('/=\r?\n/', '', $s);
+                $vCal = str_replace($s, $r, $vCal);
+            }
+        }
+
+        // Unfold any folded lines.
+        if ($this->isOldFormat()) {
+            $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
+        } else {
+            $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
+        }
+
+        // Parse the remaining attributes.
+        if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
+            foreach ($matches[0] as $attribute) {
+                preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
+                $tag = trim(String::upper($parts[1]));
+                $value = $parts[4];
+                $params = array();
+
+                // Parse parameters.
+                if (!empty($parts[2])) {
+                    preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
+                    foreach ($param_parts[2] as $key => $paramName) {
+                        $paramName = String::upper($paramName);
+                        $paramValue = $param_parts[4][$key];
+                        if ($paramName == 'TYPE') {
+                            $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
+                            if (count($paramValue) == 1) {
+                                $paramValue = $paramValue[0];
+                            }
+                        }
+                        if (is_string($paramValue)) {
+                            if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
+                                $paramValue = $parts[1];
+                            }
+                        } else {
+                            foreach ($paramValue as $k => $tmp) {
+                                if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
+                                    $paramValue[$k] = $parts[1];
+                                }
+                            }
+                        }
+                        $params[$paramName] = $paramValue;
+                    }
+                }
+
+                // Charset and encoding handling.
+                if ((isset($params['ENCODING']) &&
+                     String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
+                    isset($params['QUOTED-PRINTABLE'])) {
+
+                    $value = quoted_printable_decode($value);
+                    if (isset($params['CHARSET'])) {
+                        $value = String::convertCharset($value, $params['CHARSET']);
+                    } else {
+                        $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+                    }
+                } elseif (isset($params['CHARSET'])) {
+                    $value = String::convertCharset($value, $params['CHARSET']);
+                } else {
+                    // As per RFC 2279, assume UTF8 if we don't have an
+                    // explicit charset parameter.
+                    $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+                }
+
+                // Get timezone info for date fields from $params.
+                $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
+
+                switch ($tag) {
+                // Date fields.
+                case 'COMPLETED':
+                case 'CREATED':
+                case 'LAST-MODIFIED':
+                case 'X-MOZ-LASTACK': 
+                case 'X-MOZ-SNOOZE-TIME': 
+                    $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+                    break;
+
+                case 'BDAY':
+                case 'X-SYNCJE-ANNIVERSARY':
+                case 'X-ANNIVERSARY':
+                    $this->setAttribute($tag, $this->_parseDate($value), $params);
+                    break;
+
+                case 'DTEND':
+                case 'DTSTART':
+                case 'DTSTAMP':
+                case 'DUE':
+                case 'AALARM':
+                case 'RECURRENCE-ID':
+                    // types like AALARM may contain additional data after a ;
+                    // ignore these.
+                    $ts = explode(';', $value);
+                    if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
+                        $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
+                    } else {
+                        $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
+                    }
+                    break;
+
+                case 'TRIGGER':
+                    if (isset($params['VALUE']) &&
+                        $params['VALUE'] == 'DATE-TIME') {
+                            $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+                    } else {
+                        $this->setAttribute($tag, $this->_parseDuration($value), $params);
+                    }
+                    break;
+
+                // Comma seperated dates.
+                case 'EXDATE':
+                case 'RDATE':
+                    if (!strlen($value)) {
+                        break;
+                    }
+                    $dates = array();
+                    $separator = $this->isOldFormat() ? ';' : ',';
+                    preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
+
+                    foreach ($values[1] as $value) {
+                        $dates[] = $this->_parseDate($value);
+                    }
+                    $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
+                    break;
+
+                // Duration fields.
+                case 'DURATION':
+                    $this->setAttribute($tag, $this->_parseDuration($value), $params);
+                    break;
+
+                // Period of time fields.
+                case 'FREEBUSY':
+                    $periods = array();
+                    preg_match_all('/,([^,]*)/', ',' . $value, $values);
+                    foreach ($values[1] as $value) {
+                        $periods[] = $this->_parsePeriod($value);
+                    }
+
+                    $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
+                    break;
+
+                // UTC offset fields.
+                case 'TZOFFSETFROM':
+                case 'TZOFFSETTO':
+                    $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
+                    break;
+
+                // Integer fields.
+                case 'PERCENT-COMPLETE':
+                case 'PRIORITY':
+                case 'REPEAT':
+                case 'SEQUENCE':
+                    $this->setAttribute($tag, intval($value), $params);
+                    break;
+
+                // Geo fields.
+                case 'GEO':
+                    if ($this->isOldFormat()) {
+                        $floats = explode(',', $value);
+                        $value = array('latitude' => floatval($floats[1]),
+                                       'longitude' => floatval($floats[0]));
+                    } else {
+                        $floats = explode(';', $value);
+                        $value = array('latitude' => floatval($floats[0]),
+                                       'longitude' => floatval($floats[1]));
+                    }
+                    $this->setAttribute($tag, $value, $params);
+                    break;
+
+                // Recursion fields.
+                case 'EXRULE':
+                case 'RRULE':
+                    $this->setAttribute($tag, trim($value), $params);
+                    break;
+
+                // ADR, ORG and N are lists seperated by unescaped semicolons
+                // with a specific number of slots.
+                case 'ADR':
+                case 'N':
+                case 'ORG':
+                    $value = trim($value);
+                    // As of rfc 2426 2.4.2 semicolon, comma, and colon must
+                    // be escaped (comma is unescaped after splitting below).
+                    $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
+                                         array($this->_newline, $this->_newline, ';', ':'),
+                                         $value);
+
+                    // Split by unescaped semicolons:
+                    $values = preg_split('/(?<!\\\\);/', $value);
+                    $value = str_replace('\\;', ';', $value);
+                    $values = str_replace('\\;', ';', $values);
+                    $this->setAttribute($tag, trim($value), $params, true, $values);
+                    break;
+
+                // String fields.
+                default:
+                    if ($this->isOldFormat()) {
+                        // vCalendar 1.0 and vCard 2.1 only escape semicolons
+                        // and use unescaped semicolons to create lists.
+                        $value = trim($value);
+                        // Split by unescaped semicolons:
+                        $values = preg_split('/(?<!\\\\);/', $value);
+                        $value = str_replace('\\;', ';', $value);
+                        $values = str_replace('\\;', ';', $values);
+                        $this->setAttribute($tag, trim($value), $params, true, $values);
+                    } else {
+                        $value = trim($value);
+                        // As of rfc 2426 2.4.2 semicolon, comma, and colon
+                        // must be escaped (comma is unescaped after splitting
+                        // below).
+                        $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
+                                             array($this->_newline, $this->_newline, ';', ':', '\\'),
+                                             $value);
+
+                        // Split by unescaped commas.
+                        $values = preg_split('/(?<!\\\\),/', $value);
+                        $value = str_replace('\\,', ',', $value);
+                        $values = str_replace('\\,', ',', $values);
+
+                        $this->setAttribute($tag, trim($value), $params, true, $values);
+                    }
+                    break;
+                }
+            }
+        }
+
+        // Process all components.
+        if ($components) {
+            // vTimezone components are processed first. They are
+            // needed to process vEvents that may use a TZID.
+            foreach ($components[0] as $key => $data) {
+                $type = trim($components[1][$key]);
+                if ($type != 'VTIMEZONE') {
+                    continue;
+                }
+                $component = &Horde_iCalendar::newComponent($type, $this);
+                if ($component === false) {
+                    return PEAR::raiseError("Unable to create object for type $type");
+                }
+                $component->parsevCalendar($data, $type, $charset);
+
+                $this->addComponent($component);
+            }
+
+            // Now process the non-vTimezone components.
+            foreach ($components[0] as $key => $data) {
+                $type = trim($components[1][$key]);
+                if ($type == 'VTIMEZONE') {
+                    continue;
+                }
+                $component = &Horde_iCalendar::newComponent($type, $this);
+                if ($component === false) {
+                    return PEAR::raiseError("Unable to create object for type $type");
+                }
+                $component->parsevCalendar($data, $type, $charset);
+
+                $this->addComponent($component);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Export this component in vCal format.
+     *
+     * @param string $base  The type of the base object.
+     *
+     * @return string  vCal format data.
+     */
+    function _exportvData($base = 'VCALENDAR')
+    {
+        $result = 'BEGIN:' . String::upper($base) . $this->_newline;
+
+        // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
+        // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
+        if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
+            $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
+            // Ensure that version is the first attribute.
+            $result .= 'VERSION:' . $this->_version . $this->_newline;
+        }
+        foreach ($this->_attributes as $attribute) {
+            $name = $attribute['name'];
+            if ($name == 'VERSION') {
+                // Already done.
+                continue;
+            }
+
+            $params_str = '';
+            $params = $attribute['params'];
+            if ($params) {
+                foreach ($params as $param_name => $param_value) {
+                    /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
+                    if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
+                        continue;
+                    }
+                    /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
+                    if ($this->isOldFormat() &&
+                        $param_name == 'VALUE' && $param_value == 'DATE') {
+                        continue;
+                    }
+
+                    if ($param_value === null) {
+                        $params_str .= ";$param_name";
+                    } else {
+                        $len = strlen($param_value);
+                        $safe_value = '';
+                        $quote = false;
+                        for ($i = 0; $i < $len; ++$i) {
+                            $ord = ord($param_value[$i]);
+                            // Accept only valid characters.
+                            if ($ord == 9 || $ord == 32 || $ord == 33 ||
+                                ($ord >= 35 && $ord <= 126) ||
+                                $ord >= 128) {
+                                $safe_value .= $param_value[$i];
+                                // Characters above 128 do not need to be
+                                // quoted as per RFC2445 but Outlook requires
+                                // this.
+                                if ($ord == 44 || $ord == 58 || $ord == 59 ||
+                                    $ord >= 128) {
+                                    $quote = true;
+                                }
+                            }
+                        }
+                        if ($quote) {
+                            $safe_value = '"' . $safe_value . '"';
+                        }
+                        $params_str .= ";$param_name=$safe_value";
+                    }
+                }
+            }
+
+            $value = $attribute['value'];
+            switch ($name) {
+            // Date fields.
+            case 'COMPLETED':
+            case 'CREATED':
+            case 'DCREATED':
+            case 'LAST-MODIFIED':
+            case 'X-MOZ-LASTACK':
+            case 'X-MOZ-SNOOZE-TIME':
+                $value = $this->_exportDateTime($value);
+                break;
+
+            case 'DTEND':
+            case 'DTSTART':
+            case 'DTSTAMP':
+            case 'DUE':
+            case 'AALARM':
+            case 'RECURRENCE-ID':
+                if (isset($params['VALUE'])) {
+                    if ($params['VALUE'] == 'DATE') {
+                        // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
+                        if ($this->isOldFormat() && $name == 'DTEND') {
+                            $d = new Horde_Date($value);
+                            $value = new Horde_Date(array(
+                                'year' => $d->year,
+                                'month' => $d->month,
+                                'mday' => $d->mday - 1));
+                            $value->correct();
+                            $value = $this->_exportDate($value, '235959');
+                        } else {
+                            $value = $this->_exportDate($value, '000000');
+                        }
+                    } else {
+                        $value = $this->_exportDateTime($value);
+                    }
+                } else {
+                    $value = $this->_exportDateTime($value);
+                }
+                break;
+
+            // Comma seperated dates.
+            case 'EXDATE':
+            case 'RDATE':
+                $dates = array();
+                foreach ($value as $date) {
+                    if (isset($params['VALUE'])) {
+                        if ($params['VALUE'] == 'DATE') {
+                            $dates[] = $this->_exportDate($date, '000000');
+                        } elseif ($params['VALUE'] == 'PERIOD') {
+                            $dates[] = $this->_exportPeriod($date);
+                        } else {
+                            $dates[] = $this->_exportDateTime($date);
+                        }
+                    } else {
+                        $dates[] = $this->_exportDateTime($date);
+                    }
+                }
+                $value = implode($this->isOldFormat() ? ';' : ',', $dates);
+                break;
+
+            case 'TRIGGER':
+                if (isset($params['VALUE'])) {
+                    if ($params['VALUE'] == 'DATE-TIME') {
+                        $value = $this->_exportDateTime($value);
+                    } elseif ($params['VALUE'] == 'DURATION') {
+                        $value = $this->_exportDuration($value);
+                    }
+                } else {
+                    $value = $this->_exportDuration($value);
+                }
+                break;
+
+            // Duration fields.
+            case 'DURATION':
+                $value = $this->_exportDuration($value);
+                break;
+
+            // Period of time fields.
+            case 'FREEBUSY':
+                $value_str = '';
+                foreach ($value as $period) {
+                    $value_str .= empty($value_str) ? '' : ',';
+                    $value_str .= $this->_exportPeriod($period);
+                }
+                $value = $value_str;
+                break;
+
+            // UTC offset fields.
+            case 'TZOFFSETFROM':
+            case 'TZOFFSETTO':
+                $value = $this->_exportUtcOffset($value);
+                break;
+
+            // Integer fields.
+            case 'PERCENT-COMPLETE':
+            case 'PRIORITY':
+            case 'REPEAT':
+            case 'SEQUENCE':
+                $value = "$value";
+                break;
+
+            // Geo fields.
+            case 'GEO':
+                if ($this->isOldFormat()) {
+                    $value = $value['longitude'] . ',' . $value['latitude'];
+                } else {
+                    $value = $value['latitude'] . ';' . $value['longitude'];
+                }
+                break;
+
+            // Recurrence fields.
+            case 'EXRULE':
+            case 'RRULE':
+                break;
+
+            default:
+                if ($this->isOldFormat()) {
+                    if (is_array($attribute['values']) &&
+                        count($attribute['values']) > 1) {
+                        $values = $attribute['values'];
+                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+                            $glue = ';';
+                        } else {
+                            $glue = ',';
+                        }
+                        $values = str_replace(';', '\\;', $values);
+                        $value = implode($glue, $values);
+                    } else {
+                        /* vcard 2.1 and vcalendar 1.0 escape only
+                         * semicolons */
+                        $value = str_replace(';', '\\;', $value);
+                    }
+                    // Text containing newlines or ASCII >= 127 must be BASE64
+                    // or QUOTED-PRINTABLE encoded. Currently we use
+                    // QUOTED-PRINTABLE as default.
+                    if (preg_match("/[^\x20-\x7F]/", $value) &&
+                        empty($params['ENCODING']))  {
+                        $params['ENCODING'] = 'QUOTED-PRINTABLE';
+                        $params_str .= ';ENCODING=QUOTED-PRINTABLE';
+                        // Add CHARSET as well. At least the synthesis client
+                        // gets confused otherwise
+                        if (empty($params['CHARSET'])) {
+                            $params['CHARSET'] = 'UTF-8';
+                            $params_str .= ';CHARSET=' . $params['CHARSET'];
+                        }
+                    }
+                } else {
+                    if (is_array($attribute['values']) &&
+                        count($attribute['values'])) {
+                        $values = $attribute['values'];
+                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+                            $glue = ';';
+                        } else {
+                            $glue = ',';
+                        }
+                        // As of rfc 2426 2.5 semicolon and comma must be
+                        // escaped.
+                        $values = str_replace(array('\\', ';', ','),
+                                              array('\\\\', '\\;', '\\,'),
+                                              $values);
+                        $value = implode($glue, $values);
+                    } else {
+                        // As of rfc 2426 2.5 semicolon and comma must be
+                        // escaped.
+                        $value = str_replace(array('\\', ';', ','),
+                                             array('\\\\', '\\;', '\\,'),
+                                             $value);
+                    }
+                    $value = preg_replace('/\r?\n/', '\n', $value);
+                }
+                break;
+            }
+
+            $value = str_replace("\r", '', $value);
+            if (!empty($params['ENCODING']) &&
+                $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
+                strlen(trim($value))) {
+                $result .= $name . $params_str . ':'
+                    . str_replace('=0A', '=0D=0A',
+                                  $this->_quotedPrintableEncode($value))
+                    . $this->_newline;
+            } else {
+                $attr_string = $name . $params_str . ':' . $value;
+                if (!$this->isOldFormat()) {
+                    $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
+                                                    true, 'utf-8', true);
+                }
+                $result .= $attr_string . $this->_newline;
+            }
+        }
+
+        foreach ($this->_components as $component) {
+            $result .= $component->exportvCalendar();
+        }
+
+        return $result . 'END:' . $base . $this->_newline;
+    }
+
+    /**
+     * Parse a UTC Offset field.
+     */
+    function _parseUtcOffset($text)
+    {
+        $offset = array();
+        if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
+            $offset['ahead']  = (bool)($timeParts[1] == '+');
+            $offset['hour']   = intval($timeParts[2]);
+            $offset['minute'] = intval($timeParts[3]);
+            if (isset($timeParts[4])) {
+                $offset['second'] = intval($timeParts[4]);
+            }
+            return $offset;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Export a UTC Offset field.
+     */
+    function _exportUtcOffset($value)
+    {
+        $offset = $value['ahead'] ? '+' : '-';
+        $offset .= sprintf('%02d%02d',
+                           $value['hour'], $value['minute']);
+        if (isset($value['second'])) {
+            $offset .= sprintf('%02d', $value['second']);
+        }
+
+        return $offset;
+    }
+
+    /**
+     * Parse a Time Period field.
+     */
+    function _parsePeriod($text)
+    {
+        $periodParts = explode('/', $text);
+
+        $start = $this->_parseDateTime($periodParts[0]);
+
+        if ($duration = $this->_parseDuration($periodParts[1])) {
+            return array('start' => $start, 'duration' => $duration);
+        } elseif ($end = $this->_parseDateTime($periodParts[1])) {
+            return array('start' => $start, 'end' => $end);
+        }
+    }
+
+    /**
+     * Export a Time Period field.
+     */
+    function _exportPeriod($value)
+    {
+        $period = $this->_exportDateTime($value['start']);
+        $period .= '/';
+        if (isset($value['duration'])) {
+            $period .= $this->_exportDuration($value['duration']);
+        } else {
+            $period .= $this->_exportDateTime($value['end']);
+        }
+        return $period;
+    }
+
+    /**
+     * Grok the TZID and return an offset in seconds from UTC for this
+     * date and time.
+     */
+    function _parseTZID($date, $time, $tzid)
+    {
+        $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
+        if (!$vtimezone) {
+            return false;
+        }
+
+        $change_times = array();
+        foreach ($vtimezone->getComponents() as $o) {
+            $t = $vtimezone->parseChild($o, $date['year']);
+            if ($t !== false) {
+                $change_times[] = $t;
+            }
+        }
+
+        if (!$change_times) {
+            return false;
+        }
+
+        sort($change_times);
+
+        // Time is arbitrarily based on UTC for comparison.
+        $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
+                       $date['month'], $date['mday'], $date['year']);
+
+        if ($t < $change_times[0]['time']) {
+            return $change_times[0]['from'];
+        }
+
+        for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
+            if (($t >= $change_times[$i]['time']) &&
+                ($t < $change_times[$i + 1]['time'])) {
+                return $change_times[$i]['to'];
+            }
+        }
+
+        if ($t >= $change_times[$n - 1]['time']) {
+            return $change_times[$n - 1]['to'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Parses a DateTime field and returns a unix timestamp. If the
+     * field cannot be parsed then the original text is returned
+     * unmodified.
+     *
+     * @todo This function should be moved to Horde_Date and made public.
+     */
+    function _parseDateTime($text, $tzid = false)
+    {
+        $dateParts = explode('T', $text);
+        if (count($dateParts) != 2 && !empty($text)) {
+            // Not a datetime field but may be just a date field.
+            if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+                // Or not
+                return $text;
+            }
+            $newtext = $text.'T000000';
+            $dateParts = explode('T', $newtext);
+        }
+
+        if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
+            return $text;
+        }
+        if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
+            return $text;
+        }
+
+        // Get timezone info for date fields from $tzid and container.
+        $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
+            ? $this->_parseTZID($date, $time, $tzid) : false;
+        if ($time['zone'] == 'UTC' || $tzoffset !== false) {
+            $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
+                                $date['month'], $date['mday'], $date['year']);
+            if ($tzoffset) {
+                $result -= $tzoffset;
+            }
+        } else {
+            // We don't know the timezone so assume local timezone.
+            // FIXME: shouldn't this be based on the user's timezone
+            // preference rather than the server's timezone?
+            $result = @mktime($time['hour'], $time['minute'], $time['second'],
+                              $date['month'], $date['mday'], $date['year']);
+        }
+
+        return ($result !== false) ? $result : $text;
+    }
+
+    /**
+     * Export a DateTime field.
+     */
+    function _exportDateTime($value)
+    {
+        $temp = array();
+        if (!is_object($value) && !is_array($value)) {
+            $tz = date('O', $value);
+            $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
+            $value -= $TZOffset;
+
+            $temp['zone']   = 'UTC';
+            list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
+        } else {
+            $dateOb = new Horde_Date($value);
+            return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
+        }
+
+        return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
+    }
+
+    /**
+     * Parses a Time field.
+     *
+     * @static
+     */
+    function _parseTime($text)
+    {
+        if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
+            $time['hour'] = intval($timeParts[1]);
+            $time['minute'] = intval($timeParts[2]);
+            $time['second'] = intval($timeParts[3]);
+            if (isset($timeParts[4])) {
+                $time['zone'] = 'UTC';
+            } else {
+                $time['zone'] = 'Local';
+            }
+            return $time;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Exports a Time field.
+     */
+    function _exportTime($value)
+    {
+        $time = sprintf('%02d%02d%02d',
+                        $value['hour'], $value['minute'], $value['second']);
+        if ($value['zone'] == 'UTC') {
+            $time .= 'Z';
+        }
+        return $time;
+    }
+
+    /**
+     * Parses a Date field.
+     *
+     * @static
+     */
+    function _parseDate($text)
+    {
+        $parts = explode('T', $text);
+        if (count($parts) == 2) {
+            $text = $parts[0];
+        }
+
+        if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+            return false;
+        }
+
+        return array('year' => $match[1],
+                     'month' => $match[2],
+                     'mday' => $match[3]);
+    }
+
+    /**
+     * Exports a date field.
+     *
+     * @param object|array $value  Date object or hash.
+     * @param string $autoconvert  If set, use this as time part to export the
+     *                             date as datetime when exporting to Vcalendar
+     *                             1.0. Examples: '000000' or '235959'
+     */
+    function _exportDate($value, $autoconvert = false)
+    {
+        if (is_object($value)) {
+            $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
+        }
+        if ($autoconvert !== false && $this->isOldFormat()) {
+            return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
+        } else {
+            return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
+        }
+    }
+
+    /**
+     * Parse a Duration Value field.
+     */
+    function _parseDuration($text)
+    {
+        if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
+            // Weeks.
+            $duration = 7 * 86400 * intval($durvalue[3]);
+
+            if (count($durvalue) > 4) {
+                // Days.
+                $duration += 86400 * intval($durvalue[4]);
+            }
+            if (count($durvalue) > 5) {
+                // Hours.
+                $duration += 3600 * intval($durvalue[7]);
+
+                // Mins.
+                if (isset($durvalue[8])) {
+                    $duration += 60 * intval($durvalue[8]);
+                }
+
+                // Secs.
+                if (isset($durvalue[9])) {
+                    $duration += intval($durvalue[9]);
+                }
+            }
+
+            // Sign.
+            if ($durvalue[1] == "-") {
+                $duration *= -1;
+            }
+
+            return $duration;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Export a duration value.
+     */
+    function _exportDuration($value)
+    {
+        $duration = '';
+        if ($value < 0) {
+            $value *= -1;
+            $duration .= '-';
+        }
+        $duration .= 'P';
+
+        $weeks = floor($value / (7 * 86400));
+        $value = $value % (7 * 86400);
+        if ($weeks) {
+            $duration .= $weeks . 'W';
+        }
+
+        $days = floor($value / (86400));
+        $value = $value % (86400);
+        if ($days) {
+            $duration .= $days . 'D';
+        }
+
+        if ($value) {
+            $duration .= 'T';
+
+            $hours = floor($value / 3600);
+            $value = $value % 3600;
+            if ($hours) {
+                $duration .= $hours . 'H';
+            }
+
+            $mins = floor($value / 60);
+            $value = $value % 60;
+            if ($mins) {
+                $duration .= $mins . 'M';
+            }
+
+            if ($value) {
+                $duration .= $value . 'S';
+            }
+        }
+
+        return $duration;
+    }
+
+    /**
+     * Converts an 8bit string to a quoted-printable string according to RFC
+     * 2045, section 6.7.
+     *
+     * imap_8bit() does not apply all necessary rules.
+     *
+     * @param string $input  The string to be encoded.
+     *
+     * @return string  The quoted-printable encoded string.
+     */
+    function _quotedPrintableEncode($input = '')
+    {
+        $output = $line = '';
+        $len = strlen($input);
+
+        for ($i = 0; $i < $len; ++$i) {
+            $ord = ord($input[$i]);
+            // Encode non-printable characters (rule 2).
+            if ($ord == 9 ||
+                ($ord >= 32 && $ord <= 60) ||
+                ($ord >= 62 && $ord <= 126)) {
+                $chunk = $input[$i];
+            } else {
+                // Quoted printable encoding (rule 1).
+                $chunk = '=' . String::upper(sprintf('%02X', $ord));
+            }
+            $line .= $chunk;
+            // Wrap long lines (rule 5)
+            if (strlen($line) + 1 > 76) {
+                $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
+                $newline = strrchr($line, "\r\n");
+                if ($newline !== false) {
+                    $output .= substr($line, 0, -strlen($newline) + 2);
+                    $line = substr($newline, 2);
+                } else {
+                    $output .= $line;
+                }
+                continue;
+            }
+            // Wrap at line breaks for better readability (rule 4).
+            if (substr($line, -3) == '=0A') {
+                $output .= $line . "=\r\n";
+                $line = '';
+            }
+        }
+        $output .= $line;
+
+        // Trailing whitespace must be encoded (rule 3).
+        $lastpos = strlen($output) - 1;
+        if ($output[$lastpos] == chr(9) ||
+            $output[$lastpos] == chr(32)) {
+            $output[$lastpos] = '=';
+            $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
+        }
+
+        return $output;
+    }
+
+}
+
+
+
+/**
+ * Class representing vAlarms.
+ *
+ * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_valarm extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vAlarm';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VALARM');
+    }
+
+}
+
+/**
+ * Class representing vEvents.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vevent extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vEvent';
+    }
+
+    function exportvCalendar()
+    {
+        // Default values.
+        $requiredAttributes = array();
+        $requiredAttributes['DTSTAMP'] = time();
+        $requiredAttributes['UID'] = $this->_exportDateTime(time())
+            . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
+            . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
+
+        $method = !empty($this->_container) ?
+            $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+        switch ($method) {
+        case 'PUBLISH':
+            $requiredAttributes['DTSTART'] = time();
+            $requiredAttributes['SUMMARY'] = '';
+            break;
+
+        case 'REQUEST':
+            $requiredAttributes['ATTENDEE'] = '';
+            $requiredAttributes['DTSTART'] = time();
+            $requiredAttributes['SUMMARY'] = '';
+            break;
+
+        case 'REPLY':
+            $requiredAttributes['ATTENDEE'] = '';
+            break;
+
+        case 'ADD':
+            $requiredAttributes['DTSTART'] = time();
+            $requiredAttributes['SEQUENCE'] = 1;
+            $requiredAttributes['SUMMARY'] = '';
+            break;
+
+        case 'CANCEL':
+            $requiredAttributes['ATTENDEE'] = '';
+            $requiredAttributes['SEQUENCE'] = 1;
+            break;
+
+        case 'REFRESH':
+            $requiredAttributes['ATTENDEE'] = '';
+            break;
+        }
+
+        foreach ($requiredAttributes as $name => $default_value) {
+            if (is_a($this->getAttribute($name), 'PEAR_Error')) {
+                $this->setAttribute($name, $default_value);
+            }
+        }
+
+        return parent::_exportvData('VEVENT');
+    }
+
+    /**
+     * Update the status of an attendee of an event.
+     *
+     * @param $email    The email address of the attendee.
+     * @param $status   The participant status to set.
+     * @param $fullname The full name of the participant to set.
+     */
+    function updateAttendee($email, $status, $fullname = '')
+    {
+        foreach ($this->_attributes as $key => $attribute) {
+            if ($attribute['name'] == 'ATTENDEE' &&
+                $attribute['value'] == 'mailto:' . $email) {
+                $this->_attributes[$key]['params']['PARTSTAT'] = $status;
+                if (!empty($fullname)) {
+                    $this->_attributes[$key]['params']['CN'] = $fullname;
+                }
+                unset($this->_attributes[$key]['params']['RSVP']);
+                return;
+            }
+        }
+        $params = array('PARTSTAT' => $status);
+        if (!empty($fullname)) {
+            $params['CN'] = $fullname;
+        }
+        $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
+    }
+
+    /**
+     * Return the organizer display name or email.
+     *
+     * @return string  The organizer name to display for this event.
+     */
+    function organizerName()
+    {
+        $organizer = $this->getAttribute('ORGANIZER', true);
+        if (is_a($organizer, 'PEAR_Error')) {
+            return _("An unknown person");
+        }
+
+        if (isset($organizer[0]['CN'])) {
+            return $organizer[0]['CN'];
+        }
+
+        $organizer = parse_url($this->getAttribute('ORGANIZER'));
+
+        return $organizer['path'];
+    }
+
+    /**
+     * Update this event with details from another event.
+     *
+     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details.
+     */
+    function updateFromvEvent($vevent)
+    {
+        $newAttributes = $vevent->getAllAttributes();
+        foreach ($newAttributes as $newAttribute) {
+            $currentValue = $this->getAttribute($newAttribute['name']);
+            if (is_a($currentValue, 'PEAR_error')) {
+                // Already exists so just add it.
+                $this->setAttribute($newAttribute['name'],
+                                    $newAttribute['value'],
+                                    $newAttribute['params']);
+            } else {
+                // Already exists so locate and modify.
+                $found = false;
+
+                // Try matching the attribte name and value incase
+                // only the params changed (eg attendee updating
+                // status).
+                foreach ($this->_attributes as $id => $attr) {
+                    if ($attr['name'] == $newAttribute['name'] &&
+                        $attr['value'] == $newAttribute['value']) {
+                        // merge the params
+                        foreach ($newAttribute['params'] as $param_id => $param_name) {
+                            $this->_attributes[$id]['params'][$param_id] = $param_name;
+                        }
+                        $found = true;
+                        break;
+                    }
+                }
+                if (!$found) {
+                    // Else match the first attribute with the same
+                    // name (eg changing start time).
+                    foreach ($this->_attributes as $id => $attr) {
+                        if ($attr['name'] == $newAttribute['name']) {
+                            $this->_attributes[$id]['value'] = $newAttribute['value'];
+                            // Merge the params.
+                            foreach ($newAttribute['params'] as $param_id => $param_name) {
+                                $this->_attributes[$id]['params'][$param_id] = $param_name;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Update just the attendess of event with details from another
+     * event.
+     *
+     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details
+     */
+    function updateAttendeesFromvEvent($vevent)
+    {
+        $newAttributes = $vevent->getAllAttributes();
+        foreach ($newAttributes as $newAttribute) {
+            if ($newAttribute['name'] != 'ATTENDEE') {
+                continue;
+            }
+            $currentValue = $this->getAttribute($newAttribute['name']);
+            if (is_a($currentValue, 'PEAR_error')) {
+                // Already exists so just add it.
+                $this->setAttribute($newAttribute['name'],
+                                    $newAttribute['value'],
+                                    $newAttribute['params']);
+            } else {
+                // Already exists so locate and modify.
+                $found = false;
+                // Try matching the attribte name and value incase
+                // only the params changed (eg attendee updating
+                // status).
+                foreach ($this->_attributes as $id => $attr) {
+                    if ($attr['name'] == $newAttribute['name'] &&
+                        $attr['value'] == $newAttribute['value']) {
+                        // Merge the params.
+                        foreach ($newAttribute['params'] as $param_id => $param_name) {
+                            $this->_attributes[$id]['params'][$param_id] = $param_name;
+                        }
+                        $found = true;
+                        break;
+                    }
+                }
+
+                if (!$found) {
+                    // Else match the first attribute with the same
+                    // name (eg changing start time).
+                    foreach ($this->_attributes as $id => $attr) {
+                        if ($attr['name'] == $newAttribute['name']) {
+                            $this->_attributes[$id]['value'] = $newAttribute['value'];
+                            // Merge the params.
+                            foreach ($newAttribute['params'] as $param_id => $param_name) {
+                                $this->_attributes[$id]['params'][$param_id] = $param_name;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+}
+
+/**
+ * Class representing vFreebusy components.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @todo Don't use timestamps
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
+
+    var $_busyPeriods = array();
+    var $_extraParams = array();
+
+    /**
+     * Returns the type of this calendar component.
+     *
+     * @return string  The type of this component.
+     */
+    function getType()
+    {
+        return 'vFreebusy';
+    }
+
+    /**
+     * Parses a string containing vFreebusy data.
+     *
+     * @param string $data     The data to parse.
+     */
+    function parsevCalendar($data, $type = null, $charset = null)
+    {
+        parent::parsevCalendar($data, 'VFREEBUSY', $charset);
+
+        // Do something with all the busy periods.
+        foreach ($this->_attributes as $key => $attribute) {
+            if ($attribute['name'] != 'FREEBUSY') {
+                continue;
+            }
+            foreach ($attribute['values'] as $value) {
+                $params = isset($attribute['params'])
+                    ? $attribute['params']
+                    : array();
+                if (isset($value['duration'])) {
+                    $this->addBusyPeriod('BUSY', $value['start'], null,
+                                         $value['duration'], $params);
+                } else {
+                    $this->addBusyPeriod('BUSY', $value['start'],
+                                         $value['end'], null, $params);
+                }
+            }
+            unset($this->_attributes[$key]);
+        }
+    }
+
+    /**
+     * Returns the component exported as string.
+     *
+     * @return string  The exported vFreeBusy information according to the
+     *                 iCalender format specification.
+     */
+    function exportvCalendar()
+    {
+        foreach ($this->_busyPeriods as $start => $end) {
+            $periods = array(array('start' => $start, 'end' => $end));
+            $this->setAttribute('FREEBUSY', $periods,
+                                isset($this->_extraParams[$start])
+                                ? $this->_extraParams[$start] : array());
+        }
+
+        $res = parent::_exportvData('VFREEBUSY');
+
+        foreach ($this->_attributes as $key => $attribute) {
+            if ($attribute['name'] == 'FREEBUSY') {
+                unset($this->_attributes[$key]);
+            }
+        }
+
+        return $res;
+    }
+
+    /**
+     * Returns a display name for this object.
+     *
+     * @return string  A clear text name for displaying this object.
+     */
+    function getName()
+    {
+        $name = '';
+        $method = !empty($this->_container) ?
+            $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+            $attr = 'ORGANIZER';
+        } elseif ($method == 'REPLY') {
+            $attr = 'ATTENDEE';
+        }
+
+        $name = $this->getAttribute($attr, true);
+        if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
+            return $name[0]['CN'];
+        }
+
+        $name = $this->getAttribute($attr);
+        if (is_a($name, 'PEAR_Error')) {
+            return '';
+        } else {
+            $name = parse_url($name);
+            return $name['path'];
+        }
+    }
+
+    /**
+     * Returns the email address for this object.
+     *
+     * @return string  The email address of this object's owner.
+     */
+    function getEmail()
+    {
+        $name = '';
+        $method = !empty($this->_container)
+                  ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+            $attr = 'ORGANIZER';
+        } elseif ($method == 'REPLY') {
+            $attr = 'ATTENDEE';
+        }
+
+        $name = $this->getAttribute($attr);
+        if (is_a($name, 'PEAR_Error')) {
+            return '';
+        } else {
+            $name = parse_url($name);
+            return $name['path'];
+        }
+    }
+
+    /**
+     * Returns the busy periods.
+     *
+     * @return array  All busy periods.
+     */
+    function getBusyPeriods()
+    {
+        return $this->_busyPeriods;
+    }
+
+    /**
+     * Returns any additional freebusy parameters.
+     *
+     * @return array  Additional parameters of the freebusy periods.
+     */
+    function getExtraParams()
+    {
+        return $this->_extraParams;
+    }
+
+    /**
+     * Returns all the free periods of time in a given period.
+     *
+     * @param integer $startStamp  The start timestamp.
+     * @param integer $endStamp    The end timestamp.
+     *
+     * @return array  A hash with free time periods, the start times as the
+     *                keys and the end times as the values.
+     */
+    function getFreePeriods($startStamp, $endStamp)
+    {
+        $this->simplify();
+        $periods = array();
+
+        // Check that we have data for some part of this period.
+        if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
+            return $periods;
+        }
+
+        // Locate the first time in the requested period we have data for.
+        $nextstart = max($startStamp, $this->getStart());
+
+        // Check each busy period and add free periods in between.
+        foreach ($this->_busyPeriods as $start => $end) {
+            if ($start <= $endStamp && $end >= $nextstart) {
+                if ($nextstart <= $start) {
+                    $periods[$nextstart] = min($start, $endStamp);
+                }
+                $nextstart = min($end, $endStamp);
+            }
+        }
+
+        // If we didn't read the end of the requested period but still have
+        // data then mark as free to the end of the period or available data.
+        if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
+            $periods[$nextstart] = min($this->getEnd(), $endStamp);
+        }
+
+        return $periods;
+    }
+
+    /**
+     * Adds a busy period to the info.
+     *
+     * This function may throw away data in case you add a period with a start
+     * date that already exists. The longer of the two periods will be chosen
+     * (and all information associated with the shorter one will be removed).
+     *
+     * @param string $type       The type of the period. Either 'FREE' or
+     *                           'BUSY'; only 'BUSY' supported at the moment.
+     * @param integer $start     The start timestamp of the period.
+     * @param integer $end       The end timestamp of the period.
+     * @param integer $duration  The duration of the period. If specified, the
+     *                           $end parameter will be ignored.
+     * @param array   $extra     Additional parameters for this busy period.
+     */
+    function addBusyPeriod($type, $start, $end = null, $duration = null,
+                           $extra = array())
+    {
+        if ($type == 'FREE') {
+            // Make sure this period is not marked as busy.
+            return false;
+        }
+
+        // Calculate the end time if duration was specified.
+        $tempEnd = is_null($duration) ? $end : $start + $duration;
+
+        // Make sure the period length is always positive.
+        $end = max($start, $tempEnd);
+        $start = min($start, $tempEnd);
+
+        if (isset($this->_busyPeriods[$start])) {
+            // Already a period starting at this time. Change the current
+            // period only if the new one is longer. This might be a problem
+            // if the callee assumes that there is no simplification going
+            // on. But since the periods are stored using the start time of
+            // the busy periods we have to throw away data here.
+            if ($end > $this->_busyPeriods[$start]) {
+                $this->_busyPeriods[$start] = $end;
+                $this->_extraParams[$start] = $extra;
+            }
+        } else {
+            // Add a new busy period.
+            $this->_busyPeriods[$start] = $end;
+            $this->_extraParams[$start] = $extra;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the timestamp of the start of the time period this free busy
+     * information covers.
+     *
+     * @return integer  A timestamp.
+     */
+    function getStart()
+    {
+        if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
+            return $this->getAttribute('DTSTART');
+        } elseif (count($this->_busyPeriods)) {
+            return min(array_keys($this->_busyPeriods));
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the timestamp of the end of the time period this free busy
+     * information covers.
+     *
+     * @return integer  A timestamp.
+     */
+    function getEnd()
+    {
+        if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
+            return $this->getAttribute('DTEND');
+        } elseif (count($this->_busyPeriods)) {
+            return max(array_values($this->_busyPeriods));
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Merges the busy periods of another Horde_iCalendar_vfreebusy object
+     * into this one.
+     *
+     * This might lead to simplification no matter what you specify for the
+     * "simplify" flag since periods with the same start date will lead to the
+     * shorter period being removed (see addBusyPeriod).
+     *
+     * @param Horde_iCalendar_vfreebusy $freebusy  A freebusy object.
+     * @param boolean $simplify                    If true, simplify() will
+     *                                             called after the merge.
+     */
+    function merge($freebusy, $simplify = true)
+    {
+        if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
+            return false;
+        }
+
+        $extra = $freebusy->getExtraParams();
+        foreach ($freebusy->getBusyPeriods() as $start => $end) {
+            // This might simplify the busy periods without taking the
+            // "simplify" flag into account.
+            $this->addBusyPeriod('BUSY', $start, $end, null,
+                                 isset($extra[$start])
+                                 ? $extra[$start] : array());
+        }
+
+        $thisattr = $this->getAttribute('DTSTART');
+        $thatattr = $freebusy->getAttribute('DTSTART');
+        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+            $this->setAttribute('DTSTART', $thatattr, array(), false);
+        } elseif (!is_a($thatattr, 'PEAR_Error')) {
+            if ($thatattr < $thisattr) {
+                $this->setAttribute('DTSTART', $thatattr, array(), false);
+            }
+        }
+
+        $thisattr = $this->getAttribute('DTEND');
+        $thatattr = $freebusy->getAttribute('DTEND');
+        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+            $this->setAttribute('DTEND', $thatattr, array(), false);
+        } elseif (!is_a($thatattr, 'PEAR_Error')) {
+            if ($thatattr > $thisattr) {
+                $this->setAttribute('DTEND', $thatattr, array(), false);
+            }
+        }
+
+        if ($simplify) {
+            $this->simplify();
+        }
+
+        return true;
+    }
+
+    /**
+     * Removes all overlaps and simplifies the busy periods array as much as
+     * possible.
+     */
+    function simplify()
+    {
+        $clean = false;
+        $busy  = array($this->_busyPeriods, $this->_extraParams);
+        while (!$clean) {
+            $result = $this->_simplify($busy[0], $busy[1]);
+            $clean = $result === $busy;
+            $busy = $result;
+        }
+
+        ksort($result[1], SORT_NUMERIC);
+        $this->_extraParams = $result[1];
+
+        ksort($result[0], SORT_NUMERIC);
+        $this->_busyPeriods = $result[0];
+    }
+
+    function _simplify($busyPeriods, $extraParams = array())
+    {
+        $checked = array();
+        $checkedExtra = array();
+        $checkedEmpty = true;
+
+        foreach ($busyPeriods as $start => $end) {
+            if ($checkedEmpty) {
+                $checked[$start] = $end;
+                $checkedExtra[$start] = isset($extraParams[$start])
+                    ? $extraParams[$start] : array();
+                $checkedEmpty = false;
+            } else {
+                $added = false;
+                foreach ($checked as $testStart => $testEnd) {
+                    // Replace old period if the new period lies around the
+                    // old period.
+                    if ($start <= $testStart && $end >= $testEnd) {
+                        // Remove old period entry.
+                        unset($checked[$testStart]);
+                        unset($checkedExtra[$testStart]);
+                        // Add replacing entry.
+                        $checked[$start] = $end;
+                        $checkedExtra[$start] = isset($extraParams[$start])
+                            ? $extraParams[$start] : array();
+                        $added = true;
+                    } elseif ($start >= $testStart && $end <= $testEnd) {
+                        // The new period lies fully within the old
+                        // period. Just forget about it.
+                        $added = true;
+                    } elseif (($end <= $testEnd && $end >= $testStart) ||
+                              ($start >= $testStart && $start <= $testEnd)) {
+                        // Now we are in trouble: Overlapping time periods. If
+                        // we allow for additional parameters we cannot simply
+                        // choose one of the two parameter sets. It's better
+                        // to leave two separated time periods.
+                        $extra = isset($extraParams[$start])
+                            ? $extraParams[$start] : array();
+                        $testExtra = isset($checkedExtra[$testStart])
+                            ? $checkedExtra[$testStart] : array();
+                        // Remove old period entry.
+                        unset($checked[$testStart]);
+                        unset($checkedExtra[$testStart]);
+                        // We have two periods overlapping. Are their
+                        // additional parameters the same or different?
+                        $newStart = min($start, $testStart);
+                        $newEnd = max($end, $testEnd);
+                        if ($extra === $testExtra) {
+                            // Both periods have the same information. So we
+                            // can just merge.
+                            $checked[$newStart] = $newEnd;
+                            $checkedExtra[$newStart] = $extra;
+                        } else {
+                            // Extra parameters are different. Create one
+                            // period at the beginning with the params of the
+                            // first period and create a trailing period with
+                            // the params of the second period. The break
+                            // point will be the end of the first period.
+                            $break = min($end, $testEnd);
+                            $checked[$newStart] = $break;
+                            $checkedExtra[$newStart] =
+                                isset($extraParams[$newStart])
+                                ? $extraParams[$newStart] : array();
+                            $checked[$break] = $newEnd;
+                            $highStart = max($start, $testStart);
+                            $checkedExtra[$break] =
+                                isset($extraParams[$highStart])
+                                ? $extraParams[$highStart] : array();
+
+                            // Ensure we also have the extra data in the
+                            // extraParams.
+                            $extraParams[$break] =
+                                isset($extraParams[$highStart])
+                                ? $extraParams[$highStart] : array();
+                        }
+                        $added = true;
+                    }
+
+                    if ($added) {
+                        break;
+                    }
+                }
+
+                if (!$added) {
+                    $checked[$start] = $end;
+                    $checkedExtra[$start] = isset($extraParams[$start])
+                        ? $extraParams[$start] : array();
+                }
+            }
+        }
+
+        return array($checked, $checkedExtra);
+    }
+
+}
+
+/**
+ * Class representing vJournals.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vjournal extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vJournal';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VJOURNAL');
+    }
+
+}
+
+
+
+
+/**
+ * Class representing vNotes.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @author  Karsten Fourmont <fourmont at gmx.de>
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vnote extends Horde_iCalendar {
+
+    function Horde_iCalendar_vnote($version = '1.1')
+    {
+        return parent::Horde_iCalendar($version);
+    }
+
+    function getType()
+    {
+        return 'vNote';
+    }
+
+    /**
+     * Unlike vevent and vtodo, a vnote is normally not enclosed in an
+     * iCalendar container. (BEGIN..END)
+     */
+    function exportvCalendar()
+    {
+        $requiredAttributes['BODY'] = '';
+        $requiredAttributes['VERSION'] = '1.1';
+
+        foreach ($requiredAttributes as $name => $default_value) {
+            if (is_a($this->getattribute($name), 'PEAR_Error')) {
+                $this->setAttribute($name, $default_value);
+            }
+        }
+
+        return $this->_exportvData('VNOTE');
+    }
+
+}
+
+/**
+ * Class representing vTimezones.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtimezone extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vTimeZone';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VTIMEZONE');
+    }
+
+    /**
+     * Parse child components of the vTimezone component. Returns an
+     * array with the exact time of the time change as well as the
+     * 'from' and 'to' offsets around the change. Time is arbitrarily
+     * based on UTC for comparison.
+     */
+    function parseChild(&$child, $year)
+    {
+        // Make sure 'time' key is first for sort().
+        $result['time'] = 0;
+
+        $t = $child->getAttribute('TZOFFSETFROM');
+        if (is_a($t, 'PEAR_Error')) {
+            return false;
+        }
+        $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+        $t = $child->getAttribute('TZOFFSETTO');
+        if (is_a($t, 'PEAR_Error')) {
+            return false;
+        }
+        $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+        $switch_time = $child->getAttribute('DTSTART');
+        if (is_a($switch_time, 'PEAR_Error')) {
+            return false;
+        }
+
+        $rrules = $child->getAttribute('RRULE');
+        if (is_a($rrules, 'PEAR_Error')) {
+            if (!is_int($switch_time)) {
+                return false;
+            }
+            // Convert this timestamp from local time to UTC for
+            // comparison (All dates are compared as if they are UTC).
+            $t = getdate($switch_time);
+            $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
+                                        $t['mon'], $t['mday'], $t['year']);
+            return $result;
+        }
+
+        $rrules = explode(';', $rrules);
+        foreach ($rrules as $rrule) {
+            $t = explode('=', $rrule);
+            switch ($t[0]) {
+            case 'FREQ':
+                if ($t[1] != 'YEARLY') {
+                    return false;
+                }
+                break;
+
+            case 'INTERVAL':
+                if ($t[1] != '1') {
+                    return false;
+                }
+                break;
+
+            case 'BYMONTH':
+                $month = intval($t[1]);
+                break;
+
+            case 'BYDAY':
+                $len = strspn($t[1], '1234567890-+');
+                if ($len == 0) {
+                    return false;
+                }
+                $weekday = substr($t[1], $len);
+                $weekdays = array(
+                    'SU' => 0,
+                    'MO' => 1,
+                    'TU' => 2,
+                    'WE' => 3,
+                    'TH' => 4,
+                    'FR' => 5,
+                    'SA' => 6
+                );
+                $weekday = $weekdays[$weekday];
+                $which = intval(substr($t[1], 0, $len));
+                break;
+
+            case 'UNTIL':
+                if (intval($year) > intval(substr($t[1], 0, 4))) {
+                    return false;
+                }
+                break;
+            }
+        }
+
+        if (empty($month) || !isset($weekday)) {
+            return false;
+        }
+
+        if (is_int($switch_time)) {
+            // Was stored as localtime.
+            $switch_time = strftime('%H:%M:%S', $switch_time);
+            $switch_time = explode(':', $switch_time);
+        } else {
+            $switch_time = explode('T', $switch_time);
+            if (count($switch_time) != 2) {
+                return false;
+            }
+            $switch_time[0] = substr($switch_time[1], 0, 2);
+            $switch_time[2] = substr($switch_time[1], 4, 2);
+            $switch_time[1] = substr($switch_time[1], 2, 2);
+        }
+
+        // Get the timestamp for the first day of $month.
+        $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
+                         $month, 1, $year);
+        // Get the day of the week for the first day of $month.
+        $first_of_month_weekday = intval(gmstrftime('%w', $when));
+
+        // Go to the first $weekday before first day of $month.
+        if ($weekday >= $first_of_month_weekday) {
+            $weekday -= 7;
+        }
+        $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
+
+        // If going backwards go to the first $weekday after last day
+        // of $month.
+        if ($which < 0) {
+            do {
+                $when += 60*60*24*7;
+            } while (intval(gmstrftime('%m', $when)) == $month);
+        }
+
+        // Calculate $weekday number $which.
+        $when += $which * 60 * 60 * 24 * 7;
+
+        $result['time'] = $when;
+
+        return $result;
+    }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_standard extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'standard';
+    }
+
+    function parsevCalendar($data)
+    {
+        parent::parsevCalendar($data, 'STANDARD');
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('STANDARD');
+    }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_daylight extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'daylight';
+    }
+
+    function parsevCalendar($data)
+    {
+        parent::parsevCalendar($data, 'DAYLIGHT');
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('DAYLIGHT');
+    }
+
+}
+
+/**
+ * Class representing vTodos.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtodo extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vTodo';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VTODO');
+    }
+
+    /**
+     * Convert this todo to an array of attributes.
+     *
+     * @return array  Array containing the details of the todo in a hash
+     *                as used by Horde applications.
+     */
+    function toArray()
+    {
+        $todo = array();
+
+        $name = $this->getAttribute('SUMMARY');
+        if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
+            $todo['name'] = $name;
+        }
+        $desc = $this->getAttribute('DESCRIPTION');
+        if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
+            $todo['desc'] = $desc;
+        }
+
+        $priority = $this->getAttribute('PRIORITY');
+        if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
+            $todo['priority'] = $priority;
+        }
+
+        $due = $this->getAttribute('DTSTAMP');
+        if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
+            $todo['due'] = $due;
+        }
+
+        return $todo;
+    }
+
+    /**
+     * Set the attributes for this todo item from an array.
+     *
+     * @param array $todo  Array containing the details of the todo in
+     *                     the same format that toArray() exports.
+     */
+    function fromArray($todo)
+    {
+        if (isset($todo['name'])) {
+            $this->setAttribute('SUMMARY', $todo['name']);
+        }
+        if (isset($todo['desc'])) {
+            $this->setAttribute('DESCRIPTION', $todo['desc']);
+        }
+
+        if (isset($todo['priority'])) {
+            $this->setAttribute('PRIORITY', $todo['priority']);
+        }
+
+        if (isset($todo['due'])) {
+            $this->setAttribute('DTSTAMP', $todo['due']);
+        }
+    }
+
+}
diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
index c99fab3..dd61372 100644
--- a/plugins/calendar/lib/calendar_ical.php
+++ b/plugins/calendar/lib/calendar_ical.php
@@ -124,7 +124,7 @@ class calendar_ical
   private function get_parser()
   {
     // use Horde:iCalendar to parse vcalendar file format
-    require_once 'Horde/iCalendar.php';
+    require_once($this->cal->home . '/lib/Horde_iCalendar.php');
 
     // set target charset for parsed events
     $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
diff --git a/plugins/calendar/lib/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh
new file mode 100755
index 0000000..d076af5
--- /dev/null
+++ b/plugins/calendar/lib/get_horde_icalendar.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+# Copy Horde_iCalendar classes and dependencies to stdout.
+# This will create a standalone copy of the classes requried for iCal parsing.
+
+SRCDIR=$1
+
+if [ ! -d "$SRCDIR" ]; then
+  echo "Usage: get_horde_icalendar.sh SRCDIR"
+  echo "Please enter a valid source directory of the Horde lib"
+  exit 1
+fi
+
+echo "<?php\n"
+echo "require_once(dirname(__FILE__) . '/Horde_Date.php');"
+
+sed 's/<?php//; s/?>//' $SRCDIR/String.php
+echo "\n"
+sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
+echo "\n"
+
+for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
+	sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
+done;
diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml
index 96bbeb0..1284430 100644
--- a/plugins/calendar/package.xml
+++ b/plugins/calendar/package.xml
@@ -64,10 +64,9 @@
 				<tasks:replace from="@name@" to="name" type="package-info"/>
 				<tasks:replace from="@package_version@" to="version" type="package-info"/>
 			</file>
-			<file name="lib/Horde_Date_Recurrence.php" role="php">
-				<tasks:replace from="@name@" to="name" type="package-info"/>
-				<tasks:replace from="@package_version@" to="version" type="package-info"/>
-			</file>
+			<file name="lib/Horde_Date.php" role="php"></file>
+			<file name="lib/Horde_Date_Recurrence.php" role="php"></file>
+			<file name="lib/Horde_iCalendar.php" role="php"></file>
 			<file name="lib/fullcalendar-rc.patch" role="data">
 				<tasks:replace from="@name@" to="name" type="package-info"/>
 				<tasks:replace from="@package_version@" to="version" type="package-info"/>


commit d54497e7cd034c9e1e4acdff3a7ba0a424abd03b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue May 1 14:12:48 2012 +0100

    Correct the URI to the source code repository

diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml
index 742e6e7..96bbeb0 100644
--- a/plugins/calendar/package.xml
+++ b/plugins/calendar/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>calendar</name>
-    <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+    <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Calendar plugin</summary>
 	<description>-</description>
 	<lead>
diff --git a/plugins/kolab_addressbook/package.xml b/plugins/kolab_addressbook/package.xml
index e077dcb..9d43c65 100644
--- a/plugins/kolab_addressbook/package.xml
+++ b/plugins/kolab_addressbook/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>kolab_addressbook</name>
-    <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+    <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Kolab addressbook</summary>
 	<description>
 	    Sample plugin to add a new address book source with data from Kolab storage.
diff --git a/plugins/kolab_auth/package.xml b/plugins/kolab_auth/package.xml
index 937798d..5213103 100644
--- a/plugins/kolab_auth/package.xml
+++ b/plugins/kolab_auth/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>kolab_auth</name>
-    <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+    <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Kolab Authentication</summary>
 	<description>
         Authenticates on LDAP server, finds canonized authentication ID for IMAP
diff --git a/plugins/kolab_config/package.xml b/plugins/kolab_config/package.xml
index 85c7faa..a0a2979 100644
--- a/plugins/kolab_config/package.xml
+++ b/plugins/kolab_config/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>kolab_config</name>
-    <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+    <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Kolab configuration storage</summary>
 	<description>
         Plugin to use Kolab server as a configuration storage. Provides an API to handle
diff --git a/plugins/kolab_core/package.xml b/plugins/kolab_core/package.xml
index fa40754..034e1b1 100644
--- a/plugins/kolab_core/package.xml
+++ b/plugins/kolab_core/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>kolab_core</name>
-	<uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+	<uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Kolab API</summary>
 	<description>
 	    Plugin to setup a basic environment for interaction with a Kolab server.
diff --git a/plugins/kolab_folders/package.xml b/plugins/kolab_folders/package.xml
index b1c364a..8042ba2 100644
--- a/plugins/kolab_folders/package.xml
+++ b/plugins/kolab_folders/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>kolab_folders</name>
-	<uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+	<uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Type-aware folder management/listing for Kolab</summary>
 	<description>
 	    The plugin extends folders handling with features of the Kolab Suite
diff --git a/plugins/kolab_zpush/package.xml b/plugins/kolab_zpush/package.xml
index 250d1a6..6147879 100644
--- a/plugins/kolab_zpush/package.xml
+++ b/plugins/kolab_zpush/package.xml
@@ -4,7 +4,7 @@
     http://pear.php.net/dtd/package-2.0
     http://pear.php.net/dtd/package-2.0.xsd">
 	<name>kolab_zpush</name>
-    <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
+    <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
 	<summary>Z-Push configuration utility for Kolab accounts</summary>
 	<description></description>
 	<lead>


commit aeb657c02b6962a20b0443e4953bf865687004b3
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Apr 26 08:55:06 2012 +0200

    Fix varname

diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index e1b2e63..29bcd2f 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -415,7 +415,7 @@ class kolab_folders extends rcube_plugin
         // Code copied from rcube_imap::_list_mailboxes()
         // Server supports LIST-EXTENDED, we can use selection options
         // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
-        if (!$this->rc->config->get('imap_force_lsub') && $imap->get_capability('LIST-EXTENDED')) {
+        if (!$this->rc->config->get('imap_force_lsub') && $storage->get_capability('LIST-EXTENDED')) {
             // This will also set mailbox options, LSUB doesn't do that
             $a_folders = $storage->conn->listMailboxes($root, $name,
                 NULL, array('SUBSCRIBED'));


commit 1067ef9827c83475c7e672a50262ed8a04b0b543
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 25 21:15:07 2012 +0200

    Allow odfviewer to hook into calendar attachments in order to display open office documents inline

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 05cee6c..b808323 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1552,19 +1552,29 @@ class calendar extends rcube_plugin
     }
 
     if ($attachment) {
-      $mimetype = strtolower($attachment['mimetype']);
-      list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+      // allow post-processing of the attachment body
+      $part = new rcube_message_part;
+      $part->filename  = $attachment['name'];
+      $part->size      = $attachment['size'];
+      $part->mimetype  = $attachment['mimetype'];
+
+      $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+        'body' => $this->driver->get_attachment_body($id, $event),
+        'mimetype' => strtolower($attachment['mimetype']),
+        'download' => !empty($_GET['_download']),
+        'part' => $part,
+      ));
 
-      $body = $this->driver->get_attachment_body($id, $event);
+      if ($plugin['abort'])
+        exit;
 
-      // TODO: allow post-processing of the attachment body
-      //$plugin = $RCMAIL->plugins->exec_hook('message_part_get',
-      //  array('uid' => $MESSAGE->uid, 'id' => $part->mime_id, 'mimetype' => $mimetype, 'part' => $part, 'download' => !empty($_GET['_download'])));
+      $mimetype = $plugin['mimetype'];
+      list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
 
       $browser = $this->rc->output->browser;
 
       // send download headers
-      if ($_GET['_download']) {
+      if ($plugin['download']) {
         header("Content-Type: application/octet-stream");
         if ($browser->ie)
           header("Content-Type: application/force-download");
@@ -1582,7 +1592,7 @@ class calendar extends rcube_plugin
       if ($mimetype == 'text/html' && empty($_GET['_download'])) {
         $OUTPUT = new rcube_html_page();
         // @TODO: use washtml on $body
-        $OUTPUT->write($body);
+        $OUTPUT->write($plugin['body']);
       }
       else {
         // don't kill the connection if download takes more than 30 sec.
@@ -1601,7 +1611,7 @@ class calendar extends rcube_plugin
         $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
         header("Content-Disposition: $disposition; filename=\"$filename\"");
 
-        echo $body;
+        echo $plugin['body'];
       }
 
       exit;
diff --git a/plugins/odfviewer/odfviewer.php b/plugins/odfviewer/odfviewer.php
index 40cf522..1e106bb 100644
--- a/plugins/odfviewer/odfviewer.php
+++ b/plugins/odfviewer/odfviewer.php
@@ -26,7 +26,7 @@
  */
 class odfviewer extends rcube_plugin
 {
-  public $task = 'mail|logout';
+  public $task = 'mail|calendar|logout';
   
   private $tempdir  = 'plugins/odfviewer/files/';
   private $tempbase = 'plugins/odfviewer/files/';
@@ -56,10 +56,9 @@ class odfviewer extends rcube_plugin
     $ua = new rcube_browser;
     if ($ua->ie && $ua->ver < 9)
       return;
-
     // extend list of mimetypes that should open in preview
     $rcmail = rcmail::get_instance();
-    if ($rcmail->action == 'preview' || $rcmail->action == 'show') {
+    if ($rcmail->action == 'preview' || $rcmail->action == 'show' || $rcmail->task == 'calendar') {
       $mimetypes = $rcmail->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
       if (!is_array($mimetypes))
         $mimetypes = explode(',', $mimetypes);
@@ -83,10 +82,15 @@ class odfviewer extends rcube_plugin
         // FIXME: copy file to disk because only apache can send the file correctly
         $tempfn = $this->tempdir . $fn;
         if (!file_exists($tempfn)) {
-          $fp = fopen($tempfn, 'w');
-          $imap = rcmail::get_instance()->get_storage();
-          $imap->get_message_part($args['uid'], $args['id'], $args['part'], false, $fp);
-          fclose($fp);
+          if ($args['body']) {
+            file_put_contents($tempfn, $args['body']);
+          }
+          else {
+            $fp = fopen($tempfn, 'w');
+            $imap = rcmail::get_instance()->get_storage();
+            $imap->get_message_part($args['uid'], $args['id'], $args['part'], false, $fp);
+            fclose($fp);
+          }
 
           // remember tempfiles in session to clean up on logout
           $_SESSION['odfviewer']['tempfiles'][] = $fn;


commit 1b09ae2801177dd4b732b4334712d28c6ac060ac
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 25 19:26:40 2012 +0200

    Finish attachment handling and display for events

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 0e61615..05cee6c 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1069,6 +1069,11 @@ class calendar extends rcube_plugin
       $settings['identity'] = array('name' => $identity['name'], 'email' => $identity['email'], 'emails' => ';' . join(';', $identity['emails']));
     }
 
+    // define list of file types which can be displayed inline
+    // same as in program/steps/mail/show.inc
+    $mimetypes = $this->rc->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
+    $settings['mimetypes'] = is_string($mimetypes) ? explode(',', $mimetypes) : (array)$mimetypes;
+
     return $settings;
   }
   
@@ -1534,12 +1539,8 @@ class calendar extends rcube_plugin
     }
 
     ob_end_clean();
-    send_nocacheing_headers();
 
-    if (isset($_SESSION['calendar_attachment']))
-      $attachment = $_SESSION['calendar_attachment'];
-    else
-      $attachment = $_SESSION['calendar_attachment'] = $this->driver->get_attachment($id, $event);
+    $attachment = $GLOBALS['calendar_attachment'] = $this->driver->get_attachment($id, $event);
 
     // show part page
     if (!empty($_GET['_frame'])) {
@@ -1550,12 +1551,16 @@ class calendar extends rcube_plugin
       exit;
     }
 
-    $this->rc->session->remove('calendar_attachment');
-
     if ($attachment) {
       $mimetype = strtolower($attachment['mimetype']);
       list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
 
+      $body = $this->driver->get_attachment_body($id, $event);
+
+      // TODO: allow post-processing of the attachment body
+      //$plugin = $RCMAIL->plugins->exec_hook('message_part_get',
+      //  array('uid' => $MESSAGE->uid, 'id' => $part->mime_id, 'mimetype' => $mimetype, 'part' => $part, 'download' => !empty($_GET['_download'])));
+
       $browser = $this->rc->output->browser;
 
       // send download headers
@@ -1573,8 +1578,6 @@ class calendar extends rcube_plugin
         header("Content-Transfer-Encoding: binary");
       }
 
-      $body = $this->driver->get_attachment_body($id, $event);
-
       // display page, @TODO: support text/plain (and maybe some other text formats)
       if ($mimetype == 'text/html' && empty($_GET['_download'])) {
         $OUTPUT = new rcube_html_page();
@@ -1614,7 +1617,7 @@ class calendar extends rcube_plugin
    */
   public function attachment_frame($attrib)
   {
-    $attachment = $_SESSION['calendar_attachment'];
+    $attachment = $GLOBALS['calendar_attachment'];
 
     $mimetype = strtolower($attachment['mimetype']);
     list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index f7683cf..063cbf5 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -260,7 +260,7 @@ function rcube_calendar_ui(settings)
       var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
 
       // open attachment in frame if it's of a supported mimetype
-      if (id && att.mimetype && $.inArray(att.mimetype, rcmail.mimetypes)>=0) {
+      if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
         rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubeeventattachment');
         if (rcmail.attachment_win) {
           window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
@@ -268,6 +268,8 @@ function rcube_calendar_ui(settings)
         }
       }
 
+      return;
+
       rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
     };
 
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 3a2a02a..7763159 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -169,14 +169,6 @@ class kolab_calendar
     return $this->storage;
   }
 
-  /**
-   * Getter for the attachment body
-   */
-  public function get_attachment_body($id)
-  {
-    return $this->storage->getAttachment($id);
-  }
-
 
   /**
    * Getter for a single event object
@@ -427,15 +419,15 @@ class kolab_calendar
     if ($record['end'] <= $record['start'] && $record['allday'])
       $record['end'] = $record['start'] + 3600;
 
-    if (!empty($rec['_attachments'])) {
-      foreach ($rec['_attachments'] as $name => $attachment) {
-        // @TODO: 'type' and 'key' are the only supported (no 'size')
-        $attachments[] = array(
-          'id' => $attachment['key'],
-          'mimetype' => $attachment['type'],
-          'name' => $name,
-        );
+    if (!empty($record['_attachments'])) {
+      foreach ($record['_attachments'] as $name => $attachment) {
+        if ($attachment !== false) {
+          $attachment['name'] = $name;
+          $attachments[] = $attachment;
+        }
       }
+
+      $record['attachments'] = $attachments;
     }
 
     $sensitivity_map = array_flip($this->sensitivity_map);
@@ -461,28 +453,31 @@ class kolab_calendar
 
     // in Horde attachments are indexed by name
     $object['_attachments'] = array();
-    if (!empty($event['attachments'])) {
+    if (is_array($event['attachments'])) {
       $collisions = array();
       foreach ($event['attachments'] as $idx => $attachment) {
         // Roundcube ID has nothing to do with Horde ID, remove it
         if ($attachment['content'])
           unset($attachment['id']);
 
-        // Horde code assumes that there will be no more than
-        // one file with the same name: make filenames unique
-        $filename = $attachment['name'];
-        if ($collisions[$filename]++) {
-          $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $filename, $m) ? $m[1] : null;
-          $attachment['name'] = basename($filename, $ext) . '-' . $collisions[$filename] . $ext;
+        // flagged for deletion => set to false
+        if ($attachment['_deleted']) {
+          $object['_attachments'][$attachment['name']] = false;
         }
+        else {
+          // Horde code assumes that there will be no more than
+          // one file with the same name: make filenames unique
+          $filename = $attachment['name'];
+          if ($collisions[$filename]++) {
+            $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $filename, $m) ? $m[1] : null;
+            $attachment['name'] = basename($filename, $ext) . '-' . $collisions[$filename] . $ext;
+          }
 
-        // set type parameter
-        if ($attachment['mimetype'])
-          $attachment['type'] = $attachment['mimetype'];
-
-        $object['_attachments'][$attachment['name']] = $attachment;
-        unset($event['attachments'][$idx]);
+          $object['_attachments'][$attachment['name']] = $attachment;
+        }
       }
+
+      unset($event['attachments']);
     }
 
     // translate sensitivity property
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 7744dc0..aea6783 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -579,7 +579,7 @@ class kolab_driver extends calendar_driver
         if (!empty($old['attachments'])) {
           foreach ($old['attachments'] as $idx => $att) {
             if ($att['id'] == $attachment) {
-              unset($old['attachments'][$idx]);
+              $old['attachments'][$idx]['_deleted'] = true;
             }
           }
         }
@@ -592,12 +592,12 @@ class kolab_driver extends calendar_driver
         // skip entries without content (could be existing ones)
         if (!$attachment['data'] && !$attachment['path'])
           continue;
-        // we'll read file contacts into memory, Horde/Kolab classes does the same
-        // So we cannot save memory, rcube_imap class can do this better
+
         $attachments[] = array(
           'name' => $attachment['name'],
-          'type' => $attachment['mimetype'],
-          'content' => $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']),
+          'mimetype' => $attachment['mimetype'],
+          'content' => $attachment['data'],
+          'path' => $attachment['path'],
         );
       }
     }
@@ -625,7 +625,7 @@ class kolab_driver extends calendar_driver
         // copy attachment data to new event
         foreach ((array)$event['attachments'] as $idx => $attachment) {
           if (!$attachment['data'])
-            $attachment['data'] = $fromcalendar->get_attachment_body($attachment['id']);
+            $attachment['data'] = $fromcalendar->get_attachment_body($attachment['id'], $event);
         }
         
         $success = $storage->insert_event($event);
@@ -875,13 +875,14 @@ class kolab_driver extends calendar_driver
 
   /**
    * Get attachment body
+   * @see calendar_driver::get_attachment_body()
    */
   public function get_attachment_body($id, $event)
   {
-    if (!($storage = $this->calendars[$event['calendar']]))
+    if (!($cal = $this->calendars[$event['calendar']]))
       return false;
 
-    return $storage->get_attachment_body($id);
+    return $cal->storage->get_attachment($event['id'], $id);
   }
 
   /**
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index c740e4a..ba8cc83 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -657,13 +657,13 @@ class calendar_ui
 
     if (!empty($this->cal->attachment['name'])) {
       $table->add('title', Q(rcube_label('filename')));
-      $table->add(null, Q($this->cal->attachment['name']));
-      $table->add(null, '[' . html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))) . ']');
+      $table->add('header', Q($this->cal->attachment['name']));
+      $table->add('download-link', html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))));
     }
 
     if (!empty($this->cal->attachment['size'])) {
       $table->add('title', Q(rcube_label('filesize')));
-      $table->add(null, Q(show_bytes($this->cal->attachment['size'])));
+      $table->add('header', Q(show_bytes($this->cal->attachment['size'])));
     }
 
     return $table->show($attrib);
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index a95d0fc..cba1149 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -286,39 +286,45 @@ a.miniColors-trigger {
 
 #attachmentcontainer {
 	position: absolute;
-	top: 80px;
-	left: 20px;
-	right: 20px;
-	bottom: 20px;
+	top: 60px;
+	left: 0px;
+	right: 0px;
+	bottom: 0px;
 }
 
 #attachmentframe {
 	width: 100%;
 	height: 100%;
-	border: 1px solid #999999;
-	background-color: #F9F9F9;
+	border: 0;
+	background-color: #fff;
+	border-radius: 4px;
 }
 
 #partheader {
-	position: absolute;
-	top: 20px;
-	left: 220px;
-	right: 20px;
-	height: 40px;
+	position: relative;
+	padding: 3px 0;
+	background: #f9f9f9;
+	background: -moz-linear-gradient(top, #fff 0%, #e9e9e9 100%);
+	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fff), color-stop(100%,#e9e9e9));
+	background: -o-linear-gradient(top, #fff 0%, #e9e9e9 100%);
+	background: -ms-linear-gradient(top, #fff 0%, #e9e9e9 100%);
+	background: linear-gradient(top, #fff 0%, #e9e9e9 100%);
 }
 
 #partheader table td {
-	padding-left: 2px;
-	padding-right: 4px;
-	vertical-align: middle;
-	font-size: 11px;
+	color: #666;
+	padding: 2px 8px;
 }
 
-#partheader table td.title {
-	color: #666;
+#partheader table td.header {
 	font-weight: bold;
 }
 
+#partheader table td.title a {
+	color: #666;
+	text-decoration: none;
+}
+
 #edit-attachments {
 	margin-top: 0.6em;
 }
diff --git a/plugins/calendar/skins/larry/templates/attachment.html b/plugins/calendar/skins/larry/templates/attachment.html
index 439afd4..4d4789d 100644
--- a/plugins/calendar/skins/larry/templates/attachment.html
+++ b/plugins/calendar/skins/larry/templates/attachment.html
@@ -26,7 +26,7 @@
 	</div>
 
 	<div id="attachmentcontainer" class="uibox">
-	<roundcube:object name="plugin.attachmentframe" id="attachmentframe" style="width:100%; height:100%" />
+	<roundcube:object name="plugin.attachmentframe" id="attachmentframe" class="header-table" style="width:100%" />
 	</div>
 
 </div>
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index ebfa29a..7695402 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1057,7 +1057,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
             // only fetch photo content if requested
             if ($this->action == 'photo')
-                $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['key']);
+                $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['id']);
         }
 
         // truncate publickey value for display
@@ -1123,7 +1123,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         if ($contact['photo']) {
           $attkey = 'photo.attachment';
           $contact['_attachments'][$attkey] = array(
-            'type' => rc_image_content_type($contact['photo']),
+            'mimetype' => rc_image_content_type($contact['photo']),
             'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
           );
           $contact['photo'] = $attkey;
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 97718ef..78e51ca 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -280,7 +280,17 @@ class kolab_format_event extends kolab_format
         }
         $this->obj->setAlarms($valarms);
 
-        // TODO: save attachments
+        // save attachments
+        $vattach = new vectorattachment;
+        foreach ((array)$object['_attachments'] as $name => $attr) {
+            if (empty($attr))
+                continue;
+            $attach = new Attachment;
+            $attach->setLabel($name);
+            $attach->setUri('cid:' . $name, $attr['mimetype']);
+            $vattach->push($attach);
+        }
+        $this->obj->setAttachments($vattach);
 
         // cache this data
         unset($object['_formatobj']);
@@ -425,7 +435,22 @@ class kolab_format_event extends kolab_format
             }
         }
 
-        // TODO: handle attachments
+        // handle attachments
+        $vattach = $this->obj->attachments();
+        for ($i=0; $i < $vattach->size(); $i++) {
+            $attach = $vattach->get($i);
+
+            // skip cid: attachments which are mime message parts handled by kolab_storage_folder
+            if (substr($attach->uri(), 0, 4) != 'cid') {
+                $name = $attach->label();
+                $data = $attach->data();
+                $object['_attachments'][$name] = array(
+                    'mimetype' => $attach->mimetype(),
+                    'size' => strlen($data),
+                    'content' => $data,
+                );
+            }
+        }
 
         $this->data = $object;
         return $this->data;
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 6a010e5..73b6dbb 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -350,8 +350,8 @@ class kolab_storage_folder
             }
             else if ($part->filename) {
                 $attachments[$part->filename] = array(
-                    'key' => $part->mime_id,
-                    'type' => $part->mimetype,
+                    'id' => $part->mime_id,
+                    'mimetype' => $part->mimetype,
                     'size' => $part->size,
                 );
             }
@@ -388,13 +388,10 @@ class kolab_storage_folder
         }
 
         if ($format->is_valid()) {
-            if ($formatobj)
-                return $format;
-
             $object = $format->to_array();
             $object['_msguid'] = $msguid;
             $object['_mailbox'] = $this->name;
-            $object['_attachments'] = $attachments;
+            $object['_attachments'] = array_merge((array)$object['_attachments'], $attachments);
             $object['_formatobj'] = $format;
 
             $this->objcache[$msguid] = $object;
@@ -425,8 +422,8 @@ class kolab_storage_folder
                     $object['_attachments'][$name] = $old['_attachments'][$name];
                 }
                 // load photo.attachment from old Kolab2 format to be directly embedded in xcard block
-                if ($name == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$name]['content'] && $att['key']) {
-                    $object['photo'] = $this->get_attachment($object['_msguid'], $att['key'], $object['_mailbox']);
+                if ($name == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$name]['content'] && $att['id']) {
+                    $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
                     unset($object['_attachments'][$name]);
                 }
             }
@@ -601,12 +598,15 @@ class kolab_storage_folder
         // save object attachments as separate parts
         // TODO: optimize memory consumption by using tempfiles for transfer
         foreach ((array)$object['_attachments'] as $name => $att) {
-            if (empty($att['content']) && !empty($att['key'])) {
+            if (empty($att['content']) && !empty($att['id'])) {
                 $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
-                $att['content'] = $this->get_attachment($msguid, $att['key'], $object['_mailbox']);
+                $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox']);
             }
             if (!empty($att['content'])) {
-                $mime->addAttachment($att['content'], $att['type'], $name, false);
+                $mime->addAttachment($att['content'], $att['mimetype'], $name, false);
+            }
+            else if (!empty($att['path'])) {
+                $mime->addAttachment($att['path'], $att['mimetype'], $name, true);
             }
         }
 
@@ -681,7 +681,7 @@ class kolab_storage_folder
      */
     public function getOwner()
     {
-        console("Call to deprecated method kolab_storage_folder::getOwner()");
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::getOwner()");
         return $this->get_owner();
     }
 


commit c9664e2bb7dafcb2d5c947ac6da46288d035c744
Merge: a6ffdf9 2a472e4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 25 14:13:39 2012 +0200

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit a6ffdf9ad66939108b46ce7f0dd08a7383ca42f1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 25 14:12:09 2012 +0200

    Fix/improve crypto key handling in contact records

diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index ef6d11a..3e1d200 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -292,7 +292,8 @@ class kolab_addressbook extends rcube_plugin
             $p['form']['personal']['content']['profession']    = array('size' => 40);
             $p['form']['personal']['content']['children']      = array('size' => 40);
             $p['form']['personal']['content']['freebusyurl']   = array('size' => 40);
-            $p['form']['personal']['content']['pgppublickey']  = array('size' => 40);
+            $p['form']['personal']['content']['pgppublickey']  = array('size' => 70);
+            $p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);
 
             // re-order fields according to the coltypes list
             $p['form']['contact']['content']  = $this->_sort_form_fields($p['form']['contact']['content']);
@@ -302,8 +303,9 @@ class kolab_addressbook extends rcube_plugin
             $p['form']['settings'] = array(
                 'name'    => $this->gettext('settings'),
                 'content' => array(
-                    'pgppublickey' => array('size' => 40, 'visible' => true),
                     'freebusyurl'  => array('size' => 40, 'visible' => true),
+                    'pgppublickey' => array('size' => 70, 'visible' => true),
+                    'pkcs7publickey' => array('size' => 70, 'visible' => false),
                 )
             );
             */
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 0c1a821..ebfa29a 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -63,9 +63,11 @@ class rcube_kolab_contacts extends rcube_addressbook
                                 'label' => 'kolab_addressbook.freebusyurl'),
       'pgppublickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
                                 'label' => 'kolab_addressbook.pgppublickey'),
+      'pkcs7publickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
+                                'label' => 'kolab_addressbook.pkcs7publickey'),
       'notes'        => array(),
       'photo'        => array(),
-      // TODO: define more Kolab-specific fields such as: language, latitude, longitude
+      // TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
     );
 
     /**
diff --git a/plugins/kolab_addressbook/localization/de_CH.inc b/plugins/kolab_addressbook/localization/de_CH.inc
index f91a24b..3439448 100644
--- a/plugins/kolab_addressbook/localization/de_CH.inc
+++ b/plugins/kolab_addressbook/localization/de_CH.inc
@@ -5,7 +5,8 @@ $labels['initials'] = 'Initialen';
 $labels['profession'] = 'Berufsbezeichnung';
 $labels['officelocation'] = 'Büro Adresse';
 $labels['children'] = 'Kinder';
-$labels['pgppublickey'] = 'Öffentlicher PGP-Schlüssel';
+$labels['pgppublickey'] = 'PGP-Schlüssel';
+$labels['pkcs7publickey'] = 'S/MIME-Schlüssel';
 $labels['freebusyurl'] = 'Frei/Belegt URL';
 $labels['typebusiness'] = 'Dienstlich';
 $labels['typebusinessfax'] = 'Dienst';
diff --git a/plugins/kolab_addressbook/localization/de_DE.inc b/plugins/kolab_addressbook/localization/de_DE.inc
index 5fd86b7..2c2a5d2 100644
--- a/plugins/kolab_addressbook/localization/de_DE.inc
+++ b/plugins/kolab_addressbook/localization/de_DE.inc
@@ -5,7 +5,8 @@ $labels['initials'] = 'Initialen';
 $labels['profession'] = 'Berufsbezeichnung';
 $labels['officelocation'] = 'Büro Adresse';
 $labels['children'] = 'Kinder';
-$labels['pgppublickey'] = 'Öffentlicher PGP-Schlüssel';
+$labels['pgppublickey'] = 'PGP-Schlüssel';
+$labels['pkcs7publickey'] = 'S/MIME-Schlüssel';
 $labels['freebusyurl'] = 'Frei/Belegt URL';
 $labels['typebusiness'] = 'Dienstlich';
 $labels['typebusinessfax'] = 'Dienst';
diff --git a/plugins/kolab_addressbook/localization/en_US.inc b/plugins/kolab_addressbook/localization/en_US.inc
index 36f2139..a66426f 100644
--- a/plugins/kolab_addressbook/localization/en_US.inc
+++ b/plugins/kolab_addressbook/localization/en_US.inc
@@ -5,7 +5,8 @@ $labels['initials'] = 'Initials';
 $labels['profession'] = 'Profession';
 $labels['officelocation'] = 'Office location';
 $labels['children'] = 'Children';
-$labels['pgppublickey'] = 'PGP publickey';
+$labels['pgppublickey'] = 'PGP public key';
+$labels['pkcs7publickey'] = 'S/MIME public key';
 $labels['freebusyurl'] = 'Free-busy URL';
 $labels['typebusiness'] = 'Business';
 $labels['typebusinessfax'] = 'Business Fax';
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 6df3502..69db2d1 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -270,30 +270,31 @@ class kolab_format_contact extends kolab_format
         }
         $this->obj->setRelateds($rels);
 
-        if (isset($object['pgppublickey'])) {
-            $replace = -1;
-            $keys = $this->obj->keys();
-            if (!is_object($keys))
-                $keys = new vectorkey;
-
-            for ($i=0; $i < $keys->size(); $i++) {
-                $key = $keys->get($i);
-                if ($key->type() == Key::PGP) {
-                    $replace = $i;
-                    break;
-                }
-            }
-
-            // insert/replace pgp key entry
-            $key = new Key($object['pgppublickey'], Key::PGP);
-            if ($replace >= 0)
-                $keys->set($replace, $key);
-            else
-                $keys->push($key);
-            
-            $this->obj->setKeys($keys);
+        // insert/replace crypto keys
+        $pgp_index = $pkcs7_index = -1;
+        $keys = $this->obj->keys();
+        for ($i=0; $i < $keys->size(); $i++) {
+            $key = $keys->get($i);
+            if ($pgp_index < 0 && $key->type() == Key::PGP)
+                $pgp_index = $i;
+            else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
+                $pkcs7_index = $i;
         }
 
+        $pgpkey   = $object['pgppublickey']   ? new Key($object['pgppublickey'], Key::PGP) : new Key();
+        $pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
+
+        if ($pgp_index >= 0)
+            $keys->set($pgp_index, $pgpkey);
+        else if (!empty($object['pgppublickey']))
+            $keys->push($pgpkey);
+        if ($pkcs7_index >= 0)
+            $keys->set($pkcs7_index, $pkcs7key);
+        else if (!empty($object['pkcs7publickey']))
+            $keys->push($pkcs7key);
+
+        $this->obj->setKeys($keys);
+
         // TODO: handle language, gpslocation, etc.
 
 
@@ -389,13 +390,14 @@ class kolab_format_contact extends kolab_format
         // relateds -> spouse, children
         $this->read_relateds($this->obj->relateds(), $object);
 
-        // crypto settings: currently only pgpkey is supported
+        // crypto settings: currently only key values are supported
         $keys = $this->obj->keys();
         for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
             $key = $keys->get($i);
-            if ($key->type() == Key::PGP) {
+            if ($key->type() == Key::PGP)
                 $object['pgppublickey'] = $key->key();
-            }
+            else if ($key->type() == Key::PKCS7_MIME)
+                $object['pkcs7publickey'] = $key->key();
         }
 
         $this->data = $object;


commit 2a472e41b5722b6c0994f78c23643a7af1e64a04
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Wed Apr 25 13:27:13 2012 +0200

    Use rcube instead of rcmail, so the classes can be used out of Roundcube

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index d42dc1e..689b438 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -44,10 +44,10 @@ class kolab_storage
         if (self::$ready)
             return true;
 
-        $rcmail = rcmail::get_instance();
+        $rcmail = rcube::get_instance();
         self::$config = $rcmail->config;
         self::$imap = $rcmail->get_storage();
-        self::$ready = class_exists('kolabformat') && $rcmail->storage_connect() &&
+        self::$ready = class_exists('kolabformat') &&
             (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
 
         if (self::$ready) {
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 050ff32..6a010e5 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -53,7 +53,7 @@ class kolab_storage_folder
      */
     function __construct($name, $imap = null)
     {
-        $this->imap = is_object($imap) ? $imap : rcmail::get_instance()->get_storage();
+        $this->imap = is_object($imap) ? $imap : rcube::get_instance()->get_storage();
         $this->imap->set_options(array('skip_deleted' => false));
         $this->set_folder($name);
     }
@@ -124,7 +124,7 @@ class kolab_storage_folder
             return $this->owner;
 
         $info = $this->get_folder_info();
-        $rcmail = rcmail::get_instance();
+        $rcmail = rcube::get_instance();
 
         switch ($info['namespace']) {
         case 'personal':
@@ -571,7 +571,7 @@ class kolab_storage_folder
         }
 
         $mime = new Mail_mime("\r\n");
-        $rcmail = rcmail::get_instance();
+        $rcmail = rcube::get_instance();
         $headers = array();
 
         if ($ident = $rcmail->user->get_identity()) {
@@ -581,7 +581,7 @@ class kolab_storage_folder
         $headers['Date'] = date('r');
         $headers['X-Kolab-Type'] = self::KTYPE_PREFIX . $type;
         $headers['Subject'] = $object['uid'];
-//        $headers['Message-ID'] = rcmail_gen_message_id();
+//        $headers['Message-ID'] = $rcmail->gen_message_id();
         $headers['User-Agent'] = $rcmail->config->get('useragent');
 
         $mime->headers($headers);


commit 1a60eea869f3bb3938968cd8b1fd4817290f7b8d
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 24 10:18:48 2012 +0200

    Don't use REPLACE which is mysql only (#650)

diff --git a/plugins/calendar/lib/calendar_itip.php b/plugins/calendar/lib/calendar_itip.php
index 6507b51..8008aae 100644
--- a/plugins/calendar/lib/calendar_itip.php
+++ b/plugins/calendar/lib/calendar_itip.php
@@ -239,10 +239,11 @@ class calendar_itip
     if ($stored[$base])
       return $token;
 
-    // @TODO: REPLACE works only with MySQL
+    // delete old entry
+    $this->rc->db->query("DELETE FROM itipinvitations WHERE token=?", $base);
 
     $query = $this->rc->db->query(
-      "REPLACE INTO itipinvitations
+      "INSERT INTO itipinvitations
        (token, event_uid, user_id, event, expires)
        VALUES(?, ?, ?, ?, ?)",
       $base,


commit 3d704ab521731a1626de8a88e889f4b60f3a06d4
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 24 10:14:01 2012 +0200

    Don't use REPLACE INTO which is mysql only (#650)

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 62f34b8..7744dc0 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -821,15 +821,18 @@ class kolab_driver extends calendar_driver
    */
   public function dismiss_alarm($event_id, $snooze = 0)
   {
+    // delete old alarm entry
+    $this->rc->db->query("DELETE FROM kolab_alarms WHERE event_id=?", $event_id);
+
     // set new notifyat time or unset if not snoozed
     $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
-    
+
     $query = $this->rc->db->query(
-      "REPLACE INTO kolab_alarms
+      "INSERT INTO kolab_alarms
        (event_id, dismissed, notifyat)
        VALUES(?, ?, ?)",
       $event_id,
-      $snooze > 0 ? 0 : 1, 
+      $snooze > 0 ? 0 : 1,
       $notifyat
     );
     


commit 7b2b720a68e7646f241311d9905987971f3672ac
Merge: bbf3901 2996832
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 24 09:30:26 2012 +0200

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit bbf39013080854dc4b09fe928fbd7c5c3a252783
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 24 09:30:04 2012 +0200

    Implement free/busy triggering

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 8f267dc..571afc6 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -605,34 +605,53 @@ class kolab_storage_folder
     public function trigger()
     {
         $owner = $this->get_owner();
+        $result = false;
 
         switch($this->type) {
         case 'event':
-            $url = sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), $owner, $this->subpath);
+            if ($this->get_namespace() == 'personal') {
+                $result = $this->trigger_url(
+                    sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), $owner, $this->imap->mod_folder($this->name)),
+                    $this->imap->options['user'],
+                    $this->imap->options['password']
+                );
+            }
             break;
 
         default:
             return true;
         }
 
-        $result = $this->trigger_url($url);
-        if (is_a($result, 'PEAR_Error')) {
+        if ($result && is_a($result, 'PEAR_Error')) {
             return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
                                             $this->name, $result->getMessage()));
         }
+
         return $result;
     }
 
     /**
      * Triggers a URL.
      *
-     * @param string $url The URL to be triggered.
-     * @return boolean|PEAR_Error True if successfull.
+     * @param string $url          The URL to be triggered.
+     * @param string $auth_user    Username to authenticate with
+     * @param string $auth_passwd  Password for basic auth
+     * @return boolean|PEAR_Error  True if successfull.
      */
-    private function trigger_url($url)
+    private function trigger_url($url, $auth_user = null, $auth_passwd = null)
     {
-        // TBD.
-        return PEAR::raiseError("Feature not implemented.");
+        require_once('HTTP/Request.php');
+
+        $request = new HTTP_Request($url);
+
+        // set authentication credentials
+        if ($auth_user && $auth_passwd)
+            $request->setBasicAuth($auth_user, $auth_passwd);
+
+        $result = $request->sendRequest(true);
+        // rcube::write_log('trigger', $request->getResponseBody());
+
+        return $result;
     }
 
 
@@ -652,6 +671,7 @@ class kolab_storage_folder
      */
     public function getMyRights()
     {
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::getMyRights()");
         return $this->get_myrights();
     }
 
@@ -660,6 +680,7 @@ class kolab_storage_folder
      */
     public function getData()
     {
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::getData()");
         return $this;
     }
 
@@ -668,6 +689,7 @@ class kolab_storage_folder
      */
     public function getObjects($type = null)
     {
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::getObjects()");
         return $this->get_objects($type);
     }
 
@@ -676,6 +698,7 @@ class kolab_storage_folder
      */
     public function getObject($uid)
     {
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::getObject()");
         return $this->get_object($uid);
     }
 
@@ -693,6 +716,7 @@ class kolab_storage_folder
      */
     public function deleteMessage($id, $trigger = true, $expunge = true)
     {
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::deleteMessage()");
         return $this->delete(array('_msguid' => $id), $trigger, $expunge);
     }
 
@@ -701,6 +725,7 @@ class kolab_storage_folder
      */
     public function deleteAll()
     {
+        PEAR::raiseError("Call to deprecated method kolab_storage_folder::deleteAll()");
         return $this->delete_all();
     }
 


commit 299683278916630dca75f342968a160fffe3a4cd
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sat Apr 21 20:01:25 2012 +0200

    Fix fatal errors after changes in 607fd7b4

diff --git a/plugins/kolab_zpush/kolab_zpush.php b/plugins/kolab_zpush/kolab_zpush.php
index f19fe3a..4ef242c 100644
--- a/plugins/kolab_zpush/kolab_zpush.php
+++ b/plugins/kolab_zpush/kolab_zpush.php
@@ -177,7 +177,6 @@ class kolab_zpush extends rcube_plugin
             break;
 
         case 'delete':
-            $this->init_imap();
             $devices = $this->list_devices();
             
             if ($device = $devices[$imei]) {
@@ -264,6 +263,7 @@ class kolab_zpush extends rcube_plugin
     public function list_devices()
     {
         if (!isset($this->devices)) {
+            $this->init_imap();
             $this->devices = (array)$this->root_meta['DEVICE'];
         }
         


commit 8d5bf112fa066bd0460ba6e66fb55b342896d85a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sat Apr 21 18:54:39 2012 +0200

    Fix merge conflict

diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 54849aa..a95d0fc 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1242,11 +1242,7 @@ fieldset #calendarcategories div {
 /* Invitation UI in mail */
 
 #messagemenu li a.calendarlink span.calendar {
-<<<<<<< HEAD
-	background-position: 0 -1949px;
-=======
 	background-position: 0px -1948px;
->>>>>>> e666187312ca04384a28a77bf1836db79a97ca52
 }
 
 div.calendar-invitebox {


commit e64c62462270cf5406cadbcead7a4c2a986865dc
Merge: 8dc20fb e666187
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sat Apr 21 18:53:03 2012 +0200

    Don't exclude larry skin

diff --cc plugins/calendar/.gitignore
index 93262bc,93262bc..7c2f14c
--- a/plugins/calendar/.gitignore
+++ b/plugins/calendar/.gitignore
@@@ -4,4 -4,4 +4,5 @@@
  *~
  config.inc.php
  skins/*
--!skins/default
++!skins/default
++!skins/larry
diff --cc plugins/calendar/skins/larry/calendar.css
index e5152b3,488f37c..54849aa
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@@ -1242,7 -1244,7 +1242,11 @@@ fieldset #calendarcategories div 
  /* Invitation UI in mail */
  
  #messagemenu li a.calendarlink span.calendar {
++<<<<<<< HEAD
 +	background-position: 0 -1949px;
++=======
+ 	background-position: 0px -1948px;
++>>>>>>> e666187312ca04384a28a77bf1836db79a97ca52
  }
  
  div.calendar-invitebox {


commit 66915007d41d775778d3a9ef1fabe52653cc20e0
Author: Thomas B <roundcube at gmail.com>
Date:   Sat Apr 21 18:43:33 2012 +0200

    Make use of Roundcube's IMAP chaching

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index ea08224..3a2a02a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -69,7 +69,7 @@ class kolab_calendar
         $this->alarms = true;
       }
       else {
-        $rights = $this->storage->get_acl();
+        $rights = $this->storage->get_myrights();
         if ($rights && !PEAR::isError($rights)) {
           if (strpos($rights, 'i') !== false)
             $this->readonly = false;
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 772860c..8f267dc 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -167,9 +167,14 @@ class kolab_storage_folder
      *
      * @return string  Permissions as string
      */
-    public function get_acl()
+    public function get_myrights()
     {
-        return join('', (array)$this->imap->get_acl($this->name));
+        $rights = $this->info['rights'];
+
+        if (!is_array($rights))
+            $rights = $this->imap->my_rights($this->name);
+
+        return join('', (array)$rights);
     }
 
 
@@ -225,13 +230,18 @@ class kolab_storage_folder
     {
         if (!$type) $type = $this->type;
 
-        // search by object type
         $ctype  = self::KTYPE_PREFIX . $type;
-        $search = 'UNDELETED HEADER X-Kolab-Type ' . $ctype;
-
-        $index = $this->imap->search_once($this->name, $search);
         $results = array();
 
+        // use 'list' for folder's default objects
+        if ($type == $this->type) {
+            $index = $this->imap->index($this->name);
+        }
+        else {  // search by object type
+            $search = 'UNDELETED HEADER X-Kolab-Type ' . $ctype;
+            $index = $this->imap->search_once($this->name, $search);
+        }
+
         // fetch all messages from IMAP
         foreach ($index->get() as $msguid) {
             if ($object = $this->read_object($msguid, $type)) {
@@ -298,13 +308,19 @@ class kolab_storage_folder
     {
         if (!$type) $type = $this->type;
         if (!$folder) $folder = $this->name;
-        $ctype= self::KTYPE_PREFIX . $type;
+        $ctype = self::KTYPE_PREFIX . $type;
 
-        // requested message not in local cache
+        // requested message in local cache
         if ($this->objcache[$msguid])
             return $this->objcache[$msguid];
 
         $this->imap->set_folder($folder);
+
+        // check ctype header and abort on mismatch
+        $headers = $this->imap->get_message_headers($msguid);
+        if ($headers->others['x-kolab-type'] != $ctype)
+            return false;
+
         $message = new rcube_message($msguid);
         $attachments = array();
 
@@ -636,7 +652,7 @@ class kolab_storage_folder
      */
     public function getMyRights()
     {
-        return $this->get_acl();
+        return $this->get_myrights();
     }
 
     /**
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index eefc621..23816a6 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -38,6 +38,8 @@ class libkolab extends rcube_plugin
         // require kolab_folders plugin for listing folders by type (annotation)
         $this->require_plugin('kolab_folders');
 
+        $this->add_hook('storage_init', array($this, 'storage_init'));
+
         // extend include path to load bundled lib classes
         $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
         set_include_path($include_path);
@@ -55,5 +57,16 @@ class libkolab extends rcube_plugin
         String::setDefaultCharset('UTF-8');
     }
 
+    /**
+     * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
+     */
+    function storage_init($p)
+    {
+        $rcmail = rcmail::get_instance();
+        $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE');
+
+        return $p;
+    }
+
 
 }


commit 750b4f15ca1d1546751f9b8d397b1aa359d9b7ff
Author: Thomas B <roundcube at gmail.com>
Date:   Sat Apr 21 18:23:11 2012 +0200

    Adapt to yet another set of changes in libkolabxml

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 30a8ebd..0c1a821 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -47,7 +47,7 @@ class rcube_kolab_contacts extends rcube_addressbook
       'email'        => array('subtypes' => null),
       'phone'        => array(),
       'address'      => array('subtypes' => array('home','work','office')),
-      'website'      => array('subtypes' => null),
+      'website'      => array('subtypes' => array('homepage','blog')),
       'im'           => array('subtypes' => null),
       'gender'       => array('limit' => 1),
       'birthday'     => array('limit' => 1),
@@ -120,7 +120,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                 $this->readonly = false;
             }
             else {
-                $rights = $this->storagefolder->get_acl();
+                $rights = $this->storagefolder->get_myrights();
                 if (!PEAR::isError($rights)) {
                     if (strpos($rights, 'i') !== false)
                         $this->readonly = false;
@@ -1027,6 +1027,15 @@ class rcube_kolab_contacts extends rcube_addressbook
             }
         }
 
+        if (is_array($record['website'])) {
+            $urls = $record['website'];
+            unset($record['website']);
+            foreach ((array)$urls as $i => $url) {
+                $key = 'website' . ($url['type'] ? ':' . $url['type'] : '');
+                $record[$key][] = $url['url'];
+            }
+        }
+
         if (is_array($record['address'])) {
             $addresses = $record['address'];
             unset($record['address']);
@@ -1068,8 +1077,15 @@ class rcube_kolab_contacts extends rcube_addressbook
             $contact['uid'] = $old['uid'];
 
         $contact['email'] = array_filter($this->get_col_values('email', $contact, true));
-        $contact['website'] = array_filter($this->get_col_values('website', $contact, true));
         $contact['im'] = array_filter($this->get_col_values('im', $contact, true));
+        
+        foreach ($this->get_col_values('website', $contact) as $type => $values) {
+            foreach ((array)$values as $url) {
+                if (!empty($url)) {
+                    $contact['website'][] = array('url' => $url, 'type' => $type);
+                }
+            }
+        }
 
         foreach ($this->get_col_values('phone', $contact) as $type => $values) {
             foreach ((array)$values as $phone) {
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 02e3e89..6df3502 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -188,7 +188,13 @@ class kolab_format_contact extends kolab_format
         // email, im, url
         $this->obj->setEmailAddresses(self::array2vector($object['email']));
         $this->obj->setIMaddresses(self::array2vector($object['im']));
-        $this->obj->setUrls(self::array2vector($object['website']));
+
+        $vurls = new vectorurl;
+        foreach ((array)$object['website'] as $url) {
+            $type = $url['type'] == 'blog' ? Url::Blog : Url::None;
+            $vurls->push(new Url($url['url'], $type));
+        }
+        $this->obj->setUrls($vurls);
 
         // addresses
         $adrs = new vectoraddress;
@@ -265,9 +271,27 @@ class kolab_format_contact extends kolab_format
         $this->obj->setRelateds($rels);
 
         if (isset($object['pgppublickey'])) {
-            $crypto = new Crypto;
-            $crypto->setPGPKey($object['pgppublickey']);
-            $this->obj->setCrypto($crypto);
+            $replace = -1;
+            $keys = $this->obj->keys();
+            if (!is_object($keys))
+                $keys = new vectorkey;
+
+            for ($i=0; $i < $keys->size(); $i++) {
+                $key = $keys->get($i);
+                if ($key->type() == Key::PGP) {
+                    $replace = $i;
+                    break;
+                }
+            }
+
+            // insert/replace pgp key entry
+            $key = new Key($object['pgppublickey'], Key::PGP);
+            if ($replace >= 0)
+                $keys->set($replace, $key);
+            else
+                $keys->push($key);
+            
+            $this->obj->setKeys($keys);
         }
 
         // TODO: handle language, gpslocation, etc.
@@ -325,7 +349,13 @@ class kolab_format_contact extends kolab_format
 
         $object['email']   = self::vector2array($this->obj->emailAddresses());
         $object['im']      = self::vector2array($this->obj->imAddresses());
-        $object['website'] = self::vector2array($this->obj->urls());
+
+        $urls = $this->obj->urls();
+        for ($i=0; $i < $urls->size(); $i++) {
+            $url = $urls->get($i);
+            $subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
+            $object['website'][] = array('url' => $url->url(), 'type' => $subtype);
+        }
 
         // addresses
         $this->read_addresses($this->obj->addresses(), $object);
@@ -360,9 +390,13 @@ class kolab_format_contact extends kolab_format
         $this->read_relateds($this->obj->relateds(), $object);
 
         // crypto settings: currently only pgpkey is supported
-        $crypto = $this->obj->crypto();
-        if ($pgpkey = $crypto->pgpKey())
-            $object['pgppublickey'] = $pgpkey;
+        $keys = $this->obj->keys();
+        for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
+            $key = $keys->get($i);
+            if ($key->type() == Key::PGP) {
+                $object['pgppublickey'] = $key->key();
+            }
+        }
 
         $this->data = $object;
         return $this->data;


commit 8dc20fb1ebaabdf78f8d1b4154af0d1a174439f8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Apr 15 13:49:47 2012 +0200

    Use icon from larry skin

diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 79635bb..e5152b3 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1242,7 +1242,7 @@ fieldset #calendarcategories div {
 /* Invitation UI in mail */
 
 #messagemenu li a.calendarlink span.calendar {
-/*	background-position: 7px -109px; */
+	background-position: 0 -1949px;
 }
 
 div.calendar-invitebox {


commit 11dbdf83366fc7493976885dba3be54c6e11e11c
Merge: 131b690 2559d6f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sat Apr 14 17:40:59 2012 +0200

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit 131b690e3c30004f6fbc5017c508b32f664f3102
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sat Apr 14 17:40:51 2012 +0200

    Add missing method to subscribe to kolab folders

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 9c2cf83..fa2d697 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -197,6 +197,25 @@ class kolab_storage_folder
         return false;
     }
 
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     * @param string  Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed, $type = 0)
+    {
+        if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
+            return $subscribed ? $this->imap->subscribe($this->name) : $this->imap->unsubscribe($this->name);
+        }
+        else {
+          // TODO: implement this
+        }
+
+        return false;
+    }
+
 
     /**
      * Get number of objects stored in this folder


commit 2559d6ffb7624c59576b57132fa8cfb2737b5f31
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Apr 5 08:56:37 2012 +0200

    Set alarm date/time in UTC

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index c72f9ac..42ecaab 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -75,7 +75,9 @@ abstract class kolab_format
             if (!$dateonly)
                 $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
 
-            if ($tz)
+            if ($tz && $tz->getName() == 'UTC')
+                $result->setUTC(true);
+            else if ($tz)
                 $result->setTimezone($tz->getName());
         }
 
@@ -101,6 +103,9 @@ abstract class kolab_format
                 $tz = new DateTimeZone($tzs);
                 $d->setTimezone($tz);
             }
+            else if ($cdt->isUTC()) {
+                $d->setTimezone(new DateTimeZone('UTC'));
+            }
         }
         catch (Exception $e) { }
 
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index f3de425..97718ef 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -262,7 +262,7 @@ class kolab_format_event extends kolab_format
             }
 
             if (preg_match('/^@(\d+)/', $offset, $d)) {
-                $alarm->setStart(self::get_datetime($d[1]));
+                $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
             }
             else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
                 $days = $hours = $minutes = $seconds = 0;


commit 6fe30d09c62084fd88bb40f9192fcacf19184f69
Merge: e18857f 60f6bbd
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Apr 5 08:55:35 2012 +0200

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit 60f6bbd54ea6fab0ac0fa5dabc8040e90433ffa3
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 4 13:02:35 2012 +0200

    Update TODO list and package file

diff --git a/plugins/calendar/TODO b/plugins/calendar/TODO
index aefb61d..35d5e0a 100644
--- a/plugins/calendar/TODO
+++ b/plugins/calendar/TODO
@@ -25,13 +25,13 @@
 + View: 3.9: Alter event with drag/drop
 + Option: 4.12: Set default reminder time
 + Option: 3.23: Specify folder for new event (prefs)
-- Option: Set date/time format in prefs
++ Option: Set date/time format in prefs
 + Receive: 1.20: Invitation handling
   - Jump to calendar view from mail ("Show event")
   - Allow to re-send invitations
   - Implement iTIP delegation
 
-- View: 3.4: Fish-Eye View For Busy Days
++ View: 3.4: Fish-Eye View For Busy Days
 + View: 3.8: Color according to calendar and category (similar to Kontact)
 
 + Support for multiple calendars (replace categories)
@@ -40,8 +40,8 @@
 + ICS parser/generator (http://code.google.com/p/qcal/)
 
 - Export *with* attachments
-- Importing ICS files (upload, drag & drop)
 - Remember last visited view
 - Create/manage invdividual views
-- Support for tasks/todos with task list view (ordered by date/time)
++ Importing ICS files (upload, drag & drop)
+
 
diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml
index ed42622..742e6e7 100644
--- a/plugins/calendar/package.xml
+++ b/plugins/calendar/package.xml
@@ -157,6 +157,7 @@
 
 			<file name="config.inc.php.dist" role="data"></file>
 			<file name="LICENSE" role="data"></file>
+			<file name="README" role="data"></file>
 			<file name="TODO" role="data"></file>
 
 			<file name="localization/bg_BG.inc" role="data"></file>


commit cf94b34f6abfbaa8647aa0a0889733c5e8888194
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 4 13:01:59 2012 +0200

    Adapt to new kolab_storage backend

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index ade85e9..ea08224 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -60,7 +60,7 @@ class kolab_calendar
 
     // fetch objects from the given IMAP folder
     $this->storage = kolab_storage::get_folder($this->imap_folder);
-    $this->ready = !PEAR::isError($this->storage);
+    $this->ready = $this->storage && !PEAR::isError($this->storage);
 
     // Set readonly and alarms flags according to folder permissions
     if ($this->ready) {
@@ -70,7 +70,7 @@ class kolab_calendar
       }
       else {
         $rights = $this->storage->get_acl();
-        if (!PEAR::isError($rights)) {
+        if ($rights && !PEAR::isError($rights)) {
           if (strpos($rights, 'i') !== false)
             $this->readonly = false;
         }
@@ -271,7 +271,7 @@ class kolab_calendar
     $object = $this->_from_rcube_event($event);
     $saved = $this->storage->save($object, 'event');
     
-    if (PEAR::isError($saved)) {
+    if (!$saved || PEAR::isError($saved)) {
       raise_error(array(
         'code' => 600, 'type' => 'php',
         'file' => __FILE__, 'line' => __LINE__,
@@ -297,15 +297,15 @@ class kolab_calendar
   public function update_event($event)
   {
     $updated = false;
-    $old = $this->storage->getObject($event['id']);
-    if (PEAR::isError($old))
+    $old = $this->storage->get_object($event['id']);
+    if (!$old || PEAR::isError($old))
       return false;
 
     $old['recurrence'] = '';  # clear old field, could have been removed in new, too
     $object = $this->_from_rcube_event($event, $old);
     $saved = $this->storage->save($object, 'event', $event['id']);
 
-    if (PEAR::isError($saved)) {
+    if (!$saved || PEAR::isError($saved)) {
       raise_error(array(
         'code' => 600, 'type' => 'php',
         'file' => __FILE__, 'line' => __LINE__,
@@ -328,29 +328,15 @@ class kolab_calendar
    */
   public function delete_event($event, $force = true)
   {
-    $deleted  = false;
+    $deleted = $this->storage->delete($event['id'], $force);
 
-    if (!$force) {
-      // Get IMAP object ID
-      $imap_uid = $this->storage->_getStorageId($event['id']);
-    }
-
-    $deleteme = $this->storage->delete($event['id'], $force);
-
-    if (PEAR::isError($deleteme)) {
+    if (!$deleted || PEAR::isError($deleted)) {
       raise_error(array(
         'code' => 600, 'type' => 'php',
         'file' => __FILE__, 'line' => __LINE__,
-        'message' => "Error deleting event object from Kolab server:" . $deleteme->getMessage()),
+        'message' => "Error deleting event object from Kolab server"),
         true, false);
     }
-    else {
-      // Save IMAP object ID in session, will be used for restore action
-      if ($imap_uid)
-        $_SESSION['kolab_delete_uids'][$event['id']] = $imap_uid;
-
-      $deleted = true;
-    }
 
     return $deleted;
   }
@@ -363,48 +349,8 @@ class kolab_calendar
    */
   public function restore_event($event)
   {
-    $imap_uid = $_SESSION['kolab_delete_uids'][$event['id']];
-
-    if (!$imap_uid)
-      return false;
-
-    $session = &Horde_Kolab_Session::singleton();
-    $imap    = &$session->getImap();
-
-    if (is_object($imap) && is_a($imap, 'PEAR_Error')) {
-      $error = $imap;
-    }
-    else {
-      $result = $imap->select($this->imap_folder);
-      if (is_object($result) && is_a($result, 'PEAR_Error')) {
-        $error = $result;
-      }
-      else {
-        $result = $imap->undeleteMessages($imap_uid);
-        if (is_object($result) && is_a($result, 'PEAR_Error')) {
-          $error = $result;
-        }
-        else {
-          // re-sync the cache
-          $this->storage->synchronize();
-        }
-      }
-    }
-
-    if ($error) {
-      raise_error(array(
-        'code' => 600, 'type' => 'php',
-        'file' => __FILE__, 'line' => __LINE__,
-        'message' => "Error undeleting an event object(s) from the Kolab server:" . $error->getMessage()),
-        true, false);
-
-      return false;
-    }
-
-    $rcmail = rcmail::get_instance();
-    $rcmail->session->remove('kolab_delete_uids');
-
-    return true;
+    // TODO: re-implement this with new kolab_storege backend
+    return false;
   }
 
   /**
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index b48a958..653a3b8 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -161,8 +161,8 @@ class kolab_driver extends calendar_driver
     }
     
     // subscribe to new calendar by default
-    $storage = $this->rc->get_storage();
-    $storage->subscribe($folder);
+    $storage = kolab_storage::get_folder($folder);
+    $storage->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION);
 
     // create ID
     $id = kolab_storage::folder_id($folder);
@@ -227,13 +227,9 @@ class kolab_driver extends calendar_driver
   public function subscribe_calendar($prop)
   {
     if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
-      $storage = $this->rc->get_storage();
-      if ($prop['active'])
-        return $storage->subscribe($cal->get_realname());
-      else
-        return $storage->unsubscribe($cal->get_realname());
+      return $cal->storage->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION);
     }
-    
+
     return false;
   }
 


commit aea9f64676c5b7a765e8ed7bc3b4e9bc9e28133d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Apr 4 11:57:20 2012 +0200

    Bugfix: check the right object property

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 75333a0..f7683cf 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2349,7 +2349,7 @@ function rcube_calendar_ui(settings)
           event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS));
         }
         // moved to all-day section: set times to 12:00 - 13:00
-        if (allDay && !event.allday) {
+        if (allDay && !event.allDay) {
           event.start.setHours(12);
           event.start.setMinutes(0);
           event.start.setSeconds(0);
@@ -2358,7 +2358,7 @@ function rcube_calendar_ui(settings)
           event.end.setSeconds(0);
         }
         // moved from all-day section: set times to working hours
-        else if (event.allday && !allDay) {
+        else if (event.allDay && !allDay) {
           var newstart = event.start.getTime();
           revertFunc();  // revert to get original duration
           var numdays = Math.max(1, Math.round((event.end.getTime() - event.start.getTime()) / DAY_MS)) - 1;


commit e18857f155460d8e2d5b28898892737fa24fe3fc
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 3 23:29:30 2012 +0200

    Implement function to move Kolab objects from one folder into another

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index b48a958..a29edcc 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -567,7 +567,6 @@ class kolab_driver extends calendar_driver
           return false;
 
         $fromcalendar = $storage;
-        $storage->storage->synchronize();
       }
     }
     else
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 9c2cf83..772860c 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -468,6 +468,33 @@ class kolab_storage_folder
 
 
     /**
+     * Move a Kolab object message to another IMAP folder
+     *
+     * @param string Object UID
+     * @param string IMAP folder to move object to
+     * @return boolean True on success, false on failure
+     */
+    public function move($uid, $target_folder)
+    {
+        if ($msguid = $this->uid2msguid($uid)) {
+            if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) {
+                // TODO: update cache
+                return true;
+            }
+            else {
+                raise_error(array(
+                    'code' => 600, 'type' => 'php',
+                    'file' => __FILE__, 'line' => __LINE__,
+                    'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
+                ), true);
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
      * Resolve an object UID into an IMAP message UID
      */
     private function uid2msguid($uid, $deleted = false)


commit 1a47608fe28100e4ef5c4c3f688d1c9b39dbbcae
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 3 23:10:49 2012 +0200

    Synchronize box styles in Larry skin

diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index ad646df..79635bb 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -502,8 +502,8 @@ td.topalign {
 .event-update-confirm .message {
 	margin-top: 0.5em;
 	padding: 0.8em;
-	background-color: #F7FDCB;
-	border: 1px solid #C2D071;
+	border: 1px solid #ffdf0e;
+	background-color: #fef893;
 }
 
 .event-dialog-message .message,
@@ -540,8 +540,6 @@ td.topalign {
 #edit-attendees-notify {
 	margin: 0.3em 0;
 	padding: 0.5em;
-	border: 1px solid #ffdf0e;
-	background-color: #fef893;
 }
 
 #edit-attendees-table {


commit bd8dceb9e7f78ed09a67b6b9ce6c1f9f7c02f647
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 3 23:10:20 2012 +0200

    The new Kolab 3 backend has better support for recurrence rules

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index eea5b32..b48a958 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -33,6 +33,7 @@ class kolab_driver extends calendar_driver
   public $freebusy = true;
   public $attachments = true;
   public $undelete = true;
+  public $alarm_types = array('DISPLAY','EMAIL');
   public $categoriesimmutable = true;
 
   private $rc;
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 35a5c9c..c740e4a 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -450,17 +450,16 @@ class calendar_ui
         $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
         $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('months')));
 
-/* multiple month selection is not supported by Kolab
-        $checkbox = new html_radiobutton(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
+        $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
         for ($monthdays = '', $d = 1; $d <= 31; $d++) {
             $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
             $monthdays .= $d % 7 ? ' ' : html::br();
         }
-*/
+
         // rule selectors
         $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
         $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
-        $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->cal->gettext('onsamedate')));  // $this->cal->gettext('each')
+        $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->cal->gettext('each')));
         $table->add(null, $monthdays);
         $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->cal->gettext('onevery')));
         $table->add(null, $this->rrule_selectors($attrib['part']));
@@ -475,8 +474,7 @@ class calendar_ui
         $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('years')));
         // month selector
         $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
-        $boxtype = is_a($this->cal->driver, 'kolab_driver') ? 'radio' : 'checkbox';
-        $checkbox = new html_inputfield(array('type' => $boxtype, 'name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
+        $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
         for ($months = '', $m = 1; $m <= 12; $m++) {
             $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->cal->gettext($monthmap[$m]));
             $months .= $m % 4 ? ' ' : html::br();
@@ -538,13 +536,10 @@ class calendar_ui
         $this->cal->gettext('first'),
         $this->cal->gettext('second'),
         $this->cal->gettext('third'),
-        $this->cal->gettext('fourth')
+        $this->cal->gettext('fourth'),
+        $this->cal->gettext('last')
       ),
-      array(1, 2, 3, 4));
-    
-    // Kolab doesn't support 'last' but others do.
-    if (!is_a($this->cal->driver, 'kolab_driver'))
-      $select_prefix->add($this->cal->gettext('last'), -1);
+      array(1, 2, 3, 4, -1));
     
     $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
     if ($noselect) $select_wday->add($noselect, '');
@@ -555,8 +550,6 @@ class calendar_ui
       $d = $j % 7;
       $select_wday->add($this->cal->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
     }
-    if ($part == 'monthly')
-      $select_wday->add($this->cal->gettext('dayofmonth'), '');
     
     return $select_prefix->show() . ' ' . $select_wday->show();
   }


commit 44a2d85c571dc75ad8912c381d294bcba657e0dc
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 3 23:08:24 2012 +0200

    Remove old and unused code; pass event owner to storage layer

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index cf99a27..ade85e9 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -513,95 +513,6 @@ class kolab_calendar
   {
     $object = &$event;
 
-/*
-    //recurr object/array
-    if (count($event['recurrence']) > 1) {
-      $ra = $event['recurrence'];
-      
-      //Frequency abd interval
-      $object['recurrence']['cycle'] = strtolower($ra['FREQ']);
-      $object['recurrence']['interval'] = intval($ra['INTERVAL']);
-
-      //Range Type
-      if ($ra['UNTIL']) {
-        $object['recurrence']['range-type'] = 'date';
-        $object['recurrence']['range'] = $ra['UNTIL'];
-      }
-      if ($ra['COUNT']) {
-        $object['recurrence']['range-type'] = 'number';
-        $object['recurrence']['range'] = $ra['COUNT'];
-      }
-      
-      //weekly
-      if ($ra['FREQ'] == 'WEEKLY') {
-        if ($ra['BYDAY']) {
-          foreach (split(",", $ra['BYDAY']) as $day)
-            $object['recurrence']['day'][] = $this->weekday_map[$day];
-        }
-        else {
-          // use weekday of start date if empty
-          $object['recurrence']['day'][] = strtolower(gmdate('l', $event['start'] + $tz_offset));
-        }
-      }
-      
-      //monthly (temporary hack to follow current Horde logic)
-      if ($ra['FREQ'] == 'MONTHLY') {
-        if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) {
-          $object['recurrence']['daynumber'] = $m[1];
-          $object['recurrence']['day'] = array($this->weekday_map[$m[2]]);
-          $object['recurrence']['cycle'] = 'monthly';
-          $object['recurrence']['type']  = 'weekday';
-        }
-        else {
-          $object['recurrence']['daynumber'] = date('j', $event['start']);
-          $object['recurrence']['cycle'] = 'monthly';
-          $object['recurrence']['type']  = 'daynumber';
-        }
-      }
-      
-      //yearly
-      if ($ra['FREQ'] == 'YEARLY') {
-        if (!$ra['BYMONTH'])
-          $ra['BYMONTH'] = gmdate('n', $event['start'] + $tz_offset);
-        
-        $object['recurrence']['cycle'] = 'yearly';
-        $object['recurrence']['month'] = $this->month_map[intval($ra['BYMONTH'])];
-        
-        if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) {
-          $object['recurrence']['type'] = 'weekday';
-          $object['recurrence']['daynumber'] = $m[1];
-          $object['recurrence']['day'] = array($this->weekday_map[$m[2]]);
-        }
-        else {
-          $object['recurrence']['type'] = 'monthday';
-          $object['recurrence']['daynumber'] = gmdate('j', $event['start'] + $tz_offset);
-        }
-      }
-      
-      //exclusions
-      foreach ((array)$ra['EXDATE'] as $excl) {
-        $object['recurrence']['exclusion'][] = gmdate('Y-m-d', $excl + $tz_offset);
-      }
-    }
-    
-    // whole day event
-    if ($event['allday']) {
-      $object['end-date'] += 12 * 3600;  // end is at 13:00 => jump to the next day
-      $object['end-date'] += $tz_offset - date('Z');   // shift 00 times from user's timezone to server's timezone 
-      $object['start-date'] += $tz_offset - date('Z');  // because Horde_Kolab_Format_Date::encodeDate() uses strftime()
-      
-      // create timestamps at exactly 00:00. This is also needed for proper re-interpretation in _to_rcube_event() after updating an event
-      $object['start-date'] = mktime(0,0,0, date('n', $object['start-date']), date('j', $object['start-date']), date('Y', $object['start-date']));
-      $object['end-date']   = mktime(0,0,0, date('n', $object['end-date']),   date('j', $object['end-date']),   date('Y', $object['end-date']));
-      
-      // sanity check: end date is same or smaller than start
-      if (date('Y-m-d', $object['end-date']) <= date('Y-m-d', $object['start-date']))
-        $object['end-date'] = mktime(13,0,0, date('n', $object['start-date']), date('j', $object['start-date']), date('Y', $object['start-date'])) + 86400;
-      
-      $object['_is_all_day'] = 1;
-    }
-*/
-
     // in Horde attachments are indexed by name
     $object['_attachments'] = array();
     if (!empty($event['attachments'])) {
@@ -632,9 +543,12 @@ class kolab_calendar
     $event['sensitivity'] = $this->sensitivity_map[$event['sensitivity']];
 
     // set current user as ORGANIZER
-    if (empty($event['attendees']) && ($identity = $this->cal->rc->user->get_identity()) && $identity['email'])
+    $identity = $this->cal->rc->user->get_identity();
+    if (empty($event['attendees']) && $identity['email'])
       $event['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
 
+    $event['_owner'] = $identity['email'];
+
     // copy meta data (starting with _) from old object
     foreach ((array)$old as $key => $val) {
       if (!isset($event[$key]) && $key[0] == '_')


commit fd141a5dbcc46320451e9df5d4a79a82ea2d4c13
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 3 23:07:09 2012 +0200

    Two small bugfixes

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 4a8b0e8..0e61615 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1278,7 +1278,7 @@ class calendar extends rcube_plugin
         case 'EXDATE':
           foreach ((array)$val as $i => $ex)
             $val[$i] = gmdate('Ymd\THis', $ex);
-          $val = join(',', $val);
+          $val = join(',', (array)$val);
           break;
       }
       $rrule .= $k . '=' . $val . ';';
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 012d176..75333a0 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2437,7 +2437,7 @@ function rcube_calendar_ui(settings)
       /* Time completions */
       var result = [];
       var now = new Date();
-      var st, start = (this.element.attr('id').indexOf('endtime') > 0
+      var st, start = (String(this.element.attr('id')).indexOf('endtime') > 0
         && (st = $('#edit-starttime').val())
         && $('#edit-startdate').val() == $('#edit-enddate').val())
         ? parse_datetime(st, '') : null;


commit 87b474eab7c399728bc24729767d9811be7c08bf
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Apr 3 23:05:59 2012 +0200

    Read/write event recurrence rules and alarms

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index db41551..f3de425 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -39,6 +39,31 @@ class kolab_format_event extends kolab_format
         'CHAIR' => kolabformat::Chair,
     );
 
+    private $rrule_type_map = array(
+        'MINUTELY' => RecurrenceRule::Minutely,
+        'HOURLY' => RecurrenceRule::Hourly,
+        'DAILY' => RecurrenceRule::Daily,
+        'WEEKLY' => RecurrenceRule::Weekly,
+        'MONTHLY' => RecurrenceRule::Monthly,
+        'YEARLY' => RecurrenceRule::Yearly,
+    );
+
+    private $weekday_map = array(
+        'MO' => kolabformat::Monday,
+        'TU' => kolabformat::Tuesday,
+        'WE' => kolabformat::Wednesday,
+        'TH' => kolabformat::Thursday,
+        'FR' => kolabformat::Friday,
+        'SA' => kolabformat::Saturday,
+        'SU' => kolabformat::Sunday,
+    );
+
+    private $alarm_type_map = array(
+        'DISPLAY' => Alarm::DisplayAlarm,
+        'EMAIL' => Alarm::EMailAlarm,
+        'AUDIO' => Alarm::AudioAlarm,
+    );
+
     private $status_map = array(
         'UNKNOWN' => kolabformat::PartNeedsAction,
         'NEEDS-ACTION' => kolabformat::PartNeedsAction,
@@ -116,7 +141,6 @@ class kolab_format_event extends kolab_format
         // do the hard work of setting object values
         $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
         $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
-
         $this->obj->setSummary($object['title']);
         $this->obj->setLocation($object['location']);
         $this->obj->setDescription($object['description']);
@@ -149,21 +173,109 @@ class kolab_format_event extends kolab_format
                 $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
                 $att->setRSVP((bool)$attendee['rsvp']);
 
-                if ($att->isValid())
+                if ($att->isValid()) {
                     $attendees->push($att);
+                }
+                else {
+                    raise_error(array(
+                        'code' => 600, 'type' => 'php',
+                        'file' => __FILE__, 'line' => __LINE__,
+                        'message' => "Invalid event attendee: " . json_encode($attendee),
+                    ), true);
+                }
             }
         }
         $this->obj->setOrganizer($organizer);
         $this->obj->setAttendees($attendees);
 
-        // TODO: save recurrence rule
+        // save recurrence rule
+        if ($object['recurrence']) {
+            $rr = new RecurrenceRule;
+            $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
+
+            if ($object['recurrence']['INTERVAL'])
+                $rr->setInterval(intval($object['recurrence']['INTERVAL']));
+
+            if ($object['recurrence']['BYDAY']) {
+                $byday = new vectordaypos;
+                foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
+                    $occurrence = 0;
+                    if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
+                        $occurrence = intval($m[1]);
+                        $day = $m[2];
+                    }
+                    if (isset($this->weekday_map[$day]))
+                        $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
+                }
+                $rr->setByday($byday);
+            }
+
+            if ($object['recurrence']['BYMONTHDAY']) {
+                $bymday = new vectori;
+                foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
+                    $bymday->push(intval($day));
+                $rr->setBymonthday($bymday);
+            }
+
+            if ($object['recurrence']['BYMONTH']) {
+                $bymonth = new vectori;
+                foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
+                    $bymonth->push(intval($month));
+                $rr->setBymonth($bymonth);
+            }
+
+            if ($object['recurrence']['COUNT'])
+                $rr->setCount(intval($object['recurrence']['COUNT']));
+            else if ($object['recurrence']['UNTIL'])
+                $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
+
+            if ($rr->isValid()) {
+                $this->obj->setRecurrenceRule($rr);
 
-        // TODO: save alarm
+                // add exception dates (only if recurrence rule is valid)
+                $exdates = new vectordatetime;
+                foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
+                    $exdates->push(self::get_datetime($exdate, null, true));
+                $this->obj->setExceptionDates($exdates);
+            }
+            else {
+                raise_error(array(
+                    'code' => 600, 'type' => 'php',
+                    'file' => __FILE__, 'line' => __LINE__,
+                    'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
+                ), true);
+            }
+        }
+
+        // save alarm
         $valarms = new vectoralarm;
         if ($object['alarms']) {
-          $alarm = new Alarm;
-            list($duration, $type) = explode(":", $object['alarms']);
-            
+            list($offset, $type) = explode(":", $object['alarms']);
+
+            if ($type == 'EMAIL') {  // email alarms implicitly go to event owner
+                $recipients = new vectorcontactref;
+                $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
+                $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
+            }
+            else {  // default: display alarm
+                $alarm = new Alarm($object['title']);
+            }
+
+            if (preg_match('/^@(\d+)/', $offset, $d)) {
+                $alarm->setStart(self::get_datetime($d[1]));
+            }
+            else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
+                $days = $hours = $minutes = $seconds = 0;
+                switch ($d[3]) {
+                    case 'W': $days  = 7*intval($d[2]); break;
+                    case 'D': $days    = intval($d[2]); break;
+                    case 'H': $hours   = intval($d[2]); break;
+                    case 'M': $minutes = intval($d[2]); break;
+                    case 'S': $seconds = intval($d[2]); break;
+                }
+                $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
+            }
+
             $valarms->push($alarm);
         }
         $this->obj->setAlarms($valarms);
@@ -243,7 +355,77 @@ class kolab_format_event extends kolab_format
             );
         }
 
-        // TODO: hanlde recurrence rules, alarms and attachments
+        // read recurrence rule
+        if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
+            $rrule_type_map = array_flip($this->rrule_type_map);
+            $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
+
+            if ($intvl = $rr->interval())
+                $object['recurrence']['INTERVAL'] = $intvl;
+
+            if (($count = $rr->count()) && $count > 0) {
+                $object['recurrence']['COUNT'] = $count;
+            }
+            else if ($until = self::php_datetime($rr->end())) {
+                $until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
+                $object['recurrence']['UNTIL'] = $until->format('U');
+            }
+
+            if (($byday = $rr->byday()) && $byday->size()) {
+                $weekday_map = array_flip($this->weekday_map);
+                $weekdays = array();
+                for ($i=0; $i < $byday->size(); $i++) {
+                    $daypos = $byday->get($i);
+                    $prefix = $daypos->occurence();
+                    $weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
+                }
+                $object['recurrence']['BYDAY'] = join(',', $weekdays);
+            }
+
+            if (($bymday = $rr->bymonthday()) && $bymday->size()) {
+                $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
+            }
+
+            if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
+                $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
+            }
+
+            if ($exceptions = $this->obj->exceptionDates()) {
+                for ($i=0; $i < $exceptions->size(); $i++) {
+                    if ($exdate = self::php_datetime($exceptions->get($i)))
+                        $object['recurrence']['EXDATE'][] = $exdate->format('U');
+                }
+            }
+        }
+
+        // read alarm
+        $valarms = $this->obj->alarms();
+        $alarm_types = array_flip($this->alarm_type_map);
+        for ($i=0; $i < $valarms->size(); $i++) {
+            $alarm = $valarms->get($i);
+            $type = $alarm_types[$alarm->type()];
+
+            if ($type == 'DISPLAY' || $type == 'EMAIL') {  // only DISPLAY and EMAIL alarms are supported
+                if ($start = self::php_datetime($alarm->start())) {
+                    $object['alarms'] = '@' . $start->format('U');
+                }
+                else if ($offset = $alarm->relativeStart()) {
+                    $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+                    if      ($w = $offset->weeks())     $value .= $w . 'W';
+                    else if ($d = $offset->days())      $value .= $d . 'D';
+                    else if ($h = $offset->hours())     $value .= $h . 'H';
+                    else if ($m = $offset->minutes())   $value .= $m . 'M';
+                    else if ($s = $offset->seconds())   $value .= $s . 'S';
+                    else continue;
+
+                    $object['alarms'] = $value;
+                }
+                $object['alarms']  .= ':' . $type;
+                break;
+            }
+        }
+
+        // TODO: handle attachments
 
         $this->data = $object;
         return $this->data;
@@ -260,7 +442,8 @@ class kolab_format_event extends kolab_format
         $start_time = date('H:i:s', $rec['start-date']);
         $allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
 
-        if ($allday) {  // in Roundcube all-day events go from 12:00 to 13:00
+        // in Roundcube all-day events go from 12:00 to 13:00
+        if ($allday) {
             $now = new DateTime('now', self::$timezone);
             $gmt_offset = $now->getOffset();
 


commit ec04074a46f5dd45a6be0fd5be267e27fdc11117
Author: Thomas B <roundcube at gmail.com>
Date:   Fri Mar 30 19:14:38 2012 +0200

    Adapt Kolab driver to work against new libkolab plugin/library

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 2881653..4a8b0e8 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -205,7 +205,7 @@ class calendar extends rcube_plugin
 
     switch ($driver_name) {
       case "kolab":
-        $this->require_plugin('kolab_core');
+        $this->require_plugin('libkolab');
       default:
         $this->driver = new $driver_class($this);
         break;
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index ccd542a..cf99a27 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) 2011, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -26,6 +26,9 @@
 
 class kolab_calendar
 {
+  const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
+  const COLOR_KEY_PRIVATE = '/shared/vendor/kolab/color';
+  
   public $id;
   public $ready = false;
   public $readonly = true;
@@ -38,14 +41,8 @@ class kolab_calendar
   private $events;
   private $id2uid;
   private $imap_folder = 'INBOX/Calendar';
-  private $namespace;
   private $search_fields = array('title', 'description', 'location', '_attendees');
   private $sensitivity_map = array('public', 'private', 'confidential');
-  private $priority_map = array('low' => 9, 'normal' => 5, 'high' => 1);
-  private $role_map = array('REQ-PARTICIPANT' => 'required', 'OPT-PARTICIPANT' => 'optional', 'CHAIR' => 'resource');
-  private $status_map = array('NEEDS-ACTION' => 'none', 'TENTATIVE' => 'tentative', 'CONFIRMED' => 'accepted', 'ACCEPTED' => 'accepted', 'DECLINED' => 'declined');
-  private $month_map = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
-  private $weekday_map = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday');
 
 
   /**
@@ -59,11 +56,10 @@ class kolab_calendar
       $this->imap_folder = $imap_folder;
 
     // ID is derrived from folder name
-    $this->id = rcube_kolab::folder_id($this->imap_folder);
+    $this->id = kolab_storage::folder_id($this->imap_folder);
 
     // fetch objects from the given IMAP folder
-    $this->storage = rcube_kolab::get_storage($this->imap_folder);
-
+    $this->storage = kolab_storage::get_folder($this->imap_folder);
     $this->ready = !PEAR::isError($this->storage);
 
     // Set readonly and alarms flags according to folder permissions
@@ -73,7 +69,7 @@ class kolab_calendar
         $this->alarms = true;
       }
       else {
-        $rights = $this->storage->_folder->getMyRights();
+        $rights = $this->storage->get_acl();
         if (!PEAR::isError($rights)) {
           if (strpos($rights, 'i') !== false)
             $this->readonly = false;
@@ -96,7 +92,7 @@ class kolab_calendar
    */
   public function get_name()
   {
-    $folder = rcube_kolab::object_name($this->imap_folder, $this->namespace);
+    $folder = kolab_storage::object_name($this->imap_folder, $this->namespace);
     return $folder;
   }
 
@@ -119,7 +115,7 @@ class kolab_calendar
    */
   public function get_owner()
   {
-    return $this->storage->_folder->getOwner();
+    return $this->storage->get_owner();
   }
 
 
@@ -130,10 +126,7 @@ class kolab_calendar
    */
   public function get_namespace()
   {
-    if ($this->namespace === null) {
-      $this->namespace = rcube_kolab::folder_namespace($this->imap_folder);
-    }
-    return $this->namespace;
+    return $this->storage->get_namespace();
   }
 
 
@@ -154,7 +147,8 @@ class kolab_calendar
   public function get_color()
   {
     // color is defined in folder METADATA
-    if ($color = $this->storage->_folder->getKolabAttribute('color', HORDE_ANNOT_READ_PRIVATE_SHARED)) {
+    $metadata = $this->storage->get_metadata(array(self::COLOR_KEY_PRIVATE, self::COLOR_KEY_SHARED));
+    if (($color = $metadata[self::COLOR_KEY_PRIVATE]) || ($color = $metadata[self::COLOR_KEY_SHARED])) {
       return $color;
     }
 
@@ -168,11 +162,11 @@ class kolab_calendar
   }
 
   /**
-   * Return the corresponding Kolab_Folder instance
+   * Return the corresponding kolab_storage_folder instance
    */
   public function get_folder()
   {
-    return $this->storage->_folder;
+    return $this->storage;
   }
 
   /**
@@ -275,7 +269,7 @@ class kolab_calendar
 
     //generate new event from RC input
     $object = $this->_from_rcube_event($event);
-    $saved = $this->storage->save($object);
+    $saved = $this->storage->save($object, 'event');
     
     if (PEAR::isError($saved)) {
       raise_error(array(
@@ -308,8 +302,8 @@ class kolab_calendar
       return false;
 
     $old['recurrence'] = '';  # clear old field, could have been removed in new, too
-    $object = array_merge($old, $this->_from_rcube_event($event));
-    $saved = $this->storage->save($object, $event['id']);
+    $object = $this->_from_rcube_event($event, $old);
+    $saved = $this->storage->save($object, 'event', $event['id']);
 
     if (PEAR::isError($saved)) {
       raise_error(array(
@@ -421,7 +415,8 @@ class kolab_calendar
   {
     if (!isset($this->events)) {
       $this->events = array();
-      foreach ((array)$this->storage->getObjects() as $record) {
+
+      foreach ((array)$this->storage->get_objects() as $record) {
         $event = $this->_to_rcube_event($record);
         $this->events[$event['id']] = $event;
       }
@@ -471,74 +466,20 @@ class kolab_calendar
   /**
    * Convert from Kolab_Format to internal representation
    */
-  private function _to_rcube_event($rec)
+  private function _to_rcube_event($record)
   {
-    $start_time = date('H:i:s', $rec['start-date']);
-    $allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
-    if ($allday) {  // in Roundcube all-day events only go from 12:00 to 13:00
-      $rec['start-date'] += 12 * 3600;
-      $rec['end-date']   -= 11 * 3600;
-      $rec['end-date']   -= $this->cal->gmt_offset - date('Z', $rec['end-date']);    // shift times from server's timezone to user's timezone
-      $rec['start-date'] -= $this->cal->gmt_offset - date('Z', $rec['start-date']);  // because generated with mktime() in Horde_Kolab_Format_Date::decodeDate()
-      // sanity check
-      if ($rec['end-date'] <= $rec['start-date'])
-        $rec['end-date'] += 86400;
-    }
-    
-    // convert alarm time into internal format
-    if ($rec['alarm']) {
-      $alarm_value = $rec['alarm'];
-      $alarm_unit = 'M';
-      if ($rec['alarm'] % 1440 == 0) {
-        $alarm_value /= 1440;
-        $alarm_unit = 'D';
-      }
-      else if ($rec['alarm'] % 60 == 0) {
-        $alarm_value /= 60;
-        $alarm_unit = 'H';
-      }
-      $alarm_value *= -1;
-    }
-    
-    // convert recurrence rules into internal pseudo-vcalendar format
-    if ($recurrence = $rec['recurrence']) {
-      $rrule = array(
-        'FREQ' => strtoupper($recurrence['cycle']),
-        'INTERVAL' => intval($recurrence['interval']),
-      );
-      
-      if ($recurrence['range-type'] == 'number')
-        $rrule['COUNT'] = intval($recurrence['range']);
-      else if ($recurrence['range-type'] == 'date')
-        $rrule['UNTIL'] = $recurrence['range'];
-      
-      if ($recurrence['day']) {
-        $byday = array();
-        $prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
-        foreach ($recurrence['day'] as $day)
-          $byday[] = $prefix . substr(strtoupper($day), 0, 2);
-        $rrule['BYDAY'] = join(',', $byday);
-      }
-      if ($recurrence['daynumber']) {
-        if ($recurrence['type'] == 'monthday' || $recurrence['type'] == 'daynumber')
-          $rrule['BYMONTHDAY'] = $recurrence['daynumber'];
-        else if ($recurrence['type'] == 'yearday')
-          $rrule['BYYEARDAY'] = $recurrence['daynumber'];
-      }
-      if ($recurrence['month']) {
-        $monthmap = array_flip($this->month_map);
-        $rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
-      }
-      
-      if ($recurrence['exclusion']) {
-        foreach ((array)$recurrence['exclusion'] as $excl)
-          $rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date']));  // use time of event start
-      }
-    }
+    $record['id'] = $record['uid'];
+    $record['calendar'] = $this->id;
 
-    $sensitivity_map = array_flip($this->sensitivity_map);
-    $status_map = array_flip($this->status_map);
-    $role_map = array_flip($this->role_map);
+    // convert from DateTime to unix timestamp
+    if (is_a($record['start'], 'DateTime'))
+      $record['start'] = $record['start']->format('U');
+    if (is_a($record['end'], 'DateTime'))
+      $record['end'] = $record['end']->format('U');
+
+    // all-day events go from 12:00 - 13:00
+    if ($record['end'] <= $record['start'] && $record['allday'])
+      $record['end'] = $record['start'] + 3600;
 
     if (!empty($rec['_attachments'])) {
       foreach ($rec['_attachments'] as $name => $attachment) {
@@ -550,95 +491,29 @@ class kolab_calendar
         );
       }
     }
-    
-    if ($rec['organizer']) {
-      $attendees[] = array(
-        'role' => 'ORGANIZER',
-        'name' => $rec['organizer']['display-name'],
-        'email' => $rec['organizer']['smtp-address'],
-        'status' => 'ACCEPTED',
-      );
-      $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
-    }
-    
-    foreach ((array)$rec['attendee'] as $attendee) {
-      $attendees[] = array(
-        'role' => $role_map[$attendee['role']],
-        'name' => $attendee['display-name'],
-        'email' => $attendee['smtp-address'],
-        'status' => $status_map[$attendee['status']],
-        'rsvp' => $attendee['request-response'],
-      );
-      $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
-    }
-    
+
+    $sensitivity_map = array_flip($this->sensitivity_map);
+    $record['sensitivity'] = intval($sensitivity_map[$record['sensitivity']]);
+
     // Roundcube only supports one category assignment
-    $categories = explode(',', $rec['categories']);
-    
-    return array(
-      'id' => $rec['uid'],
-      'uid' => $rec['uid'],
-      'title' => $rec['summary'],
-      'location' => $rec['location'],
-      'description' => $rec['body'],
-      'start' => $rec['start-date'],
-      'end' => $rec['end-date'],
-      'allday' => $allday,
-      'recurrence' => $rrule,
-      'alarms' => $alarm_value . $alarm_unit,
-      '_alarm' => intval($rec['alarm']),
-      'categories' => $categories[0],
-      'attachments' => $attachments,
-      'attendees' => $attendees,
-      '_attendees' => $_attendees,
-      'free_busy' => $rec['show-time-as'],
-      'priority' => is_numeric($rec['priority']) ? intval($rec['priority']) : (isset($this->priority_map[$rec['priority']]) ? $this->priority_map[$rec['priority']] : 0),
-      'sensitivity' => $sensitivity_map[$rec['sensitivity']],
-      'changed' => $rec['last-modification-date'],
-      'calendar' => $this->id,
-    );
+    if (is_array($record['categories']))
+      $record['categories'] = $record['categories'][0];
+
+    // remove internals
+    unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments']);
+
+    return $record;
   }
 
    /**
    * Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
    * (opposite of self::_to_rcube_event())
    */
-  private function _from_rcube_event($event)
+  private function _from_rcube_event($event, $old = array())
   {
-    $priority_map = $this->priority_map;
-    $tz_offset = $this->cal->gmt_offset;
-
-    $object = array(
-    // kolab         => roundcube
-      'uid'          => $event['uid'],
-      'summary'      => $event['title'],
-      'location'     => $event['location'],
-      'body'         => $event['description'],
-      'categories'   => $event['categories'],
-      'start-date'   => $event['start'],
-      'end-date'     => $event['end'],
-      'sensitivity'  =>$this->sensitivity_map[$event['sensitivity']],
-      'show-time-as' => $event['free_busy'],
-      'priority'     => $event['priority'],
-    );
-    
-    //handle alarms
-    if ($event['alarms']) {
-      //get the value
-      $alarmbase = explode(":", $event['alarms']);
-      
-      //get number only
-      $avalue = preg_replace('/[^0-9]/', '', $alarmbase[0]); 
-      
-      if (preg_match("/H/",$alarmbase[0])) {
-        $object['alarm'] = $avalue*60;
-      } else if (preg_match("/D/",$alarmbase[0])) {
-        $object['alarm'] = $avalue*24*60;
-      } else {
-        $object['alarm'] = $avalue;
-      }
-    }
-    
+    $object = &$event;
+
+/*
     //recurr object/array
     if (count($event['recurrence']) > 1) {
       $ra = $event['recurrence'];
@@ -725,6 +600,7 @@ class kolab_calendar
       
       $object['_is_all_day'] = 1;
     }
+*/
 
     // in Horde attachments are indexed by name
     $object['_attachments'] = array();
@@ -752,27 +628,20 @@ class kolab_calendar
       }
     }
 
-    // process event attendees
-    foreach ((array)$event['attendees'] as $attendee) {
-      $role = $attendee['role'];
-      if ($role == 'ORGANIZER') {
-        $object['organizer'] = array(
-          'display-name' => $attendee['name'],
-          'smtp-address' => $attendee['email'],
-        );
-      }
-      else {
-        $object['attendee'][] = array(
-          'display-name' => $attendee['name'],
-          'smtp-address' => $attendee['email'],
-          'status' => $this->status_map[$attendee['status']],
-          'role' => $this->role_map[$role],
-          'request-response' => $attendee['rsvp'],
-        );
-      }
+    // translate sensitivity property
+    $event['sensitivity'] = $this->sensitivity_map[$event['sensitivity']];
+
+    // set current user as ORGANIZER
+    if (empty($event['attendees']) && ($identity = $this->cal->rc->user->get_identity()) && $identity['email'])
+      $event['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
+
+    // copy meta data (starting with _) from old object
+    foreach ((array)$old as $key => $val) {
+      if (!isset($event[$key]) && $key[0] == '_')
+        $event[$key] = $val;
     }
 
-    return $object;
+    return $event;
   }
 
 
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index adca962..eea5b32 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -7,7 +7,7 @@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
- * Copyright (C) 2011, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -64,7 +64,7 @@ class kolab_driver extends calendar_driver
       return $this->calendars;
 
     // get all folders that have "event" type
-    $folders = rcube_kolab::get_folders('event');
+    $folders = kolab_storage::get_folders('event');
     $this->calendars = array();
 
     if (PEAR::isError($folders)) {
@@ -134,7 +134,7 @@ class kolab_driver extends calendar_driver
           'readonly' => $cal->readonly,
           'showalarms' => $cal->alarms,
           'class_name' => $cal->get_namespace(),
-          'active'   => rcube_kolab::is_subscribed($cal->get_realname()),
+          'active'   => $cal->storage->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
         );
       }
     }
@@ -164,7 +164,7 @@ class kolab_driver extends calendar_driver
     $storage->subscribe($folder);
 
     // create ID
-    $id = rcube_kolab::folder_id($folder);
+    $id = kolab_storage::folder_id($folder);
 
     // save color in user prefs (temp. solution)
     $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
@@ -197,7 +197,7 @@ class kolab_driver extends calendar_driver
       }
 
       // create ID
-      $id = rcube_kolab::folder_id($newfolder);
+      $id = kolab_storage::folder_id($newfolder);
 
       // fallback to local prefs
       $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
@@ -302,24 +302,24 @@ class kolab_driver extends calendar_driver
     // update the folder name
     if (strlen($oldfolder)) {
       if ($oldfolder != $folder) {
-        if (!($result = rcube_kolab::folder_rename($oldfolder, $folder)))
-          $this->last_error = rcube_kolab::$last_error;
+        if (!($result = kolab_storage::folder_rename($oldfolder, $folder)))
+          $this->last_error = kolab_storage::$last_error;
       }
       else
         $result = true;
     }
     // create new folder
     else {
-      if (!($result = rcube_kolab::folder_create($folder, 'event', false)))
-        $this->last_error = rcube_kolab::$last_error;
+      if (!($result = kolab_storage::folder_create($folder, 'event', false)))
+        $this->last_error = kolab_storage::$last_error;
     }
 
     // save color in METADATA
     // TODO: also save 'showalarams' and other properties here
 
     if ($result && $prop['color']) {
-      if (!($meta_saved = $storage->set_metadata($folder, array('/shared/vendor/kolab/color' => $prop['color']))))  // try in shared namespace
-        $meta_saved = $storage->set_metadata($folder, array('/private/vendor/kolab/color' => $prop['color']));    // try in private namespace
+      if (!($meta_saved = $storage->set_metadata(array(kolab_calendar::COLOR_KEY_SHARED => $prop['color']))))  // try in shared namespace
+        $meta_saved = $storage->set_metadata(array(kolab_calendar::COLOR_KEY_PRIVATE => $prop['color']));    // try in private namespace
       if ($meta_saved)
         unset($prop['color']);  // unsetting will prevent fallback to local user prefs
     }
@@ -337,7 +337,7 @@ class kolab_driver extends calendar_driver
   {
     if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
       $folder = $cal->get_realname();
-      if (rcube_kolab::folder_delete($folder)) {
+      if (kolab_storage::folder_delete($folder)) {
         // remove color in user prefs (temp. solution)
         $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
         unset($prefs['kolab_calendars'][$prop['id']]);
@@ -346,7 +346,7 @@ class kolab_driver extends calendar_driver
         return true;
       }
       else
-        $this->last_error = rcube_kolab::$last_error;
+        $this->last_error = kolab_storage::$last_error;
     }
 
     return false;
@@ -913,7 +913,7 @@ class kolab_driver extends calendar_driver
       'OOF' => calendar::FREEBUSY_OOF);
 
     // ask kolab server first
-    $fbdata = @file_get_contents(rcube_kolab::get_freebusy_url($email));
+    $fbdata = @file_get_contents(kolab_storage::get_freebusy_url($email));
 
     // get free-busy url from contacts
     if (!$fbdata) {
@@ -975,12 +975,11 @@ class kolab_driver extends calendar_driver
     ignore_user_abort(true);
 
     $cal = get_input_value('source', RCUBE_INPUT_GPC);
-    if (!($storage = $this->calendars[$cal]))
+    if (!($cal = $this->calendars[$cal]))
       return false;
 
     // trigger updates on folder
-    $folder = $storage->get_folder();
-    $trigger = $folder->trigger();
+    $trigger = $cal->storage->trigger();
     if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
       raise_error(array(
         'code' => 900, 'type' => 'php',
@@ -1037,7 +1036,7 @@ class kolab_driver extends calendar_driver
     // Disable folder name input
     if (!empty($options) && ($options['norename'] || $options['protected'])) {
       $input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
-      $formfields['name']['value'] = Q(str_replace($delimiter, ' » ', rcube_kolab::object_name($folder)))
+      $formfields['name']['value'] = Q(str_replace($delimiter, ' » ', kolab_storage::object_name($folder)))
         . $input_name->show($folder);
     }
 
@@ -1054,7 +1053,7 @@ class kolab_driver extends calendar_driver
       $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
     }
     else {
-      $select = rcube_kolab::folder_selector('event', array('name' => 'parent'), $folder);
+      $select = kolab_storage::folder_selector('event', array('name' => 'parent'), $folder);
       $form['props']['fieldsets']['location']['content']['path'] = array(
         'label' => $this->cal->gettext('parentcalendar'),
         'value' => $select->show(strlen($folder) ? $path_imap : ''),


commit 8354f27dddaf71e768fcc0fa603745574e177b58
Author: Thomas B <roundcube at gmail.com>
Date:   Fri Mar 30 19:13:13 2012 +0200

    Fix timezone offset computation with DateTime functions

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 7eb9dee..2881653 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -99,9 +99,9 @@ class calendar extends rcube_plugin
     // set user's timezone
     $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
     $now = new DateTime('now', $this->timezone);
-    $this->timezone_offset = $now->format('Z') / 3600;
-    $this->dst_active = $now->format('I');
     $this->gmt_offset = $now->getOffset();
+    $this->dst_active = $now->format('I');
+    $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
 
     require($this->home . '/lib/calendar_ui.php');
     $this->ui = new calendar_ui($this);


commit ed3c88a47c7a5c75faf2de6e2f0730f2e7873e9c
Author: Thomas B <roundcube at gmail.com>
Date:   Fri Mar 30 19:09:58 2012 +0200

    Start implementation of libkolabxml event reading/writing + convert from Kolab 2 format

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 6a262af..db41551 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,16 +26,66 @@ class kolab_format_event extends kolab_format
 {
     public $CTYPE = 'application/calendar+xml';
 
+    private $sensitivity_map = array(
+        'public'       => kolabformat::ClassPublic,
+        'private'      => kolabformat::ClassPrivate,
+        'confidential' => kolabformat::ClassConfidential,
+    );
+
+    private $role_map = array(
+        'REQ-PARTICIPANT' => kolabformat::Required,
+        'OPT-PARTICIPANT' => kolabformat::Optional,
+        'NON-PARTICIPANT' => kolabformat::NonParticipant,
+        'CHAIR' => kolabformat::Chair,
+    );
+
+    private $status_map = array(
+        'UNKNOWN' => kolabformat::PartNeedsAction,
+        'NEEDS-ACTION' => kolabformat::PartNeedsAction,
+        'TENTATIVE' => kolabformat::PartTentative,
+        'ACCEPTED' => kolabformat::PartAccepted,
+        'DECLINED' => kolabformat::PartDeclined,
+        'DELEGATED' => kolabformat::PartDelegated,
+      );
+
+    private $kolab2_rolemap = array(
+        'required' => 'REQ-PARTICIPANT',
+        'optional' => 'OPT-PARTICIPANT',
+        'resource' => 'CHAIR',
+    );
+    private $kolab2_statusmap = array(
+        'none'      => 'NEEDS-ACTION',
+        'tentative' => 'TENTATIVE',
+        'accepted'  => 'CONFIRMED',
+        'accepted'  => 'ACCEPTED',
+        'declined'  => 'DECLINED',
+    );
+    private $kolab2_monthmap = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
+
+
+    /**
+     * Default constructor
+     */
     function __construct()
     {
-        $obj = new Event;
+        $this->obj = new Event;
     }
 
+    /**
+     * Load Contact object data from the given XML block
+     *
+     * @param string XML data
+     */
     public function load($xml)
     {
         $this->obj = kolabformat::readEvent($xml, false);
     }
 
+    /**
+     * Write Contact object data to XML format
+     *
+     * @return string XML data
+     */
     public function write()
     {
         $xml = kolabformat::writeEvent($this->obj);
@@ -43,24 +93,276 @@ class kolab_format_event extends kolab_format
         return $xml;
     }
 
+    /**
+     * Set contact properties to the kolabformat object
+     *
+     * @param array  Contact data as hash array
+     */
     public function set(&$object)
     {
-        // TODO: do the hard work of setting object values
+        // set some automatic values if missing
+        if (!$this->obj->created()) {
+            if (!empty($object['created']))
+                $object['created'] = new DateTime('now', self::$timezone);
+            $this->obj->setCreated(self::get_datetime($object['created']));
+        }
+
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+
+        // TODO: increase sequence
+        // $this->obj->setSequence($this->obj->sequence()+1);
+
+        // do the hard work of setting object values
+        $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
+        $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
+
+        $this->obj->setSummary($object['title']);
+        $this->obj->setLocation($object['location']);
+        $this->obj->setDescription($object['description']);
+        $this->obj->setPriority($object['priority']);
+        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
+        $this->obj->setCategories(self::array2vector($object['categories']));
+        $this->obj->setTransparency($object['free_busy'] == 'free');
+
+        $status = kolabformat::StatusUndefined;
+        if ($object['free_busy'] == 'tentative')
+            $status = kolabformat::StatusTentative;
+        if ($object['cancelled'])
+            $status = kolabformat::StatusCancelled;
+        $this->obj->setStatus($status);
+
+        // process event attendees
+        $organizer = new ContactReference;
+        $attendees = new vectorattendee;
+        foreach ((array)$object['attendees'] as $attendee) {
+            $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
+            $cr->setName($attendee['name']);
+
+            if ($attendee['role'] == 'ORGANIZER') {
+                $organizer = $cr;
+            }
+            else {
+                $att = new Attendee;
+                $att->setContact($cr);
+                $att->setPartStat($this->status_map[$attendee['status']]);
+                $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
+                $att->setRSVP((bool)$attendee['rsvp']);
+
+                if ($att->isValid())
+                    $attendees->push($att);
+            }
+        }
+        $this->obj->setOrganizer($organizer);
+        $this->obj->setAttendees($attendees);
+
+        // TODO: save recurrence rule
+
+        // TODO: save alarm
+        $valarms = new vectoralarm;
+        if ($object['alarms']) {
+          $alarm = new Alarm;
+            list($duration, $type) = explode(":", $object['alarms']);
+            
+            $valarms->push($alarm);
+        }
+        $this->obj->setAlarms($valarms);
+
+        // TODO: save attachments
+
+        // cache this data
+        unset($object['_formatobj']);
+        $this->data = $object;
     }
 
+    /**
+     *
+     */
     public function is_valid()
     {
-        return is_object($this->obj) && $this->obj->isValid();
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
     }
 
-    public function fromkolab2($object)
+    /**
+     * Convert the Contact object into a hash array data structure
+     *
+     * @return array  Contact data as hash array
+     */
+    public function to_array()
     {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $sensitivity_map = array_flip($this->sensitivity_map);
+
+        // read object properties
+        $object = array(
+            'uid'         => $this->obj->uid(),
+            'changed'     => self::php_datetime($this->obj->lastModified()),
+            'title'       => $this->obj->summary(),
+            'location'    => $this->obj->location(),
+            'description' => $this->obj->description(),
+            'allday'      => $this->obj->start()->isDateOnly(),
+            'start'       => self::php_datetime($this->obj->start()),
+            'end'         => self::php_datetime($this->obj->end()),
+            'categories'  => self::vector2array($this->obj->categories()),
+            'free_busy'   => $this->obj->transparency() ? 'free' : 'busy',  // TODO: transparency is only boolean
+            'sensitivity' => $sensitivity_map[$this->obj->classification()],
+            'priority'    => $this->obj->priority(),
+        );
+
+        // status defines different event properties...
+        $status = $this->obj->status();
+        if ($status == kolabformat::StatusTentative)
+          $object['free_busy'] = 'tentative';
+        else if ($status == kolabformat::StatusCancelled)
+          $objec['cancelled'] = true;
+
+        // read organizer and attendees
+        if ($organizer = $this->obj->organizer()) {
+            $object['attendees'][] = array(
+                'role' => 'ORGANIZER',
+                'email' => $organizer->email(),
+                'name' => $organizer->name(),
+            );
+        }
+
+        $role_map = array_flip($this->role_map);
+        $status_map = array_flip($this->status_map);
+        $attvec = $this->obj->attendees();
+        for ($i=0; $i < $attvec->size(); $i++) {
+            $attendee = $attvec->get($i);
+            $cr = $attendee->contact();
+            $object['attendees'][] = array(
+                'role' => $role_map[$attendee->role()],
+                'status' => $status_map[$attendee->partStat()],
+                'rsvp' => $attendee->rsvp(),
+                'email' => $cr->email(),
+                'name' => $cr->name(),
+            );
+        }
+
+        // TODO: hanlde recurrence rules, alarms and attachments
+
         $this->data = $object;
+        return $this->data;
     }
 
-    public function to_array()
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($rec)
     {
-        // TODO: read object properties
-        return $this->data;
+        if (PEAR::isError($rec))
+            return;
+
+        $start_time = date('H:i:s', $rec['start-date']);
+        $allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
+
+        if ($allday) {  // in Roundcube all-day events go from 12:00 to 13:00
+            $now = new DateTime('now', self::$timezone);
+            $gmt_offset = $now->getOffset();
+
+            $rec['start-date'] += 12 * 3600;
+            $rec['end-date']   -= 11 * 3600;
+            $rec['end-date']   -= $gmt_offset - date('Z', $rec['end-date']);    // shift times from server's timezone to user's timezone
+            $rec['start-date'] -= $gmt_offset - date('Z', $rec['start-date']);  // because generated with mktime() in Horde_Kolab_Format_Date::decodeDate()
+            // sanity check
+            if ($rec['end-date'] <= $rec['start-date'])
+              $rec['end-date'] += 86400;
+        }
+
+        // convert alarm time into internal format
+        if ($rec['alarm']) {
+            $alarm_value = $rec['alarm'];
+            $alarm_unit = 'M';
+            if ($rec['alarm'] % 1440 == 0) {
+                $alarm_value /= 1440;
+                $alarm_unit = 'D';
+            }
+            else if ($rec['alarm'] % 60 == 0) {
+                $alarm_value /= 60;
+                $alarm_unit = 'H';
+            }
+            $alarm_value *= -1;
+        }
+
+        // convert recurrence rules into internal pseudo-vcalendar format
+        if ($recurrence = $rec['recurrence']) {
+            $rrule = array(
+                'FREQ' => strtoupper($recurrence['cycle']),
+                'INTERVAL' => intval($recurrence['interval']),
+            );
+
+            if ($recurrence['range-type'] == 'number')
+                $rrule['COUNT'] = intval($recurrence['range']);
+            else if ($recurrence['range-type'] == 'date')
+                $rrule['UNTIL'] = $recurrence['range'];
+
+            if ($recurrence['day']) {
+                $byday = array();
+                $prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
+                foreach ($recurrence['day'] as $day)
+                    $byday[] = $prefix . substr(strtoupper($day), 0, 2);
+                $rrule['BYDAY'] = join(',', $byday);
+            }
+            if ($recurrence['daynumber']) {
+                if ($recurrence['type'] == 'monthday' || $recurrence['type'] == 'daynumber')
+                    $rrule['BYMONTHDAY'] = $recurrence['daynumber'];
+                else if ($recurrence['type'] == 'yearday')
+                    $rrule['BYYEARDAY'] = $recurrence['daynumber'];
+            }
+            if ($recurrence['month']) {
+                $monthmap = array_flip($this->kolab2_monthmap);
+                $rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
+            }
+
+            if ($recurrence['exclusion']) {
+                foreach ((array)$recurrence['exclusion'] as $excl)
+                    $rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date']));  // use time of event start
+            }
+        }
+
+        $attendees = array();
+        if ($rec['organizer']) {
+            $attendees[] = array(
+                'role' => 'ORGANIZER',
+                'name' => $rec['organizer']['display-name'],
+                'email' => $rec['organizer']['smtp-address'],
+                'status' => 'ACCEPTED',
+            );
+            $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
+        }
+
+        foreach ((array)$rec['attendee'] as $attendee) {
+            $attendees[] = array(
+                'role' => $this->kolab2_rolemap[$attendee['role']],
+                'name' => $attendee['display-name'],
+                'email' => $attendee['smtp-address'],
+                'status' => $this->kolab2_statusmap[$attendee['status']],
+                'rsvp' => $attendee['request-response'],
+            );
+            $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
+        }
+
+        $this->data = array(
+            'uid' => $rec['uid'],
+            'title' => $rec['summary'],
+            'location' => $rec['location'],
+            'description' => $rec['body'],
+            'start' => $rec['start-date'],
+            'end' => $rec['end-date'],
+            'allday' => $allday,
+            'recurrence' => $rrule,
+            'alarms' => $alarm_value . $alarm_unit,
+            'categories' => explode(',', $rec['categories']),
+            'attachments' => $attachments,
+            'attendees' => $attendees,
+            'free_busy' => $rec['show-time-as'],
+            'priority' => $rec['priority'],
+            'sensitivity' => $rec['sensitivity'],
+            'changed' => $rec['last-modification-date'],
+        );
     }
 }


commit 3ba191ed68b37ce1cb06ec25cb26a0b42f09d2ce
Author: Thomas B <roundcube at gmail.com>
Date:   Fri Mar 30 19:08:43 2012 +0200

    Fix server-side subscription test

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 0195c01..9c2cf83 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -185,7 +185,7 @@ class kolab_storage_folder
 
         if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
             if (!$subscribed)
-                $subscribed = $this->imap->list_folders();
+                $subscribed = $this->imap->list_folders_subscribed();
 
             return in_array($this->name, $subscribed);
         }


commit 5feac6ed7bff12ddeadafe414232df2ac479fea4
Author: Thomas B <roundcube at gmail.com>
Date:   Fri Mar 30 19:07:56 2012 +0200

    Unset kolab_format object reference to reduce footprint; fix DateTime conversion

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 0f19e1a..c72f9ac 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -61,8 +61,11 @@ abstract class kolab_format
         if (!$tz) $tz = self::$timezone;
         $result = new cDateTime();
 
-        if (is_numeric($datetime))
-            $datetime = new DateTime('@'.$datetime, $tz);
+        // got a unix timestamp (in UTC)
+        if (is_numeric($datetime)) {
+            $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
+            if ($tz) $datetime->setTimezone($tz);
+        }
         else if (is_string($datetime) && strlen($datetime))
             $datetime = new DateTime($datetime, $tz);
 
@@ -71,6 +74,7 @@ abstract class kolab_format
 
             if (!$dateonly)
                 $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
+
             if ($tz)
                 $result->setTimezone($tz->getName());
         }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index eb09d64..02e3e89 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -274,6 +274,7 @@ class kolab_format_contact extends kolab_format
 
 
         // cache this data
+        unset($object['_formatobj']);
         $this->data = $object;
     }
 
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index 9fe28c1..3c5047c 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -77,6 +77,10 @@ class kolab_format_distributionlist extends kolab_format
         }
 
         $this->obj->setMembers($members);
+
+        // cache this data
+        unset($object['_formatobj']);
+        $this->data = $object;
     }
 
     public function is_valid()


commit 70c42619cd16c7f85119253be6c21270acb3d89b
Author: Thomas B <roundcube at gmail.com>
Date:   Fri Mar 30 19:04:11 2012 +0200

    Small bugfix and rubustness improvement

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index a2aac7c..012d176 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -337,7 +337,7 @@ function rcube_calendar_ui(settings)
       var $dialog = $("#eventshow").dialog('close').removeClass().addClass('uidialog');
       var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
       me.selected_event = event;
-      
+
       $dialog.find('div.event-section, div.event-line').hide();
       $('#event-title').html(Q(event.title)).show();
       
@@ -363,9 +363,10 @@ function rcube_calendar_ui(settings)
       if (event.free_busy)
         $('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
       if (event.priority > 0) {
-        var priolabels = [ '', rcmail.gettext('high'), rcmail.gettext('highest'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+        var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
         $('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority]));
       }
+
       if (event.sensitivity != 0) {
         var sensitivityclasses = { 0:'public', 1:'private', 2:'confidential' };
         $('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
@@ -415,7 +416,7 @@ function rcube_calendar_ui(settings)
         $('#event-rsvp')[(rsvp?'show':'hide')]();
         $('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+rsvp+']').prop('disabled', true);
       }
-      
+
       var buttons = {};
       if (calendar.editable && event.editable !== false) {
         buttons[rcmail.gettext('edit', 'calendar')] = function() {
@@ -2407,7 +2408,7 @@ function rcube_calendar_ui(settings)
         }
       },
       viewRender: function(view) {
-        if (view.name == 'month')
+        if (fc && view.name == 'month')
           fc.fullCalendar('option', 'maxHeight', Math.floor((view.element.parent().height()-18) / 6) - 35);
       }
     });


commit 1236a91bb506fbb9dfaa9b50b283b325bb153f87
Author: Thomas B <roundcube at gmail.com>
Date:   Wed Mar 28 17:53:32 2012 +0200

    Fix typo and function arguments

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index d4cc061..0195c01 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -108,7 +108,7 @@ class kolab_storage_folder
      */
     public function set_metadata($entries)
     {
-        return $this->imap->get_metadata($this->name, $entries);
+        return $this->imap->set_metadata($this->name, $entries);
     }
 
 
@@ -378,7 +378,7 @@ class kolab_storage_folder
      * @param string $uid       The UID of the old object if it existed before
      * @return boolean          True on success, false on error
      */
-    public function save(&$object, $type, $uid = null)
+    public function save(&$object, $type = null, $uid = null)
     {
         if (!$type)
             $type = $this->type;
@@ -574,8 +574,8 @@ class kolab_storage_folder
 
         $result = $this->trigger_url($url);
         if (is_a($result, 'PEAR_Error')) {
-            return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s"),
-                                            $this->name, $result->getMessage());
+            return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
+                                            $this->name, $result->getMessage()));
         }
         return $result;
     }


commit 47f60029138947c10a7e2f4ce0ebe2548eca43d7
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Mar 22 22:08:13 2012 +0100

    Add methods for IMAP folder manipulations

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 5146899..d42dc1e 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -25,6 +25,8 @@
 class kolab_storage
 {
     const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
+    const SERVERSIDE_SUBSCRIPTION = 0;
+    const CLIENTSIDE_SUBSCRIPTION = 1;
 
     public static $last_error;
 
@@ -40,7 +42,7 @@ class kolab_storage
     public static function setup()
     {
         if (self::$ready)
-            return;
+            return true;
 
         $rcmail = rcmail::get_instance();
         self::$config = $rcmail->config;
@@ -56,6 +58,8 @@ class kolab_storage
             ));
             self::$imap->set_pagesize(9999);
         }
+
+        return self::$ready;
     }
 
 
@@ -68,10 +72,9 @@ class kolab_storage
      */
     public static function get_folders($type)
     {
-        self::setup();
         $folders = array();
 
-        if (self::$ready) {
+        if (self::setup()) {
             foreach ((array)self::$imap->list_folders('', '*', $type) as $foldername) {
                 $folders[$foldername] = new kolab_storage_folder($foldername, self::$imap);
             }
@@ -85,12 +88,11 @@ class kolab_storage
      * Getter for a specific storage folder
      *
      * @param string  IMAP folder to access (UTF7-IMAP)
-     * @return object Kolab_Folder  The folder object
+     * @return object kolab_storage_folder  The folder object
      */
     public static function get_folder($folder)
     {
-        self::setup();
-        return self::$ready ? new kolab_storage_folder($folder, null, self::$imap) : null;
+        return self::setup() ? new kolab_storage_folder($folder, null, self::$imap) : null;
     }
 
 
@@ -104,6 +106,7 @@ class kolab_storage
      */
     public static function get_object($uid, $type)
     {
+        self::setup();
         $folder = null;
         foreach ((array)self::$imap->list_folders('', '*', $type) as $foldername) {
             if (!$folder)
@@ -151,6 +154,70 @@ class kolab_storage
 
 
     /**
+     * Deletes IMAP folder
+     *
+     * @param string $name Folder name (UTF7-IMAP)
+     *
+     * @return bool True on success, false on failure
+     */
+    public static function folder_delete($name)
+    {
+        self::setup();
+
+        $success = self::$imap->delete_folder($name);
+        self::$last_error = self::$imap->get_error_str();
+
+        return $success;
+    }
+
+    /**
+     * Creates IMAP folder
+     *
+     * @param string $name    Folder name (UTF7-IMAP)
+     * @param string $type    Folder type
+     * @param bool   $default True if older is default (for specified type)
+     *
+     * @return bool True on success, false on failure
+     */
+    public static function folder_create($name, $type=null, $default=false)
+    {
+        self::setup();
+
+        if (self::$imap->create_folder($name)) {
+            // set metadata for folder type
+            $ctype = $type . ($default ? '.default' : '');
+            $saved = self::$imap->set_metadata($name, array(self::CTYPE_KEY => $ctype));
+
+            if ($saved)
+                return true;
+            else  // revert if metadata could not be set
+                self::$imap->delete_folder($name);
+        }
+
+        self::$last_error = self::$imap->get_error_str();
+        return false;
+    }
+
+    /**
+     * Renames IMAP folder
+     *
+     * @param string $oldname Old folder name (UTF7-IMAP)
+     * @param string $newname New folder name (UTF7-IMAP)
+     *
+     * @return bool True on success, false on failure
+     */
+    public static function folder_rename($oldname, $newname)
+    {
+        self::setup();
+
+        $success = self::$imap->rename_folder($oldname, $newname);
+        self::$last_error = self::$imap->get_error_str();
+
+        return $success;
+    }
+
+
+    /**
      * Getter for human-readable name of Kolab object (folder)
      * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
      *
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 02efc57..d4cc061 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -88,6 +88,31 @@ class kolab_storage_folder
 
 
     /**
+     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+     *
+     * @param array List of metadata keys to read
+     * @return array Metadata entry-value hash array on success, NULL on error
+     */
+    public function get_metadata($keys)
+    {
+        $metadata = $this->imap->get_metadata($this->name, (array)$keys);
+        return $metadata[$this->name];
+    }
+
+
+    /**
+     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+     *
+     * @param array  $entries Entry-value array (use NULL value as NIL)
+     * @return boolean True on success, False on failure
+     */
+    public function set_metadata($entries)
+    {
+        return $this->imap->get_metadata($this->name, $entries);
+    }
+
+
+    /**
      * Returns the owner of the folder.
      *
      * @return string  The owner of this folder.
@@ -147,6 +172,32 @@ class kolab_storage_folder
         return join('', (array)$this->imap->get_acl($this->name));
     }
 
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @param string Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed($type = 0)
+    {
+        static $subscribed;  // local cache
+
+        if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
+            if (!$subscribed)
+                $subscribed = $this->imap->list_folders();
+
+            return in_array($this->name, $subscribed);
+        }
+        else if (kolab_storage::CLIENTSIDE_SUBSCRIPTION) {
+            // TODO: implement this
+            return true;
+        }
+
+        return false;
+    }
+
+
     /**
      * Get number of objects stored in this folder
      *


commit 5ab4071542b7bac1e1fc3456933df4d6e4d5b8ad
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Mar 22 20:22:26 2012 +0100

    More state checks

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 58ba389..30a8ebd 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -111,7 +111,7 @@ class rcube_kolab_contacts extends rcube_addressbook
 
         // fetch objects from the given IMAP folder
         $this->storagefolder = kolab_storage::get_folder($this->imap_folder);
-        $this->ready = !PEAR::isError($this->storagefolder);
+        $this->ready = $this->storagefolder && !PEAR::isError($this->storagefolder);
 
         // Set readonly and editable flags according to folder permissions
         if ($this->ready) {
@@ -164,7 +164,7 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function get_namespace()
     {
-        if ($this->namespace === null) {
+        if ($this->namespace === null && $this->ready) {
             $this->namespace = $this->storagefolder->get_namespace();
         }
 


commit ef550eaec42ecdd6bb5e490c7e9c0aaaeaaa970e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 21 16:15:53 2012 +0100

    Store PGP public key in contact; remove unsupported field definitions from old format

diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 8abdab6..ef6d11a 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -289,12 +289,10 @@ class kolab_addressbook extends rcube_plugin
 
         // extend the list of contact fields to be displayed in the 'personal' section
         if (is_array($p['form']['personal'])) {
-            $p['form']['contact']['content']['officelocation'] = array('size' => 40);
-            $p['form']['personal']['content']['initials']      = array('size' => 6);
             $p['form']['personal']['content']['profession']    = array('size' => 40);
             $p['form']['personal']['content']['children']      = array('size' => 40);
-            $p['form']['personal']['content']['pgppublickey']  = array('size' => 40);
             $p['form']['personal']['content']['freebusyurl']   = array('size' => 40);
+            $p['form']['personal']['content']['pgppublickey']  = array('size' => 40);
 
             // re-order fields according to the coltypes list
             $p['form']['contact']['content']  = $this->_sort_form_fields($p['form']['contact']['content']);
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 41f910e..58ba389 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -46,14 +46,10 @@ class rcube_kolab_contacts extends rcube_addressbook
       'department'   => array('limit' => 1),
       'email'        => array('subtypes' => null),
       'phone'        => array(),
-      'address'      => array('subtypes' => array('home','work')),
-//      'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
-//                                'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
+      'address'      => array('subtypes' => array('home','work','office')),
       'website'      => array('subtypes' => null),
       'im'           => array('subtypes' => null),
       'gender'       => array('limit' => 1),
-      'initials'     => array('type' => 'text', 'size' => 6, 'maxlength' => 10, 'limit' => 1,
-                                'label' => 'kolab_addressbook.initials', 'category' => 'personal'),
       'birthday'     => array('limit' => 1),
       'anniversary'  => array('limit' => 1),
       'profession'   => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
@@ -63,10 +59,10 @@ class rcube_kolab_contacts extends rcube_addressbook
       'spouse'       => array('limit' => 1),
       'children'     => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => null,
                                 'label' => 'kolab_addressbook.children', 'category' => 'personal'),
-      'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1,
-                                'label' => 'kolab_addressbook.pgppublickey'),
       'freebusyurl'  => array('type' => 'text', 'size' => 40, 'limit' => 1,
                                 'label' => 'kolab_addressbook.freebusyurl'),
+      'pgppublickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
+                                'label' => 'kolab_addressbook.pgppublickey'),
       'notes'        => array(),
       'photo'        => array(),
       // TODO: define more Kolab-specific fields such as: language, latitude, longitude
@@ -93,6 +89,7 @@ class rcube_kolab_contacts extends rcube_addressbook
     private $result;
     private $namespace;
     private $imap_folder = 'INBOX/Contacts';
+    private $action;
 
 
     public function __construct($imap_folder = null)
@@ -132,6 +129,8 @@ class rcube_kolab_contacts extends rcube_addressbook
                 }
             }
         }
+
+        $this->action = rcmail::get_instance()->action;
     }
 
 
@@ -1046,10 +1045,14 @@ class rcube_kolab_contacts extends rcube_addressbook
         // photo is stored as separate attachment
         if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
             // only fetch photo content if requested
-            if (rcmail::get_instance()->action == 'photo')
+            if ($this->action == 'photo')
                 $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['key']);
         }
 
+        // truncate publickey value for display
+        if ($record['pgppublickey'] && $this->action == 'show')
+            $record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
+
         // remove empty fields
         return array_filter($record);
     }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 5615c21..eb09d64 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -264,7 +264,13 @@ class kolab_format_contact extends kolab_format
         }
         $this->obj->setRelateds($rels);
 
-        // TODO: handle language, pgppublickey, etc.
+        if (isset($object['pgppublickey'])) {
+            $crypto = new Crypto;
+            $crypto->setPGPKey($object['pgppublickey']);
+            $this->obj->setCrypto($crypto);
+        }
+
+        // TODO: handle language, gpslocation, etc.
 
 
         // cache this data
@@ -352,6 +358,11 @@ class kolab_format_contact extends kolab_format
         // relateds -> spouse, children
         $this->read_relateds($this->obj->relateds(), $object);
 
+        // crypto settings: currently only pgpkey is supported
+        $crypto = $this->obj->crypto();
+        if ($pgpkey = $crypto->pgpKey())
+            $object['pgppublickey'] = $pgpkey;
+
         $this->data = $object;
         return $this->data;
     }


commit 8af6b9eec3693b5d0bd2518db3fbd1506599a47d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 21 15:33:30 2012 +0100

    Only search undeleted messages when resolving uids.
    Store office-location as Address block in Affiliation and profession into title attribute.
    Merge initials into nickname.

diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index b93615e..5615c21 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -43,6 +43,7 @@ class kolab_format_contact extends kolab_format
     public $addresstypes = array(
         'home' => Address::Home,
         'work' => Address::Work,
+        'office' => 0,
     );
 
     private $gendermap = array(
@@ -70,13 +71,11 @@ class kolab_format_contact extends kolab_format
       'organization' => 'organization',
       'department'   => 'department',
       'job-title'    => 'jobtitle',
-      'initials'     => 'initials',
       'birthday'     => 'birthday',
       'anniversary'  => 'anniversary',
       'phone'        => 'phone',
       'im-address'   => 'im',
       'web-page'     => 'website',
-      'office-location' => 'officelocation',
       'profession'   => 'profession',
       'manager-name' => 'manager',
       'assistant'    => 'assistant',
@@ -162,17 +161,18 @@ class kolab_format_contact extends kolab_format
 
         if (isset($object['nickname']))
             $this->obj->setNickNames(self::array2vector($object['nickname']));
+        if (isset($object['profession']))
+            $this->obj->setTitles(self::array2vector($object['profession']));
 
         // organisation related properties (affiliation)
         $org = new Affiliation;
+        $offices = new vectoraddress;
         if ($object['organization'])
             $org->setOrganisation($object['organization']);
         if ($object['department'])
             $org->setOrganisationalUnits(self::array2vector($object['department']));
         if ($object['jobtitle'])
             $org->setRoles(self::array2vector($object['jobtitle']));
-//        if ($object['officelocation'])
-//           $org->setOffices(self::array2vector($object['officelocation']));
 
         $rels = new vectorrelated;
         if ($object['manager']) {
@@ -185,10 +185,6 @@ class kolab_format_contact extends kolab_format
         }
         $org->setRelateds($rels);
 
-        $orgs = new vectoraffiliation;
-        $orgs->push($org);
-        $this->obj->setAffiliations($orgs);
-
         // email, im, url
         $this->obj->setEmailAddresses(self::array2vector($object['email']));
         $this->obj->setIMaddresses(self::array2vector($object['im']));
@@ -214,9 +210,18 @@ class kolab_format_contact extends kolab_format
             if ($address['country'])
                 $adr->setCountry($address['country']);
 
-            $adrs->push($adr);
+            if ($address['type'] == 'office')
+                $offices->push($adr);
+            else
+                $adrs->push($adr);
         }
         $this->obj->setAddresses($adrs);
+        $org->setAddresses($offices);
+
+        // add org affiliation after addresses are set
+        $orgs = new vectoraffiliation;
+        $orgs->push($org);
+        $this->obj->setAffiliations($orgs);
 
         // telephones
         $tels = new vectortelephone;
@@ -259,7 +264,7 @@ class kolab_format_contact extends kolab_format
         }
         $this->obj->setRelateds($rels);
 
-        // TODO: handle profession, language, pgppublickey, etc.
+        // TODO: handle language, pgppublickey, etc.
 
 
         // cache this data
@@ -299,6 +304,7 @@ class kolab_format_contact extends kolab_format
         $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
         $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
         $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
+        $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
 
         // organisation related properties (affiliation)
         $orgs = $this->obj->affiliations();
@@ -306,7 +312,6 @@ class kolab_format_contact extends kolab_format
             $org = $orgs->get(0);
             $object['organization']   = $org->organisation();
             $object['jobtitle']       = join(' ', self::vector2array($org->roles()));
-//            $object['officelocation'] = join(' ', self::vector2array($org->offices()));
             $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
             $this->read_relateds($org->relateds(), $object);
         }
@@ -316,19 +321,9 @@ class kolab_format_contact extends kolab_format
         $object['website'] = self::vector2array($this->obj->urls());
 
         // addresses
-        $adrtypes = array_flip($this->addresstypes);
-        $addresses = $this->obj->addresses();
-        for ($i=0; $i < $addresses->size(); $i++) {
-            $adr = $addresses->get($i);
-            $object['address'][] = array(
-                'type'     => $adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : '', /*$adr->label(),*/
-                'street'   => $adr->street(),
-                'code'     => $adr->code(),
-                'locality' => $adr->locality(),
-                'region'   => $adr->region(),
-                'country'  => $adr->country()
-            );
-        }
+        $this->read_addresses($this->obj->addresses(), $object);
+        if ($org && ($offices = $org->addresses()))
+            $this->read_addresses($offices, $object, 'office');
 
         // telehones
         $tels = $this->obj->telephones();
@@ -401,11 +396,39 @@ class kolab_format_contact extends kolab_format
             }
         }
 
+        // office location goes into an address block
+        if ($record['office-location'])
+            $object['address'][] = array('type' => 'office', 'locality' => $record['office-location']);
+
+        // merge initials into nickname
+        if ($record['initials'])
+            $object['nickname'] = trim($object['nickname'] . ', ' . $record['initials'], ', ');
+
         // remove empty fields
         $this->data = array_filter($object);
     }
 
     /**
+     * Helper method to copy contents of an Address vector to the contact data object
+     */
+    private function read_addresses($addresses, &$object, $type = null)
+    {
+        $adrtypes = array_flip($this->addresstypes);
+
+        for ($i=0; $i < $addresses->size(); $i++) {
+            $adr = $addresses->get($i);
+            $object['address'][] = array(
+                'type'     => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
+                'street'   => $adr->street(),
+                'code'     => $adr->code(),
+                'locality' => $adr->locality(),
+                'region'   => $adr->region(),
+                'country'  => $adr->country()
+            );
+        }
+    }
+
+    /**
      * Helper method to map contents of a Related vector to the contact data object
      */
     private function read_relateds($rels, &$object)
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 0ac475c..02efc57 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -406,7 +406,7 @@ class kolab_storage_folder
      */
     public function undelete($uid)
     {
-        if ($msguid = $this->uid2msguid($uid)) {
+        if ($msguid = $this->uid2msguid($uid, true)) {
             if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
                 return $msguid;
             }
@@ -419,11 +419,11 @@ class kolab_storage_folder
     /**
      * Resolve an object UID into an IMAP message UID
      */
-    private function uid2msguid($uid)
+    private function uid2msguid($uid, $deleted = false)
     {
         if (!isset($this->uid2msg[$uid])) {
             // use IMAP SEARCH to get the right message
-            $index = $this->imap->search_once($this->name, 'HEADER SUBJECT ' . $uid);
+            $index = $this->imap->search_once($this->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
             $results = $index->get();
             $this->uid2msg[$uid] = $results[0];
 


commit bafdb3bd63a50c1472f14b335d94d44ac33a5171
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 21 11:10:17 2012 +0100

    Pass the uid of an updated object to the storage layer

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index d4b5c06..41f910e 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -567,8 +567,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         if ($old = $this->storagefolder->get_object($this->_id2uid($id))) {
             $object = $this->_from_rcube_contact($save_data, $old);
 
-            $saved = $this->storagefolder->save($object, 'contact', $uid);
-            if (!$saved) {
+            if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
                 raise_error(array(
                   'code' => 600, 'type' => 'php',
                   'file' => __FILE__, 'line' => __LINE__,
@@ -1062,6 +1061,8 @@ class rcube_kolab_contacts extends rcube_addressbook
     {
         if (!$contact['uid'] && $contact['ID'])
             $contact['uid'] = $this->_id2uid($contact['ID']);
+        else if (!$contact['uid'] && $old['uid'])
+            $contact['uid'] = $old['uid'];
 
         $contact['email'] = array_filter($this->get_col_values('email', $contact, true));
         $contact['website'] = array_filter($this->get_col_values('website', $contact, true));


commit 88d6ce950086ee971a7ec57e859baac915057038
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Mar 20 23:51:43 2012 +0100

    Adapt to recent changes in libkolabxml:
    - store manager, assistant, spouse and children in Related objects
    - add support for both uid and mailto distlist members
    - fix contact photo transition from old Kolab2 format

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 359d8ad..d4b5c06 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -47,8 +47,8 @@ class rcube_kolab_contacts extends rcube_addressbook
       'email'        => array('subtypes' => null),
       'phone'        => array(),
       'address'      => array('subtypes' => array('home','work')),
-      'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
-                                'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
+//      'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
+//                                'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
       'website'      => array('subtypes' => null),
       'im'           => array('subtypes' => null),
       'gender'       => array('limit' => 1),
@@ -58,10 +58,10 @@ class rcube_kolab_contacts extends rcube_addressbook
       'anniversary'  => array('limit' => 1),
       'profession'   => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
                                 'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
-      'manager'      => array('limit' => 1),
-      'assistant'    => array('limit' => 1),
+      'manager'      => array('limit' => null),
+      'assistant'    => array('limit' => null),
       'spouse'       => array('limit' => 1),
-      'children'     => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
+      'children'     => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => null,
                                 'label' => 'kolab_addressbook.children', 'category' => 'personal'),
       'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1,
                                 'label' => 'kolab_addressbook.pgppublickey'),
@@ -69,7 +69,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                                 'label' => 'kolab_addressbook.freebusyurl'),
       'notes'        => array(),
       'photo'        => array(),
-      // TODO: define more Kolab-specific fields such as: role, language, latitude, longitude
+      // TODO: define more Kolab-specific fields such as: language, latitude, longitude
     );
 
     /**
@@ -257,11 +257,14 @@ class rcube_kolab_contacts extends rcube_addressbook
                 if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
                     continue;
 
-                $contact = $this->storagefolder->get_object($member['uid']);
-                if ($contact  && !$seen[$member['ID']]++) {
+                if ($member['uid'] && ($contact = $this->storagefolder->get_object($member['uid'])) && !$seen[$member['ID']]++) {
                     $this->contacts[$member['ID']] = $this->_to_rcube_contact($contact);
                     $this->result->count++;
                 }
+                else if ($member['email'] && !$seen[$member['ID']]++) {
+                    $this->contacts[$member['ID']] = $member;
+                    $this->result->count++;
+                }
             }
             $ids = array_keys($seen);
         }
@@ -458,8 +461,18 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function get_record($id, $assoc=false)
     {
-        if ($object = $this->storagefolder->get_object($this->_id2uid($id))) {
+        $rec = null;
+        $uid = $this->_id2uid($id);
+        if (strpos($uid, 'mailto:') === 0) {
+            $this->_fetch_groups(true);
+            $rec = $this->contacts[$id];
+            $this->readonly = true;  // set source to read-only
+        }
+        else if ($object = $this->storagefolder->get_object($uid)) {
             $rec = $this->_to_rcube_contact($object);
+        }
+
+        if ($rec) {
             $this->result = new rcube_result_set(1);
             $this->result->add($rec);
             return $assoc ? $rec : $this->result;
@@ -592,19 +605,22 @@ class rcube_kolab_contacts extends rcube_addressbook
         $count = 0;
         foreach ($ids as $id) {
             if ($uid = $this->_id2uid($id)) {
-                $deleted = $this->storagefolder->delete($uid, $force);
+                $is_mailto = strpos($uid, 'mailto:') === 0;
+                $deleted = $is_mailto || $this->storagefolder->delete($uid, $force);
 
                 if (!$deleted) {
                     raise_error(array(
                       'code' => 600, 'type' => 'php',
                       'file' => __FILE__, 'line' => __LINE__,
-                      'message' => "Error deleting a contact object from the Kolab server"),
+                      'message' => "Error deleting a contact object $uid from the Kolab server"),
                     true, false);
                 }
                 else {
                     // remove from distribution lists
-                    foreach ((array)$this->groupmembers[$id] as $gid)
-                        $this->remove_from_group($gid, $id);
+                    foreach ((array)$this->groupmembers[$id] as $gid) {
+                        if (!$is_mailto || $gid == $this->gid)
+                            $this->remove_from_group($gid, $id);
+                    }
 
                     // clear internal cache
                     unset($this->contacts[$id], $this->groupmembers[$id]);
@@ -776,7 +792,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         $added = 0;
         $exists = array();
 
-        $this->_fetch_groups();
+        $this->_fetch_groups(true);
         $list = $this->distlists[$gid];
 
         foreach ((array)$list['member'] as $i => $member)
@@ -786,16 +802,24 @@ class rcube_kolab_contacts extends rcube_addressbook
         $ids = array_diff($ids, $exists);
 
         foreach ($ids as $contact_id) {
-            if ($uid = $this->_id2uid($contact_id)) {
-                $contact = $this->storagefolder->get_object($uid);
-                foreach ($this->get_col_values('email', $contact, true) as $email) {
-                    $list['member'][] = array(
-                        'uid' => $uid,
-                        'mailto' => $email,
-                        'name' => $contact['name'],
-                    );
+            $uid = $this->_id2uid($contact_id);
+            if ($contact = $this->storagefolder->get_object($uid)) {
+                foreach ($this->get_col_values('email', $contact, true) as $email)
                     break;
-                }
+
+                $list['member'][] = array(
+                    'uid' => $uid,
+                    'email' => $email,
+                    'name' => $contact['name'],
+                );
+                $this->groupmembers[$contact_id][] = $gid;
+                $added++;
+            }
+            else if (strpos($uid, 'mailto:') === 0 && ($contact = $this->contacts[$contact_id])) {
+                $list['member'][] = array(
+                    'email' => $contact['email'],
+                    'name' => $contact['name'],
+                );
                 $this->groupmembers[$contact_id][] = $gid;
                 $added++;
             }
@@ -953,16 +977,20 @@ class rcube_kolab_contacts extends rcube_addressbook
     /**
      * Read distribution-lists AKA groups from server
      */
-    private function _fetch_groups()
+    private function _fetch_groups($with_contacts = false)
     {
         if (!isset($this->distlists)) {
             $this->distlists = $this->groupmembers = array();
             foreach ((array)$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']);
+                    $mid = $this->_uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);
                     $record['member'][$i]['ID'] = $mid;
+                    $record['member'][$i]['readonly'] = empty($member['uid']);
                     $this->groupmembers[$mid][] = $record['ID'];
+
+                    if ($with_contacts && empty($member['uid']))
+                        $this->contacts[$mid] = $record['member'][$i];
                 }
                 $this->distlists[$record['ID']] = $record;
             }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index daef994..b93615e 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -47,7 +47,14 @@ class kolab_format_contact extends kolab_format
 
     private $gendermap = array(
         'female' => Contact::Female,
-        'male' => Contact::Male,
+        'male'   => Contact::Male,
+    );
+
+    private $relatedmap = array(
+        'manager'   => Related::Manager,
+        'assistant' => Related::Assistant,
+        'spouse'    => Related::Spouse,
+        'children'  => Related::Child,
     );
 
     // old Kolab 2 format field map
@@ -160,15 +167,23 @@ class kolab_format_contact extends kolab_format
         $org = new Affiliation;
         if ($object['organization'])
             $org->setOrganisation($object['organization']);
+        if ($object['department'])
+            $org->setOrganisationalUnits(self::array2vector($object['department']));
         if ($object['jobtitle'])
-            $org->setTitles(self::array2vector($object['jobtitle']));
-        if ($object['officelocation'])
-            $org->setOffices(self::array2vector($object['officelocation']));
-        if ($object['manager'])
-            $org->setManagers(self::array2vector($object['manager']));
-        if ($object['assistant'])
-            $org->setAssistants(self::array2vector($object['assistant']));
-        // department ?
+            $org->setRoles(self::array2vector($object['jobtitle']));
+//        if ($object['officelocation'])
+//           $org->setOffices(self::array2vector($object['officelocation']));
+
+        $rels = new vectorrelated;
+        if ($object['manager']) {
+            foreach ((array)$object['manager'] as $manager)
+                $rels->push(new Related(Related::Text, $manager, Related::Manager));
+        }
+        if ($object['assistant']) {
+            foreach ((array)$object['assistant'] as $assistant)
+                $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+        }
+        $org->setRelateds($rels);
 
         $orgs = new vectoraffiliation;
         $orgs->push($org);
@@ -226,21 +241,26 @@ class kolab_format_contact extends kolab_format
             $this->obj->setAnniversary(self::get_datetime($object['anniversary'], null, true));
 
         if (!empty($object['photo'])) {
-            if (strlen($object['photo']) < 255 && ($att = $object['_attachments'][$object['photo']])) {
-                if ($att['content'])
-                    $this->obj->setPhoto($att['content'], $att['type']);
-                $object['_attachments'][$object['photo']] = false;
-            }
-            else if ($type = rc_image_content_type($object['photo'])) {
+            if ($type = rc_image_content_type($object['photo']))
                 $this->obj->setPhoto($object['photo'], $type);
-                $object['_attachments']['photo.attachment'] = false;
-            }
         }
         else if (isset($object['photo'])) {
             $this->obj->setPhoto('','');
         }
 
-        // TODO: handle spouse, children, profession, initials, pgppublickey, etc.
+        // spouse and children are relateds
+        $rels = new vectorrelated;
+        if ($object['spouse']) {
+            $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
+        }
+        if ($object['children']) {
+            foreach ((array)$object['children'] as $child)
+                $rels->push(new Related(Related::Text, $child, Related::Child));
+        }
+        $this->obj->setRelateds($rels);
+
+        // TODO: handle profession, language, pgppublickey, etc.
+
 
         // cache this data
         $this->data = $object;
@@ -285,10 +305,10 @@ class kolab_format_contact extends kolab_format
         if ($orgs->size()) {
             $org = $orgs->get(0);
             $object['organization']   = $org->organisation();
-            $object['jobtitle']       = join(' ', self::vector2array($org->titles()));
-            $object['manager']        = join(' ', self::vector2array($org->managers()));
-            $object['assistant']      = join(' ', self::vector2array($org->assistants()));
-            $object['officelocation'] = join(' ', self::vector2array($org->offices()));
+            $object['jobtitle']       = join(' ', self::vector2array($org->roles()));
+//            $object['officelocation'] = join(' ', self::vector2array($org->offices()));
+            $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
+            $this->read_relateds($org->relateds(), $object);
         }
 
         $object['email']   = self::vector2array($this->obj->emailAddresses());
@@ -334,6 +354,9 @@ class kolab_format_contact extends kolab_format
         if ($this->obj->photoMimetype())
             $object['photo'] = $this->obj->photo();
 
+        // relateds -> spouse, children
+        $this->read_relateds($this->obj->relateds(), $object);
+
         $this->data = $object;
         return $this->data;
     }
@@ -381,4 +404,26 @@ class kolab_format_contact extends kolab_format
         // remove empty fields
         $this->data = array_filter($object);
     }
+
+    /**
+     * Helper method to map contents of a Related vector to the contact data object
+     */
+    private function read_relateds($rels, &$object)
+    {
+        $typemap = array_flip($this->relatedmap);
+
+        for ($i=0; $i < $rels->size(); $i++) {
+            $rel = $rels->get($i);
+            if ($rel->type() != Related::Text)  // we can't handle UID relations yet
+                continue;
+
+            $types = $rel->relationTypes();
+            foreach ($typemap as $t => $field) {
+                if ($types & $t) {
+                    $object[$field][] = $rel->text();
+                    break;
+                }
+            }
+        }
+    }
 }
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index 8477176..9fe28c1 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -61,14 +61,21 @@ class kolab_format_distributionlist extends kolab_format
 
         $this->obj->setName($object['name']);
 
-        $members = new vectormember;
+        $seen = array();
+        $members = new vectorcontactref;
         foreach ($object['member'] as $member) {
-            $m = new Member;
+            if ($member['uid'])
+                $m = new ContactReference(ContactReference::UidReference, $member['uid']);
+            else if ($member['email'])
+                $m = new ContactReference(ContactReference::EmailReference, $member['email']);
+            else
+                continue;
+
             $m->setName($member['name']);
-            $m->setEmail($member['mailto']);
-            $m->setUid($member['uid']);
             $members->push($m);
+            $seen[$member['email']]++;
         }
+
         $this->obj->setMembers($members);
     }
 
@@ -91,7 +98,7 @@ class kolab_format_distributionlist extends kolab_format
 
         foreach ($record['member'] as $member) {
             $object['member'][] = array(
-                'mailto' => $member['smtp-address'],
+                'email' => $member['smtp-address'],
                 'name' => $member['display-name'],
                 'uid' => $member['uid'],
             );
@@ -122,11 +129,11 @@ class kolab_format_distributionlist extends kolab_format
         $members = $this->obj->members();
         for ($i=0; $i < $members->size(); $i++) {
             $member = $members->get($i);
-            if ($mailto = $member->email())
+#            if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
                 $object['member'][] = array(
-                    'mailto' => $mailto,
-                    'name' => $member->name(),
                     'uid' => $member->uid(),
+                    'email' => $member->email(),
+                    'name' => $member->name(),
                 );
         }
 
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 2e50753..0ac475c 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -338,9 +338,11 @@ class kolab_storage_folder
                 if (!isset($object['_attachments'][$name])) {
                     $object['_attachments'][$name] = $old['_attachments'][$name];
                 }
-                // load photo.attachment contents to be directly embedded in xcard block
-                if ($name == 'photo.attachment' && !$object['_attachments'][$name]['content'] && $att['key'])
-                    $object['_attachments'][$name]['content'] = $this->get_attachment($object['_msguid'], $att['key'], $object['_mailbox']);
+                // load photo.attachment from old Kolab2 format to be directly embedded in xcard block
+                if ($name == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$name]['content'] && $att['key']) {
+                    $object['photo'] = $this->get_attachment($object['_msguid'], $att['key'], $object['_mailbox']);
+                    unset($object['_attachments'][$name]);
+                }
             }
         }
 


commit 824c89b326eae3273c01e79c5f78738d20757e22
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Mar 16 21:26:34 2012 +0100

    Updated to latest WebODF version from git

diff --git a/plugins/odfviewer/webodf.js b/plugins/odfviewer/webodf.js
index 5d04c2a..5fd9248 100644
--- a/plugins/odfviewer/webodf.js
+++ b/plugins/odfviewer/webodf.js
@@ -34,161 +34,168 @@
 var core={},gui={},xmldom={},odf={};
 // Input 1
 function Runtime(){}Runtime.ByteArray=function(){};Runtime.ByteArray.prototype.slice=function(){};Runtime.prototype.byteArrayFromArray=function(){};Runtime.prototype.byteArrayFromString=function(){};Runtime.prototype.byteArrayToString=function(){};Runtime.prototype.concatByteArrays=function(){};Runtime.prototype.read=function(){};Runtime.prototype.readFile=function(){};Runtime.prototype.readFileSync=function(){};Runtime.prototype.loadXML=function(){};Runtime.prototype.writeFile=function(){};
-Runtime.prototype.isFile=function(){};Runtime.prototype.getFileSize=function(){};Runtime.prototype.deleteFile=function(){};Runtime.prototype.log=function(){};Runtime.prototype.setTimeout=function(){};Runtime.prototype.libraryPaths=function(){};Runtime.prototype.type=function(){};Runtime.prototype.getDOMImplementation=function(){};Runtime.prototype.getWindow=function(){};var IS_COMPILED_CODE=true;
-Runtime.byteArrayToString=function(i,k){function e(e){var a="",b,h=e.length,c,d,f;for(b=0;b<h;b+=1)c=e[b],c<128?a+=String.fromCharCode(c):(b+=1,d=e[b],c<224?a+=String.fromCharCode((c&31)<<6|d&63):(b+=1,f=e[b],a+=String.fromCharCode((c&15)<<12|(d&63)<<6|f&63)));return a}if(k==="utf8")return e(i);else k!=="binary"&&this.log("Unsupported encoding: "+k);return function(e){var a="",b,h=e.length;for(b=0;b<h;b+=1)a+=String.fromCharCode(e[b]&255);return a}(i)};
-Runtime.getFunctionName=function(i){return i.name===void 0?(i=/function\s+(\w+)/.exec(i))&&i[1]:i.name};
-function BrowserRuntime(i){function k(b,a){var c,d,f;a?f=b:a=b;if(i){d=i.ownerDocument;if(f)c=d.createElement("span"),c.className=f,c.appendChild(d.createTextNode(f)),i.appendChild(c),i.appendChild(d.createTextNode(" "));c=d.createElement("span");c.appendChild(d.createTextNode(a));i.appendChild(c);i.appendChild(d.createElement("br"))}else console&&console.log(a)}var e=this,g={},a=window.ArrayBuffer&&window.Uint8Array;this.ByteArray=a?function(b){Uint8Array.prototype.slice=function(b,c){if(c===void 0)b===
-void 0&&(b=0),c=this.length;var a=this.subarray(b,c),f,j;c-=b;f=new Uint8Array(new ArrayBuffer(c));for(j=0;j<c;j+=1)f[j]=a[j];return f};return new Uint8Array(new ArrayBuffer(b))}:function(b){var a=[];a.length=b;return a};this.concatByteArrays=a?function(b,a){var c,d=b.length,f=a.length,j=new this.ByteArray(d+f);for(c=0;c<d;c+=1)j[c]=b[c];for(c=0;c<f;c+=1)j[c+d]=a[c];return j}:function(b,a){return b.concat(a)};this.byteArrayFromArray=function(b){return b.slice()};this.byteArrayFromString=function(b,
-a){if(a==="utf8"){var c=b.length,d,f,j,g=0;for(f=0;f<c;f+=1)j=b.charCodeAt(f),g+=1+(j>128)+(j>2048);d=new e.ByteArray(g);for(f=g=0;f<c;f+=1)j=b.charCodeAt(f),j<128?(d[g]=j,g+=1):j<2048?(d[g]=192|j>>>6,d[g+1]=128|j&63,g+=2):(d[g]=224|j>>>12&15,d[g+1]=128|j>>>6&63,d[g+2]=128|j&63,g+=3);return d}else a!=="binary"&&e.log("unknown encoding: "+a);c=b.length;d=new e.ByteArray(c);for(f=0;f<c;f+=1)d[f]=b.charCodeAt(f)&255;return d};this.byteArrayToString=Runtime.byteArrayToString;this.readFile=function(b,
-a,c){if(g.hasOwnProperty(b))c(null,g[b]);else{var d=new XMLHttpRequest;d.open("GET",b,true);d.onreadystatechange=function(){var f;d.readyState===4&&(d.status===0&&!d.responseText?c("File "+b+" is empty."):d.status===200||d.status===0?(f=a==="binary"?typeof VBArray!=="undefined"?(new VBArray(d.responseBody)).toArray():e.byteArrayFromString(d.responseText,"binary"):d.responseText,g[b]=f,c(null,f)):c(d.responseText||d.statusText))};d.overrideMimeType&&(a!=="binary"?d.overrideMimeType("text/plain; charset="+
-a):d.overrideMimeType("text/plain; charset=x-user-defined"));try{d.send(null)}catch(f){c(f.message)}}};this.read=function(b,a,c,d){if(g.hasOwnProperty(b))d(null,g[b].slice(a,a+c));else{var f=new XMLHttpRequest;f.open("GET",b,true);f.onreadystatechange=function(){var j;f.readyState===4&&(f.status===0&&!f.responseText?d("File "+b+" is empty."):f.status===200||f.status===0?(j=typeof VBArray!=="undefined"?(new VBArray(f.responseBody)).toArray():e.byteArrayFromString(f.responseText,"binary"),g[b]=j,d(null,
-j.slice(a,a+c))):d(f.responseText||f.statusText))};f.overrideMimeType&&f.overrideMimeType("text/plain; charset=x-user-defined");try{f.send(null)}catch(j){d(j.message)}}};this.readFileSync=function(b,a){var c=new XMLHttpRequest,d;c.open("GET",b,false);c.overrideMimeType&&(a!=="binary"?c.overrideMimeType("text/plain; charset="+a):c.overrideMimeType("text/plain; charset=x-user-defined"));try{if(c.send(null),c.status===200||c.status===0)d=c.responseText}catch(f){}return d};this.writeFile=function(b,a,
-c){g[b]=a;var d=new XMLHttpRequest;d.open("PUT",b,true);d.onreadystatechange=function(){d.readyState===4&&(d.status===0&&!d.responseText?c("File "+b+" is empty."):d.status>=200&&d.status<300||d.status===0?c(null):c("Status "+String(d.status)+": "+d.responseText||d.statusText))};a=a.buffer&&!d.sendAsBinary?a.buffer:e.byteArrayToString(a,"binary");try{d.sendAsBinary?d.sendAsBinary(a):d.send(a)}catch(f){e.log("HUH? "+f+" "+a),c(f.message)}};this.deleteFile=function(b,a){var c=new XMLHttpRequest;c.open("DELETE",
-b,true);c.onreadystatechange=function(){c.readyState===4&&(c.status<200&&c.status>=300?a(c.responseText):a(null))};c.send(null)};this.loadXML=function(b,a){var c=new XMLHttpRequest;c.open("GET",b,true);c.overrideMimeType("text/xml");c.onreadystatechange=function(){c.readyState===4&&(c.status===0&&!c.responseText?a("File "+b+" is empty."):c.status===200||c.status===0?a(null,c.responseXML):a(c.responseText))};try{c.send(null)}catch(d){a(d.message)}};this.isFile=function(b,a){e.getFileSize(b,function(b){a(b!==
--1)})};this.getFileSize=function(b,a){var c=new XMLHttpRequest;c.open("HEAD",b,true);c.onreadystatechange=function(){if(c.readyState===4){var b=c.getResponseHeader("Content-Length");b?a(parseInt(b,10)):a(-1)}};c.send(null)};this.log=k;this.setTimeout=function(b,a){setTimeout(function(){b()},a)};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(){};this.type=function(){return"BrowserRuntime"};this.getDOMImplementation=function(){return window.document.implementation};this.exit=
-function(b){k("Calling exit with code "+String(b)+", but exit() is not implemented.")};this.getWindow=function(){return window}}
-function NodeJSRuntime(){var i=require("fs"),k="";this.ByteArray=function(e){return new Buffer(e)};this.byteArrayFromArray=function(e){var g=new Buffer(e.length),a,b=e.length;for(a=0;a<b;a+=1)g[a]=e[a];return g};this.concatByteArrays=function(e,g){var a=new Buffer(e.length+g.length);e.copy(a,0,0);g.copy(a,e.length,0);return a};this.byteArrayFromString=function(e,g){return new Buffer(e,g)};this.byteArrayToString=function(e,g){return e.toString(g)};this.readFile=function(e,g,a){g!=="binary"?i.readFile(e,
-g,a):i.readFile(e,null,a)};this.writeFile=function(e,g,a){i.writeFile(e,g,"binary",function(b){a(b||null)})};this.deleteFile=i.unlink;this.read=function(e,g,a,b){k&&(e=k+"/"+e);i.open(e,"r+",666,function(h,c){if(h)b(h);else{var d=new Buffer(a);i.read(c,d,0,a,g,function(a){i.close(c);b(a,d)})}})};this.readFileSync=function(e,g){return!g?"":i.readFileSync(e,g)};this.loadXML=function(){throw"Not implemented.";};this.isFile=function(e,g){k&&(e=k+"/"+e);i.stat(e,function(a,b){g(!a&&b.isFile())})};this.getFileSize=
-function(e,g){k&&(e=k+"/"+e);i.stat(e,function(a,b){a?g(-1):g(b.size)})};this.log=function(e){process.stderr.write(e+"\n")};this.setTimeout=function(e,g){setTimeout(function(){e()},g)};this.libraryPaths=function(){return[__dirname]};this.setCurrentDirectory=function(e){k=e};this.currentDirectory=function(){return k};this.type=function(){return"NodeJSRuntime"};this.getDOMImplementation=function(){return null};this.exit=process.exit;this.getWindow=function(){return null}}
-function RhinoRuntime(){var i=this,k=Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(),e,g,a="";k.setValidating(false);k.setNamespaceAware(true);k.setExpandEntityReferences(false);k.setSchema(null);g=Packages.org.xml.sax.EntityResolver({resolveEntity:function(a,h){var c=new Packages.java.io.FileReader(h);return new Packages.org.xml.sax.InputSource(c)}});e=k.newDocumentBuilder();e.setEntityResolver(g);this.ByteArray=function(a){return[a]};this.byteArrayFromArray=function(a){return a};
-this.byteArrayFromString=function(a){var h=[],c,d=a.length;for(c=0;c<d;c+=1)h[c]=a.charCodeAt(c)&255;return h};this.byteArrayToString=Runtime.byteArrayToString;this.concatByteArrays=function(a,h){return a.concat(h)};this.loadXML=function(a,h){var c=new Packages.java.io.File(a),d;try{d=e.parse(c)}catch(f){print(f);h(f);return}h(null,d)};this.readFile=function(a,h,c){var d=new Packages.java.io.File(a),f=h==="binary"?"latin1":h;d.isFile()?(a=readFile(a,f),h==="binary"&&(a=i.byteArrayFromString(a,"binary")),
-c(null,a)):c(a+" is not a file.")};this.writeFile=function(a,h,c){var a=new Packages.java.io.FileOutputStream(a),d,f=h.length;for(d=0;d<f;d+=1)a.write(h[d]);a.close();c(null)};this.deleteFile=function(a,h){(new Packages.java.io.File(a))["delete"]()?h(null):h("Could not delete "+a)};this.read=function(b,h,c,d){a&&(b=a+"/"+b);var f;f=b;var j="binary";(new Packages.java.io.File(f)).isFile()?(j==="binary"&&(j="latin1"),f=readFile(f,j)):f=null;f?d(null,this.byteArrayFromString(f.substring(h,h+c),"binary")):
-d("Cannot read "+b)};this.readFileSync=function(a,h){return!h?"":readFile(a,h)};this.isFile=function(b,h){a&&(b=a+"/"+b);var c=new Packages.java.io.File(b);h(c.isFile())};this.getFileSize=function(b,h){a&&(b=a+"/"+b);var c=new Packages.java.io.File(b);h(c.length())};this.log=print;this.setTimeout=function(a){a()};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(b){a=b};this.currentDirectory=function(){return a};this.type=function(){return"RhinoRuntime"};this.getDOMImplementation=
-function(){return e.getDOMImplementation()};this.exit=quit;this.getWindow=function(){return null}}var runtime=function(){return typeof window!=="undefined"?new BrowserRuntime(window.document.getElementById("logoutput")):typeof require!=="undefined"?new NodeJSRuntime:new RhinoRuntime}();
-(function(){function i(e){var a=e[0],b;b=eval("if (typeof "+a+" === 'undefined') {eval('"+a+" = {};');}"+a);for(a=1;a<e.length-1;a+=1)b.hasOwnProperty(e[a])||(b=b[e[a]]={});return b[e[e.length-1]]}var k={},e={};runtime.loadClass=function(g){if(!IS_COMPILED_CODE&&!k.hasOwnProperty(g)){var a=g.split("."),b;b=i(a);if(!b&&(b=function(a){var b,d,f,j,g;d=a.replace(".","/")+".js";j=runtime.libraryPaths();runtime.currentDirectory&&j.push(runtime.currentDirectory());for(g=0;!b&&g<j.length;g+=1){f=j[g];if(!e.hasOwnProperty(f))if((b=
-runtime.readFileSync(j[g]+"/manifest.js","utf8"))&&b.length)try{e[f]=eval(b)}catch(i){e[f]=null,runtime.log("Cannot load manifest for "+f+".")}else e[f]=null;b=null;if((f=e[f])&&f.indexOf&&f.indexOf(d)!==-1)try{b=runtime.readFileSync(j[g]+"/"+d,"utf8")}catch(l){throw runtime.log("Error loading "+a+" "+l),l;}}if(b===void 0)throw"Cannot load class "+a;try{b=eval(a+" = eval(code);")}catch(k){throw runtime.log("Error loading "+a+" "+k),k;}return b}(g),!b||Runtime.getFunctionName(b)!==a[a.length-1]))throw runtime.log("Loaded code is not for "+
-a[a.length-1]),"Loaded code is not for "+a[a.length-1];k[g]=true}}})();
-(function(i){function k(e){if(e.length){var g=e[0];runtime.readFile(g,"utf8",function(a,b){function h(){var a;(a=eval(b))&&runtime.exit(a)}var c="";runtime.libraryPaths();g.indexOf("/")!==-1&&(c=g.substring(0,g.indexOf("/")));runtime.setCurrentDirectory(c);a?(runtime.log(a),runtime.exit(1)):h.apply(null,e)})}}i=Array.prototype.slice.call(i);runtime.type()==="NodeJSRuntime"?k(process.argv.slice(2)):runtime.type()==="RhinoRuntime"?k(i):k(i.slice(1))})(typeof arguments!=="undefined"&&arguments);
+Runtime.prototype.isFile=function(){};Runtime.prototype.getFileSize=function(){};Runtime.prototype.deleteFile=function(){};Runtime.prototype.log=function(){};Runtime.prototype.setTimeout=function(){};Runtime.prototype.libraryPaths=function(){};Runtime.prototype.type=function(){};Runtime.prototype.getDOMImplementation=function(){};Runtime.prototype.getWindow=function(){};var IS_COMPILED_CODE=!0;
+Runtime.byteArrayToString=function(g,m){function e(e){var a="",c,b=e.length,d,o,f;for(c=0;c<b;c+=1)d=e[c],128>d?a+=String.fromCharCode(d):(c+=1,o=e[c],224>d?a+=String.fromCharCode((d&31)<<6|o&63):(c+=1,f=e[c],a+=String.fromCharCode((d&15)<<12|(o&63)<<6|f&63)));return a}if("utf8"===m)return e(g);"binary"!==m&&this.log("Unsupported encoding: "+m);return function(e){var a="",c,b=e.length;for(c=0;c<b;c+=1)a+=String.fromCharCode(e[c]&255);return a}(g)};
+Runtime.getFunctionName=function(g){return void 0===g.name?(g=/function\s+(\w+)/.exec(g))&&g[1]:g.name};
+function BrowserRuntime(g){function m(c,b){var d,a,f;b?f=c:b=c;g?(a=g.ownerDocument,f&&(d=a.createElement("span"),d.className=f,d.appendChild(a.createTextNode(f)),g.appendChild(d),g.appendChild(a.createTextNode(" "))),d=a.createElement("span"),d.appendChild(a.createTextNode(b)),g.appendChild(d),g.appendChild(a.createElement("br"))):console&&console.log(b)}var e=this,k={},a=window.ArrayBuffer&&window.Uint8Array;this.ByteArray=a?function(c){Uint8Array.prototype.slice=function(c,d){void 0===d&&(void 0===
+c&&(c=0),d=this.length);var a=this.subarray(c,d),f,h,d=d-c;f=new Uint8Array(new ArrayBuffer(d));for(h=0;h<d;h+=1)f[h]=a[h];return f};return new Uint8Array(new ArrayBuffer(c))}:function(c){var b=[];b.length=c;return b};this.concatByteArrays=a?function(c,b){var d,a=c.length,f=b.length,h=new this.ByteArray(a+f);for(d=0;d<a;d+=1)h[d]=c[d];for(d=0;d<f;d+=1)h[d+a]=b[d];return h}:function(c,b){return c.concat(b)};this.byteArrayFromArray=function(c){return c.slice()};this.byteArrayFromString=function(c,b){if("utf8"===
+b){var d=c.length,a,f,h,i=0;for(f=0;f<d;f+=1)h=c.charCodeAt(f),i+=1+(128<h)+(2048<h);a=new e.ByteArray(i);for(f=i=0;f<d;f+=1)h=c.charCodeAt(f),128>h?(a[i]=h,i+=1):2048>h?(a[i]=192|h>>>6,a[i+1]=128|h&63,i+=2):(a[i]=224|h>>>12&15,a[i+1]=128|h>>>6&63,a[i+2]=128|h&63,i+=3);return a}"binary"!==b&&e.log("unknown encoding: "+b);d=c.length;a=new e.ByteArray(d);for(f=0;f<d;f+=1)a[f]=c.charCodeAt(f)&255;return a};this.byteArrayToString=Runtime.byteArrayToString;this.readFile=function(c,b,d){if(k.hasOwnProperty(c))d(null,
+k[c]);else{var a=new XMLHttpRequest;a.open("GET",c,!0);a.onreadystatechange=function(){var f;4===a.readyState&&(0===a.status&&!a.responseText?d("File "+c+" is empty."):200===a.status||0===a.status?(f="binary"===b?"undefined"!==typeof VBArray?(new VBArray(a.responseBody)).toArray():e.byteArrayFromString(a.responseText,"binary"):a.responseText,k[c]=f,d(null,f)):d(a.responseText||a.statusText))};a.overrideMimeType&&("binary"!==b?a.overrideMimeType("text/plain; charset="+b):a.overrideMimeType("text/plain; charset=x-user-defined"));
+try{a.send(null)}catch(f){d(f.message)}}};this.read=function(c,a,d,o){if(k.hasOwnProperty(c))o(null,k[c].slice(a,a+d));else{var f=new XMLHttpRequest;f.open("GET",c,!0);f.onreadystatechange=function(){var i;4===f.readyState&&(0===f.status&&!f.responseText?o("File "+c+" is empty."):200===f.status||0===f.status?(i="undefined"!==typeof VBArray?(new VBArray(f.responseBody)).toArray():e.byteArrayFromString(f.responseText,"binary"),k[c]=i,o(null,i.slice(a,a+d))):o(f.responseText||f.statusText))};f.overrideMimeType&&
+f.overrideMimeType("text/plain; charset=x-user-defined");try{f.send(null)}catch(h){o(h.message)}}};this.readFileSync=function(c,a){var d=new XMLHttpRequest,o;d.open("GET",c,!1);d.overrideMimeType&&("binary"!==a?d.overrideMimeType("text/plain; charset="+a):d.overrideMimeType("text/plain; charset=x-user-defined"));try{if(d.send(null),200===d.status||0===d.status)o=d.responseText}catch(f){}return o};this.writeFile=function(c,a,d){k[c]=a;var o=new XMLHttpRequest;o.open("PUT",c,!0);o.onreadystatechange=
+function(){4===o.readyState&&(0===o.status&&!o.responseText?d("File "+c+" is empty."):200<=o.status&&300>o.status||0===o.status?d(null):d("Status "+o.status+": "+o.responseText||o.statusText))};a=a.buffer&&!o.sendAsBinary?a.buffer:e.byteArrayToString(a,"binary");try{o.sendAsBinary?o.sendAsBinary(a):o.send(a)}catch(f){e.log("HUH? "+f+" "+a),d(f.message)}};this.deleteFile=function(c,a){var d=new XMLHttpRequest;d.open("DELETE",c,!0);d.onreadystatechange=function(){4===d.readyState&&(200>d.status&&300<=
+d.status?a(d.responseText):a(null))};d.send(null)};this.loadXML=function(a,b){var d=new XMLHttpRequest;d.open("GET",a,!0);d.overrideMimeType&&d.overrideMimeType("text/xml");d.onreadystatechange=function(){4===d.readyState&&(0===d.status&&!d.responseText?b("File "+a+" is empty."):200===d.status||0===d.status?b(null,d.responseXML):b(d.responseText))};try{d.send(null)}catch(o){b(o.message)}};this.isFile=function(a,b){e.getFileSize(a,function(a){b(-1!==a)})};this.getFileSize=function(a,b){var d=new XMLHttpRequest;
+d.open("HEAD",a,!0);d.onreadystatechange=function(){if(4===d.readyState){var a=d.getResponseHeader("Content-Length");a?b(parseInt(a,10)):b(-1)}};d.send(null)};this.log=m;this.setTimeout=function(a,b){setTimeout(function(){a()},b)};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(){};this.type=function(){return"BrowserRuntime"};this.getDOMImplementation=function(){return window.document.implementation};this.exit=function(a){m("Calling exit with code "+a+", but exit() is not implemented.")};
+this.getWindow=function(){return window}}
+function NodeJSRuntime(){var g=require("fs"),m="";this.ByteArray=function(e){return new Buffer(e)};this.byteArrayFromArray=function(e){var k=new Buffer(e.length),a,c=e.length;for(a=0;a<c;a+=1)k[a]=e[a];return k};this.concatByteArrays=function(e,k){var a=new Buffer(e.length+k.length);e.copy(a,0,0);k.copy(a,e.length,0);return a};this.byteArrayFromString=function(e,k){return new Buffer(e,k)};this.byteArrayToString=function(e,k){return e.toString(k)};this.readFile=function(e,k,a){"binary"!==k?g.readFile(e,
+k,a):g.readFile(e,null,a)};this.writeFile=function(e,k,a){g.writeFile(e,k,"binary",function(c){a(c||null)})};this.deleteFile=g.unlink;this.read=function(e,k,a,c){m&&(e=m+"/"+e);g.open(e,"r+",666,function(b,d){if(b)c(b);else{var o=new Buffer(a);g.read(d,o,0,a,k,function(a){g.close(d);c(a,o)})}})};this.readFileSync=function(e,k){return!k?"":g.readFileSync(e,k)};this.loadXML=function(){throw"Not implemented.";};this.isFile=function(e,k){m&&(e=m+"/"+e);g.stat(e,function(a,c){k(!a&&c.isFile())})};this.getFileSize=
+function(e,k){m&&(e=m+"/"+e);g.stat(e,function(a,c){a?k(-1):k(c.size)})};this.log=function(e){process.stderr.write(e+"\n")};this.setTimeout=function(e,k){setTimeout(function(){e()},k)};this.libraryPaths=function(){return[__dirname]};this.setCurrentDirectory=function(e){m=e};this.currentDirectory=function(){return m};this.type=function(){return"NodeJSRuntime"};this.getDOMImplementation=function(){return null};this.exit=process.exit;this.getWindow=function(){return null}}
+function RhinoRuntime(){var g=this,m=Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(),e,k,a="";m.setValidating(!1);m.setNamespaceAware(!0);m.setExpandEntityReferences(!1);m.setSchema(null);k=Packages.org.xml.sax.EntityResolver({resolveEntity:function(a,b){var d=new Packages.java.io.FileReader(b);return new Packages.org.xml.sax.InputSource(d)}});e=m.newDocumentBuilder();e.setEntityResolver(k);this.ByteArray=function(a){return[a]};this.byteArrayFromArray=function(a){return a};this.byteArrayFromString=
+function(a){var b=[],d,o=a.length;for(d=0;d<o;d+=1)b[d]=a.charCodeAt(d)&255;return b};this.byteArrayToString=Runtime.byteArrayToString;this.concatByteArrays=function(a,b){return a.concat(b)};this.loadXML=function(a,b){var d=new Packages.java.io.File(a),o;try{o=e.parse(d)}catch(f){print(f);b(f);return}b(null,o)};this.readFile=function(a,b,d){var o=new Packages.java.io.File(a),f="binary"===b?"latin1":b;o.isFile()?(a=readFile(a,f),"binary"===b&&(a=g.byteArrayFromString(a,"binary")),d(null,a)):d(a+" is not a file.")};
+this.writeFile=function(a,b,d){var a=new Packages.java.io.FileOutputStream(a),o,f=b.length;for(o=0;o<f;o+=1)a.write(b[o]);a.close();d(null)};this.deleteFile=function(a,b){(new Packages.java.io.File(a))["delete"]()?b(null):b("Could not delete "+a)};this.read=function(c,b,d,o){a&&(c=a+"/"+c);var f;f=c;var h="binary";(new Packages.java.io.File(f)).isFile()?("binary"===h&&(h="latin1"),f=readFile(f,h)):f=null;f?o(null,this.byteArrayFromString(f.substring(b,b+d),"binary")):o("Cannot read "+c)};this.readFileSync=
+function(a,b){return!b?"":readFile(a,b)};this.isFile=function(c,b){a&&(c=a+"/"+c);var d=new Packages.java.io.File(c);b(d.isFile())};this.getFileSize=function(c,b){a&&(c=a+"/"+c);var d=new Packages.java.io.File(c);b(d.length())};this.log=print;this.setTimeout=function(a){a()};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(c){a=c};this.currentDirectory=function(){return a};this.type=function(){return"RhinoRuntime"};this.getDOMImplementation=function(){return e.getDOMImplementation()};
+this.exit=quit;this.getWindow=function(){return null}}var runtime=function(){return"undefined"!==typeof window?new BrowserRuntime(window.document.getElementById("logoutput")):"undefined"!==typeof require?new NodeJSRuntime:new RhinoRuntime}();
+(function(){function g(e){var a=e[0],c;c=eval("if (typeof "+a+" === 'undefined') {eval('"+a+" = {};');}"+a);for(a=1;a<e.length-1;a+=1)c.hasOwnProperty(e[a])||(c=c[e[a]]={});return c[e[e.length-1]]}var m={},e={};runtime.loadClass=function(k){function a(a){var a=a.replace(".","/")+".js",b=runtime.libraryPaths(),c,h,i;runtime.currentDirectory&&b.push(runtime.currentDirectory());for(c=0;c<b.length;c+=1){h=b[c];if(!e.hasOwnProperty(h))if((i=runtime.readFileSync(b[c]+"/manifest.js","utf8"))&&i.length)try{e[h]=
+eval(i)}catch(j){e[h]=null,runtime.log("Cannot load manifest for "+h+".")}else e[h]=null;if((h=e[h])&&h.indexOf&&-1!==h.indexOf(a))return b[c]+"/"+a}return null}if(!IS_COMPILED_CODE&&!m.hasOwnProperty(k)){var c=k.split("."),b;b=g(c);if(!b&&(b=function(c){var b,f;f=a(c);if(!f)throw c+" is not listed in any manifest.js.";try{b=runtime.readFileSync(f,"utf8")}catch(e){throw runtime.log("Error loading "+c+" "+e),e;}if(void 0===b)throw"Cannot load class "+c;try{b=eval(c+" = eval(code);")}catch(i){throw runtime.log("Error loading "+
+c+" "+i),i;}return b}(k),!b||Runtime.getFunctionName(b)!==c[c.length-1]))throw runtime.log("Loaded code is not for "+c[c.length-1]),"Loaded code is not for "+c[c.length-1];m[k]=!0}}})();
+(function(g){function m(e){if(e.length){var g=e[0];runtime.readFile(g,"utf8",function(a,c){function b(){var a;(a=eval(c))&&runtime.exit(a)}var d="";runtime.libraryPaths();-1!==g.indexOf("/")&&(d=g.substring(0,g.indexOf("/")));runtime.setCurrentDirectory(d);a?(runtime.log(a),runtime.exit(1)):b.apply(null,e)})}}g=Array.prototype.slice.call(g);"NodeJSRuntime"===runtime.type()?m(process.argv.slice(2)):"RhinoRuntime"===runtime.type()?m(g):m(g.slice(1))})("undefined"!==typeof arguments&&arguments);
 // Input 2
-core.Base64=function(){function i(a){var b=[],f,c=a.length;for(f=0;f<c;f+=1)b[f]=a.charCodeAt(f)&255;return b}function k(a){var b,f="",c,d=a.length-2;for(c=0;c<d;c+=3)b=a[c]<<16|a[c+1]<<8|a[c+2],f+=u[b>>>18],f+=u[b>>>12&63],f+=u[b>>>6&63],f+=u[b&63];c===d+1?(b=a[c]<<4,f+=u[b>>>6],f+=u[b&63],f+="=="):c===d&&(b=a[c]<<10|a[c+1]<<2,f+=u[b>>>12],f+=u[b>>>6&63],f+=u[b&63],f+="=");return f}function e(a){var a=a.replace(/[^A-Za-z0-9+\/]+/g,""),b=[],f=a.length%4,c,d=a.length,h;for(c=0;c<d;c+=4)h=(n[a.charAt(c)]||
-0)<<18|(n[a.charAt(c+1)]||0)<<12|(n[a.charAt(c+2)]||0)<<6|(n[a.charAt(c+3)]||0),b.push(h>>16,h>>8&255,h&255);b.length-=[0,0,2,1][f];return b}function g(a){var b=[],f,c=a.length,d;for(f=0;f<c;f+=1)d=a[f],d<128?b.push(d):d<2048?b.push(192|d>>>6,128|d&63):b.push(224|d>>>12&15,128|d>>>6&63,128|d&63);return b}function a(a){var b=[],f,c=a.length,d,h,j;for(f=0;f<c;f+=1)d=a[f],d<128?b.push(d):(f+=1,h=a[f],d<224?b.push((d&31)<<6|h&63):(f+=1,j=a[f],b.push((d&15)<<12|(h&63)<<6|j&63)));return b}function b(a){return k(i(a))}
-function h(a){return String.fromCharCode.apply(String,e(a))}function c(b){return a(i(b))}function d(b){return String.fromCharCode.apply(String,a(b))}function f(a,b,f){for(var c="",d,h,j;b<f;b+=1)d=a.charCodeAt(b)&255,d<128?c+=String.fromCharCode(d):(b+=1,h=a.charCodeAt(b)&255,d<224?c+=String.fromCharCode((d&31)<<6|h&63):(b+=1,j=a.charCodeAt(b)&255,c+=String.fromCharCode((d&15)<<12|(h&63)<<6|j&63)));return c}function j(a,b){function c(){var e=j+d;if(e>a.length)e=a.length;h+=f(a,j,e);j=e;e=j===a.length;
-b(h,e)&&!e&&runtime.setTimeout(c,0)}var d=1E5,h="",j=0;a.length<d?b(f(a,0,a.length),true):(typeof a!=="string"&&(a=a.slice()),c())}function p(a){return g(i(a))}function m(a){return String.fromCharCode.apply(String,g(a))}function l(a){return String.fromCharCode.apply(String,g(i(a)))}var u="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";(function(){var a=[],b,f="A".charCodeAt(0),c="a".charCodeAt(0),d="0".charCodeAt(0);for(b=0;b<26;b+=1)a.push(f+b);for(b=0;b<26;b+=1)a.push(c+b);for(b=
-0;b<10;b+=1)a.push(d+b);a.push("+".charCodeAt(0));a.push("/".charCodeAt(0));return a})();var n=function(a){var b={},f,c;for(f=0,c=a.length;f<c;f+=1)b[a.charAt(f)]=f;return b}(u),q,r,y,C;(y=runtime.getWindow()&&runtime.getWindow().btoa)?q=function(a){return y(l(a))}:(y=b,q=function(a){return k(p(a))});(C=runtime.getWindow()&&runtime.getWindow().atob)?r=function(a){a=C(a);return f(a,0,a.length)}:(C=h,r=function(a){return d(e(a))});return function(){this.convertByteArrayToBase64=this.convertUTF8ArrayToBase64=
-k;this.convertBase64ToByteArray=this.convertBase64ToUTF8Array=e;this.convertUTF16ArrayToByteArray=this.convertUTF16ArrayToUTF8Array=g;this.convertByteArrayToUTF16Array=this.convertUTF8ArrayToUTF16Array=a;this.convertUTF8StringToBase64=b;this.convertBase64ToUTF8String=h;this.convertUTF8StringToUTF16Array=c;this.convertByteArrayToUTF16String=this.convertUTF8ArrayToUTF16String=d;this.convertUTF8StringToUTF16String=j;this.convertUTF16StringToByteArray=this.convertUTF16StringToUTF8Array=p;this.convertUTF16ArrayToUTF8String=
-m;this.convertUTF16StringToUTF8String=l;this.convertUTF16StringToBase64=q;this.convertBase64ToUTF16String=r;this.fromBase64=h;this.toBase64=b;this.atob=C;this.btoa=y;this.utob=l;this.btou=j;this.encode=q;this.encodeURI=function(a){return q(a).replace(/[+\/]/g,function(a){return a==="+"?"-":"_"}).replace(/\\=+$/,"")};this.decode=function(a){return r(a.replace(/[\-_]/g,function(a){return a==="-"?"+":"/"}))}}}();
+core.Base64=function(){function g(a){var c=[],b,d=a.length;for(b=0;b<d;b+=1)c[b]=a.charCodeAt(b)&255;return c}function m(a){var b,c="",d,f=a.length-2;for(d=0;d<f;d+=3)b=a[d]<<16|a[d+1]<<8|a[d+2],c+=x[b>>>18],c+=x[b>>>12&63],c+=x[b>>>6&63],c+=x[b&63];d===f+1?(b=a[d]<<4,c+=x[b>>>6],c+=x[b&63],c+="=="):d===f&&(b=a[d]<<10|a[d+1]<<2,c+=x[b>>>12],c+=x[b>>>6&63],c+=x[b&63],c+="=");return c}function e(a){var a=a.replace(/[^A-Za-z0-9+\/]+/g,""),c=[],b=a.length%4,d,f=a.length,e;for(d=0;d<f;d+=4)e=(p[a.charAt(d)]||
+0)<<18|(p[a.charAt(d+1)]||0)<<12|(p[a.charAt(d+2)]||0)<<6|(p[a.charAt(d+3)]||0),c.push(e>>16,e>>8&255,e&255);c.length-=[0,0,2,1][b];return c}function k(a){var c=[],b,d=a.length,f;for(b=0;b<d;b+=1)f=a[b],128>f?c.push(f):2048>f?c.push(192|f>>>6,128|f&63):c.push(224|f>>>12&15,128|f>>>6&63,128|f&63);return c}function a(a){var c=[],b,d=a.length,f,e,l;for(b=0;b<d;b+=1)f=a[b],128>f?c.push(f):(b+=1,e=a[b],224>f?c.push((f&31)<<6|e&63):(b+=1,l=a[b],c.push((f&15)<<12|(e&63)<<6|l&63)));return c}function c(a){return m(g(a))}
+function b(a){return String.fromCharCode.apply(String,e(a))}function d(c){return a(g(c))}function o(c){for(var c=a(c),b="",d=0;d<c.length;)b+=String.fromCharCode.apply(String,c.slice(d,d+45E3)),d+=45E3;return b}function f(a,c,b){var d="",f,e,l;for(l=c;l<b;l+=1)c=a.charCodeAt(l)&255,128>c?d+=String.fromCharCode(c):(l+=1,f=a.charCodeAt(l)&255,224>c?d+=String.fromCharCode((c&31)<<6|f&63):(l+=1,e=a.charCodeAt(l)&255,d+=String.fromCharCode((c&15)<<12|(f&63)<<6|e&63)));return d}function h(a,c){function b(){var l=
+j+d;l>a.length&&(l=a.length);e+=f(a,j,l);j=l;l=j===a.length;c(e,l)&&!l&&runtime.setTimeout(b,0)}var d=1E5,e="",j=0;a.length<d?c(f(a,0,a.length),!0):("string"!==typeof a&&(a=a.slice()),b())}function i(a){return k(g(a))}function j(a){return String.fromCharCode.apply(String,k(a))}function n(a){return String.fromCharCode.apply(String,k(g(a)))}var x="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";(function(){var a=[],c;for(c=0;26>c;c+=1)a.push(65+c);for(c=0;26>c;c+=1)a.push(97+c);for(c=
+0;10>c;c+=1)a.push(48+c);a.push(43);a.push(47);return a})();var p=function(a){var c={},b,d;for(b=0,d=a.length;b<d;b+=1)c[a.charAt(b)]=b;return c}(x),t,r,z,s;(z=runtime.getWindow()&&runtime.getWindow().btoa)?t=function(a){return z(n(a))}:(z=c,t=function(a){return m(i(a))});(s=runtime.getWindow()&&runtime.getWindow().atob)?r=function(a){a=s(a);return f(a,0,a.length)}:(s=b,r=function(a){return o(e(a))});return function(){this.convertByteArrayToBase64=this.convertUTF8ArrayToBase64=m;this.convertBase64ToByteArray=
+this.convertBase64ToUTF8Array=e;this.convertUTF16ArrayToByteArray=this.convertUTF16ArrayToUTF8Array=k;this.convertByteArrayToUTF16Array=this.convertUTF8ArrayToUTF16Array=a;this.convertUTF8StringToBase64=c;this.convertBase64ToUTF8String=b;this.convertUTF8StringToUTF16Array=d;this.convertByteArrayToUTF16String=this.convertUTF8ArrayToUTF16String=o;this.convertUTF8StringToUTF16String=h;this.convertUTF16StringToByteArray=this.convertUTF16StringToUTF8Array=i;this.convertUTF16ArrayToUTF8String=j;this.convertUTF16StringToUTF8String=
+n;this.convertUTF16StringToBase64=t;this.convertBase64ToUTF16String=r;this.fromBase64=b;this.toBase64=c;this.atob=s;this.btoa=z;this.utob=n;this.btou=h;this.encode=t;this.encodeURI=function(a){return t(a).replace(/[+\/]/g,function(a){return"+"===a?"-":"_"}).replace(/\\=+$/,"")};this.decode=function(a){return r(a.replace(/[\-_]/g,function(a){return"-"===a?"+":"/"}))}}}();
 // Input 3
-core.RawDeflate=function(){function i(){this.dl=this.fc=0}function k(){this.extra_bits=this.static_tree=this.dyn_tree=null;this.max_code=this.max_length=this.elems=this.extra_base=0}function e(a,b,f,c){this.good_length=a;this.max_lazy=b;this.nice_length=f;this.max_chain=c}function g(){this.next=null;this.len=0;this.ptr=Array(a);this.off=0}var a=8192,b,h,c,d,f=null,j,p,m,l,u,n,q,r,y,C,s,v,E,B,z,G,o,x,t,w,L,Q,R,X,A,J,H,S,K,F,D,U,M,I,O,T,N,$,Y,oa,da,ea,V,fa,pa,aa,ga,Z,ha,ia,qa,ra=[0,0,0,0,0,0,0,0,1,
-1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],ba=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Ha=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],va=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],ja;ja=[new e(0,0,0,0),new e(4,4,8,4),new e(4,5,16,8),new e(4,6,32,32),new e(4,4,16,16),new e(8,16,32,32),new e(8,16,128,128),new e(8,32,128,256),new e(32,128,258,1024),new e(32,258,258,4096)];var ka=function(d){f[p+j++]=d;if(p+j==a&&j!=0){var e;b!=null?(d=b,b=b.next):d=new g;d.next=null;d.len=
-d.off=0;h==null?h=c=d:c=c.next=d;d.len=j-p;for(e=0;e<d.len;e++)d.ptr[e]=f[p+e];j=p=0}},la=function(b){b&=65535;p+j<a-2?(f[p+j++]=b&255,f[p+j++]=b>>>8):(ka(b&255),ka(b>>>8))},ma=function(){s=(s<<5^l[o+3-1]&255)&8191;v=q[32768+s];q[o&32767]=v;q[32768+s]=o},W=function(a,b){P(b[a].fc,b[a].dl)},wa=function(a,b,f){return a[b].fc<a[f].fc||a[b].fc==a[f].fc&&N[b]<=N[f]},xa=function(a,b,f){var c;for(c=0;c<f&&qa<ia.length;c++)a[b+c]=ia.charCodeAt(qa++)&255;return c},ya=function(a){var b=L,f=o,c,d=G,h=o>32506?
-o-32506:0,j=o+258,e=l[f+d-1],g=l[f+d];G>=X&&(b>>=2);do if(c=a,!(l[c+d]!=g||l[c+d-1]!=e||l[c]!=l[f]||l[++c]!=l[f+1])){f+=2;c++;do;while(l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&f<j);c=258-(j-f);f=j-258;if(c>d){x=a;d=c;if(c>=258)break;e=l[f+d-1];g=l[f+d]}}while((a=q[a&32767])>h&&--b!=0);return d},sa=function(){var a,b,f=65536-w-o;if(f==-1)f--;else if(o>=65274){for(a=0;a<32768;a++)l[a]=l[a+32768];x-=32768;o-=32768;
-C-=32768;for(a=0;a<8192;a++)b=q[32768+a],q[32768+a]=b>=32768?b-32768:0;for(a=0;a<32768;a++)b=q[a],q[a]=b>=32768?b-32768:0;f+=32768}t||(a=xa(l,o+w,f),a<=0?t=true:w+=a)},Ia=function(a,b,f){var c;if(!d){if(!t){y=r=0;var e,g;if(S[0].dl==0){F.dyn_tree=A;F.static_tree=H;F.extra_bits=ra;F.extra_base=257;F.elems=286;F.max_length=15;F.max_code=0;D.dyn_tree=J;D.static_tree=S;D.extra_bits=ba;D.extra_base=0;D.elems=30;D.max_length=15;D.max_code=0;U.dyn_tree=K;U.static_tree=null;U.extra_bits=Ha;U.extra_base=0;
-U.elems=19;U.max_length=7;for(g=e=U.max_code=0;g<28;g++){oa[g]=e;for(c=0;c<1<<ra[g];c++)$[e++]=g}$[e-1]=g;for(g=e=0;g<16;g++){da[g]=e;for(c=0;c<1<<ba[g];c++)Y[e++]=g}for(e>>=7;g<30;g++){da[g]=e<<7;for(c=0;c<1<<ba[g]-7;c++)Y[256+e++]=g}for(c=0;c<=15;c++)M[c]=0;for(c=0;c<=143;)H[c++].dl=8,M[8]++;for(;c<=255;)H[c++].dl=9,M[9]++;for(;c<=279;)H[c++].dl=7,M[7]++;for(;c<=287;)H[c++].dl=8,M[8]++;za(H,287);for(c=0;c<30;c++)S[c].dl=5,S[c].fc=Aa(c,5);Ba()}for(c=0;c<8192;c++)q[32768+c]=0;Q=ja[R].max_lazy;X=ja[R].good_length;
-L=ja[R].max_chain;C=o=0;w=xa(l,0,65536);if(w<=0)t=true,w=0;else{for(t=false;w<262&&!t;)sa();for(c=s=0;c<2;c++)s=(s<<5^l[c]&255)&8191}h=null;p=j=0;R<=3?(G=2,z=0):(z=2,B=0);m=false}d=true;if(w==0)return m=true,0}if((c=Ca(a,b,f))==f)return f;if(m)return c;if(R<=3)for(;w!=0&&h==null;){ma();v!=0&&o-v<=32506&&(z=ya(v),z>w&&(z=w));if(z>=3)if(g=ca(o-x,z-3),w-=z,z<=Q){z--;do o++,ma();while(--z!=0);o++}else o+=z,z=0,s=l[o]&255,s=(s<<5^l[o+1]&255)&8191;else g=ca(0,l[o]&255),w--,o++;g&&(na(0),C=o);for(;w<262&&
-!t;)sa()}else for(;w!=0&&h==null;){ma();G=z;E=x;z=2;v!=0&&G<Q&&o-v<=32506&&(z=ya(v),z>w&&(z=w),z==3&&o-x>4096&&z--);if(G>=3&&z<=G){g=ca(o-1-E,G-3);w-=G-1;G-=2;do o++,ma();while(--G!=0);B=0;z=2;o++;g&&(na(0),C=o)}else B!=0?ca(0,l[o-1]&255)&&(na(0),C=o):B=1,o++,w--;for(;w<262&&!t;)sa()}w==0&&(B!=0&&ca(0,l[o-1]&255),na(1),m=true);return c+Ca(a,c+b,f-c)},Ca=function(a,c,d){var e,g,q;for(e=0;h!=null&&e<d;){g=d-e;if(g>h.len)g=h.len;for(q=0;q<g;q++)a[c+e+q]=h.ptr[h.off+q];h.off+=g;h.len-=g;e+=g;if(h.len==
-0)g=h,h=h.next,g.next=b,b=g}if(e==d)return e;if(p<j){g=d-e;g>j-p&&(g=j-p);for(q=0;q<g;q++)a[c+e+q]=f[p+q];p+=g;e+=g;j==p&&(j=p=0)}return e},Ba=function(){var a;for(a=0;a<286;a++)A[a].fc=0;for(a=0;a<30;a++)J[a].fc=0;for(a=0;a<19;a++)K[a].fc=0;A[256].fc=1;aa=V=fa=pa=Z=ha=0;ga=1},ta=function(a,b){for(var c=I[b],f=b<<1;f<=O;){f<O&&wa(a,I[f+1],I[f])&&f++;if(wa(a,c,I[f]))break;I[b]=I[f];b=f;f<<=1}I[b]=c},za=function(a,b){var c=Array(16),f=0,d;for(d=1;d<=15;d++)f=f+M[d-1]<<1,c[d]=f;for(f=0;f<=b;f++)if(d=
-a[f].dl,d!=0)a[f].fc=Aa(c[d]++,d)},ua=function(a){var b=a.dyn_tree,c=a.static_tree,f=a.elems,d,e=-1,h=f;O=0;T=573;for(d=0;d<f;d++)b[d].fc!=0?(I[++O]=e=d,N[d]=0):b[d].dl=0;for(;O<2;)d=I[++O]=e<2?++e:0,b[d].fc=1,N[d]=0,Z--,c!=null&&(ha-=c[d].dl);a.max_code=e;for(d=O>>1;d>=1;d--)ta(b,d);do d=I[1],I[1]=I[O--],ta(b,1),c=I[1],I[--T]=d,I[--T]=c,b[h].fc=b[d].fc+b[c].fc,N[h]=N[d]>N[c]+1?N[d]:N[c]+1,b[d].dl=b[c].dl=h,I[1]=h++,ta(b,1);while(O>=2);I[--T]=I[1];h=a.dyn_tree;d=a.extra_bits;var f=a.extra_base,c=
-a.max_code,g=a.max_length,j=a.static_tree,q,t,o,r,i=0;for(t=0;t<=15;t++)M[t]=0;h[I[T]].dl=0;for(a=T+1;a<573;a++)if(q=I[a],t=h[h[q].dl].dl+1,t>g&&(t=g,i++),h[q].dl=t,!(q>c))M[t]++,o=0,q>=f&&(o=d[q-f]),r=h[q].fc,Z+=r*(t+o),j!=null&&(ha+=r*(j[q].dl+o));if(i!=0){do{for(t=g-1;M[t]==0;)t--;M[t]--;M[t+1]+=2;M[g]--;i-=2}while(i>0);for(t=g;t!=0;t--)for(q=M[t];q!=0;)if(d=I[--a],!(d>c)){if(h[d].dl!=t)Z+=(t-h[d].dl)*h[d].fc,h[d].fc=t;q--}}za(b,e)},Da=function(a,b){var c,f=-1,d,e=a[0].dl,h=0,g=7,j=4;e==0&&(g=
-138,j=3);a[b+1].dl=65535;for(c=0;c<=b;c++)d=e,e=a[c+1].dl,++h<g&&d==e||(h<j?K[d].fc+=h:d!=0?(d!=f&&K[d].fc++,K[16].fc++):h<=10?K[17].fc++:K[18].fc++,h=0,f=d,e==0?(g=138,j=3):d==e?(g=6,j=3):(g=7,j=4))},Ea=function(a,b){var c,f=-1,d,e=a[0].dl,h=0,g=7,j=4;e==0&&(g=138,j=3);for(c=0;c<=b;c++)if(d=e,e=a[c+1].dl,!(++h<g&&d==e)){if(h<j){do W(d,K);while(--h!=0)}else d!=0?(d!=f&&(W(d,K),h--),W(16,K),P(h-3,2)):h<=10?(W(17,K),P(h-3,3)):(W(18,K),P(h-11,7));h=0;f=d;e==0?(g=138,j=3):d==e?(g=6,j=3):(g=7,j=4)}},na=
-function(a){var b,c,f,d;d=o-C;ea[pa]=aa;ua(F);ua(D);Da(A,F.max_code);Da(J,D.max_code);ua(U);for(f=18;f>=3;f--)if(K[va[f]].dl!=0)break;Z+=3*(f+1)+14;b=Z+3+7>>3;c=ha+3+7>>3;c<=b&&(b=c);if(d+4<=b&&C>=0){P(0+a,3);Fa();la(d);la(~d);for(f=0;f<d;f++)ka(l[C+f])}else if(c==b)P(2+a,3),Ga(H,S);else{P(4+a,3);d=F.max_code+1;b=D.max_code+1;f+=1;P(d-257,5);P(b-1,5);P(f-4,4);for(c=0;c<f;c++)P(K[va[c]].dl,3);Ea(A,d-1);Ea(J,b-1);Ga(A,J)}Ba();a!=0&&Fa()},ca=function(a,b){n[V++]=b;a==0?A[b].fc++:(a--,A[$[b]+256+1].fc++,
-J[(a<256?Y[a]:Y[256+(a>>7)])&255].fc++,u[fa++]=a,aa|=ga);ga<<=1;(V&7)==0&&(ea[pa++]=aa,aa=0,ga=1);if(R>2&&(V&4095)==0){var c=V*8,f=o-C,d;for(d=0;d<30;d++)c+=J[d].fc*(5+ba[d]);c>>=3;if(fa<parseInt(V/2,10)&&c<parseInt(f/2,10))return true}return V==8191||fa==8192},Ga=function(a,b){var c,f=0,d=0,h=0,e=0,g,j;if(V!=0){do(f&7)==0&&(e=ea[h++]),c=n[f++]&255,(e&1)==0?W(c,a):(g=$[c],W(g+256+1,a),j=ra[g],j!=0&&(c-=oa[g],P(c,j)),c=u[d++],g=(c<256?Y[c]:Y[256+(c>>7)])&255,W(g,b),j=ba[g],j!=0&&(c-=da[g],P(c,j))),
-e>>=1;while(f<V)}W(256,a)},P=function(a,c){y>16-c?(r|=a<<y,la(r),r=a>>16-y,y+=c-16):(r|=a<<y,y+=c)},Aa=function(a,c){var b=0;do b|=a&1,a>>=1,b<<=1;while(--c>0);return b>>1},Fa=function(){y>8?la(r):y>0&&ka(r);y=r=0};this.deflate=function(e,g){var j,o;ia=e;qa=0;typeof g=="undefined"&&(g=6);(j=g)?j<1?j=1:j>9&&(j=9):j=6;R=j;t=d=false;if(f==null){b=h=c=null;f=Array(a);l=Array(65536);u=Array(8192);n=Array(32832);q=Array(65536);A=Array(573);for(j=0;j<573;j++)A[j]=new i;J=Array(61);for(j=0;j<61;j++)J[j]=
-new i;H=Array(288);for(j=0;j<288;j++)H[j]=new i;S=Array(30);for(j=0;j<30;j++)S[j]=new i;K=Array(39);for(j=0;j<39;j++)K[j]=new i;F=new k;D=new k;U=new k;M=Array(16);I=Array(573);N=Array(573);$=Array(256);Y=Array(512);oa=Array(29);da=Array(30);ea=Array(1024)}for(var r=Array(1024),w=[];(j=Ia(r,0,r.length))>0;){var x=Array(j);for(o=0;o<j;o++)x[o]=String.fromCharCode(r[o]);w[w.length]=x.join("")}ia=null;return w.join("")}};
+core.RawDeflate=function(){function g(){this.dl=this.fc=0}function m(){this.extra_bits=this.static_tree=this.dyn_tree=null;this.max_code=this.max_length=this.elems=this.extra_base=0}function e(a,c,b,d){this.good_length=a;this.max_lazy=c;this.nice_length=b;this.max_chain=d}function k(){this.next=null;this.len=0;this.ptr=[];this.ptr.length=a;this.off=0}var a=8192,c,b,d,o,f=null,h,i,j,n,x,p,t,r,z,s,q,u,E,C,w,G,l,v,y,B,M,F,R,S,A,K,I,P,L,H,D,U,N,J,V,aa,T,ba,Q,$,W,ea,X,ia,pa,ca,fa,Y,da,ja,qa,ra=[0,0,0,
+0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],ga=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Ha=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],va=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],ka;ka=[new e(0,0,0,0),new e(4,4,8,4),new e(4,5,16,8),new e(4,6,32,32),new e(4,4,16,16),new e(8,16,32,32),new e(8,16,128,128),new e(8,32,128,256),new e(32,128,258,1024),new e(32,258,258,4096)];var la=function(l){f[i+h++]=l;if(i+h===a){var e;if(0!==h){null!==c?(l=c,c=c.next):l=new k;
+l.next=null;l.len=l.off=0;null===b?b=d=l:d=d.next=l;l.len=h-i;for(e=0;e<l.len;e++)l.ptr[e]=f[i+e];h=i=0}}},ma=function(c){c&=65535;i+h<a-2?(f[i+h++]=c&255,f[i+h++]=c>>>8):(la(c&255),la(c>>>8))},na=function(){q=(q<<5^n[l+3-1]&255)&8191;u=t[32768+q];t[l&32767]=u;t[32768+q]=l},O=function(a,c){z>16-c?(r|=a<<z,ma(r),r=a>>16-z,z+=c-16):(r|=a<<z,z+=c)},Z=function(a,c){O(c[a].fc,c[a].dl)},wa=function(a,c,b){return a[c].fc<a[b].fc||a[c].fc===a[b].fc&&T[c]<=T[b]},xa=function(a,c,b){var d;for(d=0;d<b&&qa<ja.length;d++)a[c+
+d]=ja.charCodeAt(qa++)&255;return d},sa=function(){var a,c,b=65536-B-l;if(-1===b)b--;else if(65274<=l){for(a=0;32768>a;a++)n[a]=n[a+32768];v-=32768;l-=32768;s-=32768;for(a=0;8192>a;a++)c=t[32768+a],t[32768+a]=32768<=c?c-32768:0;for(a=0;32768>a;a++)c=t[a],t[a]=32768<=c?c-32768:0;b+=32768}y||(a=xa(n,l+B,b),0>=a?y=!0:B+=a)},ya=function(a){var c=M,b=l,d,f=G,e=32506<l?l-32506:0,j=l+258,i=n[b+f-1],q=n[b+f];G>=S&&(c>>=2);do if(d=a,!(n[d+f]!==q||n[d+f-1]!==i||n[d]!==n[b]||n[++d]!==n[b+1])){b+=2;d++;do++b;
+while(n[b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&b<j);d=258-(j-b);b=j-258;if(d>f){v=a;f=d;if(258<=d)break;i=n[b+f-1];q=n[b+f]}}while((a=t[a&32767])>e&&0!==--c);return f},ha=function(a,c){p[X++]=c;0===a?A[c].fc++:(a--,A[ba[c]+256+1].fc++,K[(256>a?Q[a]:Q[256+(a>>7)])&255].fc++,x[ia++]=a,ca|=fa);fa<<=1;0===(X&7)&&(ea[pa++]=ca,ca=0,fa=1);if(2<R&&0===(X&4095)){var b=8*X,d=l-s,f;for(f=0;30>f;f++)b+=K[f].fc*(5+ga[f]);
+b>>=3;if(ia<parseInt(X/2,10)&&b<parseInt(d/2,10))return!0}return 8191===X||8192===ia},ta=function(a,c){for(var b=J[c],d=c<<1;d<=V;){d<V&&wa(a,J[d+1],J[d])&&d++;if(wa(a,b,J[d]))break;J[c]=J[d];c=d;d<<=1}J[c]=b},za=function(a,c){var b=0;do b|=a&1,a>>=1,b<<=1;while(0<--c);return b>>1},Aa=function(a,c){var b=[];b.length=16;var d=0,f;for(f=1;15>=f;f++)d=d+N[f-1]<<1,b[f]=d;for(d=0;d<=c;d++)f=a[d].dl,0!==f&&(a[d].fc=za(b[f]++,f))},ua=function(a){var c=a.dyn_tree,b=a.static_tree,d=a.elems,f,l=-1,e=d;V=0;
+aa=573;for(f=0;f<d;f++)0!==c[f].fc?(J[++V]=l=f,T[f]=0):c[f].dl=0;for(;2>V;)f=J[++V]=2>l?++l:0,c[f].fc=1,T[f]=0,Y--,null!==b&&(da-=b[f].dl);a.max_code=l;for(f=V>>1;1<=f;f--)ta(c,f);do f=J[1],J[1]=J[V--],ta(c,1),b=J[1],J[--aa]=f,J[--aa]=b,c[e].fc=c[f].fc+c[b].fc,T[e]=T[f]>T[b]+1?T[f]:T[b]+1,c[f].dl=c[b].dl=e,J[1]=e++,ta(c,1);while(2<=V);J[--aa]=J[1];e=a.dyn_tree;f=a.extra_bits;var d=a.extra_base,b=a.max_code,j=a.max_length,i=a.static_tree,q,h,o,s,v=0;for(h=0;15>=h;h++)N[h]=0;e[J[aa]].dl=0;for(a=aa+
+1;573>a;a++)q=J[a],h=e[e[q].dl].dl+1,h>j&&(h=j,v++),e[q].dl=h,q>b||(N[h]++,o=0,q>=d&&(o=f[q-d]),s=e[q].fc,Y+=s*(h+o),null!==i&&(da+=s*(i[q].dl+o)));if(0!==v){do{for(h=j-1;0===N[h];)h--;N[h]--;N[h+1]+=2;N[j]--;v-=2}while(0<v);for(h=j;0!==h;h--)for(q=N[h];0!==q;)f=J[--a],f>b||(e[f].dl!==h&&(Y+=(h-e[f].dl)*e[f].fc,e[f].fc=h),q--)}Aa(c,l)},Ba=function(a,c){var b,d=-1,f,l=a[0].dl,e=0,h=7,j=4;0===l&&(h=138,j=3);a[c+1].dl=65535;for(b=0;b<=c;b++)f=l,l=a[b+1].dl,++e<h&&f===l||(e<j?L[f].fc+=e:0!==f?(f!==d&&
+L[f].fc++,L[16].fc++):10>=e?L[17].fc++:L[18].fc++,e=0,d=f,0===l?(h=138,j=3):f===l?(h=6,j=3):(h=7,j=4))},Ca=function(){8<z?ma(r):0<z&&la(r);z=r=0},Da=function(a,c){var b,d=0,f=0,l=0,e=0,h,j;if(0!==X){do 0===(d&7)&&(e=ea[l++]),b=p[d++]&255,0===(e&1)?Z(b,a):(h=ba[b],Z(h+256+1,a),j=ra[h],0!==j&&(b-=$[h],O(b,j)),b=x[f++],h=(256>b?Q[b]:Q[256+(b>>7)])&255,Z(h,c),j=ga[h],0!==j&&(b-=W[h],O(b,j))),e>>=1;while(d<X)}Z(256,a)},Ea=function(a,c){var b,d=-1,f,l=a[0].dl,e=0,h=7,j=4;0===l&&(h=138,j=3);for(b=0;b<=c;b++)if(f=
+l,l=a[b+1].dl,!(++e<h&&f===l)){if(e<j){do Z(f,L);while(0!==--e)}else 0!==f?(f!==d&&(Z(f,L),e--),Z(16,L),O(e-3,2)):10>=e?(Z(17,L),O(e-3,3)):(Z(18,L),O(e-11,7));e=0;d=f;0===l?(h=138,j=3):f===l?(h=6,j=3):(h=7,j=4)}},Fa=function(){var a;for(a=0;286>a;a++)A[a].fc=0;for(a=0;30>a;a++)K[a].fc=0;for(a=0;19>a;a++)L[a].fc=0;A[256].fc=1;ca=X=ia=pa=Y=da=0;fa=1},oa=function(a){var c,b,d,f;f=l-s;ea[pa]=ca;ua(H);ua(D);Ba(A,H.max_code);Ba(K,D.max_code);ua(U);for(d=18;3<=d&&!(0!==L[va[d]].dl);d--);Y+=3*(d+1)+14;c=
+Y+3+7>>3;b=da+3+7>>3;b<=c&&(c=b);if(f+4<=c&&0<=s){O(0+a,3);Ca();ma(f);ma(~f);for(d=0;d<f;d++)la(n[s+d])}else if(b===c)O(2+a,3),Da(I,P);else{O(4+a,3);f=H.max_code+1;c=D.max_code+1;d+=1;O(f-257,5);O(c-1,5);O(d-4,4);for(b=0;b<d;b++)O(L[va[b]].dl,3);Ea(A,f-1);Ea(K,c-1);Da(A,K)}Fa();0!==a&&Ca()},Ga=function(a,d,l){var e,j,q;for(e=0;null!==b&&e<l;){j=l-e;j>b.len&&(j=b.len);for(q=0;q<j;q++)a[d+e+q]=b.ptr[b.off+q];b.off+=j;b.len-=j;e+=j;0===b.len&&(j=b,b=b.next,j.next=c,c=j)}if(e===l)return e;if(i<h){j=l-
+e;j>h-i&&(j=h-i);for(q=0;q<j;q++)a[d+e+q]=f[i+q];i+=j;e+=j;h===i&&(h=i=0)}return e},Ia=function(a,c,d){var f;if(!o){if(!y){z=r=0;var e,g;if(0===P[0].dl){H.dyn_tree=A;H.static_tree=I;H.extra_bits=ra;H.extra_base=257;H.elems=286;H.max_length=15;H.max_code=0;D.dyn_tree=K;D.static_tree=P;D.extra_bits=ga;D.extra_base=0;D.elems=30;D.max_length=15;D.max_code=0;U.dyn_tree=L;U.static_tree=null;U.extra_bits=Ha;U.extra_base=0;U.elems=19;U.max_length=7;for(g=e=U.max_code=0;28>g;g++){$[g]=e;for(f=0;f<1<<ra[g];f++)ba[e++]=
+g}ba[e-1]=g;for(g=e=0;16>g;g++){W[g]=e;for(f=0;f<1<<ga[g];f++)Q[e++]=g}for(e>>=7;30>g;g++){W[g]=e<<7;for(f=0;f<1<<ga[g]-7;f++)Q[256+e++]=g}for(f=0;15>=f;f++)N[f]=0;for(f=0;143>=f;)I[f++].dl=8,N[8]++;for(;255>=f;)I[f++].dl=9,N[9]++;for(;279>=f;)I[f++].dl=7,N[7]++;for(;287>=f;)I[f++].dl=8,N[8]++;Aa(I,287);for(f=0;30>f;f++)P[f].dl=5,P[f].fc=za(f,5);Fa()}for(f=0;8192>f;f++)t[32768+f]=0;F=ka[R].max_lazy;S=ka[R].good_length;M=ka[R].max_chain;s=l=0;B=xa(n,0,65536);if(0>=B)y=!0,B=0;else{for(y=!1;262>B&&!y;)sa();
+for(f=q=0;2>f;f++)q=(q<<5^n[f]&255)&8191}b=null;i=h=0;3>=R?(G=2,w=0):(w=2,C=0);j=!1}o=!0;if(0===B)return j=!0,0}if((f=Ga(a,c,d))===d)return d;if(j)return f;if(3>=R)for(;0!==B&&null===b;){na();0!==u&&32506>=l-u&&(w=ya(u),w>B&&(w=B));if(3<=w)if(g=ha(l-v,w-3),B-=w,w<=F){w--;do l++,na();while(0!==--w);l++}else l+=w,w=0,q=n[l]&255,q=(q<<5^n[l+1]&255)&8191;else g=ha(0,n[l]&255),B--,l++;g&&(oa(0),s=l);for(;262>B&&!y;)sa()}else for(;0!==B&&null===b;){na();G=w;E=v;w=2;0!==u&&G<F&&32506>=l-u&&(w=ya(u),w>B&&
+(w=B),3===w&&4096<l-v&&w--);if(3<=G&&w<=G){g=ha(l-1-E,G-3);B-=G-1;G-=2;do l++,na();while(0!==--G);C=0;w=2;l++;g&&(oa(0),s=l)}else 0!==C?ha(0,n[l-1]&255)&&(oa(0),s=l):C=1,l++,B--;for(;262>B&&!y;)sa()}0===B&&(0!==C&&ha(0,n[l-1]&255),oa(1),j=!0);return f+Ga(a,f+c,d-f)};this.deflate=function(e,l){var j,h;ja=e;qa=0;"undefined"===typeof l&&(l=6);(j=l)?1>j?j=1:9<j&&(j=9):j=6;R=j;y=o=!1;if(null===f){c=b=d=null;f=[];f.length=a;n=[];n.length=65536;x=[];x.length=8192;p=[];p.length=32832;t=[];t.length=65536;
+A=[];A.length=573;for(j=0;573>j;j++)A[j]=new g;K=[];K.length=61;for(j=0;61>j;j++)K[j]=new g;I=[];I.length=288;for(j=0;288>j;j++)I[j]=new g;P=[];P.length=30;for(j=0;30>j;j++)P[j]=new g;L=[];L.length=39;for(j=0;39>j;j++)L[j]=new g;H=new m;D=new m;U=new m;N=[];N.length=16;J=[];J.length=573;T=[];T.length=573;ba=[];ba.length=256;Q=[];Q.length=512;$=[];$.length=29;W=[];W.length=30;ea=[];ea.length=1024}for(var q=Array(1024),i=[];0<(j=Ia(q,0,q.length));){var s=[];s.length=j;for(h=0;h<j;h++)s[h]=String.fromCharCode(q[h]);
+i[i.length]=s.join("")}ja=null;return i.join("")}};
 // Input 4
-core.ByteArray=function(i){this.pos=0;this.data=i;this.readUInt32LE=function(){var i=this.data,e=this.pos+=4;return i[--e]<<24|i[--e]<<16|i[--e]<<8|i[--e]};this.readUInt16LE=function(){var i=this.data,e=this.pos+=2;return i[--e]<<8|i[--e]}};
+core.ByteArray=function(g){this.pos=0;this.data=g;this.readUInt32LE=function(){var g=this.data,e=this.pos+=4;return g[--e]<<24|g[--e]<<16|g[--e]<<8|g[--e]};this.readUInt16LE=function(){var g=this.data,e=this.pos+=2;return g[--e]<<8|g[--e]}};
 // Input 5
-core.ByteArrayWriter=function(i){var k=this,e=new runtime.ByteArray(0);this.appendByteArrayWriter=function(g){e=runtime.concatByteArrays(e,g.getByteArray())};this.appendByteArray=function(g){e=runtime.concatByteArrays(e,g)};this.appendArray=function(g){e=runtime.concatByteArrays(e,runtime.byteArrayFromArray(g))};this.appendUInt16LE=function(e){k.appendArray([e&255,e>>8&255])};this.appendUInt32LE=function(e){k.appendArray([e&255,e>>8&255,e>>16&255,e>>24&255])};this.appendString=function(g){e=runtime.concatByteArrays(e,
-runtime.byteArrayFromString(g,i))};this.getLength=function(){return e.length};this.getByteArray=function(){return e}};
+core.ByteArrayWriter=function(g){var m=this,e=new runtime.ByteArray(0);this.appendByteArrayWriter=function(g){e=runtime.concatByteArrays(e,g.getByteArray())};this.appendByteArray=function(g){e=runtime.concatByteArrays(e,g)};this.appendArray=function(g){e=runtime.concatByteArrays(e,runtime.byteArrayFromArray(g))};this.appendUInt16LE=function(e){m.appendArray([e&255,e>>8&255])};this.appendUInt32LE=function(e){m.appendArray([e&255,e>>8&255,e>>16&255,e>>24&255])};this.appendString=function(k){e=runtime.concatByteArrays(e,
+runtime.byteArrayFromString(k,g))};this.getLength=function(){return e.length};this.getByteArray=function(){return e}};
 // Input 6
-core.RawInflate=function(){var i,k,e=null,g,a,b,h,c,d,f,j,p,m,l,u,n,q,r=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],y=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],C=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,99,99],s=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],v=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],E=[16,17,18,
-0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],B=function(){this.list=this.next=null},z=function(){this.n=this.b=this.e=0;this.t=null},G=function(a,c,b,f,d,j){this.BMAX=16;this.N_MAX=288;this.status=0;this.root=null;this.m=0;var e=Array(this.BMAX+1),h,g,q,t,o,r,i,w=Array(this.BMAX+1),x,m,n,p=new z,l=Array(this.BMAX);t=Array(this.N_MAX);var v,y=Array(this.BMAX+1),k,s,C;C=this.root=null;for(o=0;o<e.length;o++)e[o]=0;for(o=0;o<w.length;o++)w[o]=0;for(o=0;o<l.length;o++)l[o]=null;for(o=0;o<t.length;o++)t[o]=
-0;for(o=0;o<y.length;o++)y[o]=0;h=c>256?a[256]:this.BMAX;x=a;m=0;o=c;do e[x[m]]++,m++;while(--o>0);if(e[0]==c)this.root=null,this.status=this.m=0;else{for(r=1;r<=this.BMAX;r++)if(e[r]!=0)break;i=r;j<r&&(j=r);for(o=this.BMAX;o!=0;o--)if(e[o]!=0)break;q=o;j>o&&(j=o);for(k=1<<r;r<o;r++,k<<=1)if((k-=e[r])<0){this.status=2;this.m=j;return}if((k-=e[o])<0)this.status=2,this.m=j;else{e[o]+=k;y[1]=r=0;x=e;m=1;for(n=2;--o>0;)y[n++]=r+=x[m++];x=a;o=m=0;do if((r=x[m++])!=0)t[y[r]++]=o;while(++o<c);c=y[q];y[0]=
-o=0;x=t;m=0;t=-1;v=w[0]=0;n=null;for(s=0;i<=q;i++)for(a=e[i];a-- >0;){for(;i>v+w[1+t];){v+=w[1+t];t++;s=(s=q-v)>j?j:s;if((g=1<<(r=i-v))>a+1){g-=a+1;for(n=i;++r<s;){if((g<<=1)<=e[++n])break;g-=e[n]}}v+r>h&&v<h&&(r=h-v);s=1<<r;w[1+t]=r;n=Array(s);for(g=0;g<s;g++)n[g]=new z;C=C==null?this.root=new B:C.next=new B;C.next=null;C.list=n;l[t]=n;if(t>0)y[t]=o,p.b=w[t],p.e=16+r,p.t=n,r=(o&(1<<v)-1)>>v-w[t],l[t-1][r].e=p.e,l[t-1][r].b=p.b,l[t-1][r].n=p.n,l[t-1][r].t=p.t}p.b=i-v;m>=c?p.e=99:x[m]<b?(p.e=x[m]<
-256?16:15,p.n=x[m++]):(p.e=d[x[m]-b],p.n=f[x[m++]-b]);g=1<<i-v;for(r=o>>v;r<s;r+=g)n[r].e=p.e,n[r].b=p.b,n[r].n=p.n,n[r].t=p.t;for(r=1<<i-1;(o&r)!=0;r>>=1)o^=r;for(o^=r;(o&(1<<v)-1)!=y[t];)v-=w[t],t--}this.m=w[1];this.status=k!=0&&q!=1?1:0}}},o=function(a){for(;h<a;)b|=(n.length==q?-1:n[q++])<<h,h+=8},x=function(a){return b&r[a]},t=function(a){b>>=a;h-=a},w=function(a,b,d){var e,h,g;if(d==0)return 0;for(g=0;;){o(l);h=p.list[x(l)];for(e=h.e;e>16;){if(e==99)return-1;t(h.b);e-=16;o(e);h=h.t[x(e)];e=
-h.e}t(h.b);if(e==16)k&=32767,a[b+g++]=i[k++]=h.n;else{if(e==15)break;o(e);f=h.n+x(e);t(e);o(u);h=m.list[x(u)];for(e=h.e;e>16;){if(e==99)return-1;t(h.b);e-=16;o(e);h=h.t[x(e)];e=h.e}t(h.b);o(e);j=k-h.n-x(e);for(t(e);f>0&&g<d;)f--,j&=32767,k&=32767,a[b+g++]=i[k++]=i[j++]}if(g==d)return d}c=-1;return g},L,Q=function(a,c,b){var f,d,e,h,j,g,q,r=Array(316);for(f=0;f<r.length;f++)r[f]=0;o(5);g=257+x(5);t(5);o(5);q=1+x(5);t(5);o(4);f=4+x(4);t(4);if(g>286||q>30)return-1;for(d=0;d<f;d++)o(3),r[E[d]]=x(3),t(3);
-for(;d<19;d++)r[E[d]]=0;l=7;d=new G(r,19,19,null,null,l);if(d.status!=0)return-1;p=d.root;l=d.m;h=g+q;for(f=e=0;f<h;)if(o(l),j=p.list[x(l)],d=j.b,t(d),d=j.n,d<16)r[f++]=e=d;else if(d==16){o(2);d=3+x(2);t(2);if(f+d>h)return-1;for(;d-- >0;)r[f++]=e}else{d==17?(o(3),d=3+x(3),t(3)):(o(7),d=11+x(7),t(7));if(f+d>h)return-1;for(;d-- >0;)r[f++]=0;e=0}l=9;d=new G(r,g,257,y,C,l);if(l==0)d.status=1;if(d.status!=0)return-1;p=d.root;l=d.m;for(f=0;f<q;f++)r[f]=r[f+g];u=6;d=new G(r,q,0,s,v,u);m=d.root;u=d.m;return u==
-0&&g>257?-1:d.status!=0?-1:w(a,c,b)};this.inflate=function(r,z){i==null&&(i=Array(65536));h=b=k=0;c=-1;d=false;f=j=0;p=null;n=r;q=0;var B=new runtime.ByteArray(z);a:{var E,H;for(E=0;E<z;){if(d&&c==-1)break;if(f>0){if(c!=0)for(;f>0&&E<z;)f--,j&=32767,k&=32767,B[0+E++]=i[k++]=i[j++];else{for(;f>0&&E<z;)f--,k&=32767,o(8),B[0+E++]=i[k++]=x(8),t(8);f==0&&(c=-1)}if(E==z)break}if(c==-1){if(d)break;o(1);x(1)!=0&&(d=true);t(1);o(2);c=x(2);t(2);p=null;f=0}switch(c){case 0:H=B;var S=0+E,K=z-E,F=void 0,F=h&7;
-t(F);o(16);F=x(16);t(16);o(16);if(F!=(~b&65535))H=-1;else{t(16);f=F;for(F=0;f>0&&F<K;)f--,k&=32767,o(8),H[S+F++]=i[k++]=x(8),t(8);f==0&&(c=-1);H=F}break;case 1:if(p!=null)H=w(B,0+E,z-E);else b:{H=B;S=0+E;K=z-E;if(e==null){for(var D=void 0,F=Array(288),D=void 0,D=0;D<144;D++)F[D]=8;for(;D<256;D++)F[D]=9;for(;D<280;D++)F[D]=7;for(;D<288;D++)F[D]=8;a=7;D=new G(F,288,257,y,C,a);if(D.status!=0){alert("HufBuild error: "+D.status);H=-1;break b}e=D.root;a=D.m;for(D=0;D<30;D++)F[D]=5;L=5;D=new G(F,30,0,s,
-v,L);if(D.status>1){e=null;alert("HufBuild error: "+D.status);H=-1;break b}g=D.root;L=D.m}p=e;m=g;l=a;u=L;H=w(H,S,K)}break;case 2:H=p!=null?w(B,0+E,z-E):Q(B,0+E,z-E);break;default:H=-1}if(H==-1)break a;E+=H}}n=null;return B}};
+core.RawInflate=function(){var g,m,e=null,k,a,c,b,d,o,f,h,i,j,n,x,p,t,r=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],z=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],s=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,99,99],q=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],u=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],E=[16,17,18,
+0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],C=function(){this.list=this.next=null},w=function(){this.n=this.b=this.e=0;this.t=null},G=function(a,c,b,f,d,e){this.BMAX=16;this.N_MAX=288;this.status=0;this.root=null;this.m=0;var j=Array(this.BMAX+1),l,h,q,i,g,s,o,v=Array(this.BMAX+1),n,y,u,t=new w,k=Array(this.BMAX);i=Array(this.N_MAX);var p,z=Array(this.BMAX+1),B,r,x;x=this.root=null;for(g=0;g<j.length;g++)j[g]=0;for(g=0;g<v.length;g++)v[g]=0;for(g=0;g<k.length;g++)k[g]=null;for(g=0;g<i.length;g++)i[g]=
+0;for(g=0;g<z.length;g++)z[g]=0;l=256<c?a[256]:this.BMAX;n=a;y=0;g=c;do j[n[y]]++,y++;while(0<--g);if(j[0]==c)this.root=null,this.status=this.m=0;else{for(s=1;s<=this.BMAX&&!(0!=j[s]);s++);o=s;e<s&&(e=s);for(g=this.BMAX;0!=g&&!(0!=j[g]);g--);q=g;e>g&&(e=g);for(B=1<<s;s<g;s++,B<<=1)if(0>(B-=j[s])){this.status=2;this.m=e;return}if(0>(B-=j[g]))this.status=2,this.m=e;else{j[g]+=B;z[1]=s=0;n=j;y=1;for(u=2;0<--g;)z[u++]=s+=n[y++];n=a;g=y=0;do if(0!=(s=n[y++]))i[z[s]++]=g;while(++g<c);c=z[q];z[0]=g=0;n=
+i;y=0;i=-1;p=v[0]=0;u=null;for(r=0;o<=q;o++)for(a=j[o];0<a--;){for(;o>p+v[1+i];){p+=v[1+i];i++;r=(r=q-p)>e?e:r;if((h=1<<(s=o-p))>a+1){h-=a+1;for(u=o;++s<r&&!((h<<=1)<=j[++u]);)h-=j[u]}p+s>l&&p<l&&(s=l-p);r=1<<s;v[1+i]=s;u=Array(r);for(h=0;h<r;h++)u[h]=new w;x=null==x?this.root=new C:x.next=new C;x.next=null;x.list=u;k[i]=u;0<i&&(z[i]=g,t.b=v[i],t.e=16+s,t.t=u,s=(g&(1<<p)-1)>>p-v[i],k[i-1][s].e=t.e,k[i-1][s].b=t.b,k[i-1][s].n=t.n,k[i-1][s].t=t.t)}t.b=o-p;y>=c?t.e=99:n[y]<b?(t.e=256>n[y]?16:15,t.n=
+n[y++]):(t.e=d[n[y]-b],t.n=f[n[y++]-b]);h=1<<o-p;for(s=g>>p;s<r;s+=h)u[s].e=t.e,u[s].b=t.b,u[s].n=t.n,u[s].t=t.t;for(s=1<<o-1;0!=(g&s);s>>=1)g^=s;for(g^=s;(g&(1<<p)-1)!=z[i];)p-=v[i],i--}this.m=v[1];this.status=0!=B&&1!=q?1:0}}},l=function(a){for(;b<a;)c|=(p.length==t?-1:p[t++])<<b,b+=8},v=function(a){return c&r[a]},y=function(a){c>>=a;b-=a},B=function(a,c,b){var e,s,q;if(0==b)return 0;for(q=0;;){l(n);s=i.list[v(n)];for(e=s.e;16<e;){if(99==e)return-1;y(s.b);e-=16;l(e);s=s.t[v(e)];e=s.e}y(s.b);if(16==
+e)m&=32767,a[c+q++]=g[m++]=s.n;else{if(15==e)break;l(e);f=s.n+v(e);y(e);l(x);s=j.list[v(x)];for(e=s.e;16<e;){if(99==e)return-1;y(s.b);e-=16;l(e);s=s.t[v(e)];e=s.e}y(s.b);l(e);h=m-s.n-v(e);for(y(e);0<f&&q<b;)f--,h&=32767,m&=32767,a[c+q++]=g[m++]=g[h++]}if(q==b)return b}d=-1;return q},M,F=function(a,c,b){var f,d,e,h,g,o,t,p=Array(316);for(f=0;f<p.length;f++)p[f]=0;l(5);o=257+v(5);y(5);l(5);t=1+v(5);y(5);l(4);f=4+v(4);y(4);if(286<o||30<t)return-1;for(d=0;d<f;d++)l(3),p[E[d]]=v(3),y(3);for(;19>d;d++)p[E[d]]=
+0;n=7;d=new G(p,19,19,null,null,n);if(0!=d.status)return-1;i=d.root;n=d.m;h=o+t;for(f=e=0;f<h;)if(l(n),g=i.list[v(n)],d=g.b,y(d),d=g.n,16>d)p[f++]=e=d;else if(16==d){l(2);d=3+v(2);y(2);if(f+d>h)return-1;for(;0<d--;)p[f++]=e}else{17==d?(l(3),d=3+v(3),y(3)):(l(7),d=11+v(7),y(7));if(f+d>h)return-1;for(;0<d--;)p[f++]=0;e=0}n=9;d=new G(p,o,257,z,s,n);0==n&&(d.status=1);if(0!=d.status)return-1;i=d.root;n=d.m;for(f=0;f<t;f++)p[f]=p[f+o];x=6;d=new G(p,t,0,q,u,x);j=d.root;x=d.m;return 0==x&&257<o||0!=d.status?
+-1:B(a,c,b)};this.inflate=function(r,w){null==g&&(g=Array(65536));b=c=m=0;d=-1;o=!1;f=h=0;i=null;p=r;t=0;var C=new runtime.ByteArray(w);a:{var E,I;for(E=0;E<w&&!(o&&-1==d);){if(0<f){if(0!=d)for(;0<f&&E<w;)f--,h&=32767,m&=32767,C[0+E++]=g[m++]=g[h++];else{for(;0<f&&E<w;)f--,m&=32767,l(8),C[0+E++]=g[m++]=v(8),y(8);0==f&&(d=-1)}if(E==w)break}if(-1==d){if(o)break;l(1);0!=v(1)&&(o=!0);y(1);l(2);d=v(2);y(2);i=null;f=0}switch(d){case 0:I=C;var P=0+E,L=w-E,H=void 0,H=b&7;y(H);l(16);H=v(16);y(16);l(16);if(H!=
+(~c&65535))I=-1;else{y(16);f=H;for(H=0;0<f&&H<L;)f--,m&=32767,l(8),I[P+H++]=g[m++]=v(8),y(8);0==f&&(d=-1);I=H}break;case 1:if(null!=i)I=B(C,0+E,w-E);else b:{I=C;P=0+E;L=w-E;if(null==e){for(var D=void 0,H=Array(288),D=void 0,D=0;144>D;D++)H[D]=8;for(;256>D;D++)H[D]=9;for(;280>D;D++)H[D]=7;for(;288>D;D++)H[D]=8;a=7;D=new G(H,288,257,z,s,a);if(0!=D.status){alert("HufBuild error: "+D.status);I=-1;break b}e=D.root;a=D.m;for(D=0;30>D;D++)H[D]=5;M=5;D=new G(H,30,0,q,u,M);if(1<D.status){e=null;alert("HufBuild error: "+
+D.status);I=-1;break b}k=D.root;M=D.m}i=e;j=k;n=a;x=M;I=B(I,P,L)}break;case 2:I=null!=i?B(C,0+E,w-E):F(C,0+E,w-E);break;default:I=-1}if(-1==I)break a;E+=I}}p=null;return C}};
 // Input 7
-core.Cursor=function(i,k){function e(a,e){for(var c=e;c&&c!==a;)c=c.parentNode;return c||e}function g(){var b,h,c;if(a.parentNode){h=0;for(b=a.parentNode.firstChild;b&&b!==a;)h+=1,b=b.nextSibling;if(a.previousSibling&&a.previousSibling.nodeType===3&&a.nextSibling&&a.nextSibling.nodeType===3)c=a.nextSibling,a.previousSibling.appendData(c.nodeValue);for(b=0;b<i.rangeCount;b+=1){var d=i.getRangeAt(b),f=h,j=void 0,g=void 0,j=a.parentNode,g=e(a,d.startContainer);e(a,d.endContainer);g===a?d.setStart(j,
-f):g===j&&d.startOffset>f&&d.setStart(j,d.startOffset-1);d.endContainer===a?d.setEnd(j,f):d.endContainer===j&&d.endOffset>f&&d.setEnd(j,d.endOffset-1)}if(c){for(b=0;b<i.rangeCount;b+=1){var d=i.getRangeAt(b),f=a.previousSibling,j=c,g=h,m=f.length-j.length;d.startContainer===j?d.setStart(f,m+d.startOffset):d.startContainer===f.parentNode&&d.startOffset===g&&d.setStart(f,m);d.endContainer===j?d.setEnd(f,m+d.endOffset):d.endContainer===f.parentNode&&d.endOffset===g&&d.setEnd(f,m)}c.parentNode.removeChild(c)}a.parentNode.removeChild(a)}}
-var a;a=k.createElementNS("urn:webodf:names:cursor","cursor");this.getNode=function(){return a};this.updateToSelection=function(){g();if(i.focusNode){var b=i.focusNode,e=i.focusOffset;if(b.nodeType===3){var c,d,f,j;j=b.parentNode;e===0?j.insertBefore(a,b):e===b.length?j.appendChild(a):(c=b.length,d=b.nextSibling,f=k.createTextNode(b.substringData(e,c)),b.deleteData(e,c),d?j.insertBefore(f,d):j.appendChild(f),j.insertBefore(a,f))}else if(b.nodeType!==9){for(c=b.firstChild;c&&e;)c=c.nextSibling,e-=
-1;b.insertBefore(a,c)}}};this.remove=function(){g()}};
+core.Cursor=function(g,m){function e(a,b){for(var d=b;d&&d!==a;)d=d.parentNode;return d||b}function k(){var c,b,d;if(a.parentNode){b=0;for(c=a.parentNode.firstChild;c&&c!==a;)b+=1,c=c.nextSibling;a.previousSibling&&3===a.previousSibling.nodeType&&a.nextSibling&&3===a.nextSibling.nodeType&&(d=a.nextSibling,a.previousSibling.appendData(d.nodeValue));for(c=0;c<g.rangeCount;c+=1){var o=g.getRangeAt(c),f=b,h=void 0,i=void 0,h=a.parentNode,i=e(a,o.startContainer);e(a,o.endContainer);i===a?o.setStart(h,
+f):i===h&&o.startOffset>f&&o.setStart(h,o.startOffset-1);o.endContainer===a?o.setEnd(h,f):o.endContainer===h&&o.endOffset>f&&o.setEnd(h,o.endOffset-1)}if(d){for(c=0;c<g.rangeCount;c+=1){var o=g.getRangeAt(c),f=a.previousSibling,h=d,i=b,j=f.length-h.length;o.startContainer===h?o.setStart(f,j+o.startOffset):o.startContainer===f.parentNode&&o.startOffset===i&&o.setStart(f,j);o.endContainer===h?o.setEnd(f,j+o.endOffset):o.endContainer===f.parentNode&&o.endOffset===i&&o.setEnd(f,j)}d.parentNode.removeChild(d)}a.parentNode.removeChild(a)}}
+var a;a=m.createElementNS("urn:webodf:names:cursor","cursor");this.getNode=function(){return a};this.updateToSelection=function(){k();if(g.focusNode){var c=g.focusNode,b=g.focusOffset;if(3===c.nodeType){var d,e,f,h;h=c.parentNode;0===b?h.insertBefore(a,c):b===c.length?h.appendChild(a):(d=c.length,e=c.nextSibling,f=m.createTextNode(c.substringData(b,d)),c.deleteData(b,d),e?h.insertBefore(f,e):h.appendChild(f),h.insertBefore(a,f))}else if(9!==c.nodeType){for(d=c.firstChild;d&&b;)d=d.nextSibling,b-=
+1;c.insertBefore(a,d)}}};this.remove=function(){k()}};
 // Input 8
 core.UnitTest=function(){};core.UnitTest.prototype.setUp=function(){};core.UnitTest.prototype.tearDown=function(){};core.UnitTest.prototype.description=function(){};core.UnitTest.prototype.tests=function(){};core.UnitTest.prototype.asyncTests=function(){};
-core.UnitTestRunner=function(){function i(a){g+=1;runtime.log("fail",a)}function k(a,b){var e;try{if(a.length!==b.length)return false;for(e=0;e<a.length;e+=1)if(a[e]!==b[e])return false}catch(c){return false}return true}function e(a,b,e){(typeof b!=="string"||typeof e!=="string")&&runtime.log("WARN: shouldBe() expects string arguments");var c,d;try{d=eval(b)}catch(f){c=f}a=eval(e);c?i(b+" should be "+a+". Threw exception "+c):(a===0?d===a&&1/d===1/a:d===a||(typeof a==="number"&&isNaN(a)?typeof d===
-"number"&&isNaN(d):Object.prototype.toString.call(a)===Object.prototype.toString.call([])&&k(d,a)))?runtime.log("pass",b+" is "+e):typeof d===typeof a?i(b+" should be "+a+". Was "+(d===0&&1/d<0?"-0":String(d))+"."):i(b+" should be "+a+" (of type "+typeof a+"). Was "+d+" (of type "+typeof d+").")}var g=0;this.shouldBeNull=function(a,b){e(a,b,"null")};this.shouldBeNonNull=function(a,b){var e,c;try{c=eval(b)}catch(d){e=d}e?i(b+" should be non-null. Threw exception "+e):c!==null?runtime.log("pass",b+
-" is non-null."):i(b+" should be non-null. Was "+c)};this.shouldBe=e;this.countFailedTests=function(){return g}};
-core.UnitTester=function(){var i=0,k={};this.runTests=function(e,g){function a(e){if(e.length===0)k[b]=f,i+=c.countFailedTests(),g();else{p=e[0];var j=Runtime.getFunctionName(p);runtime.log("Running "+j);l=c.countFailedTests();d.setUp();p(function(){d.tearDown();f[j]=l===c.countFailedTests();a(e.slice(1))})}}var b=Runtime.getFunctionName(e),h,c=new core.UnitTestRunner,d=new e(c),f={},j,p,m,l;if(b.hasOwnProperty(k))runtime.log("Test "+b+" has already run.");else{runtime.log("Running "+b+": "+d.description());
-m=d.tests();for(j=0;j<m.length;j+=1)p=m[j],h=Runtime.getFunctionName(p),runtime.log("Running "+h),l=c.countFailedTests(),d.setUp(),p(),d.tearDown(),f[h]=l===c.countFailedTests();a(d.asyncTests())}};this.countFailedTests=function(){return i};this.results=function(){return k}};
+core.UnitTestRunner=function(){function g(a){k+=1;runtime.log("fail",a)}function m(a,c){var b;try{if(a.length!==c.length)return!1;for(b=0;b<a.length;b+=1)if(a[b]!==c[b])return!1}catch(d){return!1}return!0}function e(a,c,b){("string"!==typeof c||"string"!==typeof b)&&runtime.log("WARN: shouldBe() expects string arguments");var d,e;try{e=eval(c)}catch(f){d=f}a=eval(b);d?g(c+" should be "+a+". Threw exception "+d):(0===a?e===a&&1/e===1/a:e===a||("number"===typeof a&&isNaN(a)?"number"===typeof e&&isNaN(e):
+Object.prototype.toString.call(a)===Object.prototype.toString.call([])&&m(e,a)))?runtime.log("pass",c+" is "+b):typeof e===typeof a?g(c+" should be "+a+". Was "+(0===e&&0>1/e?"-0":""+e)+"."):g(c+" should be "+a+" (of type "+typeof a+"). Was "+e+" (of type "+typeof e+").")}var k=0;this.shouldBeNull=function(a,c){e(a,c,"null")};this.shouldBeNonNull=function(a,c){var b,d;try{d=eval(c)}catch(e){b=e}b?g(c+" should be non-null. Threw exception "+b):null!==d?runtime.log("pass",c+" is non-null."):g(c+" should be non-null. Was "+
+d)};this.shouldBe=e;this.countFailedTests=function(){return k}};
+core.UnitTester=function(){var g=0,m={};this.runTests=function(e,k){function a(b){if(0===b.length)m[c]=f,g+=d.countFailedTests(),k();else{i=b[0];var e=Runtime.getFunctionName(i);runtime.log("Running "+e);n=d.countFailedTests();o.setUp();i(function(){o.tearDown();f[e]=n===d.countFailedTests();a(b.slice(1))})}}var c=Runtime.getFunctionName(e),b,d=new core.UnitTestRunner,o=new e(d),f={},h,i,j,n;if(c.hasOwnProperty(m))runtime.log("Test "+c+" has already run.");else{runtime.log("Running "+c+": "+o.description());
+j=o.tests();for(h=0;h<j.length;h+=1)i=j[h],b=Runtime.getFunctionName(i),runtime.log("Running "+b),n=d.countFailedTests(),o.setUp(),i(),o.tearDown(),f[b]=n===d.countFailedTests();a(o.asyncTests())}};this.countFailedTests=function(){return g};this.results=function(){return m}};
 // Input 9
-core.PointWalker=function(i){function k(a){for(var c=-1;a;)a=a.previousSibling,c+=1;return c}var e=i,g=null,a=i&&i.firstChild,b=0;this.setPoint=function(h,c){e=h;b=c;if(e.nodeType===3)g=a=null;else{for(a=e.firstChild;c;)c-=1,a=a.nextSibling;g=a?a.previousSibling:e.lastChild}};this.stepForward=function(){var h;if(e.nodeType===3&&(h=typeof e.nodeValue.length==="number"?e.nodeValue.length:e.nodeValue.length(),b<h))return b+=1,true;if(a)return a.nodeType===1?(e=a,g=null,a=e.firstChild,b=0):a.nodeType===
-3?(e=a,a=g=null,b=0):(g=a,a=a.nextSibling,b+=1),true;return e!==i?(g=e,a=g.nextSibling,e=e.parentNode,b=k(g)+1,true):false};this.stepBackward=function(){if(e.nodeType===3&&b>0)return b-=1,true;if(g)return g.nodeType===1?(e=g,g=e.lastChild,a=null,b=k(g)+1):g.nodeType===3?(e=g,a=g=null,b=typeof e.nodeValue.length==="number"?e.nodeValue.length:e.nodeValue.length()):(a=g,g=g.previousSibling,b-=1),true;return e!==i?(a=e,g=a.previousSibling,e=e.parentNode,b=k(a),true):false};this.node=function(){return e};
-this.position=function(){return b};this.precedingSibling=function(){return g};this.followingSibling=function(){return a}};
+core.PointWalker=function(g){function m(a){for(var c=-1;a;)a=a.previousSibling,c+=1;return c}var e=g,k=null,a=g&&g.firstChild,c=0;this.setPoint=function(b,d){e=b;c=d;if(3===e.nodeType)k=a=null;else{for(a=e.firstChild;d;)d-=1,a=a.nextSibling;k=a?a.previousSibling:e.lastChild}};this.stepForward=function(){var b;if(3===e.nodeType&&(b="number"===typeof e.nodeValue.length?e.nodeValue.length:e.nodeValue.length(),c<b))return c+=1,!0;if(a)return 1===a.nodeType?(e=a,k=null,a=e.firstChild,c=0):3===a.nodeType?
+(e=a,a=k=null,c=0):(k=a,a=a.nextSibling,c+=1),!0;return e!==g?(k=e,a=k.nextSibling,e=e.parentNode,c=m(k)+1,!0):!1};this.stepBackward=function(){if(3===e.nodeType&&0<c)return c-=1,!0;if(k)return 1===k.nodeType?(e=k,k=e.lastChild,a=null,c=m(k)+1):3===k.nodeType?(e=k,a=k=null,c="number"===typeof e.nodeValue.length?e.nodeValue.length:e.nodeValue.length()):(a=k,k=k.previousSibling,c-=1),!0;return e!==g?(a=e,k=a.previousSibling,e=e.parentNode,c=m(a),!0):!1};this.node=function(){return e};this.position=
+function(){return c};this.precedingSibling=function(){return k};this.followingSibling=function(){return a}};
 // Input 10
-core.Async=function(){this.forEach=function(i,k,e){function g(a){h!==b&&(a?(h=b,e(a)):(h+=1,h===b&&e(null)))}var a,b=i.length,h=0;for(a=0;a<b;a+=1)k(i[a],g)}};
+core.Async=function(){this.forEach=function(g,m,e){function k(a){b!==c&&(a?(b=c,e(a)):(b+=1,b===c&&e(null)))}var a,c=g.length,b=0;for(a=0;a<c;a+=1)m(g[a],k)}};
 // Input 11
-runtime.loadClass("core.RawInflate");runtime.loadClass("core.ByteArray");runtime.loadClass("core.ByteArrayWriter");
-core.Zip=function(i,k){function e(a){var c=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,
+runtime.loadClass("core.RawInflate");runtime.loadClass("core.ByteArray");runtime.loadClass("core.ByteArrayWriter");runtime.loadClass("core.Base64");
+core.Zip=function(g,m){function e(a){var c=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,
 853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,
 4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,
 225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,
 2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,
-2932959818,3654703836,1088359270,936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],b=0,f,d=a.length,e=0,e=0;b^=-1;for(f=0;f<d;f+=1)e=(b^a[f])&255,e=c[e],b=b>>>8^e;return b^-1}function g(a){return new Date((a>>25&127)+1980,(a>>21&15)-1,a>>16&31,a>>11&15,a>>5&63,(a&31)<<1)}function a(a){var c=a.getFullYear();return c<1980?0:c-
-1980<<25|a.getMonth()+1<<21|a.getDate()<<16|a.getHours()<<11|a.getMinutes()<<5|a.getSeconds()>>1}function b(a,c){var b,f,d,e,j,h,i,m=this;this.load=function(c){if(m.data!==void 0)c(null,m.data);else{var d=j+34+b+f+256;d+i>p&&(d=p-i);runtime.read(a,i,d,function(b,f){if(b)c(b,f);else a:{var d=f,g=new core.ByteArray(d),o=g.readUInt32LE(),r;if(o!==67324752)c("File entry signature is wrong."+o.toString()+" "+d.length.toString(),null);else{g.pos+=22;o=g.readUInt16LE();r=g.readUInt16LE();g.pos+=o+r;if(e){d=
-d.slice(g.pos,g.pos+j);if(j!==d.length){c("The amount of compressed bytes read was "+d.length.toString()+" instead of "+j.toString()+" for "+m.filename+" in "+a+".",null);break a}d=l(d,h)}else d=d.slice(g.pos,g.pos+h);h!==d.length?c("The amount of bytes read was "+d.length.toString()+" instead of "+h.toString()+" for "+m.filename+" in "+a+".",null):(m.data=d,c(null,d))}}})}};this.set=function(a,c,b,f){m.filename=a;m.data=c;m.compressed=b;m.date=f};this.error=null;if(c)c.readUInt32LE()!==33639248?
-this.error="Central directory entry has wrong signature at position "+(c.pos-4).toString()+' for file "'+a+'": '+c.data.length.toString():(c.pos+=6,e=c.readUInt16LE(),this.date=g(c.readUInt32LE()),c.readUInt32LE(),j=c.readUInt32LE(),h=c.readUInt32LE(),b=c.readUInt16LE(),f=c.readUInt16LE(),d=c.readUInt16LE(),c.pos+=8,i=c.readUInt32LE(),this.filename=runtime.byteArrayToString(c.data.slice(c.pos,c.pos+b),"utf8"),c.pos+=b+f+d)}function h(a,c){if(a.length!==22)c("Central directory length should be 22.",
-u);else{var f=new core.ByteArray(a),d;d=f.readUInt32LE();d!==101010256?c("Central directory signature is wrong: "+d.toString(),u):f.readUInt16LE()!==0?c("Zip files with non-zero disk numbers are not supported.",u):f.readUInt16LE()!==0?c("Zip files with non-zero disk numbers are not supported.",u):(d=f.readUInt16LE(),m=f.readUInt16LE(),d!==m?c("Number of entries is inconsistent.",u):(d=f.readUInt32LE(),f=f.readUInt16LE(),f=p-22-d,runtime.read(i,f,p-f,function(a,f){a:{var d=new core.ByteArray(f),e,
-g;j=[];for(e=0;e<m;e+=1){g=new b(i,d);if(g.error){c(g.error,u);break a}j[j.length]=g}c(null,u)}})))}}function c(c){var b=new core.ByteArrayWriter("utf8"),f=0;b.appendArray([80,75,3,4,20,0,0,0,0,0]);if(c.data)f=c.data.length;b.appendUInt32LE(a(c.date));b.appendUInt32LE(e(c.data));b.appendUInt32LE(f);b.appendUInt32LE(f);b.appendUInt16LE(c.filename.length);b.appendUInt16LE(0);b.appendString(c.filename);c.data&&b.appendByteArray(c.data);return b}function d(c,b){var f=new core.ByteArrayWriter("utf8"),
-d=0;f.appendArray([80,75,1,2,20,0,20,0,0,0,0,0]);if(c.data)d=c.data.length;f.appendUInt32LE(a(c.date));f.appendUInt32LE(e(c.data));f.appendUInt32LE(d);f.appendUInt32LE(d);f.appendUInt16LE(c.filename.length);f.appendArray([0,0,0,0,0,0,0,0,0,0,0,0]);f.appendUInt32LE(b);f.appendString(c.filename);return f}function f(a,c){if(a===j.length)c(null);else{var b=j[a];b.data!==void 0?f(a+1,c):b.load(function(b){b?c(b):f(a+1,c)})}}var j,p,m,l=(new core.RawInflate).inflate,u=this;this.load=function(a,c){var b=
-null,f,d;for(d=0;d<j.length;d+=1)if(f=j[d],f.filename===a){b=f;break}b?b.data?c(null,b.data):b.load(c):c(a+" not found.",null)};this.save=function(a,c,f,d){var e,g;for(e=0;e<j.length;e+=1)if(g=j[e],g.filename===a){g.set(a,c,f,d);return}g=new b(i);g.set(a,c,f,d);j.push(g)};this.write=function(a){f(0,function(b){if(b)a(b);else{var b=new core.ByteArrayWriter("utf8"),f,e,g,h=[0];for(f=0;f<j.length;f+=1)b.appendByteArrayWriter(c(j[f])),h.push(b.getLength());g=b.getLength();for(f=0;f<j.length;f+=1)e=j[f],
-b.appendByteArrayWriter(d(e,h[f]));f=b.getLength()-g;b.appendArray([80,75,5,6,0,0,0,0]);b.appendUInt16LE(j.length);b.appendUInt16LE(j.length);b.appendUInt32LE(f);b.appendUInt32LE(g);b.appendArray([0,0]);runtime.writeFile(i,b.getByteArray(),a)}})};this.getEntries=function(){return j.slice()};p=-1;k===null?j=[]:runtime.getFileSize(i,function(a){p=a;p<0?k("File '"+i+"' cannot be read.",u):runtime.read(i,p-22,22,function(a,c){a||k===null?k(a,u):h(c,k)})})};
+2932959818,3654703836,1088359270,936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],b,f,d=a.length,e=0,e=0;b=-1;for(f=0;f<d;f+=1)e=(b^a[f])&255,e=c[e],b=b>>>8^e;return b^-1}function k(a){return new Date((a>>25&127)+1980,(a>>21&15)-1,a>>16&31,a>>11&15,a>>5&63,(a&31)<<1)}function a(a){var c=a.getFullYear();return 1980>c?0:c-1980<<
+25|a.getMonth()+1<<21|a.getDate()<<16|a.getHours()<<11|a.getMinutes()<<5|a.getSeconds()>>1}function c(a,c){var b,f,d,e,j,h,l,g=this;this.load=function(c){if(void 0!==g.data)c(null,g.data);else{var d=j+34+b+f+256;d+l>n&&(d=n-l);runtime.read(a,l,d,function(b,f){if(b)c(b,f);else a:{var d=f,l=new core.ByteArray(d),i=l.readUInt32LE(),q;if(67324752!==i)c("File entry signature is wrong."+i.toString()+" "+d.length.toString(),null);else{l.pos+=22;i=l.readUInt16LE();q=l.readUInt16LE();l.pos+=i+q;if(e){d=d.slice(l.pos,
+l.pos+j);if(j!==d.length){c("The amount of compressed bytes read was "+d.length.toString()+" instead of "+j.toString()+" for "+g.filename+" in "+a+".",null);break a}d=p(d,h)}else d=d.slice(l.pos,l.pos+h);h!==d.length?c("The amount of bytes read was "+d.length.toString()+" instead of "+h.toString()+" for "+g.filename+" in "+a+".",null):(g.data=d,c(null,d))}}})}};this.set=function(a,c,b,d){g.filename=a;g.data=c;g.compressed=b;g.date=d};this.error=null;c&&(33639248!==c.readUInt32LE()?this.error="Central directory entry has wrong signature at position "+
+(c.pos-4).toString()+' for file "'+a+'": '+c.data.length.toString():(c.pos+=6,e=c.readUInt16LE(),this.date=k(c.readUInt32LE()),c.readUInt32LE(),j=c.readUInt32LE(),h=c.readUInt32LE(),b=c.readUInt16LE(),f=c.readUInt16LE(),d=c.readUInt16LE(),c.pos+=8,l=c.readUInt32LE(),this.filename=runtime.byteArrayToString(c.data.slice(c.pos,c.pos+b),"utf8"),c.pos+=b+f+d))}function b(a,b){if(22!==a.length)b("Central directory length should be 22.",t);else{var d=new core.ByteArray(a),f;f=d.readUInt32LE();101010256!==
+f?b("Central directory signature is wrong: "+f.toString(),t):0!==d.readUInt16LE()?b("Zip files with non-zero disk numbers are not supported.",t):0!==d.readUInt16LE()?b("Zip files with non-zero disk numbers are not supported.",t):(f=d.readUInt16LE(),x=d.readUInt16LE(),f!==x?b("Number of entries is inconsistent.",t):(f=d.readUInt32LE(),d=d.readUInt16LE(),d=n-22-f,runtime.read(g,d,n-d,function(a,d){a:{var f=new core.ByteArray(d),e,l;j=[];for(e=0;e<x;e+=1){l=new c(g,f);if(l.error){b(l.error,t);break a}j[j.length]=
+l}b(null,t)}})))}}function d(a,c){var b=null,d,f;for(f=0;f<j.length;f+=1)if(d=j[f],d.filename===a){b=d;break}b?b.data?c(null,b.data):b.load(c):c(a+" not found.",null)}function o(a,c){d(a,function(a,b){if(a)return c(a,null);b=runtime.byteArrayToString(b,"utf8");c(null,b)})}function f(c){var b=new core.ByteArrayWriter("utf8"),d=0;b.appendArray([80,75,3,4,20,0,0,0,0,0]);c.data&&(d=c.data.length);b.appendUInt32LE(a(c.date));b.appendUInt32LE(e(c.data));b.appendUInt32LE(d);b.appendUInt32LE(d);b.appendUInt16LE(c.filename.length);
+b.appendUInt16LE(0);b.appendString(c.filename);c.data&&b.appendByteArray(c.data);return b}function h(c,b){var d=new core.ByteArrayWriter("utf8"),f=0;d.appendArray([80,75,1,2,20,0,20,0,0,0,0,0]);c.data&&(f=c.data.length);d.appendUInt32LE(a(c.date));d.appendUInt32LE(e(c.data));d.appendUInt32LE(f);d.appendUInt32LE(f);d.appendUInt16LE(c.filename.length);d.appendArray([0,0,0,0,0,0,0,0,0,0,0,0]);d.appendUInt32LE(b);d.appendString(c.filename);return d}function i(a,c){if(a===j.length)c(null);else{var b=j[a];
+void 0!==b.data?i(a+1,c):b.load(function(b){b?c(b):i(a+1,c)})}}var j,n,x,p=(new core.RawInflate).inflate,t=this,r=new core.Base64;this.load=d;this.save=function(a,b,d,f){var e,h;for(e=0;e<j.length;e+=1)if(h=j[e],h.filename===a){h.set(a,b,d,f);return}h=new c(g);h.set(a,b,d,f);j.push(h)};this.write=function(a){i(0,function(c){if(c)a(c);else{var c=new core.ByteArrayWriter("utf8"),b,d,e,i=[0];for(b=0;b<j.length;b+=1)c.appendByteArrayWriter(f(j[b])),i.push(c.getLength());e=c.getLength();for(b=0;b<j.length;b+=
+1)d=j[b],c.appendByteArrayWriter(h(d,i[b]));b=c.getLength()-e;c.appendArray([80,75,5,6,0,0,0,0]);c.appendUInt16LE(j.length);c.appendUInt16LE(j.length);c.appendUInt32LE(b);c.appendUInt32LE(e);c.appendArray([0,0]);runtime.writeFile(g,c.getByteArray(),a)}})};this.loadContentXmlAsFragments=function(a,c){o(a,function(a,b){if(a)return c.rootElementReady(a);c.rootElementReady(null,b,!0)})};this.loadAsString=o;this.loadAsDOM=function(a,c){o(a,function(a,b){a?c(a,null):(b=(new DOMParser).parseFromString(b,
+"text/xml"),c(null,b))})};this.loadAsDataURL=function(a,c,b){d(a,function(a,d){if(a)return b(a,null);var f=0,e;c||(c=80===d[1]&&78===d[2]&&71===d[3]?"image/png":255===d[0]&&216===d[1]&&255===d[2]?"image/jpeg":71===d[0]&&73===d[1]&&70===d[2]?"image/gif":"");for(e="data:"+c+";base64,";f<d.length;)e+=r.convertUTF8ArrayToBase64(d.slice(f,Math.min(f+45E3,d.length))),f+=45E3;b(null,e)})};this.getEntries=function(){return j.slice()};n=-1;null===m?j=[]:runtime.getFileSize(g,function(a){n=a;0>n?m("File '"+
+g+"' cannot be read.",t):runtime.read(g,n-22,22,function(a,c){a||null===m?m(a,t):b(c,m)})})};
 // Input 12
 xmldom.LSSerializerFilter=function(){};
 // Input 13
-typeof Object.create!=="function"&&(Object.create=function(i){var k=function(){};k.prototype=i;return new k});
-xmldom.LSSerializer=function(){function i(e,g){var a="",b=Object.create(e),h=k.filter?k.filter.acceptNode(g):1,c;if(h===1){c="";var d=g.attributes,f,j,p,m="",l;if(d){if(b[g.namespaceURI]!==g.prefix)b[g.namespaceURI]=g.prefix;c+="<"+g.nodeName;f=d.length;for(j=0;j<f;j+=1)if(p=d.item(j),p.namespaceURI!=="http://www.w3.org/2000/xmlns/"&&(l=k.filter?k.filter.acceptNode(p):1,l===1)){if(p.namespaceURI){l=p.prefix;var u=p.namespaceURI;b.hasOwnProperty(u)?l=b[u]+":":(b[u]!==l&&(b[u]=l),l+=":")}else l="";
-m+=" "+(l+p.localName+'="'+p.nodeValue+'"')}for(j in b)b.hasOwnProperty(j)&&((l=b[j])?l!=="xmlns"&&(c+=" xmlns:"+b[j]+'="'+j+'"'):c+=' xmlns="'+j+'"');c+=m+">"}a+=c}if(h===1||h===3){for(c=g.firstChild;c;)a+=i(b,c),c=c.nextSibling;g.nodeValue&&(a+=g.nodeValue)}h===1&&(b="",g.nodeType===1&&(b+="</"+g.nodeName+">"),a+=b);return a}var k=this;this.filter=null;this.writeToString=function(e,g){if(!e)return"";var a;if(g){a=g;var b={},h;for(h in a)a.hasOwnProperty(h)&&(b[a[h]]=h);a=b}else a={};return i(a,
+"function"!==typeof Object.create&&(Object.create=function(g){var m=function(){};m.prototype=g;return new m});
+xmldom.LSSerializer=function(){function g(e,k){var a="",c=Object.create(e),b=m.filter?m.filter.acceptNode(k):1,d;if(1===b){d="";var o=k.attributes,f,h,i,j="",n;if(o){c[k.namespaceURI]!==k.prefix&&(c[k.namespaceURI]=k.prefix);d+="<"+k.nodeName;f=o.length;for(h=0;h<f;h+=1)if(i=o.item(h),"http://www.w3.org/2000/xmlns/"!==i.namespaceURI&&(n=m.filter?m.filter.acceptNode(i):1,1===n)){if(i.namespaceURI){n=i.prefix;var x=i.namespaceURI;c.hasOwnProperty(x)?n=c[x]+":":(c[x]!==n&&(c[x]=n),n+=":")}else n="";
+j+=" "+(n+i.localName+'="'+i.nodeValue+'"')}for(h in c)c.hasOwnProperty(h)&&((n=c[h])?"xmlns"!==n&&(d+=" xmlns:"+c[h]+'="'+h+'"'):d+=' xmlns="'+h+'"');d+=j+">"}a+=d}if(1===b||3===b){for(d=k.firstChild;d;)a+=g(c,d),d=d.nextSibling;k.nodeValue&&(a+=k.nodeValue)}1===b&&(c="",1===k.nodeType&&(c+="</"+k.nodeName+">"),a+=c);return a}var m=this;this.filter=null;this.writeToString=function(e,k){if(!e)return"";var a;if(k){a=k;var c={},b;for(b in a)a.hasOwnProperty(b)&&(c[a[b]]=b);a=c}else a={};return g(a,
 e)}};
 // Input 14
-xmldom.RelaxNGParser=function(){function i(a,c){this.message=function(){c&&(a+=c.nodeType===1?" Element ":" Node ",a+=c.nodeName,c.nodeValue&&(a+=" with value '"+c.nodeValue+"'"),a+=".");return a}}function k(a){if(a.e.length<=2)return a;var c={name:a.name,e:a.e.slice(0,2)};return k({name:a.name,e:[c].concat(a.e.slice(2))})}function e(a){var a=a.split(":",2),b="",d;a.length===1?a=["",a[0]]:b=a[0];for(d in c)c[d]===b&&(a[0]=d);return a}function g(a,c){var j;var f;for(var b=0,d,h,i=a.name;a.e&&b<a.e.length;)if(d=
-a.e[b],d.name==="ref"){h=c[d.a.name];if(!h)throw d.a.name+" was not defined.";d=a.e.slice(b+1);a.e=a.e.slice(0,b);a.e=a.e.concat(h.e);a.e=a.e.concat(d)}else b+=1,g(d,c);d=a.e;if(i==="choice"&&(!d||!d[1]||d[1].name==="empty"))!d||!d[0]||d[0].name==="empty"?(delete a.e,a.name="empty"):(d[1]=d[0],d[0]={name:"empty"});if(i==="group"||i==="interleave")if(d[0].name==="empty")d[1].name==="empty"?(delete a.e,a.name="empty"):(i=a.name=d[1].name,a.names=d[1].names,f=a.e=d[1].e,d=f);else if(d[1].name==="empty")i=
-a.name=d[0].name,a.names=d[0].names,j=a.e=d[0].e,d=j;if(i==="oneOrMore"&&d[0].name==="empty")delete a.e,a.name="empty";if(i==="attribute"){h=a.names?a.names.length:0;for(var k,q=a.localnames=[h],r=a.namespaces=[h],b=0;b<h;b+=1)k=e(a.names[b]),r[b]=k[0],q[b]=k[1]}if(i==="interleave")if(d[0].name==="interleave")d[1].name==="interleave"?a.e=d[0].e.concat(d[1].e):a.e=[d[1]].concat(d[0].e);else if(d[1].name==="interleave")a.e=[d[0]].concat(d[1].e)}function a(c,b){for(var d=0,e;c.e&&d<c.e.length;)e=c.e[d],
-e.name==="elementref"?(e.id=e.id||0,c.e[d]=b[e.id]):e.name!=="element"&&a(e,b),d+=1}var b=this,h,c={"http://www.w3.org/XML/1998/namespace":"xml"},d;d=function(a,b,g){var h=[],i,u,n=a.localName,q=[];i=a.attributes;var r=n,y=q,C={},s,v;for(s=0;s<i.length;s+=1)if(v=i.item(s),v.namespaceURI){if(v.namespaceURI==="http://www.w3.org/2000/xmlns/")c[v.value]=v.localName}else{v.localName==="name"&&(r==="element"||r==="attribute")&&y.push(v.value);if(v.localName==="name"||v.localName==="combine"||v.localName===
-"type"){var E=v,B;B=v.value;B=B.replace(/^\s\s*/,"");for(var z=/\s/,G=B.length-1;z.test(B.charAt(G));)G-=1;B=B.slice(0,G+1);E.value=B}C[v.localName]=v.value}i=C;i.combine=i.combine||void 0;a=a.firstChild;r=h;y=q;for(C="";a;){if(a.nodeType===1&&a.namespaceURI==="http://relaxng.org/ns/structure/1.0"){if(s=d(a,b,r))s.name==="name"?y.push(c[s.a.ns]+":"+s.text):s.name==="choice"&&s.names&&s.names.length&&(y=y.concat(s.names),delete s.names),r.push(s)}else a.nodeType===3&&(C+=a.nodeValue);a=a.nextSibling}a=
-C;n!=="value"&&n!=="param"&&(a=/^\s*([\s\S]*\S)?\s*$/.exec(a)[1]);if(n==="value"&&i.type===void 0)i.type="token",i.datatypeLibrary="";if((n==="attribute"||n==="element")&&i.name!==void 0)u=e(i.name),h=[{name:"name",text:u[1],a:{ns:u[0]}}].concat(h),delete i.name;if(n==="name"||n==="nsName"||n==="value"){if(i.ns===void 0)i.ns=""}else delete i.ns;if(n==="name")u=e(a),i.ns=u[0],a=u[1];if(h.length>1&&(n==="define"||n==="oneOrMore"||n==="zeroOrMore"||n==="optional"||n==="list"||n==="mixed"))h=[{name:"group",
-e:k({name:"group",e:h}).e}];h.length>2&&n==="element"&&(h=[h[0]].concat({name:"group",e:k({name:"group",e:h.slice(1)}).e}));h.length===1&&n==="attribute"&&h.push({name:"text",text:a});if(h.length===1&&(n==="choice"||n==="group"||n==="interleave"))n=h[0].name,q=h[0].names,i=h[0].a,a=h[0].text,h=h[0].e;else if(h.length>2&&(n==="choice"||n==="group"||n==="interleave"))h=k({name:n,e:h}).e;n==="mixed"&&(n="interleave",h=[h[0],{name:"text"}]);n==="optional"&&(n="choice",h=[h[0],{name:"empty"}]);n==="zeroOrMore"&&
-(n="choice",h=[{name:"oneOrMore",e:[h[0]]},{name:"empty"}]);if(n==="define"&&i.combine){a:{r=i.combine;y=i.name;C=h;for(s=0;g&&s<g.length;s+=1)if(v=g[s],v.name==="define"&&v.a&&v.a.name===y){v.e=[{name:r,e:v.e.concat(C)}];g=v;break a}g=null}if(g)return}g={name:n};if(h&&h.length>0)g.e=h;for(u in i)if(i.hasOwnProperty(u)){g.a=i;break}if(a!==void 0)g.text=a;if(q&&q.length>0)g.names=q;if(n==="element")g.id=b.length,b.push(g),g={name:"elementref",id:g.id};return g};this.parseRelaxNGDOM=function(f,e){var k=
-[],m=d(f&&f.documentElement,k,void 0),l,u,n={};for(l=0;l<m.e.length;l+=1)u=m.e[l],u.name==="define"?n[u.a.name]=u:u.name==="start"&&(h=u);if(!h)return[new i("No Relax NG start element was found.")];g(h,n);for(l in n)n.hasOwnProperty(l)&&g(n[l],n);for(l=0;l<k.length;l+=1)g(k[l],n);if(e)b.rootPattern=e(h.e[0],k);a(h,k);for(l=0;l<k.length;l+=1)a(k[l],k);b.start=h;b.elements=k;b.nsmap=c;return null}};
+xmldom.RelaxNGParser=function(){function g(a,c){this.message=function(){c&&(a+=1===c.nodeType?" Element ":" Node ",a+=c.nodeName,c.nodeValue&&(a+=" with value '"+c.nodeValue+"'"),a+=".");return a}}function m(a){if(2>=a.e.length)return a;var c={name:a.name,e:a.e.slice(0,2)};return m({name:a.name,e:[c].concat(a.e.slice(2))})}function e(a){var a=a.split(":",2),c="",b;1===a.length?a=["",a[0]]:c=a[0];for(b in d)d[b]===c&&(a[0]=b);return a}function k(a,c){for(var b=0,d,g,o=a.name;a.e&&b<a.e.length;)if(d=
+a.e[b],"ref"===d.name){g=c[d.a.name];if(!g)throw d.a.name+" was not defined.";d=a.e.slice(b+1);a.e=a.e.slice(0,b);a.e=a.e.concat(g.e);a.e=a.e.concat(d)}else b+=1,k(d,c);d=a.e;if("choice"===o&&(!d||!d[1]||"empty"===d[1].name))!d||!d[0]||"empty"===d[0].name?(delete a.e,a.name="empty"):(d[1]=d[0],d[0]={name:"empty"});if("group"===o||"interleave"===o)"empty"===d[0].name?"empty"===d[1].name?(delete a.e,a.name="empty"):(o=a.name=d[1].name,a.names=d[1].names,d=a.e=d[1].e):"empty"===d[1].name&&(o=a.name=
+d[0].name,a.names=d[0].names,d=a.e=d[0].e);"oneOrMore"===o&&"empty"===d[0].name&&(delete a.e,a.name="empty");if("attribute"===o){g=a.names?a.names.length:0;for(var p,t=a.localnames=[g],r=a.namespaces=[g],b=0;b<g;b+=1)p=e(a.names[b]),r[b]=p[0],t[b]=p[1]}"interleave"===o&&("interleave"===d[0].name?"interleave"===d[1].name?a.e=d[0].e.concat(d[1].e):a.e=[d[1]].concat(d[0].e):"interleave"===d[1].name&&(a.e=[d[0]].concat(d[1].e)))}function a(c,d){for(var b=0,e;c.e&&b<c.e.length;)e=c.e[b],"elementref"===
+e.name?(e.id=e.id||0,c.e[b]=d[e.id]):"element"!==e.name&&a(e,d),b+=1}var c=this,b,d={"http://www.w3.org/XML/1998/namespace":"xml"},o;o=function(a,c,b){var g=[],n,k,p=a.localName,t=[];n=a.attributes;var r=p,z=t,s={},q,u;for(q=0;q<n.length;q+=1)if(u=n.item(q),u.namespaceURI)"http://www.w3.org/2000/xmlns/"===u.namespaceURI&&(d[u.value]=u.localName);else{"name"===u.localName&&("element"===r||"attribute"===r)&&z.push(u.value);if("name"===u.localName||"combine"===u.localName||"type"===u.localName){var E=
+u,C;C=u.value;C=C.replace(/^\s\s*/,"");for(var w=/\s/,G=C.length-1;w.test(C.charAt(G));)G-=1;C=C.slice(0,G+1);E.value=C}s[u.localName]=u.value}n=s;n.combine=n.combine||void 0;a=a.firstChild;r=g;z=t;for(s="";a;){if(1===a.nodeType&&"http://relaxng.org/ns/structure/1.0"===a.namespaceURI){if(q=o(a,c,r))"name"===q.name?z.push(d[q.a.ns]+":"+q.text):"choice"===q.name&&q.names&&q.names.length&&(z=z.concat(q.names),delete q.names),r.push(q)}else 3===a.nodeType&&(s+=a.nodeValue);a=a.nextSibling}a=s;"value"!==
+p&&"param"!==p&&(a=/^\s*([\s\S]*\S)?\s*$/.exec(a)[1]);"value"===p&&void 0===n.type&&(n.type="token",n.datatypeLibrary="");if(("attribute"===p||"element"===p)&&void 0!==n.name)k=e(n.name),g=[{name:"name",text:k[1],a:{ns:k[0]}}].concat(g),delete n.name;"name"===p||"nsName"===p||"value"===p?void 0===n.ns&&(n.ns=""):delete n.ns;"name"===p&&(k=e(a),n.ns=k[0],a=k[1]);if(1<g.length&&("define"===p||"oneOrMore"===p||"zeroOrMore"===p||"optional"===p||"list"===p||"mixed"===p))g=[{name:"group",e:m({name:"group",
+e:g}).e}];2<g.length&&"element"===p&&(g=[g[0]].concat({name:"group",e:m({name:"group",e:g.slice(1)}).e}));1===g.length&&"attribute"===p&&g.push({name:"text",text:a});if(1===g.length&&("choice"===p||"group"===p||"interleave"===p))p=g[0].name,t=g[0].names,n=g[0].a,a=g[0].text,g=g[0].e;else if(2<g.length&&("choice"===p||"group"===p||"interleave"===p))g=m({name:p,e:g}).e;"mixed"===p&&(p="interleave",g=[g[0],{name:"text"}]);"optional"===p&&(p="choice",g=[g[0],{name:"empty"}]);"zeroOrMore"===p&&(p="choice",
+g=[{name:"oneOrMore",e:[g[0]]},{name:"empty"}]);if("define"===p&&n.combine){a:{r=n.combine;z=n.name;s=g;for(q=0;b&&q<b.length;q+=1)if(u=b[q],"define"===u.name&&u.a&&u.a.name===z){u.e=[{name:r,e:u.e.concat(s)}];b=u;break a}b=null}if(b)return}b={name:p};g&&0<g.length&&(b.e=g);for(k in n)if(n.hasOwnProperty(k)){b.a=n;break}void 0!==a&&(b.text=a);t&&0<t.length&&(b.names=t);"element"===p&&(b.id=c.length,c.push(b),b={name:"elementref",id:b.id});return b};this.parseRelaxNGDOM=function(f,e){var i=[],j=o(f&&
+f.documentElement,i,void 0),n,m,p={};for(n=0;n<j.e.length;n+=1)m=j.e[n],"define"===m.name?p[m.a.name]=m:"start"===m.name&&(b=m);if(!b)return[new g("No Relax NG start element was found.")];k(b,p);for(n in p)p.hasOwnProperty(n)&&k(p[n],p);for(n=0;n<i.length;n+=1)k(i[n],p);e&&(c.rootPattern=e(b.e[0],i));a(b,i);for(n=0;n<i.length;n+=1)a(i[n],i);c.start=b;c.elements=i;c.nsmap=d;return null}};
 // Input 15
 runtime.loadClass("xmldom.RelaxNGParser");
-xmldom.RelaxNG=function(){function i(a){return function(){var c;return function(){c===void 0&&(c=a());return c}}()}function k(a,c){return function(){var b={},d=0;return function(f){var e=f.hash||f.toString(),g;g=b[e];if(g!==void 0)return g;b[e]=g=c(f);g.hash=a+d.toString();d+=1;return g}}()}function e(a){return function(){var c={};return function(b){var d,f;f=c[b.localName];if(f===void 0)c[b.localName]=f={};else if(d=f[b.namespaceURI],d!==void 0)return d;return f[b.namespaceURI]=d=a(b)}}()}function g(a,
-c,b){return function(){var d={},f=0;return function(e,g){var h=c&&c(e,g),j,i;if(h!==void 0)return h;h=e.hash||e.toString();j=g.hash||g.toString();i=d[h];if(i===void 0)d[h]=i={};else if(h=i[j],h!==void 0)return h;i[j]=h=b(e,g);h.hash=a+f.toString();f+=1;return h}}()}function a(c,b){b.p1.type==="choice"?a(c,b.p1):c[b.p1.hash]=b.p1;b.p2.type==="choice"?a(c,b.p2):c[b.p2.hash]=b.p2}function b(a,c){return{type:"element",nc:a,nullable:false,textDeriv:function(){return s},startTagOpenDeriv:function(b){return a.contains(b)?
-l(c,v):s},attDeriv:function(){return s},startTagCloseDeriv:function(){return this}}}function h(){return{type:"list",nullable:false,hash:"list",textDeriv:function(){return v}}}function c(a,b,d,e){if(b===s)return s;if(e>=d.length)return b;e===0&&(e=0);for(var g=d.item(e);g.namespaceURI===f;){e+=1;if(e>=d.length)return b;g=d.item(e)}return g=c(a,b.attDeriv(a,d.item(e)),d,e+1)}function d(a,c,b){b.e[0].a?(a.push(b.e[0].text),c.push(b.e[0].a.ns)):d(a,c,b.e[0]);b.e[1].a?(a.push(b.e[1].text),c.push(b.e[1].a.ns)):
-d(a,c,b.e[1])}var f="http://www.w3.org/2000/xmlns/",j,p,m,l,u,n,q,r,y,C,s={type:"notAllowed",nullable:false,hash:"notAllowed",textDeriv:function(){return s},startTagOpenDeriv:function(){return s},attDeriv:function(){return s},startTagCloseDeriv:function(){return s},endTagDeriv:function(){return s}},v={type:"empty",nullable:true,hash:"empty",textDeriv:function(){return s},startTagOpenDeriv:function(){return s},attDeriv:function(){return s},startTagCloseDeriv:function(){return v},endTagDeriv:function(){return s}},
-E={type:"text",nullable:true,hash:"text",textDeriv:function(){return E},startTagOpenDeriv:function(){return s},attDeriv:function(){return s},startTagCloseDeriv:function(){return E},endTagDeriv:function(){return s}},B,z,G;j=g("choice",function(a,c){if(a===s)return c;if(c===s)return a;if(a===c)return a},function(c,b){var d={},f;a(d,{p1:c,p2:b});b=c=void 0;for(f in d)d.hasOwnProperty(f)&&(c===void 0?c=d[f]:b=b===void 0?d[f]:j(b,d[f]));return function(a,c){return{type:"choice",p1:a,p2:c,nullable:a.nullable||
-c.nullable,textDeriv:function(b,d){return j(a.textDeriv(b,d),c.textDeriv(b,d))},startTagOpenDeriv:e(function(b){return j(a.startTagOpenDeriv(b),c.startTagOpenDeriv(b))}),attDeriv:function(b,d){return j(a.attDeriv(b,d),c.attDeriv(b,d))},startTagCloseDeriv:i(function(){return j(a.startTagCloseDeriv(),c.startTagCloseDeriv())}),endTagDeriv:i(function(){return j(a.endTagDeriv(),c.endTagDeriv())})}}(c,b)});p=function(a,c,b){return function(){var d={},f=0;return function(e,g){var h=c&&c(e,g),j,i;if(h!==
-void 0)return h;h=e.hash||e.toString();j=g.hash||g.toString();h<j&&(i=h,h=j,j=i,i=e,e=g,g=i);i=d[h];if(i===void 0)d[h]=i={};else if(h=i[j],h!==void 0)return h;i[j]=h=b(e,g);h.hash=a+f.toString();f+=1;return h}}()}("interleave",function(a,c){if(a===s||c===s)return s;if(a===v)return c;if(c===v)return a},function(a,c){return{type:"interleave",p1:a,p2:c,nullable:a.nullable&&c.nullable,textDeriv:function(b,d){return j(p(a.textDeriv(b,d),c),p(a,c.textDeriv(b,d)))},startTagOpenDeriv:e(function(b){return j(B(function(a){return p(a,
-c)},a.startTagOpenDeriv(b)),B(function(c){return p(a,c)},c.startTagOpenDeriv(b)))}),attDeriv:function(b,d){return j(p(a.attDeriv(b,d),c),p(a,c.attDeriv(b,d)))},startTagCloseDeriv:i(function(){return p(a.startTagCloseDeriv(),c.startTagCloseDeriv())})}});m=g("group",function(a,c){if(a===s||c===s)return s;if(a===v)return c;if(c===v)return a},function(a,c){return{type:"group",p1:a,p2:c,nullable:a.nullable&&c.nullable,textDeriv:function(b,d){var f=m(a.textDeriv(b,d),c);return a.nullable?j(f,c.textDeriv(b,
-d)):f},startTagOpenDeriv:function(b){var d=B(function(a){return m(a,c)},a.startTagOpenDeriv(b));return a.nullable?j(d,c.startTagOpenDeriv(b)):d},attDeriv:function(b,d){return j(m(a.attDeriv(b,d),c),m(a,c.attDeriv(b,d)))},startTagCloseDeriv:i(function(){return m(a.startTagCloseDeriv(),c.startTagCloseDeriv())})}});l=g("after",function(a,c){if(a===s||c===s)return s},function(a,c){return{type:"after",p1:a,p2:c,nullable:false,textDeriv:function(b,d){return l(a.textDeriv(b,d),c)},startTagOpenDeriv:e(function(b){return B(function(a){return l(a,
-c)},a.startTagOpenDeriv(b))}),attDeriv:function(b,d){return l(a.attDeriv(b,d),c)},startTagCloseDeriv:i(function(){return l(a.startTagCloseDeriv(),c)}),endTagDeriv:i(function(){return a.nullable?c:s})}});u=k("oneormore",function(a){return a===s?s:{type:"oneOrMore",p:a,nullable:a.nullable,textDeriv:function(c,b){return m(a.textDeriv(c,b),j(this,v))},startTagOpenDeriv:function(c){var b=this;return B(function(a){return m(a,j(b,v))},a.startTagOpenDeriv(c))},attDeriv:function(c,b){return m(a.attDeriv(c,
-b),j(this,v))},startTagCloseDeriv:i(function(){return u(a.startTagCloseDeriv())})}});q=g("attribute",void 0,function(a,c){return{type:"attribute",nullable:false,nc:a,p:c,attDeriv:function(b,d){return a.contains(d)&&(c.nullable&&/^\s+$/.test(d.nodeValue)||c.textDeriv(b,d.nodeValue).nullable)?v:s},startTagCloseDeriv:function(){return s}}});n=k("value",function(a){return{type:"value",nullable:false,value:a,textDeriv:function(c,b){return b===a?v:s},attDeriv:function(){return s},startTagCloseDeriv:function(){return this}}});
-y=k("data",function(a){return{type:"data",nullable:false,dataType:a,textDeriv:function(){return v},attDeriv:function(){return s},startTagCloseDeriv:function(){return this}}});B=function x(a,c){if(c.type==="after")return l(c.p1,a(c.p2));else if(c.type==="choice")return j(x(a,c.p1),x(a,c.p2));return c};z=function(a,b,d){for(var f=d.currentNode,b=b.startTagOpenDeriv(f),b=c(a,b,f.attributes,0),e=b=b.startTagCloseDeriv(),f=d.currentNode,b=d.firstChild(),g=0,h=[];b;)b.nodeType===1?h.push(b):b.nodeType===
-3&&!/^\s*$/.test(b.nodeValue)&&(h.push(b.nodeValue),g+=1),b=d.nextSibling();h.length===0&&(h=[""]);g=e;for(e=0;g!==s&&e<h.length;e+=1)b=h[e],typeof b==="string"?g=/^\s*$/.test(b)?j(g,g.textDeriv(a,b)):g.textDeriv(a,b):(d.currentNode=b,g=z(a,g,d));d.currentNode=f;return b=g.endTagDeriv()};r=function(a){var c,b,f;if(a.name==="name")return c=a.text,b=a.a.ns,{name:c,ns:b,hash:"{"+b+"}"+c,contains:function(a){return a.namespaceURI===b&&a.localName===c}};else if(a.name==="choice"){c=[];b=[];d(c,b,a);a=
-"";for(f=0;f<c.length;f+=1)a+="{"+b[f]+"}"+c[f]+",";return{hash:a,contains:function(a){var d;for(d=0;d<c.length;d+=1)if(c[d]===a.localName&&b[d]===a.namespaceURI)return true;return false}}}return{hash:"anyName",contains:function(){return true}}};C=function t(a,c){var d,f;if(a.name==="elementref"){d=a.id||0;a=c[d];if(a.name!==void 0){var e=a;d=c[e.id]={hash:"element"+e.id.toString()};e=b(r(e.e[0]),C(e.e[1],c));for(f in e)e.hasOwnProperty(f)&&(d[f]=e[f]);f=d}else f=a;return f}switch(a.name){case "empty":return v;
-case "notAllowed":return s;case "text":return E;case "choice":return j(t(a.e[0],c),t(a.e[1],c));case "interleave":d=t(a.e[0],c);for(f=1;f<a.e.length;f+=1)d=p(d,t(a.e[f],c));return d;case "group":return m(t(a.e[0],c),t(a.e[1],c));case "oneOrMore":return u(t(a.e[0],c));case "attribute":return q(r(a.e[0]),t(a.e[1],c));case "value":return n(a.text);case "data":return d=a.a&&a.a.type,d===void 0&&(d=""),y(d);case "list":return h()}throw"No support for "+a.name;};this.makePattern=function(a,c){var b={},
-d;for(d in c)c.hasOwnProperty(d)&&(b[d]=c[d]);return d=C(a,b)};this.validate=function(a,c){var b;a.currentNode=a.root;b=z(null,G,a);b.nullable?c(null):(runtime.log("Error in Relax NG validation: "+b),c(["Error in Relax NG validation: "+b]))};this.init=function(a){G=a}};
+xmldom.RelaxNG=function(){function g(a){return function(){var c;return function(){void 0===c&&(c=a());return c}}()}function m(a,c){return function(){var b={},d=0;return function(f){var e=f.hash||f.toString(),g;g=b[e];if(void 0!==g)return g;b[e]=g=c(f);g.hash=a+d.toString();d+=1;return g}}()}function e(a){return function(){var c={};return function(b){var d,f;f=c[b.localName];if(void 0===f)c[b.localName]=f={};else if(d=f[b.namespaceURI],void 0!==d)return d;return f[b.namespaceURI]=d=a(b)}}()}function k(a,
+c,b){return function(){var d={},f=0;return function(e,g){var h=c&&c(e,g),j,i;if(void 0!==h)return h;h=e.hash||e.toString();j=g.hash||g.toString();i=d[h];if(void 0===i)d[h]=i={};else if(h=i[j],void 0!==h)return h;i[j]=h=b(e,g);h.hash=a+f.toString();f+=1;return h}}()}function a(c,b){"choice"===b.p1.type?a(c,b.p1):c[b.p1.hash]=b.p1;"choice"===b.p2.type?a(c,b.p2):c[b.p2.hash]=b.p2}function c(a,c){return{type:"element",nc:a,nullable:!1,textDeriv:function(){return q},startTagOpenDeriv:function(b){return a.contains(b)?
+n(c,u):q},attDeriv:function(){return q},startTagCloseDeriv:function(){return this}}}function b(){return{type:"list",nullable:!1,hash:"list",textDeriv:function(){return u}}}function d(a,c,b,e){if(c===q)return q;if(e>=b.length)return c;0===e&&(e=0);for(var g=b.item(e);g.namespaceURI===f;){e+=1;if(e>=b.length)return c;g=b.item(e)}return g=d(a,c.attDeriv(a,b.item(e)),b,e+1)}function o(a,c,b){b.e[0].a?(a.push(b.e[0].text),c.push(b.e[0].a.ns)):o(a,c,b.e[0]);b.e[1].a?(a.push(b.e[1].text),c.push(b.e[1].a.ns)):
+o(a,c,b.e[1])}var f="http://www.w3.org/2000/xmlns/",h,i,j,n,x,p,t,r,z,s,q={type:"notAllowed",nullable:!1,hash:"notAllowed",textDeriv:function(){return q},startTagOpenDeriv:function(){return q},attDeriv:function(){return q},startTagCloseDeriv:function(){return q},endTagDeriv:function(){return q}},u={type:"empty",nullable:!0,hash:"empty",textDeriv:function(){return q},startTagOpenDeriv:function(){return q},attDeriv:function(){return q},startTagCloseDeriv:function(){return u},endTagDeriv:function(){return q}},
+E={type:"text",nullable:!0,hash:"text",textDeriv:function(){return E},startTagOpenDeriv:function(){return q},attDeriv:function(){return q},startTagCloseDeriv:function(){return E},endTagDeriv:function(){return q}},C,w,G;h=k("choice",function(a,b){if(a===q)return b;if(b===q||a===b)return a},function(b,c){var d={},f;a(d,{p1:b,p2:c});c=b=void 0;for(f in d)d.hasOwnProperty(f)&&(void 0===b?b=d[f]:c=void 0===c?d[f]:h(c,d[f]));return function(a,b){return{type:"choice",p1:a,p2:b,nullable:a.nullable||b.nullable,
+textDeriv:function(c,d){return h(a.textDeriv(c,d),b.textDeriv(c,d))},startTagOpenDeriv:e(function(c){return h(a.startTagOpenDeriv(c),b.startTagOpenDeriv(c))}),attDeriv:function(c,d){return h(a.attDeriv(c,d),b.attDeriv(c,d))},startTagCloseDeriv:g(function(){return h(a.startTagCloseDeriv(),b.startTagCloseDeriv())}),endTagDeriv:g(function(){return h(a.endTagDeriv(),b.endTagDeriv())})}}(b,c)});i=function(a,b,c){return function(){var d={},f=0;return function(e,g){var h=b&&b(e,g),j,i;if(void 0!==h)return h;
+h=e.hash||e.toString();j=g.hash||g.toString();h<j&&(i=h,h=j,j=i,i=e,e=g,g=i);i=d[h];if(void 0===i)d[h]=i={};else if(h=i[j],void 0!==h)return h;i[j]=h=c(e,g);h.hash=a+f.toString();f+=1;return h}}()}("interleave",function(a,b){if(a===q||b===q)return q;if(a===u)return b;if(b===u)return a},function(a,b){return{type:"interleave",p1:a,p2:b,nullable:a.nullable&&b.nullable,textDeriv:function(c,d){return h(i(a.textDeriv(c,d),b),i(a,b.textDeriv(c,d)))},startTagOpenDeriv:e(function(c){return h(C(function(a){return i(a,
+b)},a.startTagOpenDeriv(c)),C(function(b){return i(a,b)},b.startTagOpenDeriv(c)))}),attDeriv:function(c,d){return h(i(a.attDeriv(c,d),b),i(a,b.attDeriv(c,d)))},startTagCloseDeriv:g(function(){return i(a.startTagCloseDeriv(),b.startTagCloseDeriv())})}});j=k("group",function(a,b){if(a===q||b===q)return q;if(a===u)return b;if(b===u)return a},function(a,b){return{type:"group",p1:a,p2:b,nullable:a.nullable&&b.nullable,textDeriv:function(c,d){var f=j(a.textDeriv(c,d),b);return a.nullable?h(f,b.textDeriv(c,
+d)):f},startTagOpenDeriv:function(c){var d=C(function(a){return j(a,b)},a.startTagOpenDeriv(c));return a.nullable?h(d,b.startTagOpenDeriv(c)):d},attDeriv:function(c,d){return h(j(a.attDeriv(c,d),b),j(a,b.attDeriv(c,d)))},startTagCloseDeriv:g(function(){return j(a.startTagCloseDeriv(),b.startTagCloseDeriv())})}});n=k("after",function(a,b){if(a===q||b===q)return q},function(a,b){return{type:"after",p1:a,p2:b,nullable:!1,textDeriv:function(c,d){return n(a.textDeriv(c,d),b)},startTagOpenDeriv:e(function(c){return C(function(a){return n(a,
+b)},a.startTagOpenDeriv(c))}),attDeriv:function(c,d){return n(a.attDeriv(c,d),b)},startTagCloseDeriv:g(function(){return n(a.startTagCloseDeriv(),b)}),endTagDeriv:g(function(){return a.nullable?b:q})}});x=m("oneormore",function(a){return a===q?q:{type:"oneOrMore",p:a,nullable:a.nullable,textDeriv:function(b,c){return j(a.textDeriv(b,c),h(this,u))},startTagOpenDeriv:function(b){var c=this;return C(function(a){return j(a,h(c,u))},a.startTagOpenDeriv(b))},attDeriv:function(b,c){return j(a.attDeriv(b,
+c),h(this,u))},startTagCloseDeriv:g(function(){return x(a.startTagCloseDeriv())})}});t=k("attribute",void 0,function(a,b){return{type:"attribute",nullable:!1,nc:a,p:b,attDeriv:function(c,d){return a.contains(d)&&(b.nullable&&/^\s+$/.test(d.nodeValue)||b.textDeriv(c,d.nodeValue).nullable)?u:q},startTagCloseDeriv:function(){return q}}});p=m("value",function(a){return{type:"value",nullable:!1,value:a,textDeriv:function(b,c){return c===a?u:q},attDeriv:function(){return q},startTagCloseDeriv:function(){return this}}});
+z=m("data",function(a){return{type:"data",nullable:!1,dataType:a,textDeriv:function(){return u},attDeriv:function(){return q},startTagCloseDeriv:function(){return this}}});C=function v(a,b){return"after"===b.type?n(b.p1,a(b.p2)):"choice"===b.type?h(v(a,b.p1),v(a,b.p2)):b};w=function(a,b,c){for(var f=c.currentNode,b=b.startTagOpenDeriv(f),b=d(a,b,f.attributes,0),e=b=b.startTagCloseDeriv(),f=c.currentNode,b=c.firstChild(),g=[],j;b;)1===b.nodeType?g.push(b):3===b.nodeType&&!/^\s*$/.test(b.nodeValue)&&
+g.push(b.nodeValue),b=c.nextSibling();0===g.length&&(g=[""]);j=e;for(e=0;j!==q&&e<g.length;e+=1)b=g[e],"string"===typeof b?j=/^\s*$/.test(b)?h(j,j.textDeriv(a,b)):j.textDeriv(a,b):(c.currentNode=b,j=w(a,j,c));c.currentNode=f;return b=j.endTagDeriv()};r=function(a){var b,c,d;if("name"===a.name)return b=a.text,c=a.a.ns,{name:b,ns:c,hash:"{"+c+"}"+b,contains:function(a){return a.namespaceURI===c&&a.localName===b}};if("choice"===a.name){b=[];c=[];o(b,c,a);a="";for(d=0;d<b.length;d+=1)a+="{"+c[d]+"}"+
+b[d]+",";return{hash:a,contains:function(a){var d;for(d=0;d<b.length;d+=1)if(b[d]===a.localName&&c[d]===a.namespaceURI)return!0;return!1}}}return{hash:"anyName",contains:function(){return!0}}};s=function y(a,d){var f,e;if("elementref"===a.name){f=a.id||0;a=d[f];if(void 0!==a.name){var g=a;f=d[g.id]={hash:"element"+g.id.toString()};g=c(r(g.e[0]),s(g.e[1],d));for(e in g)g.hasOwnProperty(e)&&(f[e]=g[e]);e=f}else e=a;return e}switch(a.name){case "empty":return u;case "notAllowed":return q;case "text":return E;
+case "choice":return h(y(a.e[0],d),y(a.e[1],d));case "interleave":f=y(a.e[0],d);for(e=1;e<a.e.length;e+=1)f=i(f,y(a.e[e],d));return f;case "group":return j(y(a.e[0],d),y(a.e[1],d));case "oneOrMore":return x(y(a.e[0],d));case "attribute":return t(r(a.e[0]),y(a.e[1],d));case "value":return p(a.text);case "data":return f=a.a&&a.a.type,void 0===f&&(f=""),z(f);case "list":return b()}throw"No support for "+a.name;};this.makePattern=function(a,b){var c={},d;for(d in b)b.hasOwnProperty(d)&&(c[d]=b[d]);return d=
+s(a,c)};this.validate=function(a,b){var c;a.currentNode=a.root;c=w(null,G,a);c.nullable?b(null):(runtime.log("Error in Relax NG validation: "+c),b(["Error in Relax NG validation: "+c]))};this.init=function(a){G=a}};
 // Input 16
 runtime.loadClass("xmldom.RelaxNGParser");
-xmldom.RelaxNG2=function(){function i(a,b){this.message=function(){b&&(a+=b.nodeType===1?" Element ":" Node ",a+=b.nodeName,b.nodeValue&&(a+=" with value '"+b.nodeValue+"'"),a+=".");return a}}function k(c,b,f,e){return c.name==="empty"?null:a(c,b,f,e)}function e(a,d){if(a.e.length!==2)throw"Element with wrong # of elements: "+a.e.length;h+=1;for(var f=d.currentNode,e=f?f.nodeType:0,g=null;e>1;){if(e!==8&&(e!==3||!/^\s+$/.test(d.currentNode.nodeValue)))return h-=1,[new i("Not allowed node of type "+
-e+".")];e=(f=d.nextSibling())?f.nodeType:0}if(!f)return h-=1,[new i("Missing element "+a.names)];if(a.names&&a.names.indexOf(b[f.namespaceURI]+":"+f.localName)===-1)return h-=1,[new i("Found "+f.nodeName+" instead of "+a.names+".",f)];if(d.firstChild()){for(g=k(a.e[1],d,f);d.nextSibling();)if(e=d.currentNode.nodeType,(!d.currentNode||!(d.currentNode.nodeType===3&&/^\s+$/.test(d.currentNode.nodeValue)))&&e!==8)return h-=1,[new i("Spurious content.",d.currentNode)];if(d.parentNode()!==f)return h-=1,
-[new i("Implementation error.")]}else g=k(a.e[1],d,f);h-=1;d.nextSibling();return g}var g,a,b,h=0;a=function(b,d,f,g){var h=b.name,m=null;if(h==="text")a:{for(var l=(b=d.currentNode)?b.nodeType:0;b!==f&&l!==3;){if(l===1){m=[new i("Element not allowed here.",b)];break a}l=(b=d.nextSibling())?b.nodeType:0}d.nextSibling();m=null}else if(h==="data")m=null;else if(h==="value")g!==b.text&&(m=[new i("Wrong value, should be '"+b.text+"', not '"+g+"'",f)]);else if(h==="list")m=null;else if(h==="attribute")a:{if(b.e.length!==
-2)throw"Attribute with wrong # of elements: "+b.e.length;h=b.localnames.length;for(m=0;m<h;m+=1){g=f.getAttributeNS(b.namespaces[m],b.localnames[m]);g===""&&!f.hasAttributeNS(b.namespaces[m],b.localnames[m])&&(g=void 0);if(l!==void 0&&g!==void 0){m=[new i("Attribute defined too often.",f)];break a}l=g}m=l===void 0?[new i("Attribute not found: "+b.names,f)]:k(b.e[1],d,f,l)}else if(h==="element")m=e(b,d,f);else if(h==="oneOrMore"){g=0;do l=d.currentNode,h=a(b.e[0],d,f),g+=1;while(!h&&l!==d.currentNode);
-g>1?(d.currentNode=l,m=null):m=h}else if(h==="choice"){if(b.e.length!==2)throw"Choice with wrong # of options: "+b.e.length;l=d.currentNode;if(b.e[0].name==="empty"){if(h=a(b.e[1],d,f,g))d.currentNode=l;m=null}else{if(h=k(b.e[0],d,f,g))d.currentNode=l,h=a(b.e[1],d,f,g);m=h}}else if(h==="group"){if(b.e.length!==2)throw"Group with wrong # of members: "+b.e.length;m=a(b.e[0],d,f)||a(b.e[1],d,f)}else if(h==="interleave")a:{for(var l=b.e.length,g=[l],u=l,n,q,r,y;u>0;){n=0;q=d.currentNode;for(m=0;m<l;m+=
-1)if(r=d.currentNode,g[m]!==true&&g[m]!==r)y=b.e[m],(h=a(y,d,f))?(d.currentNode=r,g[m]===void 0&&(g[m]=false)):r===d.currentNode||y.name==="oneOrMore"||y.name==="choice"&&(y.e[0].name==="oneOrMore"||y.e[1].name==="oneOrMore")?(n+=1,g[m]=r):(n+=1,g[m]=true);if(q===d.currentNode&&n===u)break;if(n===0){for(m=0;m<l;m+=1)if(g[m]===false){m=[new i("Interleave does not match.",f)];break a}break}for(m=u=0;m<l;m+=1)g[m]!==true&&(u+=1)}m=null}else throw h+" not allowed in nonEmptyPattern.";return m};this.validate=
-function(a,b){a.currentNode=a.root;var f=k(g.e[0],a,a.root);b(f)};this.init=function(a,d){g=a;b=d}};
+xmldom.RelaxNG2=function(){function g(a,c){this.message=function(){c&&(a+=1===c.nodeType?" Element ":" Node ",a+=c.nodeName,c.nodeValue&&(a+=" with value '"+c.nodeValue+"'"),a+=".");return a}}function m(b,c,e,f){return"empty"===b.name?null:a(b,c,e,f)}function e(a,d){if(2!==a.e.length)throw"Element with wrong # of elements: "+a.e.length;for(var e=d.currentNode,f=e?e.nodeType:0,h=null;1<f;){if(8!==f&&(3!==f||!/^\s+$/.test(d.currentNode.nodeValue)))return[new g("Not allowed node of type "+f+".")];f=
+(e=d.nextSibling())?e.nodeType:0}if(!e)return[new g("Missing element "+a.names)];if(a.names&&-1===a.names.indexOf(c[e.namespaceURI]+":"+e.localName))return[new g("Found "+e.nodeName+" instead of "+a.names+".",e)];if(d.firstChild()){for(h=m(a.e[1],d,e);d.nextSibling();)if(f=d.currentNode.nodeType,(!d.currentNode||!(3===d.currentNode.nodeType&&/^\s+$/.test(d.currentNode.nodeValue)))&&8!==f)return[new g("Spurious content.",d.currentNode)];if(d.parentNode()!==e)return[new g("Implementation error.")]}else h=
+m(a.e[1],d,e);d.nextSibling();return h}var k,a,c;a=function(b,c,o,f){var h=b.name,i=null;if("text"===h)a:{for(var j=(b=c.currentNode)?b.nodeType:0;b!==o&&3!==j;){if(1===j){i=[new g("Element not allowed here.",b)];break a}j=(b=c.nextSibling())?b.nodeType:0}c.nextSibling();i=null}else if("data"===h)i=null;else if("value"===h)f!==b.text&&(i=[new g("Wrong value, should be '"+b.text+"', not '"+f+"'",o)]);else if("list"===h)i=null;else if("attribute"===h)a:{if(2!==b.e.length)throw"Attribute with wrong # of elements: "+
+b.e.length;h=b.localnames.length;for(i=0;i<h;i+=1){f=o.getAttributeNS(b.namespaces[i],b.localnames[i]);""===f&&!o.hasAttributeNS(b.namespaces[i],b.localnames[i])&&(f=void 0);if(void 0!==j&&void 0!==f){i=[new g("Attribute defined too often.",o)];break a}j=f}i=void 0===j?[new g("Attribute not found: "+b.names,o)]:m(b.e[1],c,o,j)}else if("element"===h)i=e(b,c,o);else if("oneOrMore"===h){f=0;do j=c.currentNode,h=a(b.e[0],c,o),f+=1;while(!h&&j!==c.currentNode);1<f?(c.currentNode=j,i=null):i=h}else if("choice"===
+h){if(2!==b.e.length)throw"Choice with wrong # of options: "+b.e.length;j=c.currentNode;if("empty"===b.e[0].name){if(h=a(b.e[1],c,o,f))c.currentNode=j;i=null}else{if(h=m(b.e[0],c,o,f))c.currentNode=j,h=a(b.e[1],c,o,f);i=h}}else if("group"===h){if(2!==b.e.length)throw"Group with wrong # of members: "+b.e.length;i=a(b.e[0],c,o)||a(b.e[1],c,o)}else if("interleave"===h)a:{for(var j=b.e.length,f=[j],n=j,k,p,t,r;0<n;){k=0;p=c.currentNode;for(i=0;i<j;i+=1)t=c.currentNode,!0!==f[i]&&f[i]!==t&&(r=b.e[i],(h=
+a(r,c,o))?(c.currentNode=t,void 0===f[i]&&(f[i]=!1)):t===c.currentNode||"oneOrMore"===r.name||"choice"===r.name&&("oneOrMore"===r.e[0].name||"oneOrMore"===r.e[1].name)?(k+=1,f[i]=t):(k+=1,f[i]=!0));if(p===c.currentNode&&k===n)break;if(0===k){for(i=0;i<j;i+=1)if(!1===f[i]){i=[new g("Interleave does not match.",o)];break a}break}for(i=n=0;i<j;i+=1)!0!==f[i]&&(n+=1)}i=null}else throw h+" not allowed in nonEmptyPattern.";return i};this.validate=function(a,c){a.currentNode=a.root;var e=m(k.e[0],a,a.root);
+c(e)};this.init=function(a,d){k=a;c=d}};
 // Input 17
 xmldom.OperationalTransformInterface=function(){};xmldom.OperationalTransformInterface.prototype.retain=function(){};xmldom.OperationalTransformInterface.prototype.insertCharacters=function(){};xmldom.OperationalTransformInterface.prototype.insertElementStart=function(){};xmldom.OperationalTransformInterface.prototype.insertElementEnd=function(){};xmldom.OperationalTransformInterface.prototype.deleteCharacters=function(){};xmldom.OperationalTransformInterface.prototype.deleteElementStart=function(){};
 xmldom.OperationalTransformInterface.prototype.deleteElementEnd=function(){};xmldom.OperationalTransformInterface.prototype.replaceAttributes=function(){};xmldom.OperationalTransformInterface.prototype.updateAttributes=function(){};
 // Input 18
-xmldom.OperationalTransformDOM=function(){this.retain=function(){};this.insertCharacters=function(){};this.insertElementStart=function(){};this.insertElementEnd=function(){};this.deleteCharacters=function(){};this.deleteElementStart=function(){};this.deleteElementEnd=function(){};this.replaceAttributes=function(){};this.updateAttributes=function(){};this.atEnd=function(){return true}};
+xmldom.OperationalTransformDOM=function(){this.retain=function(){};this.insertCharacters=function(){};this.insertElementStart=function(){};this.insertElementEnd=function(){};this.deleteCharacters=function(){};this.deleteElementStart=function(){};this.deleteElementEnd=function(){};this.replaceAttributes=function(){};this.updateAttributes=function(){};this.atEnd=function(){return!0}};
 // Input 19
-xmldom.XPath=function(){function i(i,e,g){i=i.ownerDocument.evaluate(e,i,g,XPathResult.UNORDERED_NODE_ITERATOR_TYPE,null);e=[];for(g=i.iterateNext();g!==null;)g.nodeType===1&&e.push(g),g=i.iterateNext();return e}xmldom.XPath=function(){this.getODFElementsWithXPath=i};return xmldom.XPath}();
+xmldom.XPath=function(){function g(a,c,b){return-1!==a&&(a<c||-1===c)&&(a<b||-1===b)}function m(a){for(var c=[],b=0,d=a.length,f;b<d;){var e=a,h=d,o=c,k="",u=[],m=e.indexOf("[",b),C=e.indexOf("/",b),w=e.indexOf("=",b);g(C,m,w)?(k=e.substring(b,C),b=C+1):g(m,C,w)?(k=e.substring(b,m),b=i(e,m,u)):g(w,C,m)?(k=e.substring(b,w),b=w):(k=e.substring(b,h),b=h);o.push({location:k,predicates:u});if(b<d&&"="===a[b]){f=a.substring(b+1,d);if(2<f.length&&("'"===f[0]||'"'===f[0]))f=f.slice(1,f.length-1);else try{f=
+parseInt(f,10)}catch(G){}b=d}}return{steps:c,value:f}}function e(){}function k(){var a,c=!1;this.setNode=function(c){a=c};this.reset=function(){c=!1};this.next=function(){var b=c?null:a;c=!0;return b}}function a(a,c,b){this.reset=function(){a.reset()};this.next=function(){for(var d=a.next();d&&!(d=d.getAttributeNodeNS(c,b));)d=a.next();return d}}function c(a,c){var b=a.next(),d=null;this.reset=function(){a.reset();b=a.next();d=null};this.next=function(){for(;b;){if(d)if(c&&d.firstChild)d=d.firstChild;
+else{for(;!d.nextSibling&&d!==b;)d=d.parentNode;d===b?b=a.next():d=d.nextSibling}else{do(d=b.firstChild)||(b=a.next());while(b&&!d)}if(d&&1===d.nodeType)return d}return null}}function b(a,b){this.reset=function(){a.reset()};this.next=function(){for(var c=a.next();c&&!b(c);)c=a.next();return c}}function d(a,c,d){var c=c.split(":",2),f=d(c[0]),e=c[1];return new b(a,function(a){return a.localName===e&&a.namespaceURI===f})}function o(a,c,d){var f=new k,e=h(f,c,d),g=c.value;return void 0===g?new b(a,function(a){f.setNode(a);
+e.reset();return e.next()}):new b(a,function(a){f.setNode(a);e.reset();return(a=e.next())&&a.nodeValue===g})}function f(a,c,b){var d=a.ownerDocument,f=[],f=new k;f.setNode(a);a=m(c);f=h(f,a,b);a=[];for(b=f.next();b;)a.push(b),b=f.next();return f=a}var h,i;i=function(a,c,b){for(var d=c,f=a.length,e=0;d<f;)"]"===a[d]?(e-=1,0>=e&&b.push(m(a.substring(c,d)))):"["===a[d]&&(0>=e&&(c=d+1),e+=1),d+=1;return d};e.prototype.next=function(){};e.prototype.reset=function(){};h=function(b,f,e){var g,h,i,k;for(g=
+0;g<f.steps.length;g+=1){i=f.steps[g];h=i.location;""===h?b=new c(b,!1):"@"===h[0]?(k=h.slice(1).split(":",2),b=new a(b,e(k[0]),k[1])):"."!==h&&(b=new c(b,!1),-1!==h.indexOf(":")&&(b=d(b,h,e)));for(h=0;h<i.predicates.length;h+=1)k=i.predicates[h],b=o(b,k,e)}return b};xmldom.XPath=function(){this.getODFElementsWithXPath=f};return xmldom.XPath}();
 // Input 20
-odf.StyleInfo=function(){function i(e,g){for(var a=k[e.localName],b=a&&a[e.namespaceURI],h=b?b.length:0,c,d,f,a=0;a<h;a+=1)if(c=e.getAttributeNS(b[a].ns,b[a].localname))d=b[a].keygroup,(f=g[d])||(f=g[d]={}),f[c]=1;for(a=e.firstChild;a;)a.nodeType===1&&(b=a,i(b,g)),a=a.nextSibling}var k;this.UsedKeysList=function(e){var g={};this.uses=function(a){var b=a.localName,e=a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","name")||a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0",
-"name"),a=b==="style"?a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0","family"):a.namespaceURI==="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"?"data":b;return(a=g[a])?a[e]>0:false};i(e,g)};this.canElementHaveStyle=function(e,g){var a=k[g.localName];return(a=a&&a[g.namespaceURI])&&a.length>0};k=function(e){var j;var g,a,b,h,c,d={},f;for(g in e)if(e.hasOwnProperty(g)){b=e[g];c=b.length;for(a=0;a<c;a+=1)h=b[a],f=d[h.en]=d[h.en]||{},j=f[h.ens]=f[h.ens]||[],f=j,f.push({ns:h.ans,
-localname:h.a,keygroup:g})}return d}({text:[{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"tab-stop",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"leader-text-style"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"drop-cap",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"citation-body-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
+odf.StyleInfo=function(){function g(e,k){for(var a=m[e.localName],c=a&&a[e.namespaceURI],b=c?c.length:0,d,o,f,a=0;a<b;a+=1)if(d=e.getAttributeNS(c[a].ns,c[a].localname))o=c[a].keygroup,(f=k[o])||(f=k[o]={}),f[d]=1;for(a=e.firstChild;a;)1===a.nodeType&&(c=a,g(c,k)),a=a.nextSibling}var m;this.UsedKeysList=function(e){var k={};this.uses=function(a){var c=a.localName,b=a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","name")||a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+"name"),a="style"===c?a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0","family"):"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"===a.namespaceURI?"data":c;return(a=k[a])?0<a[b]:!1};g(e,k)};this.canElementHaveStyle=function(e,g){var a=m[g.localName];return(a=a&&a[g.namespaceURI])&&0<a.length};m=function(e){var g,a,c,b,d,o={},f;for(g in e)if(e.hasOwnProperty(g)){c=e[g];d=c.length;for(a=0;a<d;a+=1)b=c[a],f=o[b.en]=o[b.en]||{},f=f[b.ens]=f[b.ens]||[],f.push({ns:b.ans,localname:b.a,
+keygroup:g})}return o}({text:[{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"tab-stop",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"leader-text-style"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"drop-cap",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"citation-body-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
 en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"citation-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"a",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"alphabetical-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"linenumbering-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
 a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"list-level-style-number",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"ruby-text",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"span",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"a",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
 a:"visited-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"text-properties",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"text-line-through-text-style"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"alphabetical-index-source",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"main-entry-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-bibliography",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},
@@ -251,81 +258,84 @@ en:"user-field-get",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"dat
 a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-set",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"}],"page-layout":[{ens:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",en:"notes",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"page-layout-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"handout-master",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"page-layout-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
 en:"master-page",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"page-layout-name"}]})};
 // Input 21
-odf.Style2CSS=function(){function i(a,b){var c={},d,f,e;if(!b)return c;for(d=b.firstChild;d;){d.namespaceURI===p&&d.localName==="style"?e=d.getAttributeNS(p,"family"):d.namespaceURI===m&&d.localName==="list-style"&&(e="list");if(f=e&&d.getAttributeNS&&d.getAttributeNS(p,"name"))c[e]||(c[e]={}),c[e][f]=d;d=d.nextSibling}return c}function k(a,b){if(!b||!a)return null;if(a[b])return a[b];var c,d;for(c in a)if(a.hasOwnProperty(c)&&(d=k(a[c].derivedStyles,b)))return d;return null}function e(a,b,c){var d=
-b[a],f,g;if(d)if(f=d.getAttributeNS(p,"parent-style-name"),g=null,f&&(g=k(c,f),!g&&b[f]&&(e(f,b,c),g=b[f],b[f]=null)),g){if(!g.derivedStyles)g.derivedStyles={};g.derivedStyles[a]=d}else c[a]=d}function g(a,b){for(var c in a)a.hasOwnProperty(c)&&(e(c,a,b),a[c]=null)}function a(a,b){var c=u[a],d;if(c===null)return null;d="["+c+'|style-name="'+b+'"]';c==="presentation"&&(c="draw",d='[presentation|style-name="'+b+'"]');return c+"|"+n[a].join(d+","+c+"|")+d}function b(c,d,f){var e=[],g,h;e.push(a(c,d));
-for(g in f.derivedStyles)if(f.derivedStyles.hasOwnProperty(g))for(h in d=b(c,g,f.derivedStyles[g]),d)d.hasOwnProperty(h)&&e.push(d[h]);return e}function h(a,b,c){if(!a)return null;for(a=a.firstChild;a;){if(a.namespaceURI===b&&a.localName===c)return b=a;a=a.nextSibling}return null}function c(a,b){var c="",d,f;for(d in b)b.hasOwnProperty(d)&&(d=b[d],(f=a.getAttributeNS(d[0],d[1]))&&(c+=d[2]+":"+f+";"));return c}function d(a,b,c,d){for(var b='text|list[text|style-name="'+b+'"]',c=c.getAttributeNS(m,
-"level"),f="",c=c&&parseInt(c,10);c>1;)b+=" > text|list-item > text|list",c-=1;b+=" > list-item:before";try{a.insertRule(b+"{"+d+"}",a.cssRules.length)}catch(e){throw e;}}function f(a,e,g,i){if(e==="list")for(var k=i.firstChild,l,n;k;){if(k.namespaceURI===m)if(l=k,k.localName==="list-level-style-number"){n=l;var t=n.getAttributeNS(p,"num-format"),u=n.getAttributeNS(p,"num-suffix"),L="",L={1:"decimal",a:"lower-latin",A:"upper-latin",i:"lower-roman",I:"upper-roman"},Q="",Q=n.getAttributeNS(p,"num-prefix")||
-"";Q+=L.hasOwnProperty(t)?" counter(list, "+L[t]+")":t?"'"+t+"';":" ''";u&&(Q+=" '"+u+"'");n=L="content: "+Q+";";d(a,g,l,n)}else k.localName==="list-level-style-image"?(n="content: none;",d(a,g,l,n)):k.localName==="list-level-style-bullet"&&(n="content: '"+l.getAttributeNS(m,"bullet-char")+"';",d(a,g,l,n));k=k.nextSibling}else{g=b(e,g,i).join(",");l="";if(k=h(i,p,"text-properties")){n="";n+=c(k,q);t=k.getAttributeNS(p,"text-underline-style");t==="solid"&&(n+="text-decoration: underline;");if(t=k.getAttributeNS(p,
-"font-name"))(t='"'+t+'"')&&(n+="font-family: "+t+";");l+=n}if(k=h(i,p,"paragraph-properties")){n=k;k="";k+=c(n,y);n=n.getElementsByTagNameNS(p,"background-image");if(n.length>0&&(t=n.item(0).getAttributeNS(j,"href")))k+="background-image: url('odfkit:"+t+"');",n=n.item(0),k+=c(n,r);l+=k}if(k=h(i,p,"graphic-properties"))n="",n+=c(k,C),l+=n;if(k=h(i,p,"table-cell-properties"))n="",n+=c(k,s),l+=n;if(l.length!==0)try{a.insertRule(g+"{"+l+"}",a.cssRules.length)}catch(R){throw R;}}for(var X in i.derivedStyles)i.derivedStyles.hasOwnProperty(X)&&
-f(a,e,X,i.derivedStyles[X])}var j="http://www.w3.org/1999/xlink",p="urn:oasis:names:tc:opendocument:xmlns:style:1.0",m="urn:oasis:names:tc:opendocument:xmlns:text:1.0",l={draw:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",fo:"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",office:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",presentation:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",style:p,svg:"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",table:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
-text:m,xlink:j},u={graphic:"draw",paragraph:"text",presentation:"presentation",ruby:"text",section:"text",table:"table","table-cell":"table","table-column":"table","table-row":"table",text:"text",list:"text"},n={graphic:"circle,connected,control,custom-shape,ellipse,frame,g,line,measure,page,page-thumbnail,path,polygon,polyline,rect,regular-polygon".split(","),paragraph:"alphabetical-index-entry-template,h,illustration-index-entry-template,index-source-style,object-index-entry-template,p,table-index-entry-template,table-of-content-entry-template,user-index-entry-template".split(","),
+odf.Style2CSS=function(){function g(a,b){var c={},d,f,e;if(!b)return c;for(d=b.firstChild;d;){d.namespaceURI===i&&"style"===d.localName?e=d.getAttributeNS(i,"family"):d.namespaceURI===j&&"list-style"===d.localName&&(e="list");if(f=e&&d.getAttributeNS&&d.getAttributeNS(i,"name"))c[e]||(c[e]={}),c[e][f]=d;d=d.nextSibling}return c}function m(a,b){if(!b||!a)return null;if(a[b])return a[b];var c,d;for(c in a)if(a.hasOwnProperty(c)&&(d=m(a[c].derivedStyles,b)))return d;return null}function e(a,b,c){var d=
+b[a],f,g;d&&(f=d.getAttributeNS(i,"parent-style-name"),g=null,f&&(g=m(c,f),!g&&b[f]&&(e(f,b,c),g=b[f],b[f]=null)),g?(g.derivedStyles||(g.derivedStyles={}),g.derivedStyles[a]=d):c[a]=d)}function k(a,b){for(var c in a)a.hasOwnProperty(c)&&(e(c,a,b),a[c]=null)}function a(a,b){var c=x[a],d;if(null===c)return null;d="["+c+'|style-name="'+b+'"]';"presentation"===c&&(c="draw",d='[presentation|style-name="'+b+'"]');return c+"|"+p[a].join(d+","+c+"|")+d}function c(b,d,f){var e=[],g,h;e.push(a(b,d));for(g in f.derivedStyles)if(f.derivedStyles.hasOwnProperty(g))for(h in d=
+c(b,g,f.derivedStyles[g]),d)d.hasOwnProperty(h)&&e.push(d[h]);return e}function b(a,b,c){if(!a)return null;for(a=a.firstChild;a;){if(a.namespaceURI===b&&a.localName===c)return b=a;a=a.nextSibling}return null}function d(a,b){var c="",d,f;for(d in b)b.hasOwnProperty(d)&&(d=b[d],(f=a.getAttributeNS(d[0],d[1]))&&(c+=d[2]+":"+f+";"));return c}function o(a,b,c,d){b='text|list[text|style-name="'+b+'"]';for(c=(c=c.getAttributeNS(j,"level"))&&parseInt(c,10);1<c;)b+=" > text|list-item > text|list",c-=1;try{a.insertRule(b+
+" > list-item:before{"+d+"}",a.cssRules.length)}catch(f){throw f;}}function f(a,e,g,k){if("list"===e)for(var n=k.firstChild,l,m;n;){if(n.namespaceURI===j)if(l=n,"list-level-style-number"===n.localName){m=l;var p=m.getAttributeNS(i,"num-format"),x=m.getAttributeNS(i,"num-suffix"),M="",M={1:"decimal",a:"lower-latin",A:"upper-latin",i:"lower-roman",I:"upper-roman"},F="",F=m.getAttributeNS(i,"num-prefix")||"",F=M.hasOwnProperty(p)?F+(" counter(list, "+M[p]+")"):p?F+("'"+p+"';"):F+" ''";x&&(F+=" '"+x+
+"'");m=M="content: "+F+";";o(a,g,l,m)}else"list-level-style-image"===n.localName?(m="content: none;",o(a,g,l,m)):"list-level-style-bullet"===n.localName&&(m="content: '"+l.getAttributeNS(j,"bullet-char")+"';",o(a,g,l,m));n=n.nextSibling}else{g=c(e,g,k).join(",");n="";if(l=b(k,i,"text-properties")){m=""+d(l,t);p=l.getAttributeNS(i,"text-underline-style");"solid"===p&&(m+="text-decoration: underline;");if(p=l.getAttributeNS(i,"font-name"))(p='"'+p+'"')&&(m+="font-family: "+p+";");n+=m}if(l=b(k,i,"paragraph-properties")){m=
+l;l=""+d(m,z);m=m.getElementsByTagNameNS(i,"background-image");if(0<m.length&&(p=m.item(0).getAttributeNS(h,"href")))l+="background-image: url('odfkit:"+p+"');",m=m.item(0),l+=d(m,r);n+=l}if(l=b(k,i,"graphic-properties"))l=""+d(l,s),n+=l;if(l=b(k,i,"table-cell-properties"))l=""+d(l,q),n+=l;if(0!==n.length)try{a.insertRule(g+"{"+n+"}",a.cssRules.length)}catch(R){throw R;}}for(var S in k.derivedStyles)k.derivedStyles.hasOwnProperty(S)&&f(a,e,S,k.derivedStyles[S])}var h="http://www.w3.org/1999/xlink",
+i="urn:oasis:names:tc:opendocument:xmlns:style:1.0",j="urn:oasis:names:tc:opendocument:xmlns:text:1.0",n={draw:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",fo:"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",office:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",presentation:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",style:i,svg:"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",table:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",text:j,xlink:h},x=
+{graphic:"draw",paragraph:"text",presentation:"presentation",ruby:"text",section:"text",table:"table","table-cell":"table","table-column":"table","table-row":"table",text:"text",list:"text"},p={graphic:"circle,connected,control,custom-shape,ellipse,frame,g,line,measure,page,page-thumbnail,path,polygon,polyline,rect,regular-polygon".split(","),paragraph:"alphabetical-index-entry-template,h,illustration-index-entry-template,index-source-style,object-index-entry-template,p,table-index-entry-template,table-of-content-entry-template,user-index-entry-template".split(","),
 presentation:"caption,circle,connector,control,custom-shape,ellipse,frame,g,line,measure,page-thumbnail,path,polygon,polyline,rect,regular-polygon".split(","),ruby:["ruby","ruby-text"],section:"alphabetical-index,bibliography,illustration-index,index-title,object-index,section,table-of-content,table-index,user-index".split(","),table:["background","table"],"table-cell":"body,covered-table-cell,even-columns,even-rows,first-column,first-row,last-column,last-row,odd-columns,odd-rows,table-cell".split(","),
-"table-column":["table-column"],"table-row":["table-row"],text:"a,index-entry-chapter,index-entry-link-end,index-entry-link-start,index-entry-page-number,index-entry-span,index-entry-tab-stop,index-entry-text,index-title-template,linenumbering-configuration,list-level-style-number,list-level-style-bullet,outline-level-style,span".split(","),list:["list-item"]},q=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","color","color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
-"background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-weight","font-weight"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-style","font-style"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-size","font-size"]],r=[[p,"repeat","background-repeat"]],y=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+"table-column":["table-column"],"table-row":["table-row"],text:"a,index-entry-chapter,index-entry-link-end,index-entry-link-start,index-entry-page-number,index-entry-span,index-entry-tab-stop,index-entry-text,index-title-template,linenumbering-configuration,list-level-style-number,list-level-style-bullet,outline-level-style,span".split(","),list:["list-item"]},t=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","color","color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+"background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-weight","font-weight"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-style","font-style"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-size","font-size"]],r=[[i,"repeat","background-repeat"]],z=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
 "text-align","text-align"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-left","padding-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-right","padding-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-top","padding-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-bottom","padding-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-left","border-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
 "border-right","border-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-top","border-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-bottom","border-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","margin-left","margin-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","margin-right","margin-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","margin-top","margin-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
-"margin-bottom","margin-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border","border"]],C=[["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill","background"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","min-height","min-height"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","stroke","border"],["urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
-"stroke-color","border-color"]],s=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-left","border-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-right","border-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-top","border-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-bottom","border-bottom"]];
-this.namespaces=l;this.namespaceResolver=function(a){return l[a]||null};this.namespaceResolver.lookupNamespaceURI=this.namespaceResolver;this.style2css=function(a,b,c){for(var d,e,h,j,r;a.cssRules.length;)a.deleteRule(a.cssRules.length-1);d=null;if(b)d=b.ownerDocument;if(c)d=c.ownerDocument;if(d){for(e in l)if(l.hasOwnProperty(e)){j="@namespace "+e+" url("+l[e]+");";try{a.insertRule(j,a.cssRules.length)}catch(q){}}b=i(d,b);d=i(d,c);for(r in u)if(u.hasOwnProperty(r))for(h in c={},g(b[r],c),g(d[r],
-c),c)c.hasOwnProperty(h)&&f(a,r,h,c[h])}}};
+"margin-bottom","margin-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border","border"]],s=[["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill","background"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","min-height","min-height"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","stroke","border"],["urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
+"stroke-color","border-color"]],q=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-left","border-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-right","border-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-top","border-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-bottom","border-bottom"]];
+this.namespaces=n;this.namespaceResolver=function(a){return n[a]||null};this.namespaceResolver.lookupNamespaceURI=this.namespaceResolver;this.style2css=function(a,b,c){for(var d,e,h,i,j;a.cssRules.length;)a.deleteRule(a.cssRules.length-1);d=null;b&&(d=b.ownerDocument);c&&(d=c.ownerDocument);if(d){for(e in n)if(n.hasOwnProperty(e)){i="@namespace "+e+" url("+n[e]+");";try{a.insertRule(i,a.cssRules.length)}catch(o){}}b=g(d,b);e=g(d,c);c={};for(j in x)if(x.hasOwnProperty(j))for(h in d=c[j]={},k(b[j],
+d),k(e[j],d),d)d.hasOwnProperty(h)&&f(a,j,h,d[h])}}};
 // Input 22
 runtime.loadClass("core.Base64");runtime.loadClass("xmldom.XPath");runtime.loadClass("odf.Style2CSS");
-odf.FontLoader=function(){function i(b,e,c,d,f){var g,k=0,m;for(m in b)b.hasOwnProperty(m)&&(k===c&&(g=m),k+=1);if(!g)return f();e.load(b[g].href,function(k,m){if(k)runtime.log(k);else{var n=d,n=document.styleSheets[0],q='@font-face { font-family: "'+g+'"; src: url(data:application/x-font-ttf;charset=binary;base64,'+a.convertUTF8ArrayToBase64(m)+') format("truetype"); }';try{n.insertRule(q,n.cssRules.length)}catch(r){runtime.log("Problem inserting rule in CSS: "+q)}}return i(b,e,c+1,d,f)})}function k(a,
-e,c){i(a,e,0,c,function(){})}var e=new odf.Style2CSS,g=new xmldom.XPath,a=new core.Base64;odf.FontLoader=function(){this.loadFonts=function(a,h,c){var d={},f,i,p;if(a){a=g.getODFElementsWithXPath(a,"style:font-face[svg:font-face-src]",e.namespaceResolver);for(f=0;f<a.length;f+=1)i=a[f],p=i.getAttributeNS(e.namespaces.style,"name"),i=g.getODFElementsWithXPath(i,"svg:font-face-src/svg:font-face-uri",e.namespaceResolver),i.length>0&&(i=i[0].getAttributeNS(e.namespaces.xlink,"href"),d[p]={href:i})}k(d,
-h,c)}};return odf.FontLoader}();
+odf.FontLoader=function(){function g(c,b,d,e,f){var h,i=0,j;for(j in c)c.hasOwnProperty(j)&&(i===d&&(h=j),i+=1);if(!h)return f();b.load(c[h].href,function(i,j){if(i)runtime.log(i);else{var k=e,k=document.styleSheets[0],m='@font-face { font-family: "'+h+'"; src: url(data:application/x-font-ttf;charset=binary;base64,'+a.convertUTF8ArrayToBase64(j)+') format("truetype"); }';try{k.insertRule(m,k.cssRules.length)}catch(r){runtime.log("Problem inserting rule in CSS: "+m)}}return g(c,b,d+1,e,f)})}function m(a,
+b,d){g(a,b,0,d,function(){})}var e=new odf.Style2CSS,k=new xmldom.XPath,a=new core.Base64;odf.FontLoader=function(){this.loadFonts=function(a,b,d){var g={},f,h,i;if(a){a=k.getODFElementsWithXPath(a,"style:font-face[svg:font-face-src]",e.namespaceResolver);for(f=0;f<a.length;f+=1)h=a[f],i=h.getAttributeNS(e.namespaces.style,"name"),h=k.getODFElementsWithXPath(h,"svg:font-face-src/svg:font-face-uri",e.namespaceResolver),0<h.length&&(h=h[0].getAttributeNS(e.namespaces.xlink,"href"),g[i]={href:h})}m(g,
+b,d)}};return odf.FontLoader}();
 // Input 23
 runtime.loadClass("core.Base64");runtime.loadClass("core.Zip");runtime.loadClass("xmldom.LSSerializer");runtime.loadClass("odf.StyleInfo");runtime.loadClass("odf.Style2CSS");runtime.loadClass("odf.FontLoader");
-odf.OdfContainer=function(){function i(a,b,c){for(a=a?a.firstChild:null;a;){if(a.localName===c&&a.namespaceURI===b)return a;a=a.nextSibling}return null}function k(a){var b,c=p.length;for(b=0;b<c;b+=1)if(a.namespaceURI===f&&a.localName===p[b])return b;return-1}function e(a,b){var d=a.automaticStyles,f;b&&(f=new c.UsedKeysList(b));this.acceptNode=function(a){if(a.namespaceURI==="http://www.w3.org/1999/xhtml")return 3;else if(f&&a.parentNode===d&&a.nodeType===1)return f.uses(a)?1:2;return 1}}function g(a,
-b){if(b){var c=k(b),d,f=a.firstChild;if(c!==-1){for(;f;){d=k(f);if(d!==-1&&d>c)break;f=f.nextSibling}a.insertBefore(b,f)}}}function a(a){this.OdfContainer=a}function b(a,b,c){var d=this,f;this.size=0;this.type=null;this.name=a;this.container=b;this.onchange=this.onreadystatechange=this.document=this.url=null;this.EMPTY=0;this.LOADING=1;this.DONE=2;this.state=this.EMPTY;this.load=function(){c.load(a,function(b,c){f=c;d.url=null;if(f){var e=0,g=u[a];g||(g=f[1]===80&&f[2]===78&&f[3]===71?"image/png":
-f[0]===255&&f[1]===216&&f[2]===255?"image/jpeg":f[0]===71&&f[1]===73&&f[2]===70?"image/gif":"");for(d.url="data:"+g+";base64,";e<f.length;)d.url+=m.convertUTF8ArrayToBase64(f.slice(e,Math.min(e+45E3,f.length))),e+=45E3}if(d.onchange)d.onchange(d);if(d.onstatereadychange)d.onstatereadychange(d)})};this.abort=function(){}}function h(){this.length=0;this.item=function(){}}var c=new odf.StyleInfo,d=new odf.Style2CSS,f="urn:oasis:names:tc:opendocument:xmlns:office:1.0",j="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
-p="meta,settings,scripts,font-face-decls,styles,automatic-styles,master-styles,body".split(","),m=new core.Base64,l=new odf.FontLoader,u={};a.prototype=new function(){};a.prototype.constructor=a;a.namespaceURI=f;a.localName="document";b.prototype.load=function(){};b.prototype.getUrl=function(){return this.data?"data:;base64,"+m.toBase64(this.data):null};odf.OdfContainer=function q(c,k){function m(a){for(var b=a.firstChild,c;b;)c=b.nextSibling,b.nodeType===1?m(b):b.nodeType===7&&a.removeChild(b),b=
-c}function s(a){var b=A.rootElement.ownerDocument,c;if(a){m(a.documentElement);try{c=b.importNode(a.documentElement,true)}catch(d){}}return c}function p(a){A.state=a;if(A.onchange)A.onchange(A);if(A.onstatereadychange)A.onstatereadychange(A)}function E(a){var a=s(a),b=A.rootElement;!a||a.localName!=="document-styles"||a.namespaceURI!==f?p(q.INVALID):(b.fontFaceDecls=i(a,f,"font-face-decls"),g(b,b.fontFaceDecls),b.styles=i(a,f,"styles"),g(b,b.styles),b.automaticStyles=i(a,f,"automatic-styles"),g(b,
-b.automaticStyles),b.masterStyles=i(a,f,"master-styles"),g(b,b.masterStyles),l.loadFonts(b.fontFaceDecls,J,null))}function B(a){var a=s(a),b,c,d;if(!a||a.localName!=="document-content"||a.namespaceURI!==f)p(q.INVALID);else{b=A.rootElement;c=i(a,f,"font-face-decls");if(b.fontFaceDecls&&c)for(d=c.firstChild;d;)b.fontFaceDecls.appendChild(d),d=c.firstChild;else if(c)b.fontFaceDecls=c,g(b,c);c=i(a,f,"automatic-styles");if(b.automaticStyles&&c)for(d=c.firstChild;d;)b.automaticStyles.appendChild(d),d=c.firstChild;
-else if(c)b.automaticStyles=c,g(b,c);b.body=i(a,f,"body");g(b,b.body)}}function z(a){var a=s(a),b;if(a&&!(a.localName!=="document-meta"||a.namespaceURI!==f))b=A.rootElement,b.meta=i(a,f,"meta"),g(b,b.meta)}function G(a){var a=s(a),b;if(a&&!(a.localName!=="document-settings"||a.namespaceURI!==f))b=A.rootElement,b.settings=i(a,f,"settings"),g(b,b.settings)}function o(a,b){J.load(a,function(a,c){if(a)b(a,null);else{var d=runtime.byteArrayToString(c,"utf8"),d=(new DOMParser).parseFromString(d,"text/xml");
-b(null,d)}})}function x(){o("styles.xml",function(a,b){E(b);A.state!==q.INVALID&&o("content.xml",function(a,b){B(b);A.state!==q.INVALID&&o("meta.xml",function(a,b){z(b);A.state!==q.INVALID&&o("settings.xml",function(a,b){b&&G(b);o("META-INF/manifest.xml",function(a,b){if(b){var c=s(b),d;if(c&&!(c.localName!=="manifest"||c.namespaceURI!==j)){d=A.rootElement;d.manifest=c;for(c=d.manifest.firstChild;c;)c.nodeType===1&&c.localName==="file-entry"&&c.namespaceURI===j&&(u[c.getAttributeNS(j,"full-path")]=
-c.getAttributeNS(j,"media-type")),c=c.nextSibling}}A.state!==q.INVALID&&p(q.DONE)})})})})})}function t(a,b){var c="",d;for(d in b)b.hasOwnProperty(d)&&(c+=" xmlns:"+d+'="'+b[d]+'"');return'<?xml version="1.0" encoding="UTF-8"?><office:'+a+" "+c+' office:version="1.2">'}function w(){var a=d.namespaces,b=new xmldom.LSSerializer,c=t("document-meta",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.meta,a);c+="</office:document-meta>";return c}function L(){var a=d.namespaces,b=new xmldom.LSSerializer,
-c=t("document-settings",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.settings,a);c+="</office:document-settings>";return c}function Q(){var a=d.namespaces,b=new xmldom.LSSerializer,c=t("document-styles",a);b.filter=new e(A.rootElement,A.rootElement.masterStyles);c+=b.writeToString(A.rootElement.fontFaceDecls,a);c+=b.writeToString(A.rootElement.styles,a);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.masterStyles,a);c+="</office:document-styles>";
-return c}function R(){var a=d.namespaces,b=new xmldom.LSSerializer,c=t("document-content",a);b.filter=new e(A.rootElement,A.rootElement.body);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.body,a);c+="</office:document-content>";return c}function X(a,b){runtime.loadXML(a,function(a,c){if(a)b(a);else{var d=s(c);!d||d.localName!=="document"||d.namespaceURI!==f?p(q.INVALID):(A.rootElement=d,d.fontFaceDecls=i(d,f,"font-face-decls"),d.styles=i(d,f,"styles"),d.automaticStyles=
-i(d,f,"automatic-styles"),d.masterStyles=i(d,f,"master-styles"),d.body=i(d,f,"body"),d.meta=i(d,f,"meta"),p(q.DONE))}})}var A=this,J=null;this.onstatereadychange=k;this.parts=this.rootElement=this.state=this.onchange=null;this.getPart=function(a){return new b(a,A,J)};this.save=function(a){var b;b=runtime.byteArrayFromString(L(),"utf8");J.save("settings.xml",b,true,new Date);b=runtime.byteArrayFromString(w(),"utf8");J.save("meta.xml",b,true,new Date);b=runtime.byteArrayFromString(Q(),"utf8");J.save("styles.xml",
-b,true,new Date);b=runtime.byteArrayFromString(R(),"utf8");J.save("content.xml",b,true,new Date);J.write(function(b){a(b)})};this.state=q.LOADING;this.rootElement=function(a){var b=document.createElementNS(a.namespaceURI,a.localName),c,a=new a;for(c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}(a);this.parts=new h(this);J=new core.Zip(c,function(a,b){J=b;a?X(c,function(b){if(a)J.error=a+"\n"+b,p(q.INVALID)}):x()})};odf.OdfContainer.EMPTY=0;odf.OdfContainer.LOADING=1;odf.OdfContainer.DONE=2;odf.OdfContainer.INVALID=
-3;odf.OdfContainer.SAVING=4;odf.OdfContainer.MODIFIED=5;odf.OdfContainer.getContainer=function(a){return new odf.OdfContainer(a,null)};return odf.OdfContainer}();
+odf.OdfContainer=function(){function g(a,b,c){for(a=a?a.firstChild:null;a;){if(a.localName===c&&a.namespaceURI===b)return a;a=a.nextSibling}return null}function m(a){var b,c=i.length;for(b=0;b<c;b+=1)if(a.namespaceURI===f&&a.localName===i[b])return b;return-1}function e(a,b){var c=a.automaticStyles,f;b&&(f=new d.UsedKeysList(b));this.acceptNode=function(a){return"http://www.w3.org/1999/xhtml"===a.namespaceURI?3:f&&a.parentNode===c&&1===a.nodeType?f.uses(a)?1:2:1}}function k(a,b){if(b){var c=m(b),
+d,f=a.firstChild;if(-1!==c){for(;f;){d=m(f);if(-1!==d&&d>c)break;f=f.nextSibling}a.insertBefore(b,f)}}}function a(a){this.OdfContainer=a}function c(a,b,c){var d=this;this.size=0;this.type=null;this.name=a;this.container=b;this.onchange=this.onreadystatechange=this.document=this.url=null;this.EMPTY=0;this.LOADING=1;this.DONE=2;this.state=this.EMPTY;this.load=function(){c.loadAsDataURL(a,x[a],function(a,b){d.url=b;if(d.onchange)d.onchange(d);if(d.onstatereadychange)d.onstatereadychange(d)})};this.abort=
+function(){}}function b(){this.length=0;this.item=function(){}}var d=new odf.StyleInfo,o=new odf.Style2CSS,f="urn:oasis:names:tc:opendocument:xmlns:office:1.0",h="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",i="meta,settings,scripts,font-face-decls,styles,automatic-styles,master-styles,body".split(","),j=new core.Base64,n=new odf.FontLoader,x={};a.prototype=new function(){};a.prototype.constructor=a;a.namespaceURI=f;a.localName="document";c.prototype.load=function(){};c.prototype.getUrl=function(){return this.data?
+"data:;base64,"+j.toBase64(this.data):null};odf.OdfContainer=function t(d,i){function j(a){for(var b=a.firstChild,c;b;)c=b.nextSibling,1===b.nodeType?j(b):7===b.nodeType&&a.removeChild(b),b=c}function q(a){var b=A.rootElement.ownerDocument,c;if(a){j(a.documentElement);try{c=b.importNode(a.documentElement,!0)}catch(d){}}return c}function m(a){A.state=a;if(A.onchange)A.onchange(A);if(A.onstatereadychange)A.onstatereadychange(A)}function E(a){var a=q(a),b=A.rootElement;!a||"document-styles"!==a.localName||
+a.namespaceURI!==f?m(t.INVALID):(b.fontFaceDecls=g(a,f,"font-face-decls"),k(b,b.fontFaceDecls),b.styles=g(a,f,"styles"),k(b,b.styles),b.automaticStyles=g(a,f,"automatic-styles"),k(b,b.automaticStyles),b.masterStyles=g(a,f,"master-styles"),k(b,b.masterStyles),n.loadFonts(b.fontFaceDecls,K,null))}function C(a){var a=q(a),b,c,d;if(!a||"document-content"!==a.localName||a.namespaceURI!==f)m(t.INVALID);else{b=A.rootElement;c=g(a,f,"font-face-decls");if(b.fontFaceDecls&&c)for(d=c.firstChild;d;)b.fontFaceDecls.appendChild(d),
+d=c.firstChild;else c&&(b.fontFaceDecls=c,k(b,c));c=g(a,f,"automatic-styles");if(b.automaticStyles&&c)for(d=c.firstChild;d;)b.automaticStyles.appendChild(d),d=c.firstChild;else c&&(b.automaticStyles=c,k(b,c));b.body=g(a,f,"body");k(b,b.body)}}function w(a){var a=q(a),b;if(a&&!("document-meta"!==a.localName||a.namespaceURI!==f))b=A.rootElement,b.meta=g(a,f,"meta"),k(b,b.meta)}function G(a){var a=q(a),b;if(a&&!("document-settings"!==a.localName||a.namespaceURI!==f))b=A.rootElement,b.settings=g(a,f,
+"settings"),k(b,b.settings)}function l(a,b){K.loadAsDOM(a,b)}function v(){l("styles.xml",function(a,b){E(b);A.state!==t.INVALID&&l("content.xml",function(a,b){C(b);A.state!==t.INVALID&&l("meta.xml",function(a,b){w(b);A.state!==t.INVALID&&l("settings.xml",function(a,b){b&&G(b);l("META-INF/manifest.xml",function(a,b){if(b){var c=q(b),d;if(c&&!("manifest"!==c.localName||c.namespaceURI!==h)){d=A.rootElement;d.manifest=c;for(c=d.manifest.firstChild;c;)1===c.nodeType&&"file-entry"===c.localName&&c.namespaceURI===
+h&&(x[c.getAttributeNS(h,"full-path")]=c.getAttributeNS(h,"media-type")),c=c.nextSibling}}A.state!==t.INVALID&&m(t.DONE)})})})})})}function y(a,b){var c="",d;for(d in b)b.hasOwnProperty(d)&&(c+=" xmlns:"+d+'="'+b[d]+'"');return'<?xml version="1.0" encoding="UTF-8"?><office:'+a+" "+c+' office:version="1.2">'}function B(){var a=o.namespaces,b=new xmldom.LSSerializer,c=y("document-meta",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.meta,a);return c+"</office:document-meta>"}function M(){var a=
+o.namespaces,b=new xmldom.LSSerializer,c=y("document-settings",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.settings,a);return c+"</office:document-settings>"}function F(){var a=o.namespaces,b=new xmldom.LSSerializer,c=y("document-styles",a);b.filter=new e(A.rootElement,A.rootElement.masterStyles);c+=b.writeToString(A.rootElement.fontFaceDecls,a);c+=b.writeToString(A.rootElement.styles,a);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.masterStyles,
+a);return c+"</office:document-styles>"}function R(){var a=o.namespaces,b=new xmldom.LSSerializer,c=y("document-content",a);b.filter=new e(A.rootElement,A.rootElement.body);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.body,a);return c+"</office:document-content>"}function S(a,b){runtime.loadXML(a,function(a,c){if(a)b(a);else{var d=q(c);!d||"document"!==d.localName||d.namespaceURI!==f?m(t.INVALID):(A.rootElement=d,d.fontFaceDecls=g(d,f,"font-face-decls"),d.styles=
+g(d,f,"styles"),d.automaticStyles=g(d,f,"automatic-styles"),d.masterStyles=g(d,f,"master-styles"),d.body=g(d,f,"body"),d.meta=g(d,f,"meta"),m(t.DONE))}})}var A=this,K=null;this.onstatereadychange=i;this.parts=this.rootElement=this.state=this.onchange=null;this.getPart=function(a){return new c(a,A,K)};this.save=function(a){var b;b=runtime.byteArrayFromString(M(),"utf8");K.save("settings.xml",b,!0,new Date);b=runtime.byteArrayFromString(B(),"utf8");K.save("meta.xml",b,!0,new Date);b=runtime.byteArrayFromString(F(),
+"utf8");K.save("styles.xml",b,!0,new Date);b=runtime.byteArrayFromString(R(),"utf8");K.save("content.xml",b,!0,new Date);K.write(function(b){a(b)})};this.state=t.LOADING;this.rootElement=function(a){var b=document.createElementNS(a.namespaceURI,a.localName),c,a=new a;for(c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}(a);this.parts=new b(this);K=new core.Zip(d,function(a,b){K=b;a?S(d,function(b){a&&(K.error=a+"\n"+b,m(t.INVALID))}):v()})};odf.OdfContainer.EMPTY=0;odf.OdfContainer.LOADING=1;odf.OdfContainer.DONE=
+2;odf.OdfContainer.INVALID=3;odf.OdfContainer.SAVING=4;odf.OdfContainer.MODIFIED=5;odf.OdfContainer.getContainer=function(a){return new odf.OdfContainer(a,null)};return odf.OdfContainer}();
 // Input 24
-odf.Formatting=function(){function i(e){function g(a,e){for(var c=a&&a.firstChild;c&&e;)c=c.nextSibling,e-=1;return c}var a=g(e.startContainer,e.startOffset);g(e.endContainer,e.endOffset);this.next=function(){return a===null?a:null}}var k=new odf.StyleInfo;this.setOdfContainer=function(){};this.isCompletelyBold=function(){return false};this.getAlignment=function(e){this.getParagraphStyles(e)};this.getParagraphStyles=function(e){var g,a,b,h=[];for(g=0;g<e.length;g+=0){a=void 0;b=[];for(a=(new i(e[g])).next();a;)k.canElementHaveStyle("paragraph",
-a)&&b.push(a);for(a=0;a<b.length;a+=1)h.indexOf(b[a])===-1&&h.push(b[a])}return h};this.getTextStyles=function(){return[]}};
+odf.Formatting=function(){function g(e){function g(a,b){for(var d=a&&a.firstChild;d&&b;)d=d.nextSibling,b-=1;return d}var a=g(e.startContainer,e.startOffset);g(e.endContainer,e.endOffset);this.next=function(){return null===a?a:null}}var m=new odf.StyleInfo;this.setOdfContainer=function(){};this.isCompletelyBold=function(){return!1};this.getAlignment=function(e){this.getParagraphStyles(e)};this.getParagraphStyles=function(e){var k,a,c,b=[];for(k=0;k<e.length;k+=0){a=void 0;c=[];for(a=(new g(e[k])).next();a;)m.canElementHaveStyle("paragraph",
+a)&&c.push(a);for(a=0;a<c.length;a+=1)-1===b.indexOf(c[a])&&b.push(c[a])}return b};this.getTextStyles=function(){return[]}};
 // Input 25
 runtime.loadClass("odf.OdfContainer");runtime.loadClass("odf.Formatting");runtime.loadClass("xmldom.XPath");
-odf.OdfCanvas=function(){function i(a,b,c){a.addEventListener?a.addEventListener(b,c,false):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function k(a){function b(a,c){for(;c;){if(c===a)return true;c=c.parentNode}return false}function c(){var e=[],g=runtime.getWindow().getSelection(),h,i;for(h=0;h<g.rangeCount;h+=1)i=g.getRangeAt(h),i!==null&&b(a,i.startContainer)&&b(a,i.endContainer)&&e.push(i);if(e.length===d.length){for(g=0;g<e.length;g+=1)if(h=e[g],i=d[g],h=h===i?false:h===null||i===null?
-true:h.startContainer!==i.startContainer||h.startOffset!==i.startOffset||h.endContainer!==i.endContainer||h.endOffset!==i.endOffset,h)break;if(g===e.length)return}d=e;var g=Array(e.length),j,k=a.ownerDocument;for(h=0;h<e.length;h+=1)i=e[h],j=k.createRange(),j.setStart(i.startContainer,i.startOffset),j.setEnd(i.endContainer,i.endOffset),g[h]=j;d=g;g=f.length;for(e=0;e<g;e+=1)f[e](a,d)}var d=[],f=[];this.addListener=function(a,b){var c,d=f.length;for(c=0;c<d;c+=1)if(f[c]===b)return;f.push(b)};i(a,"mouseup",
-c);i(a,"keyup",c);i(a,"keydown",c)}function e(a){for(a=a.firstChild;a;){if(a.namespaceURI===f&&a.localName==="binary-data")return"data:image/png;base64,"+a.textContent;a=a.nextSibling}return""}function g(a,b,c,d){function f(b){b='draw|image[styleid="'+a+'"] {'+("background-image: url("+b+");")+"}";d.insertRule(b,d.cssRules.length)}c.setAttribute("styleid",a);var g=c.getAttributeNS(m,"href"),h;if(g)try{b.getPartUrl?(g=b.getPartUrl(g),f(g)):(h=b.getPart(g),h.onchange=function(a){f(a.url)},h.load())}catch(i){runtime.log("slight problem: "+
-i)}else g=e(c),f(g)}function a(a){var b=a.getElementsByTagName("style"),c=a.getElementsByTagName("head")[0],d="",f,b=b&&b.length>0?b[0].cloneNode(false):a.createElement("style");for(f in h)h.hasOwnProperty(f)&&f&&(d+="@namespace "+f+" url("+h[f]+");\n");b.appendChild(a.createTextNode(d));c.appendChild(b);return b}var b=new odf.Style2CSS,h=b.namespaces,c=h.draw,d=h.fo,f=h.office,j=h.svg,p=h.text,m=h.xlink,l=runtime.getWindow(),u=new xmldom.XPath,n={},q;odf.OdfCanvas=function(f){function e(a){function h(){for(var e=
-f;e.firstChild;)e.removeChild(e.firstChild);f.style.display="inline-block";f.style.background="white";e=a.rootElement;f.ownerDocument.importNode(e,true);E.setOdfContainer(a);var i=G;(new odf.Style2CSS).style2css(i.sheet,e.styles,e.automaticStyles);var i=o.sheet,k=a,q=e.body,l,m,s;m=[];for(l=q.firstChild;l&&l!==q;)if(l.namespaceURI===c&&(m[m.length]=l),l.firstChild)l=l.firstChild;else{for(;l&&l!==q&&!l.nextSibling;)l=l.parentNode;if(l&&l.nextSibling)l=l.nextSibling}for(s=0;s<m.length;s+=1){l=m[s];
-var v="frame"+String(s),y=i;l.setAttribute("styleid",v);var w=void 0,C=l.getAttributeNS(p,"anchor-type"),x=l.getAttributeNS(j,"x"),z=l.getAttributeNS(j,"y"),B=l.getAttributeNS(j,"width"),O=l.getAttributeNS(j,"height"),T=l.getAttributeNS(d,"min-height"),N=l.getAttributeNS(d,"min-width");if(C==="as-char")w="display: inline-block;";else if(C||x||z)w="position: absolute;";else if(B||O||T||N)w="display: block;";x&&(w+="left: "+x+";");z&&(w+="top: "+z+";");B&&(w+="width: "+B+";");O&&(w+="height: "+O+";");
-T&&(w+="min-height: "+T+";");N&&(w+="min-width: "+N+";");w&&(w="draw|"+l.localName+'[styleid="'+v+'"] {'+w+"}",y.insertRule(w,y.cssRules.length))}m=q.getElementsByTagNameNS(c,"image");for(s=0;s<m.length;s+=1)l=m.item(s),g("image"+String(s),k,l,i);s=u.getODFElementsWithXPath(q,".//*[*[@text:anchor-type='paragraph']]",b.namespaceResolver);for(q=0;q<s.length;q+=1)k=s[q],k.setAttributeNS&&k.setAttributeNS("urn:webodf","containsparagraphanchor",true);i.insertRule("office|presentation draw|page:nth-child(1n) { display:block; }",
-i.cssRules.length);i.insertRule("draw|page { background-color:#fff; }",i.cssRules.length);for(i=f;i.firstChild;)i.removeChild(i.firstChild);f.appendChild(e);if(n.hasOwnProperty("statereadychange")){e=n.statereadychange;for(i=0;i<e.length;i+=1)e[i](void 0)}}if(v===a)v.state===odf.OdfContainer.DONE?h():v.onchange=h}function h(){if(q){for(var a=q.ownerDocument.createDocumentFragment();q.firstChild;)a.insertBefore(q.firstChild,null);q.parentNode.replaceChild(a,q)}}var m=f.ownerDocument,v,E=new odf.Formatting,
-B=new k(f),z=a(m),G=a(m),o=a(m),x=false;this.odfContainer=function(){return v};this.slidevisibilitycss=function(){return z};this.load=this.load=function(a){f.innerHTML="loading "+a;v=new odf.OdfContainer(a,function(a){v=a;e(a)});v.onstatereadychange=e};this.save=function(a){h();v.save(a)};this.setEditable=function(a){(x=a)||h()};this.addListener=function(a,b){if(a==="selectionchange")B.addListener(a,b);else{var c=n[a];c===void 0&&(c=n[a]=[]);c.push(b)}};this.getFormatting=function(){return E};i(f,
-"click",function(a){for(var a=a||l.event,b=a.target,c=l.getSelection(),d=c.getRangeAt(0),f=d&&d.startContainer,e=d&&d.startOffset,g=d&&d.endContainer,i=d&&d.endOffset;b&&!((b.localName==="p"||b.localName==="h")&&b.namespaceURI===p);)b=b.parentNode;if(x&&b&&b.parentNode!==q)q?q.parentNode&&h():(q=b.ownerDocument.createElement("p"),q.style||(q=b.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml","p")),q.style.margin="0px",q.style.padding="0px",q.style.border="0px",q.setAttribute("contenteditable",
-true)),b.parentNode.replaceChild(q,b),q.appendChild(b),q.focus(),d&&(c.removeAllRanges(),d=b.ownerDocument.createRange(),d.setStart(f,e),d.setEnd(g,i),c.addRange(d)),a.preventDefault?(a.preventDefault(),a.stopPropagation()):(a.returnValue=false,a.cancelBubble=true)})};return odf.OdfCanvas}();
+odf.OdfCanvas=function(){function g(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function m(a){function b(a,c){for(;c;){if(c===a)return!0;c=c.parentNode}return!1}function c(){var e=[],g=runtime.getWindow().getSelection(),h,i;for(h=0;h<g.rangeCount;h+=1)i=g.getRangeAt(h),null!==i&&b(a,i.startContainer)&&b(a,i.endContainer)&&e.push(i);if(e.length===d.length){for(g=0;g<e.length&&!(h=e[g],i=d[g],h=h===i?!1:null===h||null===i?!0:h.startContainer!==
+i.startContainer||h.startOffset!==i.startOffset||h.endContainer!==i.endContainer||h.endOffset!==i.endOffset,h);g+=1);if(g===e.length)return}d=e;var g=[e.length],j,k=a.ownerDocument;for(h=0;h<e.length;h+=1)i=e[h],j=k.createRange(),j.setStart(i.startContainer,i.startOffset),j.setEnd(i.endContainer,i.endOffset),g[h]=j;d=g;g=f.length;for(e=0;e<g;e+=1)f[e](a,d)}var d=[],f=[];this.addListener=function(a,b){var c,d=f.length;for(c=0;c<d;c+=1)if(f[c]===b)return;f.push(b)};g(a,"mouseup",c);g(a,"keyup",c);g(a,
+"keydown",c)}function e(a){for(a=a.firstChild;a;){if(a.namespaceURI===h&&"binary-data"===a.localName)return"data:image/png;base64,"+a.textContent;a=a.nextSibling}return""}function k(a,b,c,d){function f(b){b='draw|image[styleid="'+a+'"] {'+("background-image: url("+b+");")+"}";d.insertRule(b,d.cssRules.length)}c.setAttribute("styleid",a);var g=c.getAttributeNS(n,"href"),h;if(g)try{b.getPartUrl?(g=b.getPartUrl(g),f(g)):(h=b.getPart(g),h.onchange=function(a){f(a.url)},h.load())}catch(i){runtime.log("slight problem: "+
+i)}else g=e(c),f(g)}function a(a,b,c){function d(a,b,c,f){z.addToQueue(function(){k(a,b,c,f)})}var f,e;f=b.getElementsByTagNameNS(o,"image");for(b=0;b<f.length;b+=1)e=f.item(b),d("image"+b,a,e,c)}function c(a){var b=a.getElementsByTagName("style"),c=a.getElementsByTagName("head")[0],f="",e,b=b&&0<b.length?b[0].cloneNode(!1):a.createElement("style");for(e in d)d.hasOwnProperty(e)&&e&&(f+="@namespace "+e+" url("+d[e]+");\n");b.appendChild(a.createTextNode(f));c.appendChild(b);return b}var b=new odf.Style2CSS,
+d=b.namespaces,o=d.draw,f=d.fo,h=d.office,i=d.svg,j=d.text,n=d.xlink,x=runtime.getWindow(),p=new xmldom.XPath,t={},r,z=new function(){function a(d){c=!0;runtime.setTimeout(function(){try{d()}catch(f){runtime.log(f)}c=!1;0<b.length&&a(b.pop())},10)}var b=[],c=!1;this.clearQueue=function(){b.length=0};this.addToQueue=function(d){if(0===b.length&&!c)return a(d);b.push(d)}};odf.OdfCanvas=function(d){function e(){var a=d.firstChild.firstChild;a&&(d.style.WebkitTransform="scale("+F+")",d.style.WebkitTransformOrigin=
+"left top",d.style.width=Math.round(F*a.offsetWidth)+"px",d.style.height=Math.round(F*a.offsetHeight)+"px")}function h(c){function g(){for(var h=d;h.firstChild;)h.removeChild(h.firstChild);d.style.display="inline-block";h=c.rootElement;d.ownerDocument.importNode(h,!0);G.setOdfContainer(c);var k=y;(new odf.Style2CSS).style2css(k.sheet,h.styles,h.automaticStyles);var k=c,m=B.sheet,l;l=h.body;var r,z,u;z=[];for(r=l.firstChild;r&&r!==l;)if(r.namespaceURI===o&&(z[z.length]=r),r.firstChild)r=r.firstChild;
+else{for(;r&&r!==l&&!r.nextSibling;)r=r.parentNode;r&&r.nextSibling&&(r=r.nextSibling)}for(u=0;u<z.length;u+=1){r=z[u];var w="frame"+u,x=m;r.setAttribute("styleid",w);var v=void 0,S=r.getAttributeNS(j,"anchor-type"),E=r.getAttributeNS(i,"x"),F=r.getAttributeNS(i,"y"),M=r.getAttributeNS(i,"width"),Q=r.getAttributeNS(i,"height"),$=r.getAttributeNS(f,"min-height"),W=r.getAttributeNS(f,"min-width");if("as-char"===S)v="display: inline-block;";else if(S||E||F)v="position: absolute;";else if(M||Q||$||W)v=
+"display: block;";E&&(v+="left: "+E+";");F&&(v+="top: "+F+";");M&&(v+="width: "+M+";");Q&&(v+="height: "+Q+";");$&&(v+="min-height: "+$+";");W&&(v+="min-width: "+W+";");v&&(v="draw|"+r.localName+'[styleid="'+w+'"] {'+v+"}",x.insertRule(v,x.cssRules.length))}u=p.getODFElementsWithXPath(l,".//*[*[@text:anchor-type='paragraph']]",b.namespaceResolver);for(z=0;z<u.length;z+=1)l=u[z],l.setAttributeNS&&l.setAttributeNS("urn:webodf","containsparagraphanchor",!0);m.insertRule("office|presentation draw|page:nth-child(1n) {display:block;}",
+m.cssRules.length);m.insertRule("draw|page { background-color:#fff; }",m.cssRules.length);for(l=d;l.firstChild;)l.removeChild(l.firstChild);l=n.createElement("div");l.style.display="inline-block";l.style.background="white";l.appendChild(h);d.appendChild(l);a(k,h.body,m);e();if(t.hasOwnProperty("statereadychange")){h=t.statereadychange;for(k=0;k<h.length;k+=1)h[k](void 0)}}w===c&&(w.state===odf.OdfContainer.DONE?g():w.onchange=g)}function k(){if(r){for(var a=r.ownerDocument.createDocumentFragment();r.firstChild;)a.insertBefore(r.firstChild,
+null);r.parentNode.replaceChild(a,r)}}var n=d.ownerDocument,w,G=new odf.Formatting,l=new m(d),v=c(n),y=c(n),B=c(n),M=!1,F=1;this.odfContainer=function(){return w};this.slidevisibilitycss=function(){return v};this.load=this.load=function(a){z.clearQueue();d.innerHTML="loading "+a;w=new odf.OdfContainer(a,function(a){w=a;h(a)});w.onstatereadychange=h};this.save=function(a){k();w.save(a)};this.setEditable=function(a){(M=a)||k()};this.addListener=function(a,b){if("selectionchange"===a)l.addListener(a,
+b);else{var c=t[a];void 0===c&&(c=t[a]=[]);b&&-1===c.indexOf(b)&&c.push(b)}};this.getFormatting=function(){return G};this.setZoomLevel=function(a){F=a;e()};this.getZoomLevel=function(){return F};this.fitToContainingElement=function(a,b){var c=d.offsetHeight/F;F=a/(d.offsetWidth/F);b/c<F&&(F=b/c);e()};this.fitToWidth=function(a){F=a/(d.offsetWidth/F);e()};this.fitToHeight=function(a){F=a/(d.offsetHeight/F);e()};g(d,"click",function(a){for(var a=a||x.event,b=a.target,c=x.getSelection(),d=0<c.rangeCount?
+c.getRangeAt(0):null,f=d&&d.startContainer,e=d&&d.startOffset,g=d&&d.endContainer,h=d&&d.endOffset;b&&!(("p"===b.localName||"h"===b.localName)&&b.namespaceURI===j);)b=b.parentNode;M&&b&&b.parentNode!==r&&(r?r.parentNode&&k():(r=b.ownerDocument.createElement("p"),r.style||(r=b.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml","p")),r.style.margin="0px",r.style.padding="0px",r.style.border="0px",r.setAttribute("contenteditable",!0)),b.parentNode.replaceChild(r,b),r.appendChild(b),r.focus(),
+d&&(c.removeAllRanges(),d=b.ownerDocument.createRange(),d.setStart(f,e),d.setEnd(g,h),c.addRange(d)),a.preventDefault?(a.preventDefault(),a.stopPropagation()):(a.returnValue=!1,a.cancelBubble=!0))})};return odf.OdfCanvas}();
 // Input 26
 runtime.loadClass("xmldom.XPath");runtime.loadClass("odf.Style2CSS");
-gui.PresenterUI=function(){var i=new odf.Style2CSS,k=new xmldom.XPath,e=i.namespaceResolver;return function(g){var a=this;a.setInitialSlideMode=function(){a.startSlideMode("single")};a.keyDownHandler=function(b){if(!b.target.isContentEditable&&b.target.nodeName!=="input")switch(b.keyCode){case 84:a.toggleToolbar();break;case 37:case 8:a.prevSlide();break;case 39:case 32:a.nextSlide();break;case 36:a.firstSlide();break;case 35:a.lastSlide()}};a.root=function(){return a.odf_canvas.odfContainer().rootElement};
-a.firstSlide=function(){a.slideChange(function(){return 0})};a.lastSlide=function(){a.slideChange(function(a,e){return e-1})};a.nextSlide=function(){a.slideChange(function(a,e){return a+1<e?a+1:-1})};a.prevSlide=function(){a.slideChange(function(a){return a<1?-1:a-1})};a.slideChange=function(b){var e=a.getPages(a.odf_canvas.odfContainer().rootElement),c=-1,d=0;e.forEach(function(a){a=a[1];a.hasAttribute("slide_current")&&(c=d,a.removeAttribute("slide_current"));d+=1});b=b(c,e.length);b===-1&&(b=c);
-e[b][1].setAttribute("slide_current","1");document.getElementById("pagelist").selectedIndex=b;a.slide_mode==="cont"&&window.scrollBy(0,e[b][1].getBoundingClientRect().top-30)};a.selectSlide=function(b){a.slideChange(function(a,c){return b>=c?-1:b<0?-1:b})};a.scrollIntoContView=function(b){var e=a.getPages(a.odf_canvas.odfContainer().rootElement);e.length!==0&&window.scrollBy(0,e[b][1].getBoundingClientRect().top-30)};a.getPages=function(a){var a=a.getElementsByTagNameNS(e("draw"),"page"),g=[],c;for(c=
-0;c<a.length;c+=1)g.push([a[c].getAttribute("draw:name"),a[c]]);return g};a.fillPageList=function(b,e){for(var c=a.getPages(b),d,f,g;e.firstChild;)e.removeChild(e.firstChild);for(d=0;d<c.length;d+=1)f=document.createElement("option"),g=k.getODFElementsWithXPath(c[d][1],'./draw:frame[@presentation:class="title"]//draw:text-box/text:p',xmldom.XPath),g=g.length>0?g[0].textContent:c[d][0],f.textContent=d+1+": "+g,e.appendChild(f)};a.startSlideMode=function(b){var e=document.getElementById("pagelist"),
-c=a.odf_canvas.slidevisibilitycss().sheet;for(a.slide_mode=b;c.cssRules.length>0;)c.deleteRule(0);a.selectSlide(0);a.slide_mode==="single"?(c.insertRule("draw|page { position:fixed; left:0px;top:30px; z-index:1; }",0),c.insertRule("draw|page[slide_current]  { z-index:2;}",1),c.insertRule("draw|page  { -webkit-transform: scale(1);}",2),a.fitToWindow(),window.addEventListener("resize",a.fitToWindow,false)):a.slide_mode==="cont"&&window.removeEventListener("resize",a.fitToWindow,false);a.fillPageList(a.odf_canvas.odfContainer().rootElement,
-e)};a.toggleToolbar=function(){var b,e,c;b=a.odf_canvas.slidevisibilitycss().sheet;e=-1;for(c=0;c<b.cssRules.length;c+=1)if(b.cssRules[c].cssText.substring(0,8)===".toolbar"){e=c;break}e>-1?b.deleteRule(e):b.insertRule(".toolbar { position:fixed; left:0px;top:-200px; z-index:0; }",0)};a.fitToWindow=function(){var b=a.getPages(a.root()),e=(window.innerHeight-40)/b[0][1].clientHeight,b=(window.innerWidth-10)/b[0][1].clientWidth,e=e<b?e:b,b=a.odf_canvas.slidevisibilitycss().sheet;b.deleteRule(2);b.insertRule("draw|page { \n-moz-transform: scale("+
-e+"); \n-moz-transform-origin: 0% 0%; -webkit-transform-origin: 0% 0%; -webkit-transform: scale("+e+"); -o-transform-origin: 0% 0%; -o-transform: scale("+e+"); -ms-transform-origin: 0% 0%; -ms-transform: scale("+e+"); }",2)};a.load=function(b){a.odf_canvas.load(b)};a.odf_element=g;a.odf_canvas=new odf.OdfCanvas(a.odf_element);a.odf_canvas.addListener("statereadychange",a.setInitialSlideMode);a.slide_mode="undefined";document.addEventListener("keydown",a.keyDownHandler,false)}}();
+gui.PresenterUI=function(){var g=new odf.Style2CSS,m=new xmldom.XPath,e=g.namespaceResolver;return function(g){var a=this;a.setInitialSlideMode=function(){a.startSlideMode("single")};a.keyDownHandler=function(c){if(!(c.target.isContentEditable||"input"===c.target.nodeName))switch(c.keyCode){case 84:a.toggleToolbar();break;case 37:case 8:a.prevSlide();break;case 39:case 32:a.nextSlide();break;case 36:a.firstSlide();break;case 35:a.lastSlide()}};a.root=function(){return a.odf_canvas.odfContainer().rootElement};
+a.firstSlide=function(){a.slideChange(function(){return 0})};a.lastSlide=function(){a.slideChange(function(a,b){return b-1})};a.nextSlide=function(){a.slideChange(function(a,b){return a+1<b?a+1:-1})};a.prevSlide=function(){a.slideChange(function(a){return 1>a?-1:a-1})};a.slideChange=function(c){var b=a.getPages(a.odf_canvas.odfContainer().rootElement),d=-1,e=0;b.forEach(function(a){a=a[1];a.hasAttribute("slide_current")&&(d=e,a.removeAttribute("slide_current"));e+=1});c=c(d,b.length);-1===c&&(c=d);
+b[c][1].setAttribute("slide_current","1");document.getElementById("pagelist").selectedIndex=c;"cont"===a.slide_mode&&window.scrollBy(0,b[c][1].getBoundingClientRect().top-30)};a.selectSlide=function(c){a.slideChange(function(a,d){return c>=d||0>c?-1:c})};a.scrollIntoContView=function(c){var b=a.getPages(a.odf_canvas.odfContainer().rootElement);0!==b.length&&window.scrollBy(0,b[c][1].getBoundingClientRect().top-30)};a.getPages=function(a){var a=a.getElementsByTagNameNS(e("draw"),"page"),b=[],d;for(d=
+0;d<a.length;d+=1)b.push([a[d].getAttribute("draw:name"),a[d]]);return b};a.fillPageList=function(c,b){for(var d=a.getPages(c),e,f,g;b.firstChild;)b.removeChild(b.firstChild);for(e=0;e<d.length;e+=1)f=document.createElement("option"),g=m.getODFElementsWithXPath(d[e][1],'./draw:frame[@presentation:class="title"]//draw:text-box/text:p',xmldom.XPath),g=0<g.length?g[0].textContent:d[e][0],f.textContent=e+1+": "+g,b.appendChild(f)};a.startSlideMode=function(c){var b=document.getElementById("pagelist"),
+d=a.odf_canvas.slidevisibilitycss().sheet;for(a.slide_mode=c;0<d.cssRules.length;)d.deleteRule(0);a.selectSlide(0);"single"===a.slide_mode?(d.insertRule("draw|page { position:fixed; left:0px;top:30px; z-index:1; }",0),d.insertRule("draw|page[slide_current]  { z-index:2;}",1),d.insertRule("draw|page  { -webkit-transform: scale(1);}",2),a.fitToWindow(),window.addEventListener("resize",a.fitToWindow,!1)):"cont"===a.slide_mode&&window.removeEventListener("resize",a.fitToWindow,!1);a.fillPageList(a.odf_canvas.odfContainer().rootElement,
+b)};a.toggleToolbar=function(){var c,b,d;c=a.odf_canvas.slidevisibilitycss().sheet;b=-1;for(d=0;d<c.cssRules.length;d+=1)if(".toolbar"===c.cssRules[d].cssText.substring(0,8)){b=d;break}-1<b?c.deleteRule(b):c.insertRule(".toolbar { position:fixed; left:0px;top:-200px; z-index:0; }",0)};a.fitToWindow=function(){var c=a.getPages(a.root()),b=(window.innerHeight-40)/c[0][1].clientHeight,c=(window.innerWidth-10)/c[0][1].clientWidth,b=b<c?b:c,c=a.odf_canvas.slidevisibilitycss().sheet;c.deleteRule(2);c.insertRule("draw|page { \n-moz-transform: scale("+
+b+"); \n-moz-transform-origin: 0% 0%; -webkit-transform-origin: 0% 0%; -webkit-transform: scale("+b+"); -o-transform-origin: 0% 0%; -o-transform: scale("+b+"); -ms-transform-origin: 0% 0%; -ms-transform: scale("+b+"); }",2)};a.load=function(c){a.odf_canvas.load(c)};a.odf_element=g;a.odf_canvas=new odf.OdfCanvas(a.odf_element);a.odf_canvas.addListener("statereadychange",a.setInitialSlideMode);a.slide_mode="undefined";document.addEventListener("keydown",a.keyDownHandler,!1)}}();
 // Input 27
-gui.Caret=function(i,k){k.ownerDocument.createElementNS("urn:webodf:names:cursor","cursor");this.updateToSelection=function(){i.rangeCount===1&&i.getRangeAt(0)}};
+gui.Caret=function(g,m){m.ownerDocument.createElementNS("urn:webodf:names:cursor","cursor");this.updateToSelection=function(){1===g.rangeCount&&g.getRangeAt(0)}};
 // Input 28
 runtime.loadClass("core.Cursor");
-gui.SelectionMover=function(i,k){function e(a,b){if(i.rangeCount!==0){var d=i.getRangeAt(0);if(d.startContainer&&d.startContainer.nodeType===1){k.setPoint(d.startContainer,d.startOffset);b();d=k.node();k.position();var f=[],e;for(e=0;e<i.rangeCount;e+=1)f[e]=i.getRangeAt(e);i.removeAllRanges();f.length===0&&(f[0]=d.ownerDocument.createRange());f[f.length-1].setStart(k.node(),k.position());for(e=0;e<f.length;e+=1)i.addRange(f[e])}}}function g(){b.updateToSelection();for(var a=b.getNode().getBoundingClientRect(),
-c=a.left,d=a.top,a=false,f=200;!a;){f-=1;b.remove();if(i.focusNode&&i.focusNode.nodeType===1){k.setPoint(i.focusNode,i.focusOffset);k.stepForward();var a=k.node(),e=k.position();i.collapse(a,e);b.updateToSelection()}a=b.getNode().getBoundingClientRect();a=a.top!==d&&a.left>c}}var a=k.node().ownerDocument,b=new core.Cursor(i,a);this.movePointForward=function(a){e(a,k.stepForward)};this.movePointBackward=function(a){e(a,k.stepBackward)};this.moveLineForward=function(a){i.modify?i.modify(a?"extend":
-"move","forward","line"):e(a,g)};this.moveLineBackward=function(a){i.modify?i.modify(a?"extend":"move","backward","line"):e(a,function(){})};return this};
+gui.SelectionMover=function(g,m){function e(a,c){if(0!==g.rangeCount){var e=g.getRangeAt(0);if(e.startContainer&&1===e.startContainer.nodeType){m.setPoint(e.startContainer,e.startOffset);c();e=m.node();m.position();var f=[],h;for(h=0;h<g.rangeCount;h+=1)f[h]=g.getRangeAt(h);g.removeAllRanges();0===f.length&&(f[0]=e.ownerDocument.createRange());f[f.length-1].setStart(m.node(),m.position());for(h=0;h<f.length;h+=1)g.addRange(f[h])}}}function k(){c.updateToSelection();for(var a=c.getNode().getBoundingClientRect(),
+d=a.left,e=a.top,a=!1;!a;){c.remove();if(g.focusNode&&1===g.focusNode.nodeType){m.setPoint(g.focusNode,g.focusOffset);m.stepForward();var a=m.node(),f=m.position();g.collapse(a,f);c.updateToSelection()}a=c.getNode().getBoundingClientRect();a=a.top!==e&&a.left>d}}var a=m.node().ownerDocument,c=new core.Cursor(g,a);this.movePointForward=function(a){e(a,m.stepForward)};this.movePointBackward=function(a){e(a,m.stepBackward)};this.moveLineForward=function(a){g.modify?g.modify(a?"extend":"move","forward",
+"line"):e(a,k)};this.moveLineBackward=function(a){g.modify?g.modify(a?"extend":"move","backward","line"):e(a,function(){})};return this};
 // Input 29
 runtime.loadClass("core.PointWalker");runtime.loadClass("core.Cursor");
-gui.XMLEdit=function(i,k){function e(a,b,c){a.addEventListener?a.addEventListener(b,c,false):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function g(a){a.preventDefault?a.preventDefault():a.returnValue=false}function a(){var a=i.ownerDocument.defaultView.getSelection();a&&!(a.rangeCount<=0)&&n&&(a=a.getRangeAt(0),n.setPoint(a.startContainer,a.startOffset))}function b(){var a=i.ownerDocument.defaultView.getSelection(),b,c;a.removeAllRanges();n&&n.node()&&(b=n.node(),c=b.ownerDocument.createRange(),
-c.setStart(b,n.position()),c.collapse(true),a.addRange(c))}function h(c){var d=c.charCode||c.keyCode;if(n=null,n&&d===37)a(),n.stepBackward(),b();else if(d>=16&&d<=20||d>=33&&d<=40)return;g(c)}function c(){}function d(a){i.ownerDocument.defaultView.getSelection().getRangeAt(0);g(a)}function f(a){for(var b=a.firstChild;b&&b!==a;)b.nodeType===1&&f(b),b=b.nextSibling||b.parentNode;var c,d,e,b=a.attributes;c="";for(e=b.length-1;e>=0;e-=1)d=b.item(e),c=c+" "+d.nodeName+'="'+d.nodeValue+'"';a.setAttribute("customns_name",
-a.nodeName);a.setAttribute("customns_atts",c);b=a.firstChild;for(d=/^\s*$/;b&&b!==a;)c=b,b=b.nextSibling||b.parentNode,c.nodeType===3&&d.test(c.nodeValue)&&c.parentNode.removeChild(c)}function j(a,b){for(var c=a.firstChild,d,e,f;c&&c!==a;){if(c.nodeType===1){j(c,b);d=c.attributes;for(f=d.length-1;f>=0;f-=1)if(e=d.item(f),e.namespaceURI==="http://www.w3.org/2000/xmlns/"&&!b[e.nodeValue])b[e.nodeValue]=e.localName}c=c.nextSibling||c.parentNode}}function p(){var a=i.ownerDocument.createElement("style"),
-b;b={};j(i,b);var c={},d,e,f=0;for(d in b)if(b.hasOwnProperty(d)&&d){e=b[d];if(!e||c.hasOwnProperty(e)||e==="xmlns"){do e="ns"+f,f+=1;while(c.hasOwnProperty(e));b[d]=e}c[e]=true}b="@namespace customns url(customns);\n";a.type="text/css";b+=m;a.appendChild(i.ownerDocument.createTextNode(b));k=k.parentNode.replaceChild(a,k)}var m,l,u,n=null;if(!i.id)i.id="xml"+String(Math.random()).substring(2);l="#"+i.id+" ";m=l+"*,"+l+":visited, "+l+":link {display:block; margin: 0px; margin-left: 10px; font-size: medium; color: black; background: white; font-variant: normal; font-weight: normal; font-style: normal; font-family: sans-serif; text-decoration: none; white-space: pre-wrap; height: auto; width: auto}\n"+
-l+":before {color: blue; content: '<' attr(customns_name) attr(customns_atts) '>';}\n"+l+":after {color: blue; content: '</' attr(customns_name) '>';}\n"+l+"{overflow: auto;}\n";(function(a){e(a,"click",d);e(a,"keydown",h);e(a,"keypress",c);e(a,"drop",g);e(a,"dragend",g);e(a,"beforepaste",g);e(a,"paste",g)})(i);this.updateCSS=p;this.setXML=function(a){a=a.documentElement||a;u=a=i.ownerDocument.importNode(a,true);for(f(a);i.lastChild;)i.removeChild(i.lastChild);i.appendChild(a);p();n=new core.PointWalker(a)};
-this.getXML=function(){return u}};
+gui.XMLEdit=function(g,m){function e(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function k(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function a(){var a=g.ownerDocument.defaultView.getSelection();a&&!(0>=a.rangeCount)&&p&&(a=a.getRangeAt(0),p.setPoint(a.startContainer,a.startOffset))}function c(){var a=g.ownerDocument.defaultView.getSelection(),b,c;a.removeAllRanges();p&&p.node()&&(b=p.node(),c=b.ownerDocument.createRange(),
+c.setStart(b,p.position()),c.collapse(!0),a.addRange(c))}function b(b){var d=b.charCode||b.keyCode;if(p=null,p&&37===d)a(),p.stepBackward(),c();else if(16<=d&&20>=d||33<=d&&40>=d)return;k(b)}function d(){}function o(a){g.ownerDocument.defaultView.getSelection().getRangeAt(0);k(a)}function f(a){for(var b=a.firstChild;b&&b!==a;)1===b.nodeType&&f(b),b=b.nextSibling||b.parentNode;var c,d,e,b=a.attributes;c="";for(e=b.length-1;0<=e;e-=1)d=b.item(e),c=c+" "+d.nodeName+'="'+d.nodeValue+'"';a.setAttribute("customns_name",
+a.nodeName);a.setAttribute("customns_atts",c);b=a.firstChild;for(d=/^\s*$/;b&&b!==a;)c=b,b=b.nextSibling||b.parentNode,3===c.nodeType&&d.test(c.nodeValue)&&c.parentNode.removeChild(c)}function h(a,b){for(var c=a.firstChild,d,e,f;c&&c!==a;){if(1===c.nodeType){h(c,b);d=c.attributes;for(f=d.length-1;0<=f;f-=1)e=d.item(f),"http://www.w3.org/2000/xmlns/"===e.namespaceURI&&!b[e.nodeValue]&&(b[e.nodeValue]=e.localName)}c=c.nextSibling||c.parentNode}}function i(){var a=g.ownerDocument.createElement("style"),
+b;b={};h(g,b);var c={},d,e,f=0;for(d in b)if(b.hasOwnProperty(d)&&d){e=b[d];if(!e||c.hasOwnProperty(e)||"xmlns"===e){do e="ns"+f,f+=1;while(c.hasOwnProperty(e));b[d]=e}c[e]=!0}a.type="text/css";b="@namespace customns url(customns);\n"+j;a.appendChild(g.ownerDocument.createTextNode(b));m=m.parentNode.replaceChild(a,m)}var j,n,x,p=null;g.id||(g.id="xml"+(""+Math.random()).substring(2));n="#"+g.id+" ";j=n+"*,"+n+":visited, "+n+":link {display:block; margin: 0px; margin-left: 10px; font-size: medium; color: black; background: white; font-variant: normal; font-weight: normal; font-style: normal; font-family: sans-serif; text-decoration: none; white-space: pre-wrap; height: auto; width: auto}\n"+
+n+":before {color: blue; content: '<' attr(customns_name) attr(customns_atts) '>';}\n"+n+":after {color: blue; content: '</' attr(customns_name) '>';}\n"+n+"{overflow: auto;}\n";(function(a){e(a,"click",o);e(a,"keydown",b);e(a,"keypress",d);e(a,"drop",k);e(a,"dragend",k);e(a,"beforepaste",k);e(a,"paste",k)})(g);this.updateCSS=i;this.setXML=function(a){a=a.documentElement||a;x=a=g.ownerDocument.importNode(a,!0);for(f(a);g.lastChild;)g.removeChild(g.lastChild);g.appendChild(a);i();p=new core.PointWalker(a)};
+this.getXML=function(){return x}};
+// Input 30
+(function(){return"core/Async.js,core/Base64.js,core/ByteArray.js,core/ByteArrayWriter.js,core/Cursor.js,core/JSLint.js,core/PointWalker.js,core/RawDeflate.js,core/RawInflate.js,core/UnitTester.js,core/Zip.js,gui/Caret.js,gui/SelectionMover.js,gui/XMLEdit.js,gui/PresenterUI.js,odf/FontLoader.js,odf/Formatting.js,odf/OdfCanvas.js,odf/OdfContainer.js,odf/Style2CSS.js,odf/StyleInfo.js,xmldom/LSSerializer.js,xmldom/LSSerializerFilter.js,xmldom/OperationalTransformDOM.js,xmldom/OperationalTransformInterface.js,xmldom/RelaxNG.js,xmldom/RelaxNG2.js,xmldom/RelaxNGParser.js,xmldom/XPath.js".split(",")})();


commit c65695b1da77042d6aa7fb7bc6c043033cfbe237
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Mar 16 21:25:45 2012 +0100

    Fix odfviewer plugin to work with latest Roundcube version

diff --git a/plugins/odfviewer/odfviewer.php b/plugins/odfviewer/odfviewer.php
index a64cf85..40cf522 100644
--- a/plugins/odfviewer/odfviewer.php
+++ b/plugins/odfviewer/odfviewer.php
@@ -75,20 +75,19 @@ class odfviewer extends rcube_plugin
    */
   function get_part($args)
   {
-    global $IMAP, $MESSAGE;
-    
     if (!$args['download'] && $args['mimetype'] && in_array($args['mimetype'], $this->odf_mimetypes)) {
       if (empty($_GET['_load'])) {
         $suffix = preg_match('/(\.\w+)$/', $args['part']->filename, $m) ? $m[1] : '.odt';
         $fn = md5(session_id() . $_SERVER['REQUEST_URI']) . $suffix;
-        
+
         // FIXME: copy file to disk because only apache can send the file correctly
         $tempfn = $this->tempdir . $fn;
         if (!file_exists($tempfn)) {
           $fp = fopen($tempfn, 'w');
-          $IMAP->get_message_part($MESSAGE->uid, $args['part']->mime_id, $args['part'], false, $fp);
+          $imap = rcmail::get_instance()->get_storage();
+          $imap->get_message_part($args['uid'], $args['id'], $args['part'], false, $fp);
           fclose($fp);
-          
+
           // remember tempfiles in session to clean up on logout
           $_SESSION['odfviewer']['tempfiles'][] = $fn;
         }


commit 57dad2d92deda5752bc9325ff3dcf5e861996cfe
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Mar 16 19:17:58 2012 +0100

    Require kolab_folders plugin for listing imap folders by type; fix error message

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 01e465f..2e50753 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -277,7 +277,7 @@ class kolab_storage_folder
                 'type' => 'php',
                 'file' => __FILE__,
                 'line' => __LINE__,
-                'message' => "Could not find Kolab data part in message " . $this->name . ':' . $uid,
+                'message' => "Could not find Kolab data part in message $msguid ($this->name).",
             ), true);
             return false;
         }
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 9092f46..eefc621 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -35,6 +35,9 @@ class libkolab extends rcube_plugin
         // load local config
         $this->load_config();
 
+        // require kolab_folders plugin for listing folders by type (annotation)
+        $this->require_plugin('kolab_folders');
+
         // extend include path to load bundled lib classes
         $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
         set_include_path($include_path);


commit baf6bcb23954d85dbe150936d432e72b5fea70e5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 14 19:48:54 2012 +0100

    Add documentation headers

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 1fe8e61..0f19e1a 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -3,6 +3,9 @@
 /**
  * Kolab format model class wrapping libkolabxml bindings
  *
+ * Abstract base class for different Kolab groupware objects read from/written
+ * to the new Kolab 3 format using the PHP bindings of libkolabxml.
+ *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 9d56b83..daef994 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -1,5 +1,26 @@
 <?php
 
+/**
+ * Kolab Contact model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
 
 class kolab_format_contact extends kolab_format
 {
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index b064718..8477176 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -1,5 +1,26 @@
 <?php
 
+/**
+ * Kolab Distribution List model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
 
 class kolab_format_distributionlist extends kolab_format
 {
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 00035e9..6a262af 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -1,5 +1,26 @@
 <?php
 
+/**
+ * Kolab Event model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
 
 class kolab_format_event extends kolab_format
 {


commit 3f78d8f7b5c465c423acd2a36a06495672b1c06f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 14 18:51:38 2012 +0100

    Let libkolabxml generate UIDs; preserve object properties Roundcube doens't understand by loading old XML object before updating

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 5609f6c..359d8ad 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -518,8 +518,6 @@ class rcube_kolab_contacts extends rcube_addressbook
         if (!$existing) {
             // generate new Kolab contact item
             $object = $this->_from_rcube_contact($save_data);
-            $object['uid'] = kolab_format::generate_uid();
-
             $saved = $this->storagefolder->save($object, 'contact');
 
             if (!$saved) {
@@ -554,7 +552,7 @@ class rcube_kolab_contacts extends rcube_addressbook
     {
         $updated = false;
         if ($old = $this->storagefolder->get_object($this->_id2uid($id))) {
-            $object = array_merge($old, $this->_from_rcube_contact($save_data));
+            $object = $this->_from_rcube_contact($save_data, $old);
 
             $saved = $this->storagefolder->save($object, 'contact', $uid);
             if (!$saved) {
@@ -1030,9 +1028,9 @@ class rcube_kolab_contacts extends rcube_addressbook
     }
 
     /**
-     * Map fields from Roundcube format to internal Kolab_Format
+     * Map fields from Roundcube format to internal kolab_format_contact properties
      */
-    private function _from_rcube_contact($contact)
+    private function _from_rcube_contact($contact, $old = array())
     {
         if (!$contact['uid'] && $contact['ID'])
             $contact['uid'] = $this->_id2uid($contact['ID']);
@@ -1085,7 +1083,14 @@ class rcube_kolab_contacts extends rcube_addressbook
             $contact['_attachments']['photo.attachment'] = false;
         }
 
-        return $contact;
+        // copy meta data (starting with _) from old object
+        foreach ((array)$old as $key => $val) {
+            if (!isset($contact[$key]) && $key[0] == '_')
+                $contact[$key] = $val;
+        }
+
+        // add empty values for some fields which can be removed in the UI
+        return $contact + array('nickname' => '', 'birthday' => '', 'anniversary' => '', 'freebusyurl' => '');
     }
 
 }
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 9c6fea0..1fe8e61 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -26,6 +26,9 @@ abstract class kolab_format
 {
     public static $timezone;
 
+    protected $obj;
+    protected $data;
+
     /**
      * Factory method to instantiate a kolab_format object of the given type
      */
@@ -43,27 +46,17 @@ abstract class kolab_format
     }
 
     /**
-     * Generate random UID for Kolab objects
-     *
-     * @return string  UUID with a unique MD5 value
-     */
-    public static function generate_uid()
-    {
-        return 'urn:uuid:' . md5(uniqid(mt_rand(), true));
-    }
-
-    /**
      * Convert the given date/time value into a cDateTime object
      *
      * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
      * @param DateTimeZone  The timezone the date/time is in. Use global default if empty
      * @param boolean       True of the given date has no time component
-     * @return object       The libkolabxml date/time object or null on error
+     * @return object       The libkolabxml date/time object
      */
     public static function get_datetime($datetime, $tz = null, $dateonly = false)
     {
         if (!$tz) $tz = self::$timezone;
-        $result = null;
+        $result = new cDateTime();
 
         if (is_numeric($datetime))
             $datetime = new DateTime('@'.$datetime, $tz);
@@ -71,7 +64,6 @@ abstract class kolab_format
             $datetime = new DateTime($datetime, $tz);
 
         if (is_a($datetime, 'DateTime')) {
-            $result = new cDateTime();
             $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
 
             if (!$dateonly)
@@ -141,12 +133,35 @@ abstract class kolab_format
     public static function array2vector($arr)
     {
         $vec = new vectors;
-        foreach ((array)$arr as $val)
-            $vec->push($val);
+        foreach ((array)$arr as $val) {
+            if (strlen($val))
+                $vec->push($val);
+        }
         return $vec;
     }
 
     /**
+     * Save the last generated UID to the object properties.
+     * Should be called after kolabformat::writeXXXX();
+     */
+    protected function update_uid()
+    {
+        // get generated UID
+        if (!$this->data['uid']) {
+            $this->data['uid'] = kolabformat::getSerializedUID();
+            $this->obj->setUid($this->data['uid']);
+        }
+    }
+
+    /**
+     * Direct getter for object properties
+     */
+    function __get($var)
+    {
+        return $this->data[$var];
+    }
+
+    /**
      * Load Kolab object data from the given XML block
      *
      * @param string XML data
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 48260b3..9d56b83 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -24,8 +24,10 @@ class kolab_format_contact extends kolab_format
         'work' => Address::Work,
     );
 
-    private $data;
-    private $obj;
+    private $gendermap = array(
+        'female' => Contact::Female,
+        'male' => Contact::Male,
+    );
 
     // old Kolab 2 format field map
     private $kolab2_fieldmap = array(
@@ -98,7 +100,9 @@ class kolab_format_contact extends kolab_format
      */
     public function write()
     {
-        return kolabformat::writeContact($this->obj);
+        $xml = kolabformat::writeContact($this->obj);
+        parent::update_uid();
+        return $xml;
     }
 
     /**
@@ -109,18 +113,16 @@ class kolab_format_contact extends kolab_format
     public function set(&$object)
     {
         // set some automatic values if missing
-        if (empty($object['uid']))
-            $object['uid'] = self::generate_uid();
-
         if (false && !$this->obj->created()) {
             if (!empty($object['created']))
                 $object['created'] = new DateTime('now', self::$timezone);
             $this->obj->setCreated(self::get_datetime($object['created']));
         }
 
-        // do the hard work of setting object values
-        $this->obj->setUid($object['uid']);
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
 
+        // do the hard work of setting object values
         $nc = new NameComponents;
         $nc->setSurnames(self::array2vector($object['surname']));
         $nc->setGiven(self::array2vector($object['firstname']));
@@ -130,7 +132,7 @@ class kolab_format_contact extends kolab_format
         $this->obj->setNameComponents($nc);
         $this->obj->setName($object['name']);
 
-        if ($object['nickname'])
+        if (isset($object['nickname']))
             $this->obj->setNickNames(self::array2vector($object['nickname']));
 
         // organisation related properties (affiliation)
@@ -191,15 +193,15 @@ class kolab_format_contact extends kolab_format
         }
         $this->obj->setTelephones($tels);
 
-        if ($object['gender'])
-            $this->obj->setGender($object['gender'] == 'female' ? Contact::Female : Contact::Male);
-        if ($object['notes'])
+        if (isset($object['gender']))
+            $this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
+        if (isset($object['notes']))
             $this->obj->setNote($object['notes']);
-        if ($object['freebusyurl'])
+        if (isset($object['freebusyurl']))
             $this->obj->setFreeBusyUrl($object['freebusyurl']);
-        if ($object['birthday'])
+        if (isset($object['birthday']))
             $this->obj->setBDay(self::get_datetime($object['birthday'], null, true));
-        if ($object['anniversary'])
+        if (isset($object['anniversary']))
             $this->obj->setAnniversary(self::get_datetime($object['anniversary'], null, true));
 
         if (!empty($object['photo'])) {
@@ -304,8 +306,9 @@ class kolab_format_contact extends kolab_format
         if ($anniversary = self::php_datetime($this->obj->anniversary()))
             $object['anniversary'] = $anniversary->format('c');
 
-        if ($g = $this->obj->gender())
-            $object['gender'] = $g == Contact::Female ? 'female' : 'male';
+        $gendermap = array_flip($this->gendermap);
+        if (($g = $this->obj->gender()) && $gendermap[$g])
+            $object['gender'] = $gendermap[$g];
 
         if ($this->obj->photoMimetype())
             $object['photo'] = $this->obj->photo();
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index d629781..b064718 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -4,9 +4,6 @@
 class kolab_format_distributionlist extends kolab_format
 {
     public $CTYPE = 'application/vcard+xml';
-    
-    private $data;
-    private $obj;
 
     function __construct()
     {
@@ -30,17 +27,17 @@ class kolab_format_distributionlist extends kolab_format
      */
     public function write()
     {
-        return kolabformat::writeDistlist($this->obj);
+        $xml = kolabformat::writeDistlist($this->obj);
+        parent::update_uid();
+        return $xml;
     }
 
     public function set(&$object)
     {
         // set some automatic values if missing
-        if (empty($object['uid']))
-            $object['uid'] = self::generate_uid();
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
 
-        // do the hard work of setting object values
-        $this->obj->setUid($object['uid']);
         $this->obj->setName($object['name']);
 
         $members = new vectormember;
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index d8ab276..00035e9 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -4,9 +4,6 @@
 class kolab_format_event extends kolab_format
 {
     public $CTYPE = 'application/calendar+xml';
-    
-    private $data;
-    private $obj;
 
     function __construct()
     {
@@ -20,7 +17,9 @@ class kolab_format_event extends kolab_format
 
     public function write()
     {
-        return kolabformat::writeEvent($this->obj);
+        $xml = kolabformat::writeEvent($this->obj);
+        parent::update_uid();
+        return $xml;
     }
 
     public function set(&$object)
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 4dd29a2..01e465f 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -44,6 +44,7 @@ class kolab_storage_folder
     private $imap;
     private $info;
     private $owner;
+    private $objcache = array();
     private $uid2msg = array();
 
 
@@ -239,7 +240,8 @@ class kolab_storage_folder
      *
      * @param string The IMAP message UID to fetch
      * @param string The object type expected
-     * @return array Hash array representing the Kolab object
+     * @param string The folder name where the message is stored
+     * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
      */
     private function read_object($msguid, $type = null, $folder = null)
     {
@@ -247,6 +249,10 @@ class kolab_storage_folder
         if (!$folder) $folder = $this->name;
         $ctype= self::KTYPE_PREFIX . $type;
 
+        // requested message not in local cache
+        if ($this->objcache[$msguid])
+            return $this->objcache[$msguid];
+
         $this->imap->set_folder($folder);
         $message = new rcube_message($msguid);
         $attachments = array();
@@ -296,10 +302,16 @@ class kolab_storage_folder
         }
 
         if ($format->is_valid()) {
+            if ($formatobj)
+                return $format;
+
             $object = $format->to_array();
             $object['_msguid'] = $msguid;
             $object['_mailbox'] = $this->name;
             $object['_attachments'] = $attachments;
+            $object['_formatobj'] = $format;
+
+            $this->objcache[$msguid] = $object;
             return $object;
         }
 
@@ -425,11 +437,21 @@ class kolab_storage_folder
      */
     private function build_message(&$object, $type)
     {
-        $format = kolab_format::factory($type);
+        // load old object to preserve data we don't understand/process
+        if (is_object($object['_formatobj']))
+            $format = $object['_formatobj'];
+        else if ($object['_msguid'] && ($old = $this->read_object($object['_msguid'], $type, $object['_mailbox'])))
+            $format = $old['_formatobj'];
+
+        // create new kolab_format instance
+        if (!$format)
+            $format = kolab_format::factory($type);
+
         $format->set($object);
         $xml = $format->write();
+        $object['uid'] = $format->uid;  // get read UID from format
 
-        if (!$format->is_valid()) {
+        if (!$format->is_valid() || empty($object['uid'])) {
             return false;
         }
 
@@ -450,7 +472,7 @@ class kolab_storage_folder
         $mime->headers($headers);
         $mime->setTXTBody('This is a Kolab Groupware object. '
             . 'To view this object you will need an email client that understands the Kolab Groupware format. '
-            . "For a list of such email clients please visit http://www.kolab.org/kolab2-clients.html\n\n");
+            . "For a list of such email clients please visit http://www.kolab.org/\n\n");
 
         $mime->addAttachment($xml,
             $format->CTYPE,


commit edaad9b47c16952ba9245e85fda26c0ca363c063
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 14 13:57:02 2012 +0100

    Support contact undelete with new storage classes; only list undeleted imap objects

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 7fe8079..5609f6c 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -632,55 +632,22 @@ class rcube_kolab_contacts extends rcube_addressbook
         if (!is_array($ids))
             $ids = explode(',', $ids);
 
-        $count     = 0;
-        $uids      = array();
-/*
-        TODO: re-implement this using kolab_storage_folder::undelete();
-
-        $imap_uids = $_SESSION['kolab_delete_uids'];
-
-        // convert contact IDs into IMAP UIDs
-        foreach ($ids as $id)
-            if ($uid = $imap_uids[$id])
-                $uids[] = $uid;
-
-        if (!empty($uids)) {
-            $session = &Horde_Kolab_Session::singleton();
-            $imap = &$session->getImap();
-
-            if (is_object($imap) && is_a($imap, 'PEAR_Error')) {
-                $error = $imap;
+        $count = 0;
+        foreach ($ids as $id) {
+            $uid = $this->_id2uid($id);
+            if ($this->storagefolder->undelete($uid)) {
+                $count++;
             }
             else {
-                $result = $imap->select($this->imap_folder);
-                if (is_object($result) && is_a($result, 'PEAR_Error')) {
-                    $error = $result;
-                }
-                else {
-                    $result = $imap->undeleteMessages(implode(',', $uids));
-                    if (is_object($result) && is_a($result, 'PEAR_Error')) {
-                        $error = $result;
-                    }
-                    else {
-                        $this->_connect();
-                        $this->storagefolder->synchronize();
-                    }
-                }
-            }
-
-            if ($error) {
                 raise_error(array(
                   'code' => 600, 'type' => 'php',
                   'file' => __FILE__, 'line' => __LINE__,
-                  'message' => "Error undeleting a contact object(s) from the Kolab server:" . $error->getMessage()),
+                  'message' => "Error undeleting a contact object $uid from the Kolab server"),
                 true, false);
             }
-
-            $rcmail = rcmail::get_instance();
-            $rcmail->session->remove('kolab_delete_uids');
         }
-*/
-        return count($uids);
+
+        return $count;
     }
 
 
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 200caed..48260b3 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -217,7 +217,7 @@ class kolab_format_contact extends kolab_format
             $this->obj->setPhoto('','');
         }
 
-        // handle spouse, children, profession, initials, pgppublickey, etc.
+        // TODO: handle spouse, children, profession, initials, pgppublickey, etc.
 
         // cache this data
         $this->data = $object;
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 9d00d33..5146899 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -62,7 +62,7 @@ class kolab_storage
     /**
      * Get a list of storage folders for the given data type
      *
-     * @param string Data type to list folders for (contact,event,task,note)
+     * @param string Data type to list folders for (contact,distribution-list,event,task,note)
      *
      * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
      */
@@ -95,6 +95,31 @@ class kolab_storage
 
 
     /**
+     * Getter for a single Kolab object, identified by its UID.
+     * This will search all folders storing objects of the given type.
+     *
+     * @param string Object UID
+     * @param string Object type (contact,distribution-list,event,task,note)
+     * @return array The Kolab object represented as hash array or false if not found
+     */
+    public static function get_object($uid, $type)
+    {
+        $folder = null;
+        foreach ((array)self::$imap->list_folders('', '*', $type) as $foldername) {
+            if (!$folder)
+                $folder = new kolab_storage_folder($foldername, self::$imap);
+            else
+                $folder->set_folder($foldername);
+
+            if ($object = $folder->get_object($uid))
+                return $object;
+        }
+
+        return false;
+    }
+
+
+    /**
      *
      */
     public static function get_freebusy_server()
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index f087048..4dd29a2 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -52,8 +52,20 @@ class kolab_storage_folder
      */
     function __construct($name, $imap = null)
     {
-        $this->name = $name;
         $this->imap = is_object($imap) ? $imap : rcmail::get_instance()->get_storage();
+        $this->imap->set_options(array('skip_deleted' => false));
+        $this->set_folder($name);
+    }
+
+
+    /**
+     * Set the IMAP folder name this instance connects to
+     *
+     * @param string The folder name/path
+     */
+    public function set_folder($name)
+    {
+        $this->name = $name;
         $this->imap->set_folder($this->name);
 
         $metadata = $this->imap->get_metadata($this->name, array(kolab_storage::CTYPE_KEY));
@@ -146,7 +158,7 @@ class kolab_storage_folder
 
         // search by object type
         $ctype  = self::KTYPE_PREFIX . $type;
-        $index = $this->imap->search_once($this->name, 'HEADER X-Kolab-Type ' . $ctype);
+        $index = $this->imap->search_once($this->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
 
         return $index->count();
     }
@@ -163,7 +175,7 @@ class kolab_storage_folder
 
         // search by object type
         $ctype  = self::KTYPE_PREFIX . $type;
-        $search = 'HEADER X-Kolab-Type ' . $ctype;
+        $search = 'UNDELETED HEADER X-Kolab-Type ' . $ctype;
 
         $index = $this->imap->search_once($this->name, $search);
         $results = array();
@@ -194,7 +206,7 @@ class kolab_storage_folder
         if ($msguid && ($object = $this->read_object($msguid)))
             return $object;
 
-        return array('uid' => $uid);
+        return false;
     }
 
 
@@ -380,7 +392,12 @@ class kolab_storage_folder
      */
     public function undelete($uid)
     {
-        // TODO: implement this
+        if ($msguid = $this->uid2msguid($uid)) {
+            if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
+                return $msguid;
+            }
+        }
+
         return false;
     }
 


commit fc1439c7bd2efe0e9d6c4616682b6a94b8c58ec2
Merge: 4353615 12e5918
Author: Thomas B <roundcube at gmail.com>
Date:   Wed Mar 14 11:25:00 2012 +0100

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit 435361570977afc1c7ef35a448255658435cf811
Author: Thomas B <roundcube at gmail.com>
Date:   Wed Mar 14 11:22:42 2012 +0100

    Handle contact photos in new format

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 9fb0d8b..9c6fea0 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -179,4 +179,10 @@ abstract class kolab_format
      */
     abstract public function to_array();
 
+    /**
+     * Load object data from Kolab2 format
+     *
+     * @param array Hash array with object properties (produced by Horde Kolab_Format classes)
+     */
+    abstract public function fromkolab2($object);
 }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 8ff6087..200caed 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -202,6 +202,21 @@ class kolab_format_contact extends kolab_format
         if ($object['anniversary'])
             $this->obj->setAnniversary(self::get_datetime($object['anniversary'], null, true));
 
+        if (!empty($object['photo'])) {
+            if (strlen($object['photo']) < 255 && ($att = $object['_attachments'][$object['photo']])) {
+                if ($att['content'])
+                    $this->obj->setPhoto($att['content'], $att['type']);
+                $object['_attachments'][$object['photo']] = false;
+            }
+            else if ($type = rc_image_content_type($object['photo'])) {
+                $this->obj->setPhoto($object['photo'], $type);
+                $object['_attachments']['photo.attachment'] = false;
+            }
+        }
+        else if (isset($object['photo'])) {
+            $this->obj->setPhoto('','');
+        }
+
         // handle spouse, children, profession, initials, pgppublickey, etc.
 
         // cache this data
@@ -292,12 +307,17 @@ class kolab_format_contact extends kolab_format
         if ($g = $this->obj->gender())
             $object['gender'] = $g == Contact::Female ? 'female' : 'male';
 
+        if ($this->obj->photoMimetype())
+            $object['photo'] = $this->obj->photo();
+
         $this->data = $object;
         return $this->data;
     }
 
     /**
      * Load data from old Kolab2 format
+     *
+     * @param array Hash array with object properties
      */
     public function fromkolab2($record)
     {
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index c47d41c..885484b 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -314,6 +314,9 @@ class kolab_storage_folder
                 if (!isset($object['_attachments'][$name])) {
                     $object['_attachments'][$name] = $old['_attachments'][$name];
                 }
+                // load photo.attachment contents to be directly embedded in xcard block
+                if ($name == 'photo.attachment' && !$object['_attachments'][$name]['content'] && $att['key'])
+                    $object['_attachments'][$name]['content'] = $this->get_attachment($object['_msguid'], $att['key'], $object['_mailbox']);
             }
         }
 


commit 12e59189cbc2f4bbf2c09cb62665903ee118fbdd
Merge: 6e70c94 7bc509a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Mar 8 21:32:35 2012 +0100

    Merge branch 'dev/kolab3' of ssh://git.kolabsys.com/git/roundcube into dev/kolab3



commit 6e70c942c51be1cb159a291c11b63faf3e05d8f4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Mar 8 21:32:22 2012 +0100

    Correctly implement the expunge argument for object deletion

diff --git a/plugins/libkolab/README b/plugins/libkolab/README
new file mode 100644
index 0000000..a16250f
--- /dev/null
+++ b/plugins/libkolab/README
@@ -0,0 +1,16 @@
+libkolab plugin to access to Kolab groupware data
+=================================================
+
+The contained library classes establish a connection to the Kolab server
+and manage the access to the Kolab groupware objects stored in various
+IMAP folders. For reading and writing these objects, the PHP bindings of
+the libkolabxml library are used.
+
+
+REQUIREMENTS
+------------
+* libkolabxml PHP bindings
+  - kolabformat.so loaded into PHP
+  - kolabformat.php placed somewhere in the include_path
+* Horde Kolab_Format package and all of its dependencies
+
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index d82c5c6..a9f8d9b 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -314,16 +314,21 @@ class kolab_storage_folder
      * Delete the specified object from this folder.
      *
      * @param  mixed   $object  The Kolab object to delete or object UID
-     * @param  boolean $trigger Should the folder be triggered?
      * @param  boolean $expunge Should the folder be expunged?
+     * @param  boolean $trigger Should the folder update be triggered?
      *
      * @return boolean True if successful, false on error
      */
     public function delete($object, $expunge = true, $trigger = true)
     {
-        if ($msguid = is_array($object) ? $object['_msguid'] : $this->uid2msguid($object)) {
+        $msguid = is_array($object) ? $object['_msguid'] : $this->uid2msguid($object);
+
+        if ($msguid && $expunge) {
             return $this->imap->delete_message($msguid, $this->name);
         }
+        else if ($msguid) {
+            return $this->imap->set_flag($msguid, 'DELETED', $this->name);
+        }
 
         return false;
     }


commit 7bc509a2cd66ca72303a28f774ddd6709c4b848a
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Mar 8 21:24:13 2012 +0100

    Larry skin for Kolab address book

diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
index d154a0a..2dabd6c 100644
--- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -5,7 +5,7 @@
  *
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
- * Copyright (C) 2011, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -156,7 +156,7 @@ class kolab_addressbook_ui
         );
 
         if (!empty($options) && ($options['norename'] || $options['protected'])) {
-            $foldername = Q(str_replace($delimiter, ' » ', rcube_kolab::object_name($folder)));
+            $foldername = Q(str_replace($delimiter, ' » ', kolab_storage::object_name($folder)));
         }
         else {
             $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
@@ -178,7 +178,7 @@ class kolab_addressbook_ui
             $hidden_fields[] = array('name' => '_parent', 'value' => $path_imap);
         }
         else {
-            $select = rcube_kolab::folder_selector('contact', array('name' => '_parent'), $folder);
+            $select = kolab_storage::folder_selector('contact', array('name' => '_parent'), $folder);
 
             $form['props']['fieldsets']['location']['content']['path'] = array(
                 'label' => $this->plugin->gettext('parentbook'),
diff --git a/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css
new file mode 100644
index 0000000..f6963b4
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css
@@ -0,0 +1,28 @@
+
+#directorylist li.addressbook.readonly,
+#directorylist li.addressbook.shared,
+#directorylist li.addressbook.other {
+/*	background-image: url(kolab_folders.png); */
+	background-position: 5px -1000px;
+	background-repeat: no-repeat;
+}
+
+#directorylist li.addressbook.readonly {
+	background-position: 5px 0px;
+}
+
+#directorylist li.addressbook.shared {
+	background-position: 5px -54px;
+}
+
+#directorylist li.addressbook.shared.readonly {
+	background-position: 5px -72px;
+}
+
+#directorylist li.addressbook.other {
+	background-position: 5px -18px;
+}
+
+#directorylist li.addressbook.other.readonly {
+	background-position: 5px -36px;
+}
diff --git a/plugins/kolab_addressbook/skins/larry/templates/bookedit.html b/plugins/kolab_addressbook/skins/larry/templates/bookedit.html
new file mode 100644
index 0000000..007d512
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/larry/templates/bookedit.html
@@ -0,0 +1,24 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body class="iframe">
+
+<h1 class="boxtitle"><roundcube:label name="kolab_addressbook.bookproperties" /></h1>
+
+<div class="boxcontent">
+  <roundcube:object name="bookdetails" class="propform" />
+</div>
+
+<div id="formfooter">
+<div class="footerleft formbuttons">
+	<roundcube:button command="book-save" type="input" class="button mainaction" label="save" />
+</div>
+</div>
+
+<roundcube:include file="/includes/footer.html" />
+
+</body>
+</html>


commit 67f3f91aac9a99a467676192e0a39895beb62202
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Mar 8 21:23:55 2012 +0100

    Fix contact group management using new libkolab

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 19bea06..7fe8079 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -249,6 +249,7 @@ class rcube_kolab_contacts extends rcube_addressbook
 
         // list member of the selected group
         if ($this->gid) {
+            $this->_fetch_groups();
             $seen = array();
             $this->result->count = 0;
             foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
@@ -256,8 +257,11 @@ class rcube_kolab_contacts extends rcube_addressbook
                 if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
                     continue;
 
-                if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++)
+                $contact = $this->storagefolder->get_object($member['uid']);
+                if ($contact  && !$seen[$member['ID']]++) {
+                    $this->contacts[$member['ID']] = $this->_to_rcube_contact($contact);
                     $this->result->count++;
+                }
             }
             $ids = array_keys($seen);
         }
@@ -518,11 +522,11 @@ class rcube_kolab_contacts extends rcube_addressbook
 
             $saved = $this->storagefolder->save($object, 'contact');
 
-            if (PEAR::isError($saved)) {
+            if (!$saved) {
                 raise_error(array(
                   'code' => 600, 'type' => 'php',
                   'file' => __FILE__, 'line' => __LINE__,
-                  'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
+                  'message' => "Error saving contact object to Kolab server"),
                 true, false);
             }
             else {
@@ -553,7 +557,7 @@ class rcube_kolab_contacts extends rcube_addressbook
             $object = array_merge($old, $this->_from_rcube_contact($save_data));
 
             $saved = $this->storagefolder->save($object, 'contact', $uid);
-            if (!$saved || PEAR::isError($saved)) {
+            if (!$saved) {
                 raise_error(array(
                   'code' => 600, 'type' => 'php',
                   'file' => __FILE__, 'line' => __LINE__,
@@ -563,6 +567,8 @@ class rcube_kolab_contacts extends rcube_addressbook
             else {
                 $this->contacts[$id] = $this->_to_rcube_contact($object);
                 $updated = true;
+                
+                // TODO: update data in groups this contact is member of
             }
         }
 
@@ -717,16 +723,16 @@ class rcube_kolab_contacts extends rcube_addressbook
         );
         $saved = $this->storagefolder->save($list, 'distribution-list');
 
-        if (PEAR::isError($saved)) {
+        if (!$saved) {
             raise_error(array(
               'code' => 600, 'type' => 'php',
               'file' => __FILE__, 'line' => __LINE__,
-              'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+              'message' => "Error saving distribution-list object to Kolab server"),
             true, false);
             return false;
         }
         else {
-            $id = md5($list['uid']);
+            $id = $this->_uid2id($list['uid']);
             $this->distlists[$id] = $list;
             $result = array('id' => $id, 'name' => $name);
         }
@@ -748,11 +754,11 @@ class rcube_kolab_contacts extends rcube_addressbook
         if ($list = $this->distlists[$gid])
             $deleted = $this->storagefolder->delete($list['uid']);
 
-        if (PEAR::isError($deleted)) {
+        if (!$deleted) {
             raise_error(array(
               'code' => 600, 'type' => 'php',
               'file' => __FILE__, 'line' => __LINE__,
-              'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
+              'message' => "Error deleting distribution-list object from the Kolab server"),
             true, false);
         }
         else
@@ -778,11 +784,11 @@ class rcube_kolab_contacts extends rcube_addressbook
             $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
         }
 
-        if (PEAR::isError($saved)) {
+        if (!$saved) {
             raise_error(array(
               'code' => 600, 'type' => 'php',
               'file' => __FILE__, 'line' => __LINE__,
-              'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+              'message' => "Error saving distribution-list object to Kolab server"),
             true, false);
             return false;
         }
@@ -806,7 +812,6 @@ class rcube_kolab_contacts extends rcube_addressbook
         $exists = array();
 
         $this->_fetch_groups();
-        $this->_fetch_contacts();
         $list = $this->distlists[$gid];
 
         foreach ((array)$list['member'] as $i => $member)
@@ -817,13 +822,14 @@ class rcube_kolab_contacts extends rcube_addressbook
 
         foreach ($ids as $contact_id) {
             if ($uid = $this->_id2uid($contact_id)) {
-                $contact = $this->contacts[$contact_id];
+                $contact = $this->storagefolder->get_object($uid);
                 foreach ($this->get_col_values('email', $contact, true) as $email) {
                     $list['member'][] = array(
                         'uid' => $uid,
-                        'display-name' => $contact['name'],
-                        'smtp-address' => $email,
+                        'mailto' => $email,
+                        'name' => $contact['name'],
                     );
+                    break;
                 }
                 $this->groupmembers[$contact_id][] = $gid;
                 $added++;
@@ -833,11 +839,11 @@ class rcube_kolab_contacts extends rcube_addressbook
         if ($added)
             $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
 
-        if (PEAR::isError($saved)) {
+        if (!$saved) {
             raise_error(array(
               'code' => 600, 'type' => 'php',
               'file' => __FILE__, 'line' => __LINE__,
-              'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
+              'message' => "Error saving distribution-list to Kolab server"),
             true, false);
             $added = false;
         }
@@ -874,11 +880,11 @@ class rcube_kolab_contacts extends rcube_addressbook
         $list['member'] = $new_member;
         $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
 
-        if (PEAR::isError($saved)) {
+        if (!$saved) {
             raise_error(array(
               'code' => 600, 'type' => 'php',
               'file' => __FILE__, 'line' => __LINE__,
-              'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+              'message' => "Error saving distribution-list object to Kolab server"),
             true, false);
         }
         else {
@@ -1048,7 +1054,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         // photo is stored as separate attachment
         if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
             // only fetch photo content if requested
-            if ($rcmail = rcmail::get_instance()->action == 'photo')
+            if (rcmail::get_instance()->action == 'photo')
                 $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['key']);
         }
 
@@ -1064,13 +1070,6 @@ class rcube_kolab_contacts extends rcube_addressbook
         if (!$contact['uid'] && $contact['ID'])
             $contact['uid'] = $this->_id2uid($contact['ID']);
 
-/*
-        // format dates
-        if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
-            $object['birthday'] = date('Y-m-d', $date);
-        if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
-            $object['anniversary'] = date('Y-m-d', $date);
-*/
         $contact['email'] = array_filter($this->get_col_values('email', $contact, true));
         $contact['website'] = array_filter($this->get_col_values('website', $contact, true));
         $contact['im'] = array_filter($this->get_col_values('im', $contact, true));


commit c8696e278cb8507ddcfd5febbad20ac6378d902d
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Mar 8 21:22:09 2012 +0100

    Implement format wrapper for distribution-list; add more utility functions

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index bcad59e..9fb0d8b 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -45,11 +45,11 @@ abstract class kolab_format
     /**
      * Generate random UID for Kolab objects
      *
-     * @return string  MD5 hash with a unique value
+     * @return string  UUID with a unique MD5 value
      */
     public static function generate_uid()
     {
-        return md5(uniqid(mt_rand(), true));
+        return 'urn:uuid:' . md5(uniqid(mt_rand(), true));
     }
 
     /**
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
index a1f5891..d629781 100644
--- a/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -10,7 +10,7 @@ class kolab_format_distributionlist extends kolab_format
 
     function __construct()
     {
-        $obj = new DistList;
+        $this->obj = new DistList;
     }
 
     /**
@@ -35,12 +35,28 @@ class kolab_format_distributionlist extends kolab_format
 
     public function set(&$object)
     {
-        // TODO: do the hard work of setting object values
+        // set some automatic values if missing
+        if (empty($object['uid']))
+            $object['uid'] = self::generate_uid();
+
+        // do the hard work of setting object values
+        $this->obj->setUid($object['uid']);
+        $this->obj->setName($object['name']);
+
+        $members = new vectormember;
+        foreach ($object['member'] as $member) {
+            $m = new Member;
+            $m->setName($member['name']);
+            $m->setEmail($member['mailto']);
+            $m->setUid($member['uid']);
+            $members->push($m);
+        }
+        $this->obj->setMembers($members);
     }
 
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && true /*$this->obj->isValid()*/);
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
     }
 
     /**
@@ -87,40 +103,17 @@ class kolab_format_distributionlist extends kolab_format
 
         $members = $this->obj->members();
         for ($i=0; $i < $members->size(); $i++) {
-            $adr = self::decode_member($members->get($i));
-            if ($adr[0]['mailto'])
+            $member = $members->get($i);
+            if ($mailto = $member->email())
                 $object['member'][] = array(
-                    'mailto' => $adr[0]['mailto'],
-                    'name' => $adr[0]['name'],
-                    'uid' => '????',
+                    'mailto' => $mailto,
+                    'name' => $member->name(),
+                    'uid' => $member->uid(),
                 );
         }
 
+        $this->data = $object;
         return $this->data;
     }
 
-    /**
-     * Compose a valid Mailto URL according to RFC 822
-     *
-     * @param string E-mail address
-     * @param string Person name
-     * @return string Formatted string
-     */
-    public static function format_member($email, $name = '')
-    {
-        // let Roundcube internals do the job
-        return 'mailto:' . format_email_recipient($email, $name);
-    }
-
-    /**
-     * Split a mailto: url into a structured member component
-     *
-     * @param string RFC 822 mailto: string
-     * @return array Hash array with member properties
-     */
-    public static function decode_member($str)
-    {
-        $adr = rcube_mime::decode_address_list(preg_replace('/^mailto:/', '', $str));
-        return $adr[0];
-    }
 }
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index ec93213..9d00d33 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -203,4 +203,25 @@ class kolab_storage
         return $folder;
     }
 
+    /**
+     * Creates a SELECT field with folders list
+     *
+     * @param string $type    Folder type
+     * @param array  $attrs   SELECT field attributes (e.g. name)
+     * @param string $current The name of current folder (to skip it)
+     *
+     * @return html_select SELECT object
+     */
+    public static function folder_selector($type, $attrs, $current = '')
+    {
+        // TODO: implement this
+
+
+        // Build SELECT field of parent folder
+        $select = new html_select($attrs);
+        $select->add('---', '');
+
+
+        return $select;
+    }
 }


commit 80fa73b895c84bc49768d80e80f3d8461b6623e9
Author: Thomas B <roundcube at gmail.com>
Date:   Thu Mar 8 10:21:21 2012 +0100

    Adapt date/time handling to recent changes in libkolabxml; forward attachment parts when saving a Kolab object in a new message

diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 08ba20a..19bea06 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -46,7 +46,7 @@ class rcube_kolab_contacts extends rcube_addressbook
       'department'   => array('limit' => 1),
       'email'        => array('subtypes' => null),
       'phone'        => array(),
-      'address'      => array('subtypes' => array('home','business')),
+      'address'      => array('subtypes' => array('home','work')),
       'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
                                 'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
       'website'      => array('subtypes' => null),
@@ -1046,8 +1046,10 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
 
         // photo is stored as separate attachment
-        if ($record['photo'] && ($att = $record['_attachments'][$record['photo']])) {
-            $out['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['key']);
+        if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
+            // only fetch photo content if requested
+            if ($rcmail = rcmail::get_instance()->action == 'photo')
+                $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['key']);
         }
 
         // remove empty fields
@@ -1112,6 +1114,10 @@ class rcube_kolab_contacts extends rcube_addressbook
           );
           $contact['photo'] = $attkey;
         }
+        else if (isset($contact['photo']) && empty($contact['photo'])) {
+            // unset photo attachment
+            $contact['_attachments']['photo.attachment'] = false;
+        }
 
         return $contact;
     }
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 928ee2f..bcad59e 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -53,25 +53,25 @@ abstract class kolab_format
     }
 
     /**
-     * Convert the given date/time value into a c_DateTime object
+     * Convert the given date/time value into a cDateTime object
      *
      * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
      * @param DateTimeZone  The timezone the date/time is in. Use global default if empty
      * @param boolean       True of the given date has no time component
-     * @return c_DateTime   The libkolabxml date/time object or null on error
+     * @return object       The libkolabxml date/time object or null on error
      */
-    public static function getDateTime($datetime, $tz = null, $dateonly = false)
+    public static function get_datetime($datetime, $tz = null, $dateonly = false)
     {
         if (!$tz) $tz = self::$timezone;
         $result = null;
 
         if (is_numeric($datetime))
             $datetime = new DateTime('@'.$datetime, $tz);
-        else if (is_string($datetime))
+        else if (is_string($datetime) && strlen($datetime))
             $datetime = new DateTime($datetime, $tz);
 
         if (is_a($datetime, 'DateTime')) {
-            $result = new KolabDateTime();
+            $result = new cDateTime();
             $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
 
             if (!$dateonly)
@@ -84,6 +84,41 @@ abstract class kolab_format
     }
 
     /**
+     * Convert the given cDateTime into a PHP DateTime object
+     *
+     * @param object cDateTime  The libkolabxml datetime object
+     * @return object DateTime  PHP datetime instance
+     */
+    public static function php_datetime($cdt)
+    {
+        if (!is_object($cdt) || !$cdt->isValid())
+            return null;
+
+        $d = new DateTime;
+        $d->setTimezone(self::$timezone);
+
+        try {
+            if ($tzs = $cdt->timezone()) {
+                $tz = new DateTimeZone($tzs);
+                $d->setTimezone($tz);
+            }
+        }
+        catch (Exception $e) { }
+
+        $d->setDate($cdt->year(), $cdt->month(), $cdt->day());
+
+        if ($cdt->isDateOnly()) {
+            $d->_dateonly = true;
+            $d->setTime(12, 0, 0);  // set time to noon to avoid timezone troubles
+        }
+        else {
+            $d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
+        }
+
+        return $d;
+    }
+
+    /**
      * Convert a libkolabxml vector to a PHP array
      *
      * @param object vector Object
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 4dbcaf8..8ff6087 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -55,6 +55,7 @@ class kolab_format_contact extends kolab_format
       'body'         => 'notes',
       'pgp-publickey' => 'pgppublickey',
       'free-busy-url' => 'freebusyurl',
+      'picture'       => 'photo',
     );
     private $kolab2_phonetypes = array(
         'home1' => 'home',
@@ -114,7 +115,7 @@ class kolab_format_contact extends kolab_format
         if (false && !$this->obj->created()) {
             if (!empty($object['created']))
                 $object['created'] = new DateTime('now', self::$timezone);
-            $this->obj->setCreated(self::getDateTime($object['created']));
+            $this->obj->setCreated(self::get_datetime($object['created']));
         }
 
         // do the hard work of setting object values
@@ -196,10 +197,10 @@ class kolab_format_contact extends kolab_format
             $this->obj->setNote($object['notes']);
         if ($object['freebusyurl'])
             $this->obj->setFreeBusyUrl($object['freebusyurl']);
-//        if ($object['birthday'])
-//            $this->obj->setBDay(self::getDateTime($object['birthday'], null, true));
-//        if ($object['anniversary'])
-//            $this->obj->setAnniversary(self::getDateTime($object['anniversary'], null, true));
+        if ($object['birthday'])
+            $this->obj->setBDay(self::get_datetime($object['birthday'], null, true));
+        if ($object['anniversary'])
+            $this->obj->setAnniversary(self::get_datetime($object['anniversary'], null, true));
 
         // handle spouse, children, profession, initials, pgppublickey, etc.
 
@@ -281,7 +282,13 @@ class kolab_format_contact extends kolab_format
 
         $object['notes'] = $this->obj->note();
         $object['freebusyurl'] = $this->obj->freeBusyUrl();
-        
+
+        if ($bday = self::php_datetime($this->obj->bDay()))
+            $object['birthday'] = $bday->format('c');
+
+        if ($anniversary = self::php_datetime($this->obj->anniversary()))
+            $object['anniversary'] = $anniversary->format('c');
+
         if ($g = $this->obj->gender())
             $object['gender'] = $g == Contact::Female ? 'female' : 'male';
 
@@ -327,11 +334,6 @@ class kolab_format_contact extends kolab_format
             }
         }
 
-        // photo is stored as separate attachment
-        if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
-            $object['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
-        }
-
         // remove empty fields
         $this->data = array_filter($object);
     }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index d82c5c6..c47d41c 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -202,16 +202,19 @@ class kolab_storage_folder
      * Fetch a Kolab object attachment which is stored in a separate part
      * of the mail MIME message that represents the Kolab record.
      *
-     * @param string Object's UID
-     * @param string The attachment key stored in the Kolab XML
-     * @return mixed The attachment content as binary string
+     * @param string  Object's UID
+     * @param string  The attachment's mime number
+     * @param string  IMAP folder where message is stored;
+     *                If set, that also implies that the given UID is an IMAP UID
+     * @return mixed  The attachment content as binary string
      */
-    public function get_attachment($uid, $key)
+    public function get_attachment($uid, $part, $mailbox = null)
     {
-        // TODO: implement this
+        if ($msguid = ($mailbox ? $uid : $this->uid2msguid($uid))) {
+            if ($mailbox)
+                $this->imap->set_folder($mailbox);
 
-        if ($msguid = $this->uid2msguid($uid)) {
-            $message = new rcube_message($msguid);
+            return $this->imap->get_message_part($msguid, $part);
         }
 
         return null;
@@ -219,21 +222,34 @@ class kolab_storage_folder
 
 
     /**
+     * Fetch the mime message from the storage server and extract
+     * the Kolab groupware object from it
      *
+     * @param string The IMAP message UID to fetch
+     * @param string The object type expected
+     * @return array Hash array representing the Kolab object
      */
-    private function read_object($msguid, $type = null)
+    private function read_object($msguid, $type = null, $folder = null)
     {
         if (!$type) $type = $this->type;
+        if (!$folder) $folder = $this->name;
         $ctype= self::KTYPE_PREFIX . $type;
 
-        $this->imap->set_folder($this->name);
+        $this->imap->set_folder($folder);
         $message = new rcube_message($msguid);
+        $attachments = array();
 
         // get XML part
         foreach ((array)$message->attachments as $part) {
-            if ($part->mimetype == $ctype || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype)) {
+            if (!$xml && ($part->mimetype == $ctype || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
                 $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
-                break;
+            }
+            else if ($part->filename) {
+                $attachments[$part->filename] = array(
+                    'key' => $part->mime_id,
+                    'type' => $part->mimetype,
+                    'size' => $part->size,
+                );
             }
         }
 
@@ -271,6 +287,7 @@ class kolab_storage_folder
             $object = $format->to_array();
             $object['_msguid'] = $msguid;
             $object['_mailbox'] = $this->name;
+            $object['_attachments'] = $attachments;
             return $object;
         }
 
@@ -291,6 +308,15 @@ class kolab_storage_folder
         if (!$type)
             $type = $this->type;
 
+        // copy attachments from old message
+        if (!empty($object['_msguid']) && ($old = $this->read_object($object['_msguid'], $type, $object['_mailbox']))) {
+            foreach ($old['_attachments'] as $name => $att) {
+                if (!isset($object['_attachments'][$name])) {
+                    $object['_attachments'][$name] = $old['_attachments'][$name];
+                }
+            }
+        }
+
         if ($raw_msg = $this->build_message($object, $type)) {
             $result = $this->imap->save_message($this->name, $raw_msg, '', false);
 
@@ -410,6 +436,18 @@ class kolab_storage_folder
             '', RCMAIL_CHARSET
         );
 
+        // save object attachments as separate parts
+        // TODO: optimize memory consumption by using tempfiles for transfer
+        foreach ((array)$object['_attachments'] as $name => $att) {
+            if (empty($att['content']) && !empty($att['key'])) {
+                $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
+                $att['content'] = $this->get_attachment($msguid, $att['key'], $object['_mailbox']);
+            }
+            if (!empty($att['content'])) {
+                $mime->addAttachment($att['content'], $att['type'], $name, false);
+            }
+        }
+
         return $mime->getMessage();
     }
 


commit 83fe5ad8f592e589aab930ac3610d024597e1783
Author: Thomas B <roundcube at gmail.com>
Date:   Wed Mar 7 11:02:51 2012 +0100

    Make kolab_addressbook use the new libkolab

diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 5a47657..8abdab6 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -51,7 +51,7 @@ class kolab_addressbook extends rcube_plugin
         $this->rc = rcmail::get_instance();
 
         // load required plugin
-        $this->require_plugin('kolab_core');
+        $this->require_plugin('libkolab');
 
         // register hooks
         $this->add_hook('addressbooks_list', array($this, 'address_sources'));
@@ -245,7 +245,7 @@ class kolab_addressbook extends rcube_plugin
         }
 
         // get all folders that have "contact" type
-        $this->folders = rcube_kolab::get_folders('contact');
+        $this->folders = kolab_storage::get_folders('contact');
 
         if (PEAR::isError($this->folders)) {
             raise_error(array(
@@ -264,7 +264,7 @@ class kolab_addressbook extends rcube_plugin
 
             foreach ($names as $utf7name => $name) {
                 // create instance of rcube_contacts
-                $abook_id = rcube_kolab::folder_id($utf7name);
+                $abook_id = kolab_storage::folder_id($utf7name);
                 $abook = new rcube_kolab_contacts($utf7name);
                 $this->sources[$abook_id] = $abook;
             }
@@ -481,7 +481,7 @@ class kolab_addressbook extends rcube_plugin
 
                 if (!$plugin['abort']) {
                     if ($oldfolder != $folder)
-                        $result = rcube_kolab::folder_rename($oldfolder, $folder);
+                        $result = kolab_storage::folder_rename($oldfolder, $folder);
                     else
                         $result = true;
                 }
@@ -497,7 +497,7 @@ class kolab_addressbook extends rcube_plugin
                 $folder = $plugin['name'];
 
                 if (!$plugin['abort']) {
-                    $result = rcube_kolab::folder_create($folder, 'contact', false);
+                    $result = kolab_storage::folder_create($folder, 'contact', false);
                 }
                 else {
                     $result = $plugin['result'];
@@ -545,7 +545,7 @@ class kolab_addressbook extends rcube_plugin
             $this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
             $this->rc->output->command('set_env', 'delimiter', $delimiter);
             $this->rc->output->command('book_update', array(
-                'id'       => rcube_kolab::folder_id($folder),
+                'id'       => kolab_storage::folder_id($folder),
                 'name'     => $name,
                 'readonly' => false,
                 'editable' => true,
@@ -553,7 +553,7 @@ class kolab_addressbook extends rcube_plugin
                 'realname' => rcube_charset::convert($folder, 'UTF7-IMAP'), // IMAP folder name
                 'class_name' => $kolab_folder->get_namespace(),
                 'kolab'    => true,
-            ), rcube_kolab::folder_id($oldfolder));
+            ), kolab_storage::folder_id($oldfolder));
 
             $this->rc->output->send('iframe');
         }
@@ -574,12 +574,12 @@ class kolab_addressbook extends rcube_plugin
     {
         $folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true, 'UTF7-IMAP'));
 
-        if (rcube_kolab::folder_delete($folder)) {
+        if (kolab_storage::folder_delete($folder)) {
             $this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
             $this->rc->output->set_env('pagecount', 0);
             $this->rc->output->command('set_rowcount', rcmail_get_rowcount_text(new rcube_result_set()));
             $this->rc->output->command('list_contacts_clear');
-            $this->rc->output->command('book_delete_done', rcube_kolab::folder_id($folder));
+            $this->rc->output->command('book_delete_done', kolab_storage::folder_id($folder));
         }
         else {
             $this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index f028f52..08ba20a 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -4,7 +4,7 @@
  * Backend class for a custom address book
  *
  * This part of the Roundcube+Kolab integration and connects the
- * rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
+ * rcube_addressbook interface with the kolab_storage wrapper from libkolab
  *
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  * @author Aleksander Machniak <machniak at kolabsys.com>
@@ -46,11 +46,11 @@ class rcube_kolab_contacts extends rcube_addressbook
       'department'   => array('limit' => 1),
       'email'        => array('subtypes' => null),
       'phone'        => array(),
-      'address'      => array('limit' => 2, 'subtypes' => array('home','business')),
+      'address'      => array('subtypes' => array('home','business')),
       'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
                                 'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
-      'website'      => array('limit' => 1, 'subtypes' => null),
-      'im'           => array('limit' => 1, 'subtypes' => null),
+      'website'      => array('subtypes' => null),
+      'im'           => array('subtypes' => null),
       'gender'       => array('limit' => 1),
       'initials'     => array('type' => 'text', 'size' => 6, 'maxlength' => 10, 'limit' => 1,
                                 'label' => 'kolab_addressbook.initials', 'category' => 'personal'),
@@ -69,7 +69,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                                 'label' => 'kolab_addressbook.freebusyurl'),
       'notes'        => array(),
       'photo'        => array(),
-      // TODO: define more Kolab-specific fields such as: language, latitude, longitude
+      // TODO: define more Kolab-specific fields such as: role, language, latitude, longitude
     );
 
     /**
@@ -86,47 +86,13 @@ class rcube_kolab_contacts extends rcube_addressbook
 
     private $gid;
     private $storagefolder;
-    private $contactstorage;
-    private $liststorage;
     private $contacts;
     private $distlists;
     private $groupmembers;
-    private $id2uid;
     private $filter;
     private $result;
     private $namespace;
     private $imap_folder = 'INBOX/Contacts';
-    private $gender_map = array(0 => 'male', 1 => 'female');
-    private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax');
-    private $addresstypemap = array('work' => 'business');
-    private $fieldmap = array(
-      // kolab       => roundcube
-      'full-name'    => 'name',
-      'given-name'   => 'firstname',
-      'middle-names' => 'middlename',
-      'last-name'    => 'surname',
-      'prefix'       => 'prefix',
-      'suffix'       => 'suffix',
-      'nick-name'    => 'nickname',
-      'organization' => 'organization',
-      'department'   => 'department',
-      'job-title'    => 'jobtitle',
-      'initials'     => 'initials',
-      'birthday'     => 'birthday',
-      'anniversary'  => 'anniversary',
-      'im-address'   => 'im',
-      'web-page'     => 'website',
-      'office-location' => 'officelocation',
-      'profession'   => 'profession',
-      'manager-name' => 'manager',
-      'assistant'    => 'assistant',
-      'spouse-name'  => 'spouse',
-      'children'     => 'children',
-      'body'         => 'notes',
-      'pgp-publickey' => 'pgppublickey',
-      'free-busy-url' => 'freebusyurl',
-      'gender'       => 'gender',
-    );
 
 
     public function __construct($imap_folder = null)
@@ -136,9 +102,9 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
 
         // extend coltypes configuration 
-        $format = rcube_kolab::get_format('contact');
-        $this->coltypes['phone']['subtypes'] = $format->_phone_types;
-        $this->coltypes['address']['subtypes'] = $format->_address_types;
+        $format = kolab_format::factory('contact');
+        $this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
+        $this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
 
         // set localized labels for proprietary cols
         foreach ($this->coltypes as $col => $prop) {
@@ -147,17 +113,17 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
 
         // fetch objects from the given IMAP folder
-        $this->storagefolder = rcube_kolab::get_folder($this->imap_folder);
+        $this->storagefolder = kolab_storage::get_folder($this->imap_folder);
         $this->ready = !PEAR::isError($this->storagefolder);
 
         // Set readonly and editable flags according to folder permissions
         if ($this->ready) {
-            if ($this->get_owner() == $_SESSION['username']) {
+            if ($this->storagefolder->get_owner() == $_SESSION['username']) {
                 $this->editable = true;
                 $this->readonly = false;
             }
             else {
-                $rights = $this->storagefolder->getMyRights();
+                $rights = $this->storagefolder->get_acl();
                 if (!PEAR::isError($rights)) {
                     if (strpos($rights, 'i') !== false)
                         $this->readonly = false;
@@ -176,7 +142,7 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function get_name()
     {
-        $folder = rcube_kolab::object_name($this->imap_folder, $this->namespace);
+        $folder = kolab_storage::object_name($this->imap_folder, $this->namespace);
         return $folder;
     }
 
@@ -193,17 +159,6 @@ class rcube_kolab_contacts extends rcube_addressbook
 
 
     /**
-     * Getter for the IMAP folder owner
-     *
-     * @return string Name of the folder owner
-     */
-    public function get_owner()
-    {
-        return $this->storagefolder->getOwner();
-    }
-
-
-    /**
      * Getter for the name of the namespace to which the IMAP folder belongs
      *
      * @return string Name of the namespace (personal, other, shared)
@@ -211,7 +166,7 @@ class rcube_kolab_contacts extends rcube_addressbook
     public function get_namespace()
     {
         if ($this->namespace === null) {
-            $this->namespace = rcube_kolab::folder_namespace($this->imap_folder);
+            $this->namespace = $this->storagefolder->get_namespace();
         }
 
         return $this->namespace;
@@ -270,8 +225,8 @@ class rcube_kolab_contacts extends rcube_addressbook
         $this->_fetch_groups();
         $groups = array();
         foreach ((array)$this->distlists as $group) {
-            if (!$search || strstr(strtolower($group['last-name']), strtolower($search)))
-                $groups[$group['last-name']] = array('ID' => $group['ID'], 'name' => $group['last-name']);
+            if (!$search || strstr(strtolower($group['name']), strtolower($search)))
+                $groups[$group['name']] = array('ID' => $group['ID'], 'name' => $group['name']);
         }
 
         // sort groups
@@ -291,6 +246,7 @@ class rcube_kolab_contacts extends rcube_addressbook
     public function list_records($cols=null, $subset=0)
     {
         $this->result = $this->count();
+
         // list member of the selected group
         if ($this->gid) {
             $seen = array();
@@ -305,8 +261,10 @@ class rcube_kolab_contacts extends rcube_addressbook
             }
             $ids = array_keys($seen);
         }
-        else
+        else {
+            $this->_fetch_contacts();
             $ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
+        }
 
         // sort data arrays according to desired list sorting
         if ($count = count($ids)) {
@@ -461,9 +419,17 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function count()
     {
-        $this->_fetch_contacts();
-        $this->_fetch_groups();
-        $count = $this->gid ? count($this->distlists[$this->gid]['member']) : (is_array($this->filter['ids']) ? count($this->filter['ids']) : count($this->contacts));
+        if ($this->gid) {
+            $this->_fetch_groups();
+            $count = count($this->distlists[$this->gid]['member']);
+        }
+        else if (is_array($this->filter['ids'])) {
+            $count = count($this->filter['ids']);
+        }
+        else {
+            $count = $this->storagefolder->count();
+        }
+
         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
     }
 
@@ -488,11 +454,11 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function get_record($id, $assoc=false)
     {
-        $this->_fetch_contacts();
-        if ($this->contacts[$id]) {
+        if ($object = $this->storagefolder->get_object($this->_id2uid($id))) {
+            $rec = $this->_to_rcube_contact($object);
             $this->result = new rcube_result_set(1);
-            $this->result->add($this->contacts[$id]);
-            return $assoc ? $this->contacts[$id] : $this->result;
+            $this->result->add($rec);
+            return $assoc ? $rec : $this->result;
         }
 
         return false;
@@ -512,7 +478,7 @@ class rcube_kolab_contacts extends rcube_addressbook
 
         foreach ((array)$this->groupmembers[$id] as $gid) {
             if ($group = $this->distlists[$gid])
-                $out[$gid] = $group['last-name'];
+                $out[$gid] = $group['name'];
         }
 
         return $out;
@@ -546,13 +512,11 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
 
         if (!$existing) {
-            $this->_connect();
-
             // generate new Kolab contact item
             $object = $this->_from_rcube_contact($save_data);
-            $object['uid'] = $this->contactstorage->generateUID();
+            $object['uid'] = kolab_format::generate_uid();
 
-            $saved = $this->contactstorage->save($object);
+            $saved = $this->storagefolder->save($object, 'contact');
 
             if (PEAR::isError($saved)) {
                 raise_error(array(
@@ -565,7 +529,6 @@ class rcube_kolab_contacts extends rcube_addressbook
                 $contact = $this->_to_rcube_contact($object);
                 $id = $contact['ID'];
                 $this->contacts[$id] = $contact;
-                $this->id2uid[$id] = $object['uid'];
                 $insert_id = $id;
             }
         }
@@ -586,17 +549,15 @@ class rcube_kolab_contacts extends rcube_addressbook
     public function update($id, $save_data)
     {
         $updated = false;
-        $this->_fetch_contacts();
-        if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
-            $old = $this->contactstorage->getObject($uid);
+        if ($old = $this->storagefolder->get_object($this->_id2uid($id))) {
             $object = array_merge($old, $this->_from_rcube_contact($save_data));
 
-            $saved = $this->contactstorage->save($object, $uid);
-            if (PEAR::isError($saved)) {
+            $saved = $this->storagefolder->save($object, 'contact', $uid);
+            if (!$saved || PEAR::isError($saved)) {
                 raise_error(array(
                   'code' => 600, 'type' => 'php',
                   'file' => __FILE__, 'line' => __LINE__,
-                  'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
+                  'message' => "Error saving contact object to Kolab server"),
                 true, false);
             }
             else {
@@ -619,25 +580,21 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function delete($ids, $force=true)
     {
-        $this->_fetch_contacts();
         $this->_fetch_groups();
 
         if (!is_array($ids))
             $ids = explode(',', $ids);
 
         $count = 0;
-        $imap_uids = array();
-
         foreach ($ids as $id) {
-            if ($uid = $this->id2uid[$id]) {
-                $imap_uid = $this->contactstorage->_getStorageId($uid);
-                $deleted = $this->contactstorage->delete($uid, $force);
+            if ($uid = $this->_id2uid($id)) {
+                $deleted = $this->storagefolder->delete($uid, $force);
 
-                if (PEAR::isError($deleted)) {
+                if (!$deleted) {
                     raise_error(array(
                       'code' => 600, 'type' => 'php',
                       'file' => __FILE__, 'line' => __LINE__,
-                      'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
+                      'message' => "Error deleting a contact object from the Kolab server"),
                     true, false);
                 }
                 else {
@@ -645,19 +602,13 @@ class rcube_kolab_contacts extends rcube_addressbook
                     foreach ((array)$this->groupmembers[$id] as $gid)
                         $this->remove_from_group($gid, $id);
 
-                    $imap_uids[$id] = $imap_uid;
                     // clear internal cache
-                    unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
+                    unset($this->contacts[$id], $this->groupmembers[$id]);
                     $count++;
                 }
             }
         }
 
-        // store IMAP uids for undelete()
-        if (!$force) {
-            $_SESSION['kolab_delete_uids'] = $imap_uids;
-        }
-
         return $count;
     }
 
@@ -677,6 +628,9 @@ class rcube_kolab_contacts extends rcube_addressbook
 
         $count     = 0;
         $uids      = array();
+/*
+        TODO: re-implement this using kolab_storage_folder::undelete();
+
         $imap_uids = $_SESSION['kolab_delete_uids'];
 
         // convert contact IDs into IMAP UIDs
@@ -703,7 +657,7 @@ class rcube_kolab_contacts extends rcube_addressbook
                     }
                     else {
                         $this->_connect();
-                        $this->contactstorage->synchronize();
+                        $this->storagefolder->synchronize();
                     }
                 }
             }
@@ -719,7 +673,7 @@ class rcube_kolab_contacts extends rcube_addressbook
             $rcmail = rcmail::get_instance();
             $rcmail->session->remove('kolab_delete_uids');
         }
-
+*/
         return count($uids);
     }
 
@@ -729,11 +683,8 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     public function delete_all()
     {
-        $this->_connect();
-
-        if (!PEAR::isError($this->contactstorage->deleteAll())) {
+        if ($this->storagefolder->delete_all()) {
             $this->contacts = array();
-            $this->id2uid = array();
             $this->result = null;
         }
     }
@@ -760,11 +711,11 @@ class rcube_kolab_contacts extends rcube_addressbook
         $result = false;
 
         $list = array(
-            'uid' => $this->liststorage->generateUID(),
-            'last-name' => $name,
+            'uid' => kolab_format::generate_uid(),
+            'name' => $name,
             'member' => array(),
         );
-        $saved = $this->liststorage->save($list);
+        $saved = $this->storagefolder->save($list, 'distribution-list');
 
         if (PEAR::isError($saved)) {
             raise_error(array(
@@ -795,7 +746,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         $result = false;
 
         if ($list = $this->distlists[$gid])
-            $deleted = $this->liststorage->delete($list['uid']);
+            $deleted = $this->storagefolder->delete($list['uid']);
 
         if (PEAR::isError($deleted)) {
             raise_error(array(
@@ -822,9 +773,9 @@ class rcube_kolab_contacts extends rcube_addressbook
         $this->_fetch_groups();
         $list = $this->distlists[$gid];
 
-        if ($newname != $list['last-name']) {
-            $list['last-name'] = $newname;
-            $saved = $this->liststorage->save($list, $list['uid']);
+        if ($newname != $list['name']) {
+            $list['name'] = $newname;
+            $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
         }
 
         if (PEAR::isError($saved)) {
@@ -865,7 +816,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         $ids = array_diff($ids, $exists);
 
         foreach ($ids as $contact_id) {
-            if ($uid = $this->id2uid[$contact_id]) {
+            if ($uid = $this->_id2uid($contact_id)) {
                 $contact = $this->contacts[$contact_id];
                 foreach ($this->get_col_values('email', $contact, true) as $email) {
                     $list['member'][] = array(
@@ -880,7 +831,7 @@ class rcube_kolab_contacts extends rcube_addressbook
         }
 
         if ($added)
-            $saved = $this->liststorage->save($list, $list['uid']);
+            $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
 
         if (PEAR::isError($saved)) {
             raise_error(array(
@@ -921,7 +872,7 @@ class rcube_kolab_contacts extends rcube_addressbook
 
         // write distribution list back to server
         $list['member'] = $new_member;
-        $saved = $this->liststorage->save($list, $list['uid']);
+        $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
 
         if (PEAR::isError($saved)) {
             raise_error(array(
@@ -970,36 +921,13 @@ class rcube_kolab_contacts extends rcube_addressbook
     }
 
     /**
-     * Establishes a connection to the Kolab_Data object for accessing contact data
-     */
-    private function _connect()
-    {
-        if (!isset($this->contactstorage)) {
-            $this->contactstorage = $this->storagefolder->getData(null);
-        }
-    }
-
-    /**
-     * Establishes a connection to the Kolab_Data object for accessing groups data
-     */
-    private function _connect_groups()
-    {
-        if (!isset($this->liststorage)) {
-            $this->liststorage = $this->storagefolder->getData('distributionlist');
-        }
-    }
-
-    /**
      * Simply fetch all records and store them in private member vars
      */
     private function _fetch_contacts()
     {
         if (!isset($this->contacts)) {
-            $this->_connect();
-
-            // read contacts
-            $this->contacts = $this->id2uid = array();
-            foreach ((array)$this->contactstorage->getObjects() as $record) {
+            $this->contacts = array();
+            foreach ((array)$this->storagefolder->get_objects() as $record) {
                 // Because of a bug, sometimes group records are returned
                 if ($record['__type'] == 'Group')
                     continue;
@@ -1007,7 +935,6 @@ class rcube_kolab_contacts extends rcube_addressbook
                 $contact = $this->_to_rcube_contact($record);
                 $id = $contact['ID'];
                 $this->contacts[$id] = $contact;
-                $this->id2uid[$id] = $record['uid'];
             }
         }
     }
@@ -1058,17 +985,11 @@ class rcube_kolab_contacts extends rcube_addressbook
     private function _fetch_groups()
     {
         if (!isset($this->distlists)) {
-            $this->_connect_groups();
-
             $this->distlists = $this->groupmembers = array();
-            foreach ((array)$this->liststorage->getObjects() as $record) {
-                // FIXME: folders without any distribution-list objects return contacts instead ?!
-                if ($record['__type'] != 'Group')
-                    continue;
-
-                $record['ID'] = md5($record['uid']);
+            foreach ((array)$this->storagefolder->get_objects('distribution-list') as $record) {
+                $record['ID'] = $this->_uid2id($record['uid']);
                 foreach ((array)$record['member'] as $i => $member) {
-                    $mid = md5($member['uid']);
+                    $mid = $this->_uid2id($member['uid']);
                     $record['member'][$i]['ID'] = $mid;
                     $this->groupmembers[$mid][] = $record['ID'];
                 }
@@ -1078,53 +999,59 @@ class rcube_kolab_contacts extends rcube_addressbook
     }
 
     /**
+     * Encode object UID into a safe identifier
+     */
+    private function _uid2id($uid)
+    {
+        return rtrim(strtr(base64_encode($uid), '+/', '-_'), '=');
+    }
+
+    /**
+     * Convert Roundcube object identifier back into the original UID
+     */
+    private function _id2uid($id)
+    {
+        return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
+    }
+
+    /**
      * Map fields from internal Kolab_Format to Roundcube contact format
      */
     private function _to_rcube_contact($record)
     {
-        $out = array(
-          'ID' => md5($record['uid']),
-          'email' => array(),
-          'phone' => array(),
-        );
-
-        foreach ($this->fieldmap as $kolab => $rcube) {
-          if (strlen($record[$kolab]))
-            $out[$rcube] = $record[$kolab];
+        $record['ID'] = $this->_uid2id($record['uid']);
+
+        if (is_array($record['phone'])) {
+            $phones = $record['phone'];
+            unset($record['phone']);
+            foreach ((array)$phones as $i => $phone) {
+                $key = 'phone' . ($phone['type'] ? ':' . $phone['type'] : '');
+                $record[$key][] = $phone['number'];
+            }
         }
 
-        if (isset($record['gender']))
-            $out['gender'] = $this->gender_map[$record['gender']];
-
-        foreach ((array)$record['email'] as $i => $email)
-            $out['email'][] = $email['smtp-address'];
-
-        if (!$record['email'] && $record['emails'])
-            $out['email'] = preg_split('/,\s*/', $record['emails']);
-
-        foreach ((array)$record['phone'] as $i => $phone)
-            $out['phone:'.$phone['type']][] = $phone['number'];
-
         if (is_array($record['address'])) {
-            foreach ($record['address'] as $i => $adr) {
-                $key = 'address:' . $adr['type'];
-                $out[$key][] = array(
-                    'street' => $adr['street'],
+            $addresses = $record['address'];
+            unset($record['address']);
+            foreach ($addresses as $i => $adr) {
+                $key = 'address' . ($adr['type'] ? ':' . $adr['type'] : '');
+                $record[$key][] = array(
+                    'street'   => $adr['street'],
                     'locality' => $adr['locality'],
-                    'zipcode' => $adr['postal-code'],
-                    'region' => $adr['region'],
-                    'country' => $adr['country'],
+                    'zipcode'  => $adr['code'],
+                    'region'   => $adr['region'],
+                    'country'  => $adr['country'],
                 );
             }
         }
 
         // photo is stored as separate attachment
-        if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
-            $out['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
+        if ($record['photo'] && ($att = $record['_attachments'][$record['photo']])) {
+            $out['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['key']);
         }
 
         // remove empty fields
-        return array_filter($out);
+        return array_filter($record);
     }
 
     /**
@@ -1132,101 +1059,61 @@ class rcube_kolab_contacts extends rcube_addressbook
      */
     private function _from_rcube_contact($contact)
     {
-        $object = array();
-
-        foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
-            if (isset($contact[$rcube]))
-                $object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
-            else if ($values = $this->get_col_values($rcube, $contact, true))
-                $object[$kolab] = is_array($values) ? $values[0] : $values;
-        }
+        if (!$contact['uid'] && $contact['ID'])
+            $contact['uid'] = $this->_id2uid($contact['ID']);
 
+/*
         // format dates
         if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
             $object['birthday'] = date('Y-m-d', $date);
         if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
             $object['anniversary'] = date('Y-m-d', $date);
-
-        $gendermap = array_flip($this->gender_map);
-        if (isset($object['gender']))
-            $object['gender'] = $gendermap[$object['gender']];
-
-        $emails = $this->get_col_values('email', $contact, true);
-        $object['emails'] = join(', ', array_filter($emails));
-        // overwrite 'email' field
-        $object['email'] = null;
+*/
+        $contact['email'] = array_filter($this->get_col_values('email', $contact, true));
+        $contact['website'] = array_filter($this->get_col_values('website', $contact, true));
+        $contact['im'] = array_filter($this->get_col_values('im', $contact, true));
 
         foreach ($this->get_col_values('phone', $contact) as $type => $values) {
-            if ($this->phonetypemap[$type])
-                $type = $this->phonetypemap[$type];
             foreach ((array)$values as $phone) {
                 if (!empty($phone)) {
-                    $object['phone-' . $type] = $phone;
-                    $object['phone'][] = array('number' => $phone, 'type' => $type);
+                    $contact['phone'][] = array('number' => $phone, 'type' => $type);
                 }
             }
         }
 
-        $object['address'] = array();
-
+        $addresses = array();
         foreach ($this->get_col_values('address', $contact) as $type => $values) {
-            if ($this->addresstypemap[$type])
-                $type = $this->addresstypemap[$type];
-
-            $updated = false;
-            $basekey = 'addr-' . $type . '-';
             foreach ((array)$values as $adr) {
                 // skip empty address
                 $adr = array_filter($adr);
                 if (empty($adr))
                     continue;
 
-                // switch type if slot is already taken
-                if (isset($object[$basekey . 'type'])) {
-                    $type = $type == 'home' ? 'business' : 'home';
-                    $basekey = 'addr-' . $type . '-';
-                }
-
-                if (!isset($object[$basekey . 'type'])) {
-                    $object[$basekey . 'type'] = $type;
-                    $object[$basekey . 'street'] = $adr['street'];
-                    $object[$basekey . 'locality'] = $adr['locality'];
-                    $object[$basekey . 'postal-code'] = $adr['zipcode'];
-                    $object[$basekey . 'region'] = $adr['region'];
-                    $object[$basekey . 'country'] = $adr['country'];
-
-                    // Update existing address entry of this type
-                    foreach($object['address'] as $index => $address) {
-                        if ($address['type'] == $type) {
-                            $object['address'][$index] = $new_address;
-                            $updated = true;
-                        }
-                    }
-                }
-                if (!$updated) {
-                    $object['address'][] = array(
-                        'type' => $type,
-                        'street' => $adr['street'],
-                        'locality' => $adr['locality'],
-                        'postal-code' => $adr['zipcode'],
-                        'region' => $adr['region'],
-                        'country' => $adr['country'],
-                    );
-                }
+                $addresses[] = array(
+                    'type' => $type,
+                    'street' => $adr['street'],
+                    'locality' => $adr['locality'],
+                    'code' => $adr['zipcode'],
+                    'region' => $adr['region'],
+                    'country' => $adr['country'],
+                );
             }
+
+            unset($contact['address:'.$type]);
         }
+        $contact['address'] = $addresses;
 
         // save new photo as attachment
         if ($contact['photo']) {
           $attkey = 'photo.attachment';
-          $object['_attachments'][$attkey] = array(
+          $contact['_attachments'][$attkey] = array(
             'type' => rc_image_content_type($contact['photo']),
             'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
           );
-          $object['picture'] = $attkey;
+          $contact['photo'] = $attkey;
         }
 
-        return $object;
+        return $contact;
     }
 
 }


commit a58b5fb302feea115199b117bbf0865b875a4dad
Author: Thomas B <roundcube at gmail.com>
Date:   Wed Mar 7 11:01:11 2012 +0100

    New method to return counts; some code cleanup

diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index b77bad0..d82c5c6 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -129,11 +129,27 @@ class kolab_storage_folder
      *
      * @return string  Permissions as string
      */
-    function get_acl()
+    public function get_acl()
     {
         return join('', (array)$this->imap->get_acl($this->name));
     }
 
+    /**
+     * Get number of objects stored in this folder
+     *
+     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
+     * @return integer The number of objects of the given type
+     */
+    public function count($type = null)
+    {
+        if (!$type) $type = $this->type;
+
+        // search by object type
+        $ctype  = self::KTYPE_PREFIX . $type;
+        $index = $this->imap->search_once($this->name, 'HEADER X-Kolab-Type ' . $ctype);
+
+        return $index->count();
+    }
 
     /**
      * List all Kolab objects of the given type
@@ -141,7 +157,7 @@ class kolab_storage_folder
      * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
      * @return array  List of Kolab data objects (each represented as hash array)
      */
-    function get_objects($type = null)
+    public function get_objects($type = null)
     {
         if (!$type) $type = $this->type;
 
@@ -303,7 +319,7 @@ class kolab_storage_folder
      *
      * @return boolean True if successful, false on error
      */
-    function delete($object, $expunge = true, $trigger = true)
+    public function delete($object, $expunge = true, $trigger = true)
     {
         if ($msguid = is_array($object) ? $object['_msguid'] : $this->uid2msguid($object)) {
             return $this->imap->delete_message($msguid, $this->name);
@@ -323,6 +339,19 @@ class kolab_storage_folder
 
 
     /**
+     * Restore a previously deleted object
+     *
+     * @param string Object UID
+     * @return mixed Message UID on success, false on error
+     */
+    public function undelete($uid)
+    {
+        // TODO: implement this
+        return false;
+    }
+
+
+    /**
      * Resolve an object UID into an IMAP message UID
      */
     private function uid2msguid($uid)


commit dc1fd9f7eb21f37410c69135007f90809de34215
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Mar 6 22:23:34 2012 +0100

    More work-in-progress on Kolab 3.0 storage layer

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 8681fb7..928ee2f 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -72,7 +72,7 @@ abstract class kolab_format
 
         if (is_a($datetime, 'DateTime')) {
             $result = new KolabDateTime();
-            $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'), 0, 0, 0);
+            $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
 
             if (!$dateonly)
                 $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
@@ -89,15 +89,29 @@ abstract class kolab_format
      * @param object vector Object
      * @return array Indexed array contaning vector elements
      */
-    public static function vector2array($vec)
+    public static function vector2array($vec, $max = PHP_INT_MAX)
     {
         $arr = array();
-        for ($i=0; $i < $vec->size(); $i++)
+        for ($i=0; $i < $vec->size() && $i < $max; $i++)
             $arr[] = $vec->get($i);
         return $arr;
     }
 
     /**
+     * Build a libkolabxml vector (string) from a PHP array
+     *
+     * @param array Array with vector elements
+     * @return object vectors
+     */
+    public static function array2vector($arr)
+    {
+        $vec = new vectors;
+        foreach ((array)$arr as $val)
+            $vec->push($val);
+        return $vec;
+    }
+
+    /**
      * Load Kolab object data from the given XML block
      *
      * @param string XML data
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 918355f..4dbcaf8 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -4,7 +4,26 @@
 class kolab_format_contact extends kolab_format
 {
     public $CTYPE = 'application/vcard+xml';
-    
+
+    public $phonetypes = array(
+        'home'    => Telephone::Home,
+        'work'    => Telephone::Work,
+        'text'    => Telephone::Text,
+        'main'    => Telephone::Voice,
+        'homefax' => Telephone::Fax,
+        'workfax' => Telephone::Fax,
+        'mobile'  => Telephone::Cell,
+        'video'   => Telephone::Video,
+        'pager'   => Telephone::Pager,
+        'car'     => Telephone::Car,
+        'other'   => Telephone::Textphone,
+    );
+
+    public $addresstypes = array(
+        'home' => Address::Home,
+        'work' => Address::Work,
+    );
+
     private $data;
     private $obj;
 
@@ -36,12 +55,29 @@ class kolab_format_contact extends kolab_format
       'body'         => 'notes',
       'pgp-publickey' => 'pgppublickey',
       'free-busy-url' => 'freebusyurl',
-      'gender'       => 'gender',
     );
+    private $kolab2_phonetypes = array(
+        'home1' => 'home',
+        'business1' => 'work',
+        'business2' => 'work',
+        'businessfax' => 'workfax',
+    );
+    private $kolab2_addresstypes = array(
+        'business' => 'work'
+    );
+    private $kolab2_gender = array(0 => 'male', 1 => 'female');
 
+
+    /**
+     * Default constructor
+     */
     function __construct()
     {
         $this->obj = new Contact;
+
+        // complete phone types
+        $this->phonetypes['homefax'] |= Telephone::Home;
+        $this->phonetypes['workfax'] |= Telephone::Work;
     }
 
     /**
@@ -85,44 +121,49 @@ class kolab_format_contact extends kolab_format
         $this->obj->setUid($object['uid']);
 
         $nc = new NameComponents;
-        // surname
-        $sn = new vectors();
-        $sn->push($object['surname']);
-        $nc->setSurnames($sn);
-        // firstname
-        $gn = new vectors();
-        $gn->push($object['firstname']);
-        $nc->setGiven($gn);
-        // middle name
-        $mn = new vectors();
-        if ($object['middlename'])
-            $mn->push($object['middlename']);
-        $nc->setAdditional($mn);
-        // prefix
-        $px = new vectors();
-        if ($object['prefix'])
-            $px->push($object['prefix']);
-        $nc->setPrefixes($px);
-        // suffix
-        $sx = new vectors();
-        if ($object['suffix'])
-            $sx->push($object['suffix']);
-        $nc->setSuffixes($sx);
-
+        $nc->setSurnames(self::array2vector($object['surname']));
+        $nc->setGiven(self::array2vector($object['firstname']));
+        $nc->setAdditional(self::array2vector($object['middlename']));
+        $nc->setPrefixes(self::array2vector($object['prefix']));
+        $nc->setSuffixes(self::array2vector($object['suffix']));
         $this->obj->setNameComponents($nc);
         $this->obj->setName($object['name']);
 
-        // email addresses
-        $emails = new vectors;
-        foreach ($object['email'] as $em)
-            $emails->push($em);
-        $this->obj->setEmailAddresses($emails);
+        if ($object['nickname'])
+            $this->obj->setNickNames(self::array2vector($object['nickname']));
+
+        // organisation related properties (affiliation)
+        $org = new Affiliation;
+        if ($object['organization'])
+            $org->setOrganisation($object['organization']);
+        if ($object['jobtitle'])
+            $org->setTitles(self::array2vector($object['jobtitle']));
+        if ($object['officelocation'])
+            $org->setOffices(self::array2vector($object['officelocation']));
+        if ($object['manager'])
+            $org->setManagers(self::array2vector($object['manager']));
+        if ($object['assistant'])
+            $org->setAssistants(self::array2vector($object['assistant']));
+        // department ?
+
+        $orgs = new vectoraffiliation;
+        $orgs->push($org);
+        $this->obj->setAffiliations($orgs);
+
+        // email, im, url
+        $this->obj->setEmailAddresses(self::array2vector($object['email']));
+        $this->obj->setIMaddresses(self::array2vector($object['im']));
+        $this->obj->setUrls(self::array2vector($object['website']));
 
         // addresses
         $adrs = new vectoraddress;
         foreach ($object['address'] as $address) {
             $adr = new Address;
-            $adr->setTypes($address['type'] == 'work' ? Address::Work : Address::Home);
+            $type = $this->addresstypes[$address['type']];
+            if (isset($type))
+                $adr->setTypes($type);
+            else if ($address['type'])
+                $adr->setLabel($address['type']);
             if ($address['street'])
                 $adr->setStreet($address['street']);
             if ($address['locality'])
@@ -138,6 +179,30 @@ class kolab_format_contact extends kolab_format
         }
         $this->obj->setAddresses($adrs);
 
+        // telephones
+        $tels = new vectortelephone;
+        foreach ((array)$object['phone'] as $phone) {
+            $tel = new Telephone;
+            if (isset($this->phonetypes[$phone['type']]))
+                $tel->setTypes($this->phonetypes[$phone['type']]);
+            $tel->setNumber($phone['number']);
+            $tels->push($tel);
+        }
+        $this->obj->setTelephones($tels);
+
+        if ($object['gender'])
+            $this->obj->setGender($object['gender'] == 'female' ? Contact::Female : Contact::Male);
+        if ($object['notes'])
+            $this->obj->setNote($object['notes']);
+        if ($object['freebusyurl'])
+            $this->obj->setFreeBusyUrl($object['freebusyurl']);
+//        if ($object['birthday'])
+//            $this->obj->setBDay(self::getDateTime($object['birthday'], null, true));
+//        if ($object['anniversary'])
+//            $this->obj->setAnniversary(self::getDateTime($object['anniversary'], null, true));
+
+        // handle spouse, children, profession, initials, pgppublickey, etc.
+
         // cache this data
         $this->data = $object;
     }
@@ -150,7 +215,6 @@ class kolab_format_contact extends kolab_format
         return $this->data || (is_object($this->obj) && true /*$this->obj->isValid()*/);
     }
 
-
     /**
      * Convert the Contact object into a hash array data structure
      *
@@ -162,7 +226,7 @@ class kolab_format_contact extends kolab_format
         if (!empty($this->data))
             return $this->data;
 
-        // TODO: read object properties into local data object
+        // read object properties into local data object
         $object = array(
             'uid'       => $this->obj->uid(),
             # 'changed'   => $this->obj->lastModified(),
@@ -175,14 +239,30 @@ class kolab_format_contact extends kolab_format
         $object['middlename'] = join(' ', self::vector2array($nc->additional()));
         $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
         $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
+        $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
 
-        $object['email'] = self::vector2array($this->obj->emailAddresses());
+        // organisation related properties (affiliation)
+        $orgs = $this->obj->affiliations();
+        if ($orgs->size()) {
+            $org = $orgs->get(0);
+            $object['organization']   = $org->organisation();
+            $object['jobtitle']       = join(' ', self::vector2array($org->titles()));
+            $object['manager']        = join(' ', self::vector2array($org->managers()));
+            $object['assistant']      = join(' ', self::vector2array($org->assistants()));
+            $object['officelocation'] = join(' ', self::vector2array($org->offices()));
+        }
+
+        $object['email']   = self::vector2array($this->obj->emailAddresses());
+        $object['im']      = self::vector2array($this->obj->imAddresses());
+        $object['website'] = self::vector2array($this->obj->urls());
 
+        // addresses
+        $adrtypes = array_flip($this->addresstypes);
         $addresses = $this->obj->addresses();
         for ($i=0; $i < $addresses->size(); $i++) {
             $adr = $addresses->get($i);
             $object['address'][] = array(
-                'type'     => $adr->types() == Address::Work ? 'work' : 'home',
+                'type'     => $adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : '', /*$adr->label(),*/
                 'street'   => $adr->street(),
                 'code'     => $adr->code(),
                 'locality' => $adr->locality(),
@@ -191,6 +271,20 @@ class kolab_format_contact extends kolab_format
             );
         }
 
+        // telehones
+        $tels = $this->obj->telephones();
+        $teltypes = array_flip($this->phonetypes);
+        for ($i=0; $i < $tels->size(); $i++) {
+            $tel = $tels->get($i);
+            $object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
+        }
+
+        $object['notes'] = $this->obj->note();
+        $object['freebusyurl'] = $this->obj->freeBusyUrl();
+        
+        if ($g = $this->obj->gender())
+            $object['gender'] = $g == Contact::Female ? 'female' : 'male';
+
         $this->data = $object;
         return $this->data;
     }
@@ -212,7 +306,7 @@ class kolab_format_contact extends kolab_format
         }
 
         if (isset($record['gender']))
-            $object['gender'] = $this->gender_map[$record['gender']];
+            $object['gender'] = $this->kolab2_gender[$record['gender']];
 
         foreach ((array)$record['email'] as $i => $email)
             $object['email'][] = $email['smtp-address'];
@@ -223,7 +317,7 @@ class kolab_format_contact extends kolab_format
         if (is_array($record['address'])) {
             foreach ($record['address'] as $i => $adr) {
                 $object['address'][] = array(
-                    'type' => $adr['type'],
+                    'type' => $this->kolab2_addresstypes[$adr['type']] ? $this->kolab2_addresstypes[$adr['type']] : $adr['type'],
                     'street' => $adr['street'],
                     'locality' => $adr['locality'],
                     'code' => $adr['postal-code'],
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 0c15be7..b77bad0 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -287,6 +287,7 @@ class kolab_storage_folder
             }
 
             // TODO: update cache with new UID
+            $this->uid2msg[$object['uid']] = $result;
         }
         
         return $result;
@@ -296,16 +297,16 @@ class kolab_storage_folder
     /**
      * Delete the specified object from this folder.
      *
-     * @param  array   $object  The Kolab object to delete
+     * @param  mixed   $object  The Kolab object to delete or object UID
      * @param  boolean $trigger Should the folder be triggered?
      * @param  boolean $expunge Should the folder be expunged?
      *
      * @return boolean True if successful, false on error
      */
-    function delete($object, $trigger = true, $expunge = true)
+    function delete($object, $expunge = true, $trigger = true)
     {
-        if (!empty($object['_msguid'])) {
-            return $this->imap->delete_message($object['_msguid'], $this->name);
+        if ($msguid = is_array($object) ? $object['_msguid'] : $this->uid2msguid($object)) {
+            return $this->imap->delete_message($msguid, $this->name);
         }
 
         return false;


commit 38bdff1cd0b25f34c1cae29ebc58da85cf81e34b
Author: Thomas B <roundcube at gmail.com>
Date:   Tue Mar 6 09:58:01 2012 +0100

    New Roundcube plugin for Kolab 3.0 storage layer

diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
new file mode 100644
index 0000000..8681fb7
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * Kolab format model class wrapping libkolabxml bindings
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+abstract class kolab_format
+{
+    public static $timezone;
+
+    /**
+     * Factory method to instantiate a kolab_format object of the given type
+     */
+    public static function factory($type)
+    {
+        if (!isset(self::$timezone))
+            self::$timezone = new DateTimeZone('UTC');
+
+        $suffix = preg_replace('/[^a-z]+/', '', $type);
+        $classname = 'kolab_format_' . $suffix;
+        if (class_exists($classname))
+            return new $classname();
+
+        return PEAR::raiseError(sprintf("Failed to load Kolab Format wrapper for type %s", $type));
+    }
+
+    /**
+     * Generate random UID for Kolab objects
+     *
+     * @return string  MD5 hash with a unique value
+     */
+    public static function generate_uid()
+    {
+        return md5(uniqid(mt_rand(), true));
+    }
+
+    /**
+     * Convert the given date/time value into a c_DateTime object
+     *
+     * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
+     * @param DateTimeZone  The timezone the date/time is in. Use global default if empty
+     * @param boolean       True of the given date has no time component
+     * @return c_DateTime   The libkolabxml date/time object or null on error
+     */
+    public static function getDateTime($datetime, $tz = null, $dateonly = false)
+    {
+        if (!$tz) $tz = self::$timezone;
+        $result = null;
+
+        if (is_numeric($datetime))
+            $datetime = new DateTime('@'.$datetime, $tz);
+        else if (is_string($datetime))
+            $datetime = new DateTime($datetime, $tz);
+
+        if (is_a($datetime, 'DateTime')) {
+            $result = new KolabDateTime();
+            $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'), 0, 0, 0);
+
+            if (!$dateonly)
+                $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
+            if ($tz)
+                $result->setTimezone($tz->getName());
+        }
+
+        return $result;
+    }
+
+    /**
+     * Convert a libkolabxml vector to a PHP array
+     *
+     * @param object vector Object
+     * @return array Indexed array contaning vector elements
+     */
+    public static function vector2array($vec)
+    {
+        $arr = array();
+        for ($i=0; $i < $vec->size(); $i++)
+            $arr[] = $vec->get($i);
+        return $arr;
+    }
+
+    /**
+     * Load Kolab object data from the given XML block
+     *
+     * @param string XML data
+     */
+    abstract public function load($xml);
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    abstract public function set(&$object);
+
+    /**
+     *
+     */
+    abstract public function is_valid();
+
+    /**
+     * Write object data to XML format
+     *
+     * @return string XML data
+     */
+    abstract public function write();
+
+    /**
+     * Convert the Kolab object into a hash array data structure
+     *
+     * @return array  Kolab object data as hash array
+     */
+    abstract public function to_array();
+
+}
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
new file mode 100644
index 0000000..918355f
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -0,0 +1,244 @@
+<?php
+
+
+class kolab_format_contact extends kolab_format
+{
+    public $CTYPE = 'application/vcard+xml';
+    
+    private $data;
+    private $obj;
+
+    // old Kolab 2 format field map
+    private $kolab2_fieldmap = array(
+      // kolab       => roundcube
+      'full-name'    => 'name',
+      'given-name'   => 'firstname',
+      'middle-names' => 'middlename',
+      'last-name'    => 'surname',
+      'prefix'       => 'prefix',
+      'suffix'       => 'suffix',
+      'nick-name'    => 'nickname',
+      'organization' => 'organization',
+      'department'   => 'department',
+      'job-title'    => 'jobtitle',
+      'initials'     => 'initials',
+      'birthday'     => 'birthday',
+      'anniversary'  => 'anniversary',
+      'phone'        => 'phone',
+      'im-address'   => 'im',
+      'web-page'     => 'website',
+      'office-location' => 'officelocation',
+      'profession'   => 'profession',
+      'manager-name' => 'manager',
+      'assistant'    => 'assistant',
+      'spouse-name'  => 'spouse',
+      'children'     => 'children',
+      'body'         => 'notes',
+      'pgp-publickey' => 'pgppublickey',
+      'free-busy-url' => 'freebusyurl',
+      'gender'       => 'gender',
+    );
+
+    function __construct()
+    {
+        $this->obj = new Contact;
+    }
+
+    /**
+     * Load Contact object data from the given XML block
+     *
+     * @param string XML data
+     */
+    public function load($xml)
+    {
+        $this->obj = kolabformat::readContact($xml, false);
+    }
+
+    /**
+     * Write Contact object data to XML format
+     *
+     * @return string XML data
+     */
+    public function write()
+    {
+        return kolabformat::writeContact($this->obj);
+    }
+
+    /**
+     * Set contact properties to the kolabformat object
+     *
+     * @param array  Contact data as hash array
+     */
+    public function set(&$object)
+    {
+        // set some automatic values if missing
+        if (empty($object['uid']))
+            $object['uid'] = self::generate_uid();
+
+        if (false && !$this->obj->created()) {
+            if (!empty($object['created']))
+                $object['created'] = new DateTime('now', self::$timezone);
+            $this->obj->setCreated(self::getDateTime($object['created']));
+        }
+
+        // do the hard work of setting object values
+        $this->obj->setUid($object['uid']);
+
+        $nc = new NameComponents;
+        // surname
+        $sn = new vectors();
+        $sn->push($object['surname']);
+        $nc->setSurnames($sn);
+        // firstname
+        $gn = new vectors();
+        $gn->push($object['firstname']);
+        $nc->setGiven($gn);
+        // middle name
+        $mn = new vectors();
+        if ($object['middlename'])
+            $mn->push($object['middlename']);
+        $nc->setAdditional($mn);
+        // prefix
+        $px = new vectors();
+        if ($object['prefix'])
+            $px->push($object['prefix']);
+        $nc->setPrefixes($px);
+        // suffix
+        $sx = new vectors();
+        if ($object['suffix'])
+            $sx->push($object['suffix']);
+        $nc->setSuffixes($sx);
+
+        $this->obj->setNameComponents($nc);
+        $this->obj->setName($object['name']);
+
+        // email addresses
+        $emails = new vectors;
+        foreach ($object['email'] as $em)
+            $emails->push($em);
+        $this->obj->setEmailAddresses($emails);
+
+        // addresses
+        $adrs = new vectoraddress;
+        foreach ($object['address'] as $address) {
+            $adr = new Address;
+            $adr->setTypes($address['type'] == 'work' ? Address::Work : Address::Home);
+            if ($address['street'])
+                $adr->setStreet($address['street']);
+            if ($address['locality'])
+                $adr->setLocality($address['locality']);
+            if ($address['code'])
+                $adr->setCode($address['code']);
+            if ($address['region'])
+                $adr->setRegion($address['region']);
+            if ($address['country'])
+                $adr->setCountry($address['country']);
+
+            $adrs->push($adr);
+        }
+        $this->obj->setAddresses($adrs);
+
+        // cache this data
+        $this->data = $object;
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && true /*$this->obj->isValid()*/);
+    }
+
+
+    /**
+     * Convert the Contact object into a hash array data structure
+     *
+     * @return array  Contact data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        // TODO: read object properties into local data object
+        $object = array(
+            'uid'       => $this->obj->uid(),
+            # 'changed'   => $this->obj->lastModified(),
+            'name'      => $this->obj->name(),
+        );
+
+        $nc = $this->obj->nameComponents();
+        $object['surname']    = join(' ', self::vector2array($nc->surnames()));
+        $object['firstname']  = join(' ', self::vector2array($nc->given()));
+        $object['middlename'] = join(' ', self::vector2array($nc->additional()));
+        $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
+        $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
+
+        $object['email'] = self::vector2array($this->obj->emailAddresses());
+
+        $addresses = $this->obj->addresses();
+        for ($i=0; $i < $addresses->size(); $i++) {
+            $adr = $addresses->get($i);
+            $object['address'][] = array(
+                'type'     => $adr->types() == Address::Work ? 'work' : 'home',
+                'street'   => $adr->street(),
+                'code'     => $adr->code(),
+                'locality' => $adr->locality(),
+                'region'   => $adr->region(),
+                'country'  => $adr->country()
+            );
+        }
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+          'uid' => $record['uid'],
+          'email' => array(),
+          'phone' => array(),
+        );
+
+        foreach ($this->kolab2_fieldmap as $kolab => $rcube) {
+          if (is_array($record[$kolab]) || strlen($record[$kolab]))
+            $object[$rcube] = $record[$kolab];
+        }
+
+        if (isset($record['gender']))
+            $object['gender'] = $this->gender_map[$record['gender']];
+
+        foreach ((array)$record['email'] as $i => $email)
+            $object['email'][] = $email['smtp-address'];
+
+        if (!$record['email'] && $record['emails'])
+            $object['email'] = preg_split('/,\s*/', $record['emails']);
+
+        if (is_array($record['address'])) {
+            foreach ($record['address'] as $i => $adr) {
+                $object['address'][] = array(
+                    'type' => $adr['type'],
+                    'street' => $adr['street'],
+                    'locality' => $adr['locality'],
+                    'code' => $adr['postal-code'],
+                    'region' => $adr['region'],
+                    'country' => $adr['country'],
+                );
+            }
+        }
+
+        // photo is stored as separate attachment
+        if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
+            $object['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
+        }
+
+        // remove empty fields
+        $this->data = array_filter($object);
+    }
+}
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
new file mode 100644
index 0000000..a1f5891
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -0,0 +1,126 @@
+<?php
+
+
+class kolab_format_distributionlist extends kolab_format
+{
+    public $CTYPE = 'application/vcard+xml';
+    
+    private $data;
+    private $obj;
+
+    function __construct()
+    {
+        $obj = new DistList;
+    }
+
+    /**
+     * Load Kolab object data from the given XML block
+     *
+     * @param string XML data
+     */
+    public function load($xml)
+    {
+        $this->obj = kolabformat::readDistlist($xml, false);
+    }
+
+    /**
+     * Write object data to XML format
+     *
+     * @return string XML data
+     */
+    public function write()
+    {
+        return kolabformat::writeDistlist($this->obj);
+    }
+
+    public function set(&$object)
+    {
+        // TODO: do the hard work of setting object values
+    }
+
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && true /*$this->obj->isValid()*/);
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+            'uid'     => $record['uid'],
+            'changed' => $record['last-modification-date'],
+            'name'    => $record['last-name'],
+            'member'  => array(),
+        );
+
+        foreach ($record['member'] as $member) {
+            $object['member'][] = array(
+                'mailto' => $member['smtp-address'],
+                'name' => $member['display-name'],
+                'uid' => $member['uid'],
+            );
+        }
+
+        $this->data = $object;
+    }
+
+    /**
+     * Convert the Distlist object into a hash array data structure
+     *
+     * @return array  Distribution list data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        // read object properties
+        $object = array(
+            'uid'       => $this->obj->uid(),
+#           'changed'   => $this->obj->lastModified(),
+            'name'      => $this->obj->name(),
+            'member'    => array(),
+        );
+
+        $members = $this->obj->members();
+        for ($i=0; $i < $members->size(); $i++) {
+            $adr = self::decode_member($members->get($i));
+            if ($adr[0]['mailto'])
+                $object['member'][] = array(
+                    'mailto' => $adr[0]['mailto'],
+                    'name' => $adr[0]['name'],
+                    'uid' => '????',
+                );
+        }
+
+        return $this->data;
+    }
+
+    /**
+     * Compose a valid Mailto URL according to RFC 822
+     *
+     * @param string E-mail address
+     * @param string Person name
+     * @return string Formatted string
+     */
+    public static function format_member($email, $name = '')
+    {
+        // let Roundcube internals do the job
+        return 'mailto:' . format_email_recipient($email, $name);
+    }
+
+    /**
+     * Split a mailto: url into a structured member component
+     *
+     * @param string RFC 822 mailto: string
+     * @return array Hash array with member properties
+     */
+    public static function decode_member($str)
+    {
+        $adr = rcube_mime::decode_address_list(preg_replace('/^mailto:/', '', $str));
+        return $adr[0];
+    }
+}
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
new file mode 100644
index 0000000..d8ab276
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -0,0 +1,46 @@
+<?php
+
+
+class kolab_format_event extends kolab_format
+{
+    public $CTYPE = 'application/calendar+xml';
+    
+    private $data;
+    private $obj;
+
+    function __construct()
+    {
+        $obj = new Event;
+    }
+
+    public function load($xml)
+    {
+        $this->obj = kolabformat::readEvent($xml, false);
+    }
+
+    public function write()
+    {
+        return kolabformat::writeEvent($this->obj);
+    }
+
+    public function set(&$object)
+    {
+        // TODO: do the hard work of setting object values
+    }
+
+    public function is_valid()
+    {
+        return is_object($this->obj) && $this->obj->isValid();
+    }
+
+    public function fromkolab2($object)
+    {
+        $this->data = $object;
+    }
+
+    public function to_array()
+    {
+        // TODO: read object properties
+        return $this->data;
+    }
+}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
new file mode 100644
index 0000000..ec93213
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -0,0 +1,206 @@
+<?php
+
+/**
+ * Kolab storage class providing static methods to access groupware objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage
+{
+    const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
+
+    public static $last_error;
+
+    private static $ready = false;
+    private static $config;
+    private static $cache;
+    private static $imap;
+
+
+    /**
+     * Setup the environment needed by the libs
+     */
+    public static function setup()
+    {
+        if (self::$ready)
+            return;
+
+        $rcmail = rcmail::get_instance();
+        self::$config = $rcmail->config;
+        self::$imap = $rcmail->get_storage();
+        self::$ready = class_exists('kolabformat') && $rcmail->storage_connect() &&
+            (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
+
+        if (self::$ready) {
+            // set imap options
+            self::$imap->set_options(array(
+                'skip_deleted' => true,
+                'threading' => false,
+            ));
+            self::$imap->set_pagesize(9999);
+        }
+    }
+
+
+    /**
+     * Get a list of storage folders for the given data type
+     *
+     * @param string Data type to list folders for (contact,event,task,note)
+     *
+     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+     */
+    public static function get_folders($type)
+    {
+        self::setup();
+        $folders = array();
+
+        if (self::$ready) {
+            foreach ((array)self::$imap->list_folders('', '*', $type) as $foldername) {
+                $folders[$foldername] = new kolab_storage_folder($foldername, self::$imap);
+            }
+        }
+
+        return $folders;
+    }
+
+
+    /**
+     * Getter for a specific storage folder
+     *
+     * @param string  IMAP folder to access (UTF7-IMAP)
+     * @return object Kolab_Folder  The folder object
+     */
+    public static function get_folder($folder)
+    {
+        self::setup();
+        return self::$ready ? new kolab_storage_folder($folder, null, self::$imap) : null;
+    }
+
+
+    /**
+     *
+     */
+    public static function get_freebusy_server()
+    {
+        return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
+    }
+
+
+    /**
+     * Compose an URL to query the free/busy status for the given user
+     */
+    public static function get_freebusy_url($email)
+    {
+        return self::get_freebusy_server() . '/' . $email . '.ifb';
+    }
+
+
+    /**
+     * Creates folder ID from folder name
+     *
+     * @param string $folder Folder name (UTF7-IMAP)
+     *
+     * @return string Folder ID string
+     */
+    public static function folder_id($folder)
+    {
+        return asciiwords(strtr($folder, '/.-', '___'));
+    }
+
+
+    /**
+     * Getter for human-readable name of Kolab object (folder)
+     * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
+     *
+     * @param string $folder    IMAP folder name (UTF7-IMAP)
+     * @param string $folder_ns Will be set to namespace name of the folder
+     *
+     * @return string Name of the folder-object
+     */
+    public static function object_name($folder, &$folder_ns=null)
+    {
+        self::setup();
+
+        $found     = false;
+        $namespace = self::$imap->get_namespace();
+
+        if (!empty($namespace['shared'])) {
+            foreach ($namespace['shared'] as $ns) {
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+                    $prefix = '';
+                    $folder = substr($folder, strlen($ns[0]));
+                    $delim  = $ns[1];
+                    $found  = true;
+                    $folder_ns = 'shared';
+                    break;
+                }
+            }
+        }
+        if (!$found && !empty($namespace['other'])) {
+            foreach ($namespace['other'] as $ns) {
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+                    // remove namespace prefix
+                    $folder = substr($folder, strlen($ns[0]));
+                    $delim  = $ns[1];
+                    // get username
+                    $pos    = strpos($folder, $delim);
+                    if ($pos) {
+                        $prefix = '('.substr($folder, 0, $pos).') ';
+                        $folder = substr($folder, $pos+1);
+                    }
+                    else {
+                        $prefix = '('.$folder.')';
+                        $folder = '';
+                    }
+                    $found  = true;
+                    $folder_ns = 'other';
+                    break;
+                }
+            }
+        }
+        if (!$found && !empty($namespace['personal'])) {
+            foreach ($namespace['personal'] as $ns) {
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+                    // remove namespace prefix
+                    $folder = substr($folder, strlen($ns[0]));
+                    $prefix = '';
+                    $delim  = $ns[1];
+                    $found  = true;
+                    break;
+                }
+            }
+        }
+
+        if (empty($delim))
+            $delim = self::$imap->get_hierarchy_delimiter();
+
+        $folder = rcube_charset::convert($folder, 'UTF7-IMAP');
+        $folder = str_replace($delim, ' » ', $folder);
+
+        if ($prefix)
+            $folder = $prefix . ' ' . $folder;
+
+        if (!$folder_ns)
+            $folder_ns = 'personal';
+
+        return $folder;
+    }
+
+}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
new file mode 100644
index 0000000..0c15be7
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -0,0 +1,498 @@
+<?php
+
+/**
+ * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder
+{
+    const KTYPE_PREFIX = 'application/x-vnd.kolab.';
+
+    /**
+     * The folder name.
+     *
+     * @var string
+     */
+    public $name;
+
+    /**
+     * The type of this folder.
+     *
+     * @var string
+     */
+    public $type;
+
+    private $type_annotation;
+    private $subpath;
+    private $imap;
+    private $info;
+    private $owner;
+    private $uid2msg = array();
+
+
+    /**
+     * Default constructor
+     */
+    function __construct($name, $imap = null)
+    {
+        $this->name = $name;
+        $this->imap = is_object($imap) ? $imap : rcmail::get_instance()->get_storage();
+        $this->imap->set_folder($this->name);
+
+        $metadata = $this->imap->get_metadata($this->name, array(kolab_storage::CTYPE_KEY));
+        $this->type_annotation = $metadata[$this->name][kolab_storage::CTYPE_KEY];
+        $this->type = reset(explode('.', $this->type_annotation));
+    }
+
+
+    /**
+     *
+     */
+    private function get_folder_info()
+    {
+        if (!isset($this->info))
+            $this->info = $this->imap->folder_info($this->name);
+
+        return $this->info;
+    }
+
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        // return cached value
+        if (isset($this->owner))
+            return $this->owner;
+
+        $info = $this->get_folder_info();
+        $rcmail = rcmail::get_instance();
+
+        switch ($info['namespace']) {
+        case 'personal':
+            $this->owner = $rcmail->user->get_username();
+            break;
+
+        case 'shared':
+            $this->owner = 'anonymous';
+            break;
+
+        default:
+            $owner = '';
+            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+            if (strpos($user, '@') === false) {
+                $domain = strstr($rcmail->user->get_username(), '@');
+                if (!empty($domain))
+                    $user .= $domain;
+            }
+            $this->owner = $user;
+            break;
+        }
+
+        return $this->owner;
+    }
+
+
+    /**
+     * 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->imap->folder_namespace($this->name);
+    }
+
+
+    /**
+     * Get IMAP ACL information for this folder
+     *
+     * @return string  Permissions as string
+     */
+    function get_acl()
+    {
+        return join('', (array)$this->imap->get_acl($this->name));
+    }
+
+
+    /**
+     * List all Kolab objects of the given type
+     *
+     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
+     * @return array  List of Kolab data objects (each represented as hash array)
+     */
+    function get_objects($type = null)
+    {
+        if (!$type) $type = $this->type;
+
+        // search by object type
+        $ctype  = self::KTYPE_PREFIX . $type;
+        $search = 'HEADER X-Kolab-Type ' . $ctype;
+
+        $index = $this->imap->search_once($this->name, $search);
+        $results = array();
+
+        // fetch all messages from IMAP
+        foreach ($index->get() as $msguid) {
+            if ($object = $this->read_object($msguid, $type)) {
+                $results[] = $object;
+                $this->uid2msg[$object['uid']] = $msguid;
+            }
+        }
+
+        // TODO: write $this->uid2msg to cache
+
+        return $results;
+    }
+
+
+    /**
+     * Getter for a single Kolab object, identified by its UID
+     *
+     * @param string Object UID
+     * @return array The Kolab object represented as hash array
+     */
+    public function get_object($uid)
+    {
+        $msguid = $this->uid2msguid($uid);
+        if ($msguid && ($object = $this->read_object($msguid)))
+            return $object;
+
+        return array('uid' => $uid);
+    }
+
+
+    /**
+     * Fetch a Kolab object attachment which is stored in a separate part
+     * of the mail MIME message that represents the Kolab record.
+     *
+     * @param string Object's UID
+     * @param string The attachment key stored in the Kolab XML
+     * @return mixed The attachment content as binary string
+     */
+    public function get_attachment($uid, $key)
+    {
+        // TODO: implement this
+
+        if ($msguid = $this->uid2msguid($uid)) {
+            $message = new rcube_message($msguid);
+        }
+
+        return null;
+    }
+
+
+    /**
+     *
+     */
+    private function read_object($msguid, $type = null)
+    {
+        if (!$type) $type = $this->type;
+        $ctype= self::KTYPE_PREFIX . $type;
+
+        $this->imap->set_folder($this->name);
+        $message = new rcube_message($msguid);
+
+        // get XML part
+        foreach ((array)$message->attachments as $part) {
+            if ($part->mimetype == $ctype || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype)) {
+                $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
+                break;
+            }
+        }
+
+        if (!$xml) {
+            raise_error(array(
+                'code' => 600,
+                'type' => 'php',
+                'file' => __FILE__,
+                'line' => __LINE__,
+                'message' => "Could not find Kolab data part in message " . $this->name . ':' . $uid,
+            ), true);
+            return false;
+        }
+
+        $format = kolab_format::factory($type);
+
+        // check kolab format version
+        if (strpos($xml, '<' . $type) !== false) {
+            // old Kolab 2.0 format detected
+            $handler = Horde_Kolab_Format::factory('XML', $type);
+            if (is_object($handler) && is_a($handler, 'PEAR_Error')) {
+                continue;
+            }
+
+            // XML-to-array
+            $object = $handler->load($xml);
+            $format->fromkolab2($object);
+        }
+        else {
+            // load Kolab 3 format using libkolabxml
+            $format->load($xml);
+        }
+
+        if ($format->is_valid()) {
+            $object = $format->to_array();
+            $object['_msguid'] = $msguid;
+            $object['_mailbox'] = $this->name;
+            return $object;
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Save an object in this folder.
+     *
+     * @param array  $object    The array that holds the data of the object.
+     * @param string $type      The type of the kolab object.
+     * @param string $uid       The UID of the old object if it existed before
+     * @return boolean          True on success, false on error
+     */
+    public function save(&$object, $type, $uid = null)
+    {
+        if (!$type)
+            $type = $this->type;
+
+        if ($raw_msg = $this->build_message($object, $type)) {
+            $result = $this->imap->save_message($this->name, $raw_msg, '', false);
+
+            // delete old message
+            if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
+                $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
+            }
+            else if ($result && $uid && ($msguid = $this->uid2msguid($uid))) {
+                $this->imap->delete_message($msguid, $this->name);
+            }
+
+            // TODO: update cache with new UID
+        }
+        
+        return $result;
+    }
+
+
+    /**
+     * Delete the specified object from this folder.
+     *
+     * @param  array   $object  The Kolab object to delete
+     * @param  boolean $trigger Should the folder be triggered?
+     * @param  boolean $expunge Should the folder be expunged?
+     *
+     * @return boolean True if successful, false on error
+     */
+    function delete($object, $trigger = true, $expunge = true)
+    {
+        if (!empty($object['_msguid'])) {
+            return $this->imap->delete_message($object['_msguid'], $this->name);
+        }
+
+        return false;
+    }
+
+
+    /**
+     *
+     */
+    public function delete_all()
+    {
+        return $this->imap->clear_folder($this->name);
+    }
+
+
+    /**
+     * Resolve an object UID into an IMAP message UID
+     */
+    private function uid2msguid($uid)
+    {
+        if (!isset($this->uid2msg[$uid])) {
+            // use IMAP SEARCH to get the right message
+            $index = $this->imap->search_once($this->name, 'HEADER SUBJECT ' . $uid);
+            $results = $index->get();
+            $this->uid2msg[$uid] = $results[0];
+
+            // TODO: cache this lookup
+        }
+
+        return $this->uid2msg[$uid];
+    }
+
+
+    /**
+     * Creates source of the configuration object message
+     */
+    private function build_message(&$object, $type)
+    {
+        $format = kolab_format::factory($type);
+        $format->set($object);
+        $xml = $format->write();
+
+        if (!$format->is_valid()) {
+            return false;
+        }
+
+        $mime = new Mail_mime("\r\n");
+        $rcmail = rcmail::get_instance();
+        $headers = array();
+
+        if ($ident = $rcmail->user->get_identity()) {
+            $headers['From'] = $ident['email'];
+            $headers['To'] = $ident['email'];
+        }
+        $headers['Date'] = date('r');
+        $headers['X-Kolab-Type'] = self::KTYPE_PREFIX . $type;
+        $headers['Subject'] = $object['uid'];
+//        $headers['Message-ID'] = rcmail_gen_message_id();
+        $headers['User-Agent'] = $rcmail->config->get('useragent');
+
+        $mime->headers($headers);
+        $mime->setTXTBody('This is a Kolab Groupware object. '
+            . 'To view this object you will need an email client that understands the Kolab Groupware format. '
+            . "For a list of such email clients please visit http://www.kolab.org/kolab2-clients.html\n\n");
+
+        $mime->addAttachment($xml,
+            $format->CTYPE,
+            'kolab.xml',
+            false, '8bit', 'attachment', RCMAIL_CHARSET, '', '',
+            $rcmail->config->get('mime_param_folding') ? 'quoted-printable' : null,
+            $rcmail->config->get('mime_param_folding') == 2 ? 'quoted-printable' : null,
+            '', RCMAIL_CHARSET
+        );
+
+        return $mime->getMessage();
+    }
+
+
+    /**
+     * Triggers any required updates after changes within the
+     * folder. This is currently only required for handling free/busy
+     * information with Kolab.
+     *
+     * @return boolean|PEAR_Error True if successfull.
+     */
+    public function trigger()
+    {
+        $owner = $this->get_owner();
+
+        switch($this->type) {
+        case 'event':
+            $url = sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), $owner, $this->subpath);
+            break;
+
+        default:
+            return true;
+        }
+
+        $result = $this->trigger_url($url);
+        if (is_a($result, 'PEAR_Error')) {
+            return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s"),
+                                            $this->name, $result->getMessage());
+        }
+        return $result;
+    }
+
+    /**
+     * Triggers a URL.
+     *
+     * @param string $url The URL to be triggered.
+     * @return boolean|PEAR_Error True if successfull.
+     */
+    private function trigger_url($url)
+    {
+        // TBD.
+        return PEAR::raiseError("Feature not implemented.");
+    }
+
+
+    /* Legacy methods to keep compatibility with the old Horde Kolab_Storage classes */
+
+    /**
+     * Compatibility method
+     */
+    public function getOwner()
+    {
+        console("Call to deprecated method kolab_storage_folder::getOwner()");
+        return $this->get_owner();
+    }
+
+    /**
+     * Get IMAP ACL information for this folder
+     */
+    public function getMyRights()
+    {
+        return $this->get_acl();
+    }
+
+    /**
+     * NOP to stay compatible with the formerly used Horde classes
+     */
+    public function getData()
+    {
+        return $this;
+    }
+
+    /**
+     * List all Kolab objects of the given type
+     */
+    public function getObjects($type = null)
+    {
+        return $this->get_objects($type);
+    }
+
+    /**
+     * Getter for a single Kolab object, identified by its UID
+     */
+    public function getObject($uid)
+    {
+        return $this->get_object($uid);
+    }
+
+    /**
+     *
+     */
+    public function getAttachment($key)
+    {
+        PEAR::raiseError("Call to deprecated method not returning anything.");
+        return null;
+    }
+
+    /**
+     * Alias function of delete()
+     */
+    public function deleteMessage($id, $trigger = true, $expunge = true)
+    {
+        return $this->delete(array('_msguid' => $id), $trigger, $expunge);
+    }
+
+    /**
+     *
+     */
+    public function deleteAll()
+    {
+        return $this->delete_all();
+    }
+
+
+}
+
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
new file mode 100644
index 0000000..9092f46
--- /dev/null
+++ b/plugins/libkolab/libkolab.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Kolab core library
+ *
+ * Plugin to setup a basic environment for the interaction with a Kolab server.
+ * Other Kolab-related plugins will depend on it and can use the library classes
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libkolab extends rcube_plugin
+{
+    /**
+     * Required startup method of a Roundcube plugin
+     */
+    public function init()
+    {
+        // load local config
+        $this->load_config();
+
+        // extend include path to load bundled lib classes
+        $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
+        set_include_path($include_path);
+
+        $rcmail = rcmail::get_instance();
+        kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
+
+        // load (old) dependencies
+        require_once 'Horde/Util.php';
+        require_once 'Horde/Kolab/Format.php';
+        require_once 'Horde/Kolab/Format/XML.php';
+        require_once 'Horde/Kolab/Format/XML/contact.php';
+        require_once 'Horde/Kolab/Format/XML/event.php';
+
+        String::setDefaultCharset('UTF-8');
+    }
+
+
+}





More information about the commits mailing list