aboutsummaryrefslogtreecommitdiffstats
path: root/libcalendaring
diff options
context:
space:
mode:
Diffstat (limited to 'libcalendaring')
-rw-r--r--libcalendaring/README15
-rw-r--r--libcalendaring/composer.json25
-rw-r--r--libcalendaring/lib/.htaccess2
-rw-r--r--libcalendaring/lib/Horde_Date.php1304
-rw-r--r--libcalendaring/lib/Horde_Date_Recurrence.php1705
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component.php405
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php108
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php244
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VCard.php107
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VEvent.php70
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php68
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VJournal.php46
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VTodo.php68
-rw-r--r--libcalendaring/lib/OldSabre/VObject/DateTimeParser.php181
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Document.php109
-rw-r--r--libcalendaring/lib/OldSabre/VObject/ElementList.php172
-rw-r--r--libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php322
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Node.php187
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Parameter.php91
-rw-r--r--libcalendaring/lib/OldSabre/VObject/ParseException.php12
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property.php453
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property/Compound.php125
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property/DateTime.php245
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php180
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Reader.php223
-rw-r--r--libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php1144
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php111
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php39
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php76
-rw-r--r--libcalendaring/lib/OldSabre/VObject/StringUtil.php61
-rw-r--r--libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php527
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Version.php24
-rw-r--r--libcalendaring/lib/OldSabre/VObject/includes.php41
-rw-r--r--libcalendaring/lib/libcalendaring_itip.php817
-rw-r--r--libcalendaring/lib/libcalendaring_recurrence.php155
-rw-r--r--libcalendaring/libcalendaring.js1192
-rw-r--r--libcalendaring/libcalendaring.php1637
-rw-r--r--libcalendaring/libvcalendar.php1362
-rw-r--r--libcalendaring/localization/bg_BG.inc25
-rw-r--r--libcalendaring/localization/ca_ES.inc15
-rw-r--r--libcalendaring/localization/cs_CZ.inc131
-rw-r--r--libcalendaring/localization/da_DK.inc85
-rw-r--r--libcalendaring/localization/de_CH.inc81
-rw-r--r--libcalendaring/localization/de_DE.inc135
-rw-r--r--libcalendaring/localization/en_US.inc165
-rw-r--r--libcalendaring/localization/es_AR.inc125
-rw-r--r--libcalendaring/localization/es_ES.inc59
-rw-r--r--libcalendaring/localization/et_EE.inc60
-rw-r--r--libcalendaring/localization/fi_FI.inc133
-rw-r--r--libcalendaring/localization/fr_FR.inc131
-rw-r--r--libcalendaring/localization/hu_HU.inc96
-rw-r--r--libcalendaring/localization/it_IT.inc85
-rw-r--r--libcalendaring/localization/ja_JP.inc77
-rw-r--r--libcalendaring/localization/nl_NL.inc89
-rw-r--r--libcalendaring/localization/pl_PL.inc12
-rw-r--r--libcalendaring/localization/pt_BR.inc85
-rw-r--r--libcalendaring/localization/pt_PT.inc12
-rw-r--r--libcalendaring/localization/ru_RU.inc139
-rw-r--r--libcalendaring/localization/sk_SK.inc9
-rw-r--r--libcalendaring/localization/sl_SI.inc15
-rw-r--r--libcalendaring/localization/sv_SE.inc12
-rw-r--r--libcalendaring/localization/th_TH.inc80
-rw-r--r--libcalendaring/localization/uk_UA.inc14
-rw-r--r--libcalendaring/localization/zh_CN.inc10
-rw-r--r--libcalendaring/skins/larry/libcal.css166
65 files changed, 15699 insertions, 0 deletions
diff --git a/libcalendaring/README b/libcalendaring/README
new file mode 100644
index 0000000..86e784d
--- /dev/null
+++ b/libcalendaring/README
@@ -0,0 +1,15 @@
+Library providing common functions for calendar-based plugins
+-------------------------------------------------------------
+
+Provides utility functions for calendar-related modules such as
+
+* alarms display and dismissal
+* attachment handling
+* iCal parsing and exporting
+
+iCal parsing and exporting is done with the help of the Sabretooth VObject
+library [1]. A copy of that library with all its dependencies is part of this
+package. In order to update it, execute ./get_sabre_vobject.sh within the
+lib/ directory.
+
+[1]: https://github.com/fruux/sabre-vobject
diff --git a/libcalendaring/composer.json b/libcalendaring/composer.json
new file mode 100644
index 0000000..4b7c5e1
--- /dev/null
+++ b/libcalendaring/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "kolab/libcalendaring",
+ "type": "roundcube-plugin",
+ "description": "Library providing common functions for calendaring plugins",
+ "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
+ "license": "AGPLv3",
+ "version": "3.2.8",
+ "authors": [
+ {
+ "name": "Thomas Bruederli",
+ "email": "bruederli@kolabsys.com",
+ "role": "Lead"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "http://plugins.roundcube.net"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "roundcube/plugin-installer": ">=0.1.3"
+ }
+}
diff --git a/libcalendaring/lib/.htaccess b/libcalendaring/lib/.htaccess
new file mode 100644
index 0000000..c0800be
--- /dev/null
+++ b/libcalendaring/lib/.htaccess
@@ -0,0 +1,2 @@
+# Deny all for risky include files of this __CENSORED__ quality plugin
+Require all denied
diff --git a/libcalendaring/lib/Horde_Date.php b/libcalendaring/lib/Horde_Date.php
new file mode 100644
index 0000000..9197f84
--- /dev/null
+++ b/libcalendaring/lib/Horde_Date.php
@@ -0,0 +1,1304 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ * Horde/Date/Utils.php, Horde/Date/Recurrence.php
+ * Pull the latest version of these files from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package Date
+ *
+ * @TODO in format():
+ * http://php.net/intldateformatter
+ *
+ * @TODO on timezones:
+ * http://trac.agavi.org/ticket/1008
+ * http://trac.agavi.org/changeset/3659
+ *
+ * @TODO on switching to PHP::DateTime:
+ * The only thing ever stored in the database *IS* Unix timestamps. Doing
+ * anything other than that is unmanageable, yet some frameworks use 'server
+ * based' times in their systems, simply because they do not bother with
+ * daylight saving and only 'serve' one timezone!
+ *
+ * The second you have to manage 'real' time across timezones then daylight
+ * saving becomes essential, BUT only on the display side! Since the browser
+ * only provides a time offset, this is useless and to be honest should simply
+ * be ignored ( until it is upgraded to provide the correct information ;)
+ * ). So we need a 'display' function that takes a simple numeric epoch, and a
+ * separate timezone id into which the epoch is to be 'converted'. My W3C
+ * mapping works simply because ADOdb then converts that to it's own simple
+ * offset abbreviation - in my case GMT or BST. As long as DateTime passes the
+ * full 64 bit number the date range from 100AD is also preserved ( and
+ * further back if 2 digit years are disabled ). If I want to display the
+ * 'real' timezone with this 'time' then I just add it in place of ADOdb's
+ * 'timezone'. I am tempted to simply adjust the ADOdb class to take a
+ * timezone in place of the simple GMT switch it currently uses.
+ *
+ * The return path is just the reverse and simply needs to take the client
+ * display offset off prior to storage of the UTC epoch. SO we use
+ * DateTimeZone to get an offset value for the clients timezone and simply add
+ * or subtract this from a timezone agnostic display on the client end when
+ * entering new times.
+ *
+ *
+ * It's not really feasible to store dates in specific timezone, as most
+ * national/local timezones support DST - and that is a pain to support, as
+ * eg. sorting breaks when some timestamps get repeated. That's why it's
+ * usually better to store datetimes as either UTC datetime or plain unix
+ * timestamp. I usually go with the former - using database datetime type.
+ */
+
+/**
+ * @category Horde
+ * @package Date
+ */
+class Horde_Date
+{
+ const DATE_SUNDAY = 0;
+ const DATE_MONDAY = 1;
+ const DATE_TUESDAY = 2;
+ const DATE_WEDNESDAY = 3;
+ const DATE_THURSDAY = 4;
+ const DATE_FRIDAY = 5;
+ const DATE_SATURDAY = 6;
+
+ const MASK_SUNDAY = 1;
+ const MASK_MONDAY = 2;
+ const MASK_TUESDAY = 4;
+ const MASK_WEDNESDAY = 8;
+ const MASK_THURSDAY = 16;
+ const MASK_FRIDAY = 32;
+ const MASK_SATURDAY = 64;
+ const MASK_WEEKDAYS = 62;
+ const MASK_WEEKEND = 65;
+ const MASK_ALLDAYS = 127;
+
+ const MASK_SECOND = 1;
+ const MASK_MINUTE = 2;
+ const MASK_HOUR = 4;
+ const MASK_DAY = 8;
+ const MASK_MONTH = 16;
+ const MASK_YEAR = 32;
+ const MASK_ALLPARTS = 63;
+
+ const DATE_DEFAULT = 'Y-m-d H:i:s';
+ const DATE_JSON = 'Y-m-d\TH:i:s';
+
+ /**
+ * Year
+ *
+ * @var integer
+ */
+ protected $_year;
+
+ /**
+ * Month
+ *
+ * @var integer
+ */
+ protected $_month;
+
+ /**
+ * Day
+ *
+ * @var integer
+ */
+ protected $_mday;
+
+ /**
+ * Hour
+ *
+ * @var integer
+ */
+ protected $_hour = 0;
+
+ /**
+ * Minute
+ *
+ * @var integer
+ */
+ protected $_min = 0;
+
+ /**
+ * Second
+ *
+ * @var integer
+ */
+ protected $_sec = 0;
+
+ /**
+ * String representation of the date's timezone.
+ *
+ * @var string
+ */
+ protected $_timezone;
+
+ /**
+ * Default format for __toString()
+ *
+ * @var string
+ */
+ protected $_defaultFormat = self::DATE_DEFAULT;
+
+ /**
+ * Default specs that are always supported.
+ * @var string
+ */
+ protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
+
+ /**
+ * Internally supported strftime() specifiers.
+ * @var string
+ */
+ protected static $_supportedSpecs = '';
+
+ /**
+ * Map of required correction masks.
+ *
+ * @see __set()
+ *
+ * @var array
+ */
+ protected static $_corrections = array(
+ 'year' => self::MASK_YEAR,
+ 'month' => self::MASK_MONTH,
+ 'mday' => self::MASK_DAY,
+ 'hour' => self::MASK_HOUR,
+ 'min' => self::MASK_MINUTE,
+ 'sec' => self::MASK_SECOND,
+ );
+
+ protected $_formatCache = array();
+
+ /**
+ * Builds a new date object. If $date contains date parts, use them to
+ * initialize the object.
+ *
+ * Recognized formats:
+ * - arrays with keys 'year', 'month', 'mday', 'day'
+ * 'hour', 'min', 'minute', 'sec'
+ * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+ * - yyyy-mm-dd hh:mm:ss
+ * - yyyymmddhhmmss
+ * - yyyymmddThhmmssZ
+ * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
+ * 03 Mar 1973)
+ * - unix timestamps
+ * - anything parsed by strtotime()/DateTime.
+ *
+ * @throws Horde_Date_Exception
+ */
+ public function __construct($date = null, $timezone = null)
+ {
+ if (!self::$_supportedSpecs) {
+ self::$_supportedSpecs = self::$_defaultSpecs;
+ if (function_exists('nl_langinfo')) {
+ self::$_supportedSpecs .= 'bBpxX';
+ }
+ }
+
+ if (func_num_args() > 2) {
+ // Handle args in order: year month day hour min sec tz
+ $this->_initializeFromArgs(func_get_args());
+ return;
+ }
+
+ $this->_initializeTimezone($timezone);
+
+ if (is_null($date)) {
+ return;
+ }
+
+ if (is_string($date)) {
+ $date = trim($date, '"');
+ }
+
+ if (is_object($date)) {
+ $this->_initializeFromObject($date);
+ } elseif (is_array($date)) {
+ $this->_initializeFromArray($date);
+ } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
+ $this->_year = (int)$parts[1];
+ $this->_month = (int)$parts[2];
+ $this->_mday = (int)$parts[3];
+ $this->_hour = (int)$parts[4];
+ $this->_min = (int)$parts[5];
+ $this->_sec = (int)$parts[6];
+ if ($parts[7]) {
+ $this->_initializeTimezone('UTC');
+ }
+ } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
+ $parts[2] > 0 && $parts[2] <= 12 &&
+ $parts[3] > 0 && $parts[3] <= 31) {
+ $this->_year = (int)$parts[1];
+ $this->_month = (int)$parts[2];
+ $this->_mday = (int)$parts[3];
+ $this->_hour = $this->_min = $this->_sec = 0;
+ } elseif ((string)(int)$date == $date) {
+ // Try as a timestamp.
+ $parts = @getdate($date);
+ if ($parts) {
+ $this->_year = $parts['year'];
+ $this->_month = $parts['mon'];
+ $this->_mday = $parts['mday'];
+ $this->_hour = $parts['hours'];
+ $this->_min = $parts['minutes'];
+ $this->_sec = $parts['seconds'];
+ }
+ } else {
+ // Use date_create() so we can catch errors with PHP 5.2. Use
+ // "new DateTime() once we require 5.3.
+ $parsed = date_create($date);
+ if (!$parsed) {
+ throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
+ }
+ $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ $this->_year = (int)$parsed->format('Y');
+ $this->_month = (int)$parsed->format('m');
+ $this->_mday = (int)$parsed->format('d');
+ $this->_hour = (int)$parsed->format('H');
+ $this->_min = (int)$parsed->format('i');
+ $this->_sec = (int)$parsed->format('s');
+ $this->_initializeTimezone(date_default_timezone_get());
+ }
+ }
+
+ /**
+ * Returns a simple string representation of the date object
+ *
+ * @return string This object converted to a string.
+ */
+ public function __toString()
+ {
+ try {
+ return $this->format($this->_defaultFormat);
+ } catch (Exception $e) {
+ return '';
+ }
+ }
+
+ /**
+ * Returns a DateTime object representing this object.
+ *
+ * @return DateTime
+ */
+ public function toDateTime()
+ {
+ $date = new DateTime(null, new DateTimeZone($this->_timezone));
+ $date->setDate($this->_year, $this->_month, $this->_mday);
+ $date->setTime($this->_hour, $this->_min, $this->_sec);
+ return $date;
+ }
+
+ /**
+ * Converts a date in the proleptic Gregorian calendar to the no of days
+ * since 24th November, 4714 B.C.
+ *
+ * Returns the no of days since Monday, 24th November, 4714 B.C. in the
+ * proleptic Gregorian calendar (which is 24th November, -4713 using
+ * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
+ * proleptic Julian calendar). This is also the first day of the 'Julian
+ * Period' proposed by Joseph Scaliger in 1583, and the number of days
+ * since this date is known as the 'Julian Day'. (It is not directly
+ * to do with the Julian calendar, although this is where the name
+ * is derived from.)
+ *
+ * The algorithm is valid for all years (positive and negative), and
+ * also for years preceding 4714 B.C.
+ *
+ * Algorithm is from PEAR::Date_Calc
+ *
+ * @author Monte Ohrt <monte@ispi.net>
+ * @author Pierre-Alain Joye <pajoye@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @author C.A. Woodcock <c01234@netcomuk.co.uk>
+ *
+ * @return integer The number of days since 24th November, 4714 B.C.
+ */
+ public function toDays()
+ {
+ if (function_exists('GregorianToJD')) {
+ return gregoriantojd($this->_month, $this->_mday, $this->_year);
+ }
+
+ $day = $this->_mday;
+ $month = $this->_month;
+ $year = $this->_year;
+
+ if ($month > 2) {
+ // March = 0, April = 1, ..., December = 9,
+ // January = 10, February = 11
+ $month -= 3;
+ } else {
+ $month += 9;
+ --$year;
+ }
+
+ $hb_negativeyear = $year < 0;
+ $century = intval($year / 100);
+ $year = $year % 100;
+
+ if ($hb_negativeyear) {
+ // Subtract 1 because year 0 is a leap year;
+ // And N.B. that we must treat the leap years as occurring
+ // one year earlier than they do, because for the purposes
+ // of calculation, the year starts on 1st March:
+ //
+ return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
+ intval((1461 * $year + 1) / 4) +
+ intval((153 * $month + 2) / 5) +
+ $day + 1721118;
+ } else {
+ return intval(146097 * $century / 4) +
+ intval(1461 * $year / 4) +
+ intval((153 * $month + 2) / 5) +
+ $day + 1721119;
+ }
+ }
+
+ /**
+ * Converts number of days since 24th November, 4714 B.C. (in the proleptic
+ * Gregorian calendar, which is year -4713 using 'Astronomical' year
+ * numbering) to Gregorian calendar date.
+ *
+ * Returned date belongs to the proleptic Gregorian calendar, using
+ * 'Astronomical' year numbering.
+ *
+ * The algorithm is valid for all years (positive and negative), and
+ * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
+ * and so the only limitation is platform-dependent (for 32-bit systems
+ * the maximum year would be something like about 1,465,190 A.D.).
+ *
+ * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
+ *
+ * Algorithm is from PEAR::Date_Calc
+ *
+ * @author Monte Ohrt <monte@ispi.net>
+ * @author Pierre-Alain Joye <pajoye@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @author C.A. Woodcock <c01234@netcomuk.co.uk>
+ *
+ * @param int $days the number of days since 24th November, 4714 B.C.
+ * @param string $format the string indicating how to format the output
+ *
+ * @return Horde_Date A Horde_Date object representing the date.
+ */
+ public static function fromDays($days)
+ {
+ if (function_exists('JDToGregorian')) {
+ list($month, $day, $year) = explode('/', JDToGregorian($days));
+ } else {
+ $days = intval($days);
+
+ $days -= 1721119;
+ $century = floor((4 * $days - 1) / 146097);
+ $days = floor(4 * $days - 1 - 146097 * $century);
+ $day = floor($days / 4);
+
+ $year = floor((4 * $day + 3) / 1461);
+ $day = floor(4 * $day + 3 - 1461 * $year);
+ $day = floor(($day + 4) / 4);
+
+ $month = floor((5 * $day - 3) / 153);
+ $day = floor(5 * $day - 3 - 153 * $month);
+ $day = floor(($day + 5) / 5);
+
+ $year = $century * 100 + $year;
+ if ($month < 10) {
+ $month +=3;
+ } else {
+ $month -=9;
+ ++$year;
+ }
+ }
+
+ return new Horde_Date($year, $month, $day);
+ }
+
+ /**
+ * Getter for the date and time properties.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ *
+ * @return integer The property value, or null if not set.
+ */
+ public function __get($name)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+
+ return $this->{'_' . $name};
+ }
+
+ /**
+ * Setter for the date and time properties.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ * @param integer $value The property value.
+ */
+ public function __set($name, $value)
+ {
+ if ($name == 'timezone') {
+ $this->_initializeTimezone($value);
+ return;
+ }
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+
+ if ($name != 'year' && $name != 'month' && $name != 'mday' &&
+ $name != 'hour' && $name != 'min' && $name != 'sec') {
+ throw new InvalidArgumentException('Undefined property ' . $name);
+ }
+
+ $down = $value < $this->{'_' . $name};
+ $this->{'_' . $name} = $value;
+ $this->_correct(self::$_corrections[$name], $down);
+ $this->_formatCache = array();
+ }
+
+ /**
+ * Returns whether a date or time property exists.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ *
+ * @return boolen True if the property exists and is set.
+ */
+ public function __isset($name)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+ return ($name == 'year' || $name == 'month' || $name == 'mday' ||
+ $name == 'hour' || $name == 'min' || $name == 'sec') &&
+ isset($this->{'_' . $name});
+ }
+
+ /**
+ * Adds a number of seconds or units to this date, returning a new Date
+ * object.
+ */
+ public function add($factor)
+ {
+ $d = clone($this);
+ if (is_array($factor) || is_object($factor)) {
+ foreach ($factor as $property => $value) {
+ $d->$property += $value;
+ }
+ } else {
+ $d->sec += $factor;
+ }
+
+ return $d;
+ }
+
+ /**
+ * Subtracts a number of seconds or units from this date, returning a new
+ * Horde_Date object.
+ */
+ public function sub($factor)
+ {
+ if (is_array($factor)) {
+ foreach ($factor as &$value) {
+ $value *= -1;
+ }
+ } else {
+ $factor *= -1;
+ }
+
+ return $this->add($factor);
+ }
+
+ /**
+ * Converts this object to a different timezone.
+ *
+ * @param string $timezone The new timezone.
+ *
+ * @return Horde_Date This object.
+ */
+ public function setTimezone($timezone)
+ {
+ $date = $this->toDateTime();
+ $date->setTimezone(new DateTimeZone($timezone));
+ $this->_timezone = $timezone;
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ $this->_formatCache = array();
+ return $this;
+ }
+
+ /**
+ * Sets the default date format used in __toString()
+ *
+ * @param string $format
+ */
+ public function setDefaultFormat($format)
+ {
+ $this->_defaultFormat = $format;
+ }
+
+ /**
+ * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
+ *
+ * @return integer The day of the week.
+ */
+ public function dayOfWeek()
+ {
+ if ($this->_month > 2) {
+ $month = $this->_month - 2;
+ $year = $this->_year;
+ } else {
+ $month = $this->_month + 10;
+ $year = $this->_year - 1;
+ }
+
+ $day = (floor((13 * $month - 1) / 5) +
+ $this->_mday + ($year % 100) +
+ floor(($year % 100) / 4) +
+ floor(($year / 100) / 4) - 2 *
+ floor($year / 100) + 77);
+
+ return (int)($day - 7 * floor($day / 7));
+ }
+
+ /**
+ * Returns the day number of the year (1 to 365/366).
+ *
+ * @return integer The day of the year.
+ */
+ public function dayOfYear()
+ {
+ return $this->format('z') + 1;
+ }
+
+ /**
+ * Returns the week of the month.
+ *
+ * @return integer The week number.
+ */
+ public function weekOfMonth()
+ {
+ return ceil($this->_mday / 7);
+ }
+
+ /**
+ * Returns the week of the year, first Monday is first day of first week.
+ *
+ * @return integer The week number.
+ */
+ public function weekOfYear()
+ {
+ return $this->format('W');
+ }
+
+ /**
+ * Returns the number of weeks in the given year (52 or 53).
+ *
+ * @param integer $year The year to count the number of weeks in.
+ *
+ * @return integer $numWeeks The number of weeks in $year.
+ */
+ public static function weeksInYear($year)
+ {
+ // Find the last Thursday of the year.
+ $date = new Horde_Date($year . '-12-31');
+ while ($date->dayOfWeek() != self::DATE_THURSDAY) {
+ --$date->mday;
+ }
+ return $date->weekOfYear();
+ }
+
+ /**
+ * Sets the date of this object to the $nth weekday of $weekday.
+ *
+ * @param integer $weekday The day of the week (0 = Sunday, etc).
+ * @param integer $nth The $nth $weekday to set to (defaults to 1).
+ */
+ public function setNthWeekday($weekday, $nth = 1)
+ {
+ if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
+ return;
+ }
+
+ if ($nth < 0) { // last $weekday of month
+ $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ $last = $this->dayOfWeek();
+ $this->_mday += ($weekday - $last);
+ if ($this->_mday > $lastday)
+ $this->_mday -= 7;
+ }
+ else {
+ $this->_mday = 1;
+ $first = $this->dayOfWeek();
+ if ($weekday < $first) {
+ $this->_mday = 8 + $weekday - $first;
+ } else {
+ $this->_mday = $weekday - $first + 1;
+ }
+ $diff = 7 * $nth - 7;
+ $this->_mday += $diff;
+ $this->_correct(self::MASK_DAY, $diff < 0);
+ }
+ }
+
+ /**
+ * Is the date currently represented by this object a valid date?
+ *
+ * @return boolean Validity, counting leap years, etc.
+ */
+ public function isValid()
+ {
+ return ($this->_year >= 0 && $this->_year <= 9999);
+ }
+
+ /**
+ * Compares this date to another date object to see which one is
+ * greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are on the same date
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ public function compareDate($other)
+ {
+ if (!($other instanceof Horde_Date)) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($this->_year != $other->year) {
+ return $this->_year - $other->year;
+ }
+ if ($this->_month != $other->month) {
+ return $this->_month - $other->month;
+ }
+
+ return $this->_mday - $other->mday;
+ }
+
+ /**
+ * Returns whether this date is after the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is after the other.
+ */
+ public function after($other)
+ {
+ return $this->compareDate($other) > 0;
+ }
+
+ /**
+ * Returns whether this date is before the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is before the other.
+ */
+ public function before($other)
+ {
+ return $this->compareDate($other) < 0;
+ }
+
+ /**
+ * Returns whether this date is the same like the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is the same like the other.
+ */
+ public function equals($other)
+ {
+ return $this->compareDate($other) == 0;
+ }
+
+ /**
+ * Compares this to another date object by time, to see which one
+ * is greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are at the same time
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ public function compareTime($other)
+ {
+ if (!($other instanceof Horde_Date)) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($this->_hour != $other->hour) {
+ return $this->_hour - $other->hour;
+ }
+ if ($this->_min != $other->min) {
+ return $this->_min - $other->min;
+ }
+
+ return $this->_sec - $other->sec;
+ }
+
+ /**
+ * Compares this to another date object, including times, to see
+ * which one is greater (later). Assumes that the dates are in the
+ * same timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are equal
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ public function compareDateTime($other)
+ {
+ if (!($other instanceof Horde_Date)) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($diff = $this->compareDate($other)) {
+ return $diff;
+ }
+
+ return $this->compareTime($other);
+ }
+
+ /**
+ * Returns number of days between this date and another.
+ *
+ * @param Horde_Date $other The other day to diff with.
+ *
+ * @return integer The absolute number of days between the two dates.
+ */
+ public function diff($other)
+ {
+ return abs($this->toDays() - $other->toDays());
+ }
+
+ /**
+ * Returns the time offset for local time zone.
+ *
+ * @param boolean $colon Place a colon between hours and minutes?
+ *
+ * @return string Timezone offset as a string in the format +HH:MM.
+ */
+ public function tzOffset($colon = true)
+ {
+ return $colon ? $this->format('P') : $this->format('O');
+ }
+
+ /**
+ * Returns the unix timestamp representation of this date.
+ *
+ * @return integer A unix timestamp.
+ */
+ public function timestamp()
+ {
+ if ($this->_year >= 1970 && $this->_year < 2038) {
+ return mktime($this->_hour, $this->_min, $this->_sec,
+ $this->_month, $this->_mday, $this->_year);
+ }
+ return $this->format('U');
+ }
+
+ /**
+ * Returns the unix timestamp representation of this date, 12:00am.
+ *
+ * @return integer A unix timestamp.
+ */
+ public function datestamp()
+ {
+ if ($this->_year >= 1970 && $this->_year < 2038) {
+ return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
+ }
+ $date = new DateTime($this->format('Y-m-d'));
+ return $date->format('U');
+ }
+
+ /**
+ * Formats date and time to be passed around as a short url parameter.
+ *
+ * @return string Date and time.
+ */
+ public function dateString()
+ {
+ return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
+ }
+
+ /**
+ * Formats date and time to the ISO format used by JSON.
+ *
+ * @return string Date and time.
+ */
+ public function toJson()
+ {
+ return $this->format(self::DATE_JSON);
+ }
+
+ /**
+ * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
+ *
+ * @param boolean $floating Whether to return a floating date-time
+ * (without time zone information).
+ *
+ * @return string Date and time.
+ */
+ public function toiCalendar($floating = false)
+ {
+ if ($floating) {
+ return $this->format('Ymd\THis');
+ }
+ $dateTime = $this->toDateTime();
+ $dateTime->setTimezone(new DateTimeZone('UTC'));
+ return $dateTime->format('Ymd\THis\Z');
+ }
+
+ /**
+ * Formats time using the specifiers available in date() or in the DateTime
+ * class' format() method.
+ *
+ * To format in languages other than English, use strftime() instead.
+ *
+ * @param string $format
+ *
+ * @return string Formatted time.
+ */
+ public function format($format)
+ {
+ if (!isset($this->_formatCache[$format])) {
+ $this->_formatCache[$format] = $this->toDateTime()->format($format);
+ }
+ return $this->_formatCache[$format];
+ }
+
+ /**
+ * Formats date and time using strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ public function strftime($format)
+ {
+ if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
+ return strftime($format, $this->timestamp());
+ } else {
+ return $this->_strftime($format);
+ }
+ }
+
+ /**
+ * Formats date and time using a limited set of the strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ protected function _strftime($format)
+ {
+ return preg_replace(
+ array('/%b/e',
+ '/%B/e',
+ '/%C/e',
+ '/%d/e',
+ '/%D/e',
+ '/%e/e',
+ '/%H/e',
+ '/%I/e',
+ '/%m/e',
+ '/%M/e',
+ '/%n/',
+ '/%p/e',
+ '/%R/e',
+ '/%S/e',
+ '/%t/',
+ '/%T/e',
+ '/%x/e',
+ '/%X/e',
+ '/%y/e',
+ '/%Y/',
+ '/%%/'),
+ array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
+ '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
+ '(int)($this->_year / 100)',
+ 'sprintf(\'%02d\', $this->_mday)',
+ '$this->_strftime(\'%m/%d/%y\')',
+ 'sprintf(\'%2d\', $this->_mday)',
+ 'sprintf(\'%02d\', $this->_hour)',
+ 'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
+ 'sprintf(\'%02d\', $this->_month)',
+ 'sprintf(\'%02d\', $this->_min)',
+ "\n",
+ '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
+ '$this->_strftime(\'%H:%M\')',
+ 'sprintf(\'%02d\', $this->_sec)',
+ "\t",
+ '$this->_strftime(\'%H:%M:%S\')',
+ '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
+ '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
+ 'substr(sprintf(\'%04d\', $this->_year), -2)',
+ (int)$this->_year,
+ '%'),
+ $format);
+ }
+
+ /**
+ * Corrects any over- or underflows in any of the date's members.
+ *
+ * @param integer $mask We may not want to correct some overflows.
+ * @param integer $down Whether to correct the date up or down.
+ */
+ protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
+ {
+ if ($mask & self::MASK_SECOND) {
+ if ($this->_sec < 0 || $this->_sec > 59) {
+ $mask |= self::MASK_MINUTE;
+
+ $this->_min += (int)($this->_sec / 60);
+ $this->_sec %= 60;
+ if ($this->_sec < 0) {
+ $this->_min--;
+ $this->_sec += 60;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_MINUTE) {
+ if ($this->_min < 0 || $this->_min > 59) {
+ $mask |= self::MASK_HOUR;
+
+ $this->_hour += (int)($this->_min / 60);
+ $this->_min %= 60;
+ if ($this->_min < 0) {
+ $this->_hour--;
+ $this->_min += 60;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_HOUR) {
+ if ($this->_hour < 0 || $this->_hour > 23) {
+ $mask |= self::MASK_DAY;
+
+ $this->_mday += (int)($this->_hour / 24);
+ $this->_hour %= 24;
+ if ($this->_hour < 0) {
+ $this->_mday--;
+ $this->_hour += 24;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_MONTH) {
+ $this->_correctMonth($down);
+ /* When correcting the month, always correct the day too. Months
+ * have different numbers of days. */
+ $mask |= self::MASK_DAY;
+ }
+
+ if ($mask & self::MASK_DAY) {
+ while ($this->_mday > 28 &&
+ $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
+ if ($down) {
+ $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ } else {
+ $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ $this->_month++;
+ }
+ $this->_correctMonth($down);
+ }
+ while ($this->_mday < 1) {
+ --$this->_month;
+ $this->_correctMonth($down);
+ $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ }
+ }
+ }
+
+ /**
+ * Corrects the current month.
+ *
+ * This cannot be done in _correct() because that would also trigger a
+ * correction of the day, which would result in an infinite loop.
+ *
+ * @param integer $down Whether to correct the date up or down.
+ */
+ protected function _correctMonth($down = false)
+ {
+ $this->_year += (int)($this->_month / 12);
+ $this->_month %= 12;
+ if ($this->_month < 1) {
+ $this->_year--;
+ $this->_month += 12;
+ }
+ }
+
+ /**
+ * Handles args in order: year month day hour min sec tz
+ */
+ protected function _initializeFromArgs($args)
+ {
+ $tz = (isset($args[6])) ? array_pop($args) : null;
+ $this->_initializeTimezone($tz);
+
+ $args = array_slice($args, 0, 6);
+ $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
+ $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
+ $date = array_merge($keys, $date);
+
+ $this->_initializeFromArray($date);
+ }
+
+ protected function _initializeFromArray($date)
+ {
+ if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
+ if ($date['year'] > 70) {
+ $date['year'] += 1900;
+ } else {
+ $date['year'] += 2000;
+ }
+ }
+
+ foreach ($date as $key => $val) {
+ if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+ $this->{'_'. $key} = (int)$val;
+ }
+ }
+
+ // If $date['day'] is present and numeric we may have been passed
+ // a Horde_Form_datetime array.
+ if (isset($date['day']) &&
+ (string)(int)$date['day'] == $date['day']) {
+ $this->_mday = (int)$date['day'];
+ }
+ // 'minute' key also from Horde_Form_datetime
+ if (isset($date['minute']) &&
+ (string)(int)$date['minute'] == $date['minute']) {
+ $this->_min = (int)$date['minute'];
+ }
+
+ $this->_correct();
+ }
+
+ protected function _initializeFromObject($date)
+ {
+ if ($date instanceof DateTime) {
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ $this->_initializeTimezone($date->getTimezone()->getName());
+ } else {
+ $is_horde_date = $date instanceof Horde_Date;
+ foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
+ if ($is_horde_date || isset($date->$key)) {
+ $this->{'_' . $key} = (int)$date->$key;
+ }
+ }
+ if (!$is_horde_date) {
+ $this->_correct();
+ } else {
+ $this->_initializeTimezone($date->timezone);
+ }
+ }
+ }
+
+ protected function _initializeTimezone($timezone)
+ {
+ if (empty($timezone)) {
+ $timezone = date_default_timezone_get();
+ }
+ $this->_timezone = $timezone;
+ }
+
+}
+
+/**
+ * @category Horde
+ * @package Date
+ */
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package Date
+ */
+class Horde_Date_Utils
+{
+ /**
+ * Returns whether a year is a leap year.
+ *
+ * @param integer $year The year.
+ *
+ * @return boolean True if the year is a leap year.
+ */
+ public static function isLeapYear($year)
+ {
+ if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+ return false;
+ }
+
+ return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+ }
+
+ /**
+ * Returns the date of the year that corresponds to the first day of the
+ * given week.
+ *
+ * @param integer $week The week of the year to find the first day of.
+ * @param integer $year The year to calculate for.
+ *
+ * @return Horde_Date The date of the first day of the given week.
+ */
+ public static function firstDayOfWeek($week, $year)
+ {
+ return new Horde_Date(sprintf('%04dW%02d', $year, $week));
+ }
+
+ /**
+ * Returns the number of days in the specified month.
+ *
+ * @param integer $month The month
+ * @param integer $year The year.
+ *
+ * @return integer The number of days in the month.
+ */
+ public static function daysInMonth($month, $year)
+ {
+ static $cache = array();
+ if (!isset($cache[$year][$month])) {
+ $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
+ $cache[$year][$month] = $date->format('t');
+ }
+ return $cache[$year][$month];
+ }
+
+ /**
+ * Returns a relative, natural language representation of a timestamp
+ *
+ * @todo Wider range of values ... maybe future time as well?
+ * @todo Support minimum resolution parameter.
+ *
+ * @param mixed $time The time. Any format accepted by Horde_Date.
+ * @param string $date_format Format to display date if timestamp is
+ * more then 1 day old.
+ * @param string $time_format Format to display time if timestamp is 1
+ * day old.
+ *
+ * @return string The relative time (i.e. 2 minutes ago)
+ */
+ public static function relativeDateTime($time, $date_format = '%x',
+ $time_format = '%X')
+ {
+ $date = new Horde_Date($time);
+
+ $delta = time() - $date->timestamp();
+ if ($delta < 60) {
+ return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
+ }
+
+ $delta = round($delta / 60);
+ if ($delta < 60) {
+ return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
+ }
+
+ $delta = round($delta / 60);
+ if ($delta < 24) {
+ return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
+ }
+
+ if ($delta > 24 && $delta < 48) {
+ $date = new Horde_Date($time);
+ return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
+ }
+
+ $delta = round($delta / 24);
+ if ($delta < 7) {
+ return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
+ }
+
+ if (round($delta / 7) < 5) {
+ $delta = round($delta / 7);
+ return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
+ }
+
+ // Default to the user specified date format.
+ return $date->strftime($date_format);
+ }
+
+ /**
+ * Tries to convert strftime() formatters to date() formatters.
+ *
+ * Unsupported formatters will be removed.
+ *
+ * @param string $format A strftime() formatting string.
+ *
+ * @return string A date() formatting string.
+ */
+ public static function strftime2date($format)
+ {
+ $replace = array(
+ '/%a/' => 'D',
+ '/%A/' => 'l',
+ '/%d/' => 'd',
+ '/%e/' => 'j',
+ '/%j/' => 'z',
+ '/%u/' => 'N',
+ '/%w/' => 'w',
+ '/%U/' => '',
+ '/%V/' => 'W',
+ '/%W/' => '',
+ '/%b/' => 'M',
+ '/%B/' => 'F',
+ '/%h/' => 'M',
+ '/%m/' => 'm',
+ '/%C/' => '',
+ '/%g/' => '',
+ '/%G/' => 'o',
+ '/%y/' => 'y',
+ '/%Y/' => 'Y',
+ '/%H/' => 'H',
+ '/%I/' => 'h',
+ '/%i/' => 'g',
+ '/%M/' => 'i',
+ '/%p/' => 'A',
+ '/%P/' => 'a',
+ '/%r/' => 'h:i:s A',
+ '/%R/' => 'H:i',
+ '/%S/' => 's',
+ '/%T/' => 'H:i:s',
+ '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
+ '/%z/' => 'O',
+ '/%Z/' => '',
+ '/%c/' => '',
+ '/%D/' => 'm/d/y',
+ '/%F/' => 'Y-m-d',
+ '/%s/' => 'U',
+ '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
+ '/%n/' => "\n",
+ '/%t/' => "\t",
+ '/%%/' => '%'
+ );
+
+ return preg_replace(array_keys($replace), array_values($replace), $format);
+ }
+
+}
diff --git a/libcalendaring/lib/Horde_Date_Recurrence.php b/libcalendaring/lib/Horde_Date_Recurrence.php
new file mode 100644
index 0000000..19e372c
--- /dev/null
+++ b/libcalendaring/lib/Horde_Date_Recurrence.php
@@ -0,0 +1,1705 @@
+<?php
+
+/**
+ * This is a modified copy of Horde/Date/Recurrence.php
+ * Pull the latest version of this file from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+if (!class_exists('Horde_Date'))
+ require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
+class Horde_Date_Translation
+{
+ function t($arg) { return $arg; }
+ function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); }
+}
+
+
+/**
+ * This file contains the Horde_Date_Recurrence class and according constants.
+ *
+ * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.horde.org/licenses/lgpl21.
+ *
+ * @category Horde
+ * @package Date
+ */
+
+/**
+ * The Horde_Date_Recurrence class implements algorithms for calculating
+ * recurrences of events, including several recurrence types, intervals,
+ * exceptions, and conversion from and to vCalendar and iCalendar recurrence
+ * rules.
+ *
+ * All methods expecting dates as parameters accept all values that the
+ * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
+ * object, an ISO time string or a hash.
+ *
+ * @author Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package Date
+ */
+class Horde_Date_Recurrence
+{
+ /** No Recurrence **/
+ const RECUR_NONE = 0;
+
+ /** Recurs daily. */
+ const RECUR_DAILY = 1;
+
+ /** Recurs weekly. */
+ const RECUR_WEEKLY = 2;
+
+ /** Recurs monthly on the same date. */
+ const RECUR_MONTHLY_DATE = 3;
+
+ /** Recurs monthly on the same week day. */
+ const RECUR_MONTHLY_WEEKDAY = 4;
+
+ /** Recurs yearly on the same date. */
+ const RECUR_YEARLY_DATE = 5;
+
+ /** Recurs yearly on the same day of the year. */
+ const RECUR_YEARLY_DAY = 6;
+
+ /** Recurs yearly on the same week day. */
+ const RECUR_YEARLY_WEEKDAY = 7;
+
+ /**
+ * The start time of the event.
+ *
+ * @var Horde_Date
+ */
+ public $start;
+
+ /**
+ * The end date of the recurrence interval.
+ *
+ * @var Horde_Date
+ */
+ public $recurEnd = null;
+
+ /**
+ * The number of recurrences.
+ *
+ * @var integer
+ */
+ public $recurCount = null;
+
+ /**
+ * The type of recurrence this event follows. RECUR_* constant.
+ *
+ * @var integer
+ */
+ public $recurType = self::RECUR_NONE;
+
+ /**
+ * The length of time between recurrences. The time unit depends on the
+ * recurrence type.
+ *
+ * @var integer
+ */
+ public $recurInterval = 1;
+
+ /**
+ * Any additional recurrence data.
+ *
+ * @var integer
+ */
+ public $recurData = null;
+
+ /**
+ * BYDAY recurrence number
+ *
+ * @var integer
+ */
+ public $recurNthDay = null;
+
+ /**
+ * BYMONTH recurrence data
+ *
+ * @var array
+ */
+ public $recurMonths = array();
+
+ /**
+ * RDATE recurrence values
+ *
+ * @var array
+ */
+ public $rdates = array();
+
+ /**
+ * All the exceptions from recurrence for this event.
+ *
+ * @var array
+ */
+ public $exceptions = array();
+
+ /**
+ * All the dates this recurrence has been marked as completed.
+ *
+ * @var array
+ */
+ public $completions = array();
+
+ /**
+ * Constructor.
+ *
+ * @param Horde_Date $start Start of the recurring event.
+ */
+ public function __construct($start)
+ {
+ $this->start = new Horde_Date($start);
+ }
+
+ /**
+ * Resets the class properties.
+ */
+ public function reset()
+ {
+ $this->recurEnd = null;
+ $this->recurCount = null;
+ $this->recurType = self::RECUR_NONE;
+ $this->recurInterval = 1;
+ $this->recurData = null;
+ $this->exceptions = array();
+ $this->completions = array();
+ }
+
+ /**
+ * Checks if this event recurs on a given day of the week.
+ *
+ * @param integer $dayMask A mask consisting of Horde_Date::MASK_*
+ * constants specifying the day(s) to check.
+ *
+ * @return boolean True if this event recurs on the given day(s).
+ */
+ public function recurOnDay($dayMask)
+ {
+ return ($this->recurData & $dayMask);
+ }
+
+ /**
+ * Specifies the days this event recurs on.
+ *
+ * @param integer $dayMask A mask consisting of Horde_Date::MASK_*
+ * constants specifying the day(s) to recur on.
+ */
+ public function setRecurOnDay($dayMask)
+ {
+ $this->recurData = $dayMask;
+ }
+
+ /**
+ *
+ * @param integer $nthDay The nth weekday of month to repeat events on
+ */
+ public function setRecurNthWeekday($nth)
+ {
+ $this->recurNthDay = (int)$nth;
+ }
+
+ /**
+ *
+ * @return integer The nth weekday of month to repeat events.
+ */
+ public function getRecurNthWeekday()
+ {
+ return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
+ }
+
+ /**
+ * Specifies the months for yearly (weekday) recurrence
+ *
+ * @param array $months List of months (integers) this event recurs on.
+ */
+ function setRecurByMonth($months)
+ {
+ $this->recurMonths = (array)$months;
+ }
+
+ /**
+ * Returns a list of months this yearly event recurs on
+ *
+ * @return array List of months (integers) this event recurs on.
+ */
+ function getRecurByMonth()
+ {
+ return $this->recurMonths;
+ }
+
+ /**
+ * Returns the days this event recurs on.
+ *
+ * @return integer A mask consisting of Horde_Date::MASK_* constants
+ * specifying the day(s) this event recurs on.
+ */
+ public function getRecurOnDays()
+ {
+ return $this->recurData;
+ }
+
+ /**
+ * Returns whether this event has a specific recurrence type.
+ *
+ * @param integer $recurrence RECUR_* constant of the
+ * recurrence type to check for.
+ *
+ * @return boolean True if the event has the specified recurrence type.
+ */
+ public function hasRecurType($recurrence)
+ {
+ return ($recurrence == $this->recurType);
+ }
+
+ /**
+ * Sets a recurrence type for this event.
+ *
+ * @param integer $recurrence A RECUR_* constant.
+ */
+ public function setRecurType($recurrence)
+ {
+ $this->recurType = $recurrence;
+ }
+
+ /**
+ * Returns recurrence type of this event.
+ *
+ * @return integer A RECUR_* constant.
+ */
+ public function getRecurType()
+ {
+ return $this->recurType;
+ }
+
+ /**
+ * Returns a description of this event's recurring type.
+ *
+ * @return string Human readable recurring type.
+ */
+ public function getRecurName()
+ {
+ switch ($this->getRecurType()) {
+ case self::RECUR_NONE: return Horde_Date_Translation::t("No recurrence");
+ case self::RECUR_DAILY: return Horde_Date_Translation::t("Daily");
+ case self::RECUR_WEEKLY: return Horde_Date_Translation::t("Weekly");
+ case self::RECUR_MONTHLY_DATE:
+ case self::RECUR_MONTHLY_WEEKDAY: return Horde_Date_Translation::t("Monthly");
+ case self::RECUR_YEARLY_DATE:
+ case self::RECUR_YEARLY_DAY:
+ case self::RECUR_YEARLY_WEEKDAY: return Horde_Date_Translation::t("Yearly");
+ }
+ }
+
+ /**
+ * Sets the length of time between recurrences of this event.
+ *
+ * @param integer $interval The time between recurrences.
+ */
+ public function setRecurInterval($interval)
+ {
+ if ($interval > 0) {
+ $this->recurInterval = $interval;
+ }
+ }
+
+ /**
+ * Retrieves the length of time between recurrences of this event.
+ *
+ * @return integer The number of seconds between recurrences.
+ */
+ public function getRecurInterval()
+ {
+ return $this->recurInterval;
+ }
+
+ /**
+ * Sets the number of recurrences of this event.
+ *
+ * @param integer $count The number of recurrences.
+ */
+ public function setRecurCount($count)
+ {
+ if ($count > 0) {
+ $this->recurCount = (int)$count;
+ // Recurrence counts and end dates are mutually exclusive.
+ $this->recurEnd = null;
+ } else {
+ $this->recurCount = null;
+ }
+ }
+
+ /**
+ * Retrieves the number of recurrences of this event.
+ *
+ * @return integer The number recurrences.
+ */
+ public function getRecurCount()
+ {
+ return $this->recurCount;
+ }
+
+ /**
+ * Returns whether this event has a recurrence with a fixed count.
+ *
+ * @return boolean True if this recurrence has a fixed count.
+ */
+ public function hasRecurCount()
+ {
+ return isset($this->recurCount);
+ }
+
+ /**
+ * Sets the start date of the recurrence interval.
+ *
+ * @param Horde_Date $start The recurrence start.
+ */
+ public function setRecurStart($start)
+ {
+ $this->start = clone $start;
+ }
+
+ /**
+ * Retrieves the start date of the recurrence interval.
+ *
+ * @return Horde_Date The recurrence start.
+ */
+ public function getRecurStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * Sets the end date of the recurrence interval.
+ *
+ * @param Horde_Date $end The recurrence end.
+ */
+ public function setRecurEnd($end)
+ {
+ if (!empty($end)) {
+ // Recurrence counts and end dates are mutually exclusive.
+ $this->recurCount = null;
+ $this->recurEnd = clone $end;
+ } else {
+ $this->recurEnd = $end;
+ }
+ }
+
+ /**
+ * Retrieves the end date of the recurrence interval.
+ *
+ * @return Horde_Date The recurrence end.
+ */
+ public function getRecurEnd()
+ {
+ return $this->recurEnd;
+ }
+
+ /**
+ * Returns whether this event has a recurrence end.
+ *
+ * @return boolean True if this recurrence ends.
+ */
+ public function hasRecurEnd()
+ {
+ return isset($this->recurEnd) && isset($this->recurEnd->year) &&
+ $this->recurEnd->year != 9999;
+ }
+
+ /**
+ * Finds the next recurrence of this event that's after $afterDate.
+ *
+ * @param Horde_Date|string $after Return events after this date.
+ *
+ * @return Horde_Date|boolean The date of the next recurrence or false
+ * if the event does not recur after
+ * $afterDate.
+ */
+ public function nextRecurrence($after)
+ {
+ if (!($after instanceof Horde_Date)) {
+ $after = new Horde_Date($after);
+ } else {
+ $after = clone($after);
+ }
+
+ // Make sure $after and $this->start are in the same TZ
+ $after->setTimezone($this->start->timezone);
+ if ($this->start->compareDateTime($after) >= 0) {
+ return clone $this->start;
+ }
+
+ if ($this->recurInterval == 0 && empty($this->rdates)) {
+ return false;
+ }
+
+ switch ($this->getRecurType()) {
+ case self::RECUR_DAILY:
+ $diff = $this->start->diff($after);
+ $recur = ceil($diff / $this->recurInterval);
+ if ($this->recurCount && $recur >= $this->recurCount) {
+ return false;
+ }
+
+ $recur *= $this->recurInterval;
+ $next = $this->start->add(array('day' => $recur));
+ if ((!$this->hasRecurEnd() ||
+ $next->compareDateTime($this->recurEnd) <= 0) &&
+ $next->compareDateTime($after) >= 0) {
+ return $next;
+ }
+ break;
+
+ case self::RECUR_WEEKLY:
+ if (empty($this->recurData)) {
+ return false;
+ }
+
+ $start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'),
+ $this->start->year);
+ $start_week->timezone = $this->start->timezone;
+ $start_week->hour = $this->start->hour;
+ $start_week->min = $this->start->min;
+ $start_week->sec = $this->start->sec;
+
+ // Make sure we are not at the ISO-8601 first week of year while
+ // still in month 12...OR in the ISO-8601 last week of year while
+ // in month 1 and adjust the year accordingly.
+ $week = $after->format('W');
+ if ($week == 1 && $after->month == 12) {
+ $theYear = $after->year + 1;
+ } elseif ($week >= 52 && $after->month == 1) {
+ $theYear = $after->year - 1;
+ } else {
+ $theYear = $after->year;
+ }
+
+ $after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear);
+ $after_week->timezone = $this->start->timezone;
+ $after_week_end = clone $after_week;
+ $after_week_end->mday += 7;
+
+ $diff = $start_week->diff($after_week);
+ $interval = $this->recurInterval * 7;
+ $repeats = floor($diff / $interval);
+ if ($diff % $interval < 7) {
+ $recur = $diff;
+ } else {
+ /**
+ * If the after_week is not in the first week interval the
+ * search needs to skip ahead a complete interval. The way it is
+ * calculated here means that an event that occurs every second
+ * week on Monday and Wednesday with the event actually starting
+ * on Tuesday or Wednesday will only have one incidence in the
+ * first week.
+ */
+ $recur = $interval * ($repeats + 1);
+ }
+
+ if ($this->hasRecurCount()) {
+ $recurrences = 0;
+ /**
+ * Correct the number of recurrences by the number of events
+ * that lay between the start of the start week and the
+ * recurrence start.
+ */
+ $next = clone $start_week;
+ while ($next->compareDateTime($this->start) < 0) {
+ if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
+ $recurrences--;
+ }
+ ++$next->mday;
+ }
+ if ($repeats > 0) {
+ $weekdays = $this->recurData;
+ $total_recurrences_per_week = 0;
+ while ($weekdays > 0) {
+ if ($weekdays % 2) {
+ $total_recurrences_per_week++;
+ }
+ $weekdays = ($weekdays - ($weekdays % 2)) / 2;
+ }
+ $recurrences += $total_recurrences_per_week * $repeats;
+ }
+ }
+
+ $next = clone $start_week;
+ $next->mday += $recur;
+ while ($next->compareDateTime($after) < 0 &&
+ $next->compareDateTime($after_week_end) < 0) {
+ if ($this->hasRecurCount()
+ && $next->compareDateTime($after) < 0
+ && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
+ $recurrences++;
+ }
+ ++$next->mday;
+ }
+ if ($this->hasRecurCount() &&
+ $recurrences >= $this->recurCount) {
+ return false;
+ }
+ if (!$this->hasRecurEnd() ||
+ $next->compareDateTime($this->recurEnd) <= 0) {
+ if ($next->compareDateTime($after_week_end) >= 0) {
+ return $this->nextRecurrence($after_week_end);
+ }
+ while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
+ $next->compareDateTime($after_week_end) < 0) {
+ ++$next->mday;
+ }
+ if (!$this->hasRecurEnd() ||
+ $next->compareDateTime($this->recurEnd) <= 0) {
+ if ($next->compareDateTime($after_week_end) >= 0) {
+ return $this->nextRecurrence($after_week_end);
+ } else {
+ return $next;
+ }
+ }
+ }
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $start = clone $this->start;
+ if ($after->compareDateTime($start) < 0) {
+ $after = clone $start;
+ } else {
+ $after = clone $after;
+ }
+
+ // If we're starting past this month's recurrence of the event,
+ // look in the next month on the day the event recurs.
+ if ($after->mday > $start->mday) {
+ ++$after->month;
+ $after->mday = $start->mday;
+ }
+
+ // Adjust $start to be the first match.
+ $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
+ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+ if ($this->recurCount &&
+ ($offset / $this->recurInterval) >= $this->recurCount) {
+ return false;
+ }
+ $start->month += $offset;
+ $count = $offset / $this->recurInterval;
+
+ do {
+ if ($this->recurCount &&
+ $count++ >= $this->recurCount) {
+ return false;
+ }
+
+ // Bail if we've gone past the end of recurrence.
+ if ($this->hasRecurEnd() &&
+ $this->recurEnd->compareDateTime($start) < 0) {
+ return false;
+ }
+ if ($start->isValid()) {
+ return $start;
+ }
+
+ // If the interval is 12, and the date isn't valid, then we
+ // need to see if February 29th is an option. If not, then the
+ // event will _never_ recur, and we need to stop checking to
+ // avoid an infinite loop.
+ if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
+ return false;
+ }
+
+ // Add the recurrence interval.
+ $start->month += $this->recurInterval;
+ } while (true);
+
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ // Start with the start date of the event.
+ $estart = clone $this->start;
+
+ // What day of the week, and week of the month, do we recur on?
+ if (isset($this->recurNthDay)) {
+ $nth = $this->recurNthDay;
+ $weekday = log($this->recurData, 2);
+ } else {
+ $nth = ceil($this->start->mday / 7);
+ $weekday = $estart->dayOfWeek();
+ }
+
+ // Adjust $estart to be the first candidate.
+ $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
+ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+ // Adjust our working date until it's after $after.
+ $estart->month += $offset - $this->recurInterval;
+
+ $count = $offset / $this->recurInterval;
+ do {
+ if ($this->recurCount &&
+ $count++ >= $this->recurCount) {
+ return false;
+ }
+
+ $estart->month += $this->recurInterval;
+
+ $next = clone $estart;
+ $next->setNthWeekday($weekday, $nth);
+
+ if ($next->compareDateTime($after) < 0) {
+ // We haven't made it past $after yet, try again.
+ continue;
+ }
+ if ($this->hasRecurEnd() &&
+ $next->compareDateTime($this->recurEnd) > 0) {
+ // We've gone past the end of recurrence; we can give up
+ // now.
+ return false;
+ }
+
+ // We have a candidate to return.
+ break;
+ } while (true);
+
+ return $next;
+
+ case self::RECUR_YEARLY_DATE:
+ // Start with the start date of the event.
+ $estart = clone $this->start;
+ $after = clone $after;
+
+ if ($after->month > $estart->month ||
+ ($after->month == $estart->month && $after->mday > $estart->mday)) {
+ ++$after->year;
+ $after->month = $estart->month;
+ $after->mday = $estart->mday;
+ }
+
+ // Seperate case here for February 29th
+ if ($estart->month == 2 && $estart->mday == 29) {
+ while (!Horde_Date_Utils::isLeapYear($after->year)) {
+ ++$after->year;
+ }
+ }
+
+ // Adjust $estart to be the first candidate.
+ $offset = $after->year - $estart->year;
+ if ($offset > 0) {
+ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+ $estart->year += $offset;
+ }
+
+ // We've gone past the end of recurrence; give up.
+ if ($this->recurCount &&
+ $offset >= $this->recurCount) {
+ return false;
+ }
+ if ($this->hasRecurEnd() &&
+ $this->recurEnd->compareDateTime($estart) < 0) {
+ return false;
+ }
+
+ return $estart;
+
+ case self::RECUR_YEARLY_DAY:
+ // Check count first.
+ $dayofyear = $this->start->dayOfYear();
+ $count = ($after->year - $this->start->year) / $this->recurInterval + 1;
+ if ($this->recurCount &&
+ ($count > $this->recurCount ||
+ ($count == $this->recurCount &&
+ $after->dayOfYear() > $dayofyear))) {
+ return false;
+ }
+
+ // Start with a rough interval.
+ $estart = clone $this->start;
+ $estart->year += floor($count - 1) * $this->recurInterval;
+
+ // Now add the difference to the required day of year.
+ $estart->mday += $dayofyear - $estart->dayOfYear();
+
+ // Add an interval if the estimation was wrong.
+ if ($estart->compareDate($after) < 0) {
+ $estart->year += $this->recurInterval;
+ $estart->mday += $dayofyear - $estart->dayOfYear();
+ }
+
+ // We've gone past the end of recurrence; give up.
+ if ($this->hasRecurEnd() &&
+ $this->recurEnd->compareDateTime($estart) < 0) {
+ return false;
+ }
+
+ return $estart;
+
+ case self::RECUR_YEARLY_WEEKDAY:
+ // Start with the start date of the event.
+ $estart = clone $this->start;
+
+ // What day of the week, and week of the month, do we recur on?
+ if (isset($this->recurNthDay)) {
+ $nth = $this->recurNthDay;
+ $weekday = log($this->recurData, 2);
+ } else {
+ $nth = ceil($this->start->mday / 7);
+ $weekday = $estart->dayOfWeek();
+ }
+
+ // Adjust $estart to be the first candidate.
+ $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+ // Adjust our working date until it's after $after.
+ $estart->year += $offset - $this->recurInterval;
+
+ $count = $offset / $this->recurInterval;
+ do {
+ if ($this->recurCount &&
+ $count++ >= $this->recurCount) {
+ return false;
+ }
+
+ $estart->year += $this->recurInterval;
+
+ $next = clone $estart;
+ $next->setNthWeekday($weekday, $nth);
+
+ if ($next->compareDateTime($after) < 0) {
+ // We haven't made it past $after yet, try again.
+ continue;
+ }
+ if ($this->hasRecurEnd() &&
+ $next->compareDateTime($this->recurEnd) > 0) {
+ // We've gone past the end of recurrence; we can give up
+ // now.
+ return false;
+ }
+
+ // We have a candidate to return.
+ break;
+ } while (true);
+
+ return $next;
+ }
+
+ // fall-back to RDATE properties
+ if (!empty($this->rdates)) {
+ $next = clone $this->start;
+ foreach ($this->rdates as $rdate) {
+ $next->year = $rdate->year;
+ $next->month = $rdate->month;
+ $next->mday = $rdate->mday;
+ if ($next->compareDateTime($after) >= 0) {
+ return $next;
+ }
+ }
+ }
+
+ // We didn't find anything, the recurType was bad, or something else
+ // went wrong - return false.
+ return false;
+ }
+
+ /**
+ * Returns whether this event has any date that matches the recurrence
+ * rules and is not an exception.
+ *
+ * @return boolean True if an active recurrence exists.
+ */
+ public function hasActiveRecurrence()
+ {
+ if (!$this->hasRecurEnd()) {
+ return true;
+ }
+
+ $next = $this->nextRecurrence(new Horde_Date($this->start));
+ while (is_object($next)) {
+ if (!$this->hasException($next->year, $next->month, $next->mday) &&
+ !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+ return true;
+ }
+
+ $next = $this->nextRecurrence($next->add(array('day' => 1)));
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the next active recurrence.
+ *
+ * @param Horde_Date $afterDate Return events after this date.
+ *
+ * @return Horde_Date|boolean The date of the next active
+ * recurrence or false if the event
+ * has no active recurrence after
+ * $afterDate.
+ */
+ public function nextActiveRecurrence($afterDate)
+ {
+ $next = $this->nextRecurrence($afterDate);
+ while (is_object($next)) {
+ if (!$this->hasException($next->year, $next->month, $next->mday) &&
+ !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+ return $next;
+ }
+ $next->mday++;
+ $next = $this->nextRecurrence($next);
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds an absolute recurrence date.
+ *
+ * @param integer $year The year of the instance.
+ * @param integer $month The month of the instance.
+ * @param integer $mday The day of the month of the instance.
+ */
+ public function addRDate($year, $month, $mday)
+ {
+ $this->rdates[] = new Horde_Date($year, $month, $mday);
+ }
+
+ /**
+ * Adds an exception to a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the exception.
+ */
+ public function addException($year, $month, $mday)
+ {
+ $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+ }
+
+ /**
+ * Deletes an exception from a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the exception.
+ */
+ public function deleteException($year, $month, $mday)
+ {
+ $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
+ if ($key !== false) {
+ unset($this->exceptions[$key]);
+ }
+ }
+
+ /**
+ * Checks if an exception exists for a given reccurence of an event.
+ *
+ * @param integer $year The year of the reucrance.
+ * @param integer $month The month of the reucrance.
+ * @param integer $mday The day of the month of the reucrance.
+ *
+ * @return boolean True if an exception exists for the given date.
+ */
+ public function hasException($year, $month, $mday)
+ {
+ return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+ $this->getExceptions());
+ }
+
+ /**
+ * Retrieves all the exceptions for this event.
+ *
+ * @return array Array containing the dates of all the exceptions in
+ * YYYYMMDD form.
+ */
+ public function getExceptions()
+ {
+ return $this->exceptions;
+ }
+
+ /**
+ * Adds a completion to a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the completion.
+ */
+ public function addCompletion($year, $month, $mday)
+ {
+ $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+ }
+
+ /**
+ * Deletes a completion from a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the completion.
+ */
+ public function deleteCompletion($year, $month, $mday)
+ {
+ $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
+ if ($key !== false) {
+ unset($this->completions[$key]);
+ }
+ }
+
+ /**
+ * Checks if a completion exists for a given reccurence of an event.
+ *
+ * @param integer $year The year of the reucrance.
+ * @param integer $month The month of the recurrance.
+ * @param integer $mday The day of the month of the recurrance.
+ *
+ * @return boolean True if a completion exists for the given date.
+ */
+ public function hasCompletion($year, $month, $mday)
+ {
+ return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+ $this->getCompletions());
+ }
+
+ /**
+ * Retrieves all the completions for this event.
+ *
+ * @return array Array containing the dates of all the completions in
+ * YYYYMMDD form.
+ */
+ public function getCompletions()
+ {
+ return $this->completions;
+ }
+
+ /**
+ * Parses a vCalendar 1.0 recurrence rule.
+ *
+ * @link http://www.imc.org/pdi/vcal-10.txt
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param string $rrule A vCalendar 1.0 conform RRULE value.
+ */
+ public function fromRRule10($rrule)
+ {
+ $this->reset();
+
+ if (!$rrule) {
+ return;
+ }
+
+ if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
+ // No recurrence data - event does not recur.
+ $this->setRecurType(self::RECUR_NONE);
+ }
+
+ // Always default the recurInterval to 1.
+ $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
+
+ $remainder = trim($matches[3]);
+
+ switch ($matches[1]) {
+ case 'D':
+ $this->setRecurType(self::RECUR_DAILY);
+ break;
+
+ case 'W':
+ $this->setRecurType(self::RECUR_WEEKLY);
+ if (!empty($remainder)) {
+ $mask = 0;
+ while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
+ $day = trim($matches[0]);
+ $remainder = substr($remainder, strlen($matches[0]));
+ $mask |= $maskdays[$day];
+ }
+ $this->setRecurOnDay($mask);
+ } else {
+ // Recur on the day of the week of the original recurrence.
+ $maskdays = array(
+ Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+ Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+ Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+ Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+ Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+ Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+ Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY,
+ );
+ $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+ }
+ break;
+
+ case 'MP':
+ $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+ break;
+
+ case 'MD':
+ $this->setRecurType(self::RECUR_MONTHLY_DATE);
+ break;
+
+ case 'YM':
+ $this->setRecurType(self::RECUR_YEARLY_DATE);
+ break;
+
+ case 'YD':
+ $this->setRecurType(self::RECUR_YEARLY_DAY);
+ break;
+ }
+
+ // We don't support modifiers at the moment, strip them.
+ while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
+ $remainder = substr($remainder, 1);
+ }
+ if (!empty($remainder)) {
+ if (strpos($remainder, '#') === 0) {
+ $this->setRecurCount(substr($remainder, 1));
+ } else {
+ list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
+ $this->setRecurEnd(new Horde_Date(array('year' => $year,
+ 'month' => $month,
+ 'mday' => $mday,
+ 'hour' => 23,
+ 'min' => 59,
+ 'sec' => 59)));
+ }
+ }
+ }
+
+ /**
+ * Creates a vCalendar 1.0 recurrence rule.
+ *
+ * @link http://www.imc.org/pdi/vcal-10.txt
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param Horde_Icalendar $calendar A Horde_Icalendar object instance.
+ *
+ * @return string A vCalendar 1.0 conform RRULE value.
+ */
+ public function toRRule10($calendar)
+ {
+ switch ($this->recurType) {
+ case self::RECUR_NONE:
+ return '';
+
+ case self::RECUR_DAILY:
+ $rrule = 'D' . $this->recurInterval;
+ break;
+
+ case self::RECUR_WEEKLY:
+ $rrule = 'W' . $this->recurInterval;
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+ for ($i = 0; $i <= 7; ++$i) {
+ if ($this->recurOnDay(pow(2, $i))) {
+ $rrule .= ' ' . $vcaldays[$i];
+ }
+ }
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ $nth_weekday = (int)($this->start->mday / 7);
+ if (($this->start->mday % 7) > 0) {
+ $nth_weekday++;
+ }
+
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+ $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
+
+ break;
+
+ case self::RECUR_YEARLY_DATE:
+ $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
+ break;
+
+ case self::RECUR_YEARLY_DAY:
+ $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
+ break;
+
+ default:
+ return '';
+ }
+
+ if ($this->hasRecurEnd()) {
+ $recurEnd = clone $this->recurEnd;
+ return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
+ }
+
+ return $rrule . ' #' . (int)$this->getRecurCount();
+ }
+
+ /**
+ * Parses an iCalendar 2.0 recurrence rule.
+ *
+ * @link http://rfc.net/rfc2445.html#s4.3.10
+ * @link http://rfc.net/rfc2445.html#s4.8.5
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param string $rrule An iCalendar 2.0 conform RRULE value.
+ */
+ public function fromRRule20($rrule)
+ {
+ $this->reset();
+
+ // Parse the recurrence rule into keys and values.
+ $rdata = array();
+ $parts = explode(';', $rrule);
+ foreach ($parts as $part) {
+ list($key, $value) = explode('=', $part, 2);
+ $rdata[strtoupper($key)] = $value;
+ }
+
+ if (isset($rdata['FREQ'])) {
+ // Always default the recurInterval to 1.
+ $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
+
+ $maskdays = array(
+ 'SU' => Horde_Date::MASK_SUNDAY,
+ 'MO' => Horde_Date::MASK_MONDAY,
+ 'TU' => Horde_Date::MASK_TUESDAY,
+ 'WE' => Horde_Date::MASK_WEDNESDAY,
+ 'TH' => Horde_Date::MASK_THURSDAY,
+ 'FR' => Horde_Date::MASK_FRIDAY,
+ 'SA' => Horde_Date::MASK_SATURDAY,
+ );
+
+ switch (strtoupper($rdata['FREQ'])) {
+ case 'DAILY':
+ $this->setRecurType(self::RECUR_DAILY);
+ break;
+
+ case 'WEEKLY':
+ $this->setRecurType(self::RECUR_WEEKLY);
+ if (isset($rdata['BYDAY'])) {
+ $days = explode(',', $rdata['BYDAY']);
+ $mask = 0;
+ foreach ($days as $day) {
+ $mask |= $maskdays[$day];
+ }
+ $this->setRecurOnDay($mask);
+ } else {
+ // Recur on the day of the week of the original
+ // recurrence.
+ $maskdays = array(
+ Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+ Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+ Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+ Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+ Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+ Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+ Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
+ $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+ }
+ break;
+
+ case 'MONTHLY':
+ if (isset($rdata['BYDAY'])) {
+ $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+ if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
+ $this->setRecurOnDay($maskdays[$m[2]]);
+ $this->setRecurNthWeekday($m[1]);
+ }
+ } else {
+ $this->setRecurType(self::RECUR_MONTHLY_DATE);
+ }
+ break;
+
+ case 'YEARLY':
+ if (isset($rdata['BYYEARDAY'])) {
+ $this->setRecurType(self::RECUR_YEARLY_DAY);
+ } elseif (isset($rdata['BYDAY'])) {
+ $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+ if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
+ $this->setRecurOnDay($maskdays[$m[2]]);
+ $this->setRecurNthWeekday($m[1]);
+ }
+ if ($rdata['BYMONTH']) {
+ $months = explode(',', $rdata['BYMONTH']);
+ $this->setRecurByMonth($months);
+ }
+ } else {
+ $this->setRecurType(self::RECUR_YEARLY_DATE);
+ }
+ break;
+ }
+
+ if (isset($rdata['UNTIL'])) {
+ list($year, $month, $mday) = sscanf($rdata['UNTIL'],
+ '%04d%02d%02d');
+ $this->setRecurEnd(new Horde_Date(array('year' => $year,
+ 'month' => $month,
+ 'mday' => $mday,
+ 'hour' => 23,
+ 'min' => 59,
+ 'sec' => 59)));
+ }
+ if (isset($rdata['COUNT'])) {
+ $this->setRecurCount($rdata['COUNT']);
+ }
+ } else {
+ // No recurrence data - event does not recur.
+ $this->setRecurType(self::RECUR_NONE);
+ }
+ }
+
+ /**
+ * Creates an iCalendar 2.0 recurrence rule.
+ *
+ * @link http://rfc.net/rfc2445.html#s4.3.10
+ * @link http://rfc.net/rfc2445.html#s4.8.5
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param Horde_Icalendar $calendar A Horde_Icalendar object instance.
+ *
+ * @return string An iCalendar 2.0 conform RRULE value.
+ */
+ public function toRRule20($calendar)
+ {
+ switch ($this->recurType) {
+ case self::RECUR_NONE:
+ return '';
+
+ case self::RECUR_DAILY:
+ $rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval;
+ break;
+
+ case self::RECUR_WEEKLY:
+ $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+ for ($i = $flag = 0; $i <= 7; ++$i) {
+ if ($this->recurOnDay(pow(2, $i))) {
+ if ($flag) {
+ $rrule .= ',';
+ }
+ $rrule .= $vcaldays[$i];
+ $flag = true;
+ }
+ }
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ if (isset($this->recurNthDay)) {
+ $nth_weekday = $this->recurNthDay;
+ $day_of_week = log($this->recurData, 2);
+ } else {
+ $day_of_week = $this->start->dayOfWeek();
+ $nth_weekday = (int)($this->start->mday / 7);
+ if (($this->start->mday % 7) > 0) {
+ $nth_weekday++;
+ }
+ }
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+ $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
+ . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
+ break;
+
+ case self::RECUR_YEARLY_DATE:
+ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
+ break;
+
+ case self::RECUR_YEARLY_DAY:
+ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+ . ';BYYEARDAY=' . $this->start->dayOfYear();
+ break;
+
+ case self::RECUR_YEARLY_WEEKDAY:
+ if (isset($this->recurNthDay)) {
+ $nth_weekday = $this->recurNthDay;
+ $day_of_week = log($this->recurData, 2);
+ } else {
+ $day_of_week = $this->start->dayOfWeek();
+ $nth_weekday = (int)($this->start->mday / 7);
+ if (($this->start->mday % 7) > 0) {
+ $nth_weekday++;
+ }
+ }
+ $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+ . ';BYDAY='
+ . $nth_weekday
+ . $vcaldays[$day_of_week]
+ . ';BYMONTH=' . $this->start->month;
+ break;
+ }
+
+ if ($this->hasRecurEnd()) {
+ $recurEnd = clone $this->recurEnd;
+ $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
+ }
+ if ($count = $this->getRecurCount()) {
+ $rrule .= ';COUNT=' . $count;
+ }
+ return $rrule;
+ }
+
+ /**
+ * Parses the recurrence data from a hash.
+ *
+ * @param array $hash The hash to convert.
+ *
+ * @return boolean True if the hash seemed valid, false otherwise.
+ */
+ public function fromHash($hash)
+ {
+ $this->reset();
+
+ if (!isset($hash['interval']) || !isset($hash['cycle'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurInterval((int)$hash['interval']);
+
+ $month2number = array(
+ 'january' => 1,
+ 'february' => 2,
+ 'march' => 3,
+ 'april' => 4,
+ 'may' => 5,
+ 'june' => 6,
+ 'july' => 7,
+ 'august' => 8,
+ 'september' => 9,
+ 'october' => 10,
+ 'november' => 11,
+ 'december' => 12,
+ );
+
+ $parse_day = false;
+ $set_daymask = false;
+ $update_month = false;
+ $update_daynumber = false;
+ $update_weekday = false;
+ $nth_weekday = -1;
+
+ switch ($hash['cycle']) {
+ case 'daily':
+ $this->setRecurType(self::RECUR_DAILY);
+ break;
+
+ case 'weekly':
+ $this->setRecurType(self::RECUR_WEEKLY);
+ $parse_day = true;
+ $set_daymask = true;
+ break;
+
+ case 'monthly':
+ if (!isset($hash['daynumber'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ switch ($hash['type']) {
+ case 'daynumber':
+ $this->setRecurType(self::RECUR_MONTHLY_DATE);
+ $update_daynumber = true;
+ break;
+
+ case 'weekday':
+ $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+ $this->setRecurNthWeekday($hash['daynumber']);
+ $parse_day = true;
+ $set_daymask = true;
+ break;
+ }
+ break;
+
+ case 'yearly':
+ if (!isset($hash['type'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ switch ($hash['type']) {
+ case 'monthday':
+ $this->setRecurType(self::RECUR_YEARLY_DATE);
+ $update_month = true;
+ $update_daynumber = true;
+ break;
+
+ case 'yearday':
+ if (!isset($hash['month'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurType(self::RECUR_YEARLY_DAY);
+ // Start counting days in January.
+ $hash['month'] = 'january';
+ $update_month = true;
+ $update_daynumber = true;
+ break;
+
+ case 'weekday':
+ if (!isset($hash['daynumber'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+ $this->setRecurNthWeekday($hash['daynumber']);
+ $parse_day = true;
+ $set_daymask = true;
+
+ if ($hash['month'] && isset($month2number[$hash['month']])) {
+ $this->setRecurByMonth($month2number[$hash['month']]);
+ }
+ break;
+ }
+ }
+
+ if (isset($hash['range-type']) && isset($hash['range'])) {
+ switch ($hash['range-type']) {
+ case 'number':
+ $this->setRecurCount((int)$hash['range']);
+ break;
+
+ case 'date':
+ $recur_end = new Horde_Date($hash['range']);
+ $recur_end->hour = 23;
+ $recur_end->min = 59;
+ $recur_end->sec = 59;
+ $this->setRecurEnd($recur_end);
+ break;
+ }
+ }
+
+ // Need to parse <day>?
+ $last_found_day = -1;
+ if ($parse_day) {
+ if (!isset($hash['day'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $mask = 0;
+ $bits = array(
+ 'monday' => Horde_Date::MASK_MONDAY,
+ 'tuesday' => Horde_Date::MASK_TUESDAY,
+ 'wednesday' => Horde_Date::MASK_WEDNESDAY,
+ 'thursday' => Horde_Date::MASK_THURSDAY,
+ 'friday' => Horde_Date::MASK_FRIDAY,
+ 'saturday' => Horde_Date::MASK_SATURDAY,
+ 'sunday' => Horde_Date::MASK_SUNDAY,
+ );
+ $days = array(
+ 'monday' => Horde_Date::DATE_MONDAY,
+ 'tuesday' => Horde_Date::DATE_TUESDAY,
+ 'wednesday' => Horde_Date::DATE_WEDNESDAY,
+ 'thursday' => Horde_Date::DATE_THURSDAY,
+ 'friday' => Horde_Date::DATE_FRIDAY,
+ 'saturday' => Horde_Date::DATE_SATURDAY,
+ 'sunday' => Horde_Date::DATE_SUNDAY,
+ );
+
+ foreach ($hash['day'] as $day) {
+ // Validity check.
+ if (empty($day) || !isset($bits[$day])) {
+ continue;
+ }
+
+ $mask |= $bits[$day];
+ $last_found_day = $days[$day];
+ }
+
+ if ($set_daymask) {
+ $this->setRecurOnDay($mask);
+ }
+ }
+
+ if ($update_month || $update_daynumber || $update_weekday) {
+ if ($update_month) {
+ if (isset($month2number[$hash['month']])) {
+ $this->start->month = $month2number[$hash['month']];
+ }
+ }
+
+ if ($update_daynumber) {
+ if (!isset($hash['daynumber'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->start->mday = $hash['daynumber'];
+ }
+
+ if ($update_weekday) {
+ $this->setNthWeekday($nth_weekday);
+ }
+ }
+
+ // Exceptions.
+ if (isset($hash['exceptions'])) {
+ $this->exceptions = $hash['exceptions'];
+ }
+
+ if (isset($hash['completions'])) {
+ $this->completions = $hash['completions'];
+ }
+
+ return true;
+ }
+
+ /**
+ * Export this object into a hash.
+ *
+ * @return array The recurrence hash.
+ */
+ public function toHash()
+ {
+ if ($this->getRecurType() == self::RECUR_NONE) {
+ return array();
+ }
+
+ $day2number = array(
+ 0 => 'sunday',
+ 1 => 'monday',
+ 2 => 'tuesday',
+ 3 => 'wednesday',
+ 4 => 'thursday',
+ 5 => 'friday',
+ 6 => 'saturday'
+ );
+ $month2number = array(
+ 1 => 'january',
+ 2 => 'february',
+ 3 => 'march',
+ 4 => 'april',
+ 5 => 'may',
+ 6 => 'june',
+ 7 => 'july',
+ 8 => 'august',
+ 9 => 'september',
+ 10 => 'october',
+ 11 => 'november',
+ 12 => 'december'
+ );
+
+ $hash = array('interval' => $this->getRecurInterval());
+ $start = $this->getRecurStart();
+
+ switch ($this->getRecurType()) {
+ case self::RECUR_DAILY:
+ $hash['cycle'] = 'daily';
+ break;
+
+ case self::RECUR_WEEKLY:
+ $hash['cycle'] = 'weekly';
+ $bits = array(
+ 'monday' => Horde_Date::MASK_MONDAY,
+ 'tuesday' => Horde_Date::MASK_TUESDAY,
+ 'wednesday' => Horde_Date::MASK_WEDNESDAY,
+ 'thursday' => Horde_Date::MASK_THURSDAY,
+ 'friday' => Horde_Date::MASK_FRIDAY,
+ 'saturday' => Horde_Date::MASK_SATURDAY,
+ 'sunday' => Horde_Date::MASK_SUNDAY,
+ );
+ $days = array();
+ foreach ($bits as $name => $bit) {
+ if ($this->recurOnDay($bit)) {
+ $days[] = $name;
+ }
+ }
+ $hash['day'] = $days;
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $hash['cycle'] = 'monthly';
+ $hash['type'] = 'daynumber';
+ $hash['daynumber'] = $start->mday;
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ $hash['cycle'] = 'monthly';
+ $hash['type'] = 'weekday';
+ $hash['daynumber'] = $start->weekOfMonth();
+ $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+ break;
+
+ case self::RECUR_YEARLY_DATE:
+ $hash['cycle'] = 'yearly';
+ $hash['type'] = 'monthday';
+ $hash['daynumber'] = $start->mday;
+ $hash['month'] = $month2number[$start->month];
+ break;
+
+ case self::RECUR_YEARLY_DAY:
+ $hash['cycle'] = 'yearly';
+ $hash['type'] = 'yearday';
+ $hash['daynumber'] = $start->dayOfYear();
+ break;
+
+ case self::RECUR_YEARLY_WEEKDAY:
+ $hash['cycle'] = 'yearly';
+ $hash['type'] = 'weekday';
+ $hash['daynumber'] = $start->weekOfMonth();
+ $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+ $hash['month'] = $month2number[$start->month];
+ }
+
+ if ($this->hasRecurCount()) {
+ $hash['range-type'] = 'number';
+ $hash['range'] = $this->getRecurCount();
+ } elseif ($this->hasRecurEnd()) {
+ $date = $this->getRecurEnd();
+ $hash['range-type'] = 'date';
+ $hash['range'] = $date->datestamp();
+ } else {
+ $hash['range-type'] = 'none';
+ $hash['range'] = '';
+ }
+
+ // Recurrence exceptions
+ $hash['exceptions'] = $this->exceptions;
+ $hash['completions'] = $this->completions;
+
+ return $hash;
+ }
+
+ /**
+ * Returns a simple object suitable for json transport representing this
+ * object.
+ *
+ * Possible properties are:
+ * - t: type
+ * - i: interval
+ * - e: end date
+ * - c: count
+ * - d: data
+ * - co: completions
+ * - ex: exceptions
+ *
+ * @return object A simple object.
+ */
+ public function toJson()
+ {
+ $json = new stdClass;
+ $json->t = $this->recurType;
+ $json->i = $this->recurInterval;
+ if ($this->hasRecurEnd()) {
+ $json->e = $this->recurEnd->toJson();
+ }
+ if ($this->recurCount) {
+ $json->c = $this->recurCount;
+ }
+ if ($this->recurData) {
+ $json->d = $this->recurData;
+ }
+ if ($this->completions) {
+ $json->co = $this->completions;
+ }
+ if ($this->exceptions) {
+ $json->ex = $this->exceptions;
+ }
+ return $json;
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Component.php b/libcalendaring/lib/OldSabre/VObject/Component.php
new file mode 100644
index 0000000..ba41176
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component.php
@@ -0,0 +1,405 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Component
+ *
+ * This class represents a VCALENDAR/VCARD component. A component is for example
+ * VEVENT, VTODO and also VCALENDAR. It starts with BEGIN:COMPONENTNAME and
+ * ends with END:COMPONENTNAME
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Component extends Node {
+
+ /**
+ * Name, for example VEVENT
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Children properties and components
+ *
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * If components are added to this map, they will be automatically mapped
+ * to their respective classes, if parsed by the reader or constructed with
+ * the 'create' method.
+ *
+ * @var array
+ */
+ static public $classMap = array(
+ 'VALARM' => 'OldSabre\\VObject\\Component\\VAlarm',
+ 'VCALENDAR' => 'OldSabre\\VObject\\Component\\VCalendar',
+ 'VCARD' => 'OldSabre\\VObject\\Component\\VCard',
+ 'VEVENT' => 'OldSabre\\VObject\\Component\\VEvent',
+ 'VJOURNAL' => 'OldSabre\\VObject\\Component\\VJournal',
+ 'VTODO' => 'OldSabre\\VObject\\Component\\VTodo',
+ 'VFREEBUSY' => 'OldSabre\\VObject\\Component\\VFreeBusy',
+ );
+
+ /**
+ * Creates the new component by name, but in addition will also see if
+ * there's a class mapped to the property name.
+ *
+ * @param string $name
+ * @param string $value
+ * @return Component
+ */
+ static public function create($name, $value = null) {
+
+ $name = strtoupper($name);
+
+ if (isset(self::$classMap[$name])) {
+ return new self::$classMap[$name]($name, $value);
+ } else {
+ return new self($name, $value);
+ }
+
+ }
+
+ /**
+ * Creates a new component.
+ *
+ * By default this object will iterate over its own children, but this can
+ * be overridden with the iterator argument
+ *
+ * @param string $name
+ * @param ElementList $iterator
+ */
+ public function __construct($name, ElementList $iterator = null) {
+
+ $this->name = strtoupper($name);
+ if (!is_null($iterator)) $this->iterator = $iterator;
+
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ $str = "BEGIN:" . $this->name . "\r\n";
+
+ /**
+ * Gives a component a 'score' for sorting purposes.
+ *
+ * This is solely used by the childrenSort method.
+ *
+ * A higher score means the item will be lower in the list.
+ * To avoid score collisions, each "score category" has a reasonable
+ * space to accomodate elements. The $key is added to the $score to
+ * preserve the original relative order of elements.
+ *
+ * @param int $key
+ * @param array $array
+ * @return int
+ */
+ $sortScore = function($key, $array) {
+
+ if ($array[$key] instanceof Component) {
+
+ // We want to encode VTIMEZONE first, this is a personal
+ // preference.
+ if ($array[$key]->name === 'VTIMEZONE') {
+ $score=300000000;
+ return $score+$key;
+ } else {
+ $score=400000000;
+ return $score+$key;
+ }
+ } else {
+ // Properties get encoded first
+ // VCARD version 4.0 wants the VERSION property to appear first
+ if ($array[$key] instanceof Property) {
+ if ($array[$key]->name === 'VERSION') {
+ $score=100000000;
+ return $score+$key;
+ } else {
+ // All other properties
+ $score=200000000;
+ return $score+$key;
+ }
+ }
+ }
+
+ };
+
+ $tmp = $this->children;
+ uksort($this->children, function($a, $b) use ($sortScore, $tmp) {
+
+ $sA = $sortScore($a, $tmp);
+ $sB = $sortScore($b, $tmp);
+
+ if ($sA === $sB) return 0;
+
+ return ($sA < $sB) ? -1 : 1;
+
+ });
+
+ foreach($this->children as $child) $str.=$child->serialize();
+ $str.= "END:" . $this->name . "\r\n";
+
+ return $str;
+
+ }
+
+ /**
+ * Adds a new component or element
+ *
+ * You can call this method with the following syntaxes:
+ *
+ * add(Node $node)
+ * add(string $name, $value, array $parameters = array())
+ *
+ * The first version adds an Element
+ * The second adds a property as a string.
+ *
+ * @param mixed $item
+ * @param mixed $itemValue
+ * @return void
+ */
+ public function add($item, $itemValue = null, array $parameters = array()) {
+
+ if ($item instanceof Node) {
+ if (!is_null($itemValue)) {
+ throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
+ }
+ $item->parent = $this;
+ $this->children[] = $item;
+ } elseif(is_string($item)) {
+
+ $item = Property::create($item,$itemValue, $parameters);
+ $item->parent = $this;
+ $this->children[] = $item;
+
+ } else {
+
+ throw new \InvalidArgumentException('The first argument must either be a \\OldSabre\\VObject\\Node or a string');
+
+ }
+
+ }
+
+ /**
+ * Returns an iterable list of children
+ *
+ * @return ElementList
+ */
+ public function children() {
+
+ return new ElementList($this->children);
+
+ }
+
+ /**
+ * Returns an array with elements that match the specified name.
+ *
+ * This function is also aware of MIME-Directory groups (as they appear in
+ * vcards). This means that if a property is grouped as "HOME.EMAIL", it
+ * will also be returned when searching for just "EMAIL". If you want to
+ * search for a property in a specific group, you can select on the entire
+ * string ("HOME.EMAIL"). If you want to search on a specific property that
+ * has not been assigned a group, specify ".EMAIL".
+ *
+ * Keys are retained from the 'children' array, which may be confusing in
+ * certain cases.
+ *
+ * @param string $name
+ * @return array
+ */
+ public function select($name) {
+
+ $group = null;
+ $name = strtoupper($name);
+ if (strpos($name,'.')!==false) {
+ list($group,$name) = explode('.', $name, 2);
+ }
+
+ $result = array();
+ foreach($this->children as $key=>$child) {
+
+ if (
+ strtoupper($child->name) === $name &&
+ (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group))
+ ) {
+
+ $result[$key] = $child;
+
+ }
+ }
+
+ reset($result);
+ return $result;
+
+ }
+
+ /**
+ * This method only returns a list of sub-components. Properties are
+ * ignored.
+ *
+ * @return array
+ */
+ public function getComponents() {
+
+ $result = array();
+ foreach($this->children as $child) {
+ if ($child instanceof Component) {
+ $result[] = $child;
+ }
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $result = array();
+ foreach($this->children as $child) {
+ $result = array_merge($result, $child->validate($options));
+ }
+ return $result;
+
+ }
+
+ /* Magic property accessors {{{ */
+
+ /**
+ * Using 'get' you will either get a property or component,
+ *
+ * If there were no child-elements found with the specified name,
+ * null is returned.
+ *
+ * @param string $name
+ * @return Property
+ */
+ public function __get($name) {
+
+ $matches = $this->select($name);
+ if (count($matches)===0) {
+ return null;
+ } else {
+ $firstMatch = current($matches);
+ /** @var $firstMatch Property */
+ $firstMatch->setIterator(new ElementList(array_values($matches)));
+ return $firstMatch;
+ }
+
+ }
+
+ /**
+ * This method checks if a sub-element with the specified name exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function __isset($name) {
+
+ $matches = $this->select($name);
+ return count($matches)>0;
+
+ }
+
+ /**
+ * Using the setter method you can add properties or subcomponents
+ *
+ * You can either pass a Component, Property
+ * object, or a string to automatically create a Property.
+ *
+ * If the item already exists, it will be removed. If you want to add
+ * a new item with the same name, always use the add() method.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function __set($name, $value) {
+
+ $matches = $this->select($name);
+ $overWrite = count($matches)?key($matches):null;
+
+ if ($value instanceof Component || $value instanceof Property) {
+ $value->parent = $this;
+ if (!is_null($overWrite)) {
+ $this->children[$overWrite] = $value;
+ } else {
+ $this->children[] = $value;
+ }
+ } elseif (is_scalar($value)) {
+ $property = Property::create($name,$value);
+ $property->parent = $this;
+ if (!is_null($overWrite)) {
+ $this->children[$overWrite] = $property;
+ } else {
+ $this->children[] = $property;
+ }
+ } else {
+ throw new \InvalidArgumentException('You must pass a \\OldSabre\\VObject\\Component, \\OldSabre\\VObject\\Property or scalar type');
+ }
+
+ }
+
+ /**
+ * Removes all properties and components within this component.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function __unset($name) {
+
+ $matches = $this->select($name);
+ foreach($matches as $k=>$child) {
+
+ unset($this->children[$k]);
+ $child->parent = null;
+
+ }
+
+ }
+
+ /* }}} */
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ *
+ * @return void
+ */
+ public function __clone() {
+
+ foreach($this->children as $key=>$child) {
+ $this->children[$key] = clone $child;
+ $this->children[$key]->parent = $this;
+ }
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php b/libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php
new file mode 100644
index 0000000..4cae670
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+use OldSabre\VObject;
+
+/**
+ * VAlarm component
+ *
+ * This component contains some additional functionality specific for VALARMs.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VAlarm extends VObject\Component {
+
+ /**
+ * Returns a DateTime object when this alarm is going to trigger.
+ *
+ * This ignores repeated alarm, only the first trigger is returned.
+ *
+ * @return DateTime
+ */
+ public function getEffectiveTriggerTime() {
+
+ $trigger = $this->TRIGGER;
+ if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') {
+ $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
+ $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START';
+
+ $parentComponent = $this->parent;
+ if ($related === 'START') {
+
+ if ($parentComponent->name === 'VTODO') {
+ $propName = 'DUE';
+ } else {
+ $propName = 'DTSTART';
+ }
+
+ $effectiveTrigger = clone $parentComponent->$propName->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ } else {
+ if ($parentComponent->name === 'VTODO') {
+ $endProp = 'DUE';
+ } elseif ($parentComponent->name === 'VEVENT') {
+ $endProp = 'DTEND';
+ } else {
+ throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
+ }
+
+ if (isset($parentComponent->$endProp)) {
+ $effectiveTrigger = clone $parentComponent->$endProp->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ } elseif (isset($parentComponent->DURATION)) {
+ $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+ $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
+ $effectiveTrigger->add($duration);
+ $effectiveTrigger->add($triggerDuration);
+ } else {
+ $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ }
+ }
+ } else {
+ $effectiveTrigger = $trigger->getDateTime();
+ }
+ return $effectiveTrigger;
+
+ }
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $effectiveTrigger = $this->getEffectiveTriggerTime();
+
+ if (isset($this->DURATION)) {
+ $duration = VObject\DateTimeParser::parseDuration($this->DURATION);
+ $repeat = (string)$this->repeat;
+ if (!$repeat) {
+ $repeat = 1;
+ }
+
+ $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat);
+
+ foreach($period as $occurrence) {
+
+ if ($start <= $occurrence && $end > $occurrence) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return ($start <= $effectiveTrigger && $end > $effectiveTrigger);
+ }
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php b/libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php
new file mode 100644
index 0000000..2b1446f
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VCalendar component
+ *
+ * This component adds functionality to a component, specific for a VCALENDAR.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VCalendar extends VObject\Document {
+
+ static $defaultName = 'VCALENDAR';
+
+ /**
+ * Returns a list of all 'base components'. For instance, if an Event has
+ * a recurrence rule, and one instance is overridden, the overridden event
+ * will have the same UID, but will be excluded from this list.
+ *
+ * VTIMEZONE components will always be excluded.
+ *
+ * @param string $componentName filter by component name
+ * @return array
+ */
+ public function getBaseComponents($componentName = null) {
+
+ $components = array();
+ foreach($this->children as $component) {
+
+ if (!$component instanceof VObject\Component)
+ continue;
+
+ if (isset($component->{'RECURRENCE-ID'}))
+ continue;
+
+ if ($componentName && $component->name !== strtoupper($componentName))
+ continue;
+
+ if ($component->name === 'VTIMEZONE')
+ continue;
+
+ $components[] = $component;
+
+ }
+
+ return $components;
+
+ }
+
+ /**
+ * If this calendar object, has events with recurrence rules, this method
+ * can be used to expand the event into multiple sub-events.
+ *
+ * Each event will be stripped from it's recurrence information, and only
+ * the instances of the event in the specified timerange will be left
+ * alone.
+ *
+ * In addition, this method will cause timezone information to be stripped,
+ * and normalized to UTC.
+ *
+ * This method will alter the VCalendar. This cannot be reversed.
+ *
+ * This functionality is specifically used by the CalDAV standard. It is
+ * possible for clients to request expand events, if they are rather simple
+ * clients and do not have the possibility to calculate recurrences.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return void
+ */
+ public function expand(\DateTime $start, \DateTime $end) {
+
+ $newEvents = array();
+
+ foreach($this->select('VEVENT') as $key=>$vevent) {
+
+ if (isset($vevent->{'RECURRENCE-ID'})) {
+ unset($this->children[$key]);
+ continue;
+ }
+
+
+ if (!$vevent->rrule) {
+ unset($this->children[$key]);
+ if ($vevent->isInTimeRange($start, $end)) {
+ $newEvents[] = $vevent;
+ }
+ continue;
+ }
+
+ $uid = (string)$vevent->uid;
+ if (!$uid) {
+ throw new \LogicException('Event did not have a UID!');
+ }
+
+ $it = new VObject\RecurrenceIterator($this, $vevent->uid);
+ $it->fastForward($start);
+
+ while($it->valid() && $it->getDTStart() < $end) {
+
+ if ($it->getDTEnd() > $start) {
+
+ $newEvents[] = $it->getEventObject();
+
+ }
+ $it->next();
+
+ }
+ unset($this->children[$key]);
+
+ }
+
+ foreach($newEvents as $newEvent) {
+
+ foreach($newEvent->children as $child) {
+ if ($child instanceof VObject\Property\DateTime &&
+ $child->getDateType() == VObject\Property\DateTime::LOCALTZ) {
+ $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC);
+ }
+ }
+
+ $this->add($newEvent);
+
+ }
+
+ // Removing all VTIMEZONE components
+ unset($this->VTIMEZONE);
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @return array
+ */
+ /*
+ public function validate() {
+
+ $warnings = array();
+
+ $version = $this->select('VERSION');
+ if (count($version)!==1) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time',
+ 'node' => $this,
+ );
+ } else {
+ if ((string)$this->VERSION !== '2.0') {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
+ 'node' => $this,
+ );
+ }
+ }
+ $version = $this->select('PRODID');
+ if (count($version)!==1) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time',
+ 'node' => $this,
+ );
+ }
+ if (count($this->CALSCALE) > 1) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The CALSCALE property must not be specified more than once.',
+ 'node' => $this,
+ );
+ }
+ if (count($this->METHOD) > 1) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The METHOD property must not be specified more than once.',
+ 'node' => $this,
+ );
+ }
+
+ $allowedComponents = array(
+ 'VEVENT',
+ 'VTODO',
+ 'VJOURNAL',
+ 'VFREEBUSY',
+ 'VTIMEZONE',
+ );
+ $allowedProperties = array(
+ 'PRODID',
+ 'VERSION',
+ 'CALSCALE',
+ 'METHOD',
+ );
+ $componentsFound = 0;
+ foreach($this->children as $child) {
+ if($child instanceof Component) {
+ $componentsFound++;
+ if (!in_array($child->name, $allowedComponents)) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component",
+ 'node' => $this,
+ );
+ }
+ }
+ if ($child instanceof Property) {
+ if (!in_array($child->name, $allowedProperties)) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component",
+ 'node' => $this,
+ );
+ }
+ }
+ }
+
+ if ($componentsFound===0) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'An iCalendar object must have at least 1 component.',
+ 'node' => $this,
+ );
+ }
+
+ return array_merge(
+ $warnings,
+ parent::validate()
+ );
+
+ }
+ */
+
+}
+
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VCard.php b/libcalendaring/lib/OldSabre/VObject/Component/VCard.php
new file mode 100644
index 0000000..93ef03f
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VCard.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VCard component
+ *
+ * This component represents the BEGIN:VCARD and END:VCARD found in every
+ * vcard.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VCard extends VObject\Component {
+
+ static $defaultName = 'VCARD';
+
+ /**
+ * VCards with version 2.1, 3.0 and 4.0 are found.
+ *
+ * If the VCARD doesn't know its version, 4.0 is assumed.
+ */
+ const DEFAULT_VERSION = '4.0';
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $warnings = array();
+
+ $version = $this->select('VERSION');
+ if (count($version)!==1) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ $this->VERSION = self::DEFAULT_VERSION;
+ }
+ } else {
+ $version = (string)$this->VERSION;
+ if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ $this->VERSION = '4.0';
+ }
+ }
+
+ }
+ $fn = $this->select('FN');
+ if (count($fn)!==1) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The FN property must appear in the VCARD component exactly 1 time',
+ 'node' => $this,
+ );
+ if (($options & self::REPAIR) && count($fn) === 0) {
+ // We're going to try to see if we can use the contents of the
+ // N property.
+ if (isset($this->N)) {
+ $value = explode(';', (string)$this->N);
+ if (isset($value[1]) && $value[1]) {
+ $this->FN = $value[1] . ' ' . $value[0];
+ } else {
+ $this->FN = $value[0];
+ }
+
+ // Otherwise, the ORG property may work
+ } elseif (isset($this->ORG)) {
+ $this->FN = (string)$this->ORG;
+ }
+
+ }
+ }
+
+ return array_merge(
+ parent::validate($options),
+ $warnings
+ );
+
+ }
+
+}
+
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VEvent.php b/libcalendaring/lib/OldSabre/VObject/Component/VEvent.php
new file mode 100644
index 0000000..1532110
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VEvent.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+use OldSabre\VObject;
+
+/**
+ * VEvent component
+ *
+ * This component contains some additional functionality specific for VEVENT's.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VEvent extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ if ($this->RRULE) {
+ $it = new VObject\RecurrenceIterator($this);
+ $it->fastForward($start);
+
+ // We fast-forwarded to a spot where the end-time of the
+ // recurrence instance exceeded the start of the requested
+ // time-range.
+ //
+ // If the starttime of the recurrence did not exceed the
+ // end of the time range as well, we have a match.
+ return ($it->getDTStart() < $end && $it->getDTEnd() > $start);
+
+ }
+
+ $effectiveStart = $this->DTSTART->getDateTime();
+ if (isset($this->DTEND)) {
+
+ // The DTEND property is considered non inclusive. So for a 3 day
+ // event in july, dtstart and dtend would have to be July 1st and
+ // July 4th respectively.
+ //
+ // See:
+ // http://tools.ietf.org/html/rfc5545#page-54
+ $effectiveEnd = $this->DTEND->getDateTime();
+
+ } elseif (isset($this->DURATION)) {
+ $effectiveEnd = clone $effectiveStart;
+ $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) );
+ } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+ $effectiveEnd = clone $effectiveStart;
+ $effectiveEnd->modify('+1 day');
+ } else {
+ $effectiveEnd = clone $effectiveStart;
+ }
+ return (
+ ($start <= $effectiveEnd) && ($end > $effectiveStart)
+ );
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php b/libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php
new file mode 100644
index 0000000..a8aa370
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VFreeBusy component
+ *
+ * This component adds functionality to a component, specific for VFREEBUSY
+ * components.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VFreeBusy extends VObject\Component {
+
+ /**
+ * Checks based on the contained FREEBUSY information, if a timeslot is
+ * available.
+ *
+ * @param DateTime $start
+ * @param Datetime $end
+ * @return bool
+ */
+ public function isFree(\DateTime $start, \Datetime $end) {
+
+ foreach($this->select('FREEBUSY') as $freebusy) {
+
+ // We are only interested in FBTYPE=BUSY (the default),
+ // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
+ if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') {
+ continue;
+ }
+
+ // The freebusy component can hold more than 1 value, separated by
+ // commas.
+ $periods = explode(',', (string)$freebusy);
+
+ foreach($periods as $period) {
+ // Every period is formatted as [start]/[end]. The start is an
+ // absolute UTC time, the end may be an absolute UTC time, or
+ // duration (relative) value.
+ list($busyStart, $busyEnd) = explode('/', $period);
+
+ $busyStart = VObject\DateTimeParser::parse($busyStart);
+ $busyEnd = VObject\DateTimeParser::parse($busyEnd);
+ if ($busyEnd instanceof \DateInterval) {
+ $tmp = clone $busyStart;
+ $tmp->add($busyEnd);
+ $busyEnd = $tmp;
+ }
+
+ if($start < $busyEnd && $end > $busyStart) {
+ return false;
+ }
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+}
+
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VJournal.php b/libcalendaring/lib/OldSabre/VObject/Component/VJournal.php
new file mode 100644
index 0000000..c9b0692
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VJournal.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * VJournal component
+ *
+ * This component contains some additional functionality specific for VJOURNALs.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VJournal extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+ if ($dtstart) {
+ $effectiveEnd = clone $dtstart;
+ if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+ $effectiveEnd->modify('+1 day');
+ }
+
+ return ($start <= $effectiveEnd && $end > $dtstart);
+
+ }
+ return false;
+
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VTodo.php b/libcalendaring/lib/OldSabre/VObject/Component/VTodo.php
new file mode 100644
index 0000000..06bac4f
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Component/VTodo.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * VTodo component
+ *
+ * This component contains some additional functionality specific for VTODOs.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VTodo extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+ $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null;
+ $due = isset($this->DUE)?$this->DUE->getDateTime():null;
+ $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null;
+ $created = isset($this->CREATED)?$this->CREATED->getDateTime():null;
+
+ if ($dtstart) {
+ if ($duration) {
+ $effectiveEnd = clone $dtstart;
+ $effectiveEnd->add($duration);
+ return $start <= $effectiveEnd && $end > $dtstart;
+ } elseif ($due) {
+ return
+ ($start < $due || $start <= $dtstart) &&
+ ($end > $dtstart || $end >= $due);
+ } else {
+ return $start <= $dtstart && $end > $dtstart;
+ }
+ }
+ if ($due) {
+ return ($start < $due && $end >= $due);
+ }
+ if ($completed && $created) {
+ return
+ ($start <= $created || $start <= $completed) &&
+ ($end >= $created || $end >= $completed);
+ }
+ if ($completed) {
+ return ($start <= $completed && $end >= $completed);
+ }
+ if ($created) {
+ return ($end > $created);
+ }
+ return true;
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/DateTimeParser.php b/libcalendaring/lib/OldSabre/VObject/DateTimeParser.php
new file mode 100644
index 0000000..a8cfca5
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/DateTimeParser.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * DateTimeParser
+ *
+ * This class is responsible for parsing the several different date and time
+ * formats iCalendar and vCards have.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class DateTimeParser {
+
+ /**
+ * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object
+ *
+ * Specifying a reference timezone is optional. It will only be used
+ * if the non-UTC format is used. The argument is used as a reference, the
+ * returned DateTime object will still be in the UTC timezone.
+ *
+ * @param string $dt
+ * @param DateTimeZone $tz
+ * @return DateTime
+ */
+ static public function parseDateTime($dt,\DateTimeZone $tz = null) {
+
+ // Format is YYYYMMDD + "T" + hhmmss
+ $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
+
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
+ }
+
+ if ($matches[7]==='Z' || is_null($tz)) {
+ $tz = new \DateTimeZone('UTC');
+ }
+ $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz);
+
+ // Still resetting the timezone, to normalize everything to UTC
+ $date->setTimeZone(new \DateTimeZone('UTC'));
+ return $date;
+
+ }
+
+ /**
+ * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object
+ *
+ * @param string $date
+ * @return DateTime
+ */
+ static public function parseDate($date) {
+
+ // Format is YYYYMMDD
+ $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
+
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date);
+ }
+
+ $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC'));
+ return $date;
+
+ }
+
+ /**
+ * Parses an iCalendar (RFC5545) formatted duration value.
+ *
+ * This method will either return a DateTimeInterval object, or a string
+ * suitable for strtotime or DateTime::modify.
+ *
+ * @param string $duration
+ * @param bool $asString
+ * @return DateInterval|string
+ */
+ static public function parseDuration($duration, $asString = false) {
+
+ $result = preg_match('/^(?P<plusminus>\+|-)?P((?P<week>\d+)W)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+)S)?)?$/', $duration, $matches);
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration);
+ }
+
+ if (!$asString) {
+ $invert = false;
+ if ($matches['plusminus']==='-') {
+ $invert = true;
+ }
+
+
+ $parts = array(
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ );
+ foreach($parts as $part) {
+ $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0;
+ }
+
+
+ // We need to re-construct the $duration string, because weeks and
+ // days are not supported by DateInterval in the same string.
+ $duration = 'P';
+ $days = $matches['day'];
+ if ($matches['week']) {
+ $days+=$matches['week']*7;
+ }
+ if ($days)
+ $duration.=$days . 'D';
+
+ if ($matches['minute'] || $matches['second'] || $matches['hour']) {
+ $duration.='T';
+
+ if ($matches['hour'])
+ $duration.=$matches['hour'].'H';
+
+ if ($matches['minute'])
+ $duration.=$matches['minute'].'M';
+
+ if ($matches['second'])
+ $duration.=$matches['second'].'S';
+
+ }
+
+ if ($duration==='P') {
+ $duration = 'PT0S';
+ }
+ $iv = new \DateInterval($duration);
+ if ($invert) $iv->invert = true;
+
+ return $iv;
+
+ }
+
+
+
+ $parts = array(
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ );
+
+ $newDur = '';
+ foreach($parts as $part) {
+ if (isset($matches[$part]) && $matches[$part]) {
+ $newDur.=' '.$matches[$part] . ' ' . $part . 's';
+ }
+ }
+
+ $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur);
+ if ($newDur === '+') { $newDur = '+0 seconds'; };
+ return $newDur;
+
+ }
+
+ /**
+ * Parses either a Date or DateTime, or Duration value.
+ *
+ * @param string $date
+ * @param DateTimeZone|string $referenceTZ
+ * @return DateTime|DateInterval
+ */
+ static public function parse($date, $referenceTZ = null) {
+
+ if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) {
+ return self::parseDuration($date);
+ } elseif (strlen($date)===8) {
+ return self::parseDate($date);
+ } else {
+ return self::parseDateTime($date, $referenceTZ);
+ }
+
+ }
+
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Document.php b/libcalendaring/lib/OldSabre/VObject/Document.php
new file mode 100644
index 0000000..5b5e2e6
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Document.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Document
+ *
+ * A document is just like a component, except that it's also the top level
+ * element.
+ *
+ * Both a VCALENDAR and a VCARD are considered documents.
+ *
+ * This class also provides a registry for document types.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Document extends Component {
+
+ /**
+ * The default name for this component.
+ *
+ * This should be 'VCALENDAR' or 'VCARD'.
+ *
+ * @var string
+ */
+ static $defaultName;
+
+ /**
+ * Creates a new document.
+ *
+ * We're changing the default behavior slightly here. First, we don't want
+ * to have to specify a name (we already know it), and we want to allow
+ * children to be specified in the first argument.
+ *
+ * But, the default behavior also works.
+ *
+ * So the two sigs:
+ *
+ * new Document(array $children = array());
+ * new Document(string $name, array $children = array())
+ *
+ * @return void
+ */
+ public function __construct() {
+
+ $args = func_get_args();
+ if (count($args)===0 || is_array($args[0])) {
+ array_unshift($args, static::$defaultName);
+ call_user_func_array(array('parent', '__construct'), $args);
+ } else {
+ call_user_func_array(array('parent', '__construct'), $args);
+ }
+
+ }
+
+ /**
+ * Creates a new component
+ *
+ * This method automatically searches for the correct component class, based
+ * on its name.
+ *
+ * You can specify the children either in key=>value syntax, in which case
+ * properties will automatically be created, or you can just pass a list of
+ * Component and Property object.
+ *
+ * @param string $name
+ * @param array $children
+ * @return Component
+ */
+ public function createComponent($name, array $children = array()) {
+
+ $component = Component::create($name);
+ foreach($children as $k=>$v) {
+
+ if ($v instanceof Node) {
+ $component->add($v);
+ } else {
+ $component->add($k, $v);
+ }
+
+ }
+ return $component;
+
+ }
+
+ /**
+ * Factory method for creating new properties
+ *
+ * This method automatically searches for the correct property class, based
+ * on its name.
+ *
+ * You can specify the parameters either in key=>value syntax, in which case
+ * parameters will automatically be created, or you can just pass a list of
+ * Parameter objects.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param array $parameters
+ * @return Property
+ */
+ public function createProperty($name, $value = null, array $parameters = array()) {
+
+ return Property::create($name, $value, $parameters);
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/ElementList.php b/libcalendaring/lib/OldSabre/VObject/ElementList.php
new file mode 100644
index 0000000..da2aa34
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/ElementList.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject ElementList
+ *
+ * This class represents a list of elements. Lists are the result of queries,
+ * such as doing $vcalendar->vevent where there's multiple VEVENT objects.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ElementList implements \Iterator, \Countable, \ArrayAccess {
+
+ /**
+ * Inner elements
+ *
+ * @var array
+ */
+ protected $elements = array();
+
+ /**
+ * Creates the element list.
+ *
+ * @param array $elements
+ */
+ public function __construct(array $elements) {
+
+ $this->elements = $elements;
+
+ }
+
+ /* {{{ Iterator interface */
+
+ /**
+ * Current position
+ *
+ * @var int
+ */
+ private $key = 0;
+
+ /**
+ * Returns current item in iteration
+ *
+ * @return Element
+ */
+ public function current() {
+
+ return $this->elements[$this->key];
+
+ }
+
+ /**
+ * To the next item in the iterator
+ *
+ * @return void
+ */
+ public function next() {
+
+ $this->key++;
+
+ }
+
+ /**
+ * Returns the current iterator key
+ *
+ * @return int
+ */
+ public function key() {
+
+ return $this->key;
+
+ }
+
+ /**
+ * Returns true if the current position in the iterator is a valid one
+ *
+ * @return bool
+ */
+ public function valid() {
+
+ return isset($this->elements[$this->key]);
+
+ }
+
+ /**
+ * Rewinds the iterator
+ *
+ * @return void
+ */
+ public function rewind() {
+
+ $this->key = 0;
+
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements
+ *
+ * @return int
+ */
+ public function count() {
+
+ return count($this->elements);
+
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * @param int $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+
+ return isset($this->elements[$offset]);
+
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+
+ return $this->elements[$offset];
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($offset,$value) {
+
+ throw new \LogicException('You can not add new objects to an ElementList');
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return void
+ */
+ public function offsetUnset($offset) {
+
+ throw new \LogicException('You can not remove objects from an ElementList');
+
+ }
+
+ /* }}} */
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php b/libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php
new file mode 100644
index 0000000..7671cd0
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php
@@ -0,0 +1,322 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class helps with generating FREEBUSY reports based on existing sets of
+ * objects.
+ *
+ * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
+ * generates a single VFREEBUSY object.
+ *
+ * VFREEBUSY components are described in RFC5545, The rules for what should
+ * go in a single freebusy report is taken from RFC4791, section 7.10.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class FreeBusyGenerator {
+
+ /**
+ * Input objects
+ *
+ * @var array
+ */
+ protected $objects;
+
+ /**
+ * Start of range
+ *
+ * @var DateTime|null
+ */
+ protected $start;
+
+ /**
+ * End of range
+ *
+ * @var DateTime|null
+ */
+ protected $end;
+
+ /**
+ * VCALENDAR object
+ *
+ * @var Component
+ */
+ protected $baseObject;
+
+ /**
+ * Creates the generator.
+ *
+ * Check the setTimeRange and setObjects methods for details about the
+ * arguments.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @param mixed $objects
+ * @return void
+ */
+ public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null) {
+
+ if ($start && $end) {
+ $this->setTimeRange($start, $end);
+ }
+
+ if ($objects) {
+ $this->setObjects($objects);
+ }
+
+ }
+
+ /**
+ * Sets the VCALENDAR object.
+ *
+ * If this is set, it will not be generated for you. You are responsible
+ * for setting things like the METHOD, CALSCALE, VERSION, etc..
+ *
+ * The VFREEBUSY object will be automatically added though.
+ *
+ * @param Component $vcalendar
+ * @return void
+ */
+ public function setBaseObject(Component $vcalendar) {
+
+ $this->baseObject = $vcalendar;
+
+ }
+
+ /**
+ * Sets the input objects
+ *
+ * You must either specify a valendar object as a strong, or as the parse
+ * Component.
+ * It's also possible to specify multiple objects as an array.
+ *
+ * @param mixed $objects
+ * @return void
+ */
+ public function setObjects($objects) {
+
+ if (!is_array($objects)) {
+ $objects = array($objects);
+ }
+
+ $this->objects = array();
+ foreach($objects as $object) {
+
+ if (is_string($object)) {
+ $this->objects[] = Reader::read($object);
+ } elseif ($object instanceof Component) {
+ $this->objects[] = $object;
+ } else {
+ throw new \InvalidArgumentException('You can only pass strings or \\OldSabre\\VObject\\Component arguments to setObjects');
+ }
+
+ }
+
+ }
+
+ /**
+ * Sets the time range
+ *
+ * Any freebusy object falling outside of this time range will be ignored.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return void
+ */
+ public function setTimeRange(\DateTime $start = null, \DateTime $end = null) {
+
+ $this->start = $start;
+ $this->end = $end;
+
+ }
+
+ /**
+ * Parses the input data and returns a correct VFREEBUSY object, wrapped in
+ * a VCALENDAR.
+ *
+ * @return Component
+ */
+ public function getResult() {
+
+ $busyTimes = array();
+
+ foreach($this->objects as $object) {
+
+ foreach($object->getBaseComponents() as $component) {
+
+ switch($component->name) {
+
+ case 'VEVENT' :
+
+ $FBTYPE = 'BUSY';
+ if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) {
+ break;
+ }
+ if (isset($component->STATUS)) {
+ $status = strtoupper($component->STATUS);
+ if ($status==='CANCELLED') {
+ break;
+ }
+ if ($status==='TENTATIVE') {
+ $FBTYPE = 'BUSY-TENTATIVE';
+ }
+ }
+
+ $times = array();
+
+ if ($component->RRULE) {
+
+ $iterator = new RecurrenceIterator($object, (string)$component->uid);
+ if ($this->start) {
+ $iterator->fastForward($this->start);
+ }
+
+ $maxRecurrences = 200;
+
+ while($iterator->valid() && --$maxRecurrences) {
+
+ $startTime = $iterator->getDTStart();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $times[] = array(
+ $iterator->getDTStart(),
+ $iterator->getDTEnd(),
+ );
+
+ $iterator->next();
+
+ }
+
+ } else {
+
+ $startTime = $component->DTSTART->getDateTime();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $endTime = null;
+ if (isset($component->DTEND)) {
+ $endTime = $component->DTEND->getDateTime();
+ } elseif (isset($component->DURATION)) {
+ $duration = DateTimeParser::parseDuration((string)$component->DURATION);
+ $endTime = clone $startTime;
+ $endTime->add($duration);
+ } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) {
+ $endTime = clone $startTime;
+ $endTime->modify('+1 day');
+ } else {
+ // The event had no duration (0 seconds)
+ break;
+ }
+
+ $times[] = array($startTime, $endTime);
+
+ }
+
+ foreach($times as $time) {
+
+ if ($this->end && $time[0] > $this->end) break;
+ if ($this->start && $time[1] < $this->start) break;
+
+ $busyTimes[] = array(
+ $time[0],
+ $time[1],
+ $FBTYPE,
+ );
+ }
+ break;
+
+ case 'VFREEBUSY' :
+ foreach($component->FREEBUSY as $freebusy) {
+
+ $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY';
+
+ // Skipping intervals marked as 'free'
+ if ($fbType==='FREE')
+ continue;
+
+ $values = explode(',', $freebusy);
+ foreach($values as $value) {
+ list($startTime, $endTime) = explode('/', $value);
+ $startTime = DateTimeParser::parseDateTime($startTime);
+
+ if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') {
+ $duration = DateTimeParser::parseDuration($endTime);
+ $endTime = clone $startTime;
+ $endTime->add($duration);
+ } else {
+ $endTime = DateTimeParser::parseDateTime($endTime);
+ }
+
+ if($this->start && $this->start > $endTime) continue;
+ if($this->end && $this->end < $startTime) continue;
+ $busyTimes[] = array(
+ $startTime,
+ $endTime,
+ $fbType
+ );
+
+ }
+
+
+ }
+ break;
+
+
+
+ }
+
+
+ }
+
+ }
+
+ if ($this->baseObject) {
+ $calendar = $this->baseObject;
+ } else {
+ $calendar = Component::create('VCALENDAR');
+ $calendar->version = '2.0';
+ $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN';
+ $calendar->calscale = 'GREGORIAN';
+ }
+
+ $vfreebusy = Component::create('VFREEBUSY');
+ $calendar->add($vfreebusy);
+
+ if ($this->start) {
+ $dtstart = Property::create('DTSTART');
+ $dtstart->setDateTime($this->start,Property\DateTime::UTC);
+ $vfreebusy->add($dtstart);
+ }
+ if ($this->end) {
+ $dtend = Property::create('DTEND');
+ $dtend->setDateTime($this->end,Property\DateTime::UTC);
+ $vfreebusy->add($dtend);
+ }
+ $dtstamp = Property::create('DTSTAMP');
+ $dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC);
+ $vfreebusy->add($dtstamp);
+
+ foreach($busyTimes as $busyTime) {
+
+ $busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
+ $busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
+
+ $prop = Property::create(
+ 'FREEBUSY',
+ $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
+ );
+ $prop['FBTYPE'] = $busyTime[2];
+ $vfreebusy->add($prop);
+
+ }
+
+ return $calendar;
+
+ }
+
+}
+
diff --git a/libcalendaring/lib/OldSabre/VObject/Node.php b/libcalendaring/lib/OldSabre/VObject/Node.php
new file mode 100644
index 0000000..e86f29e
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Node.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Base class for all nodes
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
+
+ /**
+ * The following constants are used by the validate() method.
+ */
+ const REPAIR = 1;
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ abstract function serialize();
+
+ /**
+ * Iterator override
+ *
+ * @var ElementList
+ */
+ protected $iterator = null;
+
+ /**
+ * A link to the parent node
+ *
+ * @var Node
+ */
+ public $parent = null;
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ return array();
+
+ }
+
+ /* {{{ IteratorAggregator interface */
+
+ /**
+ * Returns the iterator for this object
+ *
+ * @return ElementList
+ */
+ public function getIterator() {
+
+ if (!is_null($this->iterator))
+ return $this->iterator;
+
+ return new ElementList(array($this));
+
+ }
+
+ /**
+ * Sets the overridden iterator
+ *
+ * Note that this is not actually part of the iterator interface
+ *
+ * @param ElementList $iterator
+ * @return void
+ */
+ public function setIterator(ElementList $iterator) {
+
+ $this->iterator = $iterator;
+
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements
+ *
+ * @return int
+ */
+ public function count() {
+
+ $it = $this->getIterator();
+ return $it->count();
+
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+
+ $iterator = $this->getIterator();
+ return $iterator->offsetExists($offset);
+
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+
+ $iterator = $this->getIterator();
+ return $iterator->offsetGet($offset);
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($offset,$value) {
+
+ $iterator = $this->getIterator();
+ $iterator->offsetSet($offset,$value);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return void
+ */
+ public function offsetUnset($offset) {
+
+ $iterator = $this->getIterator();
+ $iterator->offsetUnset($offset);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+ // @codeCoverageIgnoreEnd
+
+ /* }}} */
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Parameter.php b/libcalendaring/lib/OldSabre/VObject/Parameter.php
new file mode 100644
index 0000000..19fc024
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Parameter.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Parameter
+ *
+ * This class represents a parameter. A parameter is always tied to a property.
+ * In the case of:
+ * DTSTART;VALUE=DATE:20101108
+ * VALUE=DATE would be the parameter name and value.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Parameter extends Node {
+
+ /**
+ * Parameter name
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Parameter value
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * Sets up the object
+ *
+ * @param string $name
+ * @param string $value
+ */
+ public function __construct($name, $value = null) {
+
+ if (!is_scalar($value) && !is_null($value)) {
+ throw new \InvalidArgumentException('The value argument must be a scalar value or null');
+ }
+
+ $this->name = strtoupper($name);
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the parameter's internal value.
+ *
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ if (is_null($this->value)) {
+ return $this->name;
+ }
+ $value = str_replace("\n", '\n', $this->value);
+ if (preg_match('#(?: [:;\\\\])#x', $value)) {
+ $value = '"' . $value . '"';
+ }
+ return $this->name . '=' . $value;
+
+ }
+
+ /**
+ * Called when this object is being cast to a string
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ return $this->value;
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/ParseException.php b/libcalendaring/lib/OldSabre/VObject/ParseException.php
new file mode 100644
index 0000000..7bbdd74
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/ParseException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Exception thrown by Reader if an invalid object was attempted to be parsed.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ParseException extends \Exception { }
diff --git a/libcalendaring/lib/OldSabre/VObject/Property.php b/libcalendaring/lib/OldSabre/VObject/Property.php
new file mode 100644
index 0000000..97712e4
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Property.php
@@ -0,0 +1,453 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Property
+ *
+ * A property in VObject is usually in the form PARAMNAME:paramValue.
+ * An example is : SUMMARY:Weekly meeting
+ *
+ * Properties can also have parameters:
+ * SUMMARY;LANG=en:Weekly meeting.
+ *
+ * Parameters can be accessed using the ArrayAccess interface.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Property extends Node {
+
+ /**
+ * Propertyname
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Group name
+ *
+ * This may be something like 'HOME' for vcards.
+ *
+ * @var string
+ */
+ public $group;
+
+ /**
+ * Property parameters
+ *
+ * @var array
+ */
+ public $parameters = array();
+
+ /**
+ * Property value
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * If properties are added to this map, they will be automatically mapped
+ * to their respective classes, if parsed by the reader or constructed with
+ * the 'create' method.
+ *
+ * @var array
+ */
+ static public $classMap = array(
+ 'COMPLETED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'CREATED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTEND' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTSTAMP' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTSTART' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DUE' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'EXDATE' => 'OldSabre\\VObject\\Property\\MultiDateTime',
+ 'LAST-MODIFIED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'RECURRENCE-ID' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'TRIGGER' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'N' => 'OldSabre\\VObject\\Property\\Compound',
+ 'ORG' => 'OldSabre\\VObject\\Property\\Compound',
+ 'ADR' => 'OldSabre\\VObject\\Property\\Compound',
+ 'CATEGORIES' => 'OldSabre\\VObject\\Property\\Compound',
+ );
+
+ /**
+ * Creates the new property by name, but in addition will also see if
+ * there's a class mapped to the property name.
+ *
+ * Parameters can be specified with the optional third argument. Parameters
+ * must be a key->value map of the parameter name, and value. If the value
+ * is specified as an array, it is assumed that multiple parameters with
+ * the same name should be added.
+ *
+ * @param string $name
+ * @param string $value
+ * @param array $parameters
+ * @return Property
+ */
+ static public function create($name, $value = null, array $parameters = array()) {
+
+ $name = strtoupper($name);
+ $shortName = $name;
+ $group = null;
+ if (strpos($shortName,'.')!==false) {
+ list($group, $shortName) = explode('.', $shortName);
+ }
+
+ if (isset(self::$classMap[$shortName])) {
+ return new self::$classMap[$shortName]($name, $value, $parameters);
+ } else {
+ return new self($name, $value, $parameters);
+ }
+
+ }
+
+ /**
+ * Creates a new property object
+ *
+ * Parameters can be specified with the optional third argument. Parameters
+ * must be a key->value map of the parameter name, and value. If the value
+ * is specified as an array, it is assumed that multiple parameters with
+ * the same name should be added.
+ *
+ * @param string $name
+ * @param string $value
+ * @param array $parameters
+ */
+ public function __construct($name, $value = null, array $parameters = array()) {
+
+ if (!is_scalar($value) && !is_null($value)) {
+ throw new \InvalidArgumentException('The value argument must be scalar or null');
+ }
+
+ $name = strtoupper($name);
+ $group = null;
+ if (strpos($name,'.')!==false) {
+ list($group, $name) = explode('.', $name);
+ }
+ $this->name = $name;
+ $this->group = $group;
+ $this->setValue($value);
+
+ foreach($parameters as $paramName => $paramValues) {
+
+ if (!is_array($paramValues)) {
+ $paramValues = array($paramValues);
+ }
+
+ foreach($paramValues as $paramValue) {
+ $this->add($paramName, $paramValue);
+ }
+
+ }
+
+ }
+
+ /**
+ * Updates the internal value
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setValue($value) {
+
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the internal value
+ *
+ * @param string $value
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ $str = $this->name;
+ if ($this->group) $str = $this->group . '.' . $this->name;
+
+ foreach($this->parameters as $param) {
+
+ $str.=';' . $param->serialize();
+
+ }
+
+ $src = array(
+ '\\',
+ "\n",
+ "\r",
+ );
+ $out = array(
+ '\\\\',
+ '\n',
+ '',
+ );
+
+ // avoid double-escaping of \, and \; from Compound properties
+ if (method_exists($this, 'setParts')) {
+ $src[] = '\\\\,';
+ $out[] = '\\,';
+ $src[] = '\\\\;';
+ $out[] = '\\;';
+ }
+
+ $str.=':' . str_replace($src, $out, $this->value);
+
+ $out = '';
+ while(strlen($str)>0) {
+ if (strlen($str)>75) {
+ $out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
+ $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
+ } else {
+ $out.=$str . "\r\n";
+ $str='';
+ break;
+ }
+ }
+
+ return $out;
+
+ }
+
+ /**
+ * Adds a new componenten or element
+ *
+ * You can call this method with the following syntaxes:
+ *
+ * add(Parameter $element)
+ * add(string $name, $value)
+ *
+ * The first version adds an Parameter
+ * The second adds a property as a string.
+ *
+ * @param mixed $item
+ * @param mixed $itemValue
+ * @return void
+ */
+ public function add($item, $itemValue = null) {
+
+ if ($item instanceof Parameter) {
+ if (!is_null($itemValue)) {
+ throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject');
+ }
+ $item->parent = $this;
+ $this->parameters[] = $item;
+ } elseif(is_string($item)) {
+
+ $parameter = new Parameter($item,$itemValue);
+ $parameter->parent = $this;
+ $this->parameters[] = $parameter;
+
+ } else {
+
+ throw new \InvalidArgumentException('The first argument must either be a Node a string');
+
+ }
+
+ }
+
+ /* ArrayAccess interface {{{ */
+
+ /**
+ * Checks if an array element exists
+ *
+ * @param mixed $name
+ * @return bool
+ */
+ public function offsetExists($name) {
+
+ if (is_int($name)) return parent::offsetExists($name);
+
+ $name = strtoupper($name);
+
+ foreach($this->parameters as $parameter) {
+ if ($parameter->name == $name) return true;
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns a parameter, or parameter list.
+ *
+ * @param string $name
+ * @return Node
+ */
+ public function offsetGet($name) {
+
+ if (is_int($name)) return parent::offsetGet($name);
+ $name = strtoupper($name);
+
+ $result = array();
+ foreach($this->parameters as $parameter) {
+ if ($parameter->name == $name)
+ $result[] = $parameter;
+ }
+
+ if (count($result)===0) {
+ return null;
+ } elseif (count($result)===1) {
+ return $result[0];
+ } else {
+ $result[0]->setIterator(new ElementList($result));
+ return $result[0];
+ }
+
+ }
+
+ /**
+ * Creates a new parameter
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($name, $value) {
+
+ if (is_int($name)) parent::offsetSet($name, $value);
+
+ if (is_scalar($value)) {
+ if (!is_string($name))
+ throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.');
+
+ $this->offsetUnset($name);
+ $parameter = new Parameter($name, $value);
+ $parameter->parent = $this;
+ $this->parameters[] = $parameter;
+
+ } elseif ($value instanceof Parameter) {
+ if (!is_null($name))
+ throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\OldSabre\\VObject\\Parameter. Add using $array[]=$parameterObject.');
+
+ $value->parent = $this;
+ $this->parameters[] = $value;
+ } else {
+ throw new \InvalidArgumentException('You can only add parameters to the property object');
+ }
+
+ }
+
+ /**
+ * Removes one or more parameters with the specified name
+ *
+ * @param string $name
+ * @return void
+ */
+ public function offsetUnset($name) {
+
+ if (is_int($name)) parent::offsetUnset($name);
+ $name = strtoupper($name);
+
+ foreach($this->parameters as $key=>$parameter) {
+ if ($parameter->name == $name) {
+ $parameter->parent = null;
+ unset($this->parameters[$key]);
+ }
+
+ }
+
+ }
+
+ /* }}} */
+
+ /**
+ * Called when this object is being cast to a string
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ return (string)$this->value;
+
+ }
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ *
+ * @return void
+ */
+ public function __clone() {
+
+ foreach($this->parameters as $key=>$child) {
+ $this->parameters[$key] = clone $child;
+ $this->parameters[$key]->parent = $this;
+ }
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $warnings = array();
+
+ // Checking if our value is UTF-8
+ if (!StringUtil::isUTF8($this->value)) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'Property is not valid UTF-8!',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ $this->value = StringUtil::convertToUTF8($this->value);
+ }
+ }
+
+ // Checking if the propertyname does not contain any invalid bytes.
+ if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ // Uppercasing and converting underscores to dashes.
+ $this->name = strtoupper(
+ str_replace('_', '-', $this->name)
+ );
+ // Removing every other invalid character
+ $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
+
+ }
+
+ }
+
+ // Validating inner parameters
+ foreach($this->parameters as $param) {
+ $warnings = array_merge($warnings, $param->validate($options));
+ }
+
+ return $warnings;
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Property/Compound.php b/libcalendaring/lib/OldSabre/VObject/Property/Compound.php
new file mode 100644
index 0000000..1300807
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Property/Compound.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * Compound property.
+ *
+ * This class adds (de)serialization of compound properties to/from arrays.
+ *
+ * Currently the following properties from RFC 6350 are mapped to use this
+ * class:
+ *
+ * N: Section 6.2.2
+ * ADR: Section 6.3.1
+ * ORG: Section 6.6.4
+ * CATEGORIES: Section 6.7.1
+ *
+ * In order to use this correctly, you must call setParts and getParts to
+ * retrieve and modify dates respectively.
+ *
+ * @author Thomas Tanghus (http://tanghus.net/)
+ * @author Lars Kneschke
+ * @author Evert Pot (http://evertpot.com/)
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Compound extends VObject\Property {
+
+ /**
+ * If property names are added to this map, they will be (de)serialised as arrays
+ * using the getParts() and setParts() methods.
+ * The keys are the property names, values are delimiter chars.
+ *
+ * @var array
+ */
+ static public $delimiterMap = array(
+ 'N' => ';',
+ 'ADR' => ';',
+ 'ORG' => ';',
+ 'CATEGORIES' => ',',
+ );
+
+ /**
+ * The currently used delimiter.
+ *
+ * @var string
+ */
+ protected $delimiter = null;
+
+ /**
+ * Get a compound value as an array.
+ *
+ * @param $name string
+ * @return array
+ */
+ public function getParts() {
+
+ if (is_null($this->value)) {
+ return array();
+ }
+
+ $delimiter = $this->getDelimiter();
+
+ // split by any $delimiter which is NOT prefixed by a slash.
+ // Note that this is not a a perfect solution. If a value is prefixed
+ // by two slashes, it should actually be split anyway.
+ //
+ // Hopefully we can fix this better in a future version, where we can
+ // break compatibility a bit.
+ $compoundValues = preg_split("/(?<!\\\)$delimiter/", $this->value);
+
+ // remove slashes from any semicolon and comma left escaped in the single values
+ $compoundValues = array_map(
+ function($val) {
+ return strtr($val, array('\,' => ',', '\;' => ';'));
+ }, $compoundValues);
+
+ return $compoundValues;
+
+ }
+
+ /**
+ * Returns the delimiter for this property.
+ *
+ * @return string
+ */
+ public function getDelimiter() {
+
+ if (!$this->delimiter) {
+ if (isset(self::$delimiterMap[$this->name])) {
+ $this->delimiter = self::$delimiterMap[$this->name];
+ } else {
+ // To be a bit future proof, we are going to default the
+ // delimiter to ;
+ $this->delimiter = ';';
+ }
+ }
+ return $this->delimiter;
+
+ }
+
+ /**
+ * Set a compound value as an array.
+ *
+ *
+ * @param $name string
+ * @return array
+ */
+ public function setParts(array $values) {
+
+ // add slashes to all semicolons and commas in the single values
+ $values = array_map(
+ function($val) {
+ return strtr($val, array(',' => '\,', ';' => '\;'));
+ }, $values);
+
+ $this->setValue(
+ implode($this->getDelimiter(), $values)
+ );
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Property/DateTime.php b/libcalendaring/lib/OldSabre/VObject/Property/DateTime.php
new file mode 100644
index 0000000..60a3781
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Property/DateTime.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * DateTime property
+ *
+ * This element is used for iCalendar properties such as the DTSTART property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTime and getDateTime to
+ * retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class DateTime extends VObject\Property {
+
+ /**
+ * Local 'floating' time
+ */
+ const LOCAL = 1;
+
+ /**
+ * UTC-based time
+ */
+ const UTC = 2;
+
+ /**
+ * Local time plus timezone
+ */
+ const LOCALTZ = 3;
+
+ /**
+ * Only a date, time is ignored
+ */
+ const DATE = 4;
+
+ /**
+ * DateTime representation
+ *
+ * @var \DateTime
+ */
+ protected $dateTime;
+
+ /**
+ * dateType
+ *
+ * @var int
+ */
+ protected $dateType;
+
+ /**
+ * Updates the Date and Time.
+ *
+ * @param \DateTime $dt
+ * @param int $dateType
+ * @return void
+ */
+ public function setDateTime(\DateTime $dt, $dateType = self::LOCALTZ) {
+
+ switch($dateType) {
+
+ case self::LOCAL :
+ $this->setValue($dt->format('Ymd\\THis'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case self::UTC :
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ $this->setValue($dt->format('Ymd\\THis\\Z'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case self::LOCALTZ :
+ $this->setValue($dt->format('Ymd\\THis'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ $this->offsetSet('TZID', $dt->getTimeZone()->getName());
+ break;
+ case self::DATE :
+ $this->setValue($dt->format('Ymd'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE');
+ break;
+ default :
+ throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+ }
+ $this->dateTime = $dt;
+ $this->dateType = $dateType;
+
+ }
+
+ /**
+ * Returns the current DateTime value.
+ *
+ * If no value was set, this method returns null.
+ *
+ * @return \DateTime|null
+ */
+ public function getDateTime() {
+
+ if ($this->dateTime)
+ return $this->dateTime;
+
+ list(
+ $this->dateType,
+ $this->dateTime
+ ) = self::parseData($this->value, $this);
+ return $this->dateTime;
+
+ }
+
+ /**
+ * Returns the type of Date format.
+ *
+ * This method returns one of the format constants. If no date was set,
+ * this method will return null.
+ *
+ * @return int|null
+ */
+ public function getDateType() {
+
+ if ($this->dateType)
+ return $this->dateType;
+
+ list(
+ $this->dateType,
+ $this->dateTime,
+ ) = self::parseData($this->value, $this);
+ return $this->dateType;
+
+ }
+
+ /**
+ * This method will return true, if the property had a date and a time, as
+ * opposed to only a date.
+ *
+ * @return bool
+ */
+ public function hasTime() {
+
+ return $this->getDateType()!==self::DATE;
+
+ }
+
+ /**
+ * Parses the internal data structure to figure out what the current date
+ * and time is.
+ *
+ * The returned array contains two elements:
+ * 1. A 'DateType' constant (as defined on this class), or null.
+ * 2. A DateTime object (or null)
+ *
+ * @param string|null $propertyValue The string to parse (yymmdd or
+ * ymmddThhmmss, etc..)
+ * @param \OldSabre\VObject\Property|null $property The instance of the
+ * property we're parsing.
+ * @return array
+ */
+ static public function parseData($propertyValue, VObject\Property $property = null) {
+
+ if (is_null($propertyValue)) {
+ return array(null, null);
+ }
+
+ $date = '(?P<year>[1-2][0-9]{3})(?P<month>[0-1][0-9])(?P<date>[0-3][0-9])';
+ $time = '(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9])';
+ $regex = "/^$date(T$time(?P<isutc>Z)?)?$/";
+
+ if (!preg_match($regex, $propertyValue, $matches)) {
+ throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string');
+ }
+
+ if (!isset($matches['hour'])) {
+ // Date-only
+ return array(
+ self::DATE,
+ new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')),
+ );
+ }
+
+ $dateStr =
+ $matches['year'] .'-' .
+ $matches['month'] . '-' .
+ $matches['date'] . ' ' .
+ $matches['hour'] . ':' .
+ $matches['minute'] . ':' .
+ $matches['second'];
+
+ if (isset($matches['isutc'])) {
+ $dt = new \DateTime($dateStr,new \DateTimeZone('UTC'));
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ return array(
+ self::UTC,
+ $dt
+ );
+ }
+
+ // Finding the timezone.
+ $tzid = $property['TZID'];
+ if (!$tzid) {
+ // This was a floating time string. This implies we use the
+ // timezone from date_default_timezone_set / date.timezone ini
+ // setting.
+ return array(
+ self::LOCAL,
+ new \DateTime($dateStr)
+ );
+ }
+
+ // To look up the timezone, we must first find the VCALENDAR component.
+ $root = $property;
+ while($root->parent) {
+ $root = $root->parent;
+ }
+ if ($root->name === 'VCALENDAR') {
+ $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root);
+ } else {
+ $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid);
+ }
+
+ $dt = new \DateTime($dateStr, $tz);
+ $dt->setTimeZone($tz);
+
+ return array(
+ self::LOCALTZ,
+ $dt
+ );
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php b/libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php
new file mode 100644
index 0000000..45ee811
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * Multi-DateTime property
+ *
+ * This element is used for iCalendar properties such as the EXDATE property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTimes and getDateTimes
+ * to retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class MultiDateTime extends VObject\Property {
+
+ /**
+ * DateTime representation
+ *
+ * @var DateTime[]
+ */
+ protected $dateTimes;
+
+ /**
+ * dateType
+ *
+ * This is one of the OldSabre\VObject\Property\DateTime constants.
+ *
+ * @var int
+ */
+ protected $dateType;
+
+ /**
+ * Updates the value
+ *
+ * @param array $dt Must be an array of DateTime objects.
+ * @param int $dateType
+ * @return void
+ */
+ public function setDateTimes(array $dt, $dateType = VObject\Property\DateTime::LOCALTZ) {
+
+ foreach($dt as $i)
+ if (!$i instanceof \DateTime)
+ throw new \InvalidArgumentException('You must pass an array of DateTime objects');
+
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ switch($dateType) {
+
+ case DateTime::LOCAL :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd\\THis');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case DateTime::UTC :
+ $val = array();
+ foreach($dt as $i) {
+ $i->setTimeZone(new \DateTimeZone('UTC'));
+ $val[] = $i->format('Ymd\\THis\\Z');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case DateTime::LOCALTZ :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd\\THis');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName());
+ break;
+ case DateTime::DATE :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE');
+ break;
+ default :
+ throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+ }
+ $this->dateTimes = $dt;
+ $this->dateType = $dateType;
+
+ }
+
+ /**
+ * Returns the current DateTime value.
+ *
+ * If no value was set, this method returns null.
+ *
+ * @return array|null
+ */
+ public function getDateTimes() {
+
+ if ($this->dateTimes)
+ return $this->dateTimes;
+
+ $dts = array();
+
+ if (!$this->value) {
+ $this->dateTimes = null;
+ $this->dateType = null;
+ return null;
+ }
+
+ foreach(explode(',',$this->value) as $val) {
+ list(
+ $type,
+ $dt
+ ) = DateTime::parseData($val, $this);
+ $dts[] = $dt;
+ $this->dateType = $type;
+ }
+ $this->dateTimes = $dts;
+ return $this->dateTimes;
+
+ }
+
+ /**
+ * Returns the type of Date format.
+ *
+ * This method returns one of the format constants. If no date was set,
+ * this method will return null.
+ *
+ * @return int|null
+ */
+ public function getDateType() {
+
+ if ($this->dateType)
+ return $this->dateType;
+
+ if (!$this->value) {
+ $this->dateTimes = null;
+ $this->dateType = null;
+ return null;
+ }
+
+ $dts = array();
+ foreach(explode(',',$this->value) as $val) {
+ list(
+ $type,
+ $dt
+ ) = DateTime::parseData($val, $this);
+ $dts[] = $dt;
+ $this->dateType = $type;
+ }
+ $this->dateTimes = $dts;
+ return $this->dateType;
+
+ }
+
+ /**
+ * This method will return true, if the property had a date and a time, as
+ * opposed to only a date.
+ *
+ * @return bool
+ */
+ public function hasTime() {
+
+ return $this->getDateType()!==DateTime::DATE;
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Reader.php b/libcalendaring/lib/OldSabre/VObject/Reader.php
new file mode 100644
index 0000000..5bea834
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Reader.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VCALENDAR/VCARD reader
+ *
+ * This class reads the vobject file, and returns a full element tree.
+ *
+ * TODO: this class currently completely works 'statically'. This is pointless,
+ * and defeats OOP principals. Needs refactoring in a future version.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Reader {
+
+ /**
+ * If this option is passed to the reader, it will be less strict about the
+ * validity of the lines.
+ *
+ * Currently using this option just means, that it will accept underscores
+ * in property names.
+ */
+ const OPTION_FORGIVING = 1;
+
+ /**
+ * If this option is turned on, any lines we cannot parse will be ignored
+ * by the reader.
+ */
+ const OPTION_IGNORE_INVALID_LINES = 2;
+
+ /**
+ * Parses the file and returns the top component
+ *
+ * The options argument is a bitfield. Pass any of the OPTIONS constant to
+ * alter the parsers' behaviour.
+ *
+ * @param string $data
+ * @param int $options
+ * @return Node
+ */
+ static function read($data, $options = 0) {
+
+ // Normalizing newlines
+ $data = str_replace(array("\r","\n\n"), array("\n","\n"), $data);
+
+ $lines = explode("\n", $data);
+
+ // Unfolding lines
+ $lines2 = array();
+ foreach($lines as $line) {
+
+ // Skipping empty lines
+ if (!$line) continue;
+
+ if ($line[0]===" " || $line[0]==="\t") {
+ $lines2[count($lines2)-1].=substr($line,1);
+ } else {
+ $lines2[] = $line;
+ }
+
+ }
+
+ unset($lines);
+
+ reset($lines2);
+
+ return self::readLine($lines2, $options);
+
+ }
+
+ /**
+ * Reads and parses a single line.
+ *
+ * This method receives the full array of lines. The array pointer is used
+ * to traverse.
+ *
+ * This method returns null if an invalid line was encountered, and the
+ * IGNORE_INVALID_LINES option was turned on.
+ *
+ * @param array $lines
+ * @param int $options See the OPTIONS constants.
+ * @return Node
+ */
+ static private function readLine(&$lines, $options = 0) {
+
+ $line = current($lines);
+ $lineNr = key($lines);
+ next($lines);
+
+ // Components
+ if (strtoupper(substr($line,0,6)) === "BEGIN:") {
+
+ $componentName = strtoupper(substr($line,6));
+ $obj = Component::create($componentName);
+
+ $nextLine = current($lines);
+
+ while(strtoupper(substr($nextLine,0,4))!=="END:") {
+
+ $parsedLine = self::readLine($lines, $options);
+ $nextLine = current($lines);
+
+ if (is_null($parsedLine)) {
+ continue;
+ }
+ $obj->add($parsedLine);
+
+ if ($nextLine===false)
+ throw new ParseException('Invalid VObject. Document ended prematurely.');
+
+ }
+
+ // Checking component name of the 'END:' line.
+ if (substr($nextLine,4)!==$obj->name) {
+ throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"');
+ }
+ next($lines);
+
+ return $obj;
+
+ }
+
+ // Properties
+ //$result = preg_match('/(?P<name>[A-Z0-9-]+)(?:;(?P<parameters>^(?<!:):))(.*)$/',$line,$matches);
+
+ if ($options & self::OPTION_FORGIVING) {
+ $token = '[A-Z0-9-\._]+';
+ } else {
+ $token = '[A-Z0-9-\.]+';
+ }
+ $parameters = "(?:;(?P<parameters>([^:^\"]|\"([^\"]*)\")*))?";
+ $regex = "/^(?P<name>$token)$parameters:(?P<value>.*)$/i";
+
+ $result = preg_match($regex,$line,$matches);
+
+ if (!$result) {
+ if ($options & self::OPTION_IGNORE_INVALID_LINES) {
+ return null;
+ } else {
+ throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format');
+ }
+ }
+
+ $propertyName = strtoupper($matches['name']);
+ $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) {
+ if ($matches[2]==='n' || $matches[2]==='N') {
+ return "\n";
+ } else {
+ return $matches[2];
+ }
+ }, $matches['value']);
+
+ $obj = Property::create($propertyName, $propertyValue);
+
+ if ($matches['parameters']) {
+
+ foreach(self::readParameters($matches['parameters']) as $param) {
+ $obj->add($param);
+ }
+
+ }
+
+ return $obj;
+
+
+ }
+
+ /**
+ * Reads a parameter list from a property
+ *
+ * This method returns an array of Parameter
+ *
+ * @param string $parameters
+ * @return array
+ */
+ static private function readParameters($parameters) {
+
+ $token = '[A-Z0-9-]+';
+
+ $paramValue = '(?P<paramValue>[^\"^;]*|"[^"]*")';
+
+ $regex = "/(?<=^|;)(?P<paramName>$token)(=$paramValue(?=$|;))?/i";
+ preg_match_all($regex, $parameters, $matches, PREG_SET_ORDER);
+
+ $params = array();
+ foreach($matches as $match) {
+
+ if (!isset($match['paramValue'])) {
+
+ $value = null;
+
+ } else {
+
+ $value = $match['paramValue'];
+
+ if (isset($value[0]) && $value[0]==='"') {
+ // Stripping quotes, if needed
+ $value = substr($value,1,strlen($value)-2);
+ }
+
+ $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) {
+ if ($matches[2]==='n' || $matches[2]==='N') {
+ return "\n";
+ } else {
+ return $matches[2];
+ }
+ }, $value);
+
+ }
+
+ $params[] = new Parameter($match['paramName'], $value);
+
+ }
+
+ return $params;
+
+ }
+
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php b/libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php
new file mode 100644
index 0000000..dcef36c
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php
@@ -0,0 +1,1144 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class is used to determine new for a recurring event, when the next
+ * events occur.
+ *
+ * This iterator may loop infinitely in the future, therefore it is important
+ * that if you use this class, you set hard limits for the amount of iterations
+ * you want to handle.
+ *
+ * Note that currently there is not full support for the entire iCalendar
+ * specification, as it's very complex and contains a lot of permutations
+ * that's not yet used very often in software.
+ *
+ * For the focus has been on features as they actually appear in Calendaring
+ * software, but this may well get expanded as needed / on demand
+ *
+ * The following RRULE properties are supported
+ * * UNTIL
+ * * INTERVAL
+ * * COUNT
+ * * FREQ=DAILY
+ * * BYDAY
+ * * BYHOUR
+ * * FREQ=WEEKLY
+ * * BYDAY
+ * * BYHOUR
+ * * WKST
+ * * FREQ=MONTHLY
+ * * BYMONTHDAY
+ * * BYDAY
+ * * BYSETPOS
+ * * FREQ=YEARLY
+ * * BYMONTH
+ * * BYMONTHDAY (only if BYMONTH is also set)
+ * * BYDAY (only if BYMONTH is also set)
+ *
+ * Anything beyond this is 'undefined', which means that it may get ignored, or
+ * you may get unexpected results. The effect is that in some applications the
+ * specified recurrence may look incorrect, or is missing.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class RecurrenceIterator implements \Iterator {
+
+ /**
+ * The initial event date
+ *
+ * @var DateTime
+ */
+ public $startDate;
+
+ /**
+ * The end-date of the initial event
+ *
+ * @var DateTime
+ */
+ public $endDate;
+
+ /**
+ * The 'current' recurrence.
+ *
+ * This will be increased for every iteration.
+ *
+ * @var DateTime
+ */
+ public $currentDate;
+
+
+ /**
+ * List of dates that are excluded from the rules.
+ *
+ * This list contains the items that have been overriden by the EXDATE
+ * property.
+ *
+ * @var array
+ */
+ public $exceptionDates = array();
+
+ /**
+ * Base event
+ *
+ * @var Component\VEvent
+ */
+ public $baseEvent;
+
+ /**
+ * List of dates that are overridden by other events.
+ * Similar to $overriddenEvents, but this just contains the original dates.
+ *
+ * @var array
+ */
+ public $overriddenDates = array();
+
+ /**
+ * list of events that are 'overridden'.
+ *
+ * This is an array of Component\VEvent objects.
+ *
+ * @var array
+ */
+ public $overriddenEvents = array();
+
+ /**
+ * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
+ * yearly.
+ *
+ * @var string
+ */
+ public $frequency;
+
+ /**
+ * The last instance of this recurrence, inclusively
+ *
+ * @var DateTime|null
+ */
+ public $until;
+
+ /**
+ * The number of recurrences, or 'null' if infinitely recurring.
+ *
+ * @var int
+ */
+ public $count;
+
+ /**
+ * The interval.
+ *
+ * If for example frequency is set to daily, interval = 2 would mean every
+ * 2 days.
+ *
+ * @var int
+ */
+ public $interval = 1;
+
+ /**
+ * Which seconds to recur.
+ *
+ * This is an array of integers (between 0 and 60)
+ *
+ * @var array
+ */
+ public $bySecond;
+
+ /**
+ * Which minutes to recur
+ *
+ * This is an array of integers (between 0 and 59)
+ *
+ * @var array
+ */
+ public $byMinute;
+
+ /**
+ * Which hours to recur
+ *
+ * This is an array of integers (between 0 and 23)
+ *
+ * @var array
+ */
+ public $byHour;
+
+ /**
+ * Which weekdays to recur.
+ *
+ * This is an array of weekdays
+ *
+ * This may also be preceeded by a positive or negative integer. If present,
+ * this indicates the nth occurrence of a specific day within the monthly or
+ * yearly rrule. For instance, -2TU indicates the second-last tuesday of
+ * the month, or year.
+ *
+ * @var array
+ */
+ public $byDay;
+
+ /**
+ * Which days of the month to recur
+ *
+ * This is an array of days of the months (1-31). The value can also be
+ * negative. -5 for instance means the 5th last day of the month.
+ *
+ * @var array
+ */
+ public $byMonthDay;
+
+ /**
+ * Which days of the year to recur.
+ *
+ * This is an array with days of the year (1 to 366). The values can also
+ * be negative. For instance, -1 will always represent the last day of the
+ * year. (December 31st).
+ *
+ * @var array
+ */
+ public $byYearDay;
+
+ /**
+ * Which week numbers to recur.
+ *
+ * This is an array of integers from 1 to 53. The values can also be
+ * negative. -1 will always refer to the last week of the year.
+ *
+ * @var array
+ */
+ public $byWeekNo;
+
+ /**
+ * Which months to recur
+ *
+ * This is an array of integers from 1 to 12.
+ *
+ * @var array
+ */
+ public $byMonth;
+
+ /**
+ * Which items in an existing st to recur.
+ *
+ * These numbers work together with an existing by* rule. It specifies
+ * exactly which items of the existing by-rule to filter.
+ *
+ * Valid values are 1 to 366 and -1 to -366. As an example, this can be
+ * used to recur the last workday of the month.
+ *
+ * This would be done by setting frequency to 'monthly', byDay to
+ * 'MO,TU,WE,TH,FR' and bySetPos to -1.
+ *
+ * @var array
+ */
+ public $bySetPos;
+
+ /**
+ * When a week starts
+ *
+ * @var string
+ */
+ public $weekStart = 'MO';
+
+ /**
+ * The current item in the list
+ *
+ * @var int
+ */
+ public $counter = 0;
+
+ /**
+ * Simple mapping from iCalendar day names to day numbers
+ *
+ * @var array
+ */
+ private $dayMap = array(
+ 'SU' => 0,
+ 'MO' => 1,
+ 'TU' => 2,
+ 'WE' => 3,
+ 'TH' => 4,
+ 'FR' => 5,
+ 'SA' => 6,
+ );
+
+ /**
+ * Mappings between the day number and english day name.
+ *
+ * @var array
+ */
+ private $dayNames = array(
+ 0 => 'Sunday',
+ 1 => 'Monday',
+ 2 => 'Tuesday',
+ 3 => 'Wednesday',
+ 4 => 'Thursday',
+ 5 => 'Friday',
+ 6 => 'Saturday',
+ );
+
+ /**
+ * If the current iteration of the event is an overriden event, this
+ * property will hold the VObject
+ *
+ * @var Component
+ */
+ private $currentOverriddenEvent;
+
+ /**
+ * This property may contain the date of the next not-overridden event.
+ * This date is calculated sometimes a bit early, before overridden events
+ * are evaluated.
+ *
+ * @var DateTime
+ */
+ private $nextDate;
+
+ /**
+ * This counts the number of overridden events we've handled so far
+ *
+ * @var int
+ */
+ private $handledOverridden = 0;
+
+ /**
+ * Creates the iterator
+ *
+ * You should pass a VCALENDAR component, as well as the UID of the event
+ * we're going to traverse.
+ *
+ * @param Component $vcal
+ * @param string|null $uid
+ */
+ public function __construct(Component $vcal, $uid=null) {
+
+ if (is_null($uid)) {
+ if ($vcal->name === 'VCALENDAR') {
+ throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
+ }
+ $components = array($vcal);
+ $uid = (string)$vcal->uid;
+ } else {
+ $components = $vcal->select('VEVENT');
+ }
+ foreach($components as $component) {
+ if ((string)$component->uid == $uid) {
+ if (isset($component->{'RECURRENCE-ID'})) {
+ $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component;
+ $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime();
+ } else {
+ $this->baseEvent = $component;
+ }
+ }
+ }
+
+ ksort($this->overriddenEvents);
+
+ if (!$this->baseEvent) {
+ throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid);
+ }
+
+ $this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
+
+ $this->endDate = null;
+ if (isset($this->baseEvent->DTEND)) {
+ $this->endDate = clone $this->baseEvent->DTEND->getDateTime();
+ } else {
+ $this->endDate = clone $this->startDate;
+ if (isset($this->baseEvent->DURATION)) {
+ $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value));
+ } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) {
+ $this->endDate->modify('+1 day');
+ }
+ }
+ $this->currentDate = clone $this->startDate;
+
+ $rrule = (string)$this->baseEvent->RRULE;
+
+ $parts = explode(';', $rrule);
+
+ // If no rrule was specified, we create a default setting
+ if (!$rrule) {
+ $this->frequency = 'daily';
+ $this->count = 1;
+ } else foreach($parts as $part) {
+
+ list($key, $value) = explode('=', $part, 2);
+
+ switch(strtoupper($key)) {
+
+ case 'FREQ' :
+ if (!in_array(
+ strtolower($value),
+ array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
+ )) {
+ throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
+
+ }
+ $this->frequency = strtolower($value);
+ break;
+
+ case 'UNTIL' :
+ $this->until = DateTimeParser::parse($value);
+
+ // In some cases events are generated with an UNTIL=
+ // parameter before the actual start of the event.
+ //
+ // Not sure why this is happening. We assume that the
+ // intention was that the event only recurs once.
+ //
+ // So we are modifying the parameter so our code doesn't
+ // break.
+ if($this->until < $this->baseEvent->DTSTART->getDateTime()) {
+ $this->until = $this->baseEvent->DTSTART->getDateTime();
+ }
+ break;
+
+ case 'COUNT' :
+ $this->count = (int)$value;
+ break;
+
+ case 'INTERVAL' :
+ $this->interval = (int)$value;
+ if ($this->interval < 1) {
+ throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!');
+ }
+ break;
+
+ case 'BYSECOND' :
+ $this->bySecond = explode(',', $value);
+ break;
+
+ case 'BYMINUTE' :
+ $this->byMinute = explode(',', $value);
+ break;
+
+ case 'BYHOUR' :
+ $this->byHour = explode(',', $value);
+ break;
+
+ case 'BYDAY' :
+ $this->byDay = explode(',', strtoupper($value));
+ break;
+
+ case 'BYMONTHDAY' :
+ $this->byMonthDay = explode(',', $value);
+ break;
+
+ case 'BYYEARDAY' :
+ $this->byYearDay = explode(',', $value);
+ break;
+
+ case 'BYWEEKNO' :
+ $this->byWeekNo = explode(',', $value);
+ break;
+
+ case 'BYMONTH' :
+ $this->byMonth = explode(',', $value);
+ break;
+
+ case 'BYSETPOS' :
+ $this->bySetPos = explode(',', $value);
+ break;
+
+ case 'WKST' :
+ $this->weekStart = strtoupper($value);
+ break;
+
+ }
+
+ }
+
+ // Parsing exception dates
+ if (isset($this->baseEvent->EXDATE)) {
+ foreach($this->baseEvent->EXDATE as $exDate) {
+
+ foreach(explode(',', (string)$exDate) as $exceptionDate) {
+
+ $this->exceptionDates[] =
+ DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Returns the current item in the list
+ *
+ * @return DateTime
+ */
+ public function current() {
+
+ if (!$this->valid()) return null;
+ return clone $this->currentDate;
+
+ }
+
+ /**
+ * This method returns the startdate for the current iteration of the
+ * event.
+ *
+ * @return DateTime
+ */
+ public function getDtStart() {
+
+ if (!$this->valid()) return null;
+ return clone $this->currentDate;
+
+ }
+
+ /**
+ * This method returns the enddate for the current iteration of the
+ * event.
+ *
+ * @return DateTime
+ */
+ public function getDtEnd() {
+
+ if (!$this->valid()) return null;
+ $dtEnd = clone $this->currentDate;
+ $dtEnd->add( $this->startDate->diff( $this->endDate ) );
+ return clone $dtEnd;
+
+ }
+
+ /**
+ * Returns a VEVENT object with the updated start and end date.
+ *
+ * Any recurrence information is removed, and this function may return an
+ * 'overridden' event instead.
+ *
+ * This method always returns a cloned instance.
+ *
+ * @return Component\VEvent
+ */
+ public function getEventObject() {
+
+ if ($this->currentOverriddenEvent) {
+ return clone $this->currentOverriddenEvent;
+ }
+ $event = clone $this->baseEvent;
+ unset($event->RRULE);
+ unset($event->EXDATE);
+ unset($event->RDATE);
+ unset($event->EXRULE);
+
+ $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
+ if (isset($event->DTEND)) {
+ $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
+ }
+ if ($this->counter > 0) {
+ $event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
+ }
+
+ return $event;
+
+ }
+
+ /**
+ * Returns the current item number
+ *
+ * @return int
+ */
+ public function key() {
+
+ return $this->counter;
+
+ }
+
+ /**
+ * Whether or not there is a 'next item'
+ *
+ * @return bool
+ */
+ public function valid() {
+
+ if (!is_null($this->count)) {
+ return $this->counter < $this->count;
+ }
+ if (!is_null($this->until) && $this->currentDate > $this->until) {
+
+ // Need to make sure there's no overridden events past the
+ // until date.
+ foreach($this->overriddenEvents as $overriddenEvent) {
+
+ if ($overriddenEvent->DTSTART->getDateTime() >= $this->currentDate) {
+
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * Resets the iterator
+ *
+ * @return void
+ */
+ public function rewind() {
+
+ $this->currentDate = clone $this->startDate;
+ $this->counter = 0;
+
+ }
+
+ /**
+ * This method allows you to quickly go to the next occurrence after the
+ * specified date.
+ *
+ * Note that this checks the current 'endDate', not the 'stardDate'. This
+ * means that if you forward to January 1st, the iterator will stop at the
+ * first event that ends *after* January 1st.
+ *
+ * @param DateTime $dt
+ * @return void
+ */
+ public function fastForward(\DateTime $dt) {
+
+ while($this->valid() && $this->getDTEnd() <= $dt) {
+ $this->next();
+ }
+
+ }
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ public function isInfinite() {
+
+ return !$this->count && !$this->until;
+
+ }
+
+ /**
+ * Goes on to the next iteration
+ *
+ * @return void
+ */
+ public function next() {
+
+ $previousStamp = $this->currentDate->getTimeStamp();
+
+ // Finding the next overridden event in line, and storing that for
+ // later use.
+ $overriddenEvent = null;
+ $overriddenDate = null;
+ $this->currentOverriddenEvent = null;
+
+ foreach($this->overriddenEvents as $index=>$event) {
+ if ($index > $previousStamp) {
+ $overriddenEvent = $event;
+ $overriddenDate = clone $event->DTSTART->getDateTime();
+ break;
+ }
+ }
+
+ // If we have a stored 'next date', we will use that.
+ if ($this->nextDate) {
+ if (!$overriddenDate || $this->nextDate < $overriddenDate) {
+ $this->currentDate = $this->nextDate;
+ $currentStamp = $this->currentDate->getTimeStamp();
+ $this->nextDate = null;
+ } else {
+ $this->currentDate = clone $overriddenDate;
+ $this->currentOverriddenEvent = $overriddenEvent;
+ }
+ $this->counter++;
+ return;
+ }
+
+ while(true) {
+
+ // Otherwise, we find the next event in the normal RRULE
+ // sequence.
+ switch($this->frequency) {
+
+ case 'hourly' :
+ $this->nextHourly();
+ break;
+
+ case 'daily' :
+ $this->nextDaily();
+ break;
+
+ case 'weekly' :
+ $this->nextWeekly();
+ break;
+
+ case 'monthly' :
+ $this->nextMonthly();
+ break;
+
+ case 'yearly' :
+ $this->nextYearly();
+ break;
+
+ }
+ $currentStamp = $this->currentDate->getTimeStamp();
+
+
+ // Checking exception dates
+ foreach($this->exceptionDates as $exceptionDate) {
+ if ($this->currentDate == $exceptionDate) {
+ $this->counter++;
+ continue 2;
+ }
+ }
+ foreach($this->overriddenDates as $check) {
+ if ($this->currentDate == $check) {
+ continue 2;
+ }
+ }
+ break;
+
+ }
+
+
+
+ // Is the date we have actually higher than the next overiddenEvent?
+ if ($overriddenDate && $this->currentDate > $overriddenDate) {
+ $this->nextDate = clone $this->currentDate;
+ $this->currentDate = clone $overriddenDate;
+ $this->currentOverriddenEvent = $overriddenEvent;
+ $this->handledOverridden++;
+ }
+ $this->counter++;
+
+
+ /*
+ * If we have overridden events left in the queue, but our counter is
+ * running out, we should grab one of those.
+ */
+ if (!is_null($overriddenEvent) && !is_null($this->count) && count($this->overriddenEvents) - $this->handledOverridden >= ($this->count - $this->counter)) {
+
+ $this->currentOverriddenEvent = $overriddenEvent;
+ $this->currentDate = clone $overriddenDate;
+ $this->handledOverridden++;
+
+ }
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for hourly frequency.
+ *
+ * @return void
+ */
+ protected function nextHourly() {
+
+ if (!$this->byHour) {
+ $this->currentDate->modify('+' . $this->interval . ' hours');
+ return;
+ }
+ }
+
+ /**
+ * Does the processing for advancing the iterator for daily frequency.
+ *
+ * @return void
+ */
+ protected function nextDaily() {
+
+ if (!$this->byHour && !$this->byDay) {
+ $this->currentDate->modify('+' . $this->interval . ' days');
+ return;
+ }
+
+ if (isset($this->byHour)) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ if (isset($this->byDay)) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ do {
+
+ if ($this->byHour) {
+ if ($this->currentDate->format('G') == '23') {
+ // to obey the interval rule
+ $this->currentDate->modify('+' . $this->interval-1 . ' days');
+ }
+
+ $this->currentDate->modify('+1 hours');
+
+ } else {
+ $this->currentDate->modify('+' . $this->interval . ' days');
+
+ }
+
+ // Current day of the week
+ $currentDay = $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = $this->currentDate->format('G');
+
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for weekly frequency.
+ *
+ * @return void
+ */
+ protected function nextWeekly() {
+
+ if (!$this->byHour && !$this->byDay) {
+ $this->currentDate->modify('+' . $this->interval . ' weeks');
+ return;
+ }
+
+ if ($this->byHour) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ if ($this->byDay) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ // First day of the week:
+ $firstDay = $this->dayMap[$this->weekStart];
+
+ do {
+
+ if ($this->byHour) {
+ $this->currentDate->modify('+1 hours');
+ } else {
+ $this->currentDate->modify('+1 days');
+ }
+
+ // Current day of the week
+ $currentDay = (int) $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = (int) $this->currentDate->format('G');
+
+ // We need to roll over to the next week
+ if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
+ $this->currentDate->modify('+' . $this->interval-1 . ' weeks');
+
+ // We need to go to the first day of this week, but only if we
+ // are not already on this first day of this week.
+ if($this->currentDate->format('w') != $firstDay) {
+ $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
+ }
+ }
+
+ // We have a match
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+ }
+
+ /**
+ * Does the processing for advancing the iterator for monthly frequency.
+ *
+ * @return void
+ */
+ protected function nextMonthly() {
+
+ $currentDayOfMonth = $this->currentDate->format('j');
+ if (!$this->byMonthDay && !$this->byDay) {
+
+ // If the current day is higher than the 28th, rollover can
+ // occur to the next month. We Must skip these invalid
+ // entries.
+ if ($currentDayOfMonth < 29) {
+ $this->currentDate->modify('+' . $this->interval . ' months');
+ } else {
+ $increase = 0;
+ do {
+ $increase++;
+ $tempDate = clone $this->currentDate;
+ $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
+ } while ($tempDate->format('j') != $currentDayOfMonth);
+ $this->currentDate = $tempDate;
+ }
+ return;
+ }
+
+ while(true) {
+
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach($occurrences as $occurrence) {
+
+ // The first occurrence thats higher than the current
+ // day of the month wins.
+ if ($occurrence > $currentDayOfMonth) {
+ break 2;
+ }
+
+ }
+
+ // If we made it all the way here, it means there were no
+ // valid occurrences, and we need to advance to the next
+ // month.
+ $this->currentDate->modify('first day of this month');
+ $this->currentDate->modify('+ ' . $this->interval . ' months');
+
+ // This goes to 0 because we need to start counting at hte
+ // beginning.
+ $currentDayOfMonth = 0;
+
+ }
+
+ $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for yearly frequency.
+ *
+ * @return void
+ */
+ protected function nextYearly() {
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ // No sub-rules, so we just advance by year
+ if (!$this->byMonth) {
+
+ // Unless it was a leap day!
+ if ($currentMonth==2 && $currentDayOfMonth==29) {
+
+ $counter = 0;
+ do {
+ $counter++;
+ // Here we increase the year count by the interval, until
+ // we hit a date that's also in a leap year.
+ //
+ // We could just find the next interval that's dividable by
+ // 4, but that would ignore the rule that there's no leap
+ // year every year that's dividable by a 100, but not by
+ // 400. (1800, 1900, 2100). So we just rely on the datetime
+ // functions instead.
+ $nextDate = clone $this->currentDate;
+ $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
+ } while ($nextDate->format('n')!=2);
+ $this->currentDate = $nextDate;
+
+ return;
+
+ }
+
+ // The easiest form
+ $this->currentDate->modify('+' . $this->interval . ' years');
+ return;
+
+ }
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ $advancedToNewMonth = false;
+
+ // If we got a byDay or getMonthDay filter, we must first expand
+ // further.
+ if ($this->byDay || $this->byMonthDay) {
+
+ while(true) {
+
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach($occurrences as $occurrence) {
+
+ // The first occurrence that's higher than the current
+ // day of the month wins.
+ // If we advanced to the next month or year, the first
+ // occurrence is always correct.
+ if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
+ break 2;
+ }
+
+ }
+
+ // If we made it here, it means we need to advance to
+ // the next month or year.
+ $currentDayOfMonth = 1;
+ $advancedToNewMonth = true;
+ do {
+
+ $currentMonth++;
+ if ($currentMonth>12) {
+ $currentYear+=$this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+
+ $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+ }
+
+ // If we made it here, it means we got a valid occurrence
+ $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
+ return;
+
+ } else {
+
+ // These are the 'byMonth' rules, if there are no byDay or
+ // byMonthDay sub-rules.
+ do {
+
+ $currentMonth++;
+ if ($currentMonth>12) {
+ $currentYear+=$this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+ $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+ return;
+
+ }
+
+ }
+
+ /**
+ * Returns all the occurrences for a monthly frequency with a 'byDay' or
+ * 'byMonthDay' expansion for the current month.
+ *
+ * The returned list is an array of integers with the day of month (1-31).
+ *
+ * @return array
+ */
+ protected function getMonthlyOccurrences() {
+
+ $startDate = clone $this->currentDate;
+
+ $byDayResults = array();
+
+ // Our strategy is to simply go through the byDays, advance the date to
+ // that point and add it to the results.
+ if ($this->byDay) foreach($this->byDay as $day) {
+
+ $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
+
+ // Dayname will be something like 'wednesday'. Now we need to find
+ // all wednesdays in this month.
+ $dayHits = array();
+
+ $checkDate = clone $startDate;
+ $checkDate->modify('first day of this month');
+ $checkDate->modify($dayName);
+
+ do {
+ $dayHits[] = $checkDate->format('j');
+ $checkDate->modify('next ' . $dayName);
+ } while ($checkDate->format('n') === $startDate->format('n'));
+
+ // So now we have 'all wednesdays' for month. It is however
+ // possible that the user only really wanted the 1st, 2nd or last
+ // wednesday.
+ if (strlen($day)>2) {
+ $offset = (int)substr($day,0,-2);
+
+ if ($offset>0) {
+ // It is possible that the day does not exist, such as a
+ // 5th or 6th wednesday of the month.
+ if (isset($dayHits[$offset-1])) {
+ $byDayResults[] = $dayHits[$offset-1];
+ }
+ } else {
+
+ // if it was negative we count from the end of the array
+ $byDayResults[] = $dayHits[count($dayHits) + $offset];
+ }
+ } else {
+ // There was no counter (first, second, last wednesdays), so we
+ // just need to add the all to the list).
+ $byDayResults = array_merge($byDayResults, $dayHits);
+
+ }
+
+ }
+
+ $byMonthDayResults = array();
+ if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
+
+ // Removing values that are out of range for this month
+ if ($monthDay > $startDate->format('t') ||
+ $monthDay < 0-$startDate->format('t')) {
+ continue;
+ }
+ if ($monthDay>0) {
+ $byMonthDayResults[] = $monthDay;
+ } else {
+ // Negative values
+ $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
+ }
+ }
+
+ // If there was just byDay or just byMonthDay, they just specify our
+ // (almost) final list. If both were provided, then byDay limits the
+ // list.
+ if ($this->byMonthDay && $this->byDay) {
+ $result = array_intersect($byMonthDayResults, $byDayResults);
+ } elseif ($this->byMonthDay) {
+ $result = $byMonthDayResults;
+ } else {
+ $result = $byDayResults;
+ }
+ $result = array_unique($result);
+ sort($result, SORT_NUMERIC);
+
+ // The last thing that needs checking is the BYSETPOS. If it's set, it
+ // means only certain items in the set survive the filter.
+ if (!$this->bySetPos) {
+ return $result;
+ }
+
+ $filteredResult = array();
+ foreach($this->bySetPos as $setPos) {
+
+ if ($setPos<0) {
+ $setPos = count($result)-($setPos+1);
+ }
+ if (isset($result[$setPos-1])) {
+ $filteredResult[] = $result[$setPos-1];
+ }
+ }
+
+ sort($filteredResult, SORT_NUMERIC);
+ return $filteredResult;
+
+ }
+
+ protected function getHours()
+ {
+ $recurrenceHours = array();
+ foreach($this->byHour as $byHour) {
+ $recurrenceHours[] = $byHour;
+ }
+
+ return $recurrenceHours;
+ }
+
+ protected function getDays()
+ {
+ $recurrenceDays = array();
+ foreach($this->byDay as $byDay) {
+
+ // The day may be preceeded with a positive (+n) or
+ // negative (-n) integer. However, this does not make
+ // sense in 'weekly' so we ignore it here.
+ $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
+
+ }
+
+ return $recurrenceDays;
+ }
+}
+
diff --git a/libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php b/libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php
new file mode 100644
index 0000000..5fc1e68
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+use OldSabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up iCalendar objects.
+ *
+ * This class expects a single VCALENDAR object with one or more
+ * calendar-objects inside. Objects with identical UID's will be combined into
+ * a single object.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ICalendar implements SplitterInterface {
+
+ /**
+ * Timezones
+ *
+ * @var array
+ */
+ protected $vtimezones = array();
+
+ /**
+ * iCalendar objects
+ *
+ * @var array
+ */
+ protected $objects = array();
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ public function __construct($input) {
+
+ $data = VObject\Reader::read(stream_get_contents($input));
+ $vtimezones = array();
+ $components = array();
+
+ foreach($data->children as $component) {
+ if (!$component instanceof VObject\Component) {
+ continue;
+ }
+
+ // Get all timezones
+ if ($component->name === 'VTIMEZONE') {
+ $this->vtimezones[(string)$component->TZID] = $component;
+ continue;
+ }
+
+ // Get component UID for recurring Events search
+ if($component->UID) {
+ $uid = (string)$component->UID;
+ } else {
+ // Generating a random UID
+ $uid = sha1(microtime()) . '-vobjectimport';
+ }
+
+ // Take care of recurring events
+ if (!array_key_exists($uid, $this->objects)) {
+ $this->objects[$uid] = VObject\Component::create('VCALENDAR');
+ }
+
+ $this->objects[$uid]->add(clone $component);
+ }
+
+ }
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ public function getNext() {
+
+ if($object=array_shift($this->objects)) {
+
+ // create our baseobject
+ $object->version = '2.0';
+ $object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ $object->calscale = 'GREGORIAN';
+
+ // add vtimezone information to obj (if we have it)
+ foreach ($this->vtimezones as $vtimezone) {
+ $object->add($vtimezone);
+ }
+
+ return $object;
+
+ } else {
+
+ return null;
+
+ }
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php b/libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php
new file mode 100644
index 0000000..ed73c39
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+/**
+ * VObject splitter
+ *
+ * The splitter is responsible for reading a large vCard or iCalendar object,
+ * and splitting it into multiple objects.
+ *
+ * This is for example for Card and CalDAV, which require every event and vcard
+ * to exist in their own objects, instead of one large one.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SplitterInterface {
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ function __construct($input);
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ function getNext();
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php b/libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php
new file mode 100644
index 0000000..90a5e69
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+use OldSabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up VCard objects.
+ *
+ * It is assumed that the input stream contains 1 or more VCARD objects. This
+ * class checks for BEGIN:VCARD and END:VCARD and parses each encountered
+ * component individually.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VCard implements SplitterInterface {
+
+ /**
+ * File handle
+ *
+ * @var resource
+ */
+ protected $input;
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ public function __construct($input) {
+
+ $this->input = $input;
+
+ }
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ public function getNext() {
+
+ $vcard = '';
+
+ do {
+
+ if (feof($this->input)) {
+ return false;
+ }
+
+ $line = fgets($this->input);
+ $vcard .= $line;
+
+ } while(strtoupper(substr($line,0,4))!=="END:");
+
+ $object = VObject\Reader::read($vcard);
+
+ if($object->name !== 'VCARD') {
+ throw new \InvalidArgumentException("Thats no vCard!", 1);
+ }
+
+ return $object;
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/StringUtil.php b/libcalendaring/lib/OldSabre/VObject/StringUtil.php
new file mode 100644
index 0000000..e6e2aa3
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/StringUtil.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Useful utilities for working with various strings.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class StringUtil {
+
+ /**
+ * Returns true or false depending on if a string is valid UTF-8
+ *
+ * @param string $str
+ * @return bool
+ */
+ static function isUTF8($str) {
+
+ // First check.. mb_check_encoding
+ if (!mb_check_encoding($str, 'UTF-8')) {
+ return false;
+ }
+
+ // Control characters
+ if (preg_match('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', $str)) {
+ return false;
+ }
+
+ return true;
+
+ }
+
+ /**
+ * This method tries its best to convert the input string to UTF-8.
+ *
+ * Currently only ISO-5991-1 input and UTF-8 input is supported, but this
+ * may be expanded upon if we receive other examples.
+ *
+ * @param string $str
+ * @return string
+ */
+ static function convertToUTF8($str) {
+
+ $encoding = mb_detect_encoding($str , array('UTF-8','ISO-8859-1'), true);
+
+ if ($encoding === 'ISO-8859-1') {
+ $newStr = utf8_encode($str);
+ } else {
+ $newStr = $str;
+ }
+
+ // Removing any control characters
+ return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', '', $newStr));
+
+ }
+
+}
+
diff --git a/libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php b/libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php
new file mode 100644
index 0000000..90e0ae1
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php
@@ -0,0 +1,527 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Time zone name translation
+ *
+ * This file translates well-known time zone names into "Olson database" time zone names.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Frank Edelhaeuser (fedel@users.sourceforge.net)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class TimeZoneUtil {
+
+ public static $map = array(
+
+ // from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
+ // snapshot taken on 2012/01/16
+
+ // windows
+ 'AUS Central Standard Time'=>'Australia/Darwin',
+ 'AUS Eastern Standard Time'=>'Australia/Sydney',
+ 'Afghanistan Standard Time'=>'Asia/Kabul',
+ 'Alaskan Standard Time'=>'America/Anchorage',
+ 'Arab Standard Time'=>'Asia/Riyadh',
+ 'Arabian Standard Time'=>'Asia/Dubai',
+ 'Arabic Standard Time'=>'Asia/Baghdad',
+ 'Argentina Standard Time'=>'America/Buenos_Aires',
+ 'Armenian Standard Time'=>'Asia/Yerevan',
+ 'Atlantic Standard Time'=>'America/Halifax',
+ 'Azerbaijan Standard Time'=>'Asia/Baku',
+ 'Azores Standard Time'=>'Atlantic/Azores',
+ 'Bangladesh Standard Time'=>'Asia/Dhaka',
+ 'Canada Central Standard Time'=>'America/Regina',
+ 'Cape Verde Standard Time'=>'Atlantic/Cape_Verde',
+ 'Caucasus Standard Time'=>'Asia/Yerevan',
+ 'Cen. Australia Standard Time'=>'Australia/Adelaide',
+ 'Central America Standard Time'=>'America/Guatemala',
+ 'Central Asia Standard Time'=>'Asia/Almaty',
+ 'Central Brazilian Standard Time'=>'America/Cuiaba',
+ 'Central Europe Standard Time'=>'Europe/Budapest',
+ 'Central European Standard Time'=>'Europe/Warsaw',
+ 'Central Pacific Standard Time'=>'Pacific/Guadalcanal',
+ 'Central Standard Time'=>'America/Chicago',
+ 'Central Standard Time (Mexico)'=>'America/Mexico_City',
+ 'China Standard Time'=>'Asia/Shanghai',
+ 'Dateline Standard Time'=>'Etc/GMT+12',
+ 'E. Africa Standard Time'=>'Africa/Nairobi',
+ 'E. Australia Standard Time'=>'Australia/Brisbane',
+ 'E. Europe Standard Time'=>'Europe/Minsk',
+ 'E. South America Standard Time'=>'America/Sao_Paulo',
+ 'Eastern Standard Time'=>'America/New_York',
+ 'Egypt Standard Time'=>'Africa/Cairo',
+ 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg',
+ 'FLE Standard Time'=>'Europe/Kiev',
+ 'Fiji Standard Time'=>'Pacific/Fiji',
+ 'GMT Standard Time'=>'Europe/London',
+ 'GTB Standard Time'=>'Europe/Istanbul',
+ 'Georgian Standard Time'=>'Asia/Tbilisi',
+ 'Greenland Standard Time'=>'America/Godthab',
+ 'Greenwich Standard Time'=>'Atlantic/Reykjavik',
+ 'Hawaiian Standard Time'=>'Pacific/Honolulu',
+ 'India Standard Time'=>'Asia/Calcutta',
+ 'Iran Standard Time'=>'Asia/Tehran',
+ 'Israel Standard Time'=>'Asia/Jerusalem',
+ 'Jordan Standard Time'=>'Asia/Amman',
+ 'Kamchatka Standard Time'=>'Asia/Kamchatka',
+ 'Korea Standard Time'=>'Asia/Seoul',
+ 'Magadan Standard Time'=>'Asia/Magadan',
+ 'Mauritius Standard Time'=>'Indian/Mauritius',
+ 'Mexico Standard Time'=>'America/Mexico_City',
+ 'Mexico Standard Time 2'=>'America/Chihuahua',
+ 'Mid-Atlantic Standard Time'=>'Etc/GMT-2',
+ 'Middle East Standard Time'=>'Asia/Beirut',
+ 'Montevideo Standard Time'=>'America/Montevideo',
+ 'Morocco Standard Time'=>'Africa/Casablanca',
+ 'Mountain Standard Time'=>'America/Denver',
+ 'Mountain Standard Time (Mexico)'=>'America/Chihuahua',
+ 'Myanmar Standard Time'=>'Asia/Rangoon',
+ 'N. Central Asia Standard Time'=>'Asia/Novosibirsk',
+ 'Namibia Standard Time'=>'Africa/Windhoek',
+ 'Nepal Standard Time'=>'Asia/Katmandu',
+ 'New Zealand Standard Time'=>'Pacific/Auckland',
+ 'Newfoundland Standard Time'=>'America/St_Johns',
+ 'North Asia East Standard Time'=>'Asia/Irkutsk',
+ 'North Asia Standard Time'=>'Asia/Krasnoyarsk',
+ 'Pacific SA Standard Time'=>'America/Santiago',
+ 'Pacific Standard Time'=>'America/Los_Angeles',
+ 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel',
+ 'Pakistan Standard Time'=>'Asia/Karachi',
+ 'Paraguay Standard Time'=>'America/Asuncion',
+ 'Romance Standard Time'=>'Europe/Paris',
+ 'Russian Standard Time'=>'Europe/Moscow',
+ 'SA Eastern Standard Time'=>'America/Cayenne',
+ 'SA Pacific Standard Time'=>'America/Bogota',
+ 'SA Western Standard Time'=>'America/La_Paz',
+ 'SE Asia Standard Time'=>'Asia/Bangkok',
+ 'Samoa Standard Time'=>'Pacific/Apia',
+ 'Singapore Standard Time'=>'Asia/Singapore',
+ 'South Africa Standard Time'=>'Africa/Johannesburg',
+ 'Sri Lanka Standard Time'=>'Asia/Colombo',
+ 'Syria Standard Time'=>'Asia/Damascus',
+ 'Taipei Standard Time'=>'Asia/Taipei',
+ 'Tasmania Standard Time'=>'Australia/Hobart',
+ 'Tokyo Standard Time'=>'Asia/Tokyo',
+ 'Tonga Standard Time'=>'Pacific/Tongatapu',
+ 'US Eastern Standard Time'=>'America/Indianapolis',
+ 'US Mountain Standard Time'=>'America/Phoenix',
+ 'UTC+12'=>'Etc/GMT-12',
+ 'UTC-02'=>'Etc/GMT+2',
+ 'UTC-11'=>'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time'=>'America/Caracas',
+ 'Vladivostok Standard Time'=>'Asia/Vladivostok',
+ 'W. Australia Standard Time'=>'Australia/Perth',
+ 'W. Central Africa Standard Time'=>'Africa/Lagos',
+ 'W. Europe Standard Time'=>'Europe/Berlin',
+ 'West Asia Standard Time'=>'Asia/Tashkent',
+ 'West Pacific Standard Time'=>'Pacific/Port_Moresby',
+ 'Yakutsk Standard Time'=>'Asia/Yakutsk',
+
+ // Microsoft exchange timezones
+ // Source:
+ // http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx
+ //
+ // Correct timezones deduced with help from:
+ // http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ 'Universal Coordinated Time' => 'UTC',
+ 'Casablanca, Monrovia' => 'Africa/Casablanca',
+ 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
+ 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
+ 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
+ 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
+ 'Prague, Central Europe' => 'Europe/Prague',
+ 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo',
+ 'West Central Africa' => 'Africa/Luanda', // This was a best guess
+ 'Athens, Istanbul, Minsk' => 'Europe/Athens',
+ 'Bucharest' => 'Europe/Bucharest',
+ 'Cairo' => 'Africa/Cairo',
+ 'Harare, Pretoria' => 'Africa/Harare',
+ 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki',
+ 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem',
+ 'Baghdad' => 'Asia/Baghdad',
+ 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait',
+ 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ 'East Africa, Nairobi' => 'Africa/Nairobi',
+ 'Tehran' => 'Asia/Tehran',
+ 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess
+ 'Baku, Tbilisi, Yerevan' => 'Asia/Baku',
+ 'Kabul' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
+ 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
+ 'Kathmandu, Nepal' => 'Asia/Kathmandu',
+ 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
+ 'Astana, Dhaka' => 'Asia/Dhaka',
+ 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
+ 'Rangoon' => 'Asia/Rangoon',
+ 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ 'Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
+ 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk',
+ 'Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ 'Perth, Western Australia' => 'Australia/Perth',
+ 'Taipei' => 'Asia/Taipei',
+ 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ 'Seoul, Korea Standard time' => 'Asia/Seoul',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'Adelaide, Central Australia' => 'Australia/Adelaide',
+ 'Darwin' => 'Australia/Darwin',
+ 'Brisbane, East Australia' => 'Australia/Brisbane',
+ 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney',
+ 'Guam, Port Moresby' => 'Pacific/Guam',
+ 'Hobart, Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan',
+ 'Auckland, Wellington' => 'Pacific/Auckland',
+ 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji',
+ 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu',
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde Is.' => 'Atlantic/Cape_Verde',
+ 'Mid-Atlantic' => 'America/Noronha',
+ 'Brasilia' => 'America/Sao_Paulo', // Best guess
+ 'Buenos Aires' => 'America/Argentina/Buenos_Aires',
+ 'Greenland' => 'America/Godthab',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Atlantic Time (Canada)' => 'America/Halifax',
+ 'Caracas, La Paz' => 'America/Caracas',
+ 'Santiago' => 'America/Santiago',
+ 'Bogota, Lima, Quito' => 'America/Bogota',
+ 'Eastern Time (US & Canada)' => 'America/New_York',
+ 'Indiana (East)' => 'America/Indiana/Indianapolis',
+ 'Central America' => 'America/Guatemala',
+ 'Central Time (US & Canada)' => 'America/Chicago',
+ 'Mexico City, Tegucigalpa' => 'America/Mexico_City',
+ 'Saskatchewan' => 'America/Edmonton',
+ 'Arizona' => 'America/Phoenix',
+ 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess
+ 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess
+ 'Alaska' => 'America/Anchorage',
+ 'Hawaii' => 'Pacific/Honolulu',
+ 'Midway Island, Samoa' => 'Pacific/Midway',
+ 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein',
+
+ // The following list are timezone names that could be generated by
+ // Lotus / Domino
+ 'Dateline' => 'Etc/GMT-12',
+ 'Samoa' => 'Pacific/Apia',
+ 'Hawaiian' => 'Pacific/Honolulu',
+ 'Alaskan' => 'America/Anchorage',
+ 'Pacific' => 'America/Los_Angeles',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Mexico Standard Time 2' => 'America/Chihuahua',
+ 'Mountain' => 'America/Denver',
+ 'Mountain Standard Time' => 'America/Chihuahua',
+ 'US Mountain' => 'America/Phoenix',
+ 'Canada Central' => 'America/Edmonton',
+ 'Central America' => 'America/Guatemala',
+ 'Central' => 'America/Chicago',
+ 'Central Standard Time' => 'America/Mexico_City',
+ 'Mexico' => 'America/Mexico_City',
+ 'Eastern' => 'America/New_York',
+ 'SA Pacific' => 'America/Bogota',
+ 'US Eastern' => 'America/Indiana/Indianapolis',
+ 'Venezuela' => 'America/Caracas',
+ 'Atlantic' => 'America/Halifax',
+ 'Central Brazilian' => 'America/Manaus',
+ 'Pacific SA' => 'America/Santiago',
+ 'SA Western' => 'America/La_Paz',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Argentina' => 'America/Argentina/Buenos_Aires',
+ 'E. South America' => 'America/Belem',
+ 'Greenland' => 'America/Godthab',
+ 'Montevideo' => 'America/Montevideo',
+ 'SA Eastern' => 'America/Belem',
+ 'Mid-Atlantic' => 'Etc/GMT-2',
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde' => 'Atlantic/Cape_Verde',
+ 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
+ 'Morocco' => 'Africa/Casablanca',
+ 'Central Europe' => 'Europe/Prague',
+ 'Central European' => 'Europe/Sarajevo',
+ 'Romance' => 'Europe/Paris',
+ 'W. Central Africa' => 'Africa/Lagos', // Best guess
+ 'W. Europe' => 'Europe/Amsterdam',
+ 'E. Europe' => 'Europe/Minsk',
+ 'Egypt' => 'Africa/Cairo',
+ 'FLE' => 'Europe/Helsinki',
+ 'GTB' => 'Europe/Athens',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jordan' => 'Asia/Amman',
+ 'Middle East' => 'Asia/Beirut',
+ 'Namibia' => 'Africa/Windhoek',
+ 'South Africa' => 'Africa/Harare',
+ 'Arab' => 'Asia/Kuwait',
+ 'Arabic' => 'Asia/Baghdad',
+ 'E. Africa' => 'Africa/Nairobi',
+ 'Georgian' => 'Asia/Tbilisi',
+ 'Russian' => 'Europe/Moscow',
+ 'Iran' => 'Asia/Tehran',
+ 'Arabian' => 'Asia/Muscat',
+ 'Armenian' => 'Asia/Yerevan',
+ 'Azerbijan' => 'Asia/Baku',
+ 'Caucasus' => 'Asia/Yerevan',
+ 'Mauritius' => 'Indian/Mauritius',
+ 'Afghanistan' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Pakistan' => 'Asia/Karachi',
+ 'West Asia' => 'Asia/Tashkent',
+ 'India' => 'Asia/Calcutta',
+ 'Sri Lanka' => 'Asia/Colombo',
+ 'Nepal' => 'Asia/Kathmandu',
+ 'Central Asia' => 'Asia/Dhaka',
+ 'N. Central Asia' => 'Asia/Almaty',
+ 'Myanmar' => 'Asia/Rangoon',
+ 'North Asia' => 'Asia/Krasnoyarsk',
+ 'SE Asia' => 'Asia/Bangkok',
+ 'China' => 'Asia/Shanghai',
+ 'North Asia East' => 'Asia/Irkutsk',
+ 'Singapore' => 'Asia/Singapore',
+ 'Taipei' => 'Asia/Taipei',
+ 'W. Australia' => 'Australia/Perth',
+ 'Korea' => 'Asia/Seoul',
+ 'Tokyo' => 'Asia/Tokyo',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'AUS Central' => 'Australia/Darwin',
+ 'Cen. Australia' => 'Australia/Adelaide',
+ 'AUS Eastern' => 'Australia/Sydney',
+ 'E. Australia' => 'Australia/Brisbane',
+ 'Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'West Pacific' => 'Pacific/Guam',
+ 'Central Pacific' => 'Asia/Magadan',
+ 'Fiji' => 'Pacific/Fiji',
+ 'New Zealand' => 'Pacific/Auckland',
+ 'Tonga' => 'Pacific/Tongatapu',
+
+ // PHP 5.5.10 failed on a few timezones that were valid before. We're
+ // normalizing them here.
+ 'CST6CDT' => 'America/Chicago',
+ 'Cuba' => 'America/Havana',
+ 'Egypt' => 'Africa/Cairo',
+ 'Eire' => 'Europe/Dublin',
+ 'EST5EDT' => 'America/New_York',
+ 'Factory' => 'UTC',
+ 'GB-Eire' => 'Europe/London',
+ 'GMT0' => 'UTC',
+ 'Greenwich' => 'UTC',
+ 'Hongkong' => 'Asia/Hong_Kong',
+ 'Iceland' => 'Atlantic/Reykjavik',
+ 'Iran' => 'Asia/Tehran',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jamaica' => 'America/Jamaica',
+ 'Japan' => 'Asia/Tokyo',
+ 'Kwajalein' => 'Pacific/Kwajalein',
+ 'Libya' => 'Africa/Tripoli',
+ 'MST7MDT' => 'America/Denver',
+ 'Navajo' => 'America/Denver',
+ 'NZ-CHAT' => 'Pacific/Chatham',
+ 'Poland' => 'Europe/Warsaw',
+ 'Portugal' => 'Europe/Lisbon',
+ 'PST8PDT' => 'America/Los_Angeles',
+ 'Singapore' => 'Asia/Singapore',
+ 'Turkey' => 'Europe/Istanbul',
+ 'Universal' => 'UTC',
+ 'W-SU' => 'Europe/Moscow',
+ );
+
+ /**
+ * List of microsoft exchange timezone ids.
+ *
+ * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
+ */
+ public static $microsoftExchangeMap = array(
+ 0 => 'UTC',
+ 31 => 'Africa/Casablanca',
+
+ // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
+ // I'm not even kidding.. We handle this special case in the
+ // getTimeZone method.
+ 2 => 'Europe/Lisbon',
+ 1 => 'Europe/London',
+ 4 => 'Europe/Berlin',
+ 6 => 'Europe/Prague',
+ 3 => 'Europe/Paris',
+ 69 => 'Africa/Luanda', // This was a best guess
+ 7 => 'Europe/Athens',
+ 5 => 'Europe/Bucharest',
+ 49 => 'Africa/Cairo',
+ 50 => 'Africa/Harare',
+ 59 => 'Europe/Helsinki',
+ 27 => 'Asia/Jerusalem',
+ 26 => 'Asia/Baghdad',
+ 74 => 'Asia/Kuwait',
+ 51 => 'Europe/Moscow',
+ 56 => 'Africa/Nairobi',
+ 25 => 'Asia/Tehran',
+ 24 => 'Asia/Muscat', // Best guess
+ 54 => 'Asia/Baku',
+ 48 => 'Asia/Kabul',
+ 58 => 'Asia/Yekaterinburg',
+ 47 => 'Asia/Karachi',
+ 23 => 'Asia/Calcutta',
+ 62 => 'Asia/Kathmandu',
+ 46 => 'Asia/Almaty',
+ 71 => 'Asia/Dhaka',
+ 66 => 'Asia/Colombo',
+ 61 => 'Asia/Rangoon',
+ 22 => 'Asia/Bangkok',
+ 64 => 'Asia/Krasnoyarsk',
+ 45 => 'Asia/Shanghai',
+ 63 => 'Asia/Irkutsk',
+ 21 => 'Asia/Singapore',
+ 73 => 'Australia/Perth',
+ 75 => 'Asia/Taipei',
+ 20 => 'Asia/Tokyo',
+ 72 => 'Asia/Seoul',
+ 70 => 'Asia/Yakutsk',
+ 19 => 'Australia/Adelaide',
+ 44 => 'Australia/Darwin',
+ 18 => 'Australia/Brisbane',
+ 76 => 'Australia/Sydney',
+ 43 => 'Pacific/Guam',
+ 42 => 'Australia/Hobart',
+ 68 => 'Asia/Vladivostok',
+ 41 => 'Asia/Magadan',
+ 17 => 'Pacific/Auckland',
+ 40 => 'Pacific/Fiji',
+ 67 => 'Pacific/Tongatapu',
+ 29 => 'Atlantic/Azores',
+ 53 => 'Atlantic/Cape_Verde',
+ 30 => 'America/Noronha',
+ 8 => 'America/Sao_Paulo', // Best guess
+ 32 => 'America/Argentina/Buenos_Aires',
+ 60 => 'America/Godthab',
+ 28 => 'America/St_Johns',
+ 9 => 'America/Halifax',
+ 33 => 'America/Caracas',
+ 65 => 'America/Santiago',
+ 35 => 'America/Bogota',
+ 10 => 'America/New_York',
+ 34 => 'America/Indiana/Indianapolis',
+ 55 => 'America/Guatemala',
+ 11 => 'America/Chicago',
+ 37 => 'America/Mexico_City',
+ 36 => 'America/Edmonton',
+ 38 => 'America/Phoenix',
+ 12 => 'America/Denver', // Best guess
+ 13 => 'America/Los_Angeles', // Best guess
+ 14 => 'America/Anchorage',
+ 15 => 'Pacific/Honolulu',
+ 16 => 'Pacific/Midway',
+ 39 => 'Pacific/Kwajalein',
+ );
+
+ /**
+ * This method will try to find out the correct timezone for an iCalendar
+ * date-time value.
+ *
+ * You must pass the contents of the TZID parameter, as well as the full
+ * calendar.
+ *
+ * If the lookup fails, this method will return the default PHP timezone
+ * (as configured using date_default_timezone_set, or the date.timezone ini
+ * setting).
+ *
+ * Alternatively, if $failIfUncertain is set to true, it will throw an
+ * exception if we cannot accurately determine the timezone.
+ *
+ * @param string $tzid
+ * @param OldSabre\VObject\Component $vcalendar
+ * @return DateTimeZone
+ */
+ static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) {
+
+ // First we will just see if the tzid is a support timezone identifier.
+ //
+ // The only exception is if the timezone starts with (. This is to
+ // handle cases where certain microsoft products generate timezone
+ // identifiers that for instance look like:
+ //
+ // (GMT+01.00) Sarajevo/Warsaw/Zagreb
+ //
+ // Since PHP 5.5.10, the first bit will be used as the timezone and
+ // this method will return just GMT+01:00. This is wrong, because it
+ // doesn't take DST into account.
+ if ($tzid[0]!=='(') {
+ try {
+ return new \DateTimeZone($tzid);
+ } catch (\Exception $e) {
+ }
+ }
+
+ // Next, we check if the tzid is somewhere in our tzid map.
+ if (isset(self::$map[$tzid])) {
+ return new \DateTimeZone(self::$map[$tzid]);
+ }
+
+ // Maybe the author was hyper-lazy and just included an offset. We
+ // support it, but we aren't happy about it.
+ //
+ // Note that the path in the source will never be taken from PHP 5.5.10
+ // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
+ // already gets returned early in this function. Once we drop support
+ // for versions under PHP 5.5.10, this bit can be taken out of the
+ // source.
+ if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
+ return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0'));
+ }
+
+ if ($vcalendar) {
+
+ // If that didn't work, we will scan VTIMEZONE objects
+ foreach($vcalendar->select('VTIMEZONE') as $vtimezone) {
+
+ if ((string)$vtimezone->TZID === $tzid) {
+
+ // Some clients add 'X-LIC-LOCATION' with the olson name.
+ if (isset($vtimezone->{'X-LIC-LOCATION'})) {
+
+ $lic = (string)$vtimezone->{'X-LIC-LOCATION'};
+
+ // Libical generators may specify strings like
+ // "SystemV/EST5EDT". For those we must remove the
+ // SystemV part.
+ if (substr($lic,0,8)==='SystemV/') {
+ $lic = substr($lic,8);
+ }
+
+ return self::getTimeZone($lic, null, $failIfUncertain);
+
+ }
+ // Microsoft may add a magic number, which we also have an
+ // answer for.
+ if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
+ $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value;
+
+ // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
+ if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) {
+ return new \DateTimeZone('Europe/Sarajevo');
+ }
+
+ if (isset(self::$microsoftExchangeMap[$cdoId])) {
+ return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ if ($failIfUncertain) {
+ throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid);
+ }
+
+ // If we got all the way here, we default to UTC.
+ return new \DateTimeZone(date_default_timezone_get());
+
+ }
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/Version.php b/libcalendaring/lib/OldSabre/VObject/Version.php
new file mode 100644
index 0000000..7c3becb
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class contains the version number for the VObject package
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '2.1.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/libcalendaring/lib/OldSabre/VObject/includes.php b/libcalendaring/lib/OldSabre/VObject/includes.php
new file mode 100644
index 0000000..2735d0a
--- /dev/null
+++ b/libcalendaring/lib/OldSabre/VObject/includes.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Includes file
+ *
+ * This file includes the entire VObject library in one go.
+ * The benefit is that an autoloader is not needed, which is often faster.
+ *
+ * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+
+// Begin includes
+include __DIR__ . '/DateTimeParser.php';
+include __DIR__ . '/ElementList.php';
+include __DIR__ . '/FreeBusyGenerator.php';
+include __DIR__ . '/Node.php';
+include __DIR__ . '/Parameter.php';
+include __DIR__ . '/ParseException.php';
+include __DIR__ . '/Property.php';
+include __DIR__ . '/Reader.php';
+include __DIR__ . '/RecurrenceIterator.php';
+include __DIR__ . '/Splitter/SplitterInterface.php';
+include __DIR__ . '/StringUtil.php';
+include __DIR__ . '/TimeZoneUtil.php';
+include __DIR__ . '/Version.php';
+include __DIR__ . '/Splitter/VCard.php';
+include __DIR__ . '/Component.php';
+include __DIR__ . '/Document.php';
+include __DIR__ . '/Property/Compound.php';
+include __DIR__ . '/Property/DateTime.php';
+include __DIR__ . '/Property/MultiDateTime.php';
+include __DIR__ . '/Splitter/ICalendar.php';
+include __DIR__ . '/Component/VAlarm.php';
+include __DIR__ . '/Component/VCalendar.php';
+include __DIR__ . '/Component/VEvent.php';
+include __DIR__ . '/Component/VFreeBusy.php';
+include __DIR__ . '/Component/VJournal.php';
+include __DIR__ . '/Component/VTodo.php';
+// End includes
diff --git a/libcalendaring/lib/libcalendaring_itip.php b/libcalendaring/lib/libcalendaring_itip.php
new file mode 100644
index 0000000..a67a380
--- /dev/null
+++ b/libcalendaring/lib/libcalendaring_itip.php
@@ -0,0 +1,817 @@
+<?php
+
+/**
+ * iTIP functions for the calendar-based Roudncube plugins
+ *
+ * Class providing functionality to manage iTIP invitations
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2011-2014, 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/>.
+ */
+class libcalendaring_itip
+{
+ protected $rc;
+ protected $lib;
+ protected $plugin;
+ protected $sender;
+ protected $domain;
+ protected $itip_send = false;
+ protected $rsvp_actions = array('accepted','tentative','declined','delegated');
+ protected $rsvp_status = array('accepted','tentative','declined','delegated');
+
+ function __construct($plugin, $domain = 'libcalendaring')
+ {
+ $this->plugin = $plugin;
+ $this->rc = rcube::get_instance();
+ $this->lib = libcalendaring::get_instance();
+ $this->domain = $domain;
+
+ $hook = $this->rc->plugins->exec_hook('calendar_load_itip',
+ array('identity' => $this->rc->user->list_emails(true)));
+ $this->sender = $hook['identity'];
+
+ $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook'));
+ $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook'));
+ }
+
+ public function set_sender_email($email)
+ {
+ if (!empty($email))
+ $this->sender['email'] = $email;
+ }
+
+ public function set_rsvp_actions($actions)
+ {
+ $this->rsvp_actions = (array)$actions;
+ $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
+ }
+
+ public function set_rsvp_status($status)
+ {
+ $this->rsvp_status = $status;
+ }
+
+ /**
+ * Wrapper for rcube_plugin::gettext()
+ * Checking for a label in different domains
+ *
+ * @see rcube::gettext()
+ */
+ public function gettext($p)
+ {
+ $label = is_array($p) ? $p['name'] : $p;
+ $domain = $this->domain;
+ if (!$this->rc->text_exists($label, $domain)) {
+ $domain = 'libcalendaring';
+ }
+ return $this->rc->gettext($p, $domain);
+ }
+
+ /**
+ * Send an iTip mail message
+ *
+ * @param array Event object to send
+ * @param string iTip method (REQUEST|REPLY|CANCEL)
+ * @param array Hash array with recipient data (name, email)
+ * @param string Mail subject
+ * @param string Mail body text label
+ * @param object Mail_mime object with message data
+ * @param boolean Request RSVP
+ * @return boolean True on success, false on failure
+ */
+ public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true)
+ {
+ if (!$this->sender['name'])
+ $this->sender['name'] = $this->sender['email'];
+
+ if (!$message) {
+ libcalendaring::identify_recurrence_instance($event);
+ $message = $this->compose_itip_message($event, $method, $rsvp);
+ }
+
+ $mailto = rcube_idn_to_ascii($recipient['email']);
+
+ $headers = $message->headers();
+ $headers['To'] = format_email_recipient($mailto, $recipient['name']);
+ $headers['Subject'] = $this->gettext(array(
+ 'name' => $subject,
+ 'vars' => array(
+ 'title' => $event['title'],
+ 'name' => $this->sender['name']
+ )
+ ));
+
+ // compose a list of all event attendees
+ $attendees_list = array();
+ foreach ((array)$event['attendees'] as $attendee) {
+ $attendees_list[] = ($attendee['name'] && $attendee['email']) ?
+ $attendee['name'] . ' <' . $attendee['email'] . '>' :
+ ($attendee['name'] ? $attendee['name'] : $attendee['email']);
+ }
+
+ $recurrence_info = '';
+ if (!empty($event['recurrence_id'])) {
+ $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
+ }
+ else if (!empty($event['recurrence'])) {
+ $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
+ }
+
+ $mailbody = $this->gettext(array(
+ 'name' => $bodytext,
+ 'vars' => array(
+ 'title' => $event['title'],
+ 'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
+ 'attendees' => join(",\n ", $attendees_list),
+ 'sender' => $this->sender['name'],
+ 'organizer' => $this->sender['name'],
+ )
+ ));
+
+ // if (!empty($event['comment'])) {
+ // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment'];
+ // }
+
+ // append links for direct invitation replies
+ if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) {
+ $mailbody .= "\n\n" . $this->gettext(array(
+ 'name' => 'invitationattendlinks',
+ 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))),
+ ));
+ }
+ else if ($method == 'CANCEL' && $event['cancelled']) {
+ $this->cancel_itip_invitation($event);
+ }
+
+ $message->headers($headers, true);
+ $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
+
+ if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
+ console('iTip ' . $method, $message->txtHeaders() . "\n\r" . $message->get());
+ }
+
+ // finally send the message
+ $this->itip_send = true;
+ $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
+ $this->itip_send = false;
+
+ return $sent;
+ }
+
+ /**
+ * Plugin hook triggered by rcube::deliver_message() before delivering a message.
+ * Here we can set the 'smtp_server' config option to '' in order to use
+ * PHP's mail() function for unauthenticated email sending.
+ */
+ public function before_send_hook($p)
+ {
+ if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') {
+ $this->rc->config->set('smtp_server', '');
+ }
+
+ return $p;
+ }
+
+ /**
+ * Plugin hook to alter SMTP authentication.
+ * This is used if iTip messages are to be sent from an unauthenticated session
+ */
+ public function smtp_connect_hook($p)
+ {
+ // replace smtp auth settings if we're not in an authenticated session
+ if ($this->itip_send && !$this->rc->user->ID) {
+ foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) {
+ $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]);
+ }
+ }
+
+ return $p;
+ }
+
+ /**
+ * Helper function to build a Mail_mime object to send an iTip message
+ *
+ * @param array Event object to send
+ * @param string iTip method (REQUEST|REPLY|CANCEL)
+ * @param boolean Request RSVP
+ * @return object Mail_mime object with message data
+ */
+ public function compose_itip_message($event, $method, $rsvp = true)
+ {
+ $from = rcube_idn_to_ascii($this->sender['email']);
+ $from_utf = rcube_utils::idn_to_utf8($from);
+ $sender = format_email_recipient($from, $this->sender['name']);
+
+ // truncate list attendees down to the recipient of the iTip Reply.
+ // constraints for a METHOD:REPLY according to RFC 5546
+ if ($method == 'REPLY') {
+ $replying_attendee = null;
+ $reply_attendees = array();
+ foreach ($event['attendees'] as $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $reply_attendees[] = $attendee;
+ }
+ else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
+ $replying_attendee = $attendee;
+ if ($attendee['status'] != 'DELEGATED') {
+ unset($replying_attendee['rsvp']); // unset the RSVP attribute
+ }
+ }
+ // include attendees relevant for delegation (RFC 5546, Section 4.2.5)
+ else if ((!empty($attendee['delegated-to']) &&
+ (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) ||
+ (!empty($attendee['delegated-from']) &&
+ (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) {
+ $reply_attendees[] = $attendee;
+ }
+ }
+ if ($replying_attendee) {
+ array_unshift($reply_attendees, $replying_attendee);
+ $event['attendees'] = $reply_attendees;
+ }
+ if ($event['recurrence']) {
+ unset($event['recurrence']['EXCEPTIONS']);
+ }
+ }
+ // set RSVP for every attendee
+ else if ($method == 'REQUEST') {
+ foreach ($event['attendees'] as $i => $attendee) {
+ if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) {
+ $event['attendees'][$i]['rsvp']= (bool)$rsvp;
+ }
+ }
+ }
+ else if ($method == 'CANCEL') {
+ if ($event['recurrence']) {
+ unset($event['recurrence']['EXCEPTIONS']);
+ }
+ }
+
+ // compose multipart message using PEAR:Mail_Mime
+ $message = new Mail_mime("\r\n");
+ $message->setParam('text_encoding', 'quoted-printable');
+ $message->setParam('head_encoding', 'quoted-printable');
+ $message->setParam('head_charset', RCMAIL_CHARSET);
+ $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed");
+ $message->setContentType('multipart/alternative');
+
+ // compose common headers array
+ $headers = array(
+ 'From' => $sender,
+ 'Date' => $this->rc->user_date(),
+ 'Message-ID' => $this->rc->gen_message_id(),
+ 'X-Sender' => $from,
+ );
+ if ($agent = $this->rc->config->get('useragent')) {
+ $headers['User-Agent'] = $agent;
+ }
+
+ $message->headers($headers);
+
+ // attach ics file for this event
+ $ical = libcalendaring::get_ical();
+ $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
+ $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+ $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
+
+ return $message;
+ }
+
+ /**
+ * Forward the given iTip event as delegation to another person
+ *
+ * @param array Event object to delegate
+ * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
+ * @param boolean The delegator's RSVP flag
+ * @param array List with indexes of new/updated attendees
+ * @return boolean True on success, False on failure
+ */
+ public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
+ {
+ if (is_string($delegate)) {
+ $delegates = rcube_mime::decode_address_list($delegate, 1, false);
+ if (count($delegates) > 0) {
+ $delegate = reset($delegates);
+ }
+ }
+
+ $emails = $this->lib->get_user_emails();
+ $me = $this->rc->user->list_emails(true);
+
+ // find/create the delegate attendee
+ $delegate_attendee = array(
+ 'email' => $delegate['mailto'],
+ 'name' => $delegate['name'],
+ 'role' => 'REQ-PARTICIPANT',
+ );
+ $delegate_index = count($event['attendees']);
+
+ foreach ($event['attendees'] as $i => $attendee) {
+ // set myself the DELEGATED-TO parameter
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $event['attendees'][$i]['delegated-to'] = $delegate['mailto'];
+ $event['attendees'][$i]['status'] = 'DELEGATED';
+ $event['attendees'][$i]['role'] = 'NON-PARTICIPANT';
+ $event['attendees'][$i]['rsvp'] = $rsvp;
+
+ $me['email'] = $attendee['email'];
+ $delegate_attendee['role'] = $attendee['role'];
+ }
+ // the disired delegatee is already listed as an attendee
+ else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') {
+ $delegate_attendee = $attendee;
+ $delegate_index = $i;
+ break;
+ }
+ // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me)
+ }
+
+ // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
+ $delegate_attendee['rsvp'] = true;
+ $delegate_attendee['status'] = 'NEEDS-ACTION';
+ $delegate_attendee['delegated-from'] = $me['email'];
+ $event['attendees'][$delegate_index] = $delegate_attendee;
+
+ $attendees[] = $delegate_index;
+
+ $this->set_sender_email($me['email']);
+ return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
+ }
+
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ public function get_itip_status($event, $existing = null)
+ {
+ $action = $event['rsvp'] ? 'rsvp' : '';
+ $status = $event['fallback'];
+ $latest = false;
+ $html = '';
+
+ if (is_numeric($event['changed']))
+ $event['changed'] = new DateTime('@'.$event['changed']);
+
+ // check if the given itip object matches the last state
+ if ($existing) {
+ $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
+ (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
+ }
+
+ // determine action for REQUEST
+ if ($event['method'] == 'REQUEST') {
+ $html = html::div('rsvp-status', $this->gettext('acceptinvitation'));
+
+ if ($existing) {
+ $rsvp = $event['rsvp'];
+ $emails = $this->lib->get_user_emails();
+ foreach ($existing['attendees'] as $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $status = strtoupper($attendee['status']);
+ break;
+ }
+ }
+ }
+ else {
+ $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true);
+ }
+
+ $status_lc = strtolower($status);
+
+ if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) {
+ $html = html::div('rsvp-status', $this->gettext('notanattendee'));
+ $action = 'import';
+ }
+ else if (in_array($status_lc, $this->rsvp_status)) {
+ $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc);
+
+ if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) {
+ $action = ''; // nothing to do here, outdated invitation
+ if ($status_lc == 'needs-action')
+ $status_text = $this->gettext('outdatedinvitation');
+ }
+ else if (!$existing && !$rsvp) {
+ $action = 'import';
+ }
+ else if ($latest && $status_lc != 'needs-action') {
+ $action = 'update';
+ }
+
+ $html = html::div('rsvp-status ' . $status_lc, $status_text);
+ }
+ }
+ // determine action for REPLY
+ else if ($event['method'] == 'REPLY') {
+ // check whether the sender already is an attendee
+ if ($existing) {
+ $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : '';
+ $listed = false;
+ foreach ($existing['attendees'] as $attendee) {
+ if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
+ $status_lc = strtolower($status);
+ if (in_array($status_lc, $this->rsvp_status)) {
+ $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
+ 'name' => 'attendee' . $status_lc,
+ 'vars' => array(
+ 'delegatedto' => Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')),
+ )
+ )));
+ }
+ $action = $attendee['status'] == $status || !$latest ? '' : 'update';
+ $listed = true;
+ break;
+ }
+ }
+
+ if (!$listed) {
+ $html = html::div('rsvp-status', $this->gettext('itipnewattendee'));
+ }
+ }
+ else {
+ $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
+ $action = '';
+ }
+ }
+ else if ($event['method'] == 'CANCEL') {
+ if (!$existing) {
+ $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
+ $action = '';
+ }
+ }
+
+ return array(
+ 'uid' => $event['uid'],
+ 'id' => asciiwords($event['uid'], true),
+ 'existing' => $existing ? true : false,
+ 'saved' => $existing ? true : false,
+ 'latest' => $latest,
+ 'status' => $status,
+ 'action' => $action,
+ 'html' => $html,
+ );
+ }
+
+ /**
+ * Build inline UI elements for iTip messages
+ */
+ public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null)
+ {
+ $buttons = array();
+ $dom_id = asciiwords($event['uid'], true);
+ $rsvp_status = 'unknown';
+
+ // pass some metadata about the event and trigger the asynchronous status check
+ $changed = is_object($event['changed']) ? $event['changed'] : $message_date;
+ $metadata = array(
+ 'uid' => $event['uid'],
+ '_instance' => $event['_instance'],
+ 'changed' => $changed ? $changed->format('U') : 0,
+ 'sequence' => intval($event['sequence']),
+ 'method' => $method,
+ 'task' => $task,
+ );
+
+ // create buttons to be activated from async request checking existence of this event in local calendars
+ $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
+
+ // on iTip REPLY we have two options:
+ if ($method == 'REPLY') {
+ $title = $this->gettext('itipreply');
+
+ foreach ($event['attendees'] as $attendee) {
+ if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER' &&
+ (empty($event['_sender']) || ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf']))) {
+ $metadata['attendee'] = $attendee['email'];
+ $rsvp_status = strtoupper($attendee['status']);
+ if ($attendee['delegated-to'])
+ $metadata['delegated-to'] = $attendee['delegated-to'];
+ break;
+ }
+ }
+
+ // 1. update the attendee status on our copy
+ $update_button = html::tag('input', array(
+ 'type' => 'button',
+ 'class' => 'button',
+ 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')",
+ 'value' => $this->gettext('updateattendeestatus'),
+ ));
+
+ // 2. accept or decline a new or delegate attendee
+ $accept_buttons = html::tag('input', array(
+ 'type' => 'button',
+ 'class' => "button accept",
+ 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')",
+ 'value' => $this->gettext('acceptattendee'),
+ ));
+ $accept_buttons .= html::tag('input', array(
+ 'type' => 'button',
+ 'class' => "button decline",
+ 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . JQ($mime_id) . "', '$task')",
+ 'value' => $this->gettext('declineattendee'),
+ ));
+
+ $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
+ $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons);
+ }
+ // when receiving iTip REQUEST messages:
+ else if ($method == 'REQUEST') {
+ $emails = $this->lib->get_user_emails();
+ $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
+ $metadata['rsvp'] = true;
+ $metadata['sensitivity'] = $event['sensitivity'];
+
+ if (is_object($event['start'])) {
+ $metadata['date'] = $event['start']->format('U');
+ }
+
+ // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons
+ if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') {
+ $this->rsvp_actions = array('accepted','declined');
+ $metadata['nosave'] = true;
+ }
+
+ // 1. display RSVP buttons (if the user was invited)
+ foreach ($this->rsvp_actions as $method) {
+ $rsvp_buttons .= html::tag('input', array(
+ 'type' => 'button',
+ 'class' => "button $method",
+ 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task', '$method', '$dom_id')",
+ 'value' => $this->gettext('itip' . $method),
+ ));
+ }
+
+ // add button to open calendar/preview
+ if (!empty($preview_url)) {
+ $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id;
+ $rsvp_buttons .= html::tag('input', array(
+ 'type' => 'button',
+ 'class' => "button preview",
+ 'onclick' => "rcube_libcalendaring.open_itip_preview('" . JQ($preview_url) . "', '" . JQ($msgref) . "')",
+ 'value' => $this->gettext('openpreview'),
+ ));
+ }
+
+ // 2. update the local copy with minor changes
+ $update_button = html::tag('input', array(
+ 'type' => 'button',
+ 'class' => 'button',
+ 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')",
+ 'value' => $this->gettext('updatemycopy'),
+ ));
+
+ // 3. Simply import the event without replying
+ $import_button = html::tag('input', array(
+ 'type' => 'button',
+ 'class' => 'button',
+ 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')",
+ 'value' => $this->gettext('importtocalendar'),
+ ));
+
+ // check my status
+ foreach ($event['attendees'] as $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
+ $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION';
+ break;
+ }
+ }
+
+ // add itip reply message controls
+ $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave']));
+
+ $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
+ $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
+
+ // prepare autocompletion for delegation dialog
+ if (in_array('delegated', $this->rsvp_actions)) {
+ $this->rc->autocomplete_init();
+ }
+ }
+ // for CANCEL messages, we can:
+ else if ($method == 'CANCEL') {
+ $title = $this->gettext('itipcancellation');
+ $event_prop = array_filter(array(
+ 'uid' => $event['uid'],
+ '_instance' => $event['_instance'],
+ '_savemode' => $event['_savemode'],
+ ));
+
+ // 1. remove the event from our calendar
+ $button_remove = html::tag('input', array(
+ 'type' => 'button',
+ 'class' => 'button',
+ 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . JQ($event['title']) . "')",
+ 'value' => $this->gettext('removefromcalendar'),
+ ));
+
+ // 2. update our copy with status=cancelled
+ $button_update = html::tag('input', array(
+ 'type' => 'button',
+ 'class' => 'button',
+ 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')",
+ 'value' => $this->gettext('updatemycopy'),
+ ));
+
+ $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update);
+
+ $rsvp_status = 'CANCELLED';
+ $metadata['rsvp'] = true;
+ }
+
+ // append generic import button
+ if ($import_button) {
+ $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
+ }
+
+ // pass some metadata about the event and trigger the asynchronous status check
+ $metadata['fallback'] = $rsvp_status;
+ $metadata['rsvp'] = intval($metadata['rsvp']);
+
+ $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . json_serialize($metadata) . ")", 'docready');
+
+ // get localized texts from the right domain
+ foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee',
+ 'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation',
+ 'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
+ $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
+ }
+
+ // show event details with buttons
+ return $this->itip_object_details_table($event, $title) .
+ html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons));
+ }
+
+ /**
+ * Render an RSVP UI widget with buttons to respond on iTip invitations
+ */
+ function itip_rsvp_buttons($attrib = array(), $actions = null)
+ {
+ $attrib += array('type' => 'button');
+
+ if (!$actions)
+ $actions = $this->rsvp_actions;
+
+ foreach ($actions as $method) {
+ $buttons .= html::tag('input', array(
+ 'type' => $attrib['type'],
+ 'name' => $attrib['iname'],
+ 'class' => 'button',
+ 'rel' => $method,
+ 'value' => $this->gettext('itip' . $method),
+ ));
+ }
+
+ // add localized texts for the delegation dialog
+ if (in_array('delegated', $actions)) {
+ foreach (array('itipdelegated','itipcomment','delegateinvitation',
+ 'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) {
+ $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
+ }
+ }
+
+ foreach (array('all','current','future') as $mode) {
+ $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
+ }
+
+ $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
+
+ return html::div($attrib,
+ html::div('label', $this->gettext('acceptinvitation')) .
+ html::div('rsvp-buttons',
+ $buttons .
+ html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
+ )
+ );
+ }
+
+ /**
+ * Render UI elements to control iTip reply message sending
+ */
+ public function itip_rsvp_options_ui($dom_id, $disable = false)
+ {
+ $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3);
+
+ // itip sending is entirely disabled
+ if ($itip_sending === 0) {
+ return '';
+ }
+ // add checkbox to suppress itip reply message
+ else if ($itip_sending >= 2) {
+ $rsvp_additions = html::label(array('class' => 'noreply-toggle'),
+ html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0))
+ . ' ' . $this->gettext('itipsuppressreply')
+ );
+ }
+
+ // add input field for reply comment
+ $rsvp_additions .= html::a(array('href' => '#toggle', 'class' => 'reply-comment-toggle'), $this->gettext('itipeditresponse'));
+ $rsvp_additions .= html::div('itip-reply-comment',
+ html::tag('textarea', array('id' => 'reply-comment-'.$dom_id, 'name' => '_comment', 'cols' => 40, 'rows' => 6, 'style' => 'display:none', 'placeholder' => $this->gettext('itipcomment')), '')
+ );
+
+ return $rsvp_additions;
+ }
+
+ /**
+ * Render event/task details in a table
+ */
+ function itip_object_details_table($event, $title)
+ {
+ $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
+ $table->add('ititle', $title);
+ $table->add('title', Q($event['title']));
+ if ($event['start'] && $event['end']) {
+ $table->add('label', $this->gettext('date'));
+ $table->add('date', Q($this->lib->event_date_text($event)));
+ }
+ else if ($event['due'] && $event['_type'] == 'task') {
+ $table->add('label', $this->gettext('date'));
+ $table->add('date', Q($this->lib->event_date_text($event)));
+ }
+ if (!empty($event['recurrence_date'])) {
+ $table->add('label', '');
+ $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
+ }
+ else if (!empty($event['recurrence'])) {
+ $table->add('label', $this->gettext('recurring'));
+ $table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
+ }
+ if ($event['location']) {
+ $table->add('label', $this->gettext('location'));
+ $table->add('location', Q($event['location']));
+ }
+ if ($event['sensitivity'] && $event['sensitivity'] != 'public') {
+ $table->add('label', $this->gettext('sensitivity'));
+ $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!');
+ }
+ if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') {
+ $table->add('label', $this->gettext('status'));
+ $table->add('status', $this->gettext('status-' . strtolower($event['status'])));
+ }
+ if ($event['comment']) {
+ $table->add('label', $this->gettext('comment'));
+ $table->add('location', Q($event['comment']));
+ }
+
+ return $table->show();
+ }
+
+
+ /**
+ * Create iTIP invitation token for later replies via URL
+ *
+ * @param array Hash array with event properties
+ * @param string Attendee email address
+ * @return string Invitation token
+ */
+ public function store_invitation($event, $attendee)
+ {
+ // empty stub
+ return false;
+ }
+
+ /**
+ * Mark invitations for the given event as cancelled
+ *
+ * @param array Hash array with event properties
+ */
+ public function cancel_itip_invitation($event)
+ {
+ // empty stub
+ return false;
+ }
+
+ /**
+ * Utility function to get the value of a custom property
+ */
+ public static function get_custom_property($event, $name)
+ {
+ $ret = false;
+
+ if (is_array($event['x-custom'])) {
+ array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) {
+ if (strcasecmp($prop[0], $name) === 0) {
+ $ret = $prop[1];
+ }
+ });
+ }
+
+ return $ret;
+ }
+
+}
diff --git a/libcalendaring/lib/libcalendaring_recurrence.php b/libcalendaring/lib/libcalendaring_recurrence.php
new file mode 100644
index 0000000..bbc4976
--- /dev/null
+++ b/libcalendaring/lib/libcalendaring_recurrence.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * Recurrence computation class for shared use
+ *
+ * Uitility class to compute reccurrence dates from the given rules
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, 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/>.
+ */
+class libcalendaring_recurrence
+{
+ protected $lib;
+ protected $start;
+ protected $next;
+ protected $engine;
+ protected $recurrence;
+ protected $dateonly = false;
+ protected $hour = 0;
+
+ /**
+ * Default constructor
+ *
+ * @param object calendar The calendar plugin instance
+ */
+ function __construct($lib)
+ {
+ // use Horde classes to compute recurring instances
+ // TODO: replace with something that has less than 6'000 lines of code
+ require_once(__DIR__ . '/Horde_Date_Recurrence.php');
+
+ $this->lib = $lib;
+ }
+
+ /**
+ * Initialize recurrence engine
+ *
+ * @param array The recurrence properties
+ * @param object DateTime The recurrence start date
+ */
+ public function init($recurrence, $start = null)
+ {
+ $this->recurrence = $recurrence;
+
+ $this->engine = new Horde_Date_Recurrence($start);
+ $this->engine->fromRRule20(libcalendaring::to_rrule($recurrence));
+
+ $this->set_start($start);
+
+ if (is_array($recurrence['EXDATE'])) {
+ foreach ($recurrence['EXDATE'] as $exdate) {
+ if (is_a($exdate, 'DateTime')) {
+ $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
+ }
+ }
+ }
+ if (is_array($recurrence['RDATE'])) {
+ foreach ($recurrence['RDATE'] as $rdate) {
+ if (is_a($rdate, 'DateTime')) {
+ $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+ }
+ }
+ }
+ }
+
+ /**
+ * Setter for (new) recurrence start date
+ *
+ * @param object DateTime The recurrence start date
+ */
+ public function set_start($start)
+ {
+ $this->start = $start;
+ $this->dateonly = $start->_dateonly;
+ $this->next = new Horde_Date($start, $this->lib->timezone->getName());
+ $this->hour = $this->next->hour;
+ $this->engine->setRecurStart($this->next);
+ }
+
+ /**
+ * Get date/time of the next occurence of this event
+ *
+ * @return mixed DateTime object or False if recurrence ended
+ */
+ public function next()
+ {
+ $time = false;
+ $after = clone $this->next;
+ $after->mday = $after->mday + 1;
+ if ($this->next && ($next = $this->engine->nextActiveRecurrence($after))) {
+ // avoid endless loops if recurrence computation fails
+ if (!$next->after($this->next)) {
+ return false;
+ }
+ // fix time for all-day events
+ if ($this->dateonly) {
+ $next->hour = $this->hour;
+ $next->min = 0;
+ }
+
+ $time = $next->toDateTime();
+ $this->next = $next;
+ }
+
+ return $time;
+ }
+
+ /**
+ * Get the end date of the occurence of this recurrence cycle
+ *
+ * @return DateTime|bool End datetime of the last occurence or False if recurrence exceeds limit
+ */
+ public function end()
+ {
+ // recurrence end date is given
+ if ($this->recurrence['UNTIL'] instanceof DateTime) {
+ return $this->recurrence['UNTIL'];
+ }
+
+ // take the last RDATE entry if set
+ if (is_array($this->recurrence['RDATE']) && !empty($this->recurrence['RDATE'])) {
+ $last = end($this->recurrence['RDATE']);
+ if ($last instanceof DateTime) {
+ return $last;
+ }
+ }
+
+ // run through all items till we reach the end
+ if ($this->recurrence['COUNT']) {
+ $last = $this->start;
+ $this->next = new Horde_Date($this->start, $this->lib->timezone->getName());
+ while (($next = $this->next()) && $c < 1000) {
+ $last = $next;
+ $c++;
+ }
+ }
+
+ return $last;
+ }
+
+}
diff --git a/libcalendaring/libcalendaring.js b/libcalendaring/libcalendaring.js
new file mode 100644
index 0000000..25f9b86
--- /dev/null
+++ b/libcalendaring/libcalendaring.js
@@ -0,0 +1,1192 @@
+/**
+ * Basic Javascript utilities for calendar-related plugins
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2012-2015, 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/>.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this page.
+ */
+
+function rcube_libcalendaring(settings)
+{
+ // member vars
+ this.settings = settings || {};
+ this.alarm_ids = [];
+ this.alarm_dialog = null;
+ this.snooze_popup = null;
+ this.dismiss_link = null;
+ this.group2expand = {};
+
+ // abort if env isn't set
+ if (!settings || !settings.date_format)
+ return;
+
+ // private vars
+ var me = this;
+ var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
+ var client_timezone = new Date().getTimezoneOffset();
+
+ // general datepicker settings
+ var datepicker_settings = {
+ // translate from fullcalendar format to datepicker format
+ dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
+ firstDay : settings.first_day,
+ dayNamesMin: settings.days_short,
+ monthNames: settings.months,
+ monthNamesShort: settings.months,
+ changeMonth: false,
+ showOtherMonths: true,
+ selectOtherMonths: true
+ };
+
+
+ /**
+ * Quote html entities
+ */
+ var Q = this.quote_html = function(str)
+ {
+ return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+ };
+
+ /**
+ * Create a nice human-readable string for the date/time range
+ */
+ this.event_date_text = function(event, voice)
+ {
+ if (!event.start)
+ return '';
+ if (!event.end)
+ event.end = event.start;
+
+ var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000,
+ until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — ';
+ if (event.allDay) {
+ fromto = this.format_datetime(event.start, 1, voice)
+ + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : '');
+ }
+ else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
+ fromto = this.format_datetime(event.start, 0, voice)
+ + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : '');
+ }
+ else {
+ fromto = this.format_datetime(event.start, 0, voice)
+ + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : '');
+ }
+
+ return fromto;
+ };
+
+
+ /**
+ * From time and date strings to a real date object
+ */
+ this.parse_datetime = function(time, date)
+ {
+ // we use the utility function from datepicker to parse dates
+ var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
+
+ var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
+ if (!isNaN(time_arr[0])) {
+ date.setHours(time_arr[0]);
+ if (time.match(/p[.m]*/i) && date.getHours() < 12)
+ date.setHours(parseInt(time_arr[0]) + 12);
+ else if (time.match(/a[.m]*/i) && date.getHours() == 12)
+ date.setHours(0);
+ }
+ if (!isNaN(time_arr[1]))
+ date.setMinutes(time_arr[1]);
+
+ return date;
+ }
+
+ /**
+ * Convert an ISO 8601 formatted date string from the server into a Date object.
+ * Timezone information will be ignored, the server already provides dates in user's timezone.
+ */
+ this.parseISO8601 = function(s)
+ {
+ // force d to be on check's YMD, for daylight savings purposes
+ var fixDate = function(d, check) {
+ if (+d) { // prevent infinite looping on invalid dates
+ while (d.getDate() != check.getDate()) {
+ d.setTime(+d + (d < check ? 1 : -1) * 3600000);
+ }
+ }
+ }
+
+ // derived from http://delete.me.uk/2005/03/iso8601.html
+ var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
+ if (!m) {
+ return null;
+ }
+
+ var date = new Date(m[1], 0, 2),
+ check = new Date(m[1], 0, 2, 9, 0);
+ if (m[3]) {
+ date.setMonth(m[3] - 1);
+ check.setMonth(m[3] - 1);
+ }
+ if (m[5]) {
+ date.setDate(m[5]);
+ check.setDate(m[5]);
+ }
+ fixDate(date, check);
+ if (m[7]) {
+ date.setHours(m[7]);
+ }
+ if (m[8]) {
+ date.setMinutes(m[8]);
+ }
+ if (m[10]) {
+ date.setSeconds(m[10]);
+ }
+ if (m[12]) {
+ date.setMilliseconds(Number("0." + m[12]) * 1000);
+ }
+ fixDate(date, check);
+
+ return date;
+ }
+
+ /**
+ * Turn the given date into an ISO 8601 date string understandable by PHPs strtotime()
+ */
+ this.date2ISO8601 = function(date)
+ {
+ var zeropad = function(num) { return (num < 10 ? '0' : '') + num; };
+
+ return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate())
+ + 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds());
+ };
+
+ /**
+ * Format the given date object according to user's prefs
+ */
+ this.format_datetime = function(date, mode, voice)
+ {
+ var res = '';
+ if (!mode || mode == 1) {
+ res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings);
+ }
+ if (!mode) {
+ res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' ';
+ }
+ if (!mode || mode == 2) {
+ res += this.format_time(date, voice);
+ }
+
+ return res;
+ }
+
+ /**
+ * Clone from fullcalendar.js
+ */
+ this.format_time = function(date, voice)
+ {
+ var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; }
+ var formatters = {
+ s : function(d) { return d.getSeconds() },
+ ss : function(d) { return zeroPad(d.getSeconds()) },
+ m : function(d) { return d.getMinutes() },
+ mm : function(d) { return zeroPad(d.getMinutes()) },
+ h : function(d) { return d.getHours() % 12 || 12 },
+ hh : function(d) { return zeroPad(d.getHours() % 12 || 12) },
+ H : function(d) { return d.getHours() },
+ HH : function(d) { return zeroPad(d.getHours()) },
+ t : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
+ tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
+ T : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
+ TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }
+ };
+
+ var i, i2, c, formatter, res = '',
+ format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format'];
+ for (i=0; i < format.length; i++) {
+ c = format.charAt(i);
+ for (i2=Math.min(i+2, format.length); i2 > i; i2--) {
+ if (formatter = formatters[format.substring(i, i2)]) {
+ res += formatter(date);
+ i = i2 - 1;
+ break;
+ }
+ }
+ if (i2 == i) {
+ res += c;
+ }
+ }
+
+ return res;
+ }
+
+ /**
+ * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
+ */
+ this.date2unixtime = function(date)
+ {
+ var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset
+ return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
+ }
+
+ /**
+ * Turn a unix timestamp value into a Date object
+ */
+ this.fromunixtime = function(ts)
+ {
+ ts -= gmt_offset * 3600;
+ var date = new Date(ts * 1000),
+ dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
+ if (dst_offset) // adjust DST offset
+ date.setTime((ts + 3600) * 1000);
+ return date;
+ }
+
+ /**
+ * Simple plaintext to HTML converter, makig URLs clickable
+ */
+ this.text2html = function(str, maxlen, maxlines)
+ {
+ var html = Q(String(str));
+
+ // limit visible text length
+ if (maxlen) {
+ var morelink = '<span>... <a href="#more" onclick="$(this).parent().hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','libcalendaring')+'</a></span><span style="display:none">',
+ lines = html.split(/\r?\n/),
+ words, out = '', len = 0;
+
+ for (var i=0; i < lines.length; i++) {
+ len += lines[i].length;
+ if (maxlines && i == maxlines - 1) {
+ out += lines[i] + '\n' + morelink;
+ maxlen = html.length * 2;
+ }
+ else if (len > maxlen) {
+ len = out.length;
+ words = lines[i].split(' ');
+ for (var j=0; j < words.length; j++) {
+ len += words[j].length + 1;
+ out += words[j] + ' ';
+ if (len > maxlen) {
+ out += morelink;
+ maxlen = html.length * 2;
+ maxlines = 0;
+ }
+ }
+ out += '\n';
+ }
+ else
+ out += lines[i] + '\n';
+ }
+
+ if (maxlen > str.length)
+ out += '</span>';
+
+ html = out;
+ }
+
+ // simple link parser (similar to rcube_string_replacer class in PHP)
+ var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
+ var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
+ var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig');
+ var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
+ var link_replace = function(matches, p1, p2) {
+ var title = '', text = p2;
+ if (p2 && p2.length > 55) {
+ text = p2.substr(0, 45) + '...' + p2.substr(-8);
+ title = p1 + p2;
+ }
+ return '<a href="'+p1+p2+'" class="extlink" target="_blank" title="'+title+'">'+p1+text+'</a>'
+ };
+
+ return html
+ .replace(link_pattern, link_replace)
+ .replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
+ .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
+ .replace(/\n/g, "<br/>");
+ };
+
+ this.init_alarms_edit = function(prefix, index)
+ {
+ var edit_type = $(prefix+' select.edit-alarm-type'),
+ dom_id = edit_type.attr('id');
+
+ // register events on alarm fields
+ edit_type.change(function(){
+ $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
+ });
+ $(prefix+' select.edit-alarm-offset').change(function(){
+ var mode = $(this).val() == '@' ? 'show' : 'hide';
+ $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
+ $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
+ });
+
+ $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings);
+
+ $(prefix).on('click', 'a.delete-alarm', function(e){
+ if ($(this).closest('.edit-alarm-item').siblings().length > 0) {
+ $(this).closest('.edit-alarm-item').remove();
+ }
+ return false;
+ });
+
+ // set a unique id attribute and set label reference accordingly
+ if ((index || 0) > 0 && dom_id) {
+ dom_id += ':' + (new Date().getTime());
+ edit_type.attr('id', dom_id);
+ $(prefix+' label:first').attr('for', dom_id);
+ }
+
+ $(prefix).on('click', 'a.add-alarm', function(e){
+ var i = $(this).closest('.edit-alarm-item').siblings().length + 1;
+ var item = $(this).closest('.edit-alarm-item').clone(false)
+ .removeClass('first')
+ .appendTo(prefix);
+
+ me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
+ $('select.edit-alarm-type, select.edit-alarm-offset', item).change();
+ return false;
+ });
+ }
+
+ this.set_alarms_edit = function(prefix, valarms)
+ {
+ $(prefix + ' .edit-alarm-item:gt(0)').remove();
+
+ var i, alarm, domnode, val, offset;
+ for (i=0; i < valarms.length; i++) {
+ alarm = valarms[i];
+ if (!alarm.action)
+ alarm.action = 'DISPLAY';
+
+ if (i == 0) {
+ domnode = $(prefix + ' .edit-alarm-item').eq(0);
+ }
+ else {
+ domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix);
+ this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
+ }
+
+ $('select.edit-alarm-type', domnode).val(alarm.action);
+
+ if (String(alarm.trigger).match(/@(\d+)/)) {
+ var ondate = this.fromunixtime(parseInt(RegExp.$1));
+ $('select.edit-alarm-offset', domnode).val('@');
+ $('input.edit-alarm-value', domnode).val('');
+ $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1));
+ $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2));
+ }
+ else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) {
+ val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3;
+ $('input.edit-alarm-value', domnode).val(val);
+ $('select.edit-alarm-offset', domnode).val(offset);
+ }
+ }
+
+ // set correct visibility by triggering onchange handlers
+ $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change();
+ };
+
+ this.serialize_alarms = function(prefix)
+ {
+ var valarms = [];
+
+ $(prefix + ' .edit-alarm-item').each(function(i, elem) {
+ var val, offset, alarm = { action: $('select.edit-alarm-type', elem).val() };
+ if (alarm.action) {
+ offset = $('select.edit-alarm-offset', elem).val();
+ if (offset == '@') {
+ alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val()));
+ }
+ else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) {
+ alarm.trigger = offset[0] + val + offset[1];
+ }
+
+ valarms.push(alarm);
+ }
+ });
+
+ return valarms;
+ };
+
+
+ /***** Alarms handling *****/
+
+ /**
+ * Display a notification for the given pending alarms
+ */
+ this.display_alarms = function(alarms)
+ {
+ // clear old alert first
+ if (this.alarm_dialog)
+ this.alarm_dialog.dialog('destroy').remove();
+
+ this.alarm_dialog = $('<div>').attr('id', 'alarm-display');
+
+ var i, actions, adismiss, asnooze, alarm, html, event_ids = [], buttons = {};
+ for (i=0; i < alarms.length; i++) {
+ alarm = alarms[i];
+ alarm.start = this.parseISO8601(alarm.start);
+ alarm.end = this.parseISO8601(alarm.end);
+ event_ids.push(alarm.id);
+
+ html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>';
+ html += '<div class="event-section">' + Q(alarm.location || '') + '</div>';
+ html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>';
+
+ adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','libcalendaring')).click(function(){
+ me.dismiss_link = $(this);
+ me.dismiss_alarm(me.dismiss_link.data('id'), 0);
+ });
+ asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){
+ me.snooze_dropdown($(this), e);
+ e.stopPropagation();
+ return false;
+ });
+ actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
+
+ $('<div>').addClass('alarm-item').html(html).append(actions).appendTo(this.alarm_dialog);
+ }
+
+ buttons[rcmail.gettext('close')] = function() {
+ $(this).dialog('close');
+ };
+
+ buttons[rcmail.gettext('dismissall','libcalendaring')] = function() {
+ // submit dismissed event_ids to server
+ me.dismiss_alarm(me.alarm_ids.join(','), 0);
+ $(this).dialog('close');
+ };
+
+ this.alarm_dialog.appendTo(document.body).dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: false,
+ dialogClass: 'alarms',
+ title: rcmail.gettext('alarmtitle','libcalendaring'),
+ buttons: buttons,
+ open: function() {
+ setTimeout(function() {
+ me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $('#alarm-snooze-dropdown').hide();
+ $(this).dialog('destroy').remove();
+ me.alarm_dialog = null;
+ me.alarm_ids = null;
+ },
+ drag: function(event, ui) {
+ $('#alarm-snooze-dropdown').hide();
+ }
+ });
+
+ this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog');
+
+ this.alarm_ids = event_ids;
+ };
+
+ /**
+ * Show a drop-down menu with a selection of snooze times
+ */
+ this.snooze_dropdown = function(link, event)
+ {
+ if (!this.snooze_popup) {
+ this.snooze_popup = $('#alarm-snooze-dropdown');
+ // create popup if not found
+ if (!this.snooze_popup.length) {
+ this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body);
+ this.snooze_popup.html(rcmail.env.snooze_select)
+ }
+ $('#alarm-snooze-dropdown a').click(function(e){
+ var time = String(this.href).replace(/.+#/, '');
+ me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time);
+ return false;
+ });
+ }
+
+ // hide visible popup
+ if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
+ rcmail.command('menu-close', 'alarm-snooze-dropdown');
+ this.dismiss_link = null;
+ }
+ else { // open popup below the clicked link
+ rcmail.command('menu-open', 'alarm-snooze-dropdown', link.get(0), event);
+ this.snooze_popup.data('id', link.data('id'));
+ this.dismiss_link = link;
+ }
+ };
+
+ /**
+ * Dismiss or snooze alarms for the given event
+ */
+ this.dismiss_alarm = function(id, snooze)
+ {
+ rcmail.command('menu-close', 'alarm-snooze-dropdown');
+ rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } });
+
+ // remove dismissed alarm from list
+ if (this.dismiss_link) {
+ this.dismiss_link.closest('div.alarm-item').hide();
+ var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; });
+ if (new_ids.length)
+ this.alarm_ids = new_ids;
+ else
+ this.alarm_dialog.dialog('close');
+ }
+
+ this.dismiss_link = null;
+ };
+
+
+ /***** Recurrence form handling *****/
+
+ /**
+ * Install event handlers on recurrence form elements
+ */
+ this.init_recurrence_edit = function(prefix)
+ {
+ // toggle recurrence frequency forms
+ $('#edit-recurrence-frequency').change(function(e){
+ var freq = $(this).val().toLowerCase();
+ $('.recurrence-form').hide();
+ if (freq) {
+ $('#recurrence-form-'+freq).show();
+ if (freq != 'rdate')
+ $('#recurrence-form-until').show();
+ }
+ });
+ $('#recurrence-form-rdate input.button.add').click(function(e){
+ var dt, dv = $('#edit-recurrence-rdate-input').val();
+ if (dv && (dt = me.parse_datetime('12:00', dv))) {
+ me.add_rdate(dt);
+ me.sort_rdates();
+ $('#edit-recurrence-rdate-input').val('')
+ }
+ else {
+ $('#edit-recurrence-rdate-input').select();
+ }
+ });
+ $('#edit-recurrence-rdates').on('click', 'a.delete', function(e){
+ $(this).closest('li').remove();
+ return false;
+ });
+
+ $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
+ $('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); });
+ $('#edit-recurrence-rdate-input').datepicker(datepicker_settings);
+ };
+
+ /**
+ * Set recurrence form according to the given event/task record
+ */
+ this.set_recurrence_edit = function(rec)
+ {
+ var recurrence = $('#edit-recurrence-frequency').val(rec.recurrence ? rec.recurrence.FREQ || (rec.recurrence.RDATE ? 'RDATE' : '') : '').change(),
+ interval = $('.recurrence-form select.edit-recurrence-interval').val(rec.recurrence ? rec.recurrence.INTERVAL || 1 : 1),
+ rrtimes = $('#edit-recurrence-repeat-times').val(rec.recurrence ? rec.recurrence.COUNT || 1 : 1),
+ rrenddate = $('#edit-recurrence-enddate').val(rec.recurrence && rec.recurrence.UNTIL ? this.format_datetime(this.parseISO8601(rec.recurrence.UNTIL), 1) : '');
+ $('.recurrence-form input.edit-recurrence-until:checked').prop('checked', false);
+ $('#edit-recurrence-rdates').html('');
+
+ var weekdays = ['SU','MO','TU','WE','TH','FR','SA'],
+ rrepeat_id = '#edit-recurrence-repeat-forever';
+ if (rec.recurrence && rec.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
+ else if (rec.recurrence && rec.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
+ $(rrepeat_id).prop('checked', true);
+
+ if (rec.recurrence && rec.recurrence.BYDAY && rec.recurrence.FREQ == 'WEEKLY') {
+ var wdays = rec.recurrence.BYDAY.split(',');
+ $('input.edit-recurrence-weekly-byday').val(wdays);
+ }
+ if (rec.recurrence && rec.recurrence.BYMONTHDAY) {
+ $('input.edit-recurrence-monthly-bymonthday').val(String(rec.recurrence.BYMONTHDAY).split(','));
+ $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
+ }
+ if (rec.recurrence && rec.recurrence.BYDAY && (rec.recurrence.FREQ == 'MONTHLY' || rec.recurrence.FREQ == 'YEARLY')) {
+ var byday, section = rec.recurrence.FREQ.toLowerCase();
+ if ((byday = String(rec.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
+ $('#edit-recurrence-'+section+'-prefix').val(byday[1]);
+ $('#edit-recurrence-'+section+'-byday').val(byday[2]);
+ }
+ $('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
+ }
+ else if (rec.start) {
+ $('#edit-recurrence-monthly-byday').val(weekdays[rec.start.getDay()]);
+ }
+ if (rec.recurrence && rec.recurrence.BYMONTH) {
+ $('input.edit-recurrence-yearly-bymonth').val(String(rec.recurrence.BYMONTH).split(','));
+ }
+ else if (rec.start) {
+ $('input.edit-recurrence-yearly-bymonth').val([String(rec.start.getMonth()+1)]);
+ }
+ if (rec.recurrence && rec.recurrence.RDATE) {
+ $.each(rec.recurrence.RDATE, function(i,rdate){
+ me.add_rdate(me.parseISO8601(rdate));
+ });
+ }
+ };
+
+ /**
+ * Gather recurrence settings from form
+ */
+ this.serialize_recurrence = function(timestr)
+ {
+ var recurrence = '',
+ freq = $('#edit-recurrence-frequency').val();
+
+ if (freq != '') {
+ recurrence = {
+ FREQ: freq,
+ INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
+ };
+
+ var until = $('input.edit-recurrence-until:checked').val();
+ if (until == 'count')
+ recurrence.COUNT = $('#edit-recurrence-repeat-times').val();
+ else if (until == 'until')
+ recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val()));
+
+ if (freq == 'WEEKLY') {
+ var byday = [];
+ $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
+ if (byday.length)
+ recurrence.BYDAY = byday.join(',');
+ }
+ else if (freq == 'MONTHLY') {
+ var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
+ if (mode == 'BYMONTHDAY') {
+ $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
+ if (bymonday.length)
+ recurrence.BYMONTHDAY = bymonday.join(',');
+ }
+ else
+ recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
+ }
+ else if (freq == 'YEARLY') {
+ var byday, bymonth = [];
+ $('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
+ if (bymonth.length)
+ recurrence.BYMONTH = bymonth.join(',');
+ if ((byday = $('#edit-recurrence-yearly-byday').val()))
+ recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
+ }
+ else if (freq == 'RDATE') {
+ recurrence = { RDATE:[] };
+ // take selected but not yet added date into account
+ if ($('#edit-recurrence-rdate-input').val() != '') {
+ $('#recurrence-form-rdate input.button.add').click();
+ }
+ $('#edit-recurrence-rdates li').each(function(i, li){
+ recurrence.RDATE.push($(li).attr('data-value'));
+ });
+ }
+ }
+
+ return recurrence;
+ };
+
+ // add the given date to the RDATE list
+ this.add_rdate = function(date)
+ {
+ var li = $('<li>')
+ .attr('data-value', this.date2ISO8601(date))
+ .html('<span>' + Q(this.format_datetime(date, 1)) + '</span>')
+ .appendTo('#edit-recurrence-rdates');
+
+ $('<a>').attr('href', '#del')
+ .addClass('iconbutton delete')
+ .html(rcmail.get_label('delete', 'libcalendaring'))
+ .attr('title', rcmail.get_label('delete', 'libcalendaring'))
+ .appendTo(li);
+ };
+
+ // re-sort the list items by their 'data-value' attribute
+ this.sort_rdates = function()
+ {
+ var mylist = $('#edit-recurrence-rdates'),
+ listitems = mylist.children('li').get();
+ listitems.sort(function(a, b) {
+ var compA = $(a).attr('data-value');
+ var compB = $(b).attr('data-value');
+ return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+ })
+ $.each(listitems, function(idx, item) { mylist.append(item); });
+ };
+
+
+ /***** Attendee form handling *****/
+
+ // expand the given contact group into individual event/task attendees
+ this.expand_attendee_group = function(e, add, remove)
+ {
+ var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'),
+ role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected');
+
+ this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove }
+
+ // copy group role from the according form element
+ if (role_select.length) {
+ this.group2expand[id].data.role = role_select.val();
+ }
+
+ // register callback handler
+ if (!this._expand_attendee_listener) {
+ this._expand_attendee_listener = this.expand_attendee_callback;
+ rcmail.addEventListener('plugin.expand_attendee_callback', function(result) {
+ me._expand_attendee_listener(result);
+ });
+ }
+
+ rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading'));
+ };
+
+ // callback from server to expand an attendee group
+ this.expand_attendee_callback = function(result)
+ {
+ var attendee, id = result.id,
+ data = this.group2expand[id],
+ row = $(data.link).closest('tr');
+
+ // replace group entry with all members returned by the server
+ if (data && data.adder && result.members && result.members.length) {
+ for (var i=0; i < result.members.length; i++) {
+ attendee = result.members[i];
+ attendee.role = data.data.role;
+ attendee.cutype = 'INDIVIDUAL';
+ attendee.status = 'NEEDS-ACTION';
+ data.adder(attendee, null, row);
+ }
+
+ if (data.remover) {
+ data.remover(data.link, id)
+ }
+ else {
+ row.remove();
+ }
+
+ delete this.group2expand[id];
+ }
+ else {
+ rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error');
+ }
+ };
+
+
+ // Render message reference links to the given container
+ this.render_message_links = function(links, container, edit, plugin)
+ {
+ var ul = $('<ul>').addClass('attachmentslist');
+
+ $.each(links, function(i, link) {
+ if (!link.mailurl)
+ return true; // continue
+
+ var li = $('<li>').addClass('link')
+ .addClass('message eml')
+ .append($('<a>')
+ .attr('href', link.mailurl)
+ .addClass('messagelink')
+ .text(link.subject || link.uri)
+ )
+ .appendTo(ul);
+
+ // add icon to remove the link
+ if (edit) {
+ $('<a>')
+ .attr('href', '#delete')
+ .attr('title', rcmail.gettext('removelink', plugin))
+ .attr('data-uri', link.uri)
+ .addClass('delete')
+ .text(rcmail.gettext('delete'))
+ .appendTo(li);
+ }
+ });
+
+ container.empty().append(ul);
+ }
+}
+
+////// static methods
+
+/**
+ *
+ */
+rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id)
+{
+ // ask user to delete the declined event from the local calendar (#1670)
+ var del = false;
+ if (rcmail.env.rsvp_saved && status == 'declined') {
+ del = confirm(rcmail.gettext('itip.declinedeleteconfirm'));
+ }
+
+ // open dialog for iTip delegation
+ if (status == 'delegated') {
+ rcube_libcalendaring.itip_delegate_dialog(function(data) {
+ rcmail.http_post(task + '/itip-delegate', {
+ _uid: rcmail.env.uid,
+ _mbox: rcmail.env.mailbox,
+ _part: mime_id,
+ _to: data.to,
+ _rsvp: data.rsvp ? 1 : 0,
+ _comment: data.comment,
+ _folder: data.target
+ }, rcmail.set_busy(true, 'itip.savingdata'));
+ }, $('#rsvp-'+dom_id+' .folder-select'));
+ return false;
+ }
+
+ var noreply = 0, comment = '';
+ if (dom_id) {
+ noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0;
+ if (!noreply)
+ comment = $('#reply-comment-'+dom_id).val();
+ }
+
+ rcmail.http_post(task + '/mailimportitip', {
+ _uid: rcmail.env.uid,
+ _mbox: rcmail.env.mailbox,
+ _part: mime_id,
+ _folder: $('#itip-saveto').val(),
+ _status: status,
+ _del: del?1:0,
+ _noreply: noreply,
+ _comment: comment
+ }, rcmail.set_busy(true, 'itip.savingdata'));
+
+ return false;
+};
+
+/**
+ * Helper function to render the iTip delegation dialog
+ * and trigger a callback function when submitted.
+ */
+rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
+{
+ // show dialog for entering the delegatee address and comment
+ var html = '<form class="itip-dialog-form" action="javascript:void()">' +
+ '<div class="form-section">' +
+ '<label for="itip-delegate-to">' + rcmail.gettext('itip.delegateto') + '</label><br/>' +
+ '<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' +
+ '</div>' +
+ '<div class="form-section">' +
+ '<label for="itip-delegate-rsvp">' +
+ '<input type="checkbox" id="itip-delegate-rsvp" class="checkbox" size="40" value="" />' +
+ rcmail.gettext('itip.delegatersvpme') +
+ '</label>' +
+ '</div>' +
+ '<div class="form-section">' +
+ '<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' +
+ rcmail.gettext('itip.itipcomment') + '"></textarea>' +
+ '</div>' +
+ '<div class="form-section">' +
+ (selector && selector.length ? selector.html() : '') +
+ '</div>' +
+ '</form>';
+
+ var dialog, buttons = [];
+ buttons.push({
+ text: rcmail.gettext('itipdelegated', 'itip'),
+ click: function() {
+ var doc = window.parent.document,
+ delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, '');
+
+ if (delegatee != '' && rcube_check_email(delegatee, true)) {
+ callback({
+ to: delegatee,
+ rsvp: $('#itip-delegate-rsvp', doc).prop('checked'),
+ comment: $('#itip-delegate-comment', doc).val(),
+ target: $('#itip-saveto', doc).val()
+ });
+
+ setTimeout(function() { dialog.dialog("close"); }, 500);
+ }
+ else {
+ alert(rcmail.gettext('itip.delegateinvalidaddress'));
+ $('#itip-delegate-to', doc).focus();
+ }
+ }
+ });
+
+ buttons.push({
+ text: rcmail.gettext('cancel', 'itip'),
+ click: function() {
+ dialog.dialog('close');
+ }
+ });
+
+ dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, {
+ width: 460,
+ open: function(event, ui) {
+ $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
+ $(this).find('#itip-saveto').val('');
+
+ // initialize autocompletion
+ var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
+ if (rcmail.env.autocomplete_threads > 0) {
+ ac_props = {
+ threads: rcmail.env.autocomplete_threads,
+ sources: rcmail.env.autocomplete_sources
+ };
+ }
+ rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props);
+ rcm.env.recipients_delimiter = '';
+ },
+ close: function(event, ui) {
+ rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
+ rcm.ksearch_blur();
+ $(this).remove();
+ }
+ });
+
+ return dialog;
+};
+
+/**
+ * Show a menu for selecting the RSVP reply mode
+ */
+rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
+{
+ var mnu = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode');
+
+ $.each(['all','current'/*,'future'*/], function(i, mode) {
+ $('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>')
+ .addClass('ui-menu-item')
+ .attr('rel', mode)
+ .appendTo(mnu);
+ });
+
+ var action = btn.attr('rel');
+
+ // open the mennu
+ mnu.menu({
+ select: function(event, ui) {
+ callback(action, ui.item.attr('rel'));
+ }
+ })
+ .appendTo(document.body)
+ .position({ my: 'left top', at: 'left bottom+2', of: btn })
+ .data('action', action);
+
+ setTimeout(function() {
+ $(document).one('click', function() {
+ mnu.menu('destroy');
+ mnu.remove();
+ });
+ }, 100);
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.remove_from_itip = function(event, task, title)
+{
+ if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
+ rcmail.http_post(task + '/itip-remove',
+ event,
+ rcmail.set_busy(true, 'itip.savingdata')
+ );
+ }
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.decline_attendee_reply = function(mime_id, task)
+{
+ // show dialog for entering a comment and send to server
+ var html = '<div class="itip-dialog-confirm-text">' + rcmail.gettext('itip.declineattendeeconfirm') + '</div>' +
+ '<textarea id="itip-decline-comment" class="itip-comment" cols="40" rows="8"></textarea>';
+
+ var dialog, buttons = [];
+ buttons.push({
+ text: rcmail.gettext('declineattendee', 'itip'),
+ click: function() {
+ rcmail.http_post(task + '/itip-decline-reply', {
+ _uid: rcmail.env.uid,
+ _mbox: rcmail.env.mailbox,
+ _part: mime_id,
+ _comment: $('#itip-decline-comment', window.parent.document).val()
+ }, rcmail.set_busy(true, 'itip.savingdata'));
+ dialog.dialog("close");
+ }
+ });
+
+ buttons.push({
+ text: rcmail.gettext('cancel', 'itip'),
+ click: function() {
+ dialog.dialog('close');
+ }
+ });
+
+ dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, {
+ width: 460,
+ open: function() {
+ $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
+ $('#itip-decline-comment').focus();
+ }
+ });
+
+ return false;
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.fetch_itip_object_status = function(p)
+{
+ rcmail.http_post(p.task + '/itip-status', { data: p });
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.update_itip_object_status = function(p)
+{
+ rcmail.env.rsvp_saved = p.saved;
+ rcmail.env.itip_existing = p.existing;
+
+ // hide all elements first
+ $('#itip-buttons-'+p.id+' > div').hide();
+ $('#rsvp-'+p.id+' .folder-select').remove();
+
+ if (p.html) {
+ // append/replace rsvp status display
+ $('#loading-'+p.id).next('.rsvp-status').remove();
+ $('#loading-'+p.id).hide().after(p.html);
+ }
+
+ // enable/disable rsvp buttons
+ if (p.action == 'rsvp') {
+ $('#rsvp-'+p.id+' input.button').prop('disabled', false)
+ .filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest);
+ }
+
+ // show rsvp/import buttons (with calendar selector)
+ $('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select);
+
+ // show itip box appendix after replacing the given placeholders
+ if (p.append && p.append.selector) {
+ var elem = $(p.append.selector);
+ if (p.append.replacements) {
+ $.each(p.append.replacements, function(k, html) {
+ elem.html(elem.html().replace(k, html));
+ });
+ }
+ else if (p.append.html) {
+ elem.html(p.append.html)
+ }
+ elem.show();
+ }
+};
+
+/**
+ * Callback from server after an iTip message has been processed
+ */
+rcube_libcalendaring.itip_message_processed = function(metadata)
+{
+ if (metadata.after_action) {
+ setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200);
+ }
+ else {
+ rcube_libcalendaring.fetch_itip_object_status(metadata);
+ }
+};
+
+/**
+ * After-action on iTip request message. Action types:
+ * 0 - no action
+ * 1 - move to Trash
+ * 2 - delete the message
+ * 3 - flag as deleted
+ * folder_name - move the message to the specified folder
+ */
+rcube_libcalendaring.itip_after_action = function(action)
+{
+ if (!action) {
+ return;
+ }
+
+ var rc = rcmail.is_framed() ? parent.rcmail : rcmail;
+
+ if (action === 2) {
+ rc.permanently_remove_messages();
+ }
+ else if (action === 3) {
+ rc.mark_message('delete');
+ }
+ else {
+ rc.move_messages(action === 1 ? rc.env.trash_mailbox : action);
+ }
+};
+
+/**
+ * Open the calendar preview for the current iTip event
+ */
+rcube_libcalendaring.open_itip_preview = function(url, msgref)
+{
+ if (!rcmail.env.itip_existing)
+ url += '&itip=' + escape(msgref);
+
+ var win = rcmail.open_window(url);
+};
+
+
+// extend jQuery
+(function($){
+ $.fn.serializeJSON = function(){
+ var json = {};
+ jQuery.map($(this).serializeArray(), function(n, i) {
+ json[n['name']] = n['value'];
+ });
+ return json;
+ };
+})(jQuery);
+
+
+/* libcalendaring plugin initialization */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+ if (rcmail.env.libcal_settings) {
+ var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings);
+ rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); });
+ }
+
+ rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status)
+ .addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status)
+ .addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed);
+
+ $('.rsvp-buttons').on('click', 'a.reply-comment-toggle', function(e){
+ $(this).hide().parent().find('textarea').show().focus();
+ return false;
+ });
+
+ if (rcmail.env.action == 'get-attachment' && rcmail.gui_objects['attachmentframe']) {
+ rcmail.register_command('print-attachment', function() {
+ var frame = rcmail.get_frame_window(rcmail.gui_objects['attachmentframe'].id);
+ if (frame) frame.print();
+ }, true);
+ }
+
+ if (rcmail.env.action == 'get-attachment' && rcmail.env.attachment_download_url) {
+ rcmail.register_command('download-attachment', function() {
+ rcmail.location_href(rcmail.env.attachment_download_url, window);
+ }, true);
+ }
+
+});
diff --git a/libcalendaring/libcalendaring.php b/libcalendaring/libcalendaring.php
new file mode 100644
index 0000000..fc5ba32
--- /dev/null
+++ b/libcalendaring/libcalendaring.php
@@ -0,0 +1,1637 @@
+<?php
+
+/**
+ * Library providing common functions for calendaring plugins
+ *
+ * Provides utility functions for calendar-related modules such as
+ * - alarms display and dismissal
+ * - attachment handling
+ * - recurrence computation and UI elements
+ * - ical parsing and exporting
+ * - itip scheduling protocol
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2012-2015, 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/>.
+ */
+
+class libcalendaring extends rcube_plugin
+{
+ public $rc;
+ public $timezone;
+ public $gmt_offset;
+ public $dst_active;
+ public $timezone_offset;
+ public $ical_parts = array();
+ public $ical_message;
+
+ public $defaults = array(
+ 'calendar_date_format' => "yyyy-MM-dd",
+ 'calendar_date_short' => "M-d",
+ 'calendar_date_long' => "MMM d yyyy",
+ 'calendar_date_agenda' => "ddd MM-dd",
+ 'calendar_time_format' => "HH:mm",
+ 'calendar_first_day' => 1,
+ 'calendar_first_hour' => 6,
+ 'calendar_date_format_sets' => array(
+ 'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
+ 'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
+ 'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
+ 'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
+ 'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
+ 'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
+ 'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
+ ),
+ );
+
+ private static $instance;
+ private static $email_regex = '/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/';
+
+ private $mail_ical_parser;
+
+ /**
+ * Singleton getter to allow direct access from other plugins
+ */
+ public static function get_instance()
+ {
+ return self::$instance;
+ }
+
+ /**
+ * Required plugin startup method
+ */
+ public function init()
+ {
+ self::$instance = $this;
+
+ $this->rc = rcube::get_instance();
+
+ // set user's timezone
+ try {
+ $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
+ }
+ catch (Exception $e) {
+ $this->timezone = new DateTimeZone('GMT');
+ }
+
+ $now = new DateTime('now', $this->timezone);
+
+ $this->gmt_offset = $now->getOffset();
+ $this->dst_active = $now->format('I');
+ $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
+
+ $this->add_texts('localization/', false);
+
+ // include client scripts and styles
+ if ($this->rc->output) {
+ // add hook to display alarms
+ $this->add_hook('refresh', array($this, 'refresh'));
+ $this->register_action('plugin.alarms', array($this, 'alarms_action'));
+ $this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
+ }
+
+ // proceed initialization in startup hook
+ $this->add_hook('startup', array($this, 'startup'));
+ }
+
+ /**
+ * Startup hook
+ */
+ public function startup($args)
+ {
+ if ($this->rc->output && $this->rc->output->type == 'html') {
+ $this->rc->output->set_env('libcal_settings', $this->load_settings());
+ $this->include_script('libcalendaring.js');
+ $this->include_stylesheet($this->local_skin_path() . '/libcal.css');
+ }
+
+ if ($args['task'] == 'mail') {
+ if ($args['action'] == 'show' || $args['action'] == 'preview') {
+ $this->add_hook('message_load', array($this, 'mail_message_load'));
+ }
+ }
+ }
+
+ /**
+ * Load iCalendar functions
+ */
+ public static function get_ical()
+ {
+ $self = self::get_instance();
+ require_once($self->home . '/libvcalendar.php');
+ return new libvcalendar();
+ }
+
+ /**
+ * Load iTip functions
+ */
+ public static function get_itip($domain = 'libcalendaring')
+ {
+ $self = self::get_instance();
+ require_once($self->home . '/lib/libcalendaring_itip.php');
+ return new libcalendaring_itip($self, $domain);
+ }
+
+ /**
+ * Load recurrence computation engine
+ */
+ public static function get_recurrence()
+ {
+ $self = self::get_instance();
+ require_once($self->home . '/lib/libcalendaring_recurrence.php');
+ return new libcalendaring_recurrence($self);
+ }
+
+ /**
+ * Shift dates into user's current timezone
+ *
+ * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
+ * @return object DateTime object in user's timezone
+ */
+ public function adjust_timezone($dt, $dateonly = false)
+ {
+ if (is_numeric($dt))
+ $dt = new DateTime('@'.$dt);
+ else if (is_string($dt))
+ $dt = rcube_utils::anytodatetime($dt);
+
+ if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
+ $dt->setTimezone($this->timezone);
+ }
+
+ return $dt;
+ }
+
+
+ /**
+ *
+ */
+ public function load_settings()
+ {
+ $this->date_format_defaults();
+ $settings = array();
+
+ // configuration
+ $settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
+ $settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
+ $settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
+ $settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
+ $settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
+ $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
+
+ $settings['timezone'] = $this->timezone_offset;
+ $settings['dst'] = $this->dst_active;
+
+ // localization
+ $settings['days'] = array(
+ $this->rc->gettext('sunday'), $this->rc->gettext('monday'),
+ $this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'),
+ $this->rc->gettext('thursday'), $this->rc->gettext('friday'),
+ $this->rc->gettext('saturday')
+ );
+ $settings['days_short'] = array(
+ $this->rc->gettext('sun'), $this->rc->gettext('mon'),
+ $this->rc->gettext('tue'), $this->rc->gettext('wed'),
+ $this->rc->gettext('thu'), $this->rc->gettext('fri'),
+ $this->rc->gettext('sat')
+ );
+ $settings['months'] = array(
+ $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
+ $this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
+ $this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
+ $this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
+ $this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
+ $this->rc->gettext('longnov'), $this->rc->gettext('longdec')
+ );
+ $settings['months_short'] = array(
+ $this->rc->gettext('jan'), $this->rc->gettext('feb'),
+ $this->rc->gettext('mar'), $this->rc->gettext('apr'),
+ $this->rc->gettext('may'), $this->rc->gettext('jun'),
+ $this->rc->gettext('jul'), $this->rc->gettext('aug'),
+ $this->rc->gettext('sep'), $this->rc->gettext('oct'),
+ $this->rc->gettext('nov'), $this->rc->gettext('dec')
+ );
+ $settings['today'] = $this->rc->gettext('today');
+
+ // define list of file types which can be displayed inline
+ // same as in program/steps/mail/show.inc
+ $settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes');
+
+ return $settings;
+ }
+
+
+ /**
+ * Helper function to set date/time format according to config and user preferences
+ */
+ private function date_format_defaults()
+ {
+ static $defaults = array();
+
+ // nothing to be done
+ if (isset($defaults['date_format']))
+ return;
+
+ $defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
+ $defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
+
+ // override defaults
+ if ($defaults['date_format'])
+ $this->defaults['calendar_date_format'] = $defaults['date_format'];
+ if ($defaults['time_format'])
+ $this->defaults['calendar_time_format'] = $defaults['time_format'];
+
+ // derive format variants from basic date format
+ $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
+ if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
+ $this->defaults['calendar_date_long'] = $format_set[0];
+ $this->defaults['calendar_date_short'] = $format_set[1];
+ $this->defaults['calendar_date_agenda'] = $format_set[2];
+ }
+ }
+
+ /**
+ * Compose a date string for the given event
+ */
+ public function event_date_text($event, $tzinfo = false)
+ {
+ $fromto = '--';
+
+ // handle task objects
+ if ($event['_type'] == 'task' && is_object($event['due'])) {
+ $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
+ $fromto = $this->rc->format_date($event['due'], $date_format, false);
+
+ // add timezone information
+ if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
+ $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+ }
+
+ return $fromto;
+ }
+
+ // abort if no valid event dates are given
+ if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
+ return $fromto;
+ }
+
+ $duration = $event['start']->diff($event['end'])->format('s');
+
+ $this->date_format_defaults();
+ $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
+ $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
+
+ if ($event['allday']) {
+ $fromto = format_date($event['start'], $date_format);
+ if (($todate = format_date($event['end'], $date_format)) != $fromto)
+ $fromto .= ' - ' . $todate;
+ }
+ else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
+ $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
+ ' - ' . format_date($event['end'], $time_format);
+ }
+ else {
+ $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
+ ' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
+ }
+
+ // add timezone information
+ if ($tzinfo && ($tzname = $this->timezone->getName())) {
+ $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+ }
+
+ return $fromto;
+ }
+
+
+ /**
+ * Render HTML form for alarm configuration
+ */
+ public function alarm_select($attrib, $alarm_types, $absolute_time = true)
+ {
+ unset($attrib['name']);
+ $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id']));
+ $select_type->add($this->gettext('none'), '');
+ foreach ($alarm_types as $type)
+ $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
+
+ $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
+ $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
+ $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
+
+ $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
+ foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
+ $select_offset->add($this->gettext('trigger' . $trigger), $trigger);
+
+ if ($absolute_time)
+ $select_offset->add($this->gettext('trigger@'), '@');
+
+ // pre-set with default values from user settings
+ $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
+ $hidden = array('style' => 'display:none');
+ $html = html::span('edit-alarm-set',
+ $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
+ html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
+ $input_value->show($preset[0]) . ' ' .
+ $select_offset->show($preset[1]) . ' ' .
+ $input_date->show('', $hidden) . ' ' .
+ $input_time->show('', $hidden)
+ )
+ );
+
+ // TODO: support adding more alarms
+ #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
+ # $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
+
+ return $html;
+ }
+
+ /**
+ * Get a list of email addresses of the given user (from login and identities)
+ *
+ * @param string User Email (default to current user)
+ * @return array Email addresses related to the user
+ */
+ public function get_user_emails($user = null)
+ {
+ static $_emails = array();
+
+ if (empty($user)) {
+ $user = $this->rc->user->get_username();
+ }
+
+ // return cached result
+ if (is_array($_emails[$user])) {
+ return $_emails[$user];
+ }
+
+ $emails = array($user);
+ $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
+ $emails = array_map('strtolower', $plugin['emails']);
+
+ // add all emails from the current user's identities
+ if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
+ foreach ($this->rc->user->list_emails() as $identity) {
+ $emails[] = strtolower($identity['email']);
+ }
+ }
+
+ $_emails[$user] = array_unique($emails);
+ return $_emails[$user];
+ }
+
+ /**
+ * Set the given participant status to the attendee matching the current user's identities
+ *
+ * @param array Hash array with event struct
+ * @param string The PARTSTAT value to set
+ * @return mixed Email address of the updated attendee or False if none matching found
+ */
+ public function set_partstat(&$event, $status, $recursive = true)
+ {
+ $success = false;
+ $emails = $this->get_user_emails();
+ foreach ((array)$event['attendees'] as $i => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $event['attendees'][$i]['status'] = strtoupper($status);
+ $success = $attendee['email'];
+ }
+ }
+
+ // apply partstat update to each existing exception
+ if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ $this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false);
+ }
+
+ // set link to top-level exceptions
+ $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+ }
+
+ return $success;
+ }
+
+
+ /********* Alarms handling *********/
+
+ /**
+ * Helper function to convert alarm trigger strings
+ * into two-field values (e.g. "-45M" => 45, "-M")
+ */
+ public static function parse_alarm_value($val)
+ {
+ if ($val[0] == '@') {
+ return array(new DateTime($val));
+ }
+ else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
+ if ($m[1] == '')
+ $m[1] = '+';
+ foreach ($m2 as $seg) {
+ $prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
+ if ($seg[1] > 0) { // ignore zero values
+ // convert seconds to minutes
+ if ($seg[2] == 'S') {
+ $seg[2] = 'M';
+ $seg[1] = max(1, round($seg[1]/60));
+ }
+
+ return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
+ }
+ }
+
+ // return zero value nevertheless
+ return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
+ }
+
+ return false;
+ }
+
+ /**
+ * Convert the alarms list items to be processed on the client
+ */
+ public static function to_client_alarms($valarms)
+ {
+ return array_map(function($alarm){
+ if ($alarm['trigger'] instanceof DateTime) {
+ $alarm['trigger'] = '@' . $alarm['trigger']->format('U');
+ }
+ else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
+ $alarm['trigger'] = $trigger[2];
+ }
+ return $alarm;
+ }, (array)$valarms);
+ }
+
+ /**
+ * Process the alarms values submitted by the client
+ */
+ public static function from_client_alarms($valarms)
+ {
+ return array_map(function($alarm){
+ if ($alarm['trigger'][0] == '@') {
+ try {
+ $alarm['trigger'] = new DateTime($alarm['trigger']);
+ $alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
+ }
+ catch (Exception $e) { /* handle this ? */ }
+ }
+ else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
+ $alarm['trigger'] = $trigger[3];
+ }
+ return $alarm;
+ }, (array)$valarms);
+ }
+
+ /**
+ * Render localized text for alarm settings
+ */
+ public static function alarms_text($alarms)
+ {
+ if (is_array($alarms) && is_array($alarms[0])) {
+ $texts = array();
+ foreach ($alarms as $alarm) {
+ if ($text = self::alarm_text($alarm))
+ $texts[] = $text;
+ }
+
+ return join(', ', $texts);
+ }
+ else {
+ return self::alarm_text($alarms);
+ }
+ }
+
+ /**
+ * Render localized text for a single alarm property
+ */
+ public static function alarm_text($alarm)
+ {
+ if (is_string($alarm)) {
+ list($trigger, $action) = explode(':', $alarm);
+ }
+ else {
+ $trigger = $alarm['trigger'];
+ $action = $alarm['action'];
+ }
+
+ $text = '';
+ $rcube = rcube::get_instance();
+
+ switch ($action) {
+ case 'EMAIL':
+ $text = $rcube->gettext('libcalendaring.alarmemail');
+ break;
+ case 'DISPLAY':
+ $text = $rcube->gettext('libcalendaring.alarmdisplay');
+ break;
+ case 'AUDIO':
+ $text = $rcube->gettext('libcalendaring.alarmaudio');
+ break;
+ }
+
+ if ($trigger instanceof DateTime) {
+ $text .= ' ' . $rcube->gettext(array(
+ 'name' => 'libcalendaring.alarmat',
+ 'vars' => array('datetime' => $rcube->format_date($trigger))
+ ));
+ }
+ else if (preg_match('/@(\d+)/', $trigger, $m)) {
+ $text .= ' ' . $rcube->gettext(array(
+ 'name' => 'libcalendaring.alarmat',
+ 'vars' => array('datetime' => $rcube->format_date($m[1]))
+ ));
+ }
+ else if ($val = self::parse_alarm_value($trigger)) {
+ // TODO: for all-day events say 'on date of event at XX' ?
+ if ($val[0] == 0)
+ $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime');
+ else
+ $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext('libcalendaring.trigger' . $val[1]);
+ }
+ else {
+ return false;
+ }
+
+ return $text;
+ }
+
+ /**
+ * Get the next alarm (time & action) for the given event
+ *
+ * @param array Record data
+ * @return array Hash array with alarm time/type or null if no alarms are configured
+ */
+ public static function get_next_alarm($rec, $type = 'event')
+ {
+ if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
+ return null;
+
+ if ($type == 'task') {
+ $timezone = self::get_instance()->timezone;
+ if ($rec['startdate'])
+ $rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
+ if ($rec['date'])
+ $rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
+ }
+
+ if (!$rec['end'])
+ $rec['end'] = $rec['start'];
+
+ // support legacy format
+ if (!$rec['valarms']) {
+ list($trigger, $action) = explode(':', $rec['alarms'], 2);
+ if ($alarm = self::parse_alarm_value($trigger)) {
+ $rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
+ }
+ }
+
+ $expires = new DateTime('now - 12 hours');
+ $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
+
+ // handle multiple alarms
+ $notify_at = null;
+ foreach ($rec['valarms'] as $alarm) {
+ $notify_time = null;
+
+ if ($alarm['trigger'] instanceof DateTime) {
+ $notify_time = $alarm['trigger'];
+ }
+ else if (is_string($alarm['trigger'])) {
+ $refdate = $alarm['trigger'][0] == '+' ? $rec['end'] : $rec['start'];
+
+ // abort if no reference date is available to compute notification time
+ if (!is_a($refdate, 'DateTime'))
+ continue;
+
+ // TODO: for all-day events, take start @ 00:00 as reference date ?
+
+ try {
+ $interval = new DateInterval(trim($alarm['trigger'], '+-'));
+ $interval->invert = $alarm['trigger'][0] != '+';
+ $notify_time = clone $refdate;
+ $notify_time->add($interval);
+ }
+ catch (Exception $e) {
+ rcube::raise_error($e, true);
+ continue;
+ }
+ }
+
+ if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
+ $notify_at = $notify_time;
+ $action = $alarm['action'];
+ $alarm_prop = $alarm;
+
+ // generate a unique alarm ID if multiple alarms are set
+ if (count($rec['valarms']) > 1) {
+ $alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
+ }
+ }
+ }
+
+ return !$notify_at ? null : array(
+ 'time' => $notify_at->format('U'),
+ 'action' => $action ? strtoupper($action) : 'DISPLAY',
+ 'id' => $alarm_id,
+ 'prop' => $alarm_prop,
+ );
+ }
+
+ /**
+ * Handler for keep-alive requests
+ * This will check for pending notifications and pass them to the client
+ */
+ public function refresh($attr)
+ {
+ // collect pending alarms from all providers (e.g. calendar, tasks)
+ $plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
+ 'time' => time(),
+ 'alarms' => array(),
+ ));
+
+ if (!$plugin['abort'] && !empty($plugin['alarms'])) {
+ // make sure texts and env vars are available on client
+ $this->add_texts('localization/', true);
+ $this->rc->output->add_label('close');
+ $this->rc->output->set_env('snooze_select', $this->snooze_select());
+ $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
+ }
+ }
+
+ /**
+ * Handler for alarm dismiss/snooze requests
+ */
+ public function alarms_action()
+ {
+// $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
+ $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+
+ $data['ids'] = explode(',', $data['id']);
+ $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
+
+ if ($plugin['success'])
+ $this->rc->output->show_message('successfullysaved', 'confirmation');
+ else
+ $this->rc->output->show_message('calendar.errorsaving', 'error');
+ }
+
+ /**
+ * Generate reduced and streamlined output for pending alarms
+ */
+ private function _alarms_output($alarms)
+ {
+ $out = array();
+ foreach ($alarms as $alarm) {
+ $out[] = array(
+ 'id' => $alarm['id'],
+ 'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
+ 'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '',
+ 'allDay' => ($alarm['allday'] == 1)?true:false,
+ 'title' => $alarm['title'],
+ 'location' => $alarm['location'],
+ 'calendar' => $alarm['calendar'],
+ );
+ }
+
+ return $out;
+ }
+
+ /**
+ * Render a dropdown menu to choose snooze time
+ */
+ private function snooze_select($attrib = array())
+ {
+ $steps = array(
+ 5 => 'repeatinmin',
+ 10 => 'repeatinmin',
+ 15 => 'repeatinmin',
+ 20 => 'repeatinmin',
+ 30 => 'repeatinmin',
+ 60 => 'repeatinhr',
+ 120 => 'repeatinhrs',
+ 1440 => 'repeattomorrow',
+ 10080 => 'repeatinweek',
+ );
+
+ $items = array();
+ foreach ($steps as $n => $label) {
+ $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
+ $this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
+ }
+
+ return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
+ }
+
+
+ /********* Recurrence rules handling ********/
+
+ /**
+ * Render localized text describing the recurrence rule of an event
+ */
+ public function recurrence_text($rrule)
+ {
+ // derive missing FREQ and INTERVAL from RDATE list
+ if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
+ $first = $rrule['RDATE'][0];
+ $second = $rrule['RDATE'][1];
+ $third = $rrule['RDATE'][2];
+ if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
+ $diff = $first->diff($second);
+ foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
+ if ($diff->$k != 0) {
+ $rrule['FREQ'] = $freq;
+ $rrule['INTERVAL'] = $diff->$k;
+
+ // verify interval with next item
+ if (is_a($third, 'DateTime')) {
+ $diff2 = $second->diff($third);
+ if ($diff2->$k != $diff->$k) {
+ unset($rrule['INTERVAL']);
+ }
+ }
+ break;
+ }
+ }
+ }
+ if (!$rrule['INTERVAL']) {
+ $rrule['FREQ'] = 'RDATE';
+ }
+ $rrule['UNTIL'] = end($rrule['RDATE']);
+ }
+
+ $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
+ $details = '';
+ switch ($rrule['FREQ']) {
+ case 'DAILY':
+ $freq .= $this->gettext('days');
+ break;
+ case 'WEEKLY':
+ $freq .= $this->gettext('weeks');
+ break;
+ case 'MONTHLY':
+ $freq .= $this->gettext('months');
+ break;
+ case 'YEARLY':
+ $freq .= $this->gettext('years');
+ break;
+ }
+
+ if ($rrule['INTERVAL'] <= 1) {
+ $freq = $this->gettext(strtolower($rrule['FREQ']));
+ }
+
+ if ($rrule['COUNT']) {
+ $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
+ }
+ else if ($rrule['UNTIL']) {
+ $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
+ }
+ else {
+ $until = $this->gettext('forever');
+ }
+
+ $except = '';
+ if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
+ $format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
+ $exdates = array_map(
+ function($dt) use ($format) { return format_date($dt, $format); },
+ array_slice($rrule['EXDATE'], 0, 10)
+ );
+ $except = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates);
+ }
+
+ return rtrim($freq . $details . ', ' . $until . $except);
+ }
+
+ /**
+ * Generate the form for recurrence settings
+ */
+ public function recurrence_form($attrib = array())
+ {
+ switch ($attrib['part']) {
+ // frequency selector
+ case 'frequency':
+ $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
+ $select->add($this->gettext('never'), '');
+ $select->add($this->gettext('daily'), 'DAILY');
+ $select->add($this->gettext('weekly'), 'WEEKLY');
+ $select->add($this->gettext('monthly'), 'MONTHLY');
+ $select->add($this->gettext('yearly'), 'YEARLY');
+ $select->add($this->gettext('rdate'), 'RDATE');
+ $html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show('');
+ break;
+
+ // daily recurrence
+ case 'daily':
+ $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
+ $html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days')));
+ break;
+
+ // weekly recurrence form
+ case 'weekly':
+ $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
+ $html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks')));
+ // weekday selection
+ $daymap = array('sun','mon','tue','wed','thu','fri','sat');
+ $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
+ $first = $this->rc->config->get('calendar_first_day', 1);
+ for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
+ $d = $j % 7;
+ $weekdays .= html::label(array('class' => 'weekday'),
+ $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
+ $this->gettext($daymap[$d])
+ ) . ' ';
+ }
+ $html .= html::div($attrib, html::label(null, $this->gettext('bydays')) . $weekdays);
+ break;
+
+ // monthly recurrence form
+ case 'monthly':
+ $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
+ $html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months')));
+
+ $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
+ for ($monthdays = '', $d = 1; $d <= 31; $d++) {
+ $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
+ $monthdays .= $d % 7 ? ' ' : html::br();
+ }
+
+ // rule selectors
+ $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
+ $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
+ $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
+ $table->add(null, $monthdays);
+ $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery')));
+ $table->add(null, $this->rrule_selectors($attrib['part']));
+
+ $html .= html::div($attrib, $table->show());
+ break;
+
+ // annually recurrence form
+ case 'yearly':
+ $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
+ $html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years')));
+ // month selector
+ $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
+ $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
+ for ($months = '', $m = 1; $m <= 12; $m++) {
+ $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
+ $months .= $m % 4 ? ' ' : html::br();
+ }
+ $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
+
+ // day rule selection
+ $html .= html::div($attrib, html::label(null, $this->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
+ break;
+
+ // end of recurrence form
+ case 'until':
+ $radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
+ $select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
+ $input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
+
+ $html = html::div('line first',
+ html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
+ $this->gettext('forever'))
+ );
+
+ $forntimes = $this->gettext(array(
+ 'name' => 'forntimes',
+ 'vars' => array('nr' => '%s'))
+ );
+ $html .= html::div('line',
+ $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' .
+ sprintf($forntimes, $select->show(1))
+ );
+
+ $html .= html::div('line',
+ $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' .
+ $this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
+ );
+
+ $html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html);
+ break;
+
+ case 'rdate':
+ $ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
+ $input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10"));
+ $button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
+ $html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
+ break;
+ }
+
+ return $html;
+ }
+
+ /**
+ * Input field for interval selection
+ */
+ private function interval_selector($attrib)
+ {
+ $select = new html_select($attrib);
+ $select->add(range(1,30), range(1,30));
+ return $select;
+ }
+
+ /**
+ * Drop-down menus for recurrence rules like "each last sunday of"
+ */
+ private function rrule_selectors($part, $noselect = null)
+ {
+ // rule selectors
+ $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
+ if ($noselect) $select_prefix->add($noselect, '');
+ $select_prefix->add(array(
+ $this->gettext('first'),
+ $this->gettext('second'),
+ $this->gettext('third'),
+ $this->gettext('fourth'),
+ $this->gettext('last')
+ ),
+ array(1, 2, 3, 4, -1));
+
+ $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
+ if ($noselect) $select_wday->add($noselect, '');
+
+ $daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
+ $first = $this->rc->config->get('calendar_first_day', 1);
+ for ($j = $first; $j <= $first+6; $j++) {
+ $d = $j % 7;
+ $select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
+ }
+
+ return $select_prefix->show() . '&nbsp;' . $select_wday->show();
+ }
+
+ /**
+ * Convert the recurrence settings to be processed on the client
+ */
+ public function to_client_recurrence($recurrence, $allday = false)
+ {
+ if ($recurrence['UNTIL'])
+ $recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
+
+ // format RDATE values
+ if (is_array($recurrence['RDATE'])) {
+ $libcal = $this;
+ $recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
+ return $libcal->adjust_timezone($rdate, true)->format('c');
+ }, $recurrence['RDATE']);
+ }
+
+ unset($recurrence['EXCEPTIONS']);
+
+ return $recurrence;
+ }
+
+ /**
+ * Process the alarms values submitted by the client
+ */
+ public function from_client_recurrence($recurrence, $start = null)
+ {
+ if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
+ $recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
+ }
+
+ if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
+ $tz = $this->timezone;
+ $recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
+ try {
+ $dt = new DateTime($rdate, $tz);
+ if (is_a($start, 'DateTime'))
+ $dt->setTime($start->format('G'), $start->format('i'));
+ return $dt;
+ }
+ catch (Exception $e) {
+ return null;
+ }
+ }, $recurrence['RDATE']);
+ }
+
+ return $recurrence;
+ }
+
+
+ /********* Attachments handling *********/
+
+ /**
+ * Handler for attachment uploads
+ */
+ public function attachment_upload($session_key, $id_prefix = '')
+ {
+ // Upload progress update
+ if (!empty($_GET['_progress'])) {
+ $this->rc->upload_progress();
+ }
+
+ $recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
+
+ if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
+ $_SESSION[$session_key] = array();
+ $_SESSION[$session_key]['id'] = $recid;
+ $_SESSION[$session_key]['attachments'] = array();
+ }
+
+ // clear all stored output properties (like scripts and env vars)
+ $this->rc->output->reset();
+
+ if (is_array($_FILES['_attachments']['tmp_name'])) {
+ foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
+ // Process uploaded attachment if there is no error
+ $err = $_FILES['_attachments']['error'][$i];
+
+ if (!$err) {
+ $attachment = array(
+ 'path' => $filepath,
+ 'size' => $_FILES['_attachments']['size'][$i],
+ 'name' => $_FILES['_attachments']['name'][$i],
+ 'mimetype' => rcube_mime::file_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
+ 'group' => $recid,
+ );
+
+ $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
+ }
+
+ if (!$err && $attachment['status'] && !$attachment['abort']) {
+ $id = $attachment['id'];
+
+ // store new attachment in session
+ unset($attachment['status'], $attachment['abort']);
+ $_SESSION[$session_key]['attachments'][$id] = $attachment;
+
+ if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
+ $button = html::img(array(
+ 'src' => $icon,
+ 'alt' => $this->rc->gettext('delete')
+ ));
+ }
+ else {
+ $button = Q($this->rc->gettext('delete'));
+ }
+
+ $content = html::a(array(
+ 'href' => "#delete",
+ 'class' => 'delete',
+ 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
+ 'title' => $this->rc->gettext('delete'),
+ 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
+ ), $button);
+
+ $content .= Q($attachment['name']);
+
+ $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
+ 'html' => $content,
+ 'name' => $attachment['name'],
+ 'mimetype' => $attachment['mimetype'],
+ 'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
+ 'complete' => true), $uploadid);
+ }
+ else { // upload failed
+ if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+ $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
+ 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+ }
+ else if ($attachment['error']) {
+ $msg = $attachment['error'];
+ }
+ else {
+ $msg = $this->rc->gettext('fileuploaderror');
+ }
+
+ $this->rc->output->command('display_message', $msg, 'error');
+ $this->rc->output->command('remove_from_attachment_list', $uploadid);
+ }
+ }
+ }
+ else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ // if filesize exceeds post_max_size then $_FILES array is empty,
+ // show filesizeerror instead of fileuploaderror
+ if ($maxsize = ini_get('post_max_size'))
+ $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
+ 'size' => show_bytes(parse_bytes($maxsize)))));
+ else
+ $msg = $this->rc->gettext('fileuploaderror');
+
+ $this->rc->output->command('display_message', $msg, 'error');
+ $this->rc->output->command('remove_from_attachment_list', $uploadid);
+ }
+
+ $this->rc->output->send('iframe');
+ }
+
+
+ /**
+ * Deliver an event/task attachment to the client
+ * (similar as in Roundcube core program/steps/mail/get.inc)
+ */
+ public function attachment_get($attachment)
+ {
+ ob_end_clean();
+
+ if ($attachment && $attachment['body']) {
+ // allow post-processing of the attachment body
+ $part = new rcube_message_part;
+ $part->filename = $attachment['name'];
+ $part->size = $attachment['size'];
+ $part->mimetype = $attachment['mimetype'];
+
+ $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+ 'body' => $attachment['body'],
+ 'mimetype' => strtolower($attachment['mimetype']),
+ 'download' => !empty($_GET['_download']),
+ 'part' => $part,
+ ));
+
+ if ($plugin['abort'])
+ exit;
+
+ $mimetype = $plugin['mimetype'];
+ list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+ $browser = $this->rc->output->browser;
+
+ // send download headers
+ if ($plugin['download']) {
+ header("Content-Type: application/octet-stream");
+ if ($browser->ie)
+ header("Content-Type: application/force-download");
+ }
+ else if ($ctype_primary == 'text') {
+ header("Content-Type: text/$ctype_secondary");
+ }
+ else {
+ header("Content-Type: $mimetype");
+ header("Content-Transfer-Encoding: binary");
+ }
+
+ // display page, @TODO: support text/plain (and maybe some other text formats)
+ if ($mimetype == 'text/html' && empty($_GET['_download'])) {
+ $OUTPUT = new rcube_html_page();
+ // @TODO: use washtml on $body
+ $OUTPUT->write($plugin['body']);
+ }
+ else {
+ // don't kill the connection if download takes more than 30 sec.
+ @set_time_limit(0);
+
+ $filename = $attachment['name'];
+ $filename = preg_replace('[\r\n]', '', $filename);
+
+ if ($browser->ie && $browser->ver < 7)
+ $filename = rawurlencode(abbreviate_string($filename, 55));
+ else if ($browser->ie)
+ $filename = rawurlencode($filename);
+ else
+ $filename = addcslashes($filename, '"');
+
+ $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
+ header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+ echo $plugin['body'];
+ }
+
+ exit;
+ }
+
+ // if we arrive here, the requested part was not found
+ header('HTTP/1.1 404 Not Found');
+ exit;
+ }
+
+ /**
+ * Show "loading..." page in attachment iframe
+ */
+ public function attachment_loading_page()
+ {
+ $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
+ $message = $this->rc->gettext('loadingdata');
+
+ header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
+ print "<html>\n<head>\n"
+ . '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
+ . '<meta http-equiv="content-type" content="text/html; charset='.RCUBE_CHARSET.'">' . "\n"
+ . "</head>\n<body>\n$message\n</body>\n</html>";
+ exit;
+ }
+
+ /**
+ * Template object for attachment display frame
+ */
+ public function attachment_frame($attrib = array())
+ {
+ $mimetype = strtolower($this->attachment['mimetype']);
+ list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+ $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
+
+ $this->rc->output->add_gui_object('attachmentframe', $attrib['id']);
+
+ return html::iframe($attrib);
+ }
+
+ /**
+ *
+ */
+ public function attachment_header($attrib = array())
+ {
+ $rcmail = rcmail::get_instance();
+ $dl_link = strtolower($attrib['downloadlink']) == 'true';
+ $dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET);
+
+ $table = new html_table(array('cols' => $dl_link ? 3 : 2));
+
+ if (!empty($this->attachment['name'])) {
+ $table->add('title', Q($this->rc->gettext('filename')));
+ $table->add('header', Q($this->attachment['name']));
+ if ($dl_link) {
+ $table->add('download-link', html::a($dl_url, Q($this->rc->gettext('download'))));
+ }
+ }
+
+ if (!empty($this->attachment['mimetype'])) {
+ $table->add('title', Q($this->rc->gettext('type')));
+ $table->add('header', Q($this->attachment['mimetype']));
+ }
+
+ if (!empty($this->attachment['size'])) {
+ $table->add('title', Q($this->rc->gettext('filesize')));
+ $table->add('header', Q(show_bytes($this->attachment['size'])));
+ }
+
+ $this->rc->output->set_env('attachment_download_url', $dl_url);
+
+ return $table->show($attrib);
+ }
+
+
+ /********* iTip message detection *********/
+
+ /**
+ * Check mail message structure of there are .ics files attached
+ */
+ public function mail_message_load($p)
+ {
+ $this->ical_message = $p['object'];
+ $itip_part = null;
+
+ // check all message parts for .ics files
+ foreach ((array)$this->ical_message->mime_parts as $part) {
+ if (self::part_is_vcalendar($part)) {
+ if ($part->ctype_parameters['method'])
+ $itip_part = $part->mime_id;
+ else
+ $this->ical_parts[] = $part->mime_id;
+ }
+ }
+
+ // priorize part with method parameter
+ if ($itip_part) {
+ $this->ical_parts = array($itip_part);
+ }
+ }
+
+ /**
+ * Getter for the parsed iCal objects attached to the current email message
+ *
+ * @return object libvcalendar parser instance with the parsed objects
+ */
+ public function get_mail_ical_objects()
+ {
+ // create parser and load ical objects
+ if (!$this->mail_ical_parser) {
+ $this->mail_ical_parser = $this->get_ical();
+
+ foreach ($this->ical_parts as $mime_id) {
+ $part = $this->ical_message->mime_parts[$mime_id];
+ $charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET;
+ $this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
+
+ // check if the parsed object is an instance of a recurring event/task
+ array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
+
+ // stop on the part that has an iTip method specified
+ if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
+ $this->mail_ical_parser->message_date = $this->ical_message->headers->date;
+ $this->mail_ical_parser->mime_id = $mime_id;
+
+ // store the message's sender address for comparisons
+ $this->mail_ical_parser->sender = preg_match(self::$email_regex, $this->ical_message->headers->from, $m) ? $m[1] : '';
+ if (!empty($this->mail_ical_parser->sender)) {
+ foreach ($this->mail_ical_parser->objects as $i => $object) {
+ $this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
+ $this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ return $this->mail_ical_parser;
+ }
+
+ /**
+ * Read the given mime message from IMAP and parse ical data
+ *
+ * @param string Mailbox name
+ * @param string Message UID
+ * @param string Message part ID and object index (e.g. '1.2:0')
+ * @param string Object type filter (optional)
+ *
+ * @return array Hash array with the parsed iCal
+ */
+ public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
+ {
+ $charset = RCMAIL_CHARSET;
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
+
+ if ($uid && $mime_id) {
+ list($mime_id, $index) = explode(':', $mime_id);
+
+ $part = $imap->get_message_part($uid, $mime_id);
+ $headers = $imap->get_message_headers($uid);
+ $parser = $this->get_ical();
+
+ if ($part->ctype_parameters['charset']) {
+ $charset = $part->ctype_parameters['charset'];
+ }
+
+ if ($part) {
+ $objects = $parser->import($part, $charset);
+ }
+ }
+
+ // successfully parsed events/tasks?
+ if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
+ if ($parser->method)
+ $object['_method'] = $parser->method;
+
+ // store the message's sender address for comparisons
+ $object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
+ $object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
+
+ // check if this is an instance of a recurring event/task
+ self::identify_recurrence_instance($object);
+
+ return $object;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if specified message part is a vcalendar data
+ *
+ * @param rcube_message_part Part object
+ * @return boolean True if part is of type vcard
+ */
+ public static function part_is_vcalendar($part)
+ {
+ return (
+ in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
+ // Apple sends files as application/x-any (!?)
+ ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
+ );
+ }
+
+ /**
+ * Single occourrences of recurring events are identified by their RECURRENCE-ID property
+ * in iCal which is represented as 'recurrence_date' in our internal data structure.
+ *
+ * Check if such a property exists and derive the '_instance' identifier and '_savemode'
+ * attributes which are used in the storage backend to identify the nested exception item.
+ */
+ public static function identify_recurrence_instance(&$object)
+ {
+ // for savemode=all, remove recurrence instance identifiers
+ if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
+ unset($object['_instance'], $object['recurrence_date']);
+ }
+ // set instance and 'savemode' according to recurrence-id
+ else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
+ $object['_instance'] = self::recurrence_instance_identifier($object);
+ $object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
+ }
+ else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) {
+ if (strlen($object['_instance']) > 4) {
+ $object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
+ }
+ else {
+ $object['recurrence_date'] = clone $object['start'];
+ }
+ }
+ }
+
+ /**
+ * Return a date() format string to render identifiers for recurrence instances
+ *
+ * @param array Hash array with event properties
+ * @return string Format string
+ */
+ public static function recurrence_id_format($event)
+ {
+ return $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ }
+
+ /**
+ * Return the identifer for the given instance of a recurring event
+ *
+ * @param array Hash array with event properties
+ * @return mixed Format string or null if identifier cannot be generated
+ */
+ public static function recurrence_instance_identifier($event)
+ {
+ $instance_date = $event['recurrence_date'] ?: $event['start'];
+
+ if ($instance_date && is_a($instance_date, 'DateTime')) {
+ $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+ return $instance_date->format($recurrence_id_format);
+ }
+
+ return null;
+ }
+
+
+ /********* Attendee handling functions *********/
+
+ /**
+ * Handler for attendee group expansion requests
+ */
+ public function expand_attendee_group()
+ {
+ $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
+ $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+ $result = array('id' => $id, 'members' => array());
+ $maxnum = 500;
+
+ // iterate over all autocomplete address books (we don't know the source of the group)
+ foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
+ if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
+ foreach ($abook->list_groups($data['name'], 1) as $group) {
+ // this is the matching group to expand
+ if (in_array($data['email'], (array)$group['email'])) {
+ $abook->set_pagesize($maxnum);
+ $abook->set_group($group['ID']);
+
+ // get all members
+ $res = $abook->list_records($this->rc->config->get('contactlist_fields'));
+
+ // handle errors (e.g. sizelimit, timelimit)
+ if ($abook->get_error()) {
+ $result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
+ $res = false;
+ }
+ // check for maximum number of members (we don't wanna bloat the UI too much)
+ else if ($res->count > $maxnum) {
+ $result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
+ $res = false;
+ }
+
+ while ($res && ($member = $res->iterate())) {
+ $emails = (array)$abook->get_col_values('email', $member, true);
+ if (!empty($emails) && ($email = array_shift($emails))) {
+ $result['members'][] = array(
+ 'email' => $email,
+ 'name' => rcube_addressbook::compose_list_name($member),
+ );
+ }
+ }
+
+ break 2;
+ }
+ }
+ }
+ }
+
+ $this->rc->output->command('plugin.expand_attendee_callback', $result);
+ }
+
+
+ /********* Static utility functions *********/
+
+ /**
+ * Convert the internal structured data into a vcalendar rrule 2.0 string
+ */
+ public static function to_rrule($recurrence, $allday = false)
+ {
+ if (is_string($recurrence))
+ return $recurrence;
+
+ $rrule = '';
+ foreach ((array)$recurrence as $k => $val) {
+ $k = strtoupper($k);
+ switch ($k) {
+ case 'UNTIL':
+ // convert to UTC according to RFC 5545
+ if (is_a($val, 'DateTime')) {
+ if (!$allday && !$val->_dateonly) {
+ $until = clone $val;
+ $until->setTimezone(new DateTimeZone('UTC'));
+ $val = $until->format('Ymd\THis\Z');
+ }
+ else {
+ $val = $val->format('Ymd');
+ }
+ }
+ break;
+ case 'RDATE':
+ case 'EXDATE':
+ foreach ((array)$val as $i => $ex) {
+ if (is_a($ex, 'DateTime'))
+ $val[$i] = $ex->format('Ymd\THis');
+ }
+ $val = join(',', (array)$val);
+ break;
+ case 'EXCEPTIONS':
+ continue 2;
+ }
+
+ if (strlen($val))
+ $rrule .= $k . '=' . $val . ';';
+ }
+
+ return rtrim($rrule, ';');
+ }
+
+ /**
+ * Convert from fullcalendar date format to PHP date() format string
+ */
+ public static function to_php_date_format($from)
+ {
+ // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
+ return strtr(strtr($from, array(
+ 'yyyy' => 'Y',
+ 'yy' => 'y',
+ 'MMMM' => 'F',
+ 'MMM' => 'M',
+ 'MM' => 'm',
+ 'M' => 'n',
+ 'dddd' => 'l',
+ 'ddd' => 'D',
+ 'dd' => 'd',
+ 'd' => 'j',
+ 'HH' => '**',
+ 'hh' => '%%',
+ 'H' => 'G',
+ 'h' => 'g',
+ 'mm' => 'i',
+ 'ss' => 's',
+ 'TT' => 'A',
+ 'tt' => 'a',
+ 'T' => 'A',
+ 't' => 'a',
+ 'u' => 'c',
+ )), array(
+ '**' => 'H',
+ '%%' => 'h',
+ ));
+ }
+
+ /**
+ * Convert from PHP date() format to fullcalendar format string
+ */
+ public static function from_php_date_format($from)
+ {
+ // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
+ return strtr($from, array(
+ 'y' => 'yy',
+ 'Y' => 'yyyy',
+ 'M' => 'MMM',
+ 'F' => 'MMMM',
+ 'm' => 'MM',
+ 'n' => 'M',
+ 'j' => 'd',
+ 'd' => 'dd',
+ 'D' => 'ddd',
+ 'l' => 'dddd',
+ 'H' => 'HH',
+ 'h' => 'hh',
+ 'G' => 'H',
+ 'g' => 'h',
+ 'i' => 'mm',
+ 's' => 'ss',
+ 'A' => 'TT',
+ 'a' => 'tt',
+ 'c' => 'u',
+ ));
+ }
+
+}
diff --git a/libcalendaring/libvcalendar.php b/libcalendaring/libvcalendar.php
new file mode 100644
index 0000000..6a1d1ff
--- /dev/null
+++ b/libcalendaring/libvcalendar.php
@@ -0,0 +1,1362 @@
+<?php
+
+/**
+ * iCalendar functions for the libcalendaring plugin
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2013-2015, 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/>.
+ */
+
+use \OldSabre\VObject;
+
+// load OldSabre\VObject classes
+if (!class_exists('\OldSabre\VObject\Reader')) {
+ require_once __DIR__ . '/lib/OldSabre/VObject/includes.php';
+}
+
+/**
+ * Class to parse and build vCalendar (iCalendar) files
+ *
+ * Uses the SabreTooth VObject library, version 2.1.
+ *
+ * Download from https://github.com/fruux/sabre-vobject/archive/2.1.0.zip
+ * and place the lib files in this plugin's lib directory
+ *
+ */
+class libvcalendar implements Iterator
+{
+ private $timezone;
+ private $attach_uri = null;
+ private $prodid = '-//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
+ private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
+ private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE',
+ 'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO');
+ private $iteratorkey = 0;
+ private $charset;
+ private $forward_exceptions;
+ private $vhead;
+ private $fp;
+ private $vtimezones = array();
+
+ public $method;
+ public $agent = '';
+ public $objects = array();
+ public $freebusy = array();
+
+
+ /**
+ * Default constructor
+ */
+ function __construct($tz = null)
+ {
+ $this->timezone = $tz;
+ $this->prodid = '-//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ }
+
+ /**
+ * Setter for timezone information
+ */
+ public function set_timezone($tz)
+ {
+ $this->timezone = $tz;
+ }
+
+ /**
+ * Setter for URI template for attachment links
+ */
+ public function set_attach_uri($uri)
+ {
+ $this->attach_uri = $uri;
+ }
+
+ /**
+ * Setter for a custom PRODID attribute
+ */
+ public function set_prodid($prodid)
+ {
+ $this->prodid = $prodid;
+ }
+
+ /**
+ * Setter for a user-agent string to tweak input/output accordingly
+ */
+ public function set_agent($agent)
+ {
+ $this->agent = $agent;
+ }
+
+ /**
+ * Free resources by clearing member vars
+ */
+ public function reset()
+ {
+ $this->vhead = '';
+ $this->method = '';
+ $this->objects = array();
+ $this->freebusy = array();
+ $this->vtimezones = array();
+ $this->iteratorkey = 0;
+
+ if ($this->fp) {
+ fclose($this->fp);
+ $this->fp = null;
+ }
+ }
+
+ /**
+ * Import events from iCalendar format
+ *
+ * @param string vCalendar input
+ * @param string Input charset (from envelope)
+ * @param boolean True if parsing exceptions should be forwarded to the caller
+ * @return array List of events extracted from the input
+ */
+ public function import($vcal, $charset = 'UTF-8', $forward_exceptions = false, $memcheck = true)
+ {
+ // TODO: convert charset to UTF-8 if other
+
+ try {
+ // estimate the memory usage and try to avoid fatal errors when allowed memory gets exhausted
+ if ($memcheck) {
+ $count = substr_count($vcal, 'BEGIN:VEVENT') + substr_count($vcal, 'BEGIN:VTODO');
+ $expected_memory = $count * 70*1024; // assume ~ 70K per event (empirically determined)
+
+ if (!rcube_utils::mem_check($expected_memory)) {
+ throw new Exception("iCal file too big");
+ }
+ }
+
+ $vobject = VObject\Reader::read($vcal, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
+ if ($vobject)
+ return $this->import_from_vobject($vobject);
+ }
+ catch (Exception $e) {
+ if ($forward_exceptions) {
+ throw $e;
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "iCal data parse error: " . $e->getMessage()),
+ true, false);
+ }
+ }
+
+ return array();
+ }
+
+ /**
+ * Read iCalendar events from a file
+ *
+ * @param string File path to read from
+ * @param string Input charset (from envelope)
+ * @param boolean True if parsing exceptions should be forwarded to the caller
+ * @return array List of events extracted from the file
+ */
+ public function import_from_file($filepath, $charset = 'UTF-8', $forward_exceptions = false)
+ {
+ if ($this->fopen($filepath, $charset, $forward_exceptions)) {
+ while ($this->_parse_next(false)) {
+ // nop
+ }
+
+ fclose($this->fp);
+ $this->fp = null;
+ }
+
+ return $this->objects;
+ }
+
+ /**
+ * Open a file to read iCalendar events sequentially
+ *
+ * @param string File path to read from
+ * @param string Input charset (from envelope)
+ * @param boolean True if parsing exceptions should be forwarded to the caller
+ * @return boolean True if file contents are considered valid
+ */
+ public function fopen($filepath, $charset = 'UTF-8', $forward_exceptions = false)
+ {
+ $this->reset();
+
+ // just to be sure...
+ @ini_set('auto_detect_line_endings', true);
+
+ $this->charset = $charset;
+ $this->forward_exceptions = $forward_exceptions;
+ $this->fp = fopen($filepath, 'r');
+
+ // check file content first
+ $begin = fread($this->fp, 1024);
+ if (!preg_match('/BEGIN:VCALENDAR/i', $begin)) {
+ return false;
+ }
+
+ fseek($this->fp, 0);
+ return $this->_parse_next();
+ }
+
+ /**
+ * Parse the next event/todo/freebusy object from the input file
+ */
+ private function _parse_next($reset = true)
+ {
+ if ($reset) {
+ $this->iteratorkey = 0;
+ $this->objects = array();
+ $this->freebusy = array();
+ }
+
+ $next = $this->_next_component();
+ $buffer = $next;
+
+ // load the next component(s) too, as they could contain recurrence exceptions
+ while (preg_match('/(RRULE|RECURRENCE-ID)[:;]/i', $next)) {
+ $next = $this->_next_component();
+ $buffer .= $next;
+ }
+
+ // parse the vevent block surrounded with the vcalendar heading
+ if (strlen($buffer) && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $buffer)) {
+ try {
+ $this->import($this->vhead . $buffer . "END:VCALENDAR", $this->charset, true, false);
+ }
+ catch (Exception $e) {
+ if ($this->forward_exceptions) {
+ throw new VObject\ParseException($e->getMessage() . " in\n" . $buffer);
+ }
+ else {
+ // write the failing section to error log
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => $e->getMessage() . " in\n" . $buffer),
+ true, false);
+ }
+
+ // advance to next
+ return $this->_parse_next($reset);
+ }
+
+ return count($this->objects) > 0;
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to read the next calendar component from the file
+ */
+ private function _next_component()
+ {
+ $buffer = '';
+ $vcalendar_head = false;
+ while (($line = fgets($this->fp, 1024)) !== false) {
+ // ignore END:VCALENDAR lines
+ if (preg_match('/END:VCALENDAR/i', $line)) {
+ continue;
+ }
+ // read vcalendar header (with timezone defintion)
+ if (preg_match('/BEGIN:VCALENDAR/i', $line)) {
+ $this->vhead = '';
+ $vcalendar_head = true;
+ }
+
+ // end of VCALENDAR header part
+ if ($vcalendar_head && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
+ $vcalendar_head = false;
+ }
+
+ if ($vcalendar_head) {
+ $this->vhead .= $line;
+ }
+ else {
+ $buffer .= $line;
+ if (preg_match('/END:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
+ break;
+ }
+ }
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * Import objects from an already parsed OldSabre\VObject\Component object
+ *
+ * @param object OldSabre\VObject\Component to read from
+ * @return array List of events extracted from the file
+ */
+ public function import_from_vobject($vobject)
+ {
+ $seen = array();
+ $exceptions = array();
+
+ if ($vobject->name == 'VCALENDAR') {
+ $this->method = strval($vobject->METHOD);
+ $this->agent = strval($vobject->PRODID);
+
+ foreach ($vobject->getBaseComponents() ?: $vobject->getComponents() as $ve) {
+ if ($ve->name == 'VEVENT' || $ve->name == 'VTODO') {
+ // convert to hash array representation
+ $object = $this->_to_array($ve);
+
+ // temporarily store this as exception
+ if ($object['recurrence_date']) {
+ $exceptions[] = $object;
+ }
+ else if (!$seen[$object['uid']]++) {
+ $this->objects[] = $object;
+ }
+ }
+ else if ($ve->name == 'VFREEBUSY') {
+ $this->objects[] = $this->_parse_freebusy($ve);
+ }
+ }
+
+ // add exceptions to the according master events
+ foreach ($exceptions as $exception) {
+ $uid = $exception['uid'];
+
+ // make this exception the master
+ if (!$seen[$uid]++) {
+ $this->objects[] = $exception;
+ }
+ else {
+ foreach ($this->objects as $i => $object) {
+ // add as exception to existing entry with a matching UID
+ if ($object['uid'] == $uid) {
+ $this->objects[$i]['exceptions'][] = $exception;
+
+ if (!empty($object['recurrence'])) {
+ $this->objects[$i]['recurrence']['EXCEPTIONS'] = &$this->objects[$i]['exceptions'];
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return $this->objects;
+ }
+
+ /**
+ * Getter for free-busy periods
+ */
+ public function get_busy_periods()
+ {
+ $out = array();
+ foreach ((array)$this->freebusy['periods'] as $period) {
+ if ($period[2] != 'FREE') {
+ $out[] = $period;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Helper method to determine whether the connected client is an Apple device
+ */
+ private function is_apple()
+ {
+ return stripos($this->agent, 'Apple') !== false
+ || stripos($this->agent, 'Mac OS X') !== false
+ || stripos($this->agent, 'iOS/') !== false;
+ }
+
+ /**
+ * Convert the given VEvent object to a libkolab compatible array representation
+ *
+ * @param object Vevent object to convert
+ * @return array Hash array with object properties
+ */
+ private function _to_array($ve)
+ {
+ $event = array(
+ 'uid' => self::convert_string($ve->UID),
+ 'title' => self::convert_string($ve->SUMMARY),
+ '_type' => $ve->name == 'VTODO' ? 'task' : 'event',
+ // set defaults
+ 'priority' => 0,
+ 'attendees' => array(),
+ 'x-custom' => array(),
+ );
+
+ // Catch possible exceptions when date is invalid (Bug #2144)
+ // We can skip these fields, they aren't critical
+ foreach (array('CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed') as $attr => $field) {
+ try {
+ if (!$event[$field] && $ve->{$attr}) {
+ $event[$field] = $ve->{$attr}->getDateTime();
+ }
+ } catch (Exception $e) {}
+ }
+
+ // map other attributes to internal fields
+ foreach ($ve->children as $prop) {
+ if (!($prop instanceof VObject\Property))
+ continue;
+
+ switch ($prop->name) {
+ case 'DTSTART':
+ case 'DTEND':
+ case 'DUE':
+ $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'DUE' => 'due');
+ $event[$propmap[$prop->name]] = self::convert_datetime($prop);
+ break;
+
+ case 'TRANSP':
+ $event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy';
+ break;
+
+ case 'STATUS':
+ if ($prop->value == 'TENTATIVE')
+ $event['free_busy'] = 'tentative';
+ else if ($prop->value == 'CANCELLED')
+ $event['cancelled'] = true;
+ else if ($prop->value == 'COMPLETED')
+ $event['complete'] = 100;
+
+ $event['status'] = strval($prop->value);
+ break;
+
+ case 'PRIORITY':
+ if (is_numeric($prop->value))
+ $event['priority'] = $prop->value;
+ break;
+
+ case 'RRULE':
+ $params = is_array($event['recurrence']) ? $event['recurrence'] : array();
+ // parse recurrence rule attributes
+ foreach (explode(';', $prop->value) as $par) {
+ list($k, $v) = explode('=', $par);
+ $params[$k] = $v;
+ }
+ if ($params['UNTIL'])
+ $params['UNTIL'] = date_create($params['UNTIL']);
+ if (!$params['INTERVAL'])
+ $params['INTERVAL'] = 1;
+
+ $event['recurrence'] = array_filter($params);
+ break;
+
+ case 'EXDATE':
+ if (!empty($prop->value))
+ $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], self::convert_datetime($prop, true));
+ break;
+
+ case 'RDATE':
+ if (!empty($prop->value))
+ $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], self::convert_datetime($prop, true));
+ break;
+
+ case 'RECURRENCE-ID':
+ $event['recurrence_date'] = self::convert_datetime($prop);
+ if ($prop->offsetGet('RANGE') == 'THISANDFUTURE' || $prop->offsetGet('THISANDFUTURE') !== null) {
+ $event['thisandfuture'] = true;
+ }
+ break;
+
+ case 'RELATED-TO':
+ $reltype = $prop->offsetGet('RELTYPE');
+ if ($reltype == 'PARENT' || $reltype === null) {
+ $event['parent_id'] = $prop->value;
+ }
+ break;
+
+ case 'SEQUENCE':
+ $event['sequence'] = intval($prop->value);
+ break;
+
+ case 'PERCENT-COMPLETE':
+ $event['complete'] = intval($prop->value);
+ break;
+
+ case 'LOCATION':
+ case 'DESCRIPTION':
+ case 'URL':
+ case 'COMMENT':
+ $event[strtolower($prop->name)] = self::convert_string($prop);
+ break;
+
+ case 'CATEGORY':
+ case 'CATEGORIES':
+ $event['categories'] = array_merge((array)$event['categories'], $prop->getParts());
+ break;
+
+ case 'CLASS':
+ case 'X-CALENDARSERVER-ACCESS':
+ $event['sensitivity'] = strtolower($prop->value);
+ break;
+
+ case 'X-MICROSOFT-CDO-BUSYSTATUS':
+ if ($prop->value == 'OOF')
+ $event['free_busy'] = 'outofoffice';
+ else if (in_array($prop->value, array('FREE', 'BUSY', 'TENTATIVE')))
+ $event['free_busy'] = strtolower($prop->value);
+ break;
+
+ case 'ATTENDEE':
+ case 'ORGANIZER':
+ $params = array('rsvp' => false);
+ foreach ($prop->parameters as $param) {
+ switch ($param->name) {
+ case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
+ default: $params[$param->name] = $param->value; break;
+ }
+ }
+ $attendee = self::map_keys($params, array_flip($this->attendee_keymap));
+ $attendee['email'] = preg_replace('/^mailto:/i', '', $prop->value);
+
+ if ($prop->name == 'ORGANIZER') {
+ $attendee['role'] = 'ORGANIZER';
+ $attendee['status'] = 'ACCEPTED';
+ $event['organizer'] = $attendee;
+ }
+ else if ($attendee['email'] != $event['organizer']['email']) {
+ $event['attendees'][] = $attendee;
+ }
+ break;
+
+ case 'ATTACH':
+ $params = self::parameters_array($prop);
+ if (substr($prop->value, 0, 4) == 'http' && !strpos($prop->value, ':attachment:')) {
+ $event['links'][] = $prop->value;
+ }
+ else if (strlen($prop->value) && strtoupper($params['VALUE']) == 'BINARY') {
+ $attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name'));
+ $attachment['data'] = base64_decode($prop->value);
+ $attachment['size'] = strlen($attachment['data']);
+ $event['attachments'][] = $attachment;
+ }
+ break;
+
+ default:
+ if (substr($prop->name, 0, 2) == 'X-')
+ $event['x-custom'][] = array($prop->name, strval($prop->value));
+ break;
+ }
+ }
+
+ // check DURATION property if no end date is set
+ if (empty($event['end']) && $ve->DURATION) {
+ try {
+ $duration = new DateInterval(strval($ve->DURATION));
+ $end = clone $event['start'];
+ $end->add($duration);
+ $event['end'] = $end;
+ }
+ catch (\Exception $e) {
+ trigger_error(strval($e), E_USER_WARNING);
+ }
+ }
+
+ // validate event dates
+ if ($event['_type'] == 'event') {
+ // check for all-day dates
+ if ($event['start']->_dateonly) {
+ $event['allday'] = true;
+ }
+
+ // all-day events may lack the DTEND property
+ if ($event['allday'] && empty($event['end'])) {
+ $event['end'] = clone $event['start'];
+ }
+ // shift end-date by one day (except Thunderbird)
+ else if ($event['allday'] && is_object($event['end'])) {
+ $event['end']->sub(new \DateInterval('PT23H'));
+ }
+
+ // sanity-check and fix end date
+ if (!empty($event['end']) && $event['end'] < $event['start']) {
+ $event['end'] = clone $event['start'];
+ }
+ }
+
+ // make organizer part of the attendees list for compatibility reasons
+ if (!empty($event['organizer']) && is_array($event['attendees']) && $event['_type'] == 'event') {
+ array_unshift($event['attendees'], $event['organizer']);
+ }
+
+ // find alarms
+ foreach ($ve->select('VALARM') as $valarm) {
+ $action = 'DISPLAY';
+ $trigger = null;
+ $alarm = array();
+
+ foreach ($valarm->children as $prop) {
+ switch ($prop->name) {
+ case 'TRIGGER':
+ foreach ($prop->parameters as $param) {
+ if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') {
+ $trigger = '@' . $prop->getDateTime()->format('U');
+ $alarm['trigger'] = $prop->getDateTime();
+ }
+ }
+ if (!$trigger && ($values = libcalendaring::parse_alarm_value($prop->value))) {
+ $trigger = $values[2];
+ }
+
+ if (!$alarm['trigger']) {
+ $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $prop->value), 'T');
+ // if all 0-values have been stripped, assume 'at time'
+ if ($alarm['trigger'] == 'P')
+ $alarm['trigger'] = 'PT0S';
+ }
+ break;
+
+ case 'ACTION':
+ $action = $alarm['action'] = strtoupper($prop->value);
+ break;
+
+ case 'SUMMARY':
+ case 'DESCRIPTION':
+ case 'DURATION':
+ $alarm[strtolower($prop->name)] = self::convert_string($prop);
+ break;
+
+ case 'REPEAT':
+ $alarm['repeat'] = intval($prop->value);
+ break;
+
+ case 'ATTENDEE':
+ $alarm['attendees'][] = preg_replace('/^mailto:/i', '', $prop->value);
+ break;
+
+ case 'ATTACH':
+ $params = self::parameters_array($prop);
+ if (strlen($prop->value) && (preg_match('/^[a-z]+:/', $prop->value) || strtoupper($params['VALUE']) == 'URI')) {
+ // we only support URI-type of attachments here
+ $alarm['uri'] = $prop->value;
+ }
+ break;
+ }
+ }
+
+ if ($action != 'NONE') {
+ if ($trigger && !$event['alarms']) // store first alarm in legacy property
+ $event['alarms'] = $trigger . ':' . $action;
+
+ if ($alarm['trigger'])
+ $event['valarms'][] = $alarm;
+ }
+ }
+
+ // assign current timezone to event start/end
+ if ($event['start'] instanceof DateTime) {
+ if ($this->timezone)
+ $event['start']->setTimezone($this->timezone);
+ }
+ else {
+ unset($event['start']);
+ }
+
+ if ($event['end'] instanceof DateTime) {
+ if ($this->timezone)
+ $event['end']->setTimezone($this->timezone);
+ }
+ else {
+ unset($event['end']);
+ }
+
+ // minimal validation
+ if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) {
+ throw new VObject\ParseException('Object validation failed: missing mandatory object properties');
+ }
+
+ return $event;
+ }
+
+ /**
+ * Parse the given vfreebusy component into an array representation
+ */
+ private function _parse_freebusy($ve)
+ {
+ $this->freebusy = array('_type' => 'freebusy', 'periods' => array());
+ $seen = array();
+
+ foreach ($ve->children as $prop) {
+ if (!($prop instanceof VObject\Property))
+ continue;
+
+ switch ($prop->name) {
+ case 'CREATED':
+ case 'LAST-MODIFIED':
+ case 'DTSTAMP':
+ case 'DTSTART':
+ case 'DTEND':
+ $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed');
+ $this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop);
+ break;
+
+ case 'ORGANIZER':
+ $this->freebusy['organizer'] = preg_replace('/^mailto:/i', '', $prop->value);
+ break;
+
+ case 'FREEBUSY':
+ // The freebusy component can hold more than 1 value, separated by commas.
+ $periods = explode(',', $prop->value);
+ $fbtype = strval($prop['FBTYPE']) ?: 'BUSY';
+
+ // skip dupes
+ if ($seen[$prop->value.':'.$fbtype]++)
+ continue;
+
+ foreach ($periods as $period) {
+ // Every period is formatted as [start]/[end]. The start is an
+ // absolute UTC time, the end may be an absolute UTC time, or
+ // duration (relative) value.
+ list($busyStart, $busyEnd) = explode('/', $period);
+
+ $busyStart = VObject\DateTimeParser::parse($busyStart);
+ $busyEnd = VObject\DateTimeParser::parse($busyEnd);
+ if ($busyEnd instanceof \DateInterval) {
+ $tmp = clone $busyStart;
+ $tmp->add($busyEnd);
+ $busyEnd = $tmp;
+ }
+
+ if ($busyEnd && $busyEnd > $busyStart)
+ $this->freebusy['periods'][] = array($busyStart, $busyEnd, $fbtype);
+ }
+ break;
+
+ case 'COMMENT':
+ $this->freebusy['comment'] = $prop->value;
+ }
+ }
+
+ return $this->freebusy;
+ }
+
+ /**
+ *
+ */
+ public static function convert_string($prop)
+ {
+ return str_replace('\,', ',', strval($prop->value));
+ }
+
+ /**
+ * Helper method to correctly interpret an all-day date value
+ */
+ public static function convert_datetime($prop, $as_array = false)
+ {
+ if (empty($prop)) {
+ return $as_array ? array() : null;
+ }
+ else if ($prop instanceof VObject\Property\MultiDateTime) {
+ $dt = array();
+ $dateonly = ($prop->getDateType() & VObject\Property\DateTime::DATE);
+ foreach ($prop->getDateTimes() as $item) {
+ $item->_dateonly = $dateonly;
+ $dt[] = $item;
+ }
+ }
+ else if ($prop instanceof VObject\Property\DateTime) {
+ $dt = $prop->getDateTime();
+ if ($prop->getDateType() & VObject\Property\DateTime::DATE) {
+ $dt->_dateonly = true;
+ }
+ }
+ else if ($prop instanceof VObject\Property && ($prop['VALUE'] == 'DATE' || $prop['VALUE'] == 'DATE-TIME')) {
+ try {
+ list($type, $dt) = VObject\Property\DateTime::parseData($prop->value, $prop);
+ $dt->_dateonly = ($type & VObject\Property\DateTime::DATE);
+ }
+ catch (Exception $e) {
+ // ignore date parse errors
+ }
+ }
+ else if ($prop instanceof VObject\Property && $prop['VALUE'] == 'PERIOD') {
+ $dt = array();
+ foreach(explode(',', $prop->value) as $val) {
+ try {
+ list($start, $end) = explode('/', $val);
+ list($type, $item) = VObject\Property\DateTime::parseData($start, $prop);
+ $item->_dateonly = ($type & VObject\Property\DateTime::DATE);
+ $dt[] = $item;
+ }
+ catch (Exception $e) {
+ // ignore single date parse errors
+ }
+ }
+ }
+ else if ($prop instanceof DateTime) {
+ $dt = $prop;
+ }
+
+ // force return value to array if requested
+ if ($as_array && !is_array($dt)) {
+ $dt = empty($dt) ? array() : array($dt);
+ }
+
+ return $dt;
+ }
+
+
+ /**
+ * Create a OldSabre\VObject\Property instance from a PHP DateTime object
+ *
+ * @param string Property name
+ * @param object DateTime
+ */
+ public function datetime_prop($name, $dt, $utc = false, $dateonly = null)
+ {
+ $is_utc = $utc || (($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')));
+ $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly;
+ $vdt = new VObject\Property\DateTime($name);
+ $vdt->setDateTime($dt, $is_dateonly ? VObject\Property\DateTime::DATE :
+ ($is_utc ? VObject\Property\DateTime::UTC : VObject\Property\DateTime::LOCALTZ));
+
+ // register timezone for VTIMEZONE block
+ if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) {
+ $ts = $dt->format('U');
+ if (is_array($this->vtimezones[$tzname])) {
+ $this->vtimezones[$tzname][0] = min($this->vtimezones[$tzname][0], $ts);
+ $this->vtimezones[$tzname][1] = max($this->vtimezones[$tzname][1], $ts);
+ }
+ else {
+ $this->vtimezones[$tzname] = array($ts, $ts);
+ }
+ }
+
+ return $vdt;
+ }
+
+ /**
+ * Copy values from one hash array to another using a key-map
+ */
+ public static function map_keys($values, $map)
+ {
+ $out = array();
+ foreach ($map as $from => $to) {
+ if (isset($values[$from]))
+ $out[$to] = is_array($values[$from]) ? join(',', $values[$from]) : $values[$from];
+ }
+ return $out;
+ }
+
+ /**
+ *
+ */
+ private static function parameters_array($prop)
+ {
+ $params = array();
+ foreach ($prop->parameters as $param) {
+ $params[strtoupper($param->name)] = $param->value;
+ }
+ return $params;
+ }
+
+
+ /**
+ * Export events to iCalendar format
+ *
+ * @param array Events as array
+ * @param string VCalendar method to advertise
+ * @param boolean Directly send data to stdout instead of returning
+ * @param callable Callback function to fetch attachment contents, false if no attachment export
+ * @param boolean Add VTIMEZONE block with timezone definitions for the included events
+ * @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
+ */
+ public function export($objects, $method = null, $write = false, $get_attachment = false, $with_timezones = true)
+ {
+ $this->method = $method;
+
+ // encapsulate in VCALENDAR container
+ $vcal = VObject\Component::create('VCALENDAR');
+ $vcal->version = '2.0';
+ $vcal->prodid = $this->prodid;
+ $vcal->calscale = 'GREGORIAN';
+
+ if (!empty($method)) {
+ $vcal->METHOD = $method;
+ }
+
+ // write vcalendar header
+ if ($write) {
+ echo preg_replace('/END:VCALENDAR[\r\n]*$/m', '', $vcal->serialize());
+ }
+
+ foreach ($objects as $object) {
+ $this->_to_ical($object, !$write?$vcal:false, $get_attachment);
+ }
+
+ // include timezone information
+ if ($with_timezones || !empty($method)) {
+ foreach ($this->vtimezones as $tzid => $range) {
+ $vt = self::get_vtimezone($tzid, $range[0], $range[1]);
+ if (empty($vt)) {
+ continue; // no timezone information found
+ }
+
+ if ($write) {
+ echo $vt->serialize();
+ }
+ else {
+ $vcal->add($vt);
+ }
+ }
+ }
+
+ if ($write) {
+ echo "END:VCALENDAR\r\n";
+ return true;
+ }
+ else {
+ return $vcal->serialize();
+ }
+ }
+
+ /**
+ * Build a valid iCal format block from the given event
+ *
+ * @param array Hash array with event/task properties from libkolab
+ * @param object VCalendar object to append event to or false for directly sending data to stdout
+ * @param callable Callback function to fetch attachment contents, false if no attachment export
+ * @param object RECURRENCE-ID property when serializing a recurrence exception
+ */
+ private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null)
+ {
+ $type = $event['_type'] ?: 'event';
+ $ve = VObject\Component::create($this->type_component_map[$type]);
+ $ve->add('UID', $event['uid']);
+
+ // set DTSTAMP according to RFC 5545, 3.8.7.2.
+ $dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime();
+ $ve->add($this->datetime_prop('DTSTAMP', $dtstamp, true));
+
+ // all-day events end the next day
+ if ($event['allday'] && !empty($event['end'])) {
+ $event['end'] = clone $event['end'];
+ $event['end']->add(new \DateInterval('P1D'));
+ $event['end']->_dateonly = true;
+ }
+ if (!empty($event['created']))
+ $ve->add($this->datetime_prop('CREATED', $event['created'], true));
+ if (!empty($event['changed']))
+ $ve->add($this->datetime_prop('LAST-MODIFIED', $event['changed'], true));
+ if (!empty($event['start']))
+ $ve->add($this->datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
+ if (!empty($event['end']))
+ $ve->add($this->datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
+ if (!empty($event['due']))
+ $ve->add($this->datetime_prop('DUE', $event['due'], false));
+
+ // we're exporting a recurrence instance only
+ if (!$recurrence_id && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) {
+ $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $event['recurrence_date'], false, (bool)$event['allday']);
+ if ($event['thisandfuture'])
+ $recurrence_id->add('RANGE', 'THISANDFUTURE');
+ }
+
+ if ($recurrence_id)
+ $ve->add($recurrence_id);
+
+ $ve->add('SUMMARY', $event['title']);
+
+ if ($event['location'])
+ $ve->add($this->is_apple() ? new vobject_location_property('LOCATION', $event['location']) : new VObject\Property('LOCATION', $event['location']));
+ if ($event['description'])
+ $ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
+
+ if (isset($event['sequence']))
+ $ve->add('SEQUENCE', $event['sequence']);
+
+ if ($event['recurrence'] && !$recurrence_id) {
+ $exdates = $rdates = null;
+ if (isset($event['recurrence']['EXDATE'])) {
+ $exdates = $event['recurrence']['EXDATE'];
+ unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value
+ }
+ if (isset($event['recurrence']['RDATE'])) {
+ $rdates = $event['recurrence']['RDATE'];
+ unset($event['recurrence']['RDATE']); // don't serialize RDATEs into RRULE value
+ }
+
+ if ($event['recurrence']['FREQ']) {
+ $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], (bool)$event['allday']));
+ }
+
+ // add EXDATEs each one per line (for Thunderbird Lightning)
+ if (is_array($exdates)) {
+ foreach ($exdates as $ex) {
+ if ($ex instanceof \DateTime) {
+ $exd = clone $event['start'];
+ $exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
+ $exd->setTimeZone(new \DateTimeZone('UTC'));
+ $ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
+ }
+ }
+ }
+ // add RDATEs
+ if (is_array($rdates) && !empty($rdates)) {
+ $sample = $this->datetime_prop('RDATE', $rdates[0]);
+ $rdprop = new VObject\Property\MultiDateTime('RDATE', null);
+ $rdprop->setDateTimes($rdates, $sample->getDateType());
+ $ve->add($rdprop);
+ }
+ }
+
+ if ($event['categories']) {
+ $cat = VObject\Property::create('CATEGORIES');
+ $cat->setParts((array)$event['categories']);
+ $ve->add($cat);
+ }
+
+ if (!empty($event['free_busy'])) {
+ $ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
+
+ // for Outlook clients we provide the X-MICROSOFT-CDO-BUSYSTATUS property
+ if (stripos($this->agent, 'outlook') !== false) {
+ $ve->add('X-MICROSOFT-CDO-BUSYSTATUS', $event['free_busy'] == 'outofoffice' ? 'OOF' : strtoupper($event['free_busy']));
+ }
+ }
+
+ if ($event['priority'])
+ $ve->add('PRIORITY', $event['priority']);
+
+ if ($event['cancelled'])
+ $ve->add('STATUS', 'CANCELLED');
+ else if ($event['free_busy'] == 'tentative')
+ $ve->add('STATUS', 'TENTATIVE');
+ else if ($event['complete'] == 100)
+ $ve->add('STATUS', 'COMPLETED');
+ else if (!empty($event['status']))
+ $ve->add('STATUS', $event['status']);
+
+ if (!empty($event['sensitivity']))
+ $ve->add('CLASS', strtoupper($event['sensitivity']));
+
+ if (!empty($event['complete'])) {
+ $ve->add('PERCENT-COMPLETE', intval($event['complete']));
+ // Apple iCal required the COMPLETED date to be set in order to consider a task complete
+ if ($event['complete'] == 100)
+ $ve->add($this->datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
+ }
+
+ if ($event['valarms']) {
+ foreach ($event['valarms'] as $alarm) {
+ $va = VObject\Component::create('VALARM');
+ $va->action = $alarm['action'];
+ if ($alarm['trigger'] instanceof DateTime) {
+ $va->add($this->datetime_prop('TRIGGER', $alarm['trigger'], true));
+ }
+ else {
+ $va->add('TRIGGER', $alarm['trigger']);
+ }
+
+ if ($alarm['action'] == 'EMAIL') {
+ foreach ((array)$alarm['attendees'] as $attendee) {
+ $va->add('ATTENDEE', 'mailto:' . $attendee);
+ }
+ }
+ if ($alarm['description']) {
+ $va->add('DESCRIPTION', $alarm['description'] ?: $event['title']);
+ }
+ if ($alarm['summary']) {
+ $va->add('SUMMARY', $alarm['summary']);
+ }
+ if ($alarm['duration']) {
+ $va->add('DURATION', $alarm['duration']);
+ $va->add('REPEAT', intval($alarm['repeat']));
+ }
+ if ($alarm['uri']) {
+ $va->add('ATTACH', $alarm['uri'], array('VALUE' => 'URI'));
+ }
+ $ve->add($va);
+ }
+ }
+ // legacy support
+ else if ($event['alarms']) {
+ $va = VObject\Component::create('VALARM');
+ list($trigger, $va->action) = explode(':', $event['alarms']);
+ $val = libcalendaring::parse_alarm_value($trigger);
+ if ($val[3])
+ $va->add('TRIGGER', $val[3]);
+ else if ($val[0] instanceof DateTime)
+ $va->add($this->datetime_prop('TRIGGER', $val[0]));
+ $ve->add($va);
+ }
+
+ foreach ((array)$event['attendees'] as $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ if (empty($event['organizer']))
+ $event['organizer'] = $attendee;
+ }
+ else if (!empty($attendee['email'])) {
+ if (isset($attendee['rsvp']))
+ $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
+ $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap)));
+ }
+ }
+
+ if ($event['organizer']) {
+ $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN')));
+ }
+
+ foreach ((array)$event['url'] as $url) {
+ if (!empty($url)) {
+ $ve->add('URL', $url);
+ }
+ }
+
+ if (!empty($event['parent_id'])) {
+ $ve->add('RELATED-TO', $event['parent_id'], array('RELTYPE' => 'PARENT'));
+ }
+
+ if ($event['comment'])
+ $ve->add('COMMENT', $event['comment']);
+
+ $memory_limit = parse_bytes(ini_get('memory_limit'));
+
+ // export attachments
+ if (!empty($event['attachments'])) {
+ foreach ((array)$event['attachments'] as $attach) {
+ // check available memory and skip attachment export if we can't buffer it
+ // @todo: use rcube_utils::mem_check()
+ if (is_callable($get_attachment) && $memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
+ && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
+ continue;
+ }
+ // embed attachments using the given callback function
+ if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'], $event))) {
+ // embed attachments for iCal
+ $ve->add('ATTACH',
+ base64_encode($data),
+ array_filter(array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name'])));
+ unset($data); // attempt to free memory
+ }
+ // list attachments as absolute URIs
+ else if (!empty($this->attach_uri)) {
+ $ve->add('ATTACH',
+ strtr($this->attach_uri, array(
+ '{{id}}' => urlencode($attach['id']),
+ '{{name}}' => urlencode($attach['name']),
+ '{{mimetype}}' => urlencode($attach['mimetype']),
+ )),
+ array('FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI'));
+ }
+ }
+ }
+
+ foreach ((array)$event['links'] as $uri) {
+ $ve->add('ATTACH', $uri);
+ }
+
+ // add custom properties
+ foreach ((array)$event['x-custom'] as $prop) {
+ $ve->add($prop[0], $prop[1]);
+ }
+
+ // append to vcalendar container
+ if ($vcal) {
+ $vcal->add($ve);
+ }
+ else { // serialize and send to stdout
+ echo $ve->serialize();
+ }
+
+ // append recurrence exceptions
+ if (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
+ $exdate = $ex['recurrence_date'] ?: $ex['start'];
+ $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, false, (bool)$event['allday']);
+ if ($ex['thisandfuture'])
+ $recurrence_id->add('RANGE', 'THISANDFUTURE');
+ $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
+ }
+ }
+ }
+
+ /**
+ * Returns a VTIMEZONE component for a Olson timezone identifier
+ * with daylight transitions covering the given date range.
+ *
+ * @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
+ *
+ * @return mixed A OldSabre\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)
+ {
+ if (!$from) $from = time();
+ if (!$to) $to = $from;
+
+ if (is_string($tzid)) {
+ try {
+ $tz = new \DateTimeZone($tzid);
+ }
+ catch (\Exception $e) {
+ return false;
+ }
+ }
+ else if (is_a($tzid, '\\DateTimeZone')) {
+ $tz = $tzid;
+ }
+
+ if (!is_a($tz, '\\DateTimeZone')) {
+ return false;
+ }
+
+ $year = 86400 * 360;
+ $transitions = $tz->getTransitions($from - $year, $to + $year);
+
+ $vt = new VObject\Component('VTIMEZONE');
+ $vt->TZID = $tz->getName();
+
+ $std = null; $dst = null;
+ foreach ($transitions as $i => $trans) {
+ $cmp = null;
+
+ if ($i == 0) {
+ $tzfrom = $trans['offset'] / 3600;
+ continue;
+ }
+
+ if ($trans['isdst']) {
+ $t_dst = $trans['ts'];
+ $dst = new VObject\Component('DAYLIGHT');
+ $cmp = $dst;
+ }
+ else {
+ $t_std = $trans['ts'];
+ $std = new VObject\Component('STANDARD');
+ $cmp = $std;
+ }
+
+ if ($cmp) {
+ $dt = new DateTime($trans['time']);
+ $offset = $trans['offset'] / 3600;
+
+ $cmp->DTSTART = $dt->format('Ymd\THis');
+ $cmp->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
+ $cmp->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '', floor($offset), ($offset - floor($offset)) * 60);
+
+ if (!empty($trans['abbr'])) {
+ $cmp->TZNAME = $trans['abbr'];
+ }
+
+ $tzfrom = $offset;
+ $vt->add($cmp);
+ }
+
+ // we covered the entire date range
+ if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
+ break;
+ }
+ }
+
+ // add X-MICROSOFT-CDO-TZID if available
+ $microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
+ if (array_key_exists($tz->getName(), $microsoftExchangeMap)) {
+ $vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
+ }
+
+ return $vt;
+ }
+
+
+ /*** Implement PHP 5 Iterator interface to make foreach work ***/
+
+ function current()
+ {
+ return $this->objects[$this->iteratorkey];
+ }
+
+ function key()
+ {
+ return $this->iteratorkey;
+ }
+
+ function next()
+ {
+ $this->iteratorkey++;
+
+ // read next chunk if we're reading from a file
+ if (!$this->objects[$this->iteratorkey] && $this->fp) {
+ $this->_parse_next(true);
+ }
+
+ return $this->valid();
+ }
+
+ function rewind()
+ {
+ $this->iteratorkey = 0;
+ }
+
+ function valid()
+ {
+ return !empty($this->objects[$this->iteratorkey]);
+ }
+
+}
+
+
+/**
+ * Override OldSabre\VObject\Property that quotes commas in the location property
+ * because Apple clients treat that property as list.
+ */
+class vobject_location_property extends VObject\Property
+{
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $str = $this->name;
+
+ foreach ($this->parameters as $param) {
+ $str.=';' . $param->serialize();
+ }
+
+ $src = array(
+ '\\',
+ "\n",
+ ',',
+ );
+ $out = array(
+ '\\\\',
+ '\n',
+ '\,',
+ );
+ $str.=':' . str_replace($src, $out, $this->value);
+
+ $out = '';
+ while (strlen($str) > 0) {
+ if (strlen($str) > 75) {
+ $out.= mb_strcut($str, 0, 75, 'utf-8') . "\r\n";
+ $str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8');
+ } else {
+ $out.= $str . "\r\n";
+ $str = '';
+ break;
+ }
+ }
+
+ return $out;
+ }
+}
diff --git a/libcalendaring/localization/bg_BG.inc b/libcalendaring/localization/bg_BG.inc
new file mode 100644
index 0000000..5250151
--- /dev/null
+++ b/libcalendaring/localization/bg_BG.inc
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+
+$labels['until'] = 'до';
+$labels['alarmemailoption'] = 'Електронна поща';
+$labels['itipcancellation'] = 'Отказано:';
+$labels['itipreply'] = 'Отговор на';
+$labels['itipaccepted'] = 'Приемане';
+$labels['itiptentative'] = 'Може би';
+$labels['itipdeclined'] = 'Отхвърляне';
+$labels['itipsubjectaccepted'] = '"$title" беше прието от $name';
+$labels['itipsubjectdeclined'] = '"$title" беше отхвърлено от $name';
+$labels['updateattendeestatus'] = 'Подновяване на статусът на участника';
+$labels['youhaveaccepted'] = 'Вие сте приели тази покана';
+$labels['importtocalendar'] = 'Запазване в моя календар';
+$labels['removefromcalendar'] = 'Премахване от моя календар';
+$labels['savingdata'] = 'Запазване на данни...';
+
diff --git a/libcalendaring/localization/ca_ES.inc b/libcalendaring/localization/ca_ES.inc
new file mode 100644
index 0000000..76f5a37
--- /dev/null
+++ b/libcalendaring/localization/ca_ES.inc
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'fins';
+$labels['at'] = 'a';
+$labels['alarmemailoption'] = 'Email';
+$labels['frequency'] = 'Repeteix';
+$labels['recurrencend'] = 'fins';
+$labels['itipdelegated'] = 'Delegate';
+$labels['savingdata'] = 'S\'estan desant les dades...';
diff --git a/libcalendaring/localization/cs_CZ.inc b/libcalendaring/localization/cs_CZ.inc
new file mode 100644
index 0000000..d76c1ab
--- /dev/null
+++ b/libcalendaring/localization/cs_CZ.inc
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'do';
+$labels['at'] = 'v';
+$labels['alarmemail'] = 'Poslat e-mail';
+$labels['alarmdisplay'] = 'Zobrazit zprávu';
+$labels['alarmaudio'] = 'Přehrát zvuk';
+$labels['alarmdisplayoption'] = 'Zpráva';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmaudiooption'] = 'Zvuk';
+$labels['alarmat'] = '$datetime';
+$labels['trigger@'] = 'dne';
+$labels['trigger-M'] = 'minut před';
+$labels['trigger-H'] = 'hodin před';
+$labels['trigger-D'] = 'dnů před';
+$labels['trigger+M'] = 'minut po';
+$labels['trigger+H'] = 'hodin po';
+$labels['trigger+D'] = 'dnů po';
+$labels['triggerattime'] = 'v čase';
+$labels['addalarm'] = 'Přidat upozornění';
+$labels['removealarm'] = 'Odstranit upozornění';
+$labels['alarmtitle'] = 'Blížící se události';
+$labels['dismissall'] = 'Zrušit vše';
+$labels['dismiss'] = 'Zrušit';
+$labels['snooze'] = 'Odložit';
+$labels['repeatinmin'] = 'Zopakovat za $min minut';
+$labels['repeatinhr'] = 'Zopakovat za 1 hodinu';
+$labels['repeatinhrs'] = 'Zopakovat za $hrs hodin';
+$labels['repeattomorrow'] = 'Zopakovat zítra';
+$labels['repeatinweek'] = 'Zopakovat za týden';
+$labels['showmore'] = 'Ukázat víc...';
+$labels['frequency'] = 'Opakovat';
+$labels['never'] = 'nikdy';
+$labels['daily'] = 'denně';
+$labels['weekly'] = 'týdně';
+$labels['monthly'] = 'měsíčně';
+$labels['yearly'] = 'ročně';
+$labels['rdate'] = 've dnech';
+$labels['every'] = 'Každý';
+$labels['days'] = 'den (dny)';
+$labels['weeks'] = 'týden (týdny)';
+$labels['months'] = 'měsíc(e/ů)';
+$labels['years'] = 'rok(y/ů) v:';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'do';
+$labels['each'] = 'Každý';
+$labels['onevery'] = 'Vždy v';
+$labels['onsamedate'] = 'Ve stejné datum';
+$labels['forever'] = 'trvale';
+$labels['recurrencend'] = 'do';
+$labels['untilenddate'] = 'až do';
+$labels['forntimes'] = 'jen $nrkrát';
+$labels['first'] = 'první';
+$labels['second'] = 'druhý';
+$labels['third'] = 'třetí';
+$labels['fourth'] = 'čtvrtý';
+$labels['last'] = 'poslední';
+$labels['dayofmonth'] = 'Den v měsíci';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'vyjma';
+$labels['itipinvitation'] = 'Pozvání na událost';
+$labels['itipupdate'] = 'Aktualizace události';
+$labels['itipcancellation'] = 'Zrušeno:';
+$labels['itipreply'] = 'Odpověď na';
+$labels['itipaccepted'] = 'Potvrdit';
+$labels['itiptentative'] = 'Možná';
+$labels['itipdeclined'] = 'Odmítnout';
+$labels['itipdelegated'] = 'Zástupce';
+$labels['itipneeds-action'] = 'Odložit';
+$labels['itipcomment'] = 'Vaše odpověď';
+$labels['itipeditresponse'] = 'Zadejte text odpovědi';
+$labels['itipsendercomment'] = 'Poznámka odesílatele:';
+$labels['itipsuppressreply'] = 'Neposílat odpověď';
+$labels['itipobjectnotfound'] = 'Předmět, na který tato zpráva odkazuje nebyl nalezen ve vašem účtu.';
+$labels['itipsubjectaccepted'] = '$name potvrdil(a) účas na události "$title"';
+$labels['itipsubjecttentative'] = '$name nezávazně potvrdil(a) účast na události "$title"';
+$labels['itipsubjectdeclined'] = '$name odmítl(a) účast na události "$title"';
+$labels['itipsubjectin-process'] = '"$title" je zpracováván $name';
+$labels['itipsubjectcompleted'] = '"$title" byl dokončen $name';
+$labels['itipsubjectcancel'] = 'Vaše účast v "$title" byla zrušena';
+$labels['itipsubjectdelegated'] = '"$title" byl svěřen $name';
+$labels['itipsubjectdelegatedto'] = '"$title" vám byl svěřen k vyřízení $name';
+$labels['itipnewattendee'] = 'Toto je odpověď od nového účastníka';
+$labels['updateattendeestatus'] = 'Aktualizovat stav účastníka';
+$labels['acceptinvitation'] = 'Chcete přijmout toto pozvání (potvrdit účast)?';
+$labels['acceptattendee'] = 'Přijmout účastníka';
+$labels['declineattendee'] = 'Odmítnout účastníka';
+$labels['declineattendeeconfirm'] = 'Zadejte zprávu pro odmítnutého účastníka (nepovinné)';
+$labels['youhaveaccepted'] = 'Přijal(a) jste toto pozvání';
+$labels['youhavetentative'] = 'Nezávazně jste přijal(a) toto pozvání';
+$labels['youhavedeclined'] = 'Odmítl(a) jste toto pozvání';
+$labels['youhavedelegated'] = 'Vyřízením tohoto pozvání jste pověřil někoho jiného';
+$labels['youhavein-process'] = 'Pracujete na této přidělené práci';
+$labels['youhavecompleted'] = 'Dokončil jste tuto přidělenou práci';
+$labels['youhaveneeds-action'] = 'Vaše odpověď na toto pozvání stále čeká na vyřízení';
+$labels['youhavepreviouslyaccepted'] = 'Předtím jste toto pozvání přijal';
+$labels['youhavepreviouslytentative'] = 'Předtím jste toto pozvání předběžně přijal';
+$labels['youhavepreviouslydeclined'] = 'Předtím jste toto pozvání odmítl';
+$labels['youhavepreviouslydelegated'] = 'Předtím jste vyřízením tohoto pozvání někoho pověřil';
+$labels['youhavepreviouslyin-process'] = 'Předtím jste nahlásil, že se na tomto úkolu pracuje';
+$labels['youhavepreviouslycompleted'] = 'Předtím jste tento úkol vyřídil';
+$labels['youhavepreviouslyneeds-action'] = 'Vaše odpověď na toto pozvání stále čeká na vyřízení';
+$labels['attendeeaccepted'] = 'Účastník přijal';
+$labels['attendeetentative'] = 'Účastník předběžně přijal';
+$labels['attendeedeclined'] = 'Účastník odmítl';
+$labels['attendeedelegated'] = 'Účastník pověřil vyřízením $delegatedto';
+$labels['attendeein-process'] = 'Účastník na tomto úkolu pracuje';
+$labels['attendeecompleted'] = 'Účastník úkol vyřídil';
+$labels['notanattendee'] = 'Nejste na seznamu účastníků této události';
+$labels['outdatedinvitation'] = 'Toto pozvání bylo nahrazeno novější verzí';
+$labels['importtocalendar'] = 'Uložit do kalendáře';
+$labels['removefromcalendar'] = 'Odstranit z kalendáře';
+$labels['updatemycopy'] = 'Aktualizovat moji kopii';
+$labels['openpreview'] = 'Otevřít náhled';
+$labels['deleteobjectconfirm'] = 'Opravdu chcete smazat tento předmět?';
+$labels['declinedeleteconfirm'] = 'Také chcete tento odmítnutý předmět smazat ze svého účtu?';
+$labels['delegateinvitation'] = 'Pověřit vyřízením pozvání';
+$labels['delegateto'] = 'Pověřit';
+$labels['delegatersvpme'] = 'Informovat o aktualizacích tohoto případu';
+$labels['delegateinvalidaddress'] = 'Zadejte, prosím, platnou e-mailovou adresu tohoto zástupce.';
+$labels['savingdata'] = 'Ukládám data...';
+$labels['expandattendeegroup'] = 'Nahradit členy skupiny';
+$labels['expandattendeegroupnodata'] = 'Tuto skupinu se nepodařilo nahradit. Nenalezeni žádní členové.';
+$labels['expandattendeegrouperror'] = 'Tuto skupinu se nepodařilo nahradit. Možná má příliš mnoho členů.';
+$labels['expandattendeegroupsizelimit'] = 'Tato skupina má příliš mnoho členů, a proto se ji nepodařilo nahradit.';
diff --git a/libcalendaring/localization/da_DK.inc b/libcalendaring/localization/da_DK.inc
new file mode 100644
index 0000000..a7a1103
--- /dev/null
+++ b/libcalendaring/localization/da_DK.inc
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'indtil';
+$labels['at'] = 'ved';
+$labels['alarmemail'] = 'Afsend e-post';
+$labels['alarmdisplay'] = 'Vis besked';
+$labels['alarmdisplayoption'] = 'Besked';
+$labels['alarmemailoption'] = 'E-post';
+$labels['alarmat'] = 'per $datetime';
+$labels['trigger@'] = 'per dato';
+$labels['trigger-M'] = 'minutter før';
+$labels['trigger-H'] = 'timer før';
+$labels['trigger-D'] = 'dage før';
+$labels['trigger+M'] = 'minutter efter';
+$labels['trigger+H'] = 'timer efter';
+$labels['trigger+D'] = 'dage efter';
+$labels['alarmtitle'] = 'Kommende arrangementer';
+$labels['dismissall'] = 'Afvis alle';
+$labels['dismiss'] = 'Afvis';
+$labels['snooze'] = 'Slumre';
+$labels['repeatinmin'] = 'Gentag om $min minutter';
+$labels['repeatinhr'] = 'Gentag om 1 time';
+$labels['repeatinhrs'] = 'Gentag om $hrs timer';
+$labels['repeattomorrow'] = 'Gentag i morgen';
+$labels['repeatinweek'] = 'Gentag om en uge';
+$labels['showmore'] = 'Vis mere ...';
+$labels['recurring'] = 'Gentages';
+$labels['frequency'] = 'Gentag';
+$labels['never'] = 'aldrig';
+$labels['daily'] = 'dagligt';
+$labels['weekly'] = 'ugentligt';
+$labels['monthly'] = 'månedligt';
+$labels['yearly'] = 'årligt';
+$labels['every'] = 'Hver';
+$labels['days'] = 'dag(e)';
+$labels['weeks'] = 'uge(r)';
+$labels['months'] = 'måned(er)';
+$labels['years'] = 'år i:';
+$labels['bydays'] = 'Per';
+$labels['untildate'] = 'den';
+$labels['each'] = 'Hver';
+$labels['onevery'] = 'På hver';
+$labels['onsamedate'] = 'På samme dato';
+$labels['forever'] = 'for altid';
+$labels['recurrencend'] = 'indtil';
+$labels['forntimes'] = 'for $nr gang(e)';
+$labels['first'] = 'første';
+$labels['second'] = 'anden';
+$labels['third'] = 'tredje';
+$labels['fourth'] = 'fjerde';
+$labels['last'] = 'sidste';
+$labels['dayofmonth'] = 'Dag på måneden';
+$labels['addrdate'] = 'Add repeat date';
+$labels['itipinvitation'] = 'Invitation til';
+$labels['itipupdate'] = 'Opdatering per';
+$labels['itipcancellation'] = 'Aflyst:';
+$labels['itipreply'] = 'Svar til';
+$labels['itipaccepted'] = 'Acceptér';
+$labels['itiptentative'] = 'Måske';
+$labels['itipdeclined'] = 'Afvis';
+$labels['itipdelegated'] = 'Delegér';
+$labels['itipsubjectaccepted'] = '"$title" er blevet accepteret af $name';
+$labels['itipsubjecttentative'] = '"$title" er blevet forsøgsvist accepteret af $name';
+$labels['itipsubjectdeclined'] = '"$title" af blevet afvist af $name';
+$labels['updateattendeestatus'] = 'Opdatér status for deltagere';
+$labels['acceptinvitation'] = 'Vil du acceptere denne invitation?';
+$labels['rsvpmodeall'] = 'Hele serien';
+$labels['rsvpmodecurrent'] = 'Kun denne forekomst';
+$labels['rsvpmodefuture'] = 'Denne og fremtidige forekomster';
+$labels['itipsingleoccurrence'] = 'Dette er en <em>enkelt forekomst</em> ud af en serie af flere begivenheder';
+$labels['itipfutureoccurrence'] = 'Refererer til <em>denne og alle fremtidige forekomster</em> af en serie af begivenheder';
+$labels['itipmessagesingleoccurrence'] = 'Denne besked refererer kun til denne enkelte forekomst';
+$labels['itipmessagefutureoccurrence'] = 'Denne besked refererer til denne og alle fremtidige forekomster';
+$labels['youhaveaccepted'] = 'Du har accepteret denne invitation';
+$labels['youhavetentative'] = 'Du har forsøgsvist accepteret denne invitation';
+$labels['youhavedeclined'] = 'Du har afvist denne invitation';
+$labels['importtocalendar'] = 'Gem i min kalender';
+$labels['removefromcalendar'] = 'Fjern fra min kalender';
+$labels['savingdata'] = 'Gemmer data...';
diff --git a/libcalendaring/localization/de_CH.inc b/libcalendaring/localization/de_CH.inc
new file mode 100644
index 0000000..08e2f43
--- /dev/null
+++ b/libcalendaring/localization/de_CH.inc
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'bis';
+$labels['at'] = 'um';
+$labels['alarmemail'] = 'E-Mail senden';
+$labels['alarmdisplay'] = 'Nachricht anzeigen';
+$labels['alarmdisplayoption'] = 'Nachricht';
+$labels['alarmemailoption'] = 'E-Mail';
+$labels['alarmat'] = 'um $datetime';
+$labels['trigger@'] = 'genau um';
+$labels['trigger-M'] = 'Minuten davor';
+$labels['trigger-H'] = 'Stunden davor';
+$labels['trigger-D'] = 'Tage davor';
+$labels['trigger+M'] = 'Minuten danach';
+$labels['trigger+H'] = 'Stunden danach';
+$labels['trigger+D'] = 'Tage danach';
+$labels['alarmtitle'] = 'Anstehende Termine';
+$labels['dismissall'] = 'Alle ignorieren';
+$labels['dismiss'] = 'Ignorieren';
+$labels['snooze'] = 'Später erinnern';
+$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
+$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
+$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
+$labels['repeattomorrow'] = 'Wiederholung morgen';
+$labels['repeatinweek'] = 'Wiederholung in einer Woche';
+$labels['showmore'] = 'Mehr anzeigen...';
+$labels['frequency'] = 'Wiederholung';
+$labels['never'] = 'nie';
+$labels['daily'] = 'täglich';
+$labels['weekly'] = 'wöchentlich';
+$labels['monthly'] = 'monatlich';
+$labels['yearly'] = 'jährlich';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Alle';
+$labels['days'] = 'Tag(e)';
+$labels['weeks'] = 'Woche(n)';
+$labels['months'] = 'Monat(e)';
+$labels['years'] = 'Jahre(e) im:';
+$labels['bydays'] = 'Am';
+$labels['untildate'] = 'am';
+$labels['each'] = 'Jeden';
+$labels['onevery'] = 'An jedem';
+$labels['onsamedate'] = 'Am gleichen Tag';
+$labels['forever'] = 'unendlich';
+$labels['recurrencend'] = 'bis';
+$labels['forntimes'] = '$nr Wiederholungen';
+$labels['first'] = 'erster';
+$labels['second'] = 'zweiter';
+$labels['third'] = 'dritter';
+$labels['fourth'] = 'vierter';
+$labels['last'] = 'letzter';
+$labels['dayofmonth'] = 'Tag des Montats';
+$labels['addrdate'] = 'Datum hinzufügen';
+$labels['itipinvitation'] = 'Einladung zu';
+$labels['itipupdate'] = 'Aktialisiert:';
+$labels['itipcancellation'] = 'Abgesagt:';
+$labels['itipreply'] = 'Antwort zu';
+$labels['itipaccepted'] = 'Akzeptieren';
+$labels['itiptentative'] = 'Mit Vorbehalt';
+$labels['itipdeclined'] = 'Ablehnen';
+$labels['itipdelegated'] = 'Vertreter';
+$labels['itipsubjectaccepted'] = 'Einladung zu "$title" wurde von $name angenommen';
+$labels['itipsubjecttentative'] = 'Einladung zu "$title" wurde von $name mit Vorbehalt angenommen';
+$labels['itipsubjectdeclined'] = 'Einladung zu "$title" wurde von $name abgelehnt';
+$labels['updateattendeestatus'] = 'Teilnehmerstatus aktualisieren';
+$labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin annehmen?';
+$labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen';
+$labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen';
+$labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt';
+$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgeführt';
+$labels['importtocalendar'] = 'In Kalender übernehmen';
+$labels['removefromcalendar'] = 'Aus meinem Kalender löschen';
+$labels['deleteobjectconfirm'] = 'Möchten Sie dieses Objekt wirklich löschen?';
+$labels['declinedeleteconfirm'] = 'Soll das abgelehnte Objekt ebenfalls aus Ihrem Konto gelöscht werden?';
+$labels['savingdata'] = 'Speichere...';
diff --git a/libcalendaring/localization/de_DE.inc b/libcalendaring/localization/de_DE.inc
new file mode 100644
index 0000000..a519643
--- /dev/null
+++ b/libcalendaring/localization/de_DE.inc
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'bis';
+$labels['at'] = 'um';
+$labels['alarmemail'] = 'E-Mail senden';
+$labels['alarmdisplay'] = 'Nachricht anzeigen';
+$labels['alarmaudio'] = 'Audio abspielen';
+$labels['alarmdisplayoption'] = 'Nachricht';
+$labels['alarmemailoption'] = 'E-Mail';
+$labels['alarmaudiooption'] = 'Audio';
+$labels['alarmat'] = 'um $datetime';
+$labels['trigger@'] = 'genau um';
+$labels['trigger-M'] = 'Minuten davor';
+$labels['trigger-H'] = 'Stunden davor';
+$labels['trigger-D'] = 'Tage davor';
+$labels['trigger+M'] = 'Minuten danach';
+$labels['trigger+H'] = 'Stunden danach';
+$labels['trigger+D'] = 'Tage danach';
+$labels['triggerattime'] = 'um exakt';
+$labels['addalarm'] = 'Erinnerung hinzufügen';
+$labels['removealarm'] = 'Erinnerung entfernen';
+$labels['alarmtitle'] = 'Anstehende Termine';
+$labels['dismissall'] = 'Alle ignorieren';
+$labels['dismiss'] = 'Ignorieren';
+$labels['snooze'] = 'Später erinnern';
+$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
+$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
+$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
+$labels['repeattomorrow'] = 'Wiederholung morgen';
+$labels['repeatinweek'] = 'Wiederholung in einer Woche';
+$labels['showmore'] = 'Mehr anzeigen...';
+$labels['recurring'] = 'Wiederholungen';
+$labels['frequency'] = 'Wiederholung';
+$labels['never'] = 'nie';
+$labels['daily'] = 'täglich';
+$labels['weekly'] = 'wöchentlich';
+$labels['monthly'] = 'monatlich';
+$labels['yearly'] = 'jährlich';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Alle';
+$labels['days'] = 'Tag(e)';
+$labels['weeks'] = 'Woche(n)';
+$labels['months'] = 'Monat(e)';
+$labels['years'] = 'Jahre(e) im:';
+$labels['bydays'] = 'Am';
+$labels['untildate'] = 'am';
+$labels['each'] = 'Jeden';
+$labels['onevery'] = 'An jedem';
+$labels['onsamedate'] = 'Am gleichen Tag';
+$labels['forever'] = 'unendlich';
+$labels['recurrencend'] = 'bis';
+$labels['untilenddate'] = 'bis am';
+$labels['forntimes'] = '$nr Wiederholungen';
+$labels['first'] = 'erster';
+$labels['second'] = 'zweiter';
+$labels['third'] = 'dritter';
+$labels['fourth'] = 'vierter';
+$labels['last'] = 'letzter';
+$labels['dayofmonth'] = 'Tag des Montats';
+$labels['addrdate'] = 'Datum hinzufügen';
+$labels['except'] = 'ausser';
+$labels['itipinvitation'] = 'Einladung zu';
+$labels['itipupdate'] = 'Aktialisiert:';
+$labels['itipcancellation'] = 'Abgesagt:';
+$labels['itipreply'] = 'Antwort zu';
+$labels['itipaccepted'] = 'Akzeptieren';
+$labels['itiptentative'] = 'Mit Vorbehalt';
+$labels['itipdeclined'] = 'Ablehnen';
+$labels['itipdelegated'] = 'Delegieren';
+$labels['itipneeds-action'] = 'Aufschieben';
+$labels['itipcomment'] = 'Ihre Antwort';
+$labels['itipeditresponse'] = 'Antwort eingeben';
+$labels['itipsendercomment'] = 'Kommentar des Absenders:';
+$labels['itipsuppressreply'] = 'Keine Antwort senden';
+$labels['itipobjectnotfound'] = 'Das Objekt auf welches sich diese Nachricht bezieht, wurde in Ihrem Konto nicht gefunden.';
+$labels['itipsubjectaccepted'] = 'Einladung zu "$title" wurde von $name angenommen';
+$labels['itipsubjecttentative'] = 'Einladung zu "$title" wurde von $name mit Vorbehalt angenommen';
+$labels['itipsubjectdeclined'] = 'Einladung zu "$title" wurde von $name abgelehnt';
+$labels['itipsubjectin-process'] = '"$title" ist nun in Bearbeitung von $name';
+$labels['itipsubjectcompleted'] = '"$title" wurde vom $name fertiggestellt';
+$labels['itipsubjectcancel'] = 'Ihre Teilnahme in "$title" wurde aufgehoben';
+$labels['itipsubjectdelegated'] = '"$title" wurde durch $name delegiert';
+$labels['itipsubjectdelegatedto'] = '"$title" wurde durch $name an Sie delegiert';
+$labels['itipnewattendee'] = 'Dies ist eine Antwort von einem neuen Teilnehmer';
+$labels['updateattendeestatus'] = 'Teilnehmerstatus aktualisieren';
+$labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin annehmen?';
+$labels['acceptattendee'] = 'Teilnehmer akzeptieren';
+$labels['declineattendee'] = 'Teilnehmer ablehnen';
+$labels['declineattendeeconfirm'] = 'Nachricht an den abgelehnten Teilnehmer verfassen (optional):';
+$labels['rsvpmodeall'] = 'Die gesamte Reihe';
+$labels['rsvpmodecurrent'] = 'Nur dieses Ereignis';
+$labels['rsvpmodefuture'] = 'Dieses und zukünftige Ereignisse';
+$labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen';
+$labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen';
+$labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt';
+$labels['youhavedelegated'] = 'Sie haben diese Einladung abgelehnt';
+$labels['youhavein-process'] = 'Sie arbeiten an dieser Aufgabe';
+$labels['youhavecompleted'] = 'Sie haben diese Aufgabe erledigt';
+$labels['youhaveneeds-action'] = 'Ihre Antwort auf diese Einladung ist noch ausstehend';
+$labels['youhavepreviouslyaccepted'] = 'Sie haben diese Einladung zuvor angenommen';
+$labels['youhavepreviouslytentative'] = 'Sie haben diese Einladung zuvor mit Vorbehalt angenommen';
+$labels['youhavepreviouslydeclined'] = 'Sie haben diese Einladung zuvor abgelehnt';
+$labels['youhavepreviouslydelegated'] = 'Sie haben diese Einladung zuvor delegiert';
+$labels['youhavepreviouslyin-process'] = 'Sie haben diese Aufgabe zuvor als In Bearbeitung gemeldet';
+$labels['youhavepreviouslycompleted'] = 'Sie haben diese Einladung zuvor erledigt';
+$labels['youhavepreviouslyneeds-action'] = 'Ihre Antwort auf diese Einladung ist noch ausstehend';
+$labels['attendeeaccepted'] = 'Teilnehmer hat akzeptiert';
+$labels['attendeetentative'] = 'Teilnehmer hat mit Vorbehalt akzeptiert';
+$labels['attendeedeclined'] = 'Teilnehmer hat abgelehnt';
+$labels['attendeedelegated'] = 'Teilnehmer hat an $delegatedto delegiert';
+$labels['attendeein-process'] = 'Teilnehmer arbeitet an dieser Aufgabe';
+$labels['attendeecompleted'] = 'Teilnehmer hat die Aufgabe erledigt';
+$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgeführt';
+$labels['outdatedinvitation'] = 'Diese Einladung wurde durch eine neuere Version ersetzt';
+$labels['importtocalendar'] = 'In Kalender übernehmen';
+$labels['removefromcalendar'] = 'Aus meinem Kalender löschen';
+$labels['updatemycopy'] = 'Meine Kopie aktualisieren';
+$labels['openpreview'] = 'Vorschau öffnen';
+$labels['deleteobjectconfirm'] = 'Möchten Sie dieses Objekt wirklich löschen?';
+$labels['declinedeleteconfirm'] = 'Soll das abgelehnte Objekt ebenfalls aus Ihrem Konto gelöscht werden?';
+$labels['delegateinvitation'] = 'Einladung delegieren';
+$labels['delegateto'] = 'Delegieren an';
+$labels['delegatersvpme'] = 'Informiere mich über Aktualisierungen dieses Termins';
+$labels['delegateinvalidaddress'] = 'Geben Sie eine gültige E-Mail-Adresse für den Delegierten ein';
+$labels['savingdata'] = 'Speichere...';
+$labels['expandattendeegroup'] = 'Mit Gruppenmitgliedern ersetzen';
+$labels['expandattendeegroupnodata'] = 'Diese Gruppe konnte nicht ersetzt werden. Keine Gruppenmitglieder gefunden.';
+$labels['expandattendeegrouperror'] = 'Diese Gruppe konnte nicht ersetzt werden. Sie hat möglicherweise zuviele Mitglieder.';
+$labels['expandattendeegroupsizelimit'] = 'Die Gruppe hat zuviele Mitglieder und konnte deshalb nicht ersetzt werden.';
diff --git a/libcalendaring/localization/en_US.inc b/libcalendaring/localization/en_US.inc
new file mode 100644
index 0000000..e5e0426
--- /dev/null
+++ b/libcalendaring/localization/en_US.inc
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+
+$labels = array();
+
+// words for spoken dates
+$labels['until'] = 'until';
+$labels['at'] = 'at';
+
+// alarms related labels
+$labels['alarmemail'] = 'Send Email';
+$labels['alarmdisplay'] = 'Show message';
+$labels['alarmaudio'] = 'Play sound';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmaudiooption'] = 'Sound';
+$labels['alarmat'] = 'at $datetime';
+$labels['trigger@'] = 'on date';
+$labels['trigger-M'] = 'minutes before';
+$labels['trigger-H'] = 'hours before';
+$labels['trigger-D'] = 'days before';
+$labels['trigger+M'] = 'minutes after';
+$labels['trigger+H'] = 'hours after';
+$labels['trigger+D'] = 'days after';
+$labels['triggerattime'] = 'at time';
+$labels['addalarm'] = 'Add alarm';
+$labels['removealarm'] = 'Remove alarm';
+
+$labels['alarmtitle'] = 'Upcoming events';
+$labels['dismissall'] = 'Dismiss all';
+$labels['dismiss'] = 'Dismiss';
+$labels['snooze'] = 'Snooze';
+$labels['repeatinmin'] = 'Repeat in $min minutes';
+$labels['repeatinhr'] = 'Repeat in 1 hour';
+$labels['repeatinhrs'] = 'Repeat in $hrs hours';
+$labels['repeattomorrow'] = 'Repeat tomorrow';
+$labels['repeatinweek'] = 'Repeat in a week';
+
+$labels['showmore'] = 'Show more...';
+
+// recurrence related labels
+$labels['recurring'] = 'Repeats';
+$labels['frequency'] = 'Repeat';
+$labels['never'] = 'never';
+$labels['daily'] = 'daily';
+$labels['weekly'] = 'weekly';
+$labels['monthly'] = 'monthly';
+$labels['yearly'] = 'annually';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Every';
+$labels['days'] = 'day(s)';
+$labels['weeks'] = 'week(s)';
+$labels['months'] = 'month(s)';
+$labels['years'] = 'year(s)';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'the';
+$labels['each'] = 'Each';
+$labels['onevery'] = 'On every';
+$labels['onsamedate'] = 'On the same date';
+$labels['forever'] = 'forever';
+$labels['recurrencend'] = 'until';
+$labels['untilenddate'] = 'until date';
+$labels['forntimes'] = 'for $nr time(s)';
+$labels['first'] = 'first';
+$labels['second'] = 'second';
+$labels['third'] = 'third';
+$labels['fourth'] = 'fourth';
+$labels['last'] = 'last';
+$labels['dayofmonth'] = 'Day of month';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'except';
+
+// itip related labels
+$labels['itipinvitation'] = 'Invitation to';
+$labels['itipupdate'] = 'Update of';
+$labels['itipcancellation'] = 'Cancelled:';
+$labels['itipreply'] = 'Reply to';
+$labels['itipaccepted'] = 'Accept';
+$labels['itiptentative'] = 'Maybe';
+$labels['itipdeclined'] = 'Decline';
+$labels['itipdelegated'] = 'Delegate';
+$labels['itipneeds-action'] = 'Postpone';
+$labels['itipcomment'] = 'Your response';
+$labels['itipeditresponse'] = 'Enter a response text';
+$labels['itipsendercomment'] = 'Sender\'s comment: ';
+$labels['itipsuppressreply'] = 'Do not send a response';
+
+$labels['itipobjectnotfound'] = 'The object referred by this message was not found in your account.';
+$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
+$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
+$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
+$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
+$labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
+$labels['itipsubjectdelegated'] = '"$title" has been delegated by $name';
+$labels['itipsubjectdelegatedto'] = '"$title" has been delegated to you by $name';
+
+$labels['itipnewattendee'] = 'This is a reply from a new participant';
+$labels['updateattendeestatus'] = 'Update the participant\'s status';
+$labels['acceptinvitation'] = 'Do you accept this invitation?';
+$labels['acceptattendee'] = 'Accept participant';
+$labels['declineattendee'] = 'Decline participant';
+$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
+$labels['rsvpmodeall'] = 'The entire series';
+$labels['rsvpmodecurrent'] = 'This occurrence only';
+$labels['rsvpmodefuture'] = 'This and future occurrences';
+
+$labels['itipsingleoccurrence'] = 'This is a <em>single occurrence</em> out of a series of events';
+$labels['itipfutureoccurrence'] = 'Refers to <em>this and all future occurrences</em> of a series of events';
+$labels['itipmessagesingleoccurrence'] = 'The message only refers to this single occurrence';
+$labels['itipmessagefutureoccurrence'] = 'The message refers to this and all future occurrences';
+
+$labels['youhaveaccepted'] = 'You have accepted this invitation';
+$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
+$labels['youhavedeclined'] = 'You have declined this invitation';
+$labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhavein-process'] = 'You are working on this assignment';
+$labels['youhavecompleted'] = 'You have completed this assignment';
+$labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
+
+$labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
+$labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
+$labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
+$labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
+$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
+$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
+$labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
+
+$labels['attendeeaccepted'] = 'Participant has accepted';
+$labels['attendeetentative'] = 'Participant has tentatively accepted';
+$labels['attendeedeclined'] = 'Participant has declined';
+$labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Participant is in-process';
+$labels['attendeecompleted'] = 'Participant has completed';
+$labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
+$labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
+
+$labels['importtocalendar'] = 'Save to my calendar';
+$labels['removefromcalendar'] = 'Remove from my calendar';
+$labels['updatemycopy'] = 'Update my copy';
+$labels['openpreview'] = 'Open Preview';
+
+$labels['deleteobjectconfirm'] = 'Do you really want to delete this object?';
+$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined object from your account?';
+
+$labels['delegateinvitation'] = 'Delegate Invitation';
+$labels['delegateto'] = 'Delegate to';
+$labels['delegatersvpme'] = 'Keep me informed about updates of this incidence';
+$labels['delegateinvalidaddress'] = 'Please enter a valid email address for the delegate';
+
+$labels['savingdata'] = 'Saving data...';
+
+// attendees labels
+$labels['expandattendeegroup'] = 'Substitute with group members';
+$labels['expandattendeegroupnodata'] = 'Unable to substitute this group. No members found.';
+$labels['expandattendeegrouperror'] = 'Unable to substitute this group. It might contain too many members.';
+$labels['expandattendeegroupsizelimit'] = 'This group contains too many members for substituting.';
+
diff --git a/libcalendaring/localization/es_AR.inc b/libcalendaring/localization/es_AR.inc
new file mode 100644
index 0000000..d50f508
--- /dev/null
+++ b/libcalendaring/localization/es_AR.inc
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'hasta';
+$labels['at'] = 'en';
+$labels['alarmemail'] = 'Enviar Correo Electrónico';
+$labels['alarmdisplay'] = 'Mostrar mensaje';
+$labels['alarmaudio'] = 'Reproducir sonido';
+$labels['alarmdisplayoption'] = 'Mensaje';
+$labels['alarmemailoption'] = 'Correo Electrónico';
+$labels['alarmaudiooption'] = 'Sonido';
+$labels['alarmat'] = 'en $datetime';
+$labels['trigger@'] = 'en fecha';
+$labels['trigger-M'] = 'minutos antes';
+$labels['trigger-H'] = 'horas antes';
+$labels['trigger-D'] = 'días antes';
+$labels['trigger+M'] = 'minutos después';
+$labels['trigger+H'] = 'horas después';
+$labels['trigger+D'] = 'días después';
+$labels['triggerattime'] = 'en hora';
+$labels['addalarm'] = 'Agregar alarma';
+$labels['removealarm'] = 'Eliminar alarma';
+$labels['alarmtitle'] = 'Eventos próximos';
+$labels['dismissall'] = 'Descartar todo';
+$labels['dismiss'] = 'Descartar';
+$labels['snooze'] = 'Dormitar';
+$labels['repeatinmin'] = 'Repetir en $min minutos';
+$labels['repeatinhr'] = 'Repetir en 1 hora';
+$labels['repeatinhrs'] = 'Repetir en $hrs horas';
+$labels['repeattomorrow'] = 'Repetir mañana';
+$labels['repeatinweek'] = 'Repetir en una semana';
+$labels['showmore'] = 'Mostrar más...';
+$labels['frequency'] = 'Repetir';
+$labels['never'] = 'nunca';
+$labels['daily'] = 'diariamente';
+$labels['weekly'] = 'semanalmente';
+$labels['monthly'] = 'mensualmente';
+$labels['yearly'] = 'anualmente';
+$labels['rdate'] = 'en fechas';
+$labels['every'] = 'Cada';
+$labels['days'] = 'día(s)';
+$labels['weeks'] = 'semana(s)';
+$labels['months'] = 'mes(es)';
+$labels['years'] = 'año(s)';
+$labels['bydays'] = 'En';
+$labels['untildate'] = 'el';
+$labels['each'] = 'Cada';
+$labels['onevery'] = 'En cada';
+$labels['onsamedate'] = 'En la misma fecha';
+$labels['forever'] = 'para siempre';
+$labels['recurrencend'] = 'hasta';
+$labels['untilenddate'] = 'hasta la fecha';
+$labels['forntimes'] = 'por $nr veces';
+$labels['first'] = 'primero';
+$labels['second'] = 'segundo';
+$labels['third'] = 'tercero';
+$labels['fourth'] = 'cuarto';
+$labels['last'] = 'último';
+$labels['dayofmonth'] = 'Día del mes';
+$labels['addrdate'] = 'Agregar fecha de repetición';
+$labels['except'] = 'excepto';
+$labels['itipinvitation'] = 'Invitación a';
+$labels['itipupdate'] = 'Actualizar de';
+$labels['itipcancellation'] = 'Cancelado:';
+$labels['itipreply'] = 'Responder a';
+$labels['itipaccepted'] = 'Aceptar';
+$labels['itiptentative'] = 'Quizá';
+$labels['itipdeclined'] = 'Rechazar';
+$labels['itipdelegated'] = 'Delegado';
+$labels['itipneeds-action'] = 'Posponer';
+$labels['itipcomment'] = 'Su respuesta';
+$labels['itipeditresponse'] = 'Ingresar un texto de respuesta';
+$labels['itipsendercomment'] = 'Comentario del remitente:';
+$labels['itipsuppressreply'] = 'No enviar una respuesta';
+$labels['itipobjectnotfound'] = 'El objeto referido por este mensaje no fue encontrado en su cuenta.';
+$labels['itipsubjectaccepted'] = '"$title" ha sido aceptado por $name';
+$labels['itipsubjecttentative'] = '"$title" ha sido aceptado tentativamente por $name';
+$labels['itipsubjectdeclined'] = '"$title" ha sido rechazado por $name';
+$labels['itipsubjectin-process'] = '"$title" está en proceso por $name';
+$labels['itipsubjectcompleted'] = '"$title" fue completado por $name';
+$labels['itipsubjectcancel'] = 'Su participación en "$title" ha sido cancelada';
+$labels['itipnewattendee'] = 'Esta es una respuesta de un nuevo participante';
+$labels['updateattendeestatus'] = 'Actualizar el estado del participante';
+$labels['acceptinvitation'] = '¿Acepta esta invitación?';
+$labels['acceptattendee'] = 'Aceptar participante';
+$labels['declineattendee'] = 'Rechazar participante';
+$labels['declineattendeeconfirm'] = 'Ingresar un mensaje para el participante rechazado (opcional):';
+$labels['youhaveaccepted'] = 'Ha aceptado esta invitación';
+$labels['youhavetentative'] = 'Ha aceptado tentativamente esta invitación';
+$labels['youhavedeclined'] = 'Ha rechazado esta invitación';
+$labels['youhavedelegated'] = 'Ha delegado esta invitación';
+$labels['youhavein-process'] = 'Usted está trabajando en esta asignación';
+$labels['youhavecompleted'] = 'Ha completado esta asignación';
+$labels['youhaveneeds-action'] = 'Su respuesta a esta invitación está pendiente';
+$labels['youhavepreviouslyaccepted'] = 'Ha aceptado previamente esta invitación';
+$labels['youhavepreviouslytentative'] = 'Ha aceptado previamente esta invitacion tentativamente';
+$labels['youhavepreviouslydeclined'] = 'Ha rechazado previamente esta invitación';
+$labels['youhavepreviouslydelegated'] = 'Ha delegado previamente esta invitación';
+$labels['youhavepreviouslyin-process'] = 'Ha reportado previamente que trabaja en esta asignación';
+$labels['youhavepreviouslycompleted'] = 'Ha completado previamente esta asignación';
+$labels['youhavepreviouslyneeds-action'] = 'Su respuesta a esta invitación está pendiente';
+$labels['attendeeaccepted'] = 'El participante ha aceptado';
+$labels['attendeetentative'] = 'El participante ha aceptado tentativamente';
+$labels['attendeedeclined'] = 'El participante ha rechazado';
+$labels['attendeedelegated'] = 'El participante ha delegado a $delegatedto';
+$labels['attendeein-process'] = 'El participante está en proceso';
+$labels['attendeecompleted'] = 'El participante ha completado';
+$labels['notanattendee'] = 'No esta incluído en la lista de invitados a este objeto';
+$labels['outdatedinvitation'] = 'Esta invitación ha sido reemplazada por una nueva versión';
+$labels['importtocalendar'] = 'Guardar en mi calendario';
+$labels['removefromcalendar'] = 'Eliminar de mi calendario';
+$labels['updatemycopy'] = 'Actualizar mi copia';
+$labels['openpreview'] = 'Abrir Vista Preliminar';
+$labels['deleteobjectconfirm'] = 'Confirme que desea eliminar este objeto';
+$labels['declinedeleteconfirm'] = '¿Quiere también eliminar este objeto rechazado de su cuenta?';
+$labels['savingdata'] = 'Guardando...';
+$labels['expandattendeegroup'] = 'Sustituir con miembros del grupo';
+$labels['expandattendeegroupnodata'] = 'No se puede sustituir este grupo. No se encontraron miembros.';
+$labels['expandattendeegrouperror'] = 'No se puede sustituir este grupo. Puede contener demasiados miembros.';
+$labels['expandattendeegroupsizelimit'] = 'Este grupo contiene demasiados miembros para sustituir.';
diff --git a/libcalendaring/localization/es_ES.inc b/libcalendaring/localization/es_ES.inc
new file mode 100644
index 0000000..32b089a
--- /dev/null
+++ b/libcalendaring/localization/es_ES.inc
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['at'] = 'at';
+$labels['alarmemail'] = 'Send Email';
+$labels['alarmdisplay'] = 'Show message';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'Correo electrónico';
+$labels['alarmat'] = 'at $datetime';
+$labels['trigger@'] = 'on date';
+$labels['trigger-M'] = 'minutes before';
+$labels['trigger-H'] = 'hours before';
+$labels['trigger-D'] = 'days before';
+$labels['trigger+M'] = 'minutes after';
+$labels['trigger+H'] = 'hours after';
+$labels['trigger+D'] = 'days after';
+$labels['alarmtitle'] = 'Upcoming events';
+$labels['dismissall'] = 'Dismiss all';
+$labels['dismiss'] = 'Dismiss';
+$labels['snooze'] = 'Snooze';
+$labels['repeatinmin'] = 'Repeat in $min minutes';
+$labels['repeatinhr'] = 'Repeat in 1 hour';
+$labels['repeatinhrs'] = 'Repeat in $hrs hours';
+$labels['repeattomorrow'] = 'Repeat tomorrow';
+$labels['repeatinweek'] = 'Repeat in a week';
+$labels['showmore'] = 'Show more...';
+$labels['frequency'] = 'Repeat';
+$labels['never'] = 'never';
+$labels['daily'] = 'daily';
+$labels['weekly'] = 'weekly';
+$labels['monthly'] = 'monthly';
+$labels['yearly'] = 'annually';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Every';
+$labels['days'] = 'day(s)';
+$labels['weeks'] = 'week(s)';
+$labels['months'] = 'month(s)';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'the';
+$labels['each'] = 'Each';
+$labels['onevery'] = 'On every';
+$labels['onsamedate'] = 'On the same date';
+$labels['forever'] = 'forever';
+$labels['recurrencend'] = 'until';
+$labels['forntimes'] = 'for $nr time(s)';
+$labels['first'] = 'first';
+$labels['second'] = 'second';
+$labels['third'] = 'third';
+$labels['fourth'] = 'fourth';
+$labels['last'] = 'last';
+$labels['dayofmonth'] = 'Day of month';
+$labels['addrdate'] = 'Add repeat date';
+$labels['savingdata'] = 'Guardando datos...';
diff --git a/libcalendaring/localization/et_EE.inc b/libcalendaring/localization/et_EE.inc
new file mode 100644
index 0000000..291c160
--- /dev/null
+++ b/libcalendaring/localization/et_EE.inc
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['at'] = 'at';
+$labels['alarmemail'] = 'Send Email';
+$labels['alarmdisplay'] = 'Show message';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'E-post';
+$labels['alarmat'] = 'at $datetime';
+$labels['trigger@'] = 'on date';
+$labels['trigger-M'] = 'minutes before';
+$labels['trigger-H'] = 'hours before';
+$labels['trigger-D'] = 'days before';
+$labels['trigger+M'] = 'minutes after';
+$labels['trigger+H'] = 'hours after';
+$labels['trigger+D'] = 'days after';
+$labels['alarmtitle'] = 'Upcoming events';
+$labels['dismissall'] = 'Dismiss all';
+$labels['dismiss'] = 'Dismiss';
+$labels['snooze'] = 'Snooze';
+$labels['repeatinmin'] = 'Repeat in $min minutes';
+$labels['repeatinhr'] = 'Repeat in 1 hour';
+$labels['repeatinhrs'] = 'Repeat in $hrs hours';
+$labels['repeattomorrow'] = 'Repeat tomorrow';
+$labels['repeatinweek'] = 'Repeat in a week';
+$labels['showmore'] = 'Show more...';
+$labels['frequency'] = 'Repeat';
+$labels['never'] = 'never';
+$labels['daily'] = 'daily';
+$labels['weekly'] = 'weekly';
+$labels['monthly'] = 'monthly';
+$labels['yearly'] = 'annually';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Every';
+$labels['days'] = 'day(s)';
+$labels['weeks'] = 'week(s)';
+$labels['months'] = 'month(s)';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'the';
+$labels['each'] = 'Each';
+$labels['onevery'] = 'On every';
+$labels['onsamedate'] = 'On the same date';
+$labels['forever'] = 'forever';
+$labels['recurrencend'] = 'until';
+$labels['forntimes'] = 'for $nr time(s)';
+$labels['first'] = 'first';
+$labels['second'] = 'second';
+$labels['third'] = 'third';
+$labels['fourth'] = 'fourth';
+$labels['last'] = 'last';
+$labels['dayofmonth'] = 'Day of month';
+$labels['addrdate'] = 'Add repeat date';
+$labels['itipdelegated'] = 'Delegate';
+$labels['savingdata'] = 'Saving data...';
diff --git a/libcalendaring/localization/fi_FI.inc b/libcalendaring/localization/fi_FI.inc
new file mode 100644
index 0000000..5df06c1
--- /dev/null
+++ b/libcalendaring/localization/fi_FI.inc
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'kunnes';
+$labels['alarmemail'] = 'Lähetä sähköposti';
+$labels['alarmdisplay'] = 'Näytä viesti';
+$labels['alarmaudio'] = 'Toista ääni';
+$labels['alarmdisplayoption'] = 'Viesti';
+$labels['alarmemailoption'] = 'Sähköposti';
+$labels['alarmaudiooption'] = 'Ääni';
+$labels['alarmat'] = '$datetime';
+$labels['trigger@'] = 'päivänä';
+$labels['trigger-M'] = 'minuuttia ennen';
+$labels['trigger-H'] = 'tuntia ennen';
+$labels['trigger-D'] = 'päivää ennen';
+$labels['trigger+M'] = 'minuuttia jälkeen';
+$labels['trigger+H'] = 'tuntia jälkeen';
+$labels['trigger+D'] = 'päivää jälkeen';
+$labels['triggerattime'] = 'kellonaikana';
+$labels['addalarm'] = 'Lisää heräte';
+$labels['removealarm'] = 'Poista heräte';
+$labels['alarmtitle'] = 'Tulevat tapahtumat';
+$labels['dismissall'] = 'Hylkää kaikki';
+$labels['dismiss'] = 'Hylkää';
+$labels['snooze'] = 'Torkuta';
+$labels['repeatinmin'] = 'Toista $min minuutin päästä';
+$labels['repeatinhr'] = 'Toista 1 tunnin päästä';
+$labels['repeatinhrs'] = 'Toista $hrs tunnin päästä';
+$labels['repeattomorrow'] = 'Toista huomenna';
+$labels['repeatinweek'] = 'Toista viikon päästä';
+$labels['showmore'] = 'Näytä lisää...';
+$labels['recurring'] = 'Toistuu';
+$labels['frequency'] = 'Toista';
+$labels['never'] = 'ei koskaan';
+$labels['daily'] = 'päivittäin';
+$labels['weekly'] = 'viikottain';
+$labels['monthly'] = 'kuukausittain';
+$labels['yearly'] = 'vuosittain';
+$labels['rdate'] = 'päivinä';
+$labels['every'] = 'Joka';
+$labels['days'] = 'päivä(ä)';
+$labels['weeks'] = 'viikko(a)';
+$labels['months'] = 'kuukausi(-tta)';
+$labels['years'] = 'vuosi(-tta)';
+$labels['each'] = 'Joka';
+$labels['onevery'] = 'Jokaisena';
+$labels['onsamedate'] = 'Samana päivänä';
+$labels['forever'] = 'ikuisesti';
+$labels['recurrencend'] = 'kunnes';
+$labels['untilenddate'] = 'pvm saakka';
+$labels['forntimes'] = '$nr kerta(a)';
+$labels['first'] = 'ensimmäinen';
+$labels['second'] = 'toinen';
+$labels['third'] = 'kolmas';
+$labels['fourth'] = 'neljäs';
+$labels['last'] = 'viimeinen';
+$labels['dayofmonth'] = 'Kuukaudenpäivä';
+$labels['addrdate'] = 'Lisää toiston pvm';
+$labels['except'] = 'paitsi';
+$labels['itipinvitation'] = 'Kutsu';
+$labels['itipupdate'] = 'Päivitys';
+$labels['itipcancellation'] = 'Peruttu:';
+$labels['itipreply'] = 'Vastaus';
+$labels['itipaccepted'] = 'Hyväksy';
+$labels['itiptentative'] = 'Ehkä';
+$labels['itipdeclined'] = 'Kieltäydy';
+$labels['itipdelegated'] = 'Edustaja';
+$labels['itipneeds-action'] = 'Viivästetty';
+$labels['itipcomment'] = 'Oma vastaus';
+$labels['itipeditresponse'] = 'Kirjoita vastausteksti';
+$labels['itipsendercomment'] = 'Lähettäjän kommentti:';
+$labels['itipsuppressreply'] = 'Älä lähetä vastausta';
+$labels['itipobjectnotfound'] = 'Tässä viestissä viittattua kohdetta ei löydy tunnuksestasi';
+$labels['itipsubjectaccepted'] = '$name on hyväksynyt tapahtuman "$title"';
+$labels['itipsubjecttentative'] = '$name on alustavasti hyväkstynyt tapahtuman "$title"';
+$labels['itipsubjectdeclined'] = '$name on kieltäytynyt osallistumasta tapahtumaan "$title"';
+$labels['itipsubjectin-process'] = '"$title" on prosessissa $name kautta';
+$labels['itipsubjectcompleted'] = '$title on valmis $name kautta';
+$labels['itipsubjectcancel'] = 'Osallistumisesi "$title" on peruttu';
+$labels['itipsubjectdelegated'] = '"$title" on delegoitu $name kautta';
+$labels['itipsubjectdelegatedto'] = '"$title" on delegoitu sinulle $name kautta';
+$labels['itipnewattendee'] = 'Tämä on vastaus uudelta osallistujalta';
+$labels['updateattendeestatus'] = 'Päivitä osallistujien status';
+$labels['acceptinvitation'] = 'Hyväksytkö tämän kutsun?';
+$labels['acceptattendee'] = 'Hyväksy osallistuja';
+$labels['declineattendee'] = 'Estä osallistuja';
+$labels['declineattendeeconfirm'] = 'Anna viesti estetylle osallistujalle (vaihtoehtoinen):';
+$labels['rsvpmodecurrent'] = 'Vain tämä esiintymä';
+$labels['rsvpmodefuture'] = 'Tämä ja tulevat esiintymät';
+$labels['itipmessagesingleoccurrence'] = 'Tämä viesti viittaa vain yhteen esiintymään';
+$labels['itipmessagefutureoccurrence'] = 'Tämä viesti viittaa tähän ja kaikkiin tuleviin esiintymiin';
+$labels['youhaveaccepted'] = 'Olet hyväksynyt tämän kutsun';
+$labels['youhavetentative'] = 'Olet hyväksynyt tämän kutsun alustavasti';
+$labels['youhavedeclined'] = 'Olet kieltäytynyt tästä kutsusta';
+$labels['youhavedelegated'] = 'Olet delegoitu tähän kutsuun';
+$labels['youhavein-process'] = 'Työskentelet tämän tehtävän parissa';
+$labels['youhavecompleted'] = 'Olet suorittanut tämän tehtävän';
+$labels['youhaveneeds-action'] = 'Vastauksesi tähän kutsuun on yhä jonossa';
+$labels['youhavepreviouslyaccepted'] = 'Olet aiemmin hyväksynyt tämän kutsun';
+$labels['youhavepreviouslytentative'] = 'Olet aiemmin hyväksynyt tämän kutsun varauksella';
+$labels['youhavepreviouslydeclined'] = 'Sinut on aiemmin estetty tästä kutsusta';
+$labels['youhavepreviouslydelegated'] = 'Sinut on aiemmin delegoitu tähän kutsuun';
+$labels['youhavepreviouslyin-process'] = 'Sinut on aiemmin raportoitu työskentelevän tämän tehtävän parissa';
+$labels['youhavepreviouslycompleted'] = 'Olet aiemmin suorittanut tämän tehtävän';
+$labels['youhavepreviouslyneeds-action'] = 'Vastauksesi tähän kutsuun on yhä jonossa';
+$labels['attendeeaccepted'] = 'Osallistuja on hyväksynyt';
+$labels['attendeetentative'] = 'Osallistuja on hyväksynyt varauksella';
+$labels['attendeedeclined'] = 'Osallistuja on hylännyt';
+$labels['attendeedelegated'] = 'Osallistuja on delegoitu $delegatedto';
+$labels['attendeein-process'] = 'Osallistuja on prosessissa';
+$labels['attendeecompleted'] = 'Osallistuja on suorittanut';
+$labels['notanattendee'] = 'Sinua ei ole listattu osallistujaksi tähän kohteeseen';
+$labels['outdatedinvitation'] = 'Kutsu on korvattu uudemmalla versiolla';
+$labels['importtocalendar'] = 'Tallenna omaan kalenteriin';
+$labels['removefromcalendar'] = 'Poista omasta kalenterista';
+$labels['updatemycopy'] = 'Päivitä kopioni';
+$labels['openpreview'] = 'Avaa esikatselu';
+$labels['deleteobjectconfirm'] = 'Haluatko todella poistaa tämän kohteen?';
+$labels['declinedeleteconfirm'] = 'Haluatko poistaa tämän hylätyn kohteen tunnukseltasi?';
+$labels['delegateinvitation'] = 'Delegoi kutsu';
+$labels['delegateto'] = 'Delegoi henkilölle';
+$labels['delegatersvpme'] = 'Pidä minut ajan tasalla tämän tapauksen päivityksistä';
+$labels['delegateinvalidaddress'] = 'Anna valiidi sähköpostiosoite delegoiaksesi';
+$labels['savingdata'] = 'Tallennetaan tietoja...';
+$labels['expandattendeegroup'] = 'Korvaa ryhmän jäsenillä';
+$labels['expandattendeegroupnodata'] = 'Korvaaminen ryhmän jäsenillä ei onnistu. Yhtään jäsentä ei löydy.';
+$labels['expandattendeegrouperror'] = 'Korvaaminen ryhmän jäsenillä ei onnistu. Tämä saattaa sisältää liian monta jäsentä.';
+$labels['expandattendeegroupsizelimit'] = 'Tämä ryhmä sisältää liian monta jäsentä korvaamiseen';
diff --git a/libcalendaring/localization/fr_FR.inc b/libcalendaring/localization/fr_FR.inc
new file mode 100644
index 0000000..7fd3bca
--- /dev/null
+++ b/libcalendaring/localization/fr_FR.inc
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'Jusqu\'à';
+$labels['at'] = 'à';
+$labels['alarmemail'] = 'Envoyer un email';
+$labels['alarmdisplay'] = 'Voir le message';
+$labels['alarmaudio'] = 'Alerte sonore';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmaudiooption'] = 'Son';
+$labels['alarmat'] = 'à $datetime';
+$labels['trigger@'] = 'à la date';
+$labels['trigger-M'] = 'minutes avant';
+$labels['trigger-H'] = 'heures avant';
+$labels['trigger-D'] = 'jours avant';
+$labels['trigger+M'] = 'minutes après';
+$labels['trigger+H'] = 'heures après';
+$labels['trigger+D'] = 'jours après';
+$labels['triggerattime'] = 'à l\'heure';
+$labels['addalarm'] = 'Ajouter une alarme';
+$labels['removealarm'] = 'Supprimer l\'alarme';
+$labels['alarmtitle'] = 'Evénements à venir';
+$labels['dismissall'] = 'Tout masquer';
+$labels['dismiss'] = 'Masquer';
+$labels['snooze'] = 'En pause';
+$labels['repeatinmin'] = 'Répéter dans $min minutes';
+$labels['repeatinhr'] = 'Répéter dans 1 heure';
+$labels['repeatinhrs'] = 'Répéter dans $hrs heures';
+$labels['repeattomorrow'] = 'Répéter demain';
+$labels['repeatinweek'] = 'Répéter dans une semaine';
+$labels['showmore'] = 'Afficher plus...';
+$labels['frequency'] = 'Répéter';
+$labels['never'] = 'Jamais';
+$labels['daily'] = 'Quotidienne';
+$labels['weekly'] = 'Hebdomadaire';
+$labels['monthly'] = 'Mensuelle';
+$labels['yearly'] = 'Annuelle';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Tous les';
+$labels['days'] = 'jour(s)';
+$labels['weeks'] = 'semaine(s)';
+$labels['months'] = 'mois';
+$labels['years'] = 'année(s) en :';
+$labels['bydays'] = 'Le';
+$labels['untildate'] = 'le';
+$labels['each'] = 'Chaque';
+$labels['onevery'] = 'Tous les';
+$labels['onsamedate'] = 'à la même date';
+$labels['forever'] = 'toujours';
+$labels['recurrencend'] = 'Jusqu\'à';
+$labels['untilenddate'] = 'jusqu\'à la date';
+$labels['forntimes'] = '$nr fois';
+$labels['first'] = 'premier';
+$labels['second'] = 'deuxième';
+$labels['third'] = 'troisième';
+$labels['fourth'] = 'quatrième';
+$labels['last'] = 'dernier';
+$labels['dayofmonth'] = 'Jour du mois';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'sauf';
+$labels['itipinvitation'] = 'Invitation à';
+$labels['itipupdate'] = 'Mise à jour de';
+$labels['itipcancellation'] = 'Annulation:';
+$labels['itipreply'] = 'Répondre à';
+$labels['itipaccepted'] = 'Accepter';
+$labels['itiptentative'] = 'Peut-être';
+$labels['itipdeclined'] = 'Refuser';
+$labels['itipdelegated'] = 'Déléguer';
+$labels['itipneeds-action'] = 'Reporter';
+$labels['itipcomment'] = 'Votre réponse';
+$labels['itipeditresponse'] = 'Saisissez votre réponse';
+$labels['itipsendercomment'] = 'Commentaire des expéditeurs';
+$labels['itipsuppressreply'] = 'Ne pas envoyer de réponse';
+$labels['itipobjectnotfound'] = 'L\'objet en lié à ce message n\'a pas été trouvé sur votre compte.';
+$labels['itipsubjectaccepted'] = '"$title" a été accepté par $name';
+$labels['itipsubjecttentative'] = '"$title" a été accepté provisoirement par $name';
+$labels['itipsubjectdeclined'] = '"$title" a été refusé par $name';
+$labels['itipsubjectin-process'] = '"$title" est en cours de traitement par $name';
+$labels['itipsubjectcompleted'] = '"$title" a été traité par $name';
+$labels['itipsubjectcancel'] = 'Votre affectation à "$title" a été annulée';
+$labels['itipsubjectdelegated'] = '"$title$ a été délégué par $name';
+$labels['itipsubjectdelegatedto'] = '"$title" vous a été délégué par $name';
+$labels['itipnewattendee'] = 'Ceci est une réponse d\'un nouveau participant';
+$labels['updateattendeestatus'] = 'Modifier le statut des participants';
+$labels['acceptinvitation'] = 'Acceptez-vous cette invitation?';
+$labels['acceptattendee'] = 'Accepter l\'utilisateur';
+$labels['declineattendee'] = 'Décliner l\'utilisateur';
+$labels['declineattendeeconfirm'] = 'Saisissez un message pour l\'utilisateur qui décline l\'invitation (optionnel) :';
+$labels['youhaveaccepted'] = 'Vous avez accepté cette invitation';
+$labels['youhavetentative'] = 'Vous avez accepté provisoirement cette invitation';
+$labels['youhavedeclined'] = 'Vous avez refusé cette invitation';
+$labels['youhavedelegated'] = 'Vous avez délégué cette invitation';
+$labels['youhavein-process'] = 'Vous traiter cette affectation';
+$labels['youhavecompleted'] = 'Vous avez traité cette affectation';
+$labels['youhaveneeds-action'] = 'Vous êtes en attente d\'une réponse à cette invitation';
+$labels['youhavepreviouslyaccepted'] = 'Vous avez déjà accepté cette invitation';
+$labels['youhavepreviouslytentative'] = 'Vous avez déjà tenté d\'accepter cette invitation';
+$labels['youhavepreviouslydeclined'] = 'Vous avez déjà décliné cette invitation';
+$labels['youhavepreviouslydelegated'] = 'Vous avez déjà délégué cette invitation';
+$labels['youhavepreviouslyin-process'] = 'Vous avez déjà signalé que vous traitez cette affectation';
+$labels['youhavepreviouslycompleted'] = 'Vous avez déjà fini le traitement de cette affectation';
+$labels['youhavepreviouslyneeds-action'] = 'Vous êtes en attente de la réponse pour cette invitation';
+$labels['attendeeaccepted'] = 'Cet utilisateur a accepté l\'invitaion';
+$labels['attendeetentative'] = 'Cette utilisateur tente d\'accepter l\'invitation';
+$labels['attendeedeclined'] = 'Cet utilisateur a décliné l\'invitation';
+$labels['attendeedelegated'] = 'Cet utilisateur a délégué à $delegatedto';
+$labels['attendeein-process'] = 'Cet utilisateur traite la tâche';
+$labels['attendeecompleted'] = 'Cet utilisateur a fini la tâche';
+$labels['notanattendee'] = 'Vous n\'êtes pas dans la liste des participants à cet évènement';
+$labels['outdatedinvitation'] = 'Cet invitation a été remplacée par une nouvelle version';
+$labels['importtocalendar'] = 'Enregistrer dans mon agenda';
+$labels['removefromcalendar'] = 'Supprimer de mon agenda';
+$labels['updatemycopy'] = 'Mise à jour de ma copie';
+$labels['openpreview'] = 'Ouvrir la prévisualisation';
+$labels['deleteobjectconfirm'] = 'Voulez-vous vraiment supprimer cet objet ?';
+$labels['declinedeleteconfirm'] = 'Voulez-vous supprimer cet objet décliné de votre compte ?';
+$labels['delegateinvitation'] = 'Invitation à déléguer';
+$labels['delegateto'] = 'Déléguer à ';
+$labels['delegatersvpme'] = 'Me tenir informé des mises à jours de cet effet';
+$labels['delegateinvalidaddress'] = 'Entrer une adresse électronique valide pour le délégué';
+$labels['savingdata'] = 'Enregistrer...';
+$labels['expandattendeegroup'] = 'Remplacement par les membres du groupe';
+$labels['expandattendeegroupnodata'] = 'Impossible de substituer ce groupe, il ne contient aucun membre.';
+$labels['expandattendeegrouperror'] = 'Impossible de substituer ce groupe, il contient trop de membres.';
+$labels['expandattendeegroupsizelimit'] = 'Ce groupe contient trop de membres pour être substitué.';
diff --git a/libcalendaring/localization/hu_HU.inc b/libcalendaring/localization/hu_HU.inc
new file mode 100644
index 0000000..9a01d5e
--- /dev/null
+++ b/libcalendaring/localization/hu_HU.inc
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'Eddig';
+$labels['at'] = 'időpont';
+$labels['alarmemail'] = 'E-mail küldése';
+$labels['alarmdisplay'] = 'Üzenet megjelenítése';
+$labels['alarmdisplayoption'] = 'Üzenet';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmat'] = 'ekkor $datetime';
+$labels['trigger@'] = 'adott időpontban';
+$labels['trigger-M'] = 'perccel előtte';
+$labels['trigger-H'] = 'órával előtte';
+$labels['trigger-D'] = 'nappal előtte';
+$labels['trigger+M'] = 'perccel utána';
+$labels['trigger+H'] = 'órával utána';
+$labels['trigger+D'] = 'nappal utána';
+$labels['alarmtitle'] = 'Következő események';
+$labels['dismissall'] = 'Az összes emlékeztető mellőzése';
+$labels['dismiss'] = 'Nem emlékettet újra';
+$labels['snooze'] = 'Újra emlékeztet';
+$labels['repeatinmin'] = 'Emlékeztessen $min perc múlva';
+$labels['repeatinhr'] = 'Emlékeztessen újra 1 óra múlva';
+$labels['repeatinhrs'] = 'Emlékeztessen újra $hrs óra múlva';
+$labels['repeattomorrow'] = 'Emlékeztessen újra holnap';
+$labels['repeatinweek'] = 'Emlékeztessen újra egy hét múlva';
+$labels['showmore'] = 'Tovább...';
+$labels['frequency'] = 'Ismétlődik';
+$labels['never'] = 'soha';
+$labels['daily'] = 'naponta';
+$labels['weekly'] = 'hetente';
+$labels['monthly'] = 'havonta';
+$labels['yearly'] = 'évente';
+$labels['rdate'] = 'adott időpontokban';
+$labels['every'] = 'Minden';
+$labels['days'] = 'napon';
+$labels['weeks'] = 'héten';
+$labels['months'] = 'hónapban';
+$labels['years'] = 'évben ekkor:';
+$labels['bydays'] = 'napon';
+$labels['untildate'] = 'dátumig';
+$labels['each'] = 'napon';
+$labels['onevery'] = 'minden';
+$labels['onsamedate'] = 'Ugyanazon a dátumon';
+$labels['forever'] = 'örökké';
+$labels['recurrencend'] = 'Eddig';
+$labels['forntimes'] = '$nr alkalommal';
+$labels['first'] = 'első';
+$labels['second'] = 'második';
+$labels['third'] = 'harmadik';
+$labels['fourth'] = 'negyedik';
+$labels['last'] = 'utolsó';
+$labels['dayofmonth'] = 'hónap napja';
+$labels['addrdate'] = 'Ismétlési dátum hozzáadása';
+$labels['itipinvitation'] = 'Új esemény:';
+$labels['itipupdate'] = 'Módosítva:';
+$labels['itipcancellation'] = 'Lemondva:';
+$labels['itipreply'] = 'Válasz';
+$labels['itipaccepted'] = 'Elfogadás';
+$labels['itiptentative'] = 'Feltételes';
+$labels['itipdeclined'] = 'Elutasítás';
+$labels['itipdelegated'] = 'Meghatalmazott:';
+$labels['itipcomment'] = 'Megjegyzés';
+$labels['itipeditresponse'] = 'Kérem, adjon meg egy választ';
+$labels['itipsendercomment'] = 'A feladó megjegyzése: ';
+$labels['itipobjectnotfound'] = 'A hivatkozott elem nem található az Ön fiókjában.';
+$labels['itipsubjectaccepted'] = '$title - elfogadva';
+$labels['itipsubjecttentative'] = '$title - feltételesen elfogadva';
+$labels['itipsubjectdeclined'] = '$title - elutasítva';
+$labels['itipsubjectcancel'] = '$title - lemondva';
+$labels['itipnewattendee'] = 'Válasz új résztvevőtől';
+$labels['updateattendeestatus'] = 'A résztvevő státuszának frissítése';
+$labels['acceptinvitation'] = 'Elfogadja ezt a meghívást?';
+$labels['acceptattendee'] = 'Részvétel elfogadása';
+$labels['declineattendee'] = 'Részvétel elutasítása';
+$labels['declineattendeeconfirm'] = 'Megjegyzés az elutasított részvevőnek (opcionális):';
+$labels['youhaveaccepted'] = 'Ön elfogadta ezt a meghívást';
+$labels['youhavetentative'] = 'Ön feltételesen elfogadta ezt a meghívást';
+$labels['youhavedeclined'] = 'Ön elutasította ezt a meghívást';
+$labels['youhavedelegated'] = 'Ön meghatalmazással továbbadta ezt a meghívást';
+$labels['attendeeaccepted'] = 'A résztvevő elfogadta';
+$labels['attendeetentative'] = 'A résztvevő feltételesen elfogadta';
+$labels['attendeedeclined'] = 'A résztvevő elutasította';
+$labels['attendeedelegated'] = 'A meghívott meghatalmazta részvételre: $delegatedto';
+$labels['notanattendee'] = 'Ön nem szerepel az esemény meghívottai között';
+$labels['importtocalendar'] = 'Mentés naptárba';
+$labels['removefromcalendar'] = 'Törlés a naptárból';
+$labels['updatemycopy'] = 'Naptárbejegyzés frissítése';
+$labels['deleteobjectconfirm'] = 'Biztos benne, hogy törölni szeretné ezt az elemet?';
+$labels['declinedeleteconfirm'] = 'Biztos benne, hogy törölni szeretné ezt az elutasított elemet a fiókjából?';
+$labels['savingdata'] = 'Adatok mentése...';
diff --git a/libcalendaring/localization/it_IT.inc b/libcalendaring/localization/it_IT.inc
new file mode 100644
index 0000000..c7373de
--- /dev/null
+++ b/libcalendaring/localization/it_IT.inc
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'fino al';
+$labels['at'] = 'at';
+$labels['alarmemail'] = 'Spedisci email';
+$labels['alarmdisplay'] = 'Mostra messaggio';
+$labels['alarmdisplayoption'] = 'Messaggio';
+$labels['alarmemailoption'] = 'Email';
+$labels['trigger@'] = 'in data';
+$labels['trigger-M'] = 'miniti prima';
+$labels['trigger-H'] = 'ore prima';
+$labels['trigger-D'] = 'giorni prima';
+$labels['trigger+M'] = 'minuti dopo';
+$labels['trigger+H'] = 'ore dopo';
+$labels['trigger+D'] = 'giorni dopo';
+$labels['alarmtitle'] = 'Prossimi eventi';
+$labels['dismissall'] = 'Scarta tutti';
+$labels['dismiss'] = 'Scarta';
+$labels['snooze'] = 'Sospendi';
+$labels['repeatinmin'] = 'Ripeti tra $min minuti';
+$labels['repeatinhr'] = 'Ripeti tra 1 ora';
+$labels['repeatinhrs'] = 'Ripeti tra $hrs ore';
+$labels['repeattomorrow'] = 'Ripeti domani';
+$labels['repeatinweek'] = 'Ripeti tra una settimana';
+$labels['showmore'] = 'Mostra altro...';
+$labels['frequency'] = 'Frequenza';
+$labels['never'] = 'una volta';
+$labels['daily'] = 'quotidiana';
+$labels['weekly'] = 'settimanale';
+$labels['monthly'] = 'mensile';
+$labels['yearly'] = 'annuale';
+$labels['every'] = 'Ogni';
+$labels['days'] = 'giorno/i';
+$labels['weeks'] = 'settimana/e';
+$labels['months'] = 'mese/i';
+$labels['years'] = 'anno/i in:';
+$labels['bydays'] = 'Di';
+$labels['untildate'] = 'il';
+$labels['each'] = 'Nei giorni';
+$labels['onevery'] = 'Ogni';
+$labels['onsamedate'] = 'Alla stessa data';
+$labels['forever'] = 'per sempre';
+$labels['recurrencend'] = 'fino al';
+$labels['forntimes'] = 'per $nr volte';
+$labels['first'] = 'primo';
+$labels['second'] = 'secondo';
+$labels['third'] = 'terzo';
+$labels['fourth'] = 'quarto';
+$labels['last'] = 'ultimo';
+$labels['dayofmonth'] = 'Giorno del mese';
+$labels['itipinvitation'] = 'Invito a';
+$labels['itipupdate'] = 'Aggiornamento di';
+$labels['itipcancellation'] = 'Annullato:';
+$labels['itipreply'] = 'Rispondi a';
+$labels['itipaccepted'] = 'Accetta';
+$labels['itiptentative'] = 'Forse';
+$labels['itipdeclined'] = 'Rifiuta';
+$labels['itipdelegated'] = 'Delegato';
+$labels['itipobjectnotfound'] = 'L\'oggetto a cui si riferisce questo messaggio non è stato trovato nel tuo account.';
+$labels['itipsubjectaccepted'] = '"$title" è stato accettato da $name';
+$labels['itipsubjecttentative'] = '"$title" è stato accettato con riserva da $name';
+$labels['itipsubjectdeclined'] = '"$title" è stato rifiutato da $name';
+$labels['itipnewattendee'] = 'Questa è una risposta da un nuovo partecipante';
+$labels['updateattendeestatus'] = 'Aggiorna lo stato dei partecipanti';
+$labels['acceptinvitation'] = 'Accetti questo invito?';
+$labels['youhaveaccepted'] = 'Hai accettato questo invito';
+$labels['youhavetentative'] = 'Hai accettato con riserva questo invito';
+$labels['youhavedeclined'] = 'Hai rifiutato questo invito';
+$labels['youhavedelegated'] = 'Hai delegato questo invito';
+$labels['attendeeaccepted'] = 'Il partecipante ha accettato';
+$labels['attendeetentative'] = 'Il partecipante ha accettato con riserva';
+$labels['attendeedeclined'] = 'Il partecipante ha rifiutato';
+$labels['notanattendee'] = 'Non sei nell\'elenco dei partecipanti per questo oggetto';
+$labels['importtocalendar'] = 'Salva nel mio calendario';
+$labels['removefromcalendar'] = 'Rimuovi dal mio calendario';
+$labels['updatemycopy'] = 'Aggiorna la mia copia';
+$labels['deleteobjectconfirm'] = 'Vuoi davvero eliminare questo oggetto?';
+$labels['declinedeleteconfirm'] = 'Vuoi che l\'oggetto rifiutato venga eliminato anche dal tuo account?';
+$labels['savingdata'] = 'Salvataggio dati...';
diff --git a/libcalendaring/localization/ja_JP.inc b/libcalendaring/localization/ja_JP.inc
new file mode 100644
index 0000000..2f22aab
--- /dev/null
+++ b/libcalendaring/localization/ja_JP.inc
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'まで';
+$labels['at'] = 'の';
+$labels['alarmemail'] = 'メール送信';
+$labels['alarmdisplay'] = 'メッセージ表示';
+$labels['alarmdisplayoption'] = 'メッセージ';
+$labels['alarmemailoption'] = 'Eメール';
+$labels['alarmat'] = '$datetime に';
+$labels['trigger@'] = '日付に';
+$labels['trigger-M'] = '分前';
+$labels['trigger-H'] = '時間前';
+$labels['trigger-D'] = '分後';
+$labels['trigger+M'] = '分後';
+$labels['trigger+H'] = '時間後';
+$labels['trigger+D'] = '日後';
+$labels['alarmtitle'] = '今後のイベント';
+$labels['dismissall'] = '全て削除';
+$labels['dismiss'] = '削除';
+$labels['snooze'] = 'スノーズ';
+$labels['repeatinmin'] = '$min 以内で繰返し';
+$labels['repeatinhr'] = '1時間で繰返し';
+$labels['repeatinhrs'] = '$hrs で繰返し';
+$labels['repeattomorrow'] = '明日繰返し';
+$labels['repeatinweek'] = '1週間で繰返し';
+$labels['showmore'] = 'さらに表示…';
+$labels['frequency'] = '繰返し';
+$labels['never'] = '繰返さない';
+$labels['daily'] = '毎日';
+$labels['weekly'] = '毎週';
+$labels['monthly'] = '毎月';
+$labels['yearly'] = '毎年';
+$labels['every'] = 'いつでも';
+$labels['days'] = '日(s)';
+$labels['weeks'] = '週(s)';
+$labels['months'] = '月(s)';
+$labels['years'] = '年(s):';
+$labels['bydays'] = '上';
+$labels['untildate'] = 'その';
+$labels['each'] = 'いずれも';
+$labels['onevery'] = '各';
+$labels['onsamedate'] = 'いつか';
+$labels['forever'] = 'ずっと';
+$labels['recurrencend'] = 'まで';
+$labels['forntimes'] = '$nr まで(s)';
+$labels['first'] = '第1週';
+$labels['second'] = '第2週';
+$labels['third'] = '第3週';
+$labels['fourth'] = '第4週';
+$labels['last'] = '最終週';
+$labels['dayofmonth'] = '日';
+$labels['itipinvitation'] = '招待する';
+$labels['itipupdate'] = '更新';
+$labels['itipcancellation'] = 'キャンセル';
+$labels['itipreply'] = '返信';
+$labels['itipaccepted'] = '承諾';
+$labels['itiptentative'] = 'たぶん';
+$labels['itipdeclined'] = '辞退';
+$labels['itipdelegated'] = '委任者';
+$labels['itipsubjectaccepted'] = '$name が "$title" を承諾しました';
+$labels['itipsubjecttentative'] = '$name が "$title" を仮承諾しました';
+$labels['itipsubjectdeclined'] = '$name が "$title" を辞退しました';
+$labels['updateattendeestatus'] = '参加者の状況更新';
+$labels['acceptinvitation'] = 'この招待を承諾しますか?';
+$labels['youhaveaccepted'] = 'この招待を承諾しました';
+$labels['youhavetentative'] = 'この招待を仮承諾しました。';
+$labels['youhavedeclined'] = 'この招待を辞退しました。';
+$labels['notanattendee'] = 'このイベントの出席者として一覧にありません';
+$labels['importtocalendar'] = 'カレンダーに保存';
+$labels['removefromcalendar'] = 'カレンダーから削除';
+$labels['savingdata'] = 'データを保存中…';
diff --git a/libcalendaring/localization/nl_NL.inc b/libcalendaring/localization/nl_NL.inc
new file mode 100644
index 0000000..73059e2
--- /dev/null
+++ b/libcalendaring/localization/nl_NL.inc
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'tot';
+$labels['at'] = 'om';
+$labels['alarmemail'] = 'Verzend E-mail';
+$labels['alarmdisplay'] = 'Bericht weergeven';
+$labels['alarmdisplayoption'] = 'Bericht';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmat'] = 'op $datetime';
+$labels['trigger@'] = 'op datum';
+$labels['trigger-M'] = 'minuten voor';
+$labels['trigger-H'] = 'uren voor';
+$labels['trigger-D'] = 'dagen voor';
+$labels['trigger+M'] = 'minuten na';
+$labels['trigger+H'] = 'uren na';
+$labels['trigger+D'] = 'dagen na';
+$labels['triggerattime'] = 'at time';
+$labels['alarmtitle'] = 'Aankomende evenementen';
+$labels['repeatinmin'] = 'Herhaal over $min minuten';
+$labels['repeatinhr'] = 'Herhaal over 1 uur';
+$labels['repeatinhrs'] = 'Herhaal over $hrs uur';
+$labels['repeattomorrow'] = 'Herhaal morgen';
+$labels['repeatinweek'] = 'Herhaal over een week';
+$labels['showmore'] = 'Toon meer...';
+$labels['frequency'] = 'Herhaal';
+$labels['never'] = 'nooit';
+$labels['daily'] = 'dagelijks';
+$labels['weekly'] = 'wekelijks';
+$labels['monthly'] = 'maandelijks';
+$labels['yearly'] = 'jaarlijks';
+$labels['rdate'] = 'op datums';
+$labels['every'] = 'Elke';
+$labels['days'] = 'dag(en)';
+$labels['weeks'] = 'week / weken';
+$labels['months'] = 'maand(en)';
+$labels['years'] = 'ja(a)r(en) in:';
+$labels['bydays'] = 'Op';
+$labels['untildate'] = 'de';
+$labels['each'] = 'Elke';
+$labels['onevery'] = 'Op elke';
+$labels['onsamedate'] = 'Op dezelfde datum';
+$labels['forever'] = 'oneindig';
+$labels['recurrencend'] = 'tot';
+$labels['forntimes'] = 'voor $nr keer';
+$labels['first'] = 'eerste';
+$labels['second'] = 'tweede';
+$labels['third'] = 'derde';
+$labels['fourth'] = 'vierde';
+$labels['last'] = 'laatste';
+$labels['dayofmonth'] = 'Dag van de maand';
+$labels['addrdate'] = 'Voeg herhaal datum toe';
+$labels['itipinvitation'] = 'Uitnodiging voor';
+$labels['itipupdate'] = 'Update van';
+$labels['itipcancellation'] = 'Afgelast:';
+$labels['itipaccepted'] = 'Accepteer';
+$labels['itiptentative'] = 'Misschien';
+$labels['itipdeclined'] = 'Afwijzen';
+$labels['itipdelegated'] = 'Gedelegeerde';
+$labels['itipcomment'] = 'Uw antwoord';
+$labels['itipeditresponse'] = 'Voeg een antwoord tekst toe';
+$labels['itipsendercomment'] = 'Opmerking van verzender:';
+$labels['itipobjectnotfound'] = 'Het object waaraan in dit bericht wordt gerefereerd is niet gevonden in uw account.';
+$labels['itipsubjectaccepted'] = '"$title" is geaccepteerd door $name';
+$labels['itipsubjectcancel'] = 'Uw deelname aan "$title" is geannuleerd';
+$labels['itipnewattendee'] = 'Dit is een antwoord van een nieuwe deelnemer';
+$labels['updateattendeestatus'] = 'Update status van deelnemer';
+$labels['acceptinvitation'] = 'Accepteer je deze uitnodiging?';
+$labels['acceptattendee'] = 'Accepteer deelnemer';
+$labels['declineattendee'] = 'Afwijzen deelnemer';
+$labels['declineattendeeconfirm'] = 'Voeg een bericht toe voor de afgewezen deelnemer (optioneel):';
+$labels['youhaveaccepted'] = 'Je hebt de uitnodiging geaccepteerd';
+$labels['youhavedeclined'] = 'Je hebt deze uitnodiging afgewezen';
+$labels['youhavedelegated'] = 'U heeft deze uitnodiging gedelegeerd';
+$labels['attendeeaccepted'] = 'Deelnemer heeft geaccepteerd';
+$labels['attendeetentative'] = 'Deelnemer heeft voorwaardelijk geaccepteerd';
+$labels['attendeedeclined'] = 'Deelnemer heeft afgewezen ';
+$labels['attendeedelegated'] = 'Deelnemer heeft gedelegeerd aan ';
+$labels['importtocalendar'] = 'Bewaar in mijn kalender';
+$labels['removefromcalendar'] = 'Verwijder van mijn kalender';
+$labels['updatemycopy'] = 'Wijzig mijn kopie';
+$labels['deleteobjectconfirm'] = 'Wilt u echt dit object verwijderen?';
+$labels['declinedeleteconfirm'] = 'Wilt u tevens deze afgewezen object verwijderen uit uw kalender?';
+$labels['savingdata'] = 'Data wordt opgeslagen...';
diff --git a/libcalendaring/localization/pl_PL.inc b/libcalendaring/localization/pl_PL.inc
new file mode 100644
index 0000000..ce44218
--- /dev/null
+++ b/libcalendaring/localization/pl_PL.inc
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'dopóki';
+$labels['frequency'] = 'Powtórz';
+$labels['recurrencend'] = 'dopóki';
+$labels['savingdata'] = 'Zapisuję dane...';
diff --git a/libcalendaring/localization/pt_BR.inc b/libcalendaring/localization/pt_BR.inc
new file mode 100644
index 0000000..eb3f14b
--- /dev/null
+++ b/libcalendaring/localization/pt_BR.inc
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'até';
+$labels['at'] = 'no';
+$labels['alarmemail'] = 'Enviar Email';
+$labels['alarmdisplay'] = 'Mostrar mensagem';
+$labels['alarmdisplayoption'] = 'Mensagem';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmat'] = 'como $datetime';
+$labels['trigger@'] = 'na data';
+$labels['trigger-M'] = 'minutos antes';
+$labels['trigger-H'] = 'horas antes';
+$labels['trigger-D'] = 'dias antes';
+$labels['trigger+M'] = 'minutos depois';
+$labels['trigger+H'] = 'horas depois';
+$labels['trigger+D'] = 'dias depois';
+$labels['alarmtitle'] = 'Próximos eventos';
+$labels['dismissall'] = 'Dispensar tudo';
+$labels['dismiss'] = 'Dispensar';
+$labels['snooze'] = 'Tirar uma soneca';
+$labels['repeatinmin'] = 'Repetir em $min minutos';
+$labels['repeatinhr'] = 'Repetir em 1 hora';
+$labels['repeatinhrs'] = 'Repetir em $hrs horas';
+$labels['repeattomorrow'] = 'Repetir amanhã';
+$labels['repeatinweek'] = 'Repetir em uma semana';
+$labels['showmore'] = 'Mostrar mais...';
+$labels['frequency'] = 'Repetir';
+$labels['never'] = 'nunca';
+$labels['daily'] = 'diariamente';
+$labels['weekly'] = 'semanalmente';
+$labels['monthly'] = 'mensalmente';
+$labels['yearly'] = 'anualmente';
+$labels['every'] = 'À cada';
+$labels['days'] = 'dia(s)';
+$labels['weeks'] = 'semana(s)';
+$labels['months'] = 'mês(es)';
+$labels['years'] = 'ano(s) em:';
+$labels['bydays'] = 'No';
+$labels['untildate'] = 'em';
+$labels['each'] = 'Cada';
+$labels['onevery'] = 'Em cada';
+$labels['onsamedate'] = 'Na mesma data';
+$labels['forever'] = 'nunca termina';
+$labels['recurrencend'] = 'até';
+$labels['forntimes'] = 'por $nr vez(es)';
+$labels['first'] = 'primeira';
+$labels['second'] = 'segunda';
+$labels['third'] = 'terceira';
+$labels['fourth'] = 'quarta';
+$labels['last'] = 'última';
+$labels['dayofmonth'] = 'Dia do mês';
+$labels['itipinvitation'] = 'Convite para';
+$labels['itipupdate'] = 'Atualização de';
+$labels['itipcancellation'] = 'Cancelado:';
+$labels['itipreply'] = 'Responder para';
+$labels['itipaccepted'] = 'Aceitar';
+$labels['itiptentative'] = 'Talvez';
+$labels['itipdeclined'] = 'Rejeitar';
+$labels['itipdelegated'] = 'Delegado';
+$labels['itipobjectnotfound'] = 'O objeto referenciado por esta mensagem não foi encontrado em sua conta.';
+$labels['itipsubjectaccepted'] = '"$title" foi aceito por $name';
+$labels['itipsubjecttentative'] = '"$title" foi aceito como tentativa por $name';
+$labels['itipsubjectdeclined'] = '"$title" foi recusado por $name';
+$labels['itipnewattendee'] = 'Esta é a resposta de um novo participante';
+$labels['updateattendeestatus'] = 'Atualizar o estado dos participantes';
+$labels['acceptinvitation'] = 'Você aceita este convite?';
+$labels['acceptattendee'] = 'Aceitar participante';
+$labels['declineattendee'] = 'Recusar participante';
+$labels['youhaveaccepted'] = 'Você aceitou este convite';
+$labels['youhavetentative'] = 'Você aceitou como tentativa este convite';
+$labels['youhavedeclined'] = 'Você recusou este convite';
+$labels['youhavedelegated'] = 'Você delegou este convite';
+$labels['attendeeaccepted'] = 'O participante aceitou';
+$labels['attendeetentative'] = 'O participante aceitou temporariamente';
+$labels['attendeedeclined'] = 'O participante recusou';
+$labels['attendeedelegated'] = 'O participante delegou para $delegatedto';
+$labels['importtocalendar'] = 'Salvar em meu calendário';
+$labels['removefromcalendar'] = 'Remover do meu calendário';
+$labels['savingdata'] = 'Salvando dados...';
diff --git a/libcalendaring/localization/pt_PT.inc b/libcalendaring/localization/pt_PT.inc
new file mode 100644
index 0000000..2760096
--- /dev/null
+++ b/libcalendaring/localization/pt_PT.inc
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'até';
+$labels['frequency'] = 'Repetir';
+$labels['recurrencend'] = 'até';
+$labels['savingdata'] = 'Salvando dados...';
diff --git a/libcalendaring/localization/ru_RU.inc b/libcalendaring/localization/ru_RU.inc
new file mode 100644
index 0000000..77d2024
--- /dev/null
+++ b/libcalendaring/localization/ru_RU.inc
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'Повторять';
+$labels['at'] = 'в';
+$labels['alarmemail'] = 'Послать e-mail';
+$labels['alarmdisplay'] = 'Показать сообщение';
+$labels['alarmaudio'] = 'Проиграть мелодию';
+$labels['alarmdisplayoption'] = 'Сообщение';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmaudiooption'] = 'Мелодия';
+$labels['alarmat'] = 'на $datetime';
+$labels['trigger@'] = 'на дату';
+$labels['trigger-M'] = 'минут до';
+$labels['trigger-H'] = 'часов до';
+$labels['trigger-D'] = 'дней до';
+$labels['trigger+M'] = 'минут после';
+$labels['trigger+H'] = 'часов после';
+$labels['trigger+D'] = 'дней после';
+$labels['triggerattime'] = 'в';
+$labels['addalarm'] = 'Добавить уведомление';
+$labels['removealarm'] = 'Удалить уведомление';
+$labels['alarmtitle'] = 'Предстоящие события';
+$labels['dismissall'] = 'Отменить все';
+$labels['dismiss'] = 'Отменить';
+$labels['snooze'] = 'Отложить';
+$labels['repeatinmin'] = 'Повторить через $min minutes';
+$labels['repeatinhr'] = 'Повторить через 1 час';
+$labels['repeatinhrs'] = 'Повторить через $hrs часов';
+$labels['repeattomorrow'] = 'Повторить завтра';
+$labels['repeatinweek'] = 'Повторить через неделю';
+$labels['showmore'] = 'Показать больше...';
+$labels['recurring'] = 'Совпадения';
+$labels['frequency'] = 'Повторить';
+$labels['never'] = 'никогда';
+$labels['daily'] = 'ежедневно';
+$labels['weekly'] = 'еженедельно';
+$labels['monthly'] = 'ежемесячно';
+$labels['yearly'] = 'ежегодно';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Каждый(ую)';
+$labels['days'] = 'день';
+$labels['weeks'] = 'неделю';
+$labels['months'] = 'месяц';
+$labels['years'] = 'год в:';
+$labels['bydays'] = 'В';
+$labels['untildate'] = 'до';
+$labels['each'] = 'Каждого';
+$labels['onevery'] = 'В';
+$labels['onsamedate'] = 'В ту же самую дату';
+$labels['forever'] = 'всегда';
+$labels['recurrencend'] = 'Повторять';
+$labels['untilenddate'] = 'до';
+$labels['forntimes'] = '$nr раз(а)';
+$labels['first'] = 'первый(ую)';
+$labels['second'] = 'вторый(ую)';
+$labels['third'] = 'третий(ую)';
+$labels['fourth'] = 'четвертый(ую)';
+$labels['last'] = 'последний(ую)';
+$labels['dayofmonth'] = 'День месяца';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'исключая';
+$labels['itipinvitation'] = 'Приглашение на';
+$labels['itipupdate'] = 'Обновление';
+$labels['itipcancellation'] = 'Отменённый:';
+$labels['itipreply'] = 'Ответить';
+$labels['itipaccepted'] = 'Принять';
+$labels['itiptentative'] = 'Может быть';
+$labels['itipdeclined'] = 'Отклонить';
+$labels['itipdelegated'] = 'Представитель';
+$labels['itipneeds-action'] = 'Отложить';
+$labels['itipcomment'] = 'Ваш ответ';
+$labels['itipeditresponse'] = 'Введите текст ответа';
+$labels['itipsendercomment'] = 'Комментарий отправителя:';
+$labels['itipsuppressreply'] = 'Не отправлять ответ';
+$labels['itipobjectnotfound'] = 'Объект, указанный в данном сообщении не был найден в вашем аккаунте.';
+$labels['itipsubjectaccepted'] = '"$title" принято $name';
+$labels['itipsubjecttentative'] = '"$title" предварительно принято $name';
+$labels['itipsubjectdeclined'] = '"$title" отклонено $name';
+$labels['itipsubjectin-process'] = '"$title" обрабатывается $name';
+$labels['itipsubjectcompleted'] = '"$title" завершена $name';
+$labels['itipsubjectcancel'] = 'Ваше участие в "$title" было отменено';
+$labels['itipsubjectdelegated'] = '"$title" было поручено $name';
+$labels['itipsubjectdelegatedto'] = '"$title" было поручено Вам $name';
+$labels['itipnewattendee'] = 'Это ответ от нового участника';
+$labels['updateattendeestatus'] = 'Обновить статус участника';
+$labels['acceptinvitation'] = 'Вы принимаете это приглашение?';
+$labels['acceptattendee'] = 'Принять участника';
+$labels['declineattendee'] = 'Отказать участнику';
+$labels['declineattendeeconfirm'] = 'Введите сообщение для отклонённого участника (не обязательно):';
+$labels['rsvpmodeall'] = 'Вся серия';
+$labels['rsvpmodecurrent'] = 'Только это событие';
+$labels['rsvpmodefuture'] = 'Это и последующие повторения';
+$labels['itipsingleoccurrence'] = 'Это <em>единичное событие</em> из серии событий';
+$labels['itipfutureoccurrence'] = 'Относится к <em>этому и всем последующим повторениями</em> серии событий';
+$labels['itipmessagesingleoccurrence'] = 'Это сообщение относится только к этому единичному событию';
+$labels['itipmessagefutureoccurrence'] = 'Это сообщение относится к этому и всем последующим повторениям';
+$labels['youhaveaccepted'] = 'Вы приняли это приглашение';
+$labels['youhavetentative'] = 'Вы предварительно приняли это приглашение';
+$labels['youhavedeclined'] = 'Вы отклонили это приглашение';
+$labels['youhavedelegated'] = 'Вы делегировали это приглашение';
+$labels['youhavein-process'] = 'Вы работаете над этим заданием';
+$labels['youhavecompleted'] = 'Вы завершили это задание';
+$labels['youhaveneeds-action'] = 'Ваш ответ на это приглашение всё ещё ожидается';
+$labels['youhavepreviouslyaccepted'] = 'Вы уже приняли это приглашение';
+$labels['youhavepreviouslytentative'] = 'Вы уже предварительно приняли это приглашение';
+$labels['youhavepreviouslydeclined'] = 'Вы уже отказались от этого приглашения';
+$labels['youhavepreviouslydelegated'] = 'Вы уже делегировали это приглашение';
+$labels['youhavepreviouslyin-process'] = 'Вы уже предоставили отчет о проделанной работе на это задание';
+$labels['youhavepreviouslycompleted'] = 'Вы уже завершили это задание';
+$labels['youhavepreviouslyneeds-action'] = 'Ваш ответ на это приглашение всё ещё ожидается';
+$labels['attendeeaccepted'] = 'Участник принял приглашение';
+$labels['attendeetentative'] = 'Участник предварительно принял приглашение';
+$labels['attendeedeclined'] = 'Участник отказался от приглашения';
+$labels['attendeedelegated'] = 'Участник делегировал приглашение $delegatedto';
+$labels['attendeein-process'] = 'Участник в процессе';
+$labels['attendeecompleted'] = 'Участник завершил';
+$labels['notanattendee'] = 'Вы не указаны как участник этого объекта';
+$labels['outdatedinvitation'] = 'Это приглашение было заменено новой версией';
+$labels['importtocalendar'] = 'Сохранить в мой календарь';
+$labels['removefromcalendar'] = 'Удалить из моего календаря';
+$labels['updatemycopy'] = 'Обновить мою копию';
+$labels['openpreview'] = 'Открыть предпросмотр';
+$labels['deleteobjectconfirm'] = 'Вы действительно хотите удалить этот объект?';
+$labels['declinedeleteconfirm'] = 'Вы хотите так же удалить этот объект со своего аккаунта?';
+$labels['delegateinvitation'] = 'Приглашение представителей';
+$labels['delegateto'] = 'Поручить';
+$labels['delegatersvpme'] = 'Сообщать мне об изменениях в этом событии';
+$labels['delegateinvalidaddress'] = 'Пожалуйста, введите правильный email адрес представителя';
+$labels['savingdata'] = 'Сохранение данных...';
+$labels['expandattendeegroup'] = 'Заменить участниками группы';
+$labels['expandattendeegroupnodata'] = 'Не удаётся заменить участниками группы. Ни одного не найдено.';
+$labels['expandattendeegrouperror'] = 'Не удаётся заменить участниками группы. Возможно, в ней слишком много участников.';
+$labels['expandattendeegroupsizelimit'] = 'Группа содержит слишком много участников для замены.';
diff --git a/libcalendaring/localization/sk_SK.inc b/libcalendaring/localization/sk_SK.inc
new file mode 100644
index 0000000..6679135
--- /dev/null
+++ b/libcalendaring/localization/sk_SK.inc
@@ -0,0 +1,9 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['frequency'] = 'Opakovať';
diff --git a/libcalendaring/localization/sl_SI.inc b/libcalendaring/localization/sl_SI.inc
new file mode 100644
index 0000000..946eb61
--- /dev/null
+++ b/libcalendaring/localization/sl_SI.inc
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'do';
+$labels['at'] = 'ob';
+$labels['alarmemailoption'] = 'Email';
+$labels['frequency'] = 'Ponovi';
+$labels['recurrencend'] = 'do';
+$labels['itipdelegated'] = 'Sodelujoč';
+$labels['savingdata'] = 'Shranjujem...';
diff --git a/libcalendaring/localization/sv_SE.inc b/libcalendaring/localization/sv_SE.inc
new file mode 100644
index 0000000..bd80faf
--- /dev/null
+++ b/libcalendaring/localization/sv_SE.inc
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'tills';
+$labels['frequency'] = 'Upprepa';
+$labels['recurrencend'] = 'tills';
+$labels['savingdata'] = 'Sparar data ...';
diff --git a/libcalendaring/localization/th_TH.inc b/libcalendaring/localization/th_TH.inc
new file mode 100644
index 0000000..1fa089a
--- /dev/null
+++ b/libcalendaring/localization/th_TH.inc
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'จนถึง';
+$labels['alarmemail'] = 'ส่ง อีเมล์';
+$labels['alarmdisplay'] = 'แสดงข้อความ';
+$labels['alarmaudio'] = 'เล่นเสียง';
+$labels['alarmdisplayoption'] = 'ข้อความ';
+$labels['alarmemailoption'] = 'อีเมล์';
+$labels['alarmaudiooption'] = 'เสียง';
+$labels['alarmat'] = 'เมื่อ $datetime';
+$labels['trigger@'] = 'ณ วันที่';
+$labels['trigger-M'] = 'นาที ก่อน';
+$labels['trigger-H'] = 'ชั่วโมง ก่อน';
+$labels['trigger-D'] = 'วัน ก่อน';
+$labels['trigger+M'] = 'นาที หลัง';
+$labels['trigger+H'] = 'ชั่วโมง หลัง';
+$labels['trigger+D'] = 'วัน หลัง';
+$labels['triggerattime'] = 'เมื่อเวลา';
+$labels['addalarm'] = 'เพิ่มการเตือน';
+$labels['removealarm'] = 'ลบการเตือน';
+$labels['alarmtitle'] = 'เหตุการณ์ที่กำลังจะถึง';
+$labels['dismissall'] = 'ยกเลิกทั้งหมด';
+$labels['dismiss'] = 'ยกเลิก';
+$labels['snooze'] = 'ปิดชั่วคราว';
+$labels['repeatinmin'] = 'ทำซ้ำใน $min นาที';
+$labels['repeatinhr'] = 'ทำซ้ำใน 1 ชั่วโมง';
+$labels['repeatinhrs'] = 'ทำซ้ำใน $hrs ชั่วโมง';
+$labels['repeattomorrow'] = 'ทำซ้ำพรุ่งนี้';
+$labels['repeatinweek'] = 'ทำซ้ำใน 1 สัปดาห์';
+$labels['showmore'] = 'แสดง มากกว่านี้...';
+$labels['recurring'] = 'ทำซ้ำ';
+$labels['frequency'] = 'ทำซ้ำ';
+$labels['never'] = 'ไม่มีทาง';
+$labels['daily'] = 'ทุกวัน';
+$labels['weekly'] = 'ทุกสัปดาห์';
+$labels['monthly'] = 'ทุกเดือน';
+$labels['yearly'] = 'ทุกปี';
+$labels['rdate'] = 'ณ วันที่';
+$labels['every'] = 'ทุก ๆ';
+$labels['days'] = 'วัน';
+$labels['weeks'] = 'สัปดาห์';
+$labels['months'] = 'เดือน';
+$labels['years'] = 'ปี';
+$labels['each'] = 'แต่ละ';
+$labels['forever'] = 'ตลอดไป';
+$labels['recurrencend'] = 'จนถึง';
+$labels['untilenddate'] = 'จนถึงวันที่';
+$labels['forntimes'] = 'จำนวน $nr ครั้ง';
+$labels['first'] = 'ครั้งที่ 1';
+$labels['second'] = 'ครั้งที่ 2';
+$labels['third'] = 'ครั้งที่ 3';
+$labels['fourth'] = 'ครั้งที่ 4';
+$labels['last'] = 'ครั้งสุดท้าย';
+$labels['addrdate'] = 'เพิ่มวันที่ทำซ้ำ';
+$labels['except'] = 'ยกเว้น';
+$labels['itipinvitation'] = 'คำเชิญถึง';
+$labels['itipcancellation'] = 'ยกเลิก:';
+$labels['itipreply'] = 'ตอบกลับไปยัง';
+$labels['itipaccepted'] = 'ยอมรับ';
+$labels['itiptentative'] = 'อาจจะ';
+$labels['itipdeclined'] = 'ปฎิเสธ';
+$labels['itipneeds-action'] = 'เลื่อน';
+$labels['itipcomment'] = 'ข้อความตอบกลับของคุณ';
+$labels['itipeditresponse'] = 'ป้อนข้อความตอบกลับ';
+$labels['itipsendercomment'] = 'ความคิดเห็นของผู้ส่ง:';
+$labels['itipsuppressreply'] = 'ไม่ส่งข้อความตอบกลับ';
+$labels['itipobjectnotfound'] = 'ไม่พบวัตถุที่อ้างถึงโดยข้อความนี้ในบัญชีของคุณ';
+$labels['itipsubjectaccepted'] = '"$title" ถูกตอบรับโดย $name';
+$labels['itipsubjecttentative'] = '"$title" มีแนวโน้มที่จะได้รับการตอบรับโดย $name';
+$labels['itipsubjectdeclined'] = '"$title" ถูกปฎิเสธโดย $name';
+$labels['itipsubjectin-process'] = '"$title" อยู่ระหว่างการจัดเตรียมโดย $name';
+$labels['itipsubjectcompleted'] = '"$title" ถูกทำให้สมบูรณ์โดย $name';
+$labels['itipsubjectcancel'] = 'การเข้าร่วมของคุณใน "$title" ถูกยกเลิก';
+$labels['savingdata'] = 'บันทึกข้อมูล';
diff --git a/libcalendaring/localization/uk_UA.inc b/libcalendaring/localization/uk_UA.inc
new file mode 100644
index 0000000..713cf1b
--- /dev/null
+++ b/libcalendaring/localization/uk_UA.inc
@@ -0,0 +1,14 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'до';
+$labels['alarmemailoption'] = 'Пошта';
+$labels['frequency'] = 'Повторити';
+$labels['recurrencend'] = 'до';
+$labels['itipdelegated'] = 'Представник';
+$labels['savingdata'] = 'Збереження даних...';
diff --git a/libcalendaring/localization/zh_CN.inc b/libcalendaring/localization/zh_CN.inc
new file mode 100644
index 0000000..f92012b
--- /dev/null
+++ b/libcalendaring/localization/zh_CN.inc
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['frequency'] = '循环';
+$labels['savingdata'] = '保存数据...';
diff --git a/libcalendaring/skins/larry/libcal.css b/libcalendaring/skins/larry/libcal.css
new file mode 100644
index 0000000..f679abc
--- /dev/null
+++ b/libcalendaring/skins/larry/libcal.css
@@ -0,0 +1,166 @@
+/**
+ * Roundcube libcalendaring plugin styles for skin "Larry"
+ *
+ * Copyright (c) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * The contents are subject to the Creative Commons Attribution-ShareAlike
+ * License. It is allowed to copy, distribute, transmit and to adapt the work
+ * by keeping credits to the original autors in the README file.
+ * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
+ */
+
+.alarm-item {
+ margin: 0.4em 0 1em 0;
+}
+
+.alarm-item .event-title {
+ font-size: 14px;
+ margin: 0.1em 0 0.3em 0;
+}
+
+.alarm-item div.event-section {
+ margin-top: 0.1em;
+ margin-bottom: 0.3em;
+}
+
+.alarm-item .alarm-actions {
+ margin-top: 0.4em;
+}
+
+.alarm-item div.alarm-actions a {
+ margin-right: 0.8em;
+ text-decoration: none;
+}
+
+a.alarm-action-snooze:after {
+ content: ' ▼';
+ font-size: 10px;
+ color: #666;
+}
+
+#alarm-snooze-dropdown {
+ z-index: 5000;
+}
+
+span.edit-alarm-set {
+ white-space: nowrap;
+}
+
+.ui-dialog.alarms .ui-dialog-title {
+ background-image: url(../../../../skins/larry/images/messages.png);
+ background-repeat: no-repeat;
+ background-position: 0 -91px;
+ padding-left: 24px;
+}
+
+.itip-reply-comment {
+ padding-left: 2px;
+}
+
+a.reply-comment-toggle {
+ display: inline-block;
+ color: #666;
+}
+
+label.noreply-toggle + a.reply-comment-toggle {
+ margin-left: 1em;
+}
+
+.itip-reply-comment textarea {
+ display: block;
+ width: 90%;
+ margin-top: 0.5em;
+}
+
+.itip-dialog-confirm-text {
+ margin-bottom: 1em;
+}
+
+.popup textarea.itip-comment {
+ width: 98%;
+}
+
+.edit-alarm-item {
+ position: relative;
+ padding-right: 30px;
+ margin-bottom: 0.2em;
+}
+
+.edit-alarm-buttons {
+ position: absolute;
+ top: 1px;
+ right: 0;
+}
+
+.edit-alarm-buttons a.iconbutton {
+ display: none;
+}
+
+.edit-alarm-item .edit-alarm-buttons a.delete-alarm,
+.edit-alarm-item.first .edit-alarm-buttons a.add-alarm {
+ display: inline-block;
+}
+
+.edit-alarm-item.first .edit-alarm-buttons a.delete-alarm {
+ display: none;
+}
+
+.recurrence-form {
+ display: none;
+}
+
+.recurrence-form label.weekday,
+.recurrence-form label.monthday {
+ min-width: 3em;
+}
+
+.recurrence-form label.month {
+ min-width: 5em;
+}
+
+#edit-recurrence-yearly-bymonthblock {
+ margin-left: 7.5em;
+}
+
+#edit-recurrence-rdates {
+ display: block;
+ list-style: none;
+ margin: 0 0 0.8em 0;
+ padding: 0;
+ max-height: 300px;
+ overflow: auto;
+}
+
+#edit-recurrence-rdates li {
+ display: block;
+ position: relative;
+ width: 12em;
+ padding: 4px 0 4px 0;
+}
+
+#edit-recurrence-rdates li a.delete {
+ position: absolute;
+ top: 2px;
+ right: 0;
+ width: 20px;
+ height: 18px;
+ background-position: -7px -337px;
+}
+
+#recurrence-form-until div.line {
+ margin-left: 7.5em;
+ margin-bottom: 0.3em;
+}
+
+#recurrence-form-until div.line.first {
+ margin-top: -1.4em;
+}
+
+.itip-dialog-form input.text {
+ width: 98%;
+}
+
+.itip-dialog-form label > input.checkbox {
+ margin-left: 0;
+ margin-right: 10px;
+}

© 2014-2024 Faster IT GmbH | imprint | privacy policy