Projects
Kolab:16:Testing
roundcubemail-plugins-kolab
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 67
View file
roundcubemail-plugins-kolab.spec
Changed
@@ -41,7 +41,7 @@ %global dash_rel_suffix %{?rc_rel_suffix:-%{rc_rel_suffix}} Name: roundcubemail-plugins-kolab -Version: 3.5.0 +Version: 3.5.1 Release: 1%{?dot_rel_suffix}%{?dist} @@ -62,7 +62,6 @@ Source104: plesk.libkolab.inc.php Patch0000: roundcubemail-plugins-kolab-3.4-kolab-files-manticore-api.patch -Patch0001: 0001-Fix-setting-fileid-on-file-objects-Bifrost-T227815.patch BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) BuildArch: noarch @@ -1407,7 +1406,6 @@ %endif %patch0000 -p1 -%patch0001 -p1 find -type d -name "helpdocs" -exec rm -rvf {} \; 2>/dev/null || : @@ -2794,6 +2792,9 @@ %defattr(-,root,root,-) %changelog +* Mon Oct 7 2019 Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> - 3.5.1-1 +- Release of version 3.5.1 + * Fri Aug 16 2019 Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> - 3.5.0-2 - Fix setting 'fileid' on file objects (Bifrost#T227815)
View file
0001-Fix-setting-fileid-on-file-objects-Bifrost-T227815.patch
Deleted
@@ -1,37 +0,0 @@ -From 6ba98384670aafbe0895b81aea7bb9bf32d7ced1 Mon Sep 17 00:00:00 2001 -From: Aleksander Machniak <machniak@kolabsys.com> -Date: Mon, 15 Jul 2019 11:21:30 +0000 -Subject: PATCH 1/3 Fix setting 'fileid' on file objects (Bifrost#T227815) - ---- - plugins/libkolab/lib/kolab_storage_cache_file.php | 13 +++++-------- - 1 file changed, 5 insertions(+), 8 deletions(-) - -diff --git a/plugins/libkolab/lib/kolab_storage_cache_file.php b/plugins/libkolab/lib/kolab_storage_cache_file.php -index 1f3d4d92..b62c0f7a 100644 ---- a/plugins/libkolab/lib/kolab_storage_cache_file.php -+++ b/plugins/libkolab/lib/kolab_storage_cache_file.php -@@ -61,15 +61,12 @@ class kolab_storage_cache_file extends kolab_storage_cache - { - $object = parent::_unserialize($sql_arr); - -- if ($object && $sql_arr'fast-mode') { -- if (!empty($object'_attachments')) { -- $file = $object'_attachments'key($object'_attachments'); -+ if ($object && !empty($object'_attachments')) { -+ $file = $object'_attachments'key($object'_attachments'); - -- $object'type' = $file'mimetype'; -- $object'size' = $file'size'; -- $object'fileid' = $file'id'; -- $object'filename' = $file'name'; -- } -+ $object'type' = $file'mimetype'; -+ $object'size' = $file'size'; -+ $object'fileid' = $file'id'; - } - - return $object; --- -2.21.0 -
View file
debian.changelog
Changed
@@ -1,3 +1,9 @@ +roundcubemail-plugins-kolab (1:3.5.1-0~kolab1) unstable; urgency=low + + * Release version 3.5.1 + + -- Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> Mon, 7 Oct 2019 11:11:11 +0200 + roundcubemail-plugins-kolab (1:3.5.0-0~kolab5) unstable; urgency=low * Fix setting 'fileid' on file objects (Bifrost#T227815)
View file
debian.series
Changed
@@ -1,2 +1,1 @@ roundcubemail-plugins-kolab-3.4-kolab-files-manticore-api.patch -p1 -0001-Fix-setting-fileid-on-file-objects-Bifrost-T227815.patch -p1
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_auth/kolab_auth_ldap.php
Deleted
@@ -1,480 +0,0 @@ -<?php - -/** - * Kolab Authentication - * - * @version @package_version@ - * @author Aleksander Machniak <machniak@kolabsys.com> - * - * Copyright (C) 2011-2013, Kolab Systems AG <contact@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/>. - */ - -/** - * Wrapper class for rcube_ldap_generic - */ -class kolab_auth_ldap extends rcube_ldap_generic -{ - private $conf = array(); - private $fieldmap = array(); - - - function __construct($p) - { - $rcmail = rcube::get_instance(); - - $this->conf = $p; - $this->conf'kolab_auth_user_displayname' = $rcmail->config->get('kolab_auth_user_displayname', '{name}'); - - $this->fieldmap = $p'fieldmap'; - $this->fieldmap'uid' = 'uid'; - - $p'attributes' = array_values($this->fieldmap); - $p'debug' = (bool) $rcmail->config->get('ldap_debug'); - - // Connect to the server (with bind) - parent::__construct($p); - $this->_connect(); - - $rcmail->add_shutdown_function(array($this, 'close')); - } - - /** - * Establish a connection to the LDAP server - */ - private function _connect() - { - // try to connect + bind for every host configured - // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable - // see http://www.php.net/manual/en/function.ldap-connect.php - foreach ((array)$this->config'hosts' as $host) { - // skip host if connection failed - if (!$this->connect($host)) { - continue; - } - - $bind_pass = $this->config'bind_pass'; - $bind_user = $this->config'bind_user'; - $bind_dn = $this->config'bind_dn'; - - if (empty($bind_pass)) { - $this->ready = true; - } - else { - if (!empty($bind_dn)) { - $this->ready = $this->bind($bind_dn, $bind_pass); - } - else if (!empty($this->config'auth_cid')) { - $this->ready = $this->sasl_bind($this->config'auth_cid', $bind_pass, $bind_user); - } - else { - $this->ready = $this->sasl_bind($bind_user, $bind_pass); - } - } - - // connection established, we're done here - if ($this->ready) { - break; - } - - } // end foreach hosts - - if (!is_resource($this->conn)) { - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not connect to any LDAP server, last tried $host"), true); - - $this->ready = false; - } - - return $this->ready; - } - - /** - * Fetches user data from LDAP addressbook - */ - function get_user_record($user, $host) - { - $rcmail = rcube::get_instance(); - $filter = $rcmail->config->get('kolab_auth_filter'); - $filter = $this->parse_vars($filter, $user, $host); - $base_dn = $this->parse_vars($this->config'base_dn', $user, $host); - $scope = $this->config'scope'; - - // @TODO: print error if filter is empty - - // get record - if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) { - if ($result->count() == 1) { - $entries = $result->entries(true); - $dn = key($entries); - $entry = array_pop($entries); - $entry = $this->field_mapping($dn, $entry); - - return $entry; - } - } - } - - /** - * Fetches user data from LDAP addressbook - */ - function get_user_groups($dn, $user, $host) - { - if (empty($dn) || empty($this->config'groups')) { - return array(); - } - - $base_dn = $this->parse_vars($this->config'groups''base_dn', $user, $host); - $name_attr = $this->config'groups''name_attr' ? $this->config'groups''name_attr' : 'cn'; - $member_attr = $this->get_group_member_attr(); - $filter = "(member=$dn)(uniqueMember=$dn)"; - - if ($member_attr != 'member' && $member_attr != 'uniqueMember') - $filter .= "($member_attr=$dn)"; - $filter = strtr("(|$filter)", array("\\" => "\\\\")); - - $result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr)); - - if (!$result) { - return array(); - } - - $groups = array(); - foreach ($result as $entry) { - $dn = $entry'dn'; - $entry = rcube_ldap_generic::normalize_entry($entry); - - $groups$dn = $entry$name_attr; - } - - return $groups; - } - - /** - * Get a specific LDAP record - * - * @param string DN - * - * @return array Record data - */ - function get_record($dn) - { - if (!$this->ready) { - return; - } - - if ($rec = $this->get_entry($dn, $this->attributes)) { - $rec = rcube_ldap_generic::normalize_entry($rec); - $rec = $this->field_mapping($dn, $rec); - } - - return $rec; - } - - /** - * Replace LDAP record data items - * - * @param string $dn DN - * @param array $entry LDAP entry - * - * return bool True on success, False on failure - */ - function replace($dn, $entry) - { - // fields mapping - foreach ($this->fieldmap as $field => $attr) { - if (array_key_exists($field, $entry)) { - $entry$attr = $entry$field; - if ($attr != $field) { - unset($entry$field); - } - } - } - - return $this->mod_replace($dn, $entry); - } - - /** - * Search records (simplified version of rcube_ldap::search) - * - * @param mixed $fields The field name or array of field names to search in - * @param string $value Search value - * @param int $mode Matching mode: - * 0 - partial (*abc*), - * 1 - strict (=), - * 2 - prefix (abc*) - * @param array $required List of fields that cannot be empty - * @param int $limit Number of records - * @param int $count Returns the number of records found - * - * @return array List or false on error - */ - function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0) - { - if (empty($fields)) { - return array(); - } - - $mode = intval($mode); - - // try to resolve field names into ldap attributes - $fieldmap = $this->fieldmap; - $attrs = array_map(function($f) use ($fieldmap) { - return array_key_exists($f, $fieldmap) ? $fieldmap$f : $f; - }, (array)$fields); - - // compose a full-text-search-like filter - if (count($attrs) > 1 || $mode != 1) { - $filter = self::fulltext_search_filter($value, $attrs, $mode); - } - // direct search - else { - $field = $attrs0; - $filter = "($field=" . self::quote_string($value) . ")"; - } - - // add required (non empty) fields filter - $req_filter = ''; - - foreach ((array)$required as $field) { - $attr = array_key_exists($field, $this->fieldmap) ? $this->fieldmap$field : $field; - - // only add if required field is not already in search filter - if (!in_array($attr, $attrs)) { - $req_filter .= "($attr=*)"; - } - } - - if (!empty($req_filter)) { - $filter = '(&' . $req_filter . $filter . ')'; - } - - // avoid double-wildcard if $value is empty - $filter = preg_replace('/\*+/', '*', $filter); - - // add general filter to query - if (!empty($this->config'filter')) { - $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config'filter') . ')' . $filter . ')'; - } - - $base_dn = $this->parse_vars($this->config'base_dn'); - $scope = $this->config'scope'; - $attrs = array_values($this->fieldmap); - $list = array(); - - if ($result = $this->search($base_dn, $filter, $scope, $attrs)) { - $count = $result->count(); - $i = 0; - foreach ($result as $entry) { - if ($limit && $limit <= $i) { - break; - } - - $dn = $entry'dn'; - $entry = rcube_ldap_generic::normalize_entry($entry); - $list$dn = $this->field_mapping($dn, $entry); - $i++; - } - } - - return $list; - } - - /** - * Set filter used in search() - */ - function set_filter($filter) - { - $this->config'filter' = $filter; - } - - /** - * Maps LDAP attributes to defined fields - */ - protected function field_mapping($dn, $entry) - { - $entry'dn' = $dn; - - // fields mapping - foreach ($this->fieldmap as $field => $attr) { - // $entry might be indexed by lower-case attribute names - $attr_lc = strtolower($attr); - if (isset($entry$attr_lc)) { - $entry$field = $entry$attr_lc; - } - else if (isset($entry$attr)) { - $entry$field = $entry$attr; - } - } - - // compose display name according to config - if (empty($this->fieldmap'displayname')) { - $entry'displayname' = rcube_addressbook::compose_search_name( - $entry, - $entry'email', - $entry'name', - $this->conf'kolab_auth_user_displayname' - ); - } - - return $entry; - } - - /** - * Detects group member attribute name - */ - private function get_group_member_attr($object_classes = array()) - { - if (empty($object_classes)) { - $object_classes = $this->config'groups''object_classes'; - } - if (!empty($object_classes)) { - foreach ((array)$object_classes as $oc) { - switch (strtolower($oc)) { - case 'group': - case 'groupofnames': - case 'kolabgroupofnames': - $member_attr = 'member'; - break; - - case 'groupofuniquenames': - case 'kolabgroupofuniquenames': - $member_attr = 'uniqueMember'; - break; - } - } - } - - if (!empty($member_attr)) { - return $member_attr; - } - - if (!empty($this->config'groups''member_attr')) { - return $this->config'groups''member_attr'; - } - - return 'member'; - } - - /** - * Prepares filter query for LDAP search - */ - function parse_vars($str, $user = null, $host = null) - { - // When authenticating user $user is always set - // if not set it means we use this LDAP object for other - // purposes, e.g. kolab_delegation, then username with - // correct domain is in a session - if (!$user) { - $user = $_SESSION'username'; - } - - if (isset($this->icache$user)) { - list($user, $dc) = $this->icache$user; - } - else { - $orig_user = $user; - $rcmail = rcube::get_instance(); - - // get default domain - if ($username_domain = $rcmail->config->get('username_domain')) { - if ($host && is_array($username_domain) && isset($username_domain$host)) { - $domain = rcube_utils::parse_host($username_domain$host, $host); - } - else if (is_string($username_domain)) { - $domain = rcube_utils::parse_host($username_domain, $host); - } - } - - // realmed username (with domain) - if (strpos($user, '@')) { - list($usr, $dom) = explode('@', $user); - - // unrealm domain, user login can contain a domain alias - if ($dom != $domain && ($dc = $this->domain_root_dn($dom))) { - // @FIXME: we should replace domain in $user, I suppose - } - } - else if ($domain) { - $user .= '@' . $domain; - } - - $this->icache$orig_user = array($user, $dc); - } - - // replace variables in filter - list($u, $d) = explode('@', $user); - - // hierarchal domain string - if (empty($dc)) { - $dc = 'dc=' . strtr($d, array('.' => ',dc=')); - } - - $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u); - - $this->parse_replaces = $replaces; - - return strtr($str, $replaces); - } - - /** - * Returns variables used for replacement in (last) parse_vars() call - * - * @return array Variable-value hash array - */ - public function get_parse_vars() - { - return $this->parse_replaces; - } - - /** - * Register additional fields - */ - public function extend_fieldmap($map) - { - foreach ((array)$map as $name => $attr) { - if (!in_array($attr, $this->attributes)) { - $this->attributes = $attr; - $this->fieldmap$name = $attr; - } - } - } - - /** - * HTML-safe DN string encoding - * - * @param string $str DN string - * - * @return string Encoded HTML identifier string - */ - static function dn_encode($str) - { - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); - } - - /** - * Decodes DN string encoded with _dn_encode() - * - * @param string $str Encoded HTML identifier string - * - * @return string DN string - */ - static function dn_decode($str) - { - $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT); - return base64_decode($str); - } -}
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libcalendaring/tests/resources/invalid-event.ics
Deleted
@@ -1,14 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Apple Inc.//iCal 5.0.3//EN -CALSCALE:GREGORIAN -BEGIN:VEVENT -CREATED:20130917T000000Z -LAST-MODIFIED:20130755 -UID:C968B885-08FB-40E5-B89E-6FA05F26AACC -TRANSP:TRANSPARENT -SUMMARY:Event with no end date nor duration -DTSTART;VALUE=DATE-TIME:20131001T120000Z -SEQUENCE:2 -END:VEVENT -END:VCALENDAR
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/calendar/README -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/calendar/README
Changed
@@ -69,4 +69,10 @@ ); +IMPORTANT +--------- + +This plugin doesn't work with the classic skin of Roundcube because no +templates are available for that skin. + 1 https://git.kolab.org/diffusion/RPK/
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/calendar/calendar.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/calendar/calendar.php
Changed
@@ -2459,17 +2459,20 @@ */ public static function event_diff($a, $b) { - $diff = array(); + $diff = array(); $ignore = array('changed' => 1, 'attachments' => 1); + foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { - if (!$ignore$key && $key0 != '_' && $a$key != $b$key) + if (!$ignore$key && $key0 != '_' && $a$key != $b$key) { $diff = $key; + } } - + // only compare number of attachments - if (count($a'attachments') != count($b'attachments')) + if (count((array) $a'attachments') != count((array) $b'attachments')) { $diff = 'attachments'; - + } + return $diff; }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/calendar/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/calendar/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Calendar plugin", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli", @@ -28,5 +28,11 @@ "roundcube/plugin-installer": ">=0.1.3", "kolab/libcalendaring": ">=3.4.0", "kolab/libkolab": ">=3.4.0" + }, + "extra": { + "roundcube": { + "min-version": "1.4.0", + "sql-dir": "drivers/database/SQL" + } } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/calendar/drivers/calendar_driver.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/calendar/drivers/calendar_driver.php
Changed
@@ -592,14 +592,16 @@ */ public function calendar_form($action, $calendar, $formfields) { - $html = ''; - foreach ($formfields as $field) { - $html .= html::div('form-section', - html::label($field'id', $field'label') . - $field'value'); + $table = new html_table(array('cols' => 2, 'class' => 'propform')); + + foreach ($formfields as $col => $colprop) { + $label = !empty($colprop'label') ? $colprop'label' : $rcmail->gettext("$domain.$col"); + + $table->add('title', html::label($colprop'id', rcube::Q($label))); + $table->add(null, $colprop'value'); } - return $html; + return $table->show(); } /** @@ -729,7 +731,7 @@ $rcmail = rcmail::get_instance(); - if ($source && $contact_id && ($abook = $rcmail->get_address_book($source))) { + if (strlen($source) && $contact_id && ($abook = $rcmail->get_address_book($source))) { if ($contact = $abook->get_record($contact_id, true)) { return self::parse_contact($contact, $source); }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/calendar/drivers/database/database_driver.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/calendar/drivers/database/database_driver.php
Changed
@@ -26,1471 +26,1504 @@ class database_driver extends calendar_driver { - const DB_DATE_FORMAT = 'Y-m-d H:i:s'; - - public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled'); - - // features this backend supports - public $alarms = true; - public $attendees = true; - public $freebusy = false; - public $attachments = true; - public $alarm_types = array('DISPLAY'); - - private $rc; - private $cal; - private $cache = array(); - private $calendars = array(); - private $calendar_ids = ''; - private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3); - private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2); - private $server_timezone; - - private $db_events = 'events'; - private $db_calendars = 'calendars'; - private $db_attachments = 'attachments'; - - - /** - * Default constructor - */ - public function __construct($cal) - { - $this->cal = $cal; - $this->rc = $cal->rc; - $this->server_timezone = new DateTimeZone(date_default_timezone_get()); - - // read database config - $db = $this->rc->get_dbh(); - $this->db_events = $this->rc->config->get('db_table_events', $db->table_name($this->db_events)); - $this->db_calendars = $this->rc->config->get('db_table_calendars', $db->table_name($this->db_calendars)); - $this->db_attachments = $this->rc->config->get('db_table_attachments', $db->table_name($this->db_attachments)); - - $this->_read_calendars(); - } - - /** - * Read available calendars for the current user and store them internally - */ - private function _read_calendars() - { - $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); - - if (!empty($this->rc->user->ID)) { - $calendar_ids = array(); - $result = $this->rc->db->query( - "SELECT *, calendar_id AS id FROM " . $this->db_calendars . " - WHERE user_id=? - ORDER BY name", - $this->rc->user->ID - ); - while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { - $arr'showalarms' = intval($arr'showalarms'); - $arr'active' = !in_array($arr'id', $hidden); - $arr'name' = html::quote($arr'name'); - $arr'listname' = html::quote($arr'name'); - $arr'rights' = 'lrswikxteav'; - $arr'editable' = true; - $this->calendars$arr'calendar_id' = $arr; - $calendar_ids = $this->rc->db->quote($arr'calendar_id'); - } - $this->calendar_ids = join(',', $calendar_ids); - } - } - - /** - * Get a list of available calendars from this source - * - * @param integer Bitmask defining filter criterias - * - * @return array List of calendars - */ - public function list_calendars($filter = 0) - { - // attempt to create a default calendar for this user - if (empty($this->calendars)) { - if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000', 'showalarms' => true))) + const DB_DATE_FORMAT = 'Y-m-d H:i:s'; + + public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled'); + + // features this backend supports + public $alarms = true; + public $attendees = true; + public $freebusy = false; + public $attachments = true; + public $alarm_types = array('DISPLAY'); + + private $rc; + private $cal; + private $cache = array(); + private $calendars = array(); + private $calendar_ids = ''; + private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3); + private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2); + private $server_timezone; + + private $db_events = 'events'; + private $db_calendars = 'calendars'; + private $db_attachments = 'attachments'; + + + /** + * Default constructor + */ + public function __construct($cal) + { + $this->cal = $cal; + $this->rc = $cal->rc; + $this->server_timezone = new DateTimeZone(date_default_timezone_get()); + + // read database config + $db = $this->rc->get_dbh(); + $this->db_events = $db->table_name($this->rc->config->get('db_table_events', $this->db_events)); + $this->db_calendars = $db->table_name($this->rc->config->get('db_table_calendars', $this->db_calendars)); + $this->db_attachments = $db->table_name($this->rc->config->get('db_table_attachments', $this->db_attachments)); + $this->_read_calendars(); } - $calendars = $this->calendars; + /** + * Read available calendars for the current user and store them internally + */ + private function _read_calendars() + { + $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); + + if (!empty($this->rc->user->ID)) { + $calendar_ids = array(); + $result = $this->rc->db->query( + "SELECT *, `calendar_id` AS id FROM `{$this->db_calendars}`" + . " WHERE `user_id` = ?" + . " ORDER BY `name`", + $this->rc->user->ID + ); + + while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { + $arr'showalarms' = intval($arr'showalarms'); + $arr'active' = !in_array($arr'id', $hidden); + $arr'name' = html::quote($arr'name'); + $arr'listname' = html::quote($arr'name'); + $arr'rights' = 'lrswikxteav'; + $arr'editable' = true; + + $this->calendars$arr'calendar_id' = $arr; + $calendar_ids = $this->rc->db->quote($arr'calendar_id'); + } + + $this->calendar_ids = join(',', $calendar_ids); + } + } + + /** + * Get a list of available calendars from this source + * + * @param integer Bitmask defining filter criterias + * + * @return array List of calendars + */ + public function list_calendars($filter = 0) + { + // attempt to create a default calendar for this user + if (empty($this->calendars)) { + if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000', 'showalarms' => true))) { + $this->_read_calendars(); + } + } + + $calendars = $this->calendars; + + // filter active calendars + if ($filter & self::FILTER_ACTIVE) { + foreach ($calendars as $idx => $cal) { + if (!$cal'active') { + unset($calendars$idx); + } + } + } + + // 'personal' is unsupported in this driver + + // append the virtual birthdays calendar + if ($this->rc->config->get('calendar_contact_birthdays', false)) { + $prefs = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); + $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); + $id = self::BIRTHDAY_CALENDAR_ID; + + if (!$active || !in_array($id, $hidden)) { + $calendars$id = array( + 'id' => $id, + 'name' => $this->cal->gettext('birthdays'), + 'listname' => $this->cal->gettext('birthdays'), + 'color' => $prefs'color', + 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'), + 'active' => !in_array($id, $hidden), + 'group' => 'x-birthdays', + 'editable' => false, + 'default' => false, + 'children' => false, + ); + } + } + + return $calendars; + } + + /** + * Create a new calendar assigned to the current user + * + * @param array Hash array with calendar properties + * name: Calendar name + * color: The color of the calendar + * @return mixed ID of the calendar on success, False on error + */ + public function create_calendar($prop) + { + $result = $this->rc->db->query( + "INSERT INTO `{$this->db_calendars}`" + . " (`user_id`, `name`, `color`, `showalarms`)" + . " VALUES (?, ?, ?, ?)", + $this->rc->user->ID, + $prop'name', + strval($prop'color'), + $prop'showalarms' ? 1 : 0 + ); - // filter active calendars - if ($filter & self::FILTER_ACTIVE) { - foreach ($calendars as $idx => $cal) { - if (!$cal'active') { - unset($calendars$idx); + if ($result) { + return $this->rc->db->insert_id($this->db_calendars); } - } + + return false; } - // 'personal' is unsupported in this driver - - // append the virtual birthdays calendar - if ($this->rc->config->get('calendar_contact_birthdays', false)) { - $prefs = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); - $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); - - $id = self::BIRTHDAY_CALENDAR_ID; - if (!$active || !in_array($id, $hidden)) { - $calendars$id = array( - 'id' => $id, - 'name' => $this->cal->gettext('birthdays'), - 'listname' => $this->cal->gettext('birthdays'), - 'color' => $prefs'color', - 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'), - 'active' => !in_array($id, $hidden), - 'group' => 'x-birthdays', - 'editable' => false, - 'default' => false, - 'children' => false, + /** + * Update properties of an existing calendar + * + * @see calendar_driver::edit_calendar() + */ + public function edit_calendar($prop) + { + // birthday calendar properties are saved in user prefs + if ($prop'id' == self::BIRTHDAY_CALENDAR_ID) { + $prefs'birthday_calendar' = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); + if (isset($prop'color')) { + $prefs'birthday_calendar''color' = $prop'color'; + } + if (isset($prop'showalarms')) { + $prefs'calendar_birthdays_alarm_type' = $prop'showalarms' ? $this->alarm_types0 : ''; + } + + $this->rc->user->save_prefs($prefs); + return true; + } + + $query = $this->rc->db->query( + "UPDATE `{$this->db_calendars}`" + . " SET `name` = ?, `color` = ?, `showalarms` = ?" + . " WHERE `calendar_id` = ? AND `user_id` = ?", + $prop'name', + strval($prop'color'), + $prop'showalarms' ? 1 : 0, + $prop'id', + $this->rc->user->ID ); - } + + return $this->rc->db->affected_rows($query); } - return $calendars; - } - - /** - * Create a new calendar assigned to the current user - * - * @param array Hash array with calendar properties - * name: Calendar name - * color: The color of the calendar - * @return mixed ID of the calendar on success, False on error - */ - public function create_calendar($prop) - { - $result = $this->rc->db->query( - "INSERT INTO " . $this->db_calendars . " - (user_id, name, color, showalarms) - VALUES (?, ?, ?, ?)", - $this->rc->user->ID, - $prop'name', - $prop'color', - $prop'showalarms'?1:0 - ); - - if ($result) - return $this->rc->db->insert_id($this->db_calendars); - - return false; - } - - /** - * Update properties of an existing calendar - * - * @see calendar_driver::edit_calendar() - */ - public function edit_calendar($prop) - { - // birthday calendar properties are saved in user prefs - if ($prop'id' == self::BIRTHDAY_CALENDAR_ID) { - $prefs'birthday_calendar' = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); - if (isset($prop'color')) - $prefs'birthday_calendar''color' = $prop'color'; - if (isset($prop'showalarms')) - $prefs'calendar_birthdays_alarm_type' = $prop'showalarms' ? $this->alarm_types0 : ''; - $this->rc->user->save_prefs($prefs); - return true; + /** + * Set active/subscribed state of a calendar + * Save a list of hidden calendars in user prefs + * + * @see calendar_driver::subscribe_calendar() + */ + public function subscribe_calendar($prop) + { + $hidden = array_flip(explode(',', $this->rc->config->get('hidden_calendars', ''))); + + if ($prop'active') { + unset($hidden$prop'id'); + } + else { + $hidden$prop'id' = 1; + } + + return $this->rc->user->save_prefs(array('hidden_calendars' => join(',', array_keys($hidden)))); } - $query = $this->rc->db->query( - "UPDATE " . $this->db_calendars . " - SET name=?, color=?, showalarms=? - WHERE calendar_id=? - AND user_id=?", - $prop'name', - $prop'color', - $prop'showalarms'?1:0, - $prop'id', - $this->rc->user->ID - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * Set active/subscribed state of a calendar - * Save a list of hidden calendars in user prefs - * - * @see calendar_driver::subscribe_calendar() - */ - public function subscribe_calendar($prop) - { - $hidden = array_flip(explode(',', $this->rc->config->get('hidden_calendars', ''))); - - if ($prop'active') - unset($hidden$prop'id'); - else - $hidden$prop'id' = 1; - - return $this->rc->user->save_prefs(array('hidden_calendars' => join(',', array_keys($hidden)))); - } - - /** - * Delete the given calendar with all its contents - * - * @see calendar_driver::delete_calendar() - */ - public function delete_calendar($prop) - { - if (!$this->calendars$prop'id') - return false; - - // events and attachments will be deleted by foreign key cascade - - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_calendars . " - WHERE calendar_id=?", - $prop'id' - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * Search for shared or otherwise not listed calendars the user has access - * - * @param string Search string - * @param string Section/source to search - * @return array List of calendars - */ - public function search_calendars($query, $source) - { - // not implemented - return array(); - } - - /** - * Add a single event to the database - * - * @param array Hash array with event properties - * @see calendar_driver::new_event() - */ - public function new_event($event) - { - if (!$this->validate($event)) - return false; - - if (!empty($this->calendars)) { - if ($event'calendar' && !$this->calendars$event'calendar') - return false; - if (!$event'calendar') - $event'calendar' = reset(array_keys($this->calendars)); + /** + * Delete the given calendar with all its contents + * + * @see calendar_driver::delete_calendar() + */ + public function delete_calendar($prop) + { + if (!$this->calendars$prop'id') { + return false; + } + + // events and attachments will be deleted by foreign key cascade - if ($event_id = $this->_insert_event($event)) { - $this->_update_recurring($event); - } + $query = $this->rc->db->query( + "DELETE FROM `{$this->db_calendars}` WHERE `calendar_id` = ? AND `user_id` = ?", + $prop'id', + $this->rc->user->ID + ); - return $event_id; + return $this->rc->db->affected_rows($query); } - - return false; - } - - /** - * - */ - private function _insert_event(&$event) - { - $event = $this->_save_preprocess($event); - - $this->rc->db->query(sprintf( - "INSERT INTO " . $this->db_events . " - (calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence, - title, description, location, categories, url, free_busy, priority, sensitivity, status, attendees, alarms, notifyat) - VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - $this->rc->db->quote_identifier('start'), - $this->rc->db->quote_identifier('end'), - $this->rc->db->now(), - $this->rc->db->now() - ), - $event'calendar', - strval($event'uid'), - intval($event'recurrence_id'), - strval($event'_instance'), - intval($event'isexception'), - $event'start'->format(self::DB_DATE_FORMAT), - $event'end'->format(self::DB_DATE_FORMAT), - intval($event'all_day'), - $event'_recurrence', - strval($event'title'), - strval($event'description'), - strval($event'location'), - join(',', (array)$event'categories'), - strval($event'url'), - intval($event'free_busy'), - intval($event'priority'), - intval($event'sensitivity'), - strval($event'status'), - $event'attendees', - $event'alarms', - $event'notifyat' - ); - - $event_id = $this->rc->db->insert_id($this->db_events); - - if ($event_id) { - $event'id' = $event_id; - - // add attachments - if (!empty($event'attachments')) { - foreach ($event'attachments' as $attachment) { - $this->add_attachment($attachment, $event_id); - unset($attachment); - } - } - - return $event_id; + + /** + * Search for shared or otherwise not listed calendars the user has access + * + * @param string Search string + * @param string Section/source to search + * + * @return array List of calendars + */ + public function search_calendars($query, $source) + { + // not implemented + return array(); } - return false; - } - - /** - * Update an event entry with the given data - * - * @param array Hash array with event properties - * @see calendar_driver::edit_event() - */ - public function edit_event($event) - { - if (!empty($this->calendars)) { - $update_master = false; - $update_recurring = true; - $old = $this->get_event($event); - $ret = true; - - // check if update affects scheduling and update attendee status accordingly - $reschedule = $this->_check_scheduling($event, $old, true); - - // increment sequence number - if (empty($event'sequence') && $reschedule) - $event'sequence' = max($event'sequence', $old'sequence') + 1; - - // modify a recurring event, check submitted savemode to do the right things - if ($old'recurrence' || $old'recurrence_id') { - $master = $old'recurrence_id' ? $this->get_event(array('id' => $old'recurrence_id')) : $old; - - // keep saved exceptions (not submitted by the client) - if ($old'recurrence''EXDATE') - $event'recurrence''EXDATE' = $old'recurrence''EXDATE'; - - switch ($event'_savemode') { - case 'new': - $event'uid' = $this->cal->generate_uid(); - return $this->new_event($event); - - case 'current': - // save as exception - $event'isexception' = 1; - $update_recurring = false; - - // set exception to first instance (= master) - if ($event'id' == $master'id') { - $event += $old; - $event'recurrence_id' = $master'id'; - $event'_instance' = libcalendaring::recurrence_instance_identifier($old, $master'allday'); - $event'isexception' = 1; - $event_id = $this->_insert_event($event); - return $event_id; - } - break; - - case 'future': - if ($master'id' != $event'id') { - // set until-date on master event, then save this instance as new recurring event - $master'recurrence''UNTIL' = clone $event'start'; - $master'recurrence''UNTIL'->sub(new DateInterval('P1D')); - unset($master'recurrence''COUNT'); - $update_master = true; - - // if recurrence COUNT, update value to the correct number of future occurences - if ($event'recurrence''COUNT') { - $fromdate = clone $event'start'; - $fromdate->setTimezone($this->server_timezone); - $sqlresult = $this->rc->db->query(sprintf( - "SELECT event_id FROM " . $this->db_events . " - WHERE calendar_id IN (%s) - AND %s >= ? - AND recurrence_id=?", - $this->calendar_ids, - $this->rc->db->quote_identifier('start') - ), - $fromdate->format(self::DB_DATE_FORMAT), - $master'id'); - if ($count = $this->rc->db->num_rows($sqlresult)) - $event'recurrence''COUNT' = $count; - } - - $update_recurring = true; - $event'recurrence_id' = 0; - $event'isexception' = 0; - $event'_instance' = ''; - break; + /** + * Add a single event to the database + * + * @param array Hash array with event properties + * @see calendar_driver::new_event() + */ + public function new_event($event) + { + if (!$this->validate($event)) { + return false; + } + + if (!empty($this->calendars)) { + if ($event'calendar' && !$this->calendars$event'calendar') { + return false; } - // else: 'future' == 'all' if modifying the master event - - default: // 'all' is default - $event'id' = $master'id'; - $event'recurrence_id' = 0; - - // use start date from master but try to be smart on time or duration changes - $old_start_date = $old'start'->format('Y-m-d'); - $old_start_time = $old'allday' ? '' : $old'start'->format('H:i'); - $old_duration = $old'end'->format('U') - $old'start'->format('U'); - - $new_start_date = $event'start'->format('Y-m-d'); - $new_start_time = $event'allday' ? '' : $event'start'->format('H:i'); - $new_duration = $event'end'->format('U') - $event'start'->format('U'); - - $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; - $date_shift = $old'start'->diff($event'start'); - - // shifted or resized - if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { - $event'start' = $master'start'->add($old'start'->diff($event'start')); - $event'end' = clone $event'start'; - $event'end'->add(new DateInterval('PT'.$new_duration.'S')); + + if (!$event'calendar') { + $event'calendar' = reset(array_keys($this->calendars)); } - // dates did not change, use the ones from master - else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) { - $event'start' = $master'start'; - $event'end' = $master'end'; + + if ($event_id = $this->_insert_event($event)) { + $this->_update_recurring($event); } - - // adjust recurrence-id when start changed and therefore the entire recurrence chain changes - if (is_array($event'recurrence') && ($old_start_date != $new_start_date || $old_start_time != $new_start_time) - && ($exceptions = $this->_load_exceptions($old))) { - $recurrence_id_format = libcalendaring::recurrence_id_format($event); - foreach ($exceptions as $exception) { - $recurrence_id = rcube_utils::anytodatetime($exception'_instance', $old'start'->getTimezone()); - if (is_a($recurrence_id, 'DateTime')) { - $recurrence_id->add($date_shift); - $exception'_instance' = $recurrence_id->format($recurrence_id_format); - $this->_update_event($exception, false); + + return $event_id; + } + + return false; + } + + /** + * + */ + private function _insert_event(&$event) + { + $event = $this->_save_preprocess($event); + $now = $this->rc->db->now(); + + $this->rc->db->query( + "INSERT INTO `{$this->db_events}`" + . " (`calendar_id`, `created`, `changed`, `uid`, `recurrence_id`, `instance`," + . " `isexception`, `start`, `end`, `all_day`, `recurrence`, `title`, `description`," + . " `location`, `categories`, `url`, `free_busy`, `priority`, `sensitivity`," + . " `status`, `attendees`, `alarms`, `notifyat`)" + . " VALUES (?, $now, $now, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + $event'calendar', + strval($event'uid'), + intval($event'recurrence_id'), + strval($event'_instance'), + intval($event'isexception'), + $event'start'->format(self::DB_DATE_FORMAT), + $event'end'->format(self::DB_DATE_FORMAT), + intval($event'all_day'), + $event'_recurrence', + strval($event'title'), + strval($event'description'), + strval($event'location'), + join(',', (array)$event'categories'), + strval($event'url'), + intval($event'free_busy'), + intval($event'priority'), + intval($event'sensitivity'), + strval($event'status'), + $event'attendees', + $event'alarms', + $event'notifyat' + ); + + $event_id = $this->rc->db->insert_id($this->db_events); + + if ($event_id) { + $event'id' = $event_id; + + // add attachments + if (!empty($event'attachments')) { + foreach ($event'attachments' as $attachment) { + $this->add_attachment($attachment, $event_id); + unset($attachment); } - } } - - $ret = $event'id'; // return master ID - break; - } - } - - $success = $this->_update_event($event, $update_recurring); - - if ($success && $update_master) - $this->_update_event($master, true); - - return $success ? $ret : false; - } - - return false; - } - - /** - * Extended event editing with possible changes to the argument - * - * @param array Hash array with event properties - * @param string New participant status - * @param array List of hash arrays with updated attendees - * @return boolean True on success, False on error - */ - public function edit_rsvp(&$event, $status, $attendees) - { - $update_event = $event; - - // apply changes to master (and all exceptions) - if ($event'_savemode' == 'all' && $event'recurrence_id') { - $update_event = $this->get_event(array('id' => $event'recurrence_id')); - $update_event'_savemode' = $event'_savemode'; - calendar::merge_attendee_data($update_event, $attendees); - } - if ($ret = $this->update_attendees($update_event, $attendees)) { - // replace $event with effectively updated event (for iTip reply) - if ($ret !== true && $ret != $update_event'id' && ($new_event = $this->get_event(array('id' => $ret)))) { - $event = $new_event; - } - else { - $event = $update_event; - } + return $event_id; + } + + return false; } - return $ret; - } - - /** - * Update the participant status for the given attendees - * - * @see calendar_driver::update_attendees() - */ - public function update_attendees(&$event, $attendees) - { - $success = $this->edit_event($event, true); - - // apply attendee updates to recurrence exceptions too - if ($success && $event'_savemode' == 'all' && !empty($event'recurrence') && empty($event'recurrence_id') && ($exceptions = $this->_load_exceptions($event))) { - foreach ($exceptions as $exception) { - calendar::merge_attendee_data($exception, $attendees); - $this->_update_event($exception, false); - } + /** + * Update an event entry with the given data + * + * @param array Hash array with event properties + * @see calendar_driver::edit_event() + */ + public function edit_event($event) + { + if (!empty($this->calendars)) { + $update_master = false; + $update_recurring = true; + + $old = $this->get_event($event); + $ret = true; + + // check if update affects scheduling and update attendee status accordingly + $reschedule = $this->_check_scheduling($event, $old, true); + + // increment sequence number + if (empty($event'sequence') && $reschedule) { + $event'sequence' = max($event'sequence', $old'sequence') + 1; + } + + // modify a recurring event, check submitted savemode to do the right things + if ($old'recurrence' || $old'recurrence_id') { + $master = $old'recurrence_id' ? $this->get_event(array('id' => $old'recurrence_id')) : $old; + + // keep saved exceptions (not submitted by the client) + if ($old'recurrence''EXDATE') { + $event'recurrence''EXDATE' = $old'recurrence''EXDATE'; + } + + switch ($event'_savemode') { + case 'new': + $event'uid' = $this->cal->generate_uid(); + return $this->new_event($event); + + case 'current': + // save as exception + $event'isexception' = 1; + $update_recurring = false; + + // set exception to first instance (= master) + if ($event'id' == $master'id') { + $event += $old; + $event'recurrence_id' = $master'id'; + $event'_instance' = libcalendaring::recurrence_instance_identifier($old, $master'allday'); + $event'isexception' = 1; + $event_id = $this->_insert_event($event); + + return $event_id; + } + break; + + case 'future': + if ($master'id' != $event'id') { + // set until-date on master event, then save this instance as new recurring event + $master'recurrence''UNTIL' = clone $event'start'; + $master'recurrence''UNTIL'->sub(new DateInterval('P1D')); + unset($master'recurrence''COUNT'); + $update_master = true; + + // if recurrence COUNT, update value to the correct number of future occurences + if ($event'recurrence''COUNT') { + $fromdate = clone $event'start'; + $fromdate->setTimezone($this->server_timezone); + + $query = $this->rc->db->query( + "SELECT `event_id` FROM `{$this->db_events}`" + . " WHERE `calendar_id` IN ({$this->calendar_ids})" + . " AND `start` >= ? AND `recurrence_id` = ?", + $fromdate->format(self::DB_DATE_FORMAT), + $master'id' + ); + + if ($count = $this->rc->db->num_rows($query)) { + $event'recurrence''COUNT' = $count; + } + } + + $update_recurring = true; + $event'recurrence_id' = 0; + $event'isexception' = 0; + $event'_instance' = ''; + break; + } + // else: 'future' == 'all' if modifying the master event + + default: // 'all' is default + $event'id' = $master'id'; + $event'recurrence_id' = 0; + + // use start date from master but try to be smart on time or duration changes + $old_start_date = $old'start'->format('Y-m-d'); + $old_start_time = $old'allday' ? '' : $old'start'->format('H:i'); + $old_duration = $old'end'->format('U') - $old'start'->format('U'); + + $new_start_date = $event'start'->format('Y-m-d'); + $new_start_time = $event'allday' ? '' : $event'start'->format('H:i'); + $new_duration = $event'end'->format('U') - $event'start'->format('U'); + + $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; + $date_shift = $old'start'->diff($event'start'); + + // shifted or resized + if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { + $event'start' = $master'start'->add($old'start'->diff($event'start')); + $event'end' = clone $event'start'; + $event'end'->add(new DateInterval('PT'.$new_duration.'S')); + } + // dates did not change, use the ones from master + else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) { + $event'start' = $master'start'; + $event'end' = $master'end'; + } + + // adjust recurrence-id when start changed and therefore the entire recurrence chain changes + if (is_array($event'recurrence') + && ($old_start_date != $new_start_date || $old_start_time != $new_start_time) + && ($exceptions = $this->_load_exceptions($old)) + ) { + $recurrence_id_format = libcalendaring::recurrence_id_format($event); + + foreach ($exceptions as $exception) { + $recurrence_id = rcube_utils::anytodatetime($exception'_instance', $old'start'->getTimezone()); + if (is_a($recurrence_id, 'DateTime')) { + $recurrence_id->add($date_shift); + $exception'_instance' = $recurrence_id->format($recurrence_id_format); + $this->_update_event($exception, false); + } + } + } + + $ret = $event'id'; // return master ID + break; + } + } + + $success = $this->_update_event($event, $update_recurring); + + if ($success && $update_master) { + $this->_update_event($master, true); + } + + return $success ? $ret : false; + } + + return false; } - return $success; - } - - /** - * Determine whether the current change affects scheduling and reset attendee status accordingly - */ - private function _check_scheduling(&$event, $old, $update = true) - { - // skip this check when importing iCal/iTip events - if (isset($event'sequence') || !empty($event'_method')) { - return false; + /** + * Extended event editing with possible changes to the argument + * + * @param array Hash array with event properties + * @param string New participant status + * @param array List of hash arrays with updated attendees + * + * @return boolean True on success, False on error + */ + public function edit_rsvp(&$event, $status, $attendees) + { + $update_event = $event; + + // apply changes to master (and all exceptions) + if ($event'_savemode' == 'all' && $event'recurrence_id') { + $update_event = $this->get_event(array('id' => $event'recurrence_id')); + $update_event'_savemode' = $event'_savemode'; + calendar::merge_attendee_data($update_event, $attendees); + } + + if ($ret = $this->update_attendees($update_event, $attendees)) { + // replace $event with effectively updated event (for iTip reply) + if ($ret !== true && $ret != $update_event'id' && ($new_event = $this->get_event(array('id' => $ret)))) { + $event = $new_event; + } + else { + $event = $update_event; + } + } + + return $ret; } - $reschedule = false; + /** + * Update the participant status for the given attendees + * + * @see calendar_driver::update_attendees() + */ + public function update_attendees(&$event, $attendees) + { + $success = $this->edit_event($event, true); + + // apply attendee updates to recurrence exceptions too + if ($success && $event'_savemode' == 'all' + && !empty($event'recurrence') + && empty($event'recurrence_id') + && ($exceptions = $this->_load_exceptions($event)) + ) { + foreach ($exceptions as $exception) { + calendar::merge_attendee_data($exception, $attendees); + $this->_update_event($exception, false); + } + } + + return $success; + } - // iterate through the list of properties considered 'significant' for scheduling - foreach (self::$scheduling_properties as $prop) { - $a = $old$prop; - $b = $event$prop; - if ($event'allday' && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) { - $a = $a->format('Y-m-d'); - $b = $b->format('Y-m-d'); + /** + * Determine whether the current change affects scheduling and reset attendee status accordingly + */ + private function _check_scheduling(&$event, $old, $update = true) + { + // skip this check when importing iCal/iTip events + if (isset($event'sequence') || !empty($event'_method')) { + return false; } - if ($prop == 'recurrence' && is_array($a) && is_array($b)) { - unset($a'EXCEPTIONS', $b'EXCEPTIONS'); - $a = array_filter($a); - $b = array_filter($b); - // advanced rrule comparison: no rescheduling if series was shortened - if ($a'COUNT' && $b'COUNT' && $b'COUNT' < $a'COUNT') { - unset($a'COUNT', $b'COUNT'); + $reschedule = false; + + // iterate through the list of properties considered 'significant' for scheduling + foreach (self::$scheduling_properties as $prop) { + $a = $old$prop; + $b = $event$prop; + + if ($event'allday' && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) { + $a = $a->format('Y-m-d'); + $b = $b->format('Y-m-d'); } - else if ($a'UNTIL' && $b'UNTIL' && $b'UNTIL' < $a'UNTIL') { - unset($a'UNTIL', $b'UNTIL'); + + if ($prop == 'recurrence' && is_array($a) && is_array($b)) { + unset($a'EXCEPTIONS', $b'EXCEPTIONS'); + $a = array_filter($a); + $b = array_filter($b); + + // advanced rrule comparison: no rescheduling if series was shortened + if ($a'COUNT' && $b'COUNT' && $b'COUNT' < $a'COUNT') { + unset($a'COUNT', $b'COUNT'); + } + else if ($a'UNTIL' && $b'UNTIL' && $b'UNTIL' < $a'UNTIL') { + unset($a'UNTIL', $b'UNTIL'); + } + } + + if ($a != $b) { + $reschedule = true; + break; } } - if ($a != $b) { - $reschedule = true; - break; + + // reset all attendee status to needs-action (#4360) + if ($update && $reschedule && is_array($event'attendees')) { + $is_organizer = false; + $emails = $this->cal->get_user_emails(); + $attendees = $event'attendees'; + + foreach ($attendees as $i => $attendee) { + if ($attendee'role' == 'ORGANIZER' && $attendee'email' && in_array(strtolower($attendee'email'), $emails)) { + $is_organizer = true; + } + else if ($attendee'role' != 'ORGANIZER' + && $attendee'role' != 'NON-PARTICIPANT' + && $attendee'status' != 'DELEGATED' + ) { + $attendees$i'status' = 'NEEDS-ACTION'; + $attendees$i'rsvp' = true; + } + } + + // update attendees only if I'm the organizer + if ($is_organizer || ($event'organizer' && in_array(strtolower($event'organizer''email'), $emails))) { + $event'attendees' = $attendees; + } } - } - // reset all attendee status to needs-action (#4360) - if ($update && $reschedule && is_array($event'attendees')) { - $is_organizer = false; - $emails = $this->cal->get_user_emails(); - $attendees = $event'attendees'; - foreach ($attendees as $i => $attendee) { - if ($attendee'role' == 'ORGANIZER' && $attendee'email' && in_array(strtolower($attendee'email'), $emails)) { - $is_organizer = true; - } - else if ($attendee'role' != 'ORGANIZER' && $attendee'role' != 'NON-PARTICIPANT' && $attendee'status' != 'DELEGATED') { - $attendees$i'status' = 'NEEDS-ACTION'; - $attendees$i'rsvp' = true; - } - } - - // update attendees only if I'm the organizer - if ($is_organizer || ($event'organizer' && in_array(strtolower($event'organizer''email'), $emails))) { - $event'attendees' = $attendees; - } + return $reschedule; } - return $reschedule; - } - - /** - * Convert save data to be used in SQL statements - */ - private function _save_preprocess($event) - { - // shift dates to server's timezone (except for all-day events) - if (!$event'allday') { - $event'start' = clone $event'start'; - $event'start'->setTimezone($this->server_timezone); - $event'end' = clone $event'end'; - $event'end'->setTimezone($this->server_timezone); - } - - // compose vcalendar-style recurrencue rule from structured data - $rrule = $event'recurrence' ? libcalendaring::to_rrule($event'recurrence') : ''; - $event'_recurrence' = rtrim($rrule, ';'); - $event'free_busy' = intval($this->free_busy_mapstrtolower($event'free_busy')); - $event'sensitivity' = intval($this->sensitivity_mapstrtolower($event'sensitivity')); - - if ($event'free_busy' == 'tentative') { - $event'status' = 'TENTATIVE'; - } + /** + * Convert save data to be used in SQL statements + */ + private function _save_preprocess($event) + { + // shift dates to server's timezone (except for all-day events) + if (!$event'allday') { + $event'start' = clone $event'start'; + $event'start'->setTimezone($this->server_timezone); + $event'end' = clone $event'end'; + $event'end'->setTimezone($this->server_timezone); + } - if (isset($event'allday')) { - $event'all_day' = $event'allday' ? 1 : 0; - } + // compose vcalendar-style recurrencue rule from structured data + $rrule = $event'recurrence' ? libcalendaring::to_rrule($event'recurrence') : ''; - // compute absolute time to notify the user - $event'notifyat' = $this->_get_notification($event); + $event'_recurrence' = rtrim($rrule, ';'); + $event'free_busy' = intval($this->free_busy_mapstrtolower($event'free_busy')); + $event'sensitivity' = intval($this->sensitivity_mapstrtolower($event'sensitivity')); - if (is_array($event'valarms')) { - $event'alarms' = $this->serialize_alarms($event'valarms'); - } + if ($event'free_busy' == 'tentative') { + $event'status' = 'TENTATIVE'; + } - // process event attendees - if (!empty($event'attendees')) - $event'attendees' = json_encode((array)$event'attendees'); - else - $event'attendees' = ''; - - return $event; - } - - /** - * Compute absolute time to notify the user - */ - private function _get_notification($event) - { - if ($event'valarms' && $event'start' > new DateTime()) { - $alarm = libcalendaring::get_next_alarm($event); - - if ($alarm'time' && in_array($alarm'action', $this->alarm_types)) - return date('Y-m-d H:i:s', $alarm'time'); - } + if (isset($event'allday')) { + $event'all_day' = $event'allday' ? 1 : 0; + } - return null; - } - - /** - * Save the given event record to database - * - * @param array Event data - * @param boolean True if recurring events instances should be updated, too - */ - private function _update_event($event, $update_recurring = true) - { - $event = $this->_save_preprocess($event); - $sql_set = array(); - $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat'); - foreach ($set_cols as $col) { - if (is_object($event$col) && is_a($event$col, 'DateTime')) - $sql_set = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event$col->format(self::DB_DATE_FORMAT)); - else if (is_array($event$col)) - $sql_set = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event$col)); - else if (array_key_exists($col, $event)) - $sql_set = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event$col); - } - - if ($event'_recurrence') - $sql_set = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event'_recurrence'); - - if ($event'_instance') - $sql_set = $this->rc->db->quote_identifier('instance') . '=' . $this->rc->db->quote($event'_instance'); - - if ($event'_fromcalendar' && $event'_fromcalendar' != $event'calendar') - $sql_set = 'calendar_id=' . $this->rc->db->quote($event'calendar'); - - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_events . " - SET changed=%s %s - WHERE event_id=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $this->rc->db->now(), - ($sql_set ? ', ' . join(', ', $sql_set) : '') - ), - $event'id' - ); - - $success = $this->rc->db->affected_rows($query); - - // add attachments - if ($success && !empty($event'attachments')) { - foreach ($event'attachments' as $attachment) { - $this->add_attachment($attachment, $event'id'); - unset($attachment); - } - } + // compute absolute time to notify the user + $event'notifyat' = $this->_get_notification($event); - // remove attachments - if ($success && !empty($event'deleted_attachments')) { - foreach ($event'deleted_attachments' as $attachment) { - $this->remove_attachment($attachment, $event'id'); - } - } + if (is_array($event'valarms')) { + $event'alarms' = $this->serialize_alarms($event'valarms'); + } + + // process event attendees + if (!empty($event'attendees')) { + $event'attendees' = json_encode((array)$event'attendees'); + } + else { + $event'attendees' = ''; + } - if ($success) { - unset($this->cache$event'id'); - if ($update_recurring) - $this->_update_recurring($event); + return $event; } - return $success; - } - - /** - * Insert "fake" entries for recurring occurences of this event - */ - private function _update_recurring($event) - { - if (empty($this->calendars)) - return; - - if (!empty($event'recurrence')) { - $exdata = array(); - $exceptions = $this->_load_exceptions($event); - - foreach ($exceptions as $exception) { - $exdate = substr($exception'_instance', 0, 8); - $exdata$exdate = $exception; - } + /** + * Compute absolute time to notify the user + */ + private function _get_notification($event) + { + if ($event'valarms' && $event'start' > new DateTime()) { + $alarm = libcalendaring::get_next_alarm($event); + + if ($alarm'time' && in_array($alarm'action', $this->alarm_types)) { + return date('Y-m-d H:i:s', $alarm'time'); + } + } } - // clear existing recurrence copies - $this->rc->db->query( - "DELETE FROM " . $this->db_events . " - WHERE recurrence_id=? - AND isexception=0 - AND calendar_id IN (" . $this->calendar_ids . ")", - $event'id' - ); - - // create new fake entries - if (!empty($event'recurrence')) { - // include library class - require_once($this->cal->home . '/lib/calendar_recurrence.php'); - - $recurrence = new calendar_recurrence($this->cal, $event); - - $count = 0; - $event'allday' = $event'all_day'; - $duration = $event'start'->diff($event'end'); - $recurrence_id_format = libcalendaring::recurrence_id_format($event); - while ($next_start = $recurrence->next_start()) { - $instance = $next_start->format($recurrence_id_format); - $datestr = substr($instance, 0, 8); - - // skip exceptions - // TODO: merge updated data from master event - if ($exdata$datestr) { - continue; - } - - $next_start->setTimezone($this->server_timezone); - $next_end = clone $next_start; - $next_end->add($duration); - - $notify_at = $this->_get_notification(array('alarms' => $event'alarms', 'start' => $next_start, 'end' => $next_end, 'status' => $event'status')); - $query = $this->rc->db->query(sprintf( - "INSERT INTO " . $this->db_events . " - (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat) - SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ? - FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")", - $this->rc->db->quote_identifier('start'), - $this->rc->db->quote_identifier('end'), - $this->rc->db->now(), - $this->rc->db->now() - ), - $event'id', - $instance, - $next_start->format(self::DB_DATE_FORMAT), - $next_end->format(self::DB_DATE_FORMAT), - $notify_at, - $event'id' + /** + * Save the given event record to database + * + * @param array Event data + * @param boolean True if recurring events instances should be updated, too + */ + private function _update_event($event, $update_recurring = true) + { + $event = $this->_save_preprocess($event); + $sql_args = array(); + $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', + 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', + 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat' ); - - if (!$this->rc->db->affected_rows($query)) - break; - - // stop adding events for inifinite recurrence after 20 years - if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20)) - break; - } - - // remove all exceptions after recurrence end - if ($next_end && !empty($exceptions)) { - $this->rc->db->query( - "DELETE FROM " . $this->db_events . " - WHERE `recurrence_id`=? - AND `isexception`=1 - AND `start` > ? - AND `calendar_id` IN (" . $this->calendar_ids . ")", - $event'id', - $next_end->format(self::DB_DATE_FORMAT) + + foreach ($set_cols as $col) { + if (is_object($event$col) && is_a($event$col, 'DateTime')) { + $sql_args$col = $event$col->format(self::DB_DATE_FORMAT); + } + else if (is_array($event$col)) { + $sql_args$col = join(',', $event$col); + } + else if (array_key_exists($col, $event)) { + $sql_args$col = $event$col; + } + } + + if ($event'_recurrence') { + $sql_args'recurrence' = $event'_recurrence'; + } + + if ($event'_instance') { + $sql_args'instance' = $event'_instance'; + } + + if ($event'_fromcalendar' && $event'_fromcalendar' != $event'calendar') { + $sql_args'calendar_id' = $event'calendar'; + } + + $sql_set = ''; + foreach (array_keys($sql_args) as $col) { + $sql_set .= ", `$col` = ?"; + } + + $sql_args = array_values($sql_args); + $sql_args = $event'id'; + + $query = $this->rc->db->query( + "UPDATE `{$this->db_events}`" + . " SET `changed` = " . $this->rc->db->now() . $sql_set + . " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids})", + $sql_args ); - } - } - } - - /** - * - */ - private function _load_exceptions($event, $instance_id = null) - { - $sql_add_where = ''; - if (!empty($instance_id)) { - $sql_add_where = 'AND `instance`=?'; - } - $result = $this->rc->db->query( - "SELECT * FROM " . $this->db_events . " - WHERE `recurrence_id`=? - AND `isexception`=1 - AND `calendar_id` IN (" . $this->calendar_ids . ") - $sql_add_where - ORDER BY `instance`, `start`", - $event'id', - $instance_id - ); - - $exceptions = array(); - while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr'event_id') { - $exception = $this->_read_postprocess($sql_arr); - $instance = $exception'_instance' ?: $exception'start'->format($exception'allday' ? 'Ymd' : 'Ymd\THis'); - $exceptions$instance = $exception; + $success = $this->rc->db->affected_rows($query); + + // add attachments + if ($success && !empty($event'attachments')) { + foreach ($event'attachments' as $attachment) { + $this->add_attachment($attachment, $event'id'); + unset($attachment); + } + } + + // remove attachments + if ($success && !empty($event'deleted_attachments')) { + foreach ($event'deleted_attachments' as $attachment) { + $this->remove_attachment($attachment, $event'id'); + } + } + + if ($success) { + unset($this->cache$event'id'); + if ($update_recurring) { + $this->_update_recurring($event); + } + } + + return $success; } - return $exceptions; - } - - /** - * Move a single event - * - * @param array Hash array with event properties - * @see calendar_driver::move_event() - */ - public function move_event($event) - { - // let edit_event() do all the magic - return $this->edit_event($event + (array)$this->get_event($event)); - } - - /** - * Resize a single event - * - * @param array Hash array with event properties - * @see calendar_driver::resize_event() - */ - public function resize_event($event) - { - // let edit_event() do all the magic - return $this->edit_event($event + (array)$this->get_event($event)); - } - - /** - * Remove a single event from the database - * - * @param array Hash array with event properties - * @param boolean Remove record irreversible (@TODO) - * - * @see calendar_driver::remove_event() - */ - public function remove_event($event, $force = true) - { - if (!empty($this->calendars)) { - $event += (array)$this->get_event($event); - $master = $event; - $update_master = false; - $savemode = 'all'; - $ret = true; - - // read master if deleting a recurring event - if ($event'recurrence' || $event'recurrence_id') { - $master = $event'recurrence_id' ? $this->get_event(array('id' => $event'recurrence_id')) : $event; - $savemode = $event'_savemode'; - } - - switch ($savemode) { - case 'current': - // add exception to master event - $master'recurrence''EXDATE' = $event'start'; - $update_master = true; - - // just delete this single occurence - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_events . " - WHERE calendar_id IN (" . $this->calendar_ids . ") - AND event_id=?", + /** + * Insert "fake" entries for recurring occurences of this event + */ + private function _update_recurring($event) + { + if (empty($this->calendars)) { + return; + } + + if (!empty($event'recurrence')) { + $exdata = array(); + $exceptions = $this->_load_exceptions($event); + + foreach ($exceptions as $exception) { + $exdate = substr($exception'_instance', 0, 8); + $exdata$exdate = $exception; + } + } + + // clear existing recurrence copies + $this->rc->db->query( + "DELETE FROM `{$this->db_events}`" + . " WHERE `recurrence_id` = ? AND `isexception` = 0 AND `calendar_id` IN ({$this->calendar_ids})", $event'id' - ); - break; - - case 'future': - if ($master'id' != $event'id') { - // set until-date on master event - $master'recurrence''UNTIL' = clone $event'start'; - $master'recurrence''UNTIL'->sub(new DateInterval('P1D')); - unset($master'recurrence''COUNT'); - $update_master = true; - - // delete this and all future instances - $fromdate = clone $event'start'; - $fromdate->setTimezone($this->server_timezone); - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_events . " - WHERE calendar_id IN (" . $this->calendar_ids . ") - AND " . $this->rc->db->quote_identifier('start') . " >= ? - AND recurrence_id=?", - $fromdate->format(self::DB_DATE_FORMAT), - $master'id' - ); - $ret = $master'id'; - break; - } - // else: future == all if modifying the master event - - default: // 'all' is default - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_events . " - WHERE (event_id=? OR recurrence_id=?) - AND calendar_id IN (" . $this->calendar_ids . ")", - $master'id', - $master'id' - ); - break; - } - - $success = $this->rc->db->affected_rows($query); - if ($success && $update_master) - $this->_update_event($master, true); - - return $success ? $ret : false; - } - - return false; - } - - /** - * Return data of a specific event - * @param mixed Hash array with event properties or event UID - * @param integer Bitmask defining the scope to search events in - * @param boolean If true, recurrence exceptions shall be added - * @return array Hash array with event properties - */ - public function get_event($event, $scope = 0, $full = false) - { - $id = is_array($event) ? ($event'id' ?: $event'uid') : $event; - $cal = is_array($event) ? $event'calendar' : null; - $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid'; - - $where_add = ''; - if (is_array($event) && !$event'id' && !empty($event'_instance')) { - $where_add = 'AND instance=' . $this->rc->db->quote($event'_instance'); - } + ); - if ($this->cache$id) - return $this->cache$id; + // create new fake entries + if (!empty($event'recurrence')) { + // include library class + require_once($this->cal->home . '/lib/calendar_recurrence.php'); + + $recurrence = new calendar_recurrence($this->cal, $event); + $count = 0; + $event'allday' = $event'all_day'; + $duration = $event'start'->diff($event'end'); + $recurrence_id_format = libcalendaring::recurrence_id_format($event); + + while ($next_start = $recurrence->next_start()) { + $instance = $next_start->format($recurrence_id_format); + $datestr = substr($instance, 0, 8); + + // skip exceptions + // TODO: merge updated data from master event + if ($exdata$datestr) { + continue; + } - // get event from the address books birthday calendar - if ($cal == self::BIRTHDAY_CALENDAR_ID) { - return $this->get_birthday_event($id); - } + $next_start->setTimezone($this->server_timezone); + $next_end = clone $next_start; + $next_end->add($duration); + + $notify_at = $this->_get_notification(array( + 'alarms' => $event'alarms', + 'start' => $next_start, + 'end' => $next_end, + 'status' => $event'status' + )); + + $now = $this->rc->db->now(); + $query = $this->rc->db->query( + "INSERT INTO `{$this->db_events}`" + . " (`calendar_id`, `recurrence_id`, `created`, `changed`, `uid`, `instance`, `start`, `end`," + . " `all_day`, `sequence`, `recurrence`, `title`, `description`, `location`, `categories`," + . " `url`, `free_busy`, `priority`, `sensitivity`, `status`, `alarms`, `attendees`, `notifyat`)" + . " SELECT `calendar_id`, ?, $now, $now, `uid`, ?, ?, ?," + . " `all_day`, `sequence`, `recurrence`, `title`, `description`, `location`, `categories`," + . " `url`, `free_busy`, `priority`, `sensitivity`, `status`, `alarms`, `attendees`, ?" + . " FROM `{$this->db_events}` WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids})", + $event'id', + $instance, + $next_start->format(self::DB_DATE_FORMAT), + $next_end->format(self::DB_DATE_FORMAT), + $notify_at, + $event'id' + ); + + if (!$this->rc->db->affected_rows($query)) { + break; + } - if ($scope & self::FILTER_ACTIVE) { - $calendars = $this->calendars; - foreach ($calendars as $idx => $cal) { - if (!$cal'active') { - unset($calendars$idx); + // stop adding events for inifinite recurrence after 20 years + if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20)) { + break; + } + } + + // remove all exceptions after recurrence end + if ($next_end && !empty($exceptions)) { + $this->rc->db->query( + "DELETE FROM `{$this->db_events}`" + . " WHERE `recurrence_id` = ? AND `isexception` = 1 AND `start` > ?" + . " AND `calendar_id` IN ({$this->calendar_ids})", + $event'id', + $next_end->format(self::DB_DATE_FORMAT) + ); + } } - } - $cals = join(',', $calendars); } - else { - $cals = $this->calendar_ids; + + /** + * + */ + private function _load_exceptions($event, $instance_id = null) + { + $sql_add_where = ''; + if (!empty($instance_id)) { + $sql_add_where = " AND `instance` = ?"; + } + + $result = $this->rc->db->query( + "SELECT * FROM `{$this->db_events}`" + . " WHERE `recurrence_id` = ? AND `isexception` = 1" + . " AND `calendar_id` IN ({$this->calendar_ids})" . $sql_add_where + . " ORDER BY `instance`, `start`", + $event'id', + $instance_id + ); + + $exceptions = array(); + while (($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr'event_id') { + $exception = $this->_read_postprocess($sql_arr); + $instance = $exception'_instance' ?: $exception'start'->format($exception'allday' ? 'Ymd' : 'Ymd\THis'); + $exceptions$instance = $exception; + } + + return $exceptions; } - $result = $this->rc->db->query(sprintf( - "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . " - WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments - FROM " . $this->db_events . " AS e - WHERE e.calendar_id IN (%s) - AND e.$col=? - %s", - $cals, - $where_add - ), - $id); - - if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr'event_id') { - $event = $this->_read_postprocess($sql_arr); - - // also load recurrence exceptions - if (!empty($event'recurrence') && $full) { - $event'recurrence''EXCEPTIONS' = array_values($this->_load_exceptions($event)); - } - - $this->cache$id = $event; - return $this->cache$id; + /** + * Move a single event + * + * @param array Hash array with event properties + * @see calendar_driver::move_event() + */ + public function move_event($event) + { + // let edit_event() do all the magic + return $this->edit_event($event + (array)$this->get_event($event)); } - return false; - } - - /** - * Get event data - * - * @see calendar_driver::load_events() - */ - public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null) - { - if (empty($calendars)) - $calendars = array_keys($this->calendars); - else if (!is_array($calendars)) - $calendars = explode(',', strval($calendars)); - - // only allow to select from calendars of this use - $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars))); - - // compose (slow) SQL query for searching - // FIXME: improve searching using a dedicated col and normalized values - if ($query) { - foreach (array('title','location','description','categories','attendees') as $col) - $sql_query = $this->rc->db->ilike($col, '%'.$query.'%'); - $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; + /** + * Resize a single event + * + * @param array Hash array with event properties + * @see calendar_driver::resize_event() + */ + public function resize_event($event) + { + // let edit_event() do all the magic + return $this->edit_event($event + (array)$this->get_event($event)); } - - if (!$virtual) - $sql_add .= ' AND e.recurrence_id = 0'; - - if ($modifiedsince) - $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince)); - - $events = array(); - if (!empty($calendar_ids)) { - $result = $this->rc->db->query(sprintf( - "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . " - WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments - FROM " . $this->db_events . " e - WHERE e.calendar_id IN (%s) - AND e.start <= %s AND e.end >= %s - %s", - join(',', $calendar_ids), - $this->rc->db->fromunixtime($end), - $this->rc->db->fromunixtime($start), - $sql_add - )); - - while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) { - $event = $this->_read_postprocess($sql_arr); - $add = true; - - if (!empty($event'recurrence') && !$event'recurrence_id') { - // load recurrence exceptions (i.e. for export) - if (!$virtual) { - $event'recurrence''EXCEPTIONS' = $this->_load_exceptions($event); - } - // check for exception on first instance - else { - $instance = libcalendaring::recurrence_instance_identifier($event); - $exceptions = $this->_load_exceptions($event, $instance); - if ($exceptions && is_array($exceptions$instance)) { - $event = $exceptions$instance; - $add = false; + + /** + * Remove a single event from the database + * + * @param array Hash array with event properties + * @param boolean Remove record irreversible (@TODO) + * + * @see calendar_driver::remove_event() + */ + public function remove_event($event, $force = true) + { + if (!empty($this->calendars)) { + $event += (array)$this->get_event($event); + $master = $event; + $update_master = false; + $savemode = 'all'; + $ret = true; + + // read master if deleting a recurring event + if ($event'recurrence' || $event'recurrence_id') { + $master = $event'recurrence_id' ? $this->get_event(array('id' => $event'recurrence_id')) : $event; + $savemode = $event'_savemode'; + } + + switch ($savemode) { + case 'current': + // add exception to master event + $master'recurrence''EXDATE' = $event'start'; + $update_master = true; + + // just delete this single occurence + $query = $this->rc->db->query( + "DELETE FROM `{$this->db_events}`" + . " WHERE `calendar_id` IN ({$this->calendar_ids}) AND `event_id` = ?", + $event'id' + ); + break; + + case 'future': + if ($master'id' != $event'id') { + // set until-date on master event + $master'recurrence''UNTIL' = clone $event'start'; + $master'recurrence''UNTIL'->sub(new DateInterval('P1D')); + unset($master'recurrence''COUNT'); + $update_master = true; + + // delete this and all future instances + $fromdate = clone $event'start'; + $fromdate->setTimezone($this->server_timezone); + + $query = $this->rc->db->query( + "DELETE FROM `{$this->db_events}`" + . " WHERE `calendar_id` IN ({$this->calendar_ids}) AND `start` >= ? AND `recurrence_id` = ?", + $fromdate->format(self::DB_DATE_FORMAT), + $master'id' + ); + + $ret = $master'id'; + break; + } + // else: future == all if modifying the master event + + default: // 'all' is default + $query = $this->rc->db->query( + "DELETE FROM `{$this->db_events}`" + . " WHERE (`event_id` = ? OR `recurrence_id` = ?) AND `calendar_id` IN ({$this->calendar_ids})", + $master'id', + $master'id' + ); + break; + } + + $success = $this->rc->db->affected_rows($query); + + if ($success && $update_master) { + $this->_update_event($master, true); } - } + + return $success ? $ret : false; } - if ($add) - $events = $event; - } + return false; } - // add events from the address books birthday calendar - if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars) && empty($query)) { - $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince)); + /** + * Return data of a specific event + * + * @param mixed Hash array with event properties or event UID + * @param integer Bitmask defining the scope to search events in + * @param boolean If true, recurrence exceptions shall be added + * + * @return array Hash array with event properties + */ + public function get_event($event, $scope = 0, $full = false) + { + $id = is_array($event) ? ($event'id' ?: $event'uid') : $event; + $cal = is_array($event) ? $event'calendar' : null; + $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid'; + + if ($this->cache$id) { + return $this->cache$id; + } + + // get event from the address books birthday calendar + if ($cal == self::BIRTHDAY_CALENDAR_ID) { + return $this->get_birthday_event($id); + } + + $where_add = ''; + if (is_array($event) && !$event'id' && !empty($event'_instance')) { + $where_add = " AND e.instance = " . $this->rc->db->quote($event'_instance'); + } + + if ($scope & self::FILTER_ACTIVE) { + $calendars = $this->calendars; + foreach ($calendars as $idx => $cal) { + if (!$cal'active') { + unset($calendars$idx); + } + } + $cals = join(',', $calendars); + } + else { + $cals = $this->calendar_ids; + } + + $result = $this->rc->db->query( + "SELECT e.*, (SELECT COUNT(`attachment_id`) FROM `{$this->db_attachments}`" + . " WHERE `event_id` = e.event_id OR `event_id` = e.recurrence_id) AS _attachments" + . " FROM `{$this->db_events}` AS e" + . " WHERE e.calendar_id IN ($cals) AND e.$col = ?" . $where_add, + $id + ); + + if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr'event_id') { + $event = $this->_read_postprocess($sql_arr); + + // also load recurrence exceptions + if (!empty($event'recurrence') && $full) { + $event'recurrence''EXCEPTIONS' = array_values($this->_load_exceptions($event)); + } + + $this->cache$id = $event; + + return $this->cache$id; + } + + return false; } - return $events; - } - - /** - * Get number of events in the given calendar - * - * @param mixed List of calendar IDs to count events (either as array or comma-separated string) - * @param integer Date range start (unix timestamp) - * @param integer Date range end (unix timestamp) - * @return array Hash array with counts grouped by calendar ID - */ - public function count_events($calendars, $start, $end = null) - { - // not implemented - return array(); - } - - /** - * Convert sql record into a rcube style event object - */ - private function _read_postprocess($event) - { - $free_busy_map = array_flip($this->free_busy_map); - $sensitivity_map = array_flip($this->sensitivity_map); - - $event'id' = $event'event_id'; - $event'start' = new DateTime($event'start'); - $event'end' = new DateTime($event'end'); - $event'allday' = intval($event'all_day'); - $event'created' = new DateTime($event'created'); - $event'changed' = new DateTime($event'changed'); - $event'free_busy' = $free_busy_map$event'free_busy'; - $event'sensitivity' = $sensitivity_map$event'sensitivity'; - $event'calendar' = $event'calendar_id'; - $event'recurrence_id' = intval($event'recurrence_id'); - $event'isexception' = intval($event'isexception'); - - // parse recurrence rule - if ($event'recurrence' && preg_match_all('/(A-Z+)=(^;+);?/', $event'recurrence', $m, PREG_SET_ORDER)) { - $event'recurrence' = array(); - foreach ($m as $rr) { - if (is_numeric($rr2)) - $rr2 = intval($rr2); - else if ($rr1 == 'UNTIL') - $rr2 = date_create($rr2); - else if ($rr1 == 'RDATE') - $rr2 = array_map('date_create', explode(',', $rr2)); - else if ($rr1 == 'EXDATE') - $rr2 = array_map('date_create', explode(',', $rr2)); - $event'recurrence'$rr1 = $rr2; - } + /** + * Get event data + * + * @see calendar_driver::load_events() + */ + public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null) + { + if (empty($calendars)) { + $calendars = array_keys($this->calendars); + } + else if (!is_array($calendars)) { + $calendars = explode(',', strval($calendars)); + } + + // only allow to select from calendars of this use + $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars))); + + // compose (slow) SQL query for searching + // FIXME: improve searching using a dedicated col and normalized values + if ($query) { + foreach (array('title','location','description','categories','attendees') as $col) { + $sql_query = $this->rc->db->ilike($col, '%'.$query.'%'); + } + $sql_add = " AND (" . join(' OR ', $sql_query) . ")"; + } + + if (!$virtual) { + $sql_add .= " AND e.recurrence_id = 0"; + } + + if ($modifiedsince) { + $sql_add .= " AND e.changed >= " . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince)); + } + + $events = array(); + if (!empty($calendar_ids)) { + $result = $this->rc->db->query( + "SELECT e.*, (SELECT COUNT(`attachment_id`) FROM `{$this->db_attachments}`" + . " WHERE `event_id` = e.event_id OR `event_id` = e.recurrence_id) AS _attachments" + . " FROM `{$this->db_events}` e" + . " WHERE e.calendar_id IN (" . join(',', $calendar_ids) . ")" + . " AND e.start <= " . $this->rc->db->fromunixtime($end) + . " AND e.end >= " . $this->rc->db->fromunixtime($start) + . $sql_add, + ); + + while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) { + $event = $this->_read_postprocess($sql_arr); + $add = true; + + if (!empty($event'recurrence') && !$event'recurrence_id') { + // load recurrence exceptions (i.e. for export) + if (!$virtual) { + $event'recurrence''EXCEPTIONS' = $this->_load_exceptions($event); + } + // check for exception on first instance + else { + $instance = libcalendaring::recurrence_instance_identifier($event); + $exceptions = $this->_load_exceptions($event, $instance); + + if ($exceptions && is_array($exceptions$instance)) { + $event = $exceptions$instance; + $add = false; + } + } + } + + if ($add) { + $events = $event; + } + } + } + + // add events from the address books birthday calendar + if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars) && empty($query)) { + $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince)); + } + + return $events; } - - if ($event'recurrence_id') { - libcalendaring::identify_recurrence_instance($event); + + /** + * Get number of events in the given calendar + * + * @param mixed List of calendar IDs to count events (either as array or comma-separated string) + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * + * @return array Hash array with counts grouped by calendar ID + */ + public function count_events($calendars, $start, $end = null) + { + // not implemented + return array(); } - - if (strlen($event'instance')) { - $event'_instance' = $event'instance'; - if (empty($event'recurrence_id')) { - $event'recurrence_date' = rcube_utils::anytodatetime($event'_instance', $event'start'->getTimezone()); - } + /** + * Convert sql record into a rcube style event object + */ + private function _read_postprocess($event) + { + $free_busy_map = array_flip($this->free_busy_map); + $sensitivity_map = array_flip($this->sensitivity_map); + + $event'id' = $event'event_id'; + $event'start' = new DateTime($event'start'); + $event'end' = new DateTime($event'end'); + $event'allday' = intval($event'all_day'); + $event'created' = new DateTime($event'created'); + $event'changed' = new DateTime($event'changed'); + $event'free_busy' = $free_busy_map$event'free_busy'; + $event'sensitivity' = $sensitivity_map$event'sensitivity'; + $event'calendar' = $event'calendar_id'; + $event'recurrence_id' = intval($event'recurrence_id'); + $event'isexception' = intval($event'isexception'); + + // parse recurrence rule + if ($event'recurrence' && preg_match_all('/(A-Z+)=(^;+);?/', $event'recurrence', $m, PREG_SET_ORDER)) { + $event'recurrence' = array(); + foreach ($m as $rr) { + if (is_numeric($rr2)) { + $rr2 = intval($rr2); + } + else if ($rr1 == 'UNTIL') { + $rr2 = date_create($rr2); + } + else if ($rr1 == 'RDATE') { + $rr2 = array_map('date_create', explode(',', $rr2)); + } + else if ($rr1 == 'EXDATE') { + $rr2 = array_map('date_create', explode(',', $rr2)); + } + + $event'recurrence'$rr1 = $rr2; + } + } + + if ($event'recurrence_id') { + libcalendaring::identify_recurrence_instance($event); + } + + if (strlen($event'instance')) { + $event'_instance' = $event'instance'; + + if (empty($event'recurrence_id')) { + $event'recurrence_date' = rcube_utils::anytodatetime($event'_instance', $event'start'->getTimezone()); + } + } + + if ($event'_attachments' > 0) { + $event'attachments' = (array)$this->list_attachments($event); + } + + // decode serialized event attendees + if (strlen($event'attendees')) { + $event'attendees' = $this->unserialize_attendees($event'attendees'); + } + else { + $event'attendees' = array(); + } + + // decode serialized alarms + if ($event'alarms') { + $event'valarms' = $this->unserialize_alarms($event'alarms'); + } + + unset($event'event_id', $event'calendar_id', $event'notifyat', $event'all_day', $event'instance', $event'_attachments'); + + return $event; } - - if ($event'_attachments' > 0) { - $event'attachments' = (array)$this->list_attachments($event); + + /** + * Get a list of pending alarms to be displayed to the user + * + * @see calendar_driver::pending_alarms() + */ + public function pending_alarms($time, $calendars = null) + { + if (empty($calendars)) { + $calendars = array_keys($this->calendars); + } + else if (!is_array($calendars)) { + $calendars = explode(',', (array) $calendars); + } + + // only allow to select from calendars with activated alarms + $calendar_ids = array(); + foreach ($calendars as $cid) { + if ($this->calendars$cid && $this->calendars$cid'showalarms') { + $calendar_ids = $cid; + } + } + + $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids); + $alarms = array(); + + if (!empty($calendar_ids)) { + $stime = $this->rc->db->fromunixtime($time); + $result = $this->rc->db->query( + "SELECT * FROM `{$this->db_events}`" + . " WHERE `calendar_id` IN (" . join(',', $calendar_ids) . ")" + . " AND `notifyat` <= $stime AND `end` > $stime", + ); + + while ($event = $this->rc->db->fetch_assoc($result)) { + $alarms = $this->_read_postprocess($event); + } + } + + return $alarms; } - - // decode serialized event attendees - if (strlen($event'attendees')) { - $event'attendees' = $this->unserialize_attendees($event'attendees'); + + /** + * Feedback after showing/sending an alarm notification + * + * @see calendar_driver::dismiss_alarm() + */ + public function dismiss_alarm($event_id, $snooze = 0) + { + // set new notifyat time or unset if not snoozed + $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null; + + $query = $this->rc->db->query( + "UPDATE `{$this->db_events}`" + . " SET `changed` = " . $this->rc->db->now() . ", `notifyat` = ?" + . " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids})", + $notify_at, + $event_id + ); + + return $this->rc->db->affected_rows($query); } - else { - $event'attendees' = array(); + + /** + * Save an attachment related to the given event + */ + private function add_attachment($attachment, $event_id) + { + if (isset($attachment'data')) { + $data = $attachment'data'; + } + else if (!empty($attachment'path')) { + $data = file_get_contents($attachment'path'); + } + else { + return false; + } + + $query = $this->rc->db->query( + "INSERT INTO `{$this->db_attachments}`" + . " (`event_id`, `filename`, `mimetype`, `size`, `data`)" + . " VALUES (?, ?, ?, ?, ?)", + $event_id, + $attachment'name', + $attachment'mimetype', + strlen($data), + base64_encode($data) + ); + + return $this->rc->db->affected_rows($query); } - - // decode serialized alarms - if ($event'alarms') { - $event'valarms' = $this->unserialize_alarms($event'alarms'); + + /** + * Remove a specific attachment from the given event + */ + private function remove_attachment($attachment_id, $event_id) + { + $query = $this->rc->db->query( + "DELETE FROM `{$this->db_attachments}`" + . " WHERE `attachment_id` = ? AND `event_id` IN (" + . "SELECT `event_id` FROM `{$this->db_events}`" + . " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids}))", + $attachment_id, + $event_id + ); + + return $this->rc->db->affected_rows($query); } - - unset($event'event_id', $event'calendar_id', $event'notifyat', $event'all_day', $event'instance', $event'_attachments'); - return $event; - } - - /** - * Get a list of pending alarms to be displayed to the user - * - * @see calendar_driver::pending_alarms() - */ - public function pending_alarms($time, $calendars = null) - { - if (empty($calendars)) - $calendars = array_keys($this->calendars); - else if (is_string($calendars)) - $calendars = explode(',', $calendars); - - // only allow to select from calendars with activated alarms - $calendar_ids = array(); - foreach ($calendars as $cid) { - if ($this->calendars$cid && $this->calendars$cid'showalarms') - $calendar_ids = $cid; + + /** + * List attachments of specified event + */ + public function list_attachments($event) + { + $attachments = array(); + + if (!empty($this->calendar_ids)) { + $result = $this->rc->db->query( + "SELECT `attachment_id` AS id, `filename` AS name, `mimetype`, `size`" + . " FROM `{$this->db_attachments}`" + . " WHERE `event_id` IN (" + . "SELECT `event_id` FROM `{$this->db_events}`" + . " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids}))" + . " ORDER BY `filename`", + $event'recurrence_id' ? $event'recurrence_id' : $event'event_id' + ); + + while ($arr = $this->rc->db->fetch_assoc($result)) { + $attachments = $arr; + } + } + + return $attachments; } - $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids); - - $alarms = array(); - if (!empty($calendar_ids)) { - $result = $this->rc->db->query(sprintf( - "SELECT * FROM " . $this->db_events . " - WHERE calendar_id IN (%s) - AND notifyat <= %s AND %s > %s", - join(',', $calendar_ids), - $this->rc->db->fromunixtime($time), - $this->rc->db->quote_identifier('end'), - $this->rc->db->fromunixtime($time) - )); - - while ($result && ($event = $this->rc->db->fetch_assoc($result))) - $alarms = $this->_read_postprocess($event); + + /** + * Get attachment properties + */ + public function get_attachment($id, $event) + { + if (!empty($this->calendar_ids)) { + $result = $this->rc->db->query( + "SELECT `attachment_id` AS id, `filename` AS name, `mimetype`, `size` " + . " FROM `{$this->db_attachments}`" + . " WHERE `attachment_id` = ? AND `event_id` IN (" + . "SELECT `event_id` FROM `{$this->db_events}`" + . " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids}))", + $id, + $event'recurrence_id' ? $event'recurrence_id' : $event'id' + ); + + if ($result && ($arr = $this->rc->db->fetch_assoc($result))) { + return $arr; + } + } } - return $alarms; - } - - /** - * Feedback after showing/sending an alarm notification - * - * @see calendar_driver::dismiss_alarm() - */ - public function dismiss_alarm($event_id, $snooze = 0) - { - // set new notifyat time or unset if not snoozed - $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null; - - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_events . " - SET changed=%s, notifyat=? - WHERE event_id=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $this->rc->db->now()), - $notify_at, - $event_id - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * Save an attachment related to the given event - */ - private function add_attachment($attachment, $event_id) - { - $data = $attachment'data' ? $attachment'data' : file_get_contents($attachment'path'); - - $query = $this->rc->db->query( - "INSERT INTO " . $this->db_attachments . - " (event_id, filename, mimetype, size, data)" . - " VALUES (?, ?, ?, ?, ?)", - $event_id, - $attachment'name', - $attachment'mimetype', - strlen($data), - base64_encode($data) - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * Remove a specific attachment from the given event - */ - private function remove_attachment($attachment_id, $event_id) - { - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_attachments . - " WHERE attachment_id = ?" . - " AND event_id IN (SELECT event_id FROM " . $this->db_events . - " WHERE event_id = ?" . - " AND calendar_id IN (" . $this->calendar_ids . "))", - $attachment_id, - $event_id - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * List attachments of specified event - */ - public function list_attachments($event) - { - $attachments = array(); - - if (!empty($this->calendar_ids)) { - $result = $this->rc->db->query( - "SELECT attachment_id AS id, filename AS name, mimetype, size " . - " FROM " . $this->db_attachments . - " WHERE event_id IN (SELECT event_id FROM " . $this->db_events . - " WHERE event_id=?" . - " AND calendar_id IN (" . $this->calendar_ids . "))". - " ORDER BY filename", - $event'recurrence_id' ? $event'recurrence_id' : $event'event_id' - ); - - while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { - $attachments = $arr; - } + /** + * Get attachment body + */ + public function get_attachment_body($id, $event) + { + if (!empty($this->calendar_ids)) { + $result = $this->rc->db->query( + "SELECT `data` FROM `{$this->db_attachments}`" + . " WHERE `attachment_id` = ? AND `event_id` IN (" + . "SELECT `event_id` FROM `{$this->db_events}`" + . " WHERE `event_id` = ? AND `calendar_id` IN ({$this->calendar_ids}))", + $id, + $event'id' + ); + + if ($arr = $this->rc->db->fetch_assoc($result)) { + return base64_decode($arr'data'); + } + } } - return $attachments; - } - - /** - * Get attachment properties - */ - public function get_attachment($id, $event) - { - if (!empty($this->calendar_ids)) { - $result = $this->rc->db->query( - "SELECT attachment_id AS id, filename AS name, mimetype, size " . - " FROM " . $this->db_attachments . - " WHERE attachment_id=?". - " AND event_id=?", - $id, - $event'recurrence_id' ? $event'recurrence_id' : $event'id' - ); - - if ($result && ($arr = $this->rc->db->fetch_assoc($result))) { - return $arr; - } + /** + * Remove the given category + */ + public function remove_category($name) + { + $query = $this->rc->db->query( + "UPDATE `{$this->db_events}` SET `categories` = ''" + . " WHERE `categories` = ? AND `calendar_id` IN ({$this->calendar_ids})", + $name + ); + + return $this->rc->db->affected_rows($query); } - return null; - } - - /** - * Get attachment body - */ - public function get_attachment_body($id, $event) - { - if (!empty($this->calendar_ids)) { - $result = $this->rc->db->query( - "SELECT data " . - " FROM " . $this->db_attachments . - " WHERE attachment_id=?". - " AND event_id=?", - $id, - $event'id' - ); - - if ($result && ($arr = $this->rc->db->fetch_assoc($result))) { - return base64_decode($arr'data'); - } + /** + * Update/replace a category + */ + public function replace_category($oldname, $name, $color) + { + $query = $this->rc->db->query( + "UPDATE `{$this->db_events}` SET `categories` = ?" + . " WHERE `categories` = ? AND `calendar_id` IN ({$this->calendar_ids})", + $name, + $oldname + ); + + return $this->rc->db->affected_rows($query); } - return null; - } - - /** - * Remove the given category - */ - public function remove_category($name) - { - $query = $this->rc->db->query( - "UPDATE " . $this->db_events . " - SET categories='' - WHERE categories=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $name - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * Update/replace a category - */ - public function replace_category($oldname, $name, $color) - { - $query = $this->rc->db->query( - "UPDATE " . $this->db_events . " - SET categories=? - WHERE categories=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $name, - $oldname - ); - - return $this->rc->db->affected_rows($query); - } - - /** - * Helper method to serialize the list of alarms into a string - */ - private function serialize_alarms($valarms) - { - foreach ((array)$valarms as $i => $alarm) { - if ($alarm'trigger' instanceof DateTime) { - $valarms$i'trigger' = '@' . $alarm'trigger'->format('c'); - } - } - - return $valarms ? json_encode($valarms) : null; - } - - /** - * Helper method to decode a serialized list of alarms - */ - private function unserialize_alarms($alarms) - { - // decode json serialized alarms - if ($alarms && $alarms0 == '') { - $valarms = json_decode($alarms, true); - foreach ($valarms as $i => $alarm) { - if ($alarm'trigger'0 == '@') { - try { - $valarms$i'trigger' = new DateTime(substr($alarm'trigger', 1)); - } - catch (Exception $e) { - unset($valarms$i); - } - } - } - } - // convert legacy alarms data - else if (strlen($alarms)) { - list($trigger, $action) = explode(':', $alarms, 2); - if ($trigger = libcalendaring::parse_alarm_value($trigger)) { - $valarms = array(array('action' => $action, 'trigger' => $trigger3 ?: $trigger0)); - } - } - - return $valarms; - } - - /** - * Helper method to decode the attendees list from string - */ - private function unserialize_attendees($s_attendees) - { - $attendees = array(); - - // decode json serialized string - if ($s_attendees0 == '') { - $attendees = json_decode($s_attendees, true); + /** + * Helper method to serialize the list of alarms into a string + */ + private function serialize_alarms($valarms) + { + foreach ((array)$valarms as $i => $alarm) { + if ($alarm'trigger' instanceof DateTime) { + $valarms$i'trigger' = '@' . $alarm'trigger'->format('c'); + } + } + + return $valarms ? json_encode($valarms) : null; } - // decode the old serialization format - else { - foreach (explode("\n", $s_attendees) as $line) { - $att = array(); - foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) { - list($key, $value) = explode("=", $prop); - $attstrtolower($key) = stripslashes(trim($value, '""')); - } - $attendees = $att; - } + + /** + * Helper method to decode a serialized list of alarms + */ + private function unserialize_alarms($alarms) + { + // decode json serialized alarms + if ($alarms && $alarms0 == '') { + $valarms = json_decode($alarms, true); + foreach ($valarms as $i => $alarm) { + if ($alarm'trigger'0 == '@') { + try { + $valarms$i'trigger' = new DateTime(substr($alarm'trigger', 1)); + } + catch (Exception $e) { + unset($valarms$i); + } + } + } + } + // convert legacy alarms data + else if (strlen($alarms)) { + list($trigger, $action) = explode(':', $alarms, 2); + if ($trigger = libcalendaring::parse_alarm_value($trigger)) { + $valarms = array(array('action' => $action, 'trigger' => $trigger3 ?: $trigger0)); + } + } + + return $valarms; } - return $attendees; - } - - /** - * Handler for user_delete plugin hook - */ - public function user_delete($args) - { - $db = $this->rc->db; - $user = $args'user'; - $event_ids = array(); - - $events = $db->query( - "SELECT event_id FROM " . $this->db_events . " AS ev" . - " LEFT JOIN " . $this->db_calendars . " cal ON (ev.calendar_id = cal.calendar_id)". - " WHERE user_id=?", - $user->ID); - - while ($row = $db->fetch_assoc($events)) { - $event_ids = $row'event_id'; - } - - if (!empty($event_ids)) { - foreach (array($this->db_attachments, $this->db_events) as $table) { - $db->query(sprintf("DELETE FROM $table WHERE event_id IN (%s)", join(',', $event_ids))); - } - } - - foreach (array($this->db_calendars, 'itipinvitations') as $table) { - $db->query("DELETE FROM $table WHERE user_id=?", $user->ID); - } - } + /** + * Helper method to decode the attendees list from string + */ + private function unserialize_attendees($s_attendees) + { + $attendees = array(); + // decode json serialized string + if ($s_attendees0 == '') { + $attendees = json_decode($s_attendees, true); + } + // decode the old serialization format + else { + foreach (explode("\n", $s_attendees) as $line) { + $att = array(); + foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) { + list($key, $value) = explode("=", $prop); + $attstrtolower($key) = stripslashes(trim($value, '""')); + } + $attendees = $att; + } + } + + return $attendees; + } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/calendar/drivers/kolab/kolab_calendar.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/calendar/drivers/kolab/kolab_calendar.php
Changed
@@ -841,7 +841,7 @@ $cleanup_fn = function(&$event) { unset($event'_savemode', $event'_fromcalendar', $event'_identity', $event'_folder_id', $event'calendar', $event'className', $event'recurrence_id', - $event'_attachments', $event'attachments', $event'deleted_attachments'); + $event'attachments', $event'deleted_attachments'); }; $cleanup_fn($event);
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_addressbook/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_addressbook/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Kolab addressbook", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.4.5", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli",
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_addressbook/kolab_addressbook.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_addressbook/kolab_addressbook.php
Changed
@@ -406,13 +406,7 @@ public function get_address_book($p) { if ($p'id') { - $id = kolab_storage::id_decode($p'id'); - - // check for falsely base64 decoded identifier - if (preg_match('!^A-Za-z0-9=/+&._ -!', $id)) { - $id = $p'id'; - } - + $id = kolab_storage::id_decode($p'id'); $folder = kolab_storage::get_folder($id); // try with unencoded (old-style) identifier
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_auth/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_auth/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Kolab authentication", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.4.5", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli", @@ -25,6 +25,7 @@ , "require": { "php": ">=5.3.0", - "roundcube/plugin-installer": ">=0.1.3" + "roundcube/plugin-installer": ">=0.1.3", + "kolab/libkolab": ">=3.5.1" } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_auth/kolab_auth.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_auth/kolab_auth.php
Changed
@@ -39,6 +39,7 @@ $rcmail = rcube::get_instance(); $this->load_config(); + $this->require_plugin('libkolab'); $this->add_hook('authenticate', array($this, 'authenticate')); $this->add_hook('startup', array($this, 'startup')); @@ -796,28 +797,12 @@ */ public static function ldap() { - if (self::$ldap) { - return self::$ldap; - } - - $rcmail = rcube::get_instance(); - $addressbook = $rcmail->config->get('kolab_auth_addressbook'); - - if (!is_array($addressbook)) { - $ldap_config = (array)$rcmail->config->get('ldap_public'); - $addressbook = $ldap_config$addressbook; - } + self::$ldap = kolab_storage::ldap('kolab_auth_addressbook'); - if (empty($addressbook)) { - return null; + if (self::$ldap) { + self::$ldap->extend_fieldmap(array('uniqueid' => 'nsuniqueid')); } - $addressbook'fieldmap''uniqueid' = 'nsuniqueid'; - - require_once __DIR__ . '/kolab_auth_ldap.php'; - - self::$ldap = new kolab_auth_ldap($addressbook); - return self::$ldap; } @@ -834,7 +819,7 @@ /** * Parses LDAP DN string with replacing supported variables. - * See kolab_auth_ldap::parse_vars() + * See kolab_ldap::parse_vars() * * @param string $str LDAP DN string *
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_delegation/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_delegation/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Kolab delegation feature", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Aleksander Machniak", @@ -21,7 +21,7 @@ "require": { "php": ">=5.3.0", "roundcube/plugin-installer": ">=0.1.3", - "kolab/libkolab": ">=3.4.0", - "kolab/kolab_auth": ">=3.4.0" + "kolab/libkolab": ">=3.5.1", + "kolab/kolab_auth": ">=3.5.1" } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_delegation/kolab_delegation_engine.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_delegation/kolab_delegation_engine.php
Changed
@@ -79,7 +79,7 @@ // add delegate to the list $list = $dn; - $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); + $list = array_map(array('kolab_ldap', 'dn_decode'), $list); // update user record $result = $this->user_update_delegates($list); @@ -149,7 +149,7 @@ // remove delegate from the list unset($list$dn); $list = array_keys($list); - $list = array_map(array('kolab_auth_ldap', 'dn_decode'), $list); + $list = array_map(array('kolab_ldap', 'dn_decode'), $list); $user$this->ldap_delegate_field = $list; // update user record @@ -181,7 +181,7 @@ } // Get delegate - $user = $ldap->get_record(kolab_auth_ldap::dn_decode($dn)); + $user = $ldap->get_record(kolab_ldap::dn_decode($dn)); if (empty($user)) { return array(); @@ -230,27 +230,9 @@ return $this->ldap; } - if ($addressbook = $this->rc->config->get('kolab_delegation_addressbook')) { - if (!is_array($addressbook)) { - $ldap_config = (array) $this->rc->config->get('ldap_public'); - $addressbook = $ldap_config$addressbook; - } - - if (!empty($addressbook)) { - require_once __DIR__ . '/../kolab_auth/kolab_auth_ldap.php'; - - $ldap = new kolab_auth_ldap($addressbook); - } - } - - // Fallback to kolab_auth plugin's addressbook - if (!$ldap) { - $ldap = kolab_auth::ldap(); - } - - $this->ldap = $ldap; + $this->ldap = kolab_storage::ldap('kolab_delegation_addressbook'); - if (!$ldap || !$ldap->ready) { + if (!$this->ldap || !$this->ldap->ready) { return null; } @@ -270,10 +252,10 @@ // Name of the LDAP field with organization name for identities $this->ldap_org_field = $this->rc->config->get('kolab_delegation_organization_field', $this->rc->config->get('kolab_auth_organization')); - $ldap->set_filter($this->ldap_filter); - $ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field)); + $this->ldap->set_filter($this->ldap_filter); + $this->ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field)); - return $ldap; + return $this->ldap; } /** @@ -540,7 +522,7 @@ } return array( - 'ID' => kolab_auth_ldap::dn_encode($dn), + 'ID' => kolab_ldap::dn_encode($dn), 'uid' => $uid, 'name' => $name, 'realname' => $realname,
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_files/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_files/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "User interface for Kolab File Manager (Chwala)", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Aleksander Machniak",
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_files/kolab_files.js -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_files/kolab_files.js
Changed
@@ -2286,7 +2286,7 @@ rcube_webmail.long_subject_title_ex(this); }); - if (rcmail.env.contextmenu) + if (rcmail.task == 'files' && rcmail.env.contextmenu) list.on('contextmenu', function(e) { var elem = $(e.target).closest('li'); id = rcmail.html_identifier_decode(elem.attr('id').replace(/^rcmli/, ''));
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_files/skins/elastic/templates/files.html -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_files/skins/elastic/templates/files.html
Changed
@@ -36,7 +36,7 @@ <div id="filestoolbar" class="toolbar menu"> <roundcube:button id="fileslistmenu-link" name="fileslistmenu-link" type="link" onclick="kolab_files_listoptions('files')" - label="options" class="settings" innerClass="inner" /> + label="options" class="options" innerClass="inner" /> <roundcube:button id="sessionslistmenu-link" name="sessionslistmenu-link" type="link" onclick="kolab_files_listoptions('sessions')" label="options" class="hidden settings" innerClass="inner" />
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_notes/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_notes/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Notes module for Roundcube connecting to a Kolab server for storage", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli",
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_notes/notes.js -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_notes/notes.js
Changed
@@ -770,9 +770,13 @@ is_html = false; // tag-edit line + var tagline = $('.tagline', titlecontainer); if (window.kolab_tags_input) { - $('.tagline', titlecontainer).parent('.form-group').show(); - taglist = kolab_tags_input($('.tagline', titlecontainer), data.tags, readonly); + tagline.parent('.form-group').show(); + taglist = kolab_tags_input(tagline, data.tags, readonly); + } + else { + tagline.parent('.form-group').hide(); } $('.notetitle', titlecontainer).val(data.title).prop('disabled', readonly).show(); @@ -788,7 +792,7 @@ .addClass('message eml') .append($('<a>') .attr('href', link.mailurl) - .addClass('messagelink') + .addClass('messagelink filename') .text(link.subject || link.uri) ) .appendTo(attachmentslist);
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_notes/skins/elastic/templates/dialogview.html -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_notes/skins/elastic/templates/dialogview.html
Changed
@@ -11,7 +11,7 @@ <roundcube:object name="plugin.editform" id="noteform" class="noteform" /> <roundcube:object name="plugin.detailview" id="notedetails" class="notedetails" /> <div id="notereferences"> - <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" /> + <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist linkslist" /> </div> </div>
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/kolab_notes/skins/elastic/templates/notes.html -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/kolab_notes/skins/elastic/templates/notes.html
Changed
@@ -26,7 +26,7 @@ <a class="button icon back-sidebar-button folders" href="#sidebar"><span class="inner"><roundcube:label name="kolab_notes.notebooks" /></span></a> <span id="aria-label-noteslist" class="header-title"><roundcube:label name="kolab_notes.notes" /></span> <div id="listcontrols" class="toolbar menu" role="toolbar"> - <roundcube:button name="optionsmenu" id="listmenulink" type="link" class="button settings active" + <roundcube:button name="optionsmenu" id="listmenulink" type="link" class="button options active" label="options" innerClass="inner" onclick="return kolab_notes_options_menu()" /> </div> <a class="button icon toolbar-menu-button" href="#list-menu"><span class="inner"><roundcube:label name="menu" /></span></a> @@ -76,7 +76,7 @@ <roundcube:object name="plugin.detailview" id="notedetails" class="notedetails" /> <h3 id="aria-label-messagereferences" class="voice"><roundcube:label name="kolab_notes.arialabelmessagereferences" /></h3> <div id="notereferences" class="notereferences" role="region" aria-labelledby="aria-label-messagereferences"> - <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" /> + <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist linkslist" /> </div> <roundcube:container name="notedetailview" id="notedetailsbox" /> </div>
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libcalendaring/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libcalendaring/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Library providing common functions for calendaring plugins", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli", @@ -26,6 +26,6 @@ "require": { "php": ">=5.4.0", "roundcube/plugin-installer": ">=0.1.3", - "sabre/vobject": "~3.3.3" + "sabre/vobject": "~3.5.3" } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libcalendaring/libcalendaring.js -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libcalendaring/libcalendaring.js
Changed
@@ -49,7 +49,7 @@ this.datepicker_settings = { // translate from fullcalendar (MomentJS) format to datepicker format dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmm/, 'MM').replace(/mmm/, 'M') - .replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/DD/, 'dd') + .replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/DD/, 'dd').replace(/D/, 'd') .replace(/Y/g, 'y').replace(/yyyy/, 'yy'), firstDay : settings.first_day, dayNamesMin: settings.days_short, @@ -1106,7 +1106,7 @@ // Render message reference links to the given container this.render_message_links = function(links, container, edit, plugin) { - var ul = $('<ul>').addClass('attachmentslist'); + var ul = $('<ul>').addClass('attachmentslist linkslist'); $.each(links, function(i, link) { if (!link.mailurl) @@ -1115,7 +1115,7 @@ var li = $('<li>').addClass('link') .addClass('message eml') .append($('<a>') - .attr({href: link.mailurl, 'class': 'messagelink'}) + .attr({href: link.mailurl, 'class': 'messagelink filename'}) .text(link.subject || link.uri) ) .appendTo(ul); @@ -1242,8 +1242,7 @@ '<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' + '</div>' + '<div class="form-section form-group form-check">' + - '<label for="itip-delegate-rsvp">' + rcmail.gettext('itip.delegatersvpme') + '</label>' + - '<input type="checkbox" id="itip-delegate-rsvp" value="1" />' + + '<label><input type="checkbox" id="itip-delegate-rsvp" value="1" />' + rcmail.gettext('itip.delegatersvpme') + '</label>' + '</div>' + '<div class="form-section form-group">' + '<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' +
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libcalendaring/libcalendaring.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libcalendaring/libcalendaring.php
Changed
@@ -49,12 +49,13 @@ 'calendar_first_hour' => 6, 'calendar_date_format_sets' => array( 'Y-m-d' => array('d M Y', 'm-d', 'l m-d'), + 'Y/m/d' => array('d M Y', 'm/d', 'l m/d'), + 'Y.m.d' => array('d M Y', 'm.d', 'l m.d'), 'd-m-Y' => array('d M Y', 'd-m', 'l d-m'), - 'Y/m/d' => array('M d Y', 'm/d', 'l m/d'), - 'm/d/Y' => array('M d Y', 'm/d', 'l m/d'), 'd/m/Y' => array('d M Y', 'd/m', 'l d/m'), - 'd.m.Y' => array('d. M Y', 'd.m.', 'l d.m.'), - 'd.m.Y' => array('d. M Y', 'd.m.', 'l d.m.'), + 'd.m.Y' => array('d M Y', 'd.m', 'l d.m'), + 'j.n.Y' => array('d M Y', 'd.m', 'l d.m'), + 'm/d/Y' => array('M d Y', 'm/d', 'l m/d'), ), );
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libcalendaring/libvcalendar.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libcalendaring/libvcalendar.php
Changed
@@ -1309,12 +1309,15 @@ * @param string Timezone ID as used in PHP's Date functions * @param integer Unix timestamp with first date/time in this timezone * @param integer Unix timestap with last date/time in this timezone + * @param VObject\Component\VCalendar Optional VCalendar component * * @return mixed A Sabre\VObject\Component object representing a VTIMEZONE definition * or false if no timezone information is available */ public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null) { + // TODO: Consider using tzurl.org database for better interoperability e.g. with Outlook + if (!$from) $from = time(); if (!$to) $to = $from; if (!$cal) $cal = new VObject\Component\VCalendar(); @@ -1338,6 +1341,17 @@ $year = 86400 * 360; $transitions = $tz->getTransitions($from - $year, $to + $year); + // Make sure VTIMEZONE contains at least one STANDARD/DAYLIGHT component + // when there's only one transition in specified time period (T5626) + if (count($transitions) == 1) { + // Get more transitions and use OFFSET from the previous to last + $more_transitions = $tz->getTransitions(0, $to + $year); + if (count($more_transitions) > 1) { + $index = count($more_transitions) - 2; + $tzfrom = $more_transitions$index'offset' / 3600; + } + } + $vt = $cal->createComponent('VTIMEZONE'); $vt->TZID = $tz->getName(); @@ -1345,7 +1359,7 @@ foreach ($transitions as $i => $trans) { $cmp = null; - if ($i == 0) { + if (!isset($tzfrom)) { $tzfrom = $trans'offset' / 3600; continue; }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libcalendaring/tests/libvcalendar.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libcalendaring/tests/libvcalendar.php
Changed
@@ -102,14 +102,6 @@ $this->assertFalse(array_key_exists('changed', $event), "No changed date field"); } - function test_invalid_vevent() - { - $this->setExpectedException('\Sabre\VObject\ParseException'); - - $ical = new libvcalendar(); - $events = $ical->import_from_file(__DIR__ . '/resources/invalid-event.ics', 'UTF-8', true); - } - /** * Test some extended ical properties such as attendees, recurrence rules, alarms and attachments */ @@ -589,6 +581,25 @@ $this->assertInstanceOf('\Sabre\VObject\Component', $vtz); $this->assertContains('TZOFFSETFROM:+1245', $vtz->serialize()); $this->assertContains('TZOFFSETTO:+1345', $vtz->serialize()); + + // Making sure VTIMEZOONE contains at least one STANDARD/DAYLIGHT component + // when there's only one transition in specified time period (T5626) + $vtz = libvcalendar::get_vtimezone('Europe/Istanbul', strtotime('2019-10-04T15:00:00')); + + $this->assertInstanceOf('\Sabre\VObject\Component', $vtz); + + $dst = $vtz->select('DAYLIGHT'); + $std = $vtz->select('STANDARD'); + + $this->assertEmpty($dst); + $this->assertCount(1, $std); + + $std = end($std); + $this->assertEquals('STANDARD', $std->name); + $this->assertEquals('20181009T150000', $std->DTSTART); + $this->assertEquals('+0300', $std->TZOFFSETFROM); + $this->assertEquals('+0300', $std->TZOFFSETTO); + $this->assertEquals('+03', $std->TZNAME); } function get_attachment_data($id, $event) @@ -596,4 +607,3 @@ return $this->attachment_data; } } -
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/README -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/README
Changed
@@ -6,6 +6,8 @@ IMAP folders. For reading and writing these objects, the PHP bindings of the libkolabxml library are used. +This plugin also provides elastic/larry skin support for Kolab plugins. + REQUIREMENTS ------------ @@ -26,3 +28,10 @@ ------------- Rename config.inc.php.dist to config.inc.php in the plugin folder. For available configuration options see config.inc.php.dist file. + + +IMPORTANT +--------- + +This plugin doesn't work with the classic skin of Roundcube because no +templates are available for that skin. \ No newline at end of file
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/SQL/mysql.initial.sql -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/SQL/mysql.initial.sql
Changed
@@ -12,7 +12,7 @@ CREATE TABLE `kolab_folders` ( `folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - `resource` VARCHAR(255) NOT NULL, + `resource` VARCHAR(255) BINARY NOT NULL, `type` VARCHAR(32) NOT NULL, `synclock` INT(10) NOT NULL DEFAULT '0', `ctag` VARCHAR(40) DEFAULT NULL, @@ -180,4 +180,4 @@ /*!40014 SET FOREIGN_KEY_CHECKS=1 */; -REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2018122700'); +REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2019092900');
View file
roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/SQL/mysql/2019092900.sql
Added
@@ -0,0 +1,1 @@ +ALTER TABLE `kolab_folders` CHANGE `resource` `resource` varchar(255) BINARY NOT NULL;
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/SQL/oracle.initial.sql -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/SQL/oracle.initial.sql
Changed
@@ -175,4 +175,4 @@ CREATE INDEX "kolab_cache_fb_uid2msguid" ON "kolab_cache_freebusy" ("folder_id", "uid", "msguid"); -INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2018122700'); +INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2019092900');
View file
roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/SQL/oracle/2019092900.sql
Added
@@ -0,0 +1,1 @@ +-- empty \ No newline at end of file
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/SQL/sqlite.initial.sql -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/SQL/sqlite.initial.sql
Changed
@@ -148,4 +148,4 @@ CREATE INDEX ix_freebusy_uid2msguid ON kolab_cache_freebusy(folder_id,uid,msguid); -INSERT INTO system (name, value) VALUES ('libkolab-version', '2018122700'); +INSERT INTO system (name, value) VALUES ('libkolab-version', '2019092900');
View file
roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/SQL/sqlite/2019092900.sql
Added
@@ -0,0 +1,1 @@ +-- empty \ No newline at end of file
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Plugin to setup a basic environment for the interaction with a Kolab server.", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli",
View file
roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/lib/kolab_ldap.php
Added
@@ -0,0 +1,611 @@ +<?php + +/** + * Kolab Authentication and User Base + * + * @author Aleksander Machniak <machniak@kolabsys.com> + * + * Copyright (C) 2011-2019, Kolab Systems AG <contact@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/>. + */ + +/** + * Wrapper class for rcube_ldap_generic + */ +class kolab_ldap extends rcube_ldap_generic +{ + private $conf = array(); + private $fieldmap = array(); + private $rcache; + + + function __construct($p) + { + $rcmail = rcube::get_instance(); + + $this->conf = $p; + $this->conf'kolab_auth_user_displayname' = $rcmail->config->get('kolab_auth_user_displayname', '{name}'); + + $this->fieldmap = $p'fieldmap'; + $this->fieldmap'uid' = 'uid'; + + $p'attributes' = array_values($this->fieldmap); + $p'debug' = (bool) $rcmail->config->get('ldap_debug'); + + if ($cache_type = $rcmail->config->get('ldap_cache', 'db')) { + $cache_ttl = $rcmail->config->get('ldap_cache_ttl', '10m'); + $this->cache = $rcmail->get_cache('LDAP.kolab_cache', $cache_type, $cache_ttl); + } + + // Connect to the server (with bind) + parent::__construct($p); + $this->_connect(); + + $rcmail->add_shutdown_function(array($this, 'close')); + } + + /** + * Establish a connection to the LDAP server + */ + private function _connect() + { + // try to connect + bind for every host configured + // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable + // see http://www.php.net/manual/en/function.ldap-connect.php + foreach ((array)$this->config'hosts' as $host) { + // skip host if connection failed + if (!$this->connect($host)) { + continue; + } + + $bind_pass = $this->config'bind_pass'; + $bind_user = $this->config'bind_user'; + $bind_dn = $this->config'bind_dn'; + $base_dn = $this->config'base_dn'; + $groups_base_dn = $this->config'groups''base_dn' ?: $base_dn; + + // User specific access, generate the proper values to use. + if ($this->config'user_specific') { + $rcube = rcube::get_instance(); + + // No password set, use the session password + if (empty($bind_pass)) { + $bind_pass = $rcube->get_user_password(); + } + + // Get the pieces needed for variable replacement. + if ($fu = ($rcube->get_user_email() ?: $this->config'username')) { + list($u, $d) = explode('@', $fu); + } + else { + $d = $this->config'mail_domain'; + } + + $dc = 'dc=' . strtr($d, array('.' => ',dc=')); // hierarchal domain string + + // resolve $dc through LDAP + if (!empty($this->config'domain_filter') && !empty($this->config'search_bind_dn')) { + $this->bind($this->config'search_bind_dn', $this->config'search_bind_pw'); + $dc = $this->domain_root_dn($d); + } + + $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); + + // Search for the dn to use to authenticate + if ($this->config'search_base_dn' && $this->config'search_filter' + && (strstr($bind_dn, '%dn') || strstr($base_dn, '%dn') || strstr($groups_base_dn, '%dn')) + ) { + $search_attribs = array('uid'); + if ($search_bind_attrib = (array) $this->config'search_bind_attrib') { + foreach ($search_bind_attrib as $r => $attr) { + $search_attribs = $attr; + $replaces$r = ''; + } + } + + $search_bind_dn = strtr($this->config'search_bind_dn', $replaces); + $search_base_dn = strtr($this->config'search_base_dn', $replaces); + $search_filter = strtr($this->config'search_filter', $replaces); + + $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:" . $this->config'search_bind_pw'); + + if ($this->cache && ($dn = $this->cache->get($cache_key))) { + $replaces'%dn' = $dn; + } + else { + $ldap = $this; + if (!empty($search_bind_dn) && !empty($this->config'search_bind_pw')) { + // To protect from "Critical extension is unavailable" error + // we need to use a separate LDAP connection + if (!empty($this->config'vlv')) { + $ldap = new rcube_ldap_generic($this->config); + $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); + if (!$ldap->connect($host)) { + continue; + } + } + + if (!$ldap->bind($search_bind_dn, $this->config'search_bind_pw')) { + continue; // bind failed, try next host + } + } + + $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs); + if ($res) { + $res->rewind(); + $replaces'%dn' = key($res->entries(true)); + + // add more replacements from 'search_bind_attrib' config + if ($search_bind_attrib) { + $res = $res->current(); + foreach ($search_bind_attrib as $r => $attr) { + $replaces$r = $res$attr0; + } + } + } + + if ($ldap != $this) { + $ldap->close(); + } + } + + // DN not found + if (empty($replaces'%dn')) { + if (!empty($this->config'search_dn_default')) + $replaces'%dn' = $this->config'search_dn_default'; + else { + rcube::raise_error(array( + 'code' => 100, 'type' => 'ldap', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "DN not found using LDAP search."), true); + continue; + } + } + + if ($this->cache && !empty($replaces'%dn')) { + $this->cache->set($cache_key, $replaces'%dn'); + } + } + + // Replace the bind_dn and base_dn variables. + $bind_dn = strtr($bind_dn, $replaces); + $base_dn = strtr($base_dn, $replaces); + $groups_base_dn = strtr($groups_base_dn, $replaces); + + // replace placeholders in filter settings + if (!empty($this->config'filter')) { + $this->config'filter' = strtr($this->config'filter', $replaces); + } + + foreach (array('base_dn', 'filter', 'member_filter') as $k) { + if (!empty($this->config'groups'$k)) { + $this->config'groups'$k = strtr($this->config'groups'$k, $replaces); + } + } + + if (empty($bind_user)) { + $bind_user = $u; + } + } + + if (empty($bind_pass)) { + $this->ready = true; + } + else { + if (!empty($this->config'auth_cid')) { + $this->ready = $this->sasl_bind($this->config'auth_cid', $bind_pass, $bind_dn); + } + else if (!empty($bind_dn)) { + $this->ready = $this->bind($bind_dn, $bind_pass); + } + else { + $this->ready = $this->sasl_bind($bind_user, $bind_pass); + } + } + + // connection established, we're done here + if ($this->ready) { + break; + } + + } // end foreach hosts + + if (!is_resource($this->conn)) { + rcube::raise_error(array('code' => 100, 'type' => 'ldap', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not connect to any LDAP server, last tried $host"), true); + + $this->ready = false; + } + + return $this->ready; + } + + /** + * Fetches user data from LDAP addressbook + */ + function get_user_record($user, $host) + { + $rcmail = rcube::get_instance(); + $filter = $rcmail->config->get('kolab_auth_filter'); + $filter = $this->parse_vars($filter, $user, $host); + $base_dn = $this->parse_vars($this->config'base_dn', $user, $host); + $scope = $this->config'scope'; + + // @TODO: print error if filter is empty + + // get record + if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) { + if ($result->count() == 1) { + $entries = $result->entries(true); + $dn = key($entries); + $entry = array_pop($entries); + $entry = $this->field_mapping($dn, $entry); + + return $entry; + } + } + } + + /** + * Fetches user data from LDAP addressbook + */ + function get_user_groups($dn, $user, $host) + { + if (empty($dn) || empty($this->config'groups')) { + return array(); + } + + $base_dn = $this->parse_vars($this->config'groups''base_dn', $user, $host); + $name_attr = $this->config'groups''name_attr' ? $this->config'groups''name_attr' : 'cn'; + $member_attr = $this->get_group_member_attr(); + $filter = "(member=$dn)(uniqueMember=$dn)"; + + if ($member_attr != 'member' && $member_attr != 'uniqueMember') + $filter .= "($member_attr=$dn)"; + $filter = strtr("(|$filter)", array("\\" => "\\\\")); + + $result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr)); + + if (!$result) { + return array(); + } + + $groups = array(); + foreach ($result as $entry) { + $dn = $entry'dn'; + $entry = rcube_ldap_generic::normalize_entry($entry); + + $groups$dn = $entry$name_attr; + } + + return $groups; + } + + /** + * Get a specific LDAP record + * + * @param string DN + * + * @return array Record data + */ + function get_record($dn) + { + if (!$this->ready) { + return; + } + + if ($rec = $this->get_entry($dn, $this->attributes)) { + $rec = rcube_ldap_generic::normalize_entry($rec); + $rec = $this->field_mapping($dn, $rec); + } + + return $rec; + } + + /** + * Replace LDAP record data items + * + * @param string $dn DN + * @param array $entry LDAP entry + * + * return bool True on success, False on failure + */ + function replace($dn, $entry) + { + // fields mapping + foreach ($this->fieldmap as $field => $attr) { + if (array_key_exists($field, $entry)) { + $entry$attr = $entry$field; + if ($attr != $field) { + unset($entry$field); + } + } + } + + return $this->mod_replace($dn, $entry); + } + + /** + * Search records (simplified version of rcube_ldap::search) + * + * @param mixed $fields The field name or array of field names to search in + * @param string $value Search value + * @param int $mode Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * @param array $required List of fields that cannot be empty + * @param int $limit Number of records + * @param int $count Returns the number of records found + * + * @return array List of LDAP records found + */ + function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0) + { + if (empty($fields)) { + return array(); + } + + $mode = intval($mode); + + // try to resolve field names into ldap attributes + $fieldmap = $this->fieldmap; + $attrs = array_map(function($f) use ($fieldmap) { + return array_key_exists($f, $fieldmap) ? $fieldmap$f : $f; + }, (array)$fields); + + // compose a full-text-search-like filter + if (count($attrs) > 1 || $mode != 1) { + $filter = self::fulltext_search_filter($value, $attrs, $mode); + } + // direct search + else { + $field = $attrs0; + $filter = "($field=" . self::quote_string($value) . ")"; + } + + // add required (non empty) fields filter + $req_filter = ''; + + foreach ((array)$required as $field) { + $attr = array_key_exists($field, $this->fieldmap) ? $this->fieldmap$field : $field; + + // only add if required field is not already in search filter + if (!in_array($attr, $attrs)) { + $req_filter .= "($attr=*)"; + } + } + + if (!empty($req_filter)) { + $filter = '(&' . $req_filter . $filter . ')'; + } + + // avoid double-wildcard if $value is empty + $filter = preg_replace('/\*+/', '*', $filter); + + // add general filter to query + if (!empty($this->config'filter')) { + $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config'filter') . ')' . $filter . ')'; + } + + $base_dn = $this->parse_vars($this->config'base_dn'); + $scope = $this->config'scope'; + $attrs = array_values($this->fieldmap); + $list = array(); + + if ($result = $this->search($base_dn, $filter, $scope, $attrs)) { + $count = $result->count(); + $i = 0; + foreach ($result as $entry) { + if ($limit && $limit <= $i) { + break; + } + + $dn = $entry'dn'; + $entry = rcube_ldap_generic::normalize_entry($entry); + $list$dn = $this->field_mapping($dn, $entry); + $i++; + } + } + + return $list; + } + + /** + * Set filter used in search() + */ + function set_filter($filter) + { + $this->config'filter' = $filter; + } + + /** + * Maps LDAP attributes to defined fields + */ + protected function field_mapping($dn, $entry) + { + $entry'dn' = $dn; + + // fields mapping + foreach ($this->fieldmap as $field => $attr) { + // $entry might be indexed by lower-case attribute names + $attr_lc = strtolower($attr); + if (isset($entry$attr_lc)) { + $entry$field = $entry$attr_lc; + } + else if (isset($entry$attr)) { + $entry$field = $entry$attr; + } + } + + // compose display name according to config + if (empty($this->fieldmap'displayname')) { + $entry'displayname' = rcube_addressbook::compose_search_name( + $entry, + $entry'email', + $entry'name', + $this->conf'kolab_auth_user_displayname' + ); + } + + return $entry; + } + + /** + * Detects group member attribute name + */ + private function get_group_member_attr($object_classes = array()) + { + if (empty($object_classes)) { + $object_classes = $this->config'groups''object_classes'; + } + if (!empty($object_classes)) { + foreach ((array)$object_classes as $oc) { + switch (strtolower($oc)) { + case 'group': + case 'groupofnames': + case 'kolabgroupofnames': + $member_attr = 'member'; + break; + + case 'groupofuniquenames': + case 'kolabgroupofuniquenames': + $member_attr = 'uniqueMember'; + break; + } + } + } + + if (!empty($member_attr)) { + return $member_attr; + } + + if (!empty($this->config'groups''member_attr')) { + return $this->config'groups''member_attr'; + } + + return 'member'; + } + + /** + * Prepares filter query for LDAP search + */ + function parse_vars($str, $user = null, $host = null) + { + // When authenticating user $user is always set + // if not set it means we use this LDAP object for other + // purposes, e.g. kolab_delegation, then username with + // correct domain is in a session + if (!$user) { + $user = $_SESSION'username'; + } + + if (isset($this->icache$user)) { + list($user, $dc) = $this->icache$user; + } + else { + $orig_user = $user; + $rcmail = rcube::get_instance(); + + // get default domain + if ($username_domain = $rcmail->config->get('username_domain')) { + if ($host && is_array($username_domain) && isset($username_domain$host)) { + $domain = rcube_utils::parse_host($username_domain$host, $host); + } + else if (is_string($username_domain)) { + $domain = rcube_utils::parse_host($username_domain, $host); + } + } + + // realmed username (with domain) + if (strpos($user, '@')) { + list($usr, $dom) = explode('@', $user); + + // unrealm domain, user login can contain a domain alias + if ($dom != $domain && ($dc = $this->domain_root_dn($dom))) { + // @FIXME: we should replace domain in $user, I suppose + } + } + else if ($domain) { + $user .= '@' . $domain; + } + + $this->icache$orig_user = array($user, $dc); + } + + // replace variables in filter + list($u, $d) = explode('@', $user); + + // hierarchal domain string + if (empty($dc)) { + $dc = 'dc=' . strtr($d, array('.' => ',dc=')); + } + + $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u); + + $this->parse_replaces = $replaces; + + return strtr($str, $replaces); + } + + /** + * Returns variables used for replacement in (last) parse_vars() call + * + * @return array Variable-value hash array + */ + public function get_parse_vars() + { + return $this->parse_replaces; + } + + /** + * Register additional fields + */ + public function extend_fieldmap($map) + { + foreach ((array)$map as $name => $attr) { + if (!in_array($attr, $this->attributes)) { + $this->attributes = $attr; + $this->fieldmap$name = $attr; + } + } + } + + /** + * HTML-safe DN string encoding + * + * @param string $str DN string + * + * @return string Encoded HTML identifier string + */ + static function dn_encode($str) + { + return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); + } + + /** + * Decodes DN string encoded with _dn_encode() + * + * @param string $str Encoded HTML identifier string + * + * @return string DN string + */ + static function dn_decode($str) + { + $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT); + return base64_decode($str); + } +}
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/lib/kolab_storage.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/lib/kolab_storage.php
Changed
@@ -48,10 +48,10 @@ private static $subscriptions; private static $ldapcache = array(); private static $typedata = array(); + private static $ldap = array(); private static $states; private static $config; private static $imap; - private static $ldap; // Default folder names @@ -96,7 +96,7 @@ 'message' => "required kolabformat module not found" ), true); } - else { + else if (self::$imap->get_error_code()) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "IMAP error" ), true); @@ -116,16 +116,23 @@ /** * Initializes LDAP object to resolve Kolab users + * + * @param string $name Name of the configuration option with LDAP config */ - public static function ldap() + public static function ldap($name = 'kolab_users_directory') { - if (self::$ldap) { - return self::$ldap; - } - self::setup(); - $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook')); + $config = self::$config->get($name); + + if (empty($config)) { + $name = 'kolab_auth_addressbook'; + $config = self::$config->get($name); + } + + if (self::$ldap$name) { + return self::$ldap$name; + } if (!is_array($config)) { $ldap_config = (array)self::$config->get('ldap_public'); @@ -136,17 +143,21 @@ return null; } + $ldap = new kolab_ldap($config); + // overwrite filter option if ($filter = self::$config->get('kolab_users_filter')) { self::$config->set('kolab_auth_filter', $filter); } - // re-use the LDAP wrapper class from kolab_auth plugin - require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php'; + $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); + + //$ldap->set_filter($this->ldap_filter); + $ldap->extend_fieldmap(array($user_attrib => $user_attrib)); - self::$ldap = new kolab_auth_ldap($config); + self::$ldap$name = $ldap; - return self::$ldap; + return $ldap; } /** @@ -1485,6 +1496,7 @@ } /** + * Search users in Kolab LDAP storage * * @param mixed $query Search value (or array of field => value pairs) * @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*) @@ -1492,19 +1504,23 @@ * @param int $limit Maximum number of records * @param int $count Returns the number of records found * - * @return array List or false on error + * @return array List of users */ public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0) { $query = str_replace('*', '', $query); // requires a working LDAP setup - if (!self::ldap() || strlen($query) == 0) { + if (!strlen($query) || !($ldap = self::ldap())) { return array(); } + $root = self::namespace_root('other'); + $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); + $search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')); + // search users using the configured attributes - $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count); + $results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count); // exclude myself if ($_SESSION'kolab_dn') { @@ -1512,9 +1528,6 @@ } // resolve to IMAP folder name - $root = self::namespace_root('other'); - $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); - array_walk($results, function(&$user, $dn) use ($root, $user_attrib) { list($localpart, ) = explode('@', $user$user_attrib); $user'kolabtargetfolder' = $root . $localpart; @@ -1652,7 +1665,6 @@ /** * Get user attributes for specified other user (imap) folder identifier. - * Note: This uses LDAP config/code from kolab_auth. * * @param string $folder_id Folder name w/o path (imap user identifier) * @param bool $as_string Return configured display name attribute value @@ -1692,7 +1704,7 @@ $user = $cache->get($token); } - if (empty($user) && ($ldap = kolab_storage::ldap())) { + if (empty($user) && ($ldap = self::ldap())) { $user = $ldap->get_user_record($token, $_SESSION'imap_host'); if (!empty($user)) {
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/lib/kolab_storage_cache_file.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/lib/kolab_storage_cache_file.php
Changed
@@ -61,15 +61,12 @@ { $object = parent::_unserialize($sql_arr); - if ($object && $sql_arr'fast-mode') { - if (!empty($object'_attachments')) { - $file = $object'_attachments'key($object'_attachments'); + if ($object && !empty($object'_attachments')) { + $file = $object'_attachments'key($object'_attachments'); - $object'type' = $file'mimetype'; - $object'size' = $file'size'; - $object'fileid' = $file'id'; - $object'filename' = $file'name'; - } + $object'type' = $file'mimetype'; + $object'size' = $file'size'; + $object'fileid' = $file'id'; } return $object;
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/lib/kolab_storage_config.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/lib/kolab_storage_config.php
Changed
@@ -236,7 +236,7 @@ // on success, update cached tags list if ($status && is_array($this->tags)) { foreach ($this->tags as $idx => $tag) { - if ($tag'uid' == $uid) { + if ($tag'uid' == $object'uid') { unset($this->tags$idx); break; }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/lib/kolab_storage_folder.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/lib/kolab_storage_folder.php
Changed
@@ -594,7 +594,7 @@ */ public function save(&$object, $type = null, $uid = null) { - if (!$this->valid) { + if (!$this->valid && empty($object)) { return false; }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/skins/elastic/include/calendar.less -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/skins/elastic/include/calendar.less
Changed
@@ -49,9 +49,19 @@ .listing { li { & > div { - &.calendar .calname:before { - &:extend(.font-icon-class); - content: @fa-var-calendar-alt; + &.calendar { + .calname:before { + &:extend(.font-icon-class); + content: @fa-var-calendar-alt; + } + + .actions { + padding-right: .25rem; + + :last-child { + margin-right: 0; + } + } } &.calendar.cal---invitation--pending .calname:before, @@ -68,10 +78,6 @@ &.calendar.cal-__bdays__ { font-style: italic; - a.quickview { - padding-right: .25rem; - } - a.calname { padding-right: 2.5em; }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/skins/elastic/include/libcalendaring.less -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/skins/elastic/include/libcalendaring.less
Changed
@@ -361,21 +361,6 @@ } } -.attachmentslist li.link { - &.message.eml:before { - content: @fa-var-envelope; - } - - a.messagelink { - .overflow-ellipsis; - margin-right: .25rem; - } - - a.delete .inner { - display: none; - } -} - .attachmentslist li.ics:before, .attachmentslist li.text.calendar:before, .messagelist tbody .attachment span.ical:before {
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/skins/elastic/libkolab.less -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/skins/elastic/libkolab.less
Changed
@@ -89,7 +89,7 @@ } html.touch & { - margin-left: .2rem; + padding-left: .2rem; a { margin-right: .5rem; } @@ -143,7 +143,7 @@ } } - &.selected > div { + &.selected > div > * { color: @color-list-selected; background-color: @color-list-selected-background; } @@ -416,6 +416,28 @@ color: @color-black-shade-text; } +.linkslist { + // Reset .attachmentslist style + border: 0 !important; + background: transparent !important; + + li.link { + padding: 0; + + &.message.eml:before { + content: @fa-var-envelope; + } + + a.messagelink { + .overflow-ellipsis; + } + + a.delete .inner { + display: none; + } + } +} + .print-config { position: fixed; top: 0;
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/tests/kolab_date_recurrence.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/tests/kolab_date_recurrence.php
Changed
@@ -36,6 +36,10 @@ */ function test_first_occurrence($recurrence_data, $start, $expected) { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $start = new DateTime($start); if (!empty($recurrence_data'UNTIL')) { $recurrence_data'UNTIL' = new DateTime($recurrence_data'UNTIL'); @@ -216,6 +220,10 @@ */ function test_first_occurrence_allday($recurrence_data, $start, $expected) { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $start = new DateTime($start); if (!empty($recurrence_data'UNTIL')) { $recurrence_data'UNTIL' = new DateTime($recurrence_data'UNTIL'); @@ -236,6 +244,10 @@ */ function test_next_instance() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + date_default_timezone_set('America/New_York'); $start = new DateTime('2017-08-31 11:00:00', new DateTimeZone('Europe/Berlin'));
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/tests/kolab_storage_config.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/tests/kolab_storage_config.php
Changed
@@ -34,6 +34,10 @@ $rcube = rcmail::get_instance(); $rcube->plugins->load_plugin('libkolab', true, true); + if (!kolab_format::supports(3)) { + return; + } + if ($rcube->config->get('tests_username')) { $authenticated = $rcube->login( $rcube->config->get('tests_username'), @@ -70,6 +74,10 @@ function test_001_build_member_url() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $rcube = rcube::get_instance(); $email = $rcube->get_user_email(); $personal = str_replace('$user', urlencode($email), $this->url_personal); @@ -89,6 +97,10 @@ function test_002_parse_member_url() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $rcube = rcube::get_instance(); $email = $rcube->get_user_email(); $personal = str_replace('$user', urlencode($email), $this->url_personal); @@ -113,6 +125,10 @@ function test_003_build_parse_member_url() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + // personal namespace $params = $this->params_personal; $params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params)); @@ -138,6 +154,10 @@ */ function test_save() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $config = kolab_storage_config::get_instance(); $tags = array( array( @@ -171,6 +191,10 @@ */ function test_T133() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $config = kolab_storage_config::get_instance(); // get tags @@ -206,5 +230,9 @@ // get tags again, make sure it contains the new tag $tags = $config->get_tags(); $this->assertCount(4, $tags); + + foreach ($tags as $_tag) { + $this->assertTrue($_tag'uid' != $tag'uid'); + } } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/libkolab/tests/kolab_storage_folder.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/libkolab/tests/kolab_storage_folder.php
Changed
@@ -29,6 +29,10 @@ $rcmail = rcmail::get_instance(); $rcmail->plugins->load_plugin('libkolab', true, true); + if (!kolab_format::supports(3)) { + return; + } + if ($rcmail->config->get('tests_username')) { $authenticated = $rcmail->login( $rcmail->config->get('tests_username'), @@ -65,6 +69,10 @@ function test_001_folder_type_check() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $folder = new kolab_storage_folder('Calendar', 'event', 'event.default'); $this->assertTrue($folder->valid); $this->assertEquals($folder->get_error(), 0); @@ -80,6 +88,10 @@ function test_002_get_owner() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $rcmail = rcmail::get_instance(); $folder = new kolab_storage_folder('Calendar', 'event', 'event'); $this->assertEquals($folder->get_owner(), $rcmail->config->get('tests_username')); @@ -97,6 +109,10 @@ function test_003_get_resource_uri() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $rcmail = rcmail::get_instance(); $foldername = 'Calendar'; $uri = parse_url($rcmail->config->get('default_host')); @@ -112,6 +128,10 @@ function test_004_get_uid() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $rcmail = rcmail::get_instance(); $folder = new kolab_storage_folder('Doesnt-Exist', 'event', 'event'); @@ -123,6 +143,10 @@ function test_005_subscribe() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $folder = new kolab_storage_folder('Contacts', 'contact'); $this->assertTrue($folder->subscribe(true)); $this->assertTrue($folder->is_subscribed()); @@ -135,6 +159,10 @@ function test_006_activate() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $folder = new kolab_storage_folder('Calendar', 'event'); $this->assertTrue($folder->activate(true)); $this->assertTrue($folder->is_active()); @@ -145,16 +173,25 @@ function test_010_write_contacts() { - $contacts = array( + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + + $folder = new kolab_storage_folder('Contacts', 'contact'); + + $saved = $folder->save(null, 'contact'); + $this->assertFalse($saved); + + $contact = array( 'name' => 'FN', 'surname' => 'Last', 'firstname' => 'First', 'email' => array( array('type' => 'home', 'address' => 'first.last@example.org'), ), + 'organization' => 'Company A.G.' ); - $folder = new kolab_storage_folder('Contacts', 'contact'); $saved = $folder->save($contact, 'contact'); $this->assertTrue((bool)$saved); } @@ -164,12 +201,20 @@ */ function test_011_list_contacts() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $folder = new kolab_storage_folder('Contacts', 'contact'); $this->assertEquals($folder->count(), 1); } function test_T491_get_uid() { + if (!kolab_format::supports(3)) { + $this->markTestSkipped('No Kolab support'); + } + $rcmail = rcmail::get_instance(); $imap = $rcmail->get_storage(); $db = $rcmail->get_dbh();
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/README -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/README
Changed
@@ -4,17 +4,60 @@ This plugin currently supports a local database as well as a Kolab groupware server as backends for tasklists and todo items storage. + +REQUIREMENTS +------------ + +Some functions are shared with other plugins and therefore being moved to +library plugins. Thus in order to run the tasklist plugin, you also need the +following plugins installed: + +* libcalendaring 1 +* libkolab 1 + + INSTALLATION ------------ -When usigng the database driver, some initialization of the local database is -necessary. To do so, execute the SQL commands in -drivers/database/SQL/<yourdatabase>.initial.sql +For a manual installation of the plugin (and its dependencies), +execute the following steps. This will set it up with the database backend +driver. + +1. Get the source from git + + $ cd /tmp + $ git clone https://git.kolab.org/diffusion/RPK/roundcubemail-plugins-kolab.git + $ cd /<path-to-roundcube>/plugins + $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/tasklist . + $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libcalendaring . + $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libkolab . + +2. Create tasklist plugin configuration + + $ cd tasklist/ + $ cp config.inc.php.dist config.inc.php + $ edit config.inc.php + +3. Initialize the tasklist database tables + + $ cd ../../ + $ bin/initdb.sh --dir=plugins/tasklist/drivers/database/SQL + +4. Build css styles for the Elastic skin + + $ lessc --relative-urls -x plugins/libkolab/skins/elastic/libkolab.less > plugins/libkolab/skins/elastic/libkolab.min.css + +5. Enable the tasklist plugin + + $ edit config/config.inc.php + +Add 'tasklist' to the list of active plugins: + + $config'plugins' = array( + (...) + 'tasklist', + ); -For some general calendar-based operations such as alarms handling, this -plugin requires the `libcalendaring` plugin, as well as libkolab plugin -for more skins support, which are also part of the Kolab Roundcube Plugins -repository. Make sure these plugins are installed and configured correctly. IMPORTANT ---------
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/composer.json -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/composer.json
Changed
@@ -4,7 +4,7 @@ "description": "Task management plugin", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.0", + "version": "3.5.1", "authors": { "name": "Thomas Bruederli", @@ -28,5 +28,11 @@ "roundcube/plugin-installer": ">=0.1.3", "kolab/libcalendaring": ">=3.4.0", "kolab/libkolab": ">=3.4.0" + }, + "extra": { + "roundcube": { + "min-version": "1.4.0", + "sql-dir": "drivers/database/SQL" + } } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/drivers/database/tasklist_database_driver.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/drivers/database/tasklist_database_driver.php
Changed
@@ -24,18 +24,17 @@ class tasklist_database_driver extends tasklist_driver { - const IS_COMPLETE_SQL = "(status='COMPLETED' OR (complete=1 AND status=''))"; + const IS_COMPLETE_SQL = "(`status` = 'COMPLETED' OR (`complete` = 1 AND `status` = ''))"; - public $undelete = true; // yes, we can - public $sortable = false; + public $undelete = true; // yes, we can + public $sortable = false; public $alarm_types = array('DISPLAY'); private $rc; private $plugin; - private $lists = array(); + private $lists = array(); + private $tags = array(); private $list_ids = ''; - private $tags = array(); - private $db_tasks = 'tasks'; private $db_lists = 'tasklists'; @@ -45,7 +44,7 @@ */ public function __construct($plugin) { - $this->rc = $plugin->rc; + $this->rc = $plugin->rc; $this->plugin = $plugin; // read database config @@ -61,29 +60,31 @@ */ private function _read_lists() { - $hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', ''))); + $hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', ''))); + + if (!empty($this->rc->user->ID)) { + $list_ids = array(); + $result = $this->rc->db->query( + "SELECT *, `tasklist_id` AS id FROM " . $this->db_lists + . " WHERE `user_id` = ?" + . " ORDER BY CASE WHEN `name` = 'INBOX' THEN 0 ELSE 1 END, `name`", + $this->rc->user->ID + ); - if (!empty($this->rc->user->ID)) { - $list_ids = array(); - $result = $this->rc->db->query( - "SELECT *, tasklist_id AS id FROM " . $this->db_lists . " - WHERE user_id=? - ORDER BY CASE WHEN name='INBOX' THEN 0 ELSE 1 END, name", - $this->rc->user->ID - ); + while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { + $arr'showalarms' = intval($arr'showalarms'); + $arr'active' = !in_array($arr'id', $hidden); + $arr'name' = html::quote($arr'name'); + $arr'listname' = html::quote($arr'name'); + $arr'editable' = true; + $arr'rights' = 'lrswikxtea'; - while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { - $arr'showalarms' = intval($arr'showalarms'); - $arr'active' = !in_array($arr'id', $hidden); - $arr'name' = html::quote($arr'name'); - $arr'listname' = html::quote($arr'name'); - $arr'editable' = true; - $arr'rights' = 'lrswikxtea'; - $this->lists$arr'id' = $arr; - $list_ids = $this->rc->db->quote($arr'id'); - } - $this->list_ids = join(',', $list_ids); - } + $this->lists$arr'id' = $arr; + $list_ids = $this->rc->db->quote($arr'id'); + } + + $this->list_ids = join(',', $list_ids); + } } /** @@ -91,14 +92,15 @@ */ public function get_lists($filter = 0) { - // attempt to create a default list for this user - if (empty($this->lists)) { - $prop = array('name' => 'Default', 'color' => '000000'); - if ($this->create_list($prop)) - $this->_read_lists(); - } - - return $this->lists; + // attempt to create a default list for this user + if (empty($this->lists)) { + $prop = array('name' => 'Default', 'color' => '000000'); + if ($this->create_list($prop)) { + $this->_read_lists(); + } + } + + return $this->lists; } /** @@ -111,17 +113,19 @@ public function create_list(&$prop) { $result = $this->rc->db->query( - "INSERT INTO " . $this->db_lists . " - (user_id, name, color, showalarms) - VALUES (?, ?, ?, ?)", + "INSERT INTO " . $this->db_lists + . " (`user_id`, `name`, `color`, `showalarms`)" + . " VALUES (?, ?, ?, ?)", $this->rc->user->ID, strval($prop'name'), strval($prop'color'), - $prop'showalarms'?1:0 + $prop'showalarms' ? 1 : 0 ); - if ($result) + if ($result) { + $prop'rights' = 'lrswikxtea'; return $this->rc->db->insert_id($this->db_lists); + } return false; } @@ -136,13 +140,11 @@ public function edit_list(&$prop) { $query = $this->rc->db->query( - "UPDATE " . $this->db_lists . " - SET name=?, color=?, showalarms=? - WHERE tasklist_id=? - AND user_id=?", - $prop'name', - $prop'color', - $prop'showalarms'?1:0, + "UPDATE " . $this->db_lists . " SET `name` = ?, `color` = ?, `showalarms` = ?" + . " WHERE `tasklist_id` = ? AND `user_id` = ?", + strval($prop'name'), + strval($prop'color'), + $prop'showalarms' ? 1 : 0, $prop'id', $this->rc->user->ID ); @@ -161,10 +163,12 @@ { $hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', ''))); - if ($prop'active') + if ($prop'active') { unset($hidden$prop'id'); - else + } + else { $hidden$prop'id' = 1; + } return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden)))); } @@ -181,18 +185,8 @@ $list_id = $prop'id'; if ($this->lists$list_id) { - // delete all tasks linked with this list - $this->rc->db->query( - "DELETE FROM " . $this->db_tasks . " - WHERE tasklist_id=?", - $list_id - ); - - // delete list record $query = $this->rc->db->query( - "DELETE FROM " . $this->db_lists . " - WHERE tasklist_id=? - AND user_id=?", + "DELETE FROM " . $this->db_lists . " WHERE `tasklist_id` = ? AND `user_id` = ?", $list_id, $this->rc->user->ID ); @@ -208,6 +202,7 @@ * * @param string Search string * @param string Section/source to search + * * @return array List of tasklists */ public function search_lists($query, $source) @@ -234,23 +229,24 @@ */ function count_tasks($lists = null) { - if (empty($lists)) + if (empty($lists)) { $lists = array_keys($this->lists); - else if (is_string($lists)) - $lists = explode(',', $lists); + } + else if (!is_array($lists)) { + $lists = explode(',', (string) $lists); + } // only allow to select from lists of this user $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists))); - $today_date = new DateTime('now', $this->plugin->timezone); - $today = $today_date->format('Y-m-d'); + $today_date = new DateTime('now', $this->plugin->timezone); + $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); - $tomorrow = $tomorrow_date->format('Y-m-d'); + $tomorrow = $tomorrow_date->format('Y-m-d'); $result = $this->rc->db->query(sprintf( - "SELECT task_id, flagged, date FROM " . $this->db_tasks . " - WHERE tasklist_id IN (%s) - AND del=0 AND NOT " . self::IS_COMPLETE_SQL, + "SELECT `task_id`, `flagged`, `date` FROM " . $this->db_tasks + . " WHERE `tasklist_id` IN (%s) AND `del` = 0 AND NOT " . self::IS_COMPLETE_SQL, join(',', $list_ids) )); @@ -277,30 +273,35 @@ * * @param array Hash array wiht filter criterias * @param array List of lists to get tasks from + * * @return array List of tasks records matchin the criteria * @see tasklist_driver::list_tasks() */ function list_tasks($filter, $lists = null) { - if (empty($lists)) + if (empty($lists)) { $lists = array_keys($this->lists); - else if (is_string($lists)) - $lists = explode(',', $lists); + } + else if (!is_array($lists)) { + $lists = explode(',', (string) $lists); + } // only allow to select from lists of this user $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists))); - $sql_add = ''; + $sql_add = ''; // add filter criteria if ($filter'from' || ($filter'mask' & tasklist::FILTER_MASK_TODAY)) { - $sql_add .= ' AND (date IS NULL OR date >= ?)'; + $sql_add .= " AND (`date` IS NULL OR `date` >= ?)"; $datefrom = $filter'from'; } if ($filter'to') { - if ($filter'mask' & tasklist::FILTER_MASK_OVERDUE) - $sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter'to') . ')'; - else - $sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter'to') . ')'; + if ($filter'mask' & tasklist::FILTER_MASK_OVERDUE) { + $sql_add .= " AND (`date` IS NOT NULL AND `date` <= " . $this->rc->db->quote($filter'to') . ")"; + } + else { + $sql_add .= " AND (`date` IS NULL OR `date` <= " . $this->rc->db->quote($filter'to') . ")"; + } } // special case 'today': also show all events with date before today @@ -308,47 +309,48 @@ $datefrom = date('Y-m-d', 0); } - if ($filter'mask' & tasklist::FILTER_MASK_NODATE) - $sql_add = ' AND date IS NULL'; + if ($filter'mask' & tasklist::FILTER_MASK_NODATE) { + $sql_add = " AND `date` IS NULL"; + } - if ($filter'mask' & tasklist::FILTER_MASK_COMPLETE) - $sql_add .= ' AND ' . self::IS_COMPLETE_SQL; - else if (empty($filter'since')) // don't show complete tasks by default - $sql_add .= ' AND NOT ' . self::IS_COMPLETE_SQL; + if ($filter'mask' & tasklist::FILTER_MASK_COMPLETE) { + $sql_add .= " AND " . self::IS_COMPLETE_SQL; + } + else if (empty($filter'since')) { + // don't show complete tasks by default + $sql_add .= " AND NOT " . self::IS_COMPLETE_SQL; + } - if ($filter'mask' & tasklist::FILTER_MASK_FLAGGED) - $sql_add .= ' AND flagged=1'; + if ($filter'mask' & tasklist::FILTER_MASK_FLAGGED) { + $sql_add .= " AND `flagged` = 1"; + } // compose (slow) SQL query for searching // FIXME: improve searching using a dedicated col and normalized values if ($filter'search') { $sql_query = array(); - foreach (array('title','description','organizer','attendees') as $col) - $sql_query = $this->rc->db->ilike($col, '%'.$filter'search'.'%'); - $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; + foreach (array('title', 'description', 'organizer', 'attendees') as $col) { + $sql_query = $this->rc->db->ilike($col, '%' . $filter'search' . '%'); + } + $sql_add = "AND (" . join(" OR ", $sql_query) . ")"; } if ($filter'since' && is_numeric($filter'since')) { - $sql_add .= ' AND changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $filter'since')); + $sql_add .= " AND `changed` >= " . $this->rc->db->quote(date('Y-m-d H:i:s', $filter'since')); } if ($filter'uid') { - $sql_add .= ' AND `uid` IN (' . implode(',', array_map(array($this->rc->db, 'quote'), $filter'uid')) . ')'; + $sql_add .= " AND `uid` IN (" . implode(',', array_map(array($this->rc->db, 'quote'), $filter'uid')) . ")"; } $tasks = array(); if (!empty($list_ids)) { - $result = $this->rc->db->query(sprintf( - "SELECT * FROM " . $this->db_tasks . " - WHERE tasklist_id IN (%s) - AND del=0 - %s - ORDER BY parent_id, task_id ASC", - join(',', $list_ids), - $sql_add - ), + $result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks + . " WHERE `tasklist_id` IN (" . join(',', $list_ids) . ")" + . " AND `del` = 0" . $sql_add + . " ORDER BY `parent_id`, `task_id` ASC", $datefrom - ); + ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $tasks = $this->_read_postprocess($rec); @@ -369,20 +371,16 @@ */ public function get_task($prop, $filter = 0) { - if (is_string($prop)) + if (is_string($prop)) { $prop'uid' = $prop; + } $query_col = $prop'id' ? 'task_id' : 'uid'; - $result = $this->rc->db->query(sprintf( - "SELECT * FROM " . $this->db_tasks . " - WHERE tasklist_id IN (%s) - AND %s=? - AND del=0", - $this->list_ids, - $query_col - ), - $prop'id' ? $prop'id' : $prop'uid' + $result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks + . " WHERE `tasklist_id` IN (" . $this->list_ids . ")" + . " AND `$query_col` = ? AND `del` = 0", + $prop'id' ? $prop'id' : $prop'uid' ); if ($result && ($rec = $this->rc->db->fetch_assoc($result))) { @@ -395,46 +393,46 @@ /** * Get all decendents of the given task record * - * @param mixed Hash array with task properties or task UID + * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched + * * @return array List of all child task IDs */ public function get_childs($prop, $recursive = false) { // resolve UID first if (is_string($prop)) { - $result = $this->rc->db->query(sprintf( - "SELECT task_id AS id, tasklist_id AS list FROM " . $this->db_tasks . " - WHERE tasklist_id IN (%s) - AND uid=?", - $this->list_ids - ), - $prop); + $result = $this->rc->db->query( + "SELECT `task_id` AS id, `tasklist_id` AS list FROM " . $this->db_tasks + . " WHERE `tasklist_id` IN (" . $this->list_ids . ")" + . " AND `uid` = ?", + $prop + ); + $prop = $this->rc->db->fetch_assoc($result); } - $childs = array(); + $childs = array(); $task_ids = array($prop'id'); // query for childs (recursively) while (!empty($task_ids)) { - $result = $this->rc->db->query(sprintf( - "SELECT task_id AS id FROM " . $this->db_tasks . " - WHERE tasklist_id IN (%s) - AND parent_id IN (%s) - AND del=0", - $this->list_ids, - join(',', array_map(array($this->rc->db, 'quote'), $task_ids)) - )); + $result = $this->rc->db->query( + "SELECT `task_id` AS id FROM " . $this->db_tasks + . " WHERE `tasklist_id` IN (" . $this->list_ids . ")" + . " AND `parent_id` IN (" . join(',', array_map(array($this->rc->db, 'quote'), $task_ids)) . ")" + . " AND `del` = 0", + ); $task_ids = array(); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { - $childs = $rec'id'; + $childs = $rec'id'; $task_ids = $rec'id'; } - if (!$recursive) + if (!$recursive) { break; + } } return $childs; @@ -443,38 +441,41 @@ /** * Get a list of pending alarms to be displayed to the user * - * @param integer Current time (unix timestamp) - * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) + * @param integer Current time (unix timestamp) + * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) + * * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { - if (empty($lists)) + if (empty($lists)) { $lists = array_keys($this->lists); - else if (is_string($lists)) - $lists = explode(',', $lists); + } + else if (!is_array($lists)) { + $lists = explode(',', (string) $lists); + } // only allow to select from calendars with activated alarms $list_ids = array(); foreach ($lists as $lid) { - if ($this->lists$lid && $this->lists$lid'showalarms') + if ($this->lists$lid && $this->lists$lid'showalarms') { $list_ids = $lid; + } } $list_ids = array_map(array($this->rc->db, 'quote'), $list_ids); $alarms = array(); if (!empty($list_ids)) { - $result = $this->rc->db->query(sprintf( - "SELECT * FROM " . $this->db_tasks . " - WHERE tasklist_id IN (%s) - AND notify <= %s AND NOT " . self::IS_COMPLETE_SQL, - join(',', $list_ids), - $this->rc->db->fromunixtime($time) - )); - - while ($result && ($rec = $this->rc->db->fetch_assoc($result))) + $result = $this->rc->db->query("SELECT * FROM " . $this->db_tasks + . " WHERE `tasklist_id` IN (" . join(',', $list_ids) . ")" + . " AND `notify` <= " . $this->rc->db->fromunixtime($time) + . " AND NOT " . self::IS_COMPLETE_SQL, + ); + + while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $alarms = $this->_read_postprocess($rec); + } } return $alarms; @@ -490,12 +491,9 @@ // set new notifyat time or unset if not snoozed $notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_tasks . " - SET changed=%s, notify=? - WHERE task_id=? - AND tasklist_id IN (" . $this->list_ids . ")", - $this->rc->db->now()), + $query = $this->rc->db->query("UPDATE " . $this->db_tasks + . " SET `changed` = " . $this->rc->db->now() . ", `notify` = ?" + . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", $notify_at, $task_id ); @@ -518,13 +516,15 @@ */ private function _read_postprocess($rec) { - $rec'id' = $rec'task_id'; - $rec'list' = $rec'tasklist_id'; + $rec'id' = $rec'task_id'; + $rec'list' = $rec'tasklist_id'; $rec'changed' = new DateTime($rec'changed'); - $rec'tags' = array_filter(explode(',', $rec'tags')); + $rec'created' = new DateTime($rec'created'); + $rec'tags' = array_filter(explode(',', $rec'tags')); - if (!$rec'parent_id') + if (!$rec'parent_id') { unset($rec'parent_id'); + } // decode serialized alarms if ($rec'alarms') { @@ -541,7 +541,8 @@ $this->tags = array_merge($this->tags, (array)$rec'tags'); } - unset($rec'task_id', $rec'tasklist_id', $rec'created'); + unset($rec'task_id', $rec'tasklist_id'); + return $rec; } @@ -549,6 +550,7 @@ * Add a single task to the database * * @param array Hash array with task properties (see header of this file) + * * @return mixed New event ID on success, False on error * @see tasklist_driver::create_task() */ @@ -556,8 +558,9 @@ { // check list permissions $list_id = $prop'list' ? $prop'list' : reset(array_keys($this->lists)); - if (!$this->lists$list_id || $this->lists$list_id'readonly') + if (!$this->lists$list_id || $this->lists$list_id'readonly') { return false; + } if (is_array($prop'valarms')) { $prop'alarms' = $this->serialize_alarms($prop'valarms'); @@ -570,18 +573,19 @@ } foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { - if (empty($prop$col)) + if (empty($prop$col)) { $prop$col = null; + } } $notify_at = $this->_get_notification($prop); - $result = $this->rc->db->query(sprintf( - "INSERT INTO " . $this->db_tasks . " - (tasklist_id, uid, parent_id, created, changed, title, date, time, startdate, starttime, description, tags, flagged, complete, status, alarms, recurrence, notify) - VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - $this->rc->db->now(), - $this->rc->db->now() - ), + $now = $this->rc->db->now(); + + $result = $this->rc->db->query("INSERT INTO " . $this->db_tasks + . " (`tasklist_id`, `uid`, `parent_id`, `created`, `changed`, `title`, `date`, `time`," + . " `startdate`, `starttime`, `description`, `tags`, `flagged`, `complete`, `status`," + . " `alarms`, `recurrence`, `notify`)" + . " VALUES (?, ?, ?, $now, $now, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $list_id, $prop'uid', $prop'parent_id', @@ -593,15 +597,16 @@ strval($prop'description'), join(',', (array)$prop'tags'), $prop'flagged' ? 1 : 0, - intval($prop'complete'), + $prop'complete', strval($prop'status'), $prop'alarms', $prop'recurrence', $notify_at ); - if ($result) + if ($result) { return $this->rc->db->insert_id($this->db_tasks); + } return false; } @@ -610,6 +615,7 @@ * Update an task entry with the given data * * @param array Hash array with task properties + * * @return boolean True on success, False on error * @see tasklist_driver::edit_task() */ @@ -627,15 +633,21 @@ $sql_set = array(); foreach (array('title', 'description', 'flagged', 'complete') as $col) { - if (isset($prop$col)) + if (isset($prop$col)) { $sql_set = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop$col); + } } - foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { - if (isset($prop$col)) + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence') as $col) { + if (isset($prop$col)) { $sql_set = $this->rc->db->quote_identifier($col) . '=' . (empty($prop$col) ? 'NULL' : $this->rc->db->quote($prop$col)); + } + } + if (isset($prop'status')) { + $sql_set = $this->rc->db->quote_identifier('status') . '=' . $this->rc->db->quote($prop'status'); } - if (isset($prop'tags')) + if (isset($prop'tags')) { $sql_set = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop'tags')); + } if (isset($prop'date') || isset($prop'time') || isset($prop'alarms')) { $notify_at = $this->_get_notification($prop); @@ -644,28 +656,23 @@ // moved from another list if ($prop'_fromlist' && ($newlist = $prop'list')) { - $sql_set = 'tasklist_id=' . $this->rc->db->quote($newlist); - } - - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_tasks . " - SET changed=%s %s - WHERE task_id=? - AND tasklist_id IN (%s)", - $this->rc->db->now(), - ($sql_set ? ', ' . join(', ', $sql_set) : ''), - $this->list_ids - ), - $prop'id' + $sql_set = $this->rc->db->quote_identifier('tasklist_id') . '=' . $this->rc->db->quote($newlist); + } + + $result = $this->rc->db->query("UPDATE " . $this->db_tasks + . " SET `changed` = " . $this->rc->db->now() . ($sql_set ? ', ' . join(', ', $sql_set) : '') + . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", + $prop'id' ); - return $this->rc->db->affected_rows($query); + return $this->rc->db->affected_rows($result); } /** * Move a single task to another list * - * @param array Hash array with task properties: + * @param array Hash array with task properties + * * @return boolean True on success, False on error * @see tasklist_driver::move_task() */ @@ -679,6 +686,7 @@ * * @param array Hash array with task properties * @param boolean Remove record irreversible + * * @return boolean True on success, False on error * @see tasklist_driver::delete_task() */ @@ -687,50 +695,39 @@ $task_id = $prop'id'; if ($task_id && $force) { - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_tasks . " - WHERE task_id=? - AND tasklist_id IN (" . $this->list_ids . ")", + $result = $this->rc->db->query("DELETE FROM " . $this->db_tasks + . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", $task_id ); } else if ($task_id) { - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_tasks . " - SET changed=%s, del=1 - WHERE task_id=? - AND tasklist_id IN (%s)", - $this->rc->db->now(), - $this->list_ids - ), - $task_id + $result = $this->rc->db->query("UPDATE " . $this->db_tasks + . " SET `changed` = " . $this->rc->db->now() . ", `del` = 1" + . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", + $task_id ); } - return $this->rc->db->affected_rows($query); + return $this->rc->db->affected_rows($result); } /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties + * * @return boolean True on success, False on error * @see tasklist_driver::undelete_task() */ public function undelete_task($prop) { - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_tasks . " - SET changed=%s, del=0 - WHERE task_id=? - AND tasklist_id IN (%s)", - $this->rc->db->now(), - $this->list_ids - ), - $prop'id' + $result = $this->rc->db->query("UPDATE " . $this->db_tasks + . " SET `changed` = " . $this->rc->db->now() . ", `del` = 0" + . " WHERE `task_id` = ? AND `tasklist_id` IN (" . $this->list_ids . ")", + $prop'id' ); - return $this->rc->db->affected_rows($query); + return $this->rc->db->affected_rows($result); } /** @@ -741,11 +738,10 @@ if ($task'valarms' && !$this->is_complete($task)) { $alarm = libcalendaring::get_next_alarm($task, 'task'); - if ($alarm'time' && in_array($alarm'action', $this->alarm_types)) - return date('Y-m-d H:i:s', $alarm'time'); - } - - return null; + if ($alarm'time' && in_array($alarm'action', $this->alarm_types)) { + return date('Y-m-d H:i:s', $alarm'time'); + } + } } /** @@ -837,15 +833,17 @@ public function user_delete($args) { $db = $this->rc->db; + + $lists = $db->query("SELECT `tasklist_id` FROM " . $this->db_lists . " WHERE `user_id` = ?", $args'user'->ID); + $list_ids = array(); - $lists = $db->query("SELECT tasklist_id FROM " . $this->db_lists . " WHERE user_id=?", $args'user'->ID); while ($row = $db->fetch_assoc($lists)) { $list_ids = $row'tasklist_id'; } if (!empty($list_ids)) { foreach (array($this->db_tasks, $this->db_lists) as $table) { - $db->query(sprintf("DELETE FROM $table WHERE tasklist_id IN (%s)", join(',', $list_ids))); + $db->query(sprintf("DELETE FROM $table WHERE `tasklist_id` IN (%s)", join(',', $list_ids))); } } }
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/drivers/tasklist_driver.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/drivers/tasklist_driver.php
Changed
@@ -423,18 +423,21 @@ * @param string The action called this form * @param array Tasklist properties * @param array List with form fields to be rendered + * * @return string HTML content of the form */ public function tasklist_edit_form($action, $list, $formfields) { - $html = ''; - foreach ($formfields as $field) { - $html .= html::div('form-section', - html::label($field'id', $field'label') . - $field'value'); + $table = new html_table(array('cols' => 2, 'class' => 'propform')); + + foreach ($formfields as $col => $colprop) { + $label = !empty($colprop'label') ? $colprop'label' : $rcmail->gettext("$domain.$col"); + + $table->add('title', html::label($colprop'id', rcube::Q($label))); + $table->add(null, $colprop'value'); } - return $html; + return $table->show(); } /**
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/skins/elastic/templates/mainview.html -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/skins/elastic/templates/mainview.html
Changed
@@ -27,7 +27,7 @@ <span id="aria-label-taskslist" class="header-title"><roundcube:label name="tasklist.navtitle" /></span> <div id="listcontrols" class="toolbar menu" role="toolbar"> <a href="#threads" class="button threads active" data-popup="threadselect-menu" title="<roundcube:label name="threads" />"><span class="inner"><roundcube:label name="threads" /></span></a> - <roundcube:button name="optionsmenu" id="listmenulink" type="link" class="button settings active" + <roundcube:button name="optionsmenu" id="listmenulink" type="link" class="button options active" label="options" innerClass="inner" onclick="tasklist_options_menu()" /> </div> <a class="button icon toolbar-menu-button" href="#list-menu"><span class="inner"><roundcube:label name="menu" /></span></a>
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/tasklist.js -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/tasklist.js
Changed
@@ -2398,8 +2398,8 @@ function task_edit_dialog(id, action, presets) { var elastic = false, infodialog = $('#taskshow'); - if (infodialog.data('nodialog') || $('#taskedit').data('nodialog')) { + if (infodialog.data('nodialog') || $('#taskedit').data('nodialog')) { elastic = true; infodialog.addClass('hidden').parent().addClass('watermark'); // Elastic } @@ -2479,9 +2479,13 @@ tasklist.val(rec.list || me.selected_list || selected_list); // tag-edit line + var tagline = $('#taskedit-tagline'); if (window.kolab_tags_input) { - $('#taskedit-tagline').parent('.form-group').show(); - taglist = kolab_tags_input($('#taskedit-tagline'), rec.tags, readonly); + tagline.parent().show(); + taglist = kolab_tags_input(tagline, rec.tags, readonly); + } + else { + tagline.parent().hide(); } // set alarm(s) @@ -3143,13 +3147,18 @@ }, prop.parent || null, prop.group); // flag as tasklist for drag & drop - $(tasklists_widget.get_item(prop.id)).data('type', 'tasklist'); + var li = $(tasklists_widget.get_item(prop.id)).data('type', 'tasklist'); delete prop.html; me.tasklistsprop.id = prop; // append to list selector in task edit dialog, too (#2985) $('<option>').attr('value', prop.id).html(Q(prop.name)).appendTo('#taskedit-tasklist'); + + // Elastic skin + if (window.UI && UI.pretty_checkbox) { + UI.pretty_checkbox($(li).find('input')); + } } /**
View file
roundcubemail-plugins-kolab-3.5.0.tar.gz/plugins/tasklist/tasklist.php -> roundcubemail-plugins-kolab-3.5.1.tar.gz/plugins/tasklist/tasklist.php
Changed
@@ -983,8 +983,11 @@ $list = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true); $success = false; - if (isset($list'showalarms')) - $list'showalarms' = intval($list'showalarms'); + unset($list'_token'); + + if (isset($list'showalarms')) { + $list'showalarms' = intval($list'showalarms'); + } switch ($action) { case 'form-new':
View file
roundcubemail-plugins-kolab-3.5.1.tar.gz/run-tests.sh
Added
@@ -0,0 +1,16 @@ +#!/bin/sh + +PHPUNIT=`which phpunit` + +if -f "vendor/phpunit/phpunit/phpunit" ; then + PHPUNIT="../vendor/phpunit/phpunit/phpunit" +fi + +cd tests + +$PHPUNIT ../plugins/libkolab/tests/kolab_date_recurrence.php +$PHPUNIT ../plugins/libkolab/tests/kolab_storage_config.php +$PHPUNIT ../plugins/libkolab/tests/kolab_storage_folder.php + +$PHPUNIT ../plugins/libcalendaring/tests/libcalendaring.php +$PHPUNIT ../plugins/libcalendaring/tests/libvcalendar.php
View file
roundcubemail-plugins-kolab.dsc
Changed
@@ -2,7 +2,7 @@ Source: roundcubemail-plugins-kolab Binary: roundcubemail-plugins-kolab Architecture: all -Version: 1:3.5.0-0~kolab5 +Version: 1:3.5.1-0~kolab1 Maintainer: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> Uploaders: Paul Klos <kolab@klos2day.nl> Standards-Version: 3.9.3 @@ -37,5 +37,5 @@ roundcubemail-plugin-tinymce-config deb web extra roundcubemail-plugin-wap-client deb web extra Files: - 00000000000000000000000000000000 0 roundcubemail-plugins-kolab-3.5.0.tar.gz + 00000000000000000000000000000000 0 roundcubemail-plugins-kolab-3.5.1.tar.gz 00000000000000000000000000000000 0 debian.tar.gz
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.