2 commits - plugins/calendar plugins/libcalendaring plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Tue Jul 8 12:39:49 CEST 2014


 plugins/calendar/calendar.php                                |   15 
 plugins/calendar/calendar_ui.js                              |   21 
 plugins/calendar/config.inc.php.dist                         |    3 
 plugins/calendar/drivers/calendar_driver.php                 |   12 
 plugins/calendar/drivers/database/database_driver.php        |    2 
 plugins/calendar/drivers/kolab/kolab_calendar.php            |   28 
 plugins/calendar/drivers/kolab/kolab_driver.php              |   84 ++
 plugins/calendar/drivers/kolab/kolab_invitation_calendar.php |  324 +++++++++++
 plugins/calendar/localization/en_US.inc                      |    2 
 plugins/calendar/skins/larry/calendar.css                    |   49 +
 plugins/calendar/skins/larry/templates/calendar.html         |    2 
 plugins/libcalendaring/lib/libcalendaring_itip.php           |   12 
 plugins/libcalendaring/libcalendaring.php                    |   30 -
 plugins/libcalendaring/localization/en_US.inc                |    2 
 plugins/libkolab/lib/kolab_format_event.php                  |    6 
 plugins/libkolab/lib/kolab_format_task.php                   |    5 
 plugins/libkolab/lib/kolab_format_xcal.php                   |   23 
 17 files changed, 586 insertions(+), 34 deletions(-)

New commits:
commit 7affe524f1d9b3f9a301394091f9e632e0de82f2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 8 12:36:34 2014 +0200

    List virtual calendars showing pending/declined inivtations (#1796)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index a370c22..ac7ee9f 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -234,6 +234,9 @@ class calendar extends rcube_plugin
     if (!$this->itip) {
       require_once($this->home . '/lib/calendar_itip.php');
       $this->itip = new calendar_itip($this);
+      
+      if ($this->rc->config->get('kolab_invitation_calendars'))
+        $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
     }
 
     return $this->itip;
@@ -883,12 +886,13 @@ class calendar extends rcube_plugin
         break;
 
       case "rsvp":
+        $status = get_input_value('status', RCUBE_INPUT_GPC);
         $ev = $this->driver->get_event($event);
         $ev['attendees'] = $event['attendees'];
         $event = $ev;
 
-        if ($success = $this->driver->edit_event($event)) {
-          $status = get_input_value('status', RCUBE_INPUT_GPC);
+        if ($success = $this->driver->edit_rsvp($event, $status)) {
+          $reload = $event['calendar'] != $ev['calendar'] ? 2 : 1;
           $organizer = null;
           foreach ($event['attendees'] as $i => $attendee) {
             if ($attendee['role'] == 'ORGANIZER') {
@@ -947,7 +951,7 @@ class calendar extends rcube_plugin
       if ($reload > 1)
         $args['refetch'] = true;
       else if ($success && $action != 'remove')
-        $args['update'] = $this->_client_event($this->driver->get_event($event));
+        $args['update'] = $this->_client_event($this->driver->get_event($event), true);
       $this->rc->output->command('plugin.refresh_calendar', $args);
     }
   }
@@ -1314,6 +1318,7 @@ class calendar extends rcube_plugin
     $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
     $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
     $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+    $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false);
 
     // get user identity to create default attendee
     if ($this->ui->screen == 'calendar') {
@@ -2398,7 +2403,7 @@ class calendar extends rcube_plugin
           else
             $error_msg = $this->gettext('newerversionexists');
         }
-        else if (!$existing && $status != 'declined') {
+        else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) {
           $success = $this->driver->new_event($event);
         }
         else if ($status == 'declined')
@@ -2609,7 +2614,7 @@ class calendar extends rcube_plugin
   /**
    * Get a list of email addresses of the current user (from login and identities)
    */
-  private function get_user_emails()
+  public function get_user_emails()
   {
     return $this->lib->get_user_emails();
   }
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 66525f2..b833918 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -384,6 +384,9 @@ function rcube_calendar_ui(settings)
       var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
       me.selected_event = event;
 
+      if ($dialog.is(':ui-dialog'))
+        $dialog.dialog('close');
+
       // allow other plugins to do actions when event form is opened
       rcmail.triggerEvent('calendar-event-init', {o: event});
 
@@ -408,7 +411,7 @@ function rcube_calendar_ui(settings)
         $('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
       
       if (calendar.name)
-        $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text').addClass('cal-'+calendar.id);
+        $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text cal-'+calendar.id).css('color', calendar.textColor || calendar.color || '');
       if (event.categories)
         $('#event-category').show().children('.event-text').html(Q(event.categories)).attr('class', 'event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, ''));
       if (event.free_busy)
@@ -1926,10 +1929,16 @@ function rcube_calendar_ui(settings)
             data.status = response.toUpperCase();
         }
         event_show_dialog(me.selected_event);
-        
+
         // submit status change to server
-        me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
-        rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
+        var submit_data = $.extend({}, me.selected_event, { source:null });
+        if (settings.invitation_calendars) {
+          update_event('rsvp', submit_data, { status:response });
+        }
+        else {
+          me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+          rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response });
+        }
       }
     };
     
@@ -1962,10 +1971,10 @@ function rcube_calendar_ui(settings)
     }
     
     // post the given event data to server
-    var update_event = function(action, data)
+    var update_event = function(action, data, add)
     {
       me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
-      rcmail.http_post('calendar/event', { action:action, e:data });
+      rcmail.http_post('calendar/event', $.extend({ action:action, e:data }, (add || {})));
       
       // render event temporarily into the calendar
       if ((data.start && data.end) || data.id) {
diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist
index f09d30f..5216642 100644
--- a/plugins/calendar/config.inc.php.dist
+++ b/plugins/calendar/config.inc.php.dist
@@ -127,6 +127,9 @@ $rcmail_config['calendar_itip_smtp_user'] = 'smtpauth';
 // SMTP password used to send (anonymous) itip messages
 $rcmail_config['calendar_itip_smtp_pass'] = '123456';
 
+// show virtual invitation calendars (Kolab driver only)
+$rcmail_config['kolab_invitation_calendars'] = true;
+
 // Base URL to build fully qualified URIs to access calendars via CALDAV
 // The following replacement variables are supported:
 // %h - Current HTTP host
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 9fb6ffb..4c4f516 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -191,6 +191,18 @@ abstract class calendar_driver
   abstract function edit_event($event);
 
   /**
+   * Extended event editing with possible changes to the argument
+   *
+   * @param array  Hash array with event properties
+   * @param string New participant status
+   * @return boolean True on success, False on error
+   */
+  public function edit_rsvp(&$event, $status)
+  {
+    return $this->edit_event($event);
+  }
+
+  /**
    * Move a single event
    *
    * @param array Hash array with event properties:
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 3b833ab..158adc9 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -138,7 +138,7 @@ class database_driver extends calendar_driver
           'color'      => $prefs['color'],
           'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
           'active'     => !in_array($id, $hidden),
-          'group'      => 'birthdays',
+          'group'      => 'x-birthdays',
           'readonly'   => true,
           'default'    => false,
           'children'   => false,
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 401b295..706c3cd 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -209,9 +209,10 @@ class kolab_calendar extends kolab_storage_folder_api
    * @param  string  Search query (optional)
    * @param  boolean Include virtual events (optional)
    * @param  array   Additional parameters to query storage
+   * @param  array   Additional query to filter events
    * @return array A list of event records
    */
-  public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+  public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null)
   {
     // convert to DateTime for comparisons
     try {
@@ -227,10 +228,24 @@ class kolab_calendar extends kolab_storage_folder_api
       $end = new DateTime('today +10 years');
     }
 
+    // get email addresses of the current user
+    $user_emails = $this->cal->get_user_emails();
+
     // query Kolab storage
     $query[] = array('dtstart', '<=', $end);
     $query[] = array('dtend',   '>=', $start);
 
+    // add query to exclude pending/declined invitations
+    if (empty($filter_query)) {
+      foreach ($user_emails as $email) {
+        $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
+        $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
+      }
+    }
+    else if (is_array($filter_query)) {
+      $query = array_merge($query, $filter_query);
+    }
+
     if (!empty($search)) {
         $search = mb_strtolower($search);
         foreach (rcube_utils::normalize_string($search, true) as $word) {
@@ -240,6 +255,15 @@ class kolab_calendar extends kolab_storage_folder_api
 
     $events = array();
     foreach ($this->storage->select($query) as $record) {
+      // post-filter events to skip pending and declined invitations
+      if (empty($filter_query) && is_array($record['attendees'])) {
+        foreach ($record['attendees'] as $attendee) {
+          if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], array('NEEDS-ACTION','DECLINED'))) {
+            continue 2;
+          }
+        }
+      }
+
       $event = $this->_to_rcube_event($record);
       $this->events[$event['id']] = $event;
 
@@ -671,7 +695,7 @@ class kolab_calendar extends kolab_storage_folder_api
     }
 
     // remove some internal properties which should not be saved
-    unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']);
+    unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'], $event['className']);
 
     // copy meta data (starting with _) from old object
     foreach ((array)$old as $key => $val) {
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ca14d89..ba634cb 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -25,9 +25,14 @@
 
 require_once(dirname(__FILE__) . '/kolab_calendar.php');
 require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
+require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php');
+
 
 class kolab_driver extends calendar_driver
 {
+  const INVITATIONS_CALENDAR_PENDING  = '--invitation--pending';
+  const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
+
   // features this backend supports
   public $alarms = true;
   public $attendees = true;
@@ -202,6 +207,35 @@ class kolab_driver extends calendar_driver
       }
     }
 
+    // list virtual calendars showing invitations
+    if ($this->rc->config->get('kolab_invitation_calendars')) {
+      foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
+        $cal = new kolab_invitation_calendar($id, $this->cal);
+        $this->calendars[$cal->id] = $cal;
+        if (!$active || $cal->is_active()) {
+          $calendars[$id] = array(
+            'id'       => $cal->id,
+            'name'     => $cal->get_name(),
+            'listname' => $cal->get_name(),
+            'editname' => $cal->get_foldername(),
+            'title'    => $cal->get_title(),
+            'color'    => $cal->get_color(),
+            'readonly' => $cal->readonly,
+            'showalarms' => $cal->alarms,
+            'group'    => 'x-invitations',
+            'default'  => false,
+            'active'   => $cal->is_active(),
+            'owner'    => $cal->get_owner(),
+            'children' => false,
+          );
+
+          if (is_object($tree)) {
+            $tree->children[] = $cal;
+          }
+        }
+      }
+    }
+
     // append the virtual birthdays calendar
     if ($this->rc->config->get('calendar_contact_birthdays', false)) {
       $id = self::BIRTHDAY_CALENDAR_ID;
@@ -214,7 +248,7 @@ class kolab_driver extends calendar_driver
           'color'      => $prefs[$id]['color'],
           'active'     => $prefs[$id]['active'],
           'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
-          'group'      => 'birthdays',
+          'group'      => 'x-birthdays',
           'readonly'   => true,
           'default'    => false,
           'children'   => false,
@@ -273,10 +307,13 @@ class kolab_driver extends calendar_driver
    * @param string Calendar identifier (encoded imap folder name)
    * @return object kolab_calendar Object nor null if calendar doesn't exist
    */
-  protected function get_calendar($id)
+  public function get_calendar($id)
   {
     // create calendar object if necesary
-    if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
+    if (!$this->calendars[$id] && in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
+      $this->calendars[$id] = new kolab_invitation_calendar($id, $this->cal);
+    }
+    else if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
       $calendar = kolab_calendar::factory($id, $this->cal);
       if ($calendar->ready)
         $this->calendars[$calendar->id] = $calendar;
@@ -363,7 +400,7 @@ class kolab_driver extends calendar_driver
    */
   public function subscribe_calendar($prop)
   {
-    if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
+    if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) {
       $ret = false;
       if (isset($prop['permanent']))
         $ret |= $cal->storage->subscribe(intval($prop['permanent']));
@@ -452,6 +489,7 @@ class kolab_driver extends calendar_driver
 
     // don't list the birthday calendar
     $this->rc->config->set('calendar_contact_birthdays', false);
+    $this->rc->config->set('kolab_invitation_calendars', false);
 
     return $this->list_calendars();
   }
@@ -536,6 +574,29 @@ class kolab_driver extends calendar_driver
   }
 
   /**
+   * Extended event editing with possible changes to the argument
+   *
+   * @param array  Hash array with event properties
+   * @param string New participant status
+   * @return boolean True on success, False on error
+   */
+  public function edit_rsvp(&$event, $status)
+  {
+    if (($ret = $this->update_event($event)) && $this->rc->config->get('kolab_invitation_calendars')) {
+      // re-assign to the according (virtual) calendar
+      if (strtoupper($status) == 'DECLINED')
+        $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
+      else if (strtoupper($status) == 'NEEDS-ACTION')
+        $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
+      else if ($event['_folder_id'])
+        $event['calendar'] = $event['_folder_id'];
+    }
+
+    return $ret;
+  }
+
+
+  /**
    * Move a single event
    *
    * @see calendar_driver::move_event()
@@ -580,6 +641,7 @@ class kolab_driver extends calendar_driver
   {
     $success = false;
     $savemode = $event['_savemode'];
+    $decline  = $event['decline'];
 
     if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
       $event['_savemode'] = $savemode;
@@ -664,7 +726,15 @@ class kolab_driver extends calendar_driver
           }
 
         default:  // 'all' is default
-          $success = $storage->delete_event($master, $force);
+          if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
+            // don't delete but set PARTSTAT=DECLINED
+            if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
+              $success = $storage->update_event($master);
+            }
+          }
+
+          if (!$success)
+            $success = $storage->delete_event($master, $force);
           break;
       }
     }
@@ -1227,7 +1297,9 @@ class kolab_driver extends calendar_driver
   public function calendar_form($action, $calendar, $formfields)
   {
     // show default dialog for birthday calendar
-    if ($calendar['id'] == self::BIRTHDAY_CALENDAR_ID) {
+    if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
+      if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID)
+        unset($formfields['showalarms']);
       return parent::calendar_form($action, $calendar, $formfields);
     }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
new file mode 100644
index 0000000..ab5fe4b
--- /dev/null
+++ b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
@@ -0,0 +1,324 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual calendar listing pedning/declined invitations
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_invitation_calendar
+{
+  public $id = '__invitation__';
+  public $ready = true;
+  public $alarms = false;
+  public $readonly = true;
+  public $attachments = false;
+  public $subscriptions = false;
+  public $partstats = array('unknown');
+  public $categories = array();
+  public $name = 'Invitations';
+
+  /**
+   * Default constructor
+   */
+  public function __construct($id, $calendar)
+  {
+    $this->cal = $calendar;
+    $this->id = $id;
+
+    switch ($this->id) {
+      case kolab_driver::INVITATIONS_CALENDAR_PENDING:
+        $this->partstats = array('NEEDS-ACTION');
+        $this->name = $this->cal->gettext('invitationspending');
+        if (!empty($_REQUEST['_quickview']))
+          $this->partstats[] = 'TENTATIVE';
+        break;
+
+      case kolab_driver::INVITATIONS_CALENDAR_DECLINED:
+        $this->partstats = array('DECLINED');
+        $this->name = $this->cal->gettext('invitationsdeclined');
+        break;
+    }
+
+    // user-specific alarms settings win
+    $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+    if (isset($prefs[$this->id]['showalarms']))
+      $this->alarms = $prefs[$this->id]['showalarms'];
+  }
+
+
+  /**
+   * Getter for a nice and human readable name for this calendar
+   *
+   * @return string Name of this calendar
+   */
+  public function get_name()
+  {
+    return $this->name;
+  }
+
+
+  /**
+   * Getter for the IMAP folder owner
+   *
+   * @return string Name of the folder owner
+   */
+  public function get_owner()
+  {
+    return $this->cal->rc->get_user_name();
+  }
+
+
+  /**
+   *
+   */
+  public function get_title()
+  {
+    return $this->get_name();
+  }
+
+
+  /**
+   * 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 'x-special';
+  }
+
+
+  /**
+   * Getter for the top-end calendar folder name (not the entire path)
+   *
+   * @return string Name of this calendar
+   */
+  public function get_foldername()
+  {
+    return $this->get_name();
+  }
+
+  /**
+   * Return color to display this calendar
+   */
+  public function get_color()
+  {
+    // calendar color is stored in local user prefs
+    $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+    if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+      return $prefs[$this->id]['color'];
+
+    return 'ffffff';
+  }
+
+  /**
+   * Compose an URL for CalDAV access to this calendar (if configured)
+   */
+  public function get_caldav_url()
+  {
+    return false;
+  }
+
+  /**
+   * Check activation status of this folder
+   *
+   * @return boolean True if enabled, false if not
+   */
+  public function is_active()
+  {
+    $prefs = $this->cal->rc->config->get('kolab_calendars', array());  // read local prefs
+    return (bool)$prefs[$this->id]['active'];
+  }
+
+  /**
+   * Update properties of this calendar folder
+   *
+   * @see calendar_driver::edit_calendar()
+   */
+  public function update(&$prop)
+  {
+    // don't change anything.
+    // let kolab_driver save props in local prefs
+    return $prop['id'];
+  }
+
+
+  /**
+   * Getter for a single event object
+   */
+  public function get_event($id)
+  {
+    // redirect call to kolab_driver::get_event()
+    $event = $this->cal->driver->get_event($id, true);
+
+    if (is_array($event)) {
+      // add pointer to original calendar folder
+      $event['_folder_id'] = $event['calendar'];
+      $event = $this->_mod_event($event);
+    }
+
+    return $event;
+  }
+
+
+  /**
+   * @param  integer Event's new start (unix timestamp)
+   * @param  integer Event's new end (unix timestamp)
+   * @param  string  Search query (optional)
+   * @param  boolean Include virtual events (optional)
+   * @param  array   Additional parameters to query storage
+   * @return array A list of event records
+   */
+  public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+  {
+    // convert to DateTime for comparisons
+    try {
+      $start_dt = new DateTime('@'.$start);
+    }
+    catch (Exception $e) {
+      $start_dt = new DateTime('@0');
+    }
+    try {
+      $end_dt = new DateTime('@'.$end);
+    }
+    catch (Exception $e) {
+      $end_dt = new DateTime('today +10 years');
+    }
+
+    // get email addresses of the current user
+    $user_emails = $this->cal->get_user_emails();
+    $subquery = array();
+    foreach ($user_emails as $email) {
+      foreach ($this->partstats as $partstat) {
+        $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat));
+      }
+    }
+
+    // aggregate events from all calendar folders
+    $events = array();
+    foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+      $cal = new kolab_calendar($foldername, $this->cal);
+      foreach ($cal->list_events($start, $end, $search, 1, $query, array(array($subquery, 'OR'))) as $event) {
+        $match = false;
+
+        // post-filter events to skip pending and declined invitations
+        if (is_array($event['attendees'])) {
+          foreach ($event['attendees'] as $attendee) {
+            if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $this->partstats)) {
+              $match = true;
+              break;
+            }
+          }
+        }
+
+        if ($match) {
+          $events[$event['id']] = $this->_mod_event($event);
+        }
+      }
+
+      // merge list of event categories (really?)
+      $this->categories += $cal->categories;
+    }
+
+    return $events;
+  }
+
+  /**
+   * Helper method to modify some event properties
+   */
+  private function _mod_event($event)
+  {
+    // set classes according to PARTSTAT
+    if (is_array($event['attendees'])) {
+      $user_emails = $this->cal->get_user_emails();
+      $partstat = 'UNKNOWN';
+      foreach ($event['attendees'] as $attendee) {
+        if (in_array($attendee['email'], $user_emails)) {
+          $partstat = $attendee['status'];
+          break;
+        }
+      }
+
+      if (in_array($partstat, $this->partstats)) {
+        $event['className'] = 'fc-invitation-' . strtolower($partstat);
+        $event['calendar'] = $this->id;
+      }
+    }
+
+    return $event;
+  }
+
+
+  /**
+   * Create a new event record
+   *
+   * @see calendar_driver::new_event()
+   * 
+   * @return mixed The created record ID on success, False on error
+   */
+  public function insert_event($event)
+  {
+    return false;
+  }
+
+  /**
+   * Update a specific event record
+   *
+   * @see calendar_driver::new_event()
+   * @return boolean True on success, False on error
+   */
+
+  public function update_event($event, $exception_id = null)
+  {
+    // forward call to the actual storage folder
+    if ($event['_folder_id']) {
+      $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+      if ($cal && $cal->ready) {
+        return $cal->update_event($event, $exception_id);
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Delete an event record
+   *
+   * @see calendar_driver::remove_event()
+   * @return boolean True on success, False on error
+   */
+  public function delete_event($event, $force = true)
+  {
+    return false;
+  }
+
+  /**
+   * Restore deleted event record
+   *
+   * @see calendar_driver::undelete_event()
+   * @return boolean True on success, False on error
+   */
+  public function restore_event($event)
+  {
+    return false;
+  }
+
+
+}
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 52da746..308364f 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -95,6 +95,8 @@ $labels['calendarsubscribe'] = 'List permanently';
 $labels['nocalendarsfound'] = 'No calendars found';
 $labels['nrcalendarsfound'] = '$nr calendars found';
 $labels['quickview'] = 'View only this calendar';
+$labels['invitationspending'] = 'Pending invitations';
+$labels['invitationsdeclined'] = 'Declined invitations';
 
 // agenda view
 $labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index c79e334..e44a0eb 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -237,6 +237,11 @@ pre {
 	left: 20px;
 }
 
+#calendars .treelist li.x-birthdays span.calname,
+#calendars .treelist li.x-invitations span.calname {
+	font-style: italic;
+}
+
 #calendars .treelist.flat li span.calname {
 	left: 24px;
 	right: 42px;
@@ -1524,6 +1529,43 @@ a.dropdown-link:after {
 	top: -5000px;
 }
 
+.fc-invitation-declined {
+
+}
+
+.fc-event-vert.fc-invitation-needs-action,
+.fc-event-hori.fc-invitation-needs-action {
+	border: 1px dashed #5757c7 !important;
+}
+
+.fc-event-vert.fc-invitation-tentative,
+.fc-event-hori.fc-invitation-tentative {
+	border: 1px dashed #eb8900 !important;
+}
+
+.fc-event-vert.fc-invitation-declined,
+.fc-event-hori.fc-invitation-declined {
+	border: 1px dashed #c00 !important;
+}
+
+.fc-event-vert.fc-invitation-tentative .fc-event-head,
+.fc-event-vert.fc-invitation-declined .fc-event-head,
+.fc-event-vert.fc-invitation-needs-action .fc-event-head {
+/*	background-color: transparent !important; */
+}
+
+.fc-event-vert.fc-invitation-tentative .fc-event-bg {
+	background: url(data:image/gif;base64,R0lGODlhCAAIAPABAOuJAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-event-vert.fc-invitation-needs-action .fc-event-bg {
+	background: url(data:image/gif;base64,R0lGODlhCAAIAPABAFdXx////yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-event-vert.fc-invitation-declined .fc-event-bg {
+	background: url(data:image/gif;base64,R0lGODlhCAAIAPABAMwAAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
 .calendarmain .fc-event:focus {
 	outline: 1px solid rgba(71,135,177, 0.4);
 	-webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
@@ -1767,7 +1809,8 @@ div.calendar-invitebox .rsvp-status.hint {
 div.calendar-invitebox .rsvp-status.declined,
 div.calendar-invitebox .rsvp-status.tentative,
 div.calendar-invitebox .rsvp-status.accepted,
-div.calendar-invitebox .rsvp-status.delegated  {
+div.calendar-invitebox .rsvp-status.delegated,
+div.calendar-invitebox .rsvp-status.needs-action  {
 	padding: 0 0 1px 22px;
 	background: url(images/attendee-status.png) 2px -20px no-repeat;
 }
@@ -1784,6 +1827,10 @@ div.calendar-invitebox .rsvp-status.delegated {
 	background-position: 2px -180px;
 }
 
+div.calendar-invitebox .rsvp-status.needs-action {
+	background-position: 2px 0;
+}
+
 /* iTIP attend reply page */
 
 .calendaritipattend .centerbox {
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 6e7ed49..d608157 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -130,7 +130,7 @@
 		<div class="event-text"></div>
 	</div>
 
-	<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
+	<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" class="event-dialog-message" style="display:none" />
 </div>
 
 <roundcube:include file="/templates/eventedit.html" />
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index ec4eb1a..66b20eb 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -30,6 +30,8 @@ class libcalendaring_itip
     protected $sender;
     protected $domain;
     protected $itip_send = false;
+    protected $rsvp_actions = array('accepted','tentative','declined');
+    protected $rsvp_status  = array('accepted','tentative','declined','delegated');
 
     function __construct($plugin, $domain = 'libcalendaring')
     {
@@ -52,6 +54,12 @@ class libcalendaring_itip
             $this->sender['email'] = $email;
     }
 
+    public function set_rsvp_actions($actions)
+    {
+        $this->rsvp_actions = (array)$actions;
+        // $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
+    }
+
     /**
      * Wrapper for rcube_plugin::gettext()
      * Checking for a label in different domains
@@ -276,7 +284,7 @@ class libcalendaring_itip
           $html = html::div('rsvp-status', $this->gettext('notanattendee'));
           $action = 'import';
         }
-        else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
+        else if (in_array(strtolower($status), $this->rsvp_status)) {
           $html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
 
           if ($existing && ($existing['sequence'] > $event['sequence'] || (!$event['sequence'] && $existing['changed'] && $existing['changed'] > $event['changed']))) {
@@ -402,7 +410,7 @@ class libcalendaring_itip
             $metadata['rsvp'] = true;
 
             // 1. display RSVP buttons (if the user was invited)
-            foreach (array('accepted','tentative','declined') as $method) {
+            foreach ($this->rsvp_actions as $method) {
                 $rsvp_buttons .= html::tag('input', array(
                     'type' => 'button',
                     'class' => "button $method",
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5888ff0..5a1a8b0 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -329,6 +329,13 @@ class libcalendaring extends rcube_plugin
      */
     public function get_user_emails()
     {
+        static $emails;
+
+        // return cached result
+        if (is_array($emails)) {
+            return $emails;
+        }
+
         $emails = array();
         $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
         $emails = array_map('strtolower', $plugin['emails']);
@@ -342,7 +349,28 @@ class libcalendaring extends rcube_plugin
             $emails[] = strtolower($identity['email']);
         }
 
-        return array_unique($emails);
+        $emails = array_unique($emails);
+        return $emails;
+    }
+
+    /**
+     * Set the given participant status to the attendee matching the current user's identities
+     *
+     * @param array   Hash array with event struct
+     * @param string  The PARTSTAT value to set
+     * @return mixed  Email address of the updated attendee or False if none matching found
+     */
+    public function set_partstat(&$event, $status)
+    {
+        $emails = $this->get_user_emails();
+        foreach ((array)$event['attendees'] as $i => $attendee) {
+            if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+                $event['attendees'][$i]['status'] = strtoupper($status);
+                return $attendee['email'];
+            }
+        }
+
+        return false;
     }
 
 
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index d89833c..588185a 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -75,6 +75,7 @@ $labels['itipreply'] = 'Reply to';
 $labels['itipaccepted'] = 'Accept';
 $labels['itiptentative'] = 'Maybe';
 $labels['itipdeclined'] = 'Decline';
+$labels['itipneeds-action'] = 'Postpone';
 $labels['itipcomment'] = 'Your response';
 $labels['itipeditresponse'] = 'Enter a response text';
 $labels['itipsendercomment'] = 'Sender\'s comment: ';
@@ -96,6 +97,7 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
 $labels['youhavetentative'] = 'You have tentatively accepted this invitation';
 $labels['youhavedeclined'] = 'You have declined this invitation';
 $labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhaveneeds-action'] = 'You have copied this invitation into your calendar';
 $labels['attendeeaccepted'] = 'Participant has accepted';
 $labels['attendeetentative'] = 'Participant has tentatively accepted';
 $labels['attendeedeclined'] = 'Participant has declined';


commit bdf2faafae42282e52694af90c3d21ceed69949c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 8 12:32:05 2014 +0200

    Store tags to enable partstat queries (#1796)

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index c0bcef4..c9a1c9f 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -193,16 +193,12 @@ class kolab_format_event extends kolab_format_xcal
      */
     public function get_tags()
     {
-        $tags = array();
+        $tags = parent::get_tags();
 
         foreach ((array)$this->data['categories'] as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
-        if (!empty($this->data['valarms'])) {
-            $tags[] = 'x-has-alarms';
-        }
-
         return $tags;
     }
 
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index b3c6a42..87c3df9 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -111,7 +111,7 @@ class kolab_format_task extends kolab_format_xcal
      */
     public function get_tags()
     {
-        $tags = array();
+        $tags = parent::get_tags();
 
         if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
             $tags[] = 'x-complete';
@@ -119,9 +119,6 @@ class kolab_format_task extends kolab_format_xcal
         if ($this->data['priority'] == 1)
             $tags[] = 'x-flagged';
 
-        if (!empty($this->data['valarms']))
-            $tags[] = 'x-has-alarms';
-
         if ($this->data['parent_id'])
             $tags[] = 'x-parent:' . $this->data['parent_id'];
 
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 0742f2a..c9e06ac 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -560,4 +560,27 @@ abstract class kolab_format_xcal extends kolab_format
         return array_unique(rcube_utils::normalize_string($data, true));
     }
 
+    /**
+     * 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();
+
+        if (!empty($this->data['valarms'])) {
+            $tags[] = 'x-has-alarms';
+        }
+
+        // create tags reflecting participant status
+        if (is_array($this->data['attendees'])) {
+            foreach ($this->data['attendees'] as $attendee) {
+                if (!empty($attendee['email']) && !empty($attendee['status']))
+                    $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
+            }
+        }
+
+        return $tags;
+    }
 }
\ No newline at end of file





More information about the commits mailing list