Mercurial > hg > rc1
changeset 4:888e774ee983
libcalendar plugin as distributed
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/README Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,16 @@ +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 +* iTip invitations handling + +iCal parsing and exporting is done with the help of the Sabre VObject +library [1]. It needs to be installed with Roundcube using composer: + + $ composer require "sabre/vobject" "~3.3.3" + +[1]: http://sabre.io/vobject/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Horde_Date.php Sat Jan 13 08:57:56 2018 -0500 @@ -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); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Horde_Date_Recurrence.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,1744 @@ +<?php + +/** + * This is a modified copy of Horde/Date/Recurrence.php (2015-01-05) + * 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(__DIR__ . '/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-2015 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->month != $estart->month) { + // We're already in the next month. + continue; + } + 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) + { + $key = sprintf('%04d%02d%02d', $year, $month, $mday); + if (array_search($key, $this->exceptions) === false) { + $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, $hour, $min, $sec, $tz) = + sscanf($remainder, '%04d%02d%02dT%02d%02d%02d%s'); + $this->setRecurEnd(new Horde_Date(array('year' => $year, + 'month' => $month, + 'mday' => $mday, + 'hour' => $hour, + 'min' => $min, + 'sec' => $sec), + $tz == 'Z' ? 'UTC' : $this->start->timezone)); + } + } + } + + /** + * 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; + } + + // MUST take into account the time portion if it is present. + // See Bug: 12869 and Bug: 2813 + if (isset($rdata['UNTIL'])) { + if (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $rdata['UNTIL'], $parts)) { + $until = new Horde_Date($rdata['UNTIL'], 'UTC'); + $until->setTimezone($this->start->timezone); + } else { + list($year, $month, $mday) = sscanf($rdata['UNTIL'], + '%04d%02d%02d'); + $until = new Horde_Date( + array('year' => $year, + 'month' => $month, + 'mday' => $mday + 1), + $this->start->timezone + ); + } + $this->setRecurEnd($until); + } + 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; + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + + for ($i = $flag = 0; $i <= 7; ++$i) { + if ($this->recurOnDay(pow(2, $i))) { + if ($flag == 0) { + $rrule .= ';BYDAY='; + $flag = 1; + } else { + $rrule .= ','; + } + $rrule .= $vcaldays[$i]; + } + } + 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 Kolab hash. + * + * @param array $hash The hash to convert. + * + * @return boolean True if the hash seemed valid, false otherwise. + */ + public function fromKolab($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['daynumber'])) { + $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['exclusion'])) { + foreach ($hash['exclusion'] as $exception) { + if ($exception instanceof DateTime) { + $this->exceptions[] = $exception->format('Ymd'); + } + } + } + + if (isset($hash['complete'])) { + foreach ($hash['complete'] as $completion) { + if ($exception instanceof DateTime) { + $this->completions[] = $completion->format('Ymd'); + } + } + } + + return true; + } + + /** + * Export this object into a Kolab hash. + * + * @return array The recurrence hash. + */ + public function toKolab() + { + 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->toDateTime(); + } else { + $hash['range-type'] = 'none'; + $hash['range'] = ''; + } + + // Recurrence exceptions + $hash['exclusion'] = $hash['complete'] = array(); + foreach ($this->exceptions as $exception) { + $hash['exclusion'][] = new DateTime($exception); + } + foreach ($this->completions as $completionexception) { + $hash['complete'][] = new DateTime($completionexception); + } + + 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; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,405 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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' => 'Sabre\\VObject\\Component\\VAlarm', + 'VCALENDAR' => 'Sabre\\VObject\\Component\\VCalendar', + 'VCARD' => 'Sabre\\VObject\\Component\\VCard', + 'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', + 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', + 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', + 'VFREEBUSY' => 'Sabre\\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 \\Sabre\\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 \\Sabre\\VObject\\Component, \\Sabre\\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; + } + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,108 @@ +<?php + +namespace Sabre\VObject\Component; +use Sabre\VObject; + +/** + * VAlarm component + * + * This component contains some additional functionality specific for VALARMs. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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); + } + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,244 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * The VCalendar component + * + * This component adds functionality to a component, specific for a VCALENDAR. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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() + ); + + } + */ + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,107 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * The VCard component + * + * This component represents the BEGIN:VCARD and END:VCARD found in every + * vcard. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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 + ); + + } + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,70 @@ +<?php + +namespace Sabre\VObject\Component; +use Sabre\VObject; + +/** + * VEvent component + * + * This component contains some additional functionality specific for VEVENT's. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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) + ); + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,68 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * The VFreeBusy component + * + * This component adds functionality to a component, specific for VFREEBUSY + * components. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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; + + } + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,46 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * VJournal component + * + * This component contains some additional functionality specific for VJOURNALs. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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; + + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,68 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * VTodo component + * + * This component contains some additional functionality specific for VTODOs. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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; + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,181 @@ +<?php + +namespace Sabre\VObject; + +/** + * DateTimeParser + * + * This class is responsible for parsing the several different date and time + * formats iCalendar and vCards have. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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); + } + + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Document.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,109 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH. All rights reserved. + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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); + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,172 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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'); + + } + + /* }}} */ + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,322 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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 \\Sabre\\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; + + } + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Node.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,187 @@ +<?php + +namespace Sabre\VObject; + +/** + * Base class for all nodes + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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 + + /* }}} */ + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,102 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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; + } + $src = array( + '\\', + "\n", + ); + $out = array( + '\\\\', + '\n', + ); + + // quote parameters according to RFC 5545, Section 3.2 + $quotes = ''; + if (preg_match('/[:;,]/', $this->value)) { + $quotes = '"'; + } + + return $this->name . '=' . $quotes . str_replace($src, $out, $this->value) . $quotes; + + } + + /** + * Called when this object is being cast to a string + * + * @return string + */ + public function __toString() { + + return $this->value; + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,12 @@ +<?php + +namespace Sabre\VObject; + +/** + * Exception thrown by Reader if an invalid object was attempted to be parsed. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class ParseException extends \Exception { }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Property.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,453 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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' => 'Sabre\\VObject\\Property\\DateTime', + 'CREATED' => 'Sabre\\VObject\\Property\\DateTime', + 'DTEND' => 'Sabre\\VObject\\Property\\DateTime', + 'DTSTAMP' => 'Sabre\\VObject\\Property\\DateTime', + 'DTSTART' => 'Sabre\\VObject\\Property\\DateTime', + 'DUE' => 'Sabre\\VObject\\Property\\DateTime', + 'EXDATE' => 'Sabre\\VObject\\Property\\MultiDateTime', + 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\DateTime', + 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\DateTime', + 'TRIGGER' => 'Sabre\\VObject\\Property\\DateTime', + 'N' => 'Sabre\\VObject\\Property\\Compound', + 'ORG' => 'Sabre\\VObject\\Property\\Compound', + 'ADR' => 'Sabre\\VObject\\Property\\Compound', + 'CATEGORIES' => 'Sabre\\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 \\Sabre\\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; + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,125 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @license http://code.google.com/p/sabredav/wiki/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) + ); + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,245 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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 \Sabre\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 + ); + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,180 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class MultiDateTime extends VObject\Property { + + /** + * DateTime representation + * + * @var DateTime[] + */ + protected $dateTimes; + + /** + * dateType + * + * This is one of the Sabre\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; + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Reader.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,223 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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; + + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,1144 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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; + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,111 @@ +<?php + +namespace Sabre\VObject\Splitter; + +use Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Dominik Tobschall + * @author Armin Hackmann + * @license http://code.google.com/p/sabredav/wiki/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 Sabre\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; + + } + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Dominik Tobschall + * @license http://code.google.com/p/sabredav/wiki/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 Sabre\VObject\Component|null + */ + function getNext(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,76 @@ +<?php + +namespace Sabre\VObject\Splitter; + +use Sabre\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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Dominik Tobschall + * @author Armin Hackmann + * @license http://code.google.com/p/sabredav/wiki/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 Sabre\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; + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,61 @@ +<?php + +namespace Sabre\VObject; + +/** + * Useful utilities for working with various strings. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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)); + + } + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,482 @@ +<?php + +namespace Sabre\VObject; + +/** + * Time zone name translation + * + * This file translates well-known time zone names into "Olson database" time zone names. + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Frank Edelhaeuser (fedel@users.sourceforge.net) + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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', + ); + + /** + * 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 Sabre\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. + 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. + 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); + } + + try { + return new \DateTimeZone($lic); + } catch (\Exception $e) { + } + + } + // 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()); + + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/Version.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,24 @@ +<?php + +namespace Sabre\VObject; + +/** + * This class contains the version number for the VObject package + * + * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class Version { + + /** + * Full version number + */ + const VERSION = '2.1.3'; + + /** + * Stability : alpha, beta, stable + */ + const STABILITY = 'stable'; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/Sabre/VObject/includes.php Sat Jan 13 08:57:56 2018 -0500 @@ -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) 2007-2013 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/get_sabre_vobject.sh Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,12 @@ +#!/bin/sh + +# Download and install the Sabre\Vobject library for this plugin + +wget 'https://github.com/fruux/sabre-vobject/archive/2.1.0.tar.gz' -O sabre-vobject-2.1.0.tar.gz +tar xf sabre-vobject-2.1.0.tar.gz + +mv sabre-vobject-2.1.0/lib/* . +rm -rf sabre-vobject-2.1.0 + +cd lib/Sabre/VObject && wget --no-check-certificate -O Property.php https://raw2.github.com/thomascube/sabre-vobject/84b64c65f9a94f7ec5a5e327bab3cc1335dd613c/lib/Sabre/VObject/Property.php +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,867 @@ +<?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_utils::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)) { + rcube::console('iTip ' . $method, $message->txtHeaders() . "\r\n" . $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_utils::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']); + } + } + + // Set SENT-BY property if the sender is not the organizer + if ($method == 'CANCEL' || $method == 'REQUEST') { + foreach ((array)$event['attendees'] as $idx => $attendee) { + if ($attendee['role'] == 'ORGANIZER' + && $attendee['email'] + && strcasecmp($attendee['email'], $from) != 0 + && strcasecmp($attendee['email'], $from_utf) != 0 + ) { + $attendee['sent-by'] = 'mailto:' . $from_utf; + $event['organizer'] = $event['attendees'][$idx] = $attendee; + break; + } + } + } + + // 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', RCUBE_CHARSET); + $message->setParam('text_charset', RCUBE_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', '', RCUBE_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 = $resheduled = 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; + } + } + + // Detect re-sheduling + if (!$latest) { + // FIXME: This is probably to simplistic, or maybe we should just check + // attendee's RSVP flag in the new event? + $resheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end']; + } + } + 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 ($resheduled) { + $action = 'rsvp'; + } + else if ($status_lc != 'needs-action') { + $action = !$latest ? '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' => rcube::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, + 'resheduled' => $resheduled, + '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') { + if (empty($event['_sender']) || self::compare_email($attendee['email'], $event['_sender'], $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('" . rcube::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('" . rcube::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('" . rcube::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('" . rcube::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('" . rcube::JQ($preview_url) . "', '" . rcube::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('" . rcube::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('" . rcube::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', '" . rcube::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('" . rcube::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(" . rcube_output::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 + $toggle_attrib = array( + 'href' => '#toggle', + 'class' => 'reply-comment-toggle', + 'onclick' => '$(this).hide().parent().find(\'textarea\').show().focus()' + ); + $textarea_attrib = array( + 'id' => 'reply-comment-' . $dom_id, + 'name' => '_comment', + 'cols' => 40, + 'rows' => 6, + 'style' => 'display:none', + 'placeholder' => $this->gettext('itipcomment') + ); + + $rsvp_additions .= html::a($toggle_attrib, $this->gettext('itipeditresponse')) + . html::div('itip-reply-comment', html::tag('textarea', $textarea_attrib, '')); + + 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', rcube::Q($event['title'])); + if ($event['start'] && $event['end']) { + $table->add('label', $this->gettext('date')); + $table->add('date', rcube::Q($this->lib->event_date_text($event))); + } + else if ($event['due'] && $event['_type'] == 'task') { + $table->add('label', $this->gettext('date')); + $table->add('date', rcube::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', rcube::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', rcube::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; + } + + /** + * Compare email address + */ + public static function compare_email($value, $email, $email_utf = null) + { + $v1 = !empty($email) && strcasecmp($value, $email) === 0; + $v2 = !empty($email_utf) && strcasecmp($value, $email_utf) === 0; + + return $v1 || $v2; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/lib/libcalendaring_recurrence.php Sat Jan 13 08:57:56 2018 -0500 @@ -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; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/libcalendaring.js Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,1464 @@ +/** + * 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, '<').replace(/>/g, '>').replace(/"/g, '"'); + }; + + /** + * 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; + }; + + /** + * Checks if the event/task has 'real' attendees, excluding the current user + */ + this.has_attendees = function(event) + { + return !!(event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email)); + }; + + /** + * Check if the current user is an attendee of this event/task + */ + this.is_attendee = function(event, role, email) + { + var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails; + + for (i=0; event.attendees && i < event.attendees.length; i++) { + if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) { + return event.attendees[i]; + } + } + + return false; + }; + + /** + * Checks if the current user is the organizer of the event/task + */ + this.is_organizer = function(event, email) + { + return this.is_attendee(event, 'ORGANIZER', email) || !event.id; + }; + + /** + * Check permissions on the given folder object + */ + this.has_permission = function(folder, perm) + { + // multiple chars means "either of" + if (String(perm).length > 1) { + for (var i=0; i < perm.length; i++) { + if (this.has_permission(folder, perm[i])) { + return true; + } + } + } + + if (folder.rights && String(folder.rights).indexOf(perm) >= 0) { + return true; + } + + return (perm == 'i' && folder.editable) || (perm == 'v' && folder.editable); + }; + + + /** + * 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) + { + // already a Date object? + if (s && s.getMonth) { + return 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 val = $(this).val(), parent = $(this).parent(); + parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide'](); + parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0'); + parent.find('.edit-alarm-related')[val == '@' ? 'hide' : '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); + $('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start'); + + 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(/^[-+]*0[MHDS]$/)) { + $('input.edit-alarm-value', domnode).val('0'); + $('select.edit-alarm-offset', domnode).val('0'); + } + 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(), + related: $('select.edit-alarm-related', 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 (offset === '0') { + alarm.trigger = '0S'; + } + 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; + }; + + // format time string + var time_autocomplete_format = function(hour, minutes, start) { + var time, diff, unit, duration = '', d = new Date(); + + d.setHours(hour); + d.setMinutes(minutes); + time = me.format_time(d); + + if (start) { + diff = Math.floor((d.getTime() - start.getTime()) / 60000); + if (diff > 0) { + unit = 'm'; + if (diff >= 60) { + unit = 'h'; + diff = Math.round(diff / 3) / 20; + } + duration = ' (' + diff + unit + ')'; + } + } + + return [time, duration]; + }; + + var time_autocomplete_list = function(p, callback) { + // Time completions + var st, h, step = 15, result = [], now = new Date(), + id = String(this.element.attr('id')), + m = id.match(/^(.*)-(starttime|endtime)$/), + start = (m && m[2] == 'endtime' + && (st = $('#' + m[1] + '-starttime').val()) + && $('#' + m[1] + '-startdate').val() == $('#' + m[1] + '-enddate').val()) + ? me.parse_datetime(st, '') : null, + full = p.term - 1 > 0 || p.term.length > 1, + hours = start ? start.getHours() : (full ? me.parse_datetime(p.term, '') : now).getHours(), + minutes = hours * 60 + (full ? 0 : now.getMinutes()), + min = Math.ceil(minutes / step) * step % 60, + hour = Math.floor(Math.ceil(minutes / step) * step / 60); + + // list hours from 0:00 till now + for (h = start ? start.getHours() : 0; h < hours; h++) + result.push(time_autocomplete_format(h, 0, start)); + + // list 15min steps for the next two hours + for (; h < hour + 2 && h < 24; h++) { + while (min < 60) { + result.push(time_autocomplete_format(h, min, start)); + min += step; + } + min = 0; + } + + // list the remaining hours till 23:00 + while (h < 24) + result.push(time_autocomplete_format((h++), 0, start)); + + return callback(result); + }; + + var time_autocomplete_open = function(event, ui) { + // scroll to current time + var $this = $(this), + widget = $this.autocomplete('widget') + menu = $this.data('ui-autocomplete').menu, + amregex = /^(.+)(a[.m]*)/i, + pmregex = /^(.+)(a[.m]*)/i, + val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1'); + + widget.css('width', '10em'); + + if (val === '') + menu._scrollIntoView(widget.children('li:first')); + else + widget.children().each(function() { + var li = $(this), + html = li.children().first().html() + .replace(/\s+\(.+\)$/, '') + .replace(amregex, '0:$1') + .replace(pmregex, '1:$1'); + + if (html.indexOf(val) == 0) + menu._scrollIntoView(li); + }); + }; + + /** + * Initializes time autocompletion + */ + this.init_time_autocomplete = function(elem, props) + { + var default_props = { + delay: 100, + minLength: 1, + appendTo: props.container, + source: time_autocomplete_list, + open: time_autocomplete_open, + // change: time_autocomplete_change, + select: function(event, ui) { + $(this).val(ui.item[0]).change(); + return false; + } + }; + + $(elem).attr('autocomplete', "off") + .autocomplete($.extend(default_props, props)) + .click(function() { // show drop-down upon clicks + $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " "); + }); + + $(elem).data('ui-autocomplete')._renderItem = function(ul, item) { + return $('<li>') + .data('ui-autocomplete-item', item) + .append('<a>' + item[0] + item[1] + '</a>') + .appendTo(ul); + }; + }; + + /***** 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(); + + var i, actions, adismiss, asnooze, alarm, html, + audio_alarms = [], records = [], 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); + + if (alarm.action == 'AUDIO') { + audio_alarms.push(alarm); + continue; + } + + 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(e){ + me.dismiss_link = $(this); + me.dismiss_alarm(me.dismiss_link.data('id'), 0, e); + }); + 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)); + + records.push($('<div>').addClass('alarm-item').html(html).append(actions)); + } + + if (audio_alarms.length) + this.audio_alarms(audio_alarms); + + if (!records.length) + return; + + this.alarm_dialog = $('<div>').attr('id', 'alarm-display').append(records); + + buttons[rcmail.gettext('close')] = function() { + $(this).dialog('close'); + }; + + buttons[rcmail.gettext('dismissall','libcalendaring')] = function(e) { + // submit dismissed event_ids to server + me.dismiss_alarm(me.alarm_ids.join(','), 0, e); + $(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; + }; + + /** + * Display a notification and play a sound for a set of alarms + */ + this.audio_alarms = function(alarms) + { + var elem, txt = [], + src = rcmail.assets_path('plugins/libcalendaring/alarm'), + plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {}; + + // first generate and display notification text + $.each(alarms, function() { txt.push(this.title); }); + + rcmail.display_message(rcmail.gettext('alarmtitle','libcalendaring') + ': ' + Q(txt.join(', ')), 'notice', 10000); + + // Internet Explorer does not support wav files, + // support in other browsers depends on enabled plugins, + // so we use wav as a fallback + src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav'; + + // HTML5 + try { + elem = $('<audio>').attr('src', src); + elem.get(0).play(); + } + // old method + catch (e) { + elem = $('<embed id="libcalsound" src="' + src + '" hidden=true autostart=true loop=false />'); + elem.appendTo($('body')); + window.setTimeout("$('#libcalsound').remove()", 10000); + } + }; + + /** + * 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, e); + 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', link.get(0), event); + 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, event) + { + rcmail.command('menu-close', 'alarm-snooze-dropdown', null, event); + 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); + } + + // resize and reposition (center) the dialog window + this.dialog_resize = function(id, height, width) + { + var win = $(window), w = win.width(), h = win.height(); + + $(id).dialog('option', { + height: Math.min(h-20, height+130), + width: Math.min(w-20, width+50) + }); + }; +} + +////// static methods + +// render HTML code for displaying an attendee record +rcube_libcalendaring.attendee_html = function(data) +{ + var name, tooltip = '', context = 'libcalendaring', + dispname = data.name || data.email, + status = data.role == 'ORGANIZER' ? 'ORGANIZER' : data.status; + + if (status) + status = status.toLowerCase(); + + if (data.email) { + tooltip = data.email; + name = $('<a>').attr({href: 'mailto:' + data.email, 'class': 'mailtolink', 'data-cutype': data.cutype}) + + if (status) + tooltip += ' (' + rcmail.gettext('status' + status, context) + ')'; + } + else { + name = $('<span>'); + } + + if (data['delegated-to']) + tooltip = rcmail.gettext('delegatedto', context) + ' ' + data['delegated-to']; + else if (data['delegated-from']) + tooltip = rcmail.gettext('delegatedfrom', context) + ' ' + data['delegated-from']; + + return $('<span>').append( + $('<span>').attr({'class': 'attendee ' + status, title: tooltip}).append(name.text(dispname)) + ).html(); +}; + +/** + * + */ +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); + + // highlight date if date change detected + if (p.resheduled) + $('.calendar-eventdetails td.date').addClass('modified'); + + // 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); + + 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); + } +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/libcalendaring.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,1762 @@ +<?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 $mail_ical_parser; + + /** + * Singleton getter to allow direct access from other plugins + */ + public static function get_instance() + { + if (!self::$instance) { + self::$instance = new libcalendaring(rcube::get_instance()->plugins); + self::$instance->init_instance(); + } + + return self::$instance; + } + + /** + * Initializes class properties + */ + public function init_instance() + { + $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); + } + + /** + * Required plugin startup method + */ + public function init() + { + self::$instance = $this; + + $this->rc = rcube::get_instance(); + $this->init_instance(); + + // 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'); + + $this->add_label( + 'itipaccepted', 'itiptentative', 'itipdeclined', + 'itipdelegated', 'expandattendeegroup', 'expandattendeegroupnodata', + 'statusorganizer', 'statusaccepted', 'statusdeclined', + 'statusdelegated', 'statusunknown', 'statusneeds-action', + 'statustentative', 'statuscompleted', 'statusin-process', + 'delegatedto', 'delegatedfrom' + ); + } + + 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 __DIR__ . '/libvcalendar.php'; + return new libvcalendar(); + } + + /** + * Load iTip functions + */ + public static function get_itip($domain = 'libcalendaring') + { + $self = self::get_instance(); + require_once __DIR__ . '/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 __DIR__ . '/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(); + $keys = array('date_format', 'time_format', 'date_short', 'date_long'); + + foreach ($keys as $key) { + $settings[$key] = (string)$this->rc->config->get('calendar_' . $key, $this->defaults['calendar_' . $key]); + $settings[$key] = str_replace('Y', 'y', $settings[$key]); + } + + $settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '—' " . $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 = $this->rc->format_date($event['start'], $date_format); + if (($todate = $this->rc->format_date($event['end'], $date_format)) != $fromto) + $fromto .= ' - ' . $todate; + } + else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) { + $fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) . + ' - ' . $this->rc->format_date($event['end'], $time_format); + } + else { + $fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) . + ' - ' . $this->rc->format_date($event['end'], $date_format) . ' ' . $this->rc->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']); + + $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_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id'])); + $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); + $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related')); + $object_type = $attrib['_type'] ?: 'event'; + + $select_type->add($this->gettext('none'), ''); + foreach ($alarm_types as $type) + $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type); + + foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) + $select_offset->add($this->gettext('trigger' . $trigger), $trigger); + + $select_offset->add($this->gettext('trigger0'), '0'); + if ($absolute_time) + $select_offset->add($this->gettext('trigger@'), '@'); + + $select_related->add($this->gettext('relatedstart'), 'start'); + $select_related->add($this->gettext('relatedend' . $object_type), 'end'); + + // 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]) . ' ' . + $select_related->show() . ' ' . + $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']; + $related = $alarm['related']; + } + + $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)) { + $r = strtoupper($related ?: 'start') == 'END' ? 'end' : ''; + // TODO: for all-day events say 'on date of event at XX' ? + if ($val[0] == 0) { + $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r); + } + else { + $label = 'libcalendaring.trigger' . $r . $val[1]; + $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label); + } + } + 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['related'] == 'END' ? $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, + 'action' => $alarm['action'], + '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) + { + $limit = 10; + $exdates = array(); + $format = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']); + $format = self::to_php_date_format($format); + $format_fn = function($dt) use ($format) { + return rcmail::get_instance()->format_date($dt, $format); + }; + + if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) { + $exdates = array_map($format_fn, $rrule['EXDATE']); + } + + if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { + $rdates = array_map($format_fn, $rrule['RDATE']); + + if (!empty($exdates)) { + $rdates = array_diff($rdates, $exdates); + } + + if (count($rdates) > $limit) { + $rdates = array_slice($rdates, 0, $limit); + $more = true; + } + + return $this->gettext('ondate') . ' ' . join(', ', $rdates) + . ($more ? '...' : ''); + } + + $output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1); + + switch ($rrule['FREQ']) { + case 'DAILY': + $output .= $this->gettext('days'); + break; + case 'WEEKLY': + $output .= $this->gettext('weeks'); + break; + case 'MONTHLY': + $output .= $this->gettext('months'); + break; + case 'YEARLY': + $output .= $this->gettext('years'); + break; + } + + if ($rrule['COUNT']) { + $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); + } + else if ($rrule['UNTIL']) { + $until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format); + } + else { + $until = $this->gettext('forever'); + } + + $output .= ', ' . $until; + + if (!empty($exdates)) { + if (count($exdates) > $limit) { + $exdates = array_slice($exdates, 0, $limit); + $more = true; + } + + $output = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates) + . ($more ? '...' : ''); + } + + return $output; + } + + /** + * 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() . ' ' . $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 = rcube::Q($this->rc->gettext('delete')); + } + + $content = html::a(array( + 'href' => "#delete", + 'class' => 'delete', + 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id), + 'title' => $this->rc->gettext('delete'), + 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'], + ), $button); + + $content .= rcube::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' => $this->rc->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' => $this->rc->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 rcmail_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='.rcube::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', rcube::Q($this->rc->gettext('filename'))); + $table->add('header', rcube::Q($this->attachment['name'])); + if ($dl_link) { + $table->add('download-link', html::a($dl_url, rcube::Q($this->rc->gettext('download')))); + } + } + + if (!empty($this->attachment['mimetype'])) { + $table->add('title', rcube::Q($this->rc->gettext('type'))); + $table->add('header', rcube::Q($this->attachment['mimetype'])); + } + + if (!empty($this->attachment['size'])) { + $table->add('title', rcube::Q($this->rc->gettext('filesize'))); + $table->add('header', rcube::Q($this->rc->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, $this->ical_message)) { + 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'] ?: RCUBE_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 + $from = rcube_mime::decode_address_list($this->ical_message->headers->from, 1, true, null, true); + $this->mail_ical_parser->sender = !empty($from) ? $from[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 = RCUBE_CHARSET; + + // establish imap connection + $imap = $this->rc->get_storage(); + $imap->set_folder($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 + $from = rcube_mime::decode_address_list($headers->from, 1, true, null, true); + $object['_sender'] = !empty($from) ? $from[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 + * @param rcube_message Message object + * + * @return boolean True if part is of type vcard + */ + public static function part_is_vcalendar($part, $message = null) + { + // First check if the message is "valid" (i.e. not multipart/report) + if ($message) { + $level = explode('.', $part->mime_id); + + while (array_pop($level) !== null) { + $parent = $message->mime_parts[join('.', $level) ?: 0]; + if ($parent->mimetype == 'multipart/report') { + return false; + } + } + } + + 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 + * @param bool All-day flag from the main event + * + * @return mixed Format string or null if identifier cannot be generated + */ + public static function recurrence_instance_identifier($event, $allday = null) + { + $instance_date = $event['recurrence_date'] ?: $event['start']; + + if ($instance_date && is_a($instance_date, 'DateTime')) { + // According to RFC5545 (3.8.4.4) RECURRENCE-ID format should + // be date/date-time depending on the main event type, not the exception + if ($allday === null) { + $allday = $event['allday']; + } + + return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis'); + } + } + + + /********* 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); + } + + /** + * Merge attendees of the old and new event version + * with keeping current user and his delegatees status + * + * @param array &$new New object data + * @param array $old Old object data + * @param bool $status New status of the current user + */ + public function merge_attendees(&$new, $old, $status = null) + { + if (empty($status)) { + $emails = $this->get_user_emails(); + $delegates = array(); + $attendees = array(); + + // keep attendee status of the current user + foreach ((array) $new['attendees'] as $i => $attendee) { + if (empty($attendee['email'])) { + continue; + } + + $attendees[] = $email = strtolower($attendee['email']); + + if (in_array($email, $emails)) { + foreach ($old['attendees'] as $_attendee) { + if ($attendee['email'] == $_attendee['email']) { + $new['attendees'][$i] = $_attendee; + if ($_attendee['status'] == 'DELEGATED' && ($email = $_attendee['delegated-to'])) { + $delegates[] = strtolower($email); + } + + break; + } + } + } + } + + // make sure delegated attendee is not lost + foreach ($delegates as $delegatee) { + if (!in_array($delegatee, $attendees)) { + foreach ((array) $old['attendees'] as $attendee) { + if ($attendee['email'] && ($email = strtolower($attendee['email'])) && $email == $delegatee) { + $new['attendees'][] = $attendee; + break; + } + } + } + } + } + + // We also make sure that status of any attendee + // is not overriden by NEEDS-ACTION if it was already set + // which could happen if you work with shared events + foreach ((array) $new['attendees'] as $i => $attendee) { + if ($attendee['email'] && $attendee['status'] == 'NEEDS-ACTION') { + foreach ($old['attendees'] as $_attendee) { + if ($attendee['email'] == $_attendee['email']) { + $new['attendees'][$i]['status'] = $_attendee['status']; + unset($new['attendees'][$i]['rsvp']); + break; + } + } + } + } + } + + + /********* 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', + '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', + )); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/libvcalendar.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,1439 @@ +<?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 \Sabre\VObject; +use \Sabre\VObject\DateTimeParser; + +/** + * Class to parse and build vCalendar (iCalendar) files + * + * Uses the Sabre VObject library, version 3.x. + * + */ +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', + 'schedule-status' => 'SCHEDULE-STATUS', + 'schedule-agent' => 'SCHEDULE-AGENT', + 'sent-by' => 'SENT-BY', + ); + private $organizer_keymap = array( + 'name' => 'CN', + 'schedule-status' => 'SCHEDULE-STATUS', + 'schedule-agent' => 'SCHEDULE-AGENT', + 'sent-by' => 'SENT-BY', + ); + 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 Sabre\VObject\Component object + * + * @param object Sabre\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->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; + + $value = strval($prop); + + 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'] = strval($prop) == 'TRANSPARENT' ? 'free' : 'busy'; + break; + + case 'STATUS': + if ($value == 'TENTATIVE') + $event['free_busy'] = 'tentative'; + else if ($value == 'CANCELLED') + $event['cancelled'] = true; + else if ($value == 'COMPLETED') + $event['complete'] = 100; + + $event['status'] = $value; + break; + + case 'COMPLETED': + if (self::convert_datetime($prop)) { + $event['status'] = 'COMPLETED'; + $event['complete'] = 100; + } + break; + + case 'PRIORITY': + if (is_numeric($value)) + $event['priority'] = $value; + break; + + case 'RRULE': + $params = is_array($event['recurrence']) ? $event['recurrence'] : array(); + // parse recurrence rule attributes + foreach ($prop->getParts() as $k => $v) { + $params[strtoupper($k)] = is_array($v) ? implode(',', $v) : $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($value)) { + $exdates = array_map(function($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true)); + $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], $exdates); + } + break; + + case 'RDATE': + if (!empty($value)) { + $rdates = array_map(function($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true)); + $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], $rdates); + } + 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'] = $value; + } + break; + + case 'SEQUENCE': + $event['sequence'] = intval($value); + break; + + case 'PERCENT-COMPLETE': + $event['complete'] = intval($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($value); + break; + + case 'X-MICROSOFT-CDO-BUSYSTATUS': + if ($value == 'OOF') + $event['free_busy'] = 'outofoffice'; + else if (in_array($value, array('FREE', 'BUSY', 'TENTATIVE'))) + $event['free_busy'] = strtolower($value); + break; + + case 'ATTENDEE': + case 'ORGANIZER': + $params = array('RSVP' => false); + foreach ($prop->parameters() as $pname => $pvalue) { + switch ($pname) { + case 'RSVP': $params[$pname] = strtolower($pvalue) == 'true'; break; + case 'CN': $params[$pname] = self::unescape($pvalue); break; + default: $params[$pname] = strval($pvalue); break; + } + } + $attendee = self::map_keys($params, array_flip($this->attendee_keymap)); + $attendee['email'] = preg_replace('!^mailto:!i', '', $value); + + if ($prop->name == 'ORGANIZER') { + $attendee['role'] = 'ORGANIZER'; + $attendee['status'] = 'ACCEPTED'; + $event['organizer'] = $attendee; + + if (array_key_exists('schedule-agent', $attendee)) { + $schedule_agent = $attendee['schedule-agent']; + } + } + else if ($attendee['email'] != $event['organizer']['email']) { + $event['attendees'][] = $attendee; + } + break; + + case 'ATTACH': + $params = self::parameters_array($prop); + if (substr($value, 0, 4) == 'http' && !strpos($value, ':attachment:')) { + $event['links'][] = $value; + } + else if (strlen($value) && strtoupper($params['VALUE']) == 'BINARY') { + $attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name', 'X-APPLE-FILENAME' => 'name')); + $attachment['data'] = $value; + $attachment['size'] = strlen($value); + $event['attachments'][] = $attachment; + } + break; + + default: + if (substr($prop->name, 0, 2) == 'X-') + $event['x-custom'][] = array($prop->name, strval($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') { + $event['allday'] = false; + + // check for all-day dates + if ($event['start']->_dateonly) { + $event['allday'] = true; + } + + // events may lack the DTEND property, set it to DTSTART (RFC5545 3.6.1) + if (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) { + $value = strval($prop); + + switch ($prop->name) { + case 'TRIGGER': + foreach ($prop->parameters as $param) { + if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') { + $trigger = '@' . $prop->getDateTime()->format('U'); + $alarm['trigger'] = $prop->getDateTime(); + } + else if ($param->name == 'RELATED') { + $alarm['related'] = $param->getValue(); + } + } + if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) { + $trigger = $values[2]; + } + + if (!$alarm['trigger']) { + $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $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($value); + break; + + case 'SUMMARY': + case 'DESCRIPTION': + case 'DURATION': + $alarm[strtolower($prop->name)] = self::convert_string($prop); + break; + + case 'REPEAT': + $alarm['repeat'] = intval($value); + break; + + case 'ATTENDEE': + $alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value); + break; + + case 'ATTACH': + $params = self::parameters_array($prop); + if (strlen($value) && (preg_match('/^[a-z]+:/', $value) || strtoupper($params['VALUE']) == 'URI')) { + // we only support URI-type of attachments here + $alarm['uri'] = $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']); + } + + // some iTip CANCEL messages only contain the start date + if (!$event['end'] && $event['start'] && $this->method == 'CANCEL') { + $event['end'] = clone $event['start']; + } + + // T2531: Remember SCHEDULE-AGENT in custom property to properly + // support event updates via CalDAV when SCHEDULE-AGENT=CLIENT is used + if (isset($schedule_agent)) { + $event['x-custom'][] = array('SCHEDULE-AGENT', $schedule_agent); + } + + // 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; + + $value = strval($prop); + + 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', '', $value); + break; + + case 'FREEBUSY': + // The freebusy component can hold more than 1 value, separated by commas. + $periods = explode(',', $value); + $fbtype = strval($prop['FBTYPE']) ?: 'BUSY'; + + // skip dupes + if ($seen[$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 = DateTimeParser::parse($busyStart); + $busyEnd = 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'] = $value; + } + } + + return $this->freebusy; + } + + /** + * + */ + public static function convert_string($prop) + { + return strval($prop); + } + + /** + * + */ + public static function unescape($prop) + { + return str_replace('\,', ',', strval($prop)); + } + + /** + * 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\iCalendar\DateTime) { + if (count($prop->getDateTimes()) > 1) { + $dt = array(); + $dateonly = !$prop->hasTime(); + foreach ($prop->getDateTimes() as $item) { + $item->_dateonly = $dateonly; + $dt[] = $item; + } + } + else { + $dt = $prop->getDateTime(); + if (!$prop->hasTime()) { + $dt->_dateonly = true; + } + } + } + else if ($prop instanceof VObject\Property\iCalendar\Period) { + $dt = array(); + foreach ($prop->getParts() as $val) { + try { + list($start, $end) = explode('/', $val); + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ($end[0] === 'P') { + $dur = DateTimeParser::parseDuration($end); + $end = clone $start; + $end->add($dur); + } + else { + $end = DateTimeParser::parseDateTime($end); + } + $dt[] = array($start, $end); + } + 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 Sabre\VObject\Property instance from a PHP DateTime object + * + * @param object VObject\Document parent node to create property for + * @param string Property name + * @param object DateTime + * @param boolean Set as UTC date + * @param boolean Set as VALUE=DATE property + */ + public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null, $set_type = false) + { + if ($utc) { + $dt->setTimeZone(new \DateTimeZone('UTC')); + $is_utc = true; + } + else { + $is_utc = ($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')); + } + $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly; + $vdt = $cal->createProperty($name, $dt, null, $is_dateonly ? 'DATE' : 'DATE-TIME'); + + if ($is_dateonly) { + $vdt['VALUE'] = 'DATE'; + } + else if ($set_type) { + $vdt['VALUE'] = 'DATE-TIME'; + } + + // 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 $name => $value) { + $params[strtoupper($name)] = strval($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 = new VObject\Component\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], $vcal); + 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'; + + $cal = $vcal ?: new VObject\Component\VCalendar(); + $ve = $cal->create($this->type_component_map[$type]); + $ve->UID = $event['uid']; + + // set DTSTAMP according to RFC 5545, 3.8.7.2. + $dtstamp = !empty($event['changed']) && empty($this->method) ? $event['changed'] : new DateTime('now', new \DateTimeZone('UTC')); + $ve->DTSTAMP = $this->datetime_prop($cal, '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($cal, 'CREATED', $event['created'], true)); + if (!empty($event['changed'])) + $ve->add($this->datetime_prop($cal, 'LAST-MODIFIED', $event['changed'], true)); + if (!empty($event['start'])) + $ve->add($this->datetime_prop($cal, 'DTSTART', $event['start'], false, (bool)$event['allday'])); + if (!empty($event['end'])) + $ve->add($this->datetime_prop($cal, 'DTEND', $event['end'], false, (bool)$event['allday'])); + if (!empty($event['due'])) + $ve->add($this->datetime_prop($cal, '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($cal, '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($cal, 'LOCATION', $event['location']) : $cal->create('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($this->datetime_prop($cal, 'EXDATE', $exd, true)); + } + } + } + // add RDATEs + if (!empty($rdates)) { + foreach ((array)$rdates as $rdate) { + $ve->add($this->datetime_prop($cal, 'RDATE', $rdate)); + } + } + } + + if ($event['categories']) { + $cat = $cal->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 and BusyCal required the COMPLETED date to be set in order to consider a task complete + if ($event['status'] == 'COMPLETED' || $event['complete'] == 100) { + $ve->add($this->datetime_prop($cal, 'COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true)); + } + + if ($event['valarms']) { + foreach ($event['valarms'] as $alarm) { + $va = $cal->createComponent('VALARM'); + $va->action = $alarm['action']; + if ($alarm['trigger'] instanceof DateTime) { + $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true)); + } + else { + $alarm_props = array(); + if (strtoupper($alarm['related']) == 'END') { + $alarm_props['RELATED'] = 'END'; + } + $va->add('TRIGGER', $alarm['trigger'], $alarm_props); + } + + 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 = $cal->createComponent('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($cal, 'TRIGGER', $val[0], true, null, true)); + $ve->add($va); + } + + // Find SCHEDULE-AGENT + foreach ((array)$event['x-custom'] as $prop) { + if ($prop[0] === 'SCHEDULE-AGENT') { + $schedule_agent = $prop[1]; + } + } + + 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; + + $mailto = $attendee['email']; + $attendee = array_filter(self::map_keys($attendee, $this->attendee_keymap)); + + if ($schedule_agent !== null && !isset($attendee['SCHEDULE-AGENT'])) { + $attendee['SCHEDULE-AGENT'] = $schedule_agent; + } + + $ve->add('ATTENDEE', 'mailto:' . $mailto, $attendee); + } + } + + if ($event['organizer']) { + $organizer = array_filter(self::map_keys($event['organizer'], $this->organizer_keymap)); + + if ($schedule_agent !== null && !isset($organizer['SCHEDULE-AGENT'])) { + $organizer['SCHEDULE-AGENT'] = $schedule_agent; + } + + $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], $organizer); + } + + 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', + $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($cal, '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 Sabre\VObject\Component object representing a VTIMEZONE definition + * or false if no timezone information is available + */ + public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null) + { + if (!$from) $from = time(); + if (!$to) $to = $from; + if (!$cal) $cal = new VObject\Component\VCalendar(); + + 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 = $cal->createComponent('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 = $cal->createComponent('DAYLIGHT'); + $cmp = $dst; + } + else { + $t_std = $trans['ts']; + $std = $cal->createComponent('STANDARD'); + $cmp = $std; + } + + if ($cmp) { + $dt = new DateTime($trans['time']); + $offset = $trans['offset'] / 3600; + + $cmp->DTSTART = $dt->format('Ymd\THis'); + $cmp->TZOFFSETFROM = sprintf('%+03d%02d', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60); + $cmp->TZOFFSETTO = sprintf('%+03d%02d', 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 Sabre\VObject\Property\Text that quotes commas in the location property + * because Apple clients treat that property as list. + */ +class vobject_location_property extends VObject\Property\Text +{ + /** + * List of properties that are considered 'structured'. + * + * @var array + */ + protected $structuredValues = array( + // vCard + 'N', + 'ADR', + 'ORG', + 'GENDER', + 'LOCATION', + // iCalendar + 'REQUEST-STATUS', + ); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/bg_BG.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,30 @@ +<?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['statusorganizer'] = 'ĐŃĐłĐ°ĐœĐžĐ·Đ°ŃĐŸŃ'; +$labels['statustentative'] = 'ĐŃДЎĐČĐ°ŃĐžŃĐ”Đ»ĐœĐŸ'; +$labels['statusneeds-action'] = 'ĐŃĐ¶ĐœĐŸ Đ” ĐŽĐ”ĐčŃŃĐČОД'; +$labels['statusunknown'] = 'ĐŃĐŒĐ° ĐžĐœŃĐŸŃĐŒĐ°ŃĐžŃ'; +$labels['statuscompleted'] = 'ĐĐ°ĐČŃŃŃĐ”Đœ'; +$labels['statusin-process'] = 'Đ ĐżŃĐŸŃĐ”Ń'; +$labels['itipcancellation'] = 'ĐŃĐșĐ°Đ·Đ°ĐœĐŸ:'; +$labels['itipreply'] = 'ĐŃĐłĐŸĐČĐŸŃ ĐœĐ°'; +$labels['itipaccepted'] = 'ĐŃĐžĐ”ĐŒĐ°ĐœĐ”'; +$labels['itiptentative'] = 'ĐĐŸĐ¶Đ” бО'; +$labels['itipdeclined'] = 'ĐŃŃ ĐČŃŃĐ»ŃĐœĐ”'; +$labels['itipsubjectaccepted'] = '"$title" бДŃĐ” ĐżŃОДŃĐŸ ĐŸŃ $name'; +$labels['itipsubjectdeclined'] = '"$title" бДŃĐ” ĐŸŃŃ ĐČŃŃĐ»Đ”ĐœĐŸ ĐŸŃ $name'; +$labels['updateattendeestatus'] = 'ĐĐŸĐŽĐœĐŸĐČŃĐČĐ°ĐœĐ” ĐœĐ° ŃŃĐ°ŃŃŃŃŃ ĐœĐ° ŃŃĐ°ŃŃĐœĐžĐșĐ°'; +$labels['acceptinvitation'] = 'ĐŃĐžĐ”ĐŒĐ°ŃĐ” лО ŃĐ°Đ·Đž ĐżĐŸĐșĐ°ĐœĐ°?'; +$labels['youhaveaccepted'] = 'ĐОД ŃŃĐ” ĐżŃОДлО ŃĐ°Đ·Đž ĐżĐŸĐșĐ°ĐœĐ°'; +$labels['importtocalendar'] = 'ĐапазĐČĐ°ĐœĐ” ĐČ ĐŒĐŸŃ ĐșĐ°Đ»Đ”ĐœĐŽĐ°Ń'; +$labels['removefromcalendar'] = 'ĐŃĐ”ĐŒĐ°Ń ĐČĐ°ĐœĐ” ĐŸŃ ĐŒĐŸŃ ĐșĐ°Đ»Đ”ĐœĐŽĐ°Ń'; +$labels['savingdata'] = 'ĐапазĐČĐ°ĐœĐ” ĐœĐ° ĐŽĐ°ĐœĐœĐž...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/ca_ES.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,20 @@ +<?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['statusorganizer'] = 'Organitzador'; +$labels['statustentative'] = 'Provisional'; +$labels['statusneeds-action'] = 'Necessita una acció'; +$labels['statusunknown'] = 'Desconegut'; +$labels['statuscompleted'] = 'Completat'; +$labels['statusin-process'] = 'En procés'; +$labels['savingdata'] = 'S\'estan desant les dades...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/cs_CZ.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = '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['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['statusorganizer'] = 'PoĆadatel'; +$labels['statusaccepted'] = 'PĆijato'; +$labels['statustentative'] = 'NezĂĄvaznÄ'; +$labels['statusdeclined'] = 'OdmĂtnuto'; +$labels['statusdelegated'] = 'SvÄĆeno'; +$labels['statusneeds-action'] = 'PotĆebuje Äinnost'; +$labels['statusunknown'] = 'NeznĂĄmĂœ'; +$labels['statuscompleted'] = 'Hotovo'; +$labels['statusin-process'] = 'RozpracovĂĄno'; +$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.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/da_DK.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,146 @@ +<?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['alarmaudio'] = 'Afspil lyd'; +$labels['alarmdisplayoption'] = 'Besked'; +$labels['alarmemailoption'] = 'E-post'; +$labels['alarmaudiooption'] = 'Lyd'; +$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['addalarm'] = 'TilfÞj alarm'; +$labels['removealarm'] = 'Fjern alarm'; +$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['rdate'] = 'pÄ datoer'; +$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['untilenddate'] = 'indtil datoen'; +$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['except'] = 'undtagen'; +$labels['statusorganizer'] = 'Organisator'; +$labels['statusaccepted'] = 'Accepteret'; +$labels['statustentative'] = 'ForsÞgsvis'; +$labels['statusdeclined'] = 'Afvist'; +$labels['statusneeds-action'] = 'Needs action'; +$labels['statusunknown'] = 'Ukendt'; +$labels['statuscompleted'] = 'Completed'; +$labels['statusin-process'] = 'In process'; +$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['itipneeds-action'] = 'UdsÊt'; +$labels['itipcomment'] = 'Dit svar'; +$labels['itipeditresponse'] = 'Angiv en svartekst'; +$labels['itipsendercomment'] = 'Afsenders kommentar:'; +$labels['itipsuppressreply'] = 'Undlad at sende et svar'; +$labels['itipobjectnotfound'] = 'Objektet som denne besked refererer til, blev ikke fundet i din konto.'; +$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['itipsubjectin-process'] = '"$title" er en igangvÊrende proces hos $name'; +$labels['itipsubjectcompleted'] = '"$title" blev fuldfÞrt af $name'; +$labels['itipsubjectcancel'] = 'Din deltagelse i "$title" er blevet annulleret'; +$labels['itipsubjectdelegated'] = '"$title" er blevet delegeret af $name'; +$labels['itipsubjectdelegatedto'] = '"$title" er blevet delegeret til dig af $name'; +$labels['itipnewattendee'] = 'Dette er et svar fra en ny deltager'; +$labels['updateattendeestatus'] = 'Opdatér status for deltagere'; +$labels['acceptinvitation'] = 'Vil du acceptere denne invitation?'; +$labels['acceptattendee'] = 'Acceptér deltager'; +$labels['declineattendee'] = 'Afvis deltager'; +$labels['declineattendeeconfirm'] = 'Indtast en besked til den afviste deltager (valgfrit):'; +$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['youhavedelegated'] = 'Du har delegeret denne invitation'; +$labels['youhavein-process'] = 'Du arbejder pÄ denne tildelte opgave'; +$labels['youhavecompleted'] = 'Du har afsluttet denne tildelte opgave'; +$labels['youhaveneeds-action'] = 'Dit svar til denne invitation afventes stadig'; +$labels['youhavepreviouslyaccepted'] = 'Du har tidligere accepteret denne invitation'; +$labels['youhavepreviouslytentative'] = 'Du har tidligere accepteret denne invitation tentativt'; +$labels['youhavepreviouslydeclined'] = 'Du har tidligere afvist denne invitation'; +$labels['youhavepreviouslydelegated'] = 'Du har tidligere delegeret denne invitation'; +$labels['youhavepreviouslyin-process'] = 'Du har tidligere rapporteret, at du vil arbejdet pÄ denne tildelte opgave'; +$labels['youhavepreviouslycompleted'] = 'Du har tidligere afsluttet denne tildelte opgave'; +$labels['youhavepreviouslyneeds-action'] = 'Du svar til denne invitation afventes stadig'; +$labels['attendeeaccepted'] = 'Deltageren er blevet accepteret'; +$labels['attendeetentative'] = 'Deltageren er tentativt blevet accepteret'; +$labels['attendeedeclined'] = 'Deltageren er blevet afvist'; +$labels['attendeedelegated'] = 'Deltageren har delegeret til $delegatedto'; +$labels['attendeein-process'] = 'Deltager i under behandling'; +$labels['attendeecompleted'] = 'Deltageren er afsluttet'; +$labels['notanattendee'] = 'Du har ikke opfÞrt en deltager for dette objekt'; +$labels['outdatedinvitation'] = 'Denne invitation er blevet erstattet af en nyere version'; +$labels['importtocalendar'] = 'Gem i min kalender'; +$labels['removefromcalendar'] = 'Fjern fra min kalender'; +$labels['updatemycopy'] = 'Opdatér min kopi'; +$labels['openpreview'] = 'à bn forhÄndsvisning'; +$labels['deleteobjectconfirm'] = 'Sikker pÄ at du vil slette dette objekt?'; +$labels['declinedeleteconfirm'] = 'Vil du ogsÄ slette dette afviste objekt fra din konto?'; +$labels['delegateinvitation'] = 'Delegér invitation'; +$labels['delegateto'] = 'Delegér til'; +$labels['delegatersvpme'] = 'Hold mig orienteret om opdateringer vedrÞrende denne hÊndelse'; +$labels['delegateinvalidaddress'] = 'Angiv venligst en gyldig e-mailadresse for den delegerede'; +$labels['savingdata'] = 'Gemmer data...'; +$labels['expandattendeegroup'] = 'Substituér med gruppemedlemmer'; +$labels['expandattendeegroupnodata'] = 'Kan ikke substituere denne gruppe. Fandt ikke medlemmer.'; +$labels['expandattendeegrouperror'] = 'Kan ikke substituere denne gruppe. Den kan indeholde for mange medlemmer.'; +$labels['expandattendeegroupsizelimit'] = 'Denne gruppe indeholder for mange medlemmer til substituering.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/de_CH.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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 am'; +$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['triggerend-M'] = 'Minuten vor dem Ende'; +$labels['triggerend-H'] = 'Stunden vor dem Ende'; +$labels['triggerend-D'] = 'Tage vor dem Ende'; +$labels['triggerend+M'] = 'Minuten nach dem Ende'; +$labels['triggerend+H'] = 'Stunden nach dem Ende'; +$labels['triggerend+D'] = 'Tage nach dem Ende'; +$labels['trigger0'] = 'um exakt'; +$labels['triggerattime'] = 'zur Startzeit'; +$labels['triggerattimeend'] = 'zur Endzeit'; +$labels['relatedstart'] = 'Beginn'; +$labels['relatedendevent'] = 'Ende'; +$labels['relatedendtask'] = 'Frist'; +$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'] = 'genau um'; +$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['statusorganizer'] = 'Organisator'; +$labels['statusaccepted'] = 'Akzeptiert'; +$labels['statustentative'] = 'Mit Vorbehalt'; +$labels['statusdeclined'] = 'Abgelehnt'; +$labels['statusdelegated'] = 'Delegiert'; +$labels['statusneeds-action'] = 'Braucht Aktion'; +$labels['statusunknown'] = 'Unbekannt'; +$labels['statuscompleted'] = 'Abgeschlossen'; +$labels['statusin-process'] = 'In Bearbeitung'; +$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['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['itipsingleoccurrence'] = 'Dieses ist eine <em>einzelne Wiederholung</emauĂerhalb einer Serie von Ereignissen'; +$labels['itipfutureoccurrence'] = 'Bezieht sich auf <em>diese and alle zukĂŒnftigen Wiederholungen</emeiner Serie von Ereignissen'; +$labels['itipmessagesingleoccurrence'] = 'Diese Nachricht bezieht sich nur auf eine einzelne Wiederholung'; +$labels['itipmessagefutureoccurrence'] = 'Diese Nachricht bezieht sich auf diese und alle zukĂŒnftigen Wiederholungen'; +$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.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/de_DE.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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 am'; +$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['triggerend-M'] = 'Minuten vor dem Ende'; +$labels['triggerend-H'] = 'Stunden vor dem Ende'; +$labels['triggerend-D'] = 'Tage vor dem Ende'; +$labels['triggerend+M'] = 'Minuten nach dem Ende'; +$labels['triggerend+H'] = 'Stunden nach dem Ende'; +$labels['triggerend+D'] = 'Tage nach dem Ende'; +$labels['trigger0'] = 'um exakt'; +$labels['triggerattime'] = 'zur Startzeit'; +$labels['triggerattimeend'] = 'zur Endzeit'; +$labels['relatedstart'] = 'Beginn'; +$labels['relatedendevent'] = 'Ende'; +$labels['relatedendtask'] = 'FĂ€llig'; +$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'] = 'genau am'; +$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['statusorganizer'] = 'Organisator'; +$labels['statusaccepted'] = 'Akzeptiert'; +$labels['statustentative'] = 'Mit Vorbehalt'; +$labels['statusdeclined'] = 'Abgelehnt'; +$labels['statusdelegated'] = 'Delegiert'; +$labels['statusneeds-action'] = 'Braucht Aktion'; +$labels['statusunknown'] = 'Unbekannt'; +$labels['statuscompleted'] = 'Abgeschlossen'; +$labels['statusin-process'] = 'In Bearbeitung'; +$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['itipsingleoccurrence'] = 'Dieses ist eine <em>einzelne Wiederholung</em> einer Serie von Ereignissen'; +$labels['itipfutureoccurrence'] = 'Bezieht sich auf <em>diese and alle zukĂŒnftigen Wiederholungen</em> einer Serie von Ereignissen'; +$labels['itipmessagesingleoccurrence'] = 'Diese Nachricht bezieht sich nur auf eine einzelne Wiederholung'; +$labels['itipmessagefutureoccurrence'] = 'Diese Nachricht bezieht sich auf diese und alle zukĂŒnftigen Wiederholungen'; +$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.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/en_US.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,186 @@ +<?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['triggerend-M'] = 'minutes before end'; +$labels['triggerend-H'] = 'hours before end'; +$labels['triggerend-D'] = 'days before end'; +$labels['triggerend+M'] = 'minutes after end'; +$labels['triggerend+H'] = 'hours after end'; +$labels['triggerend+D'] = 'days after end'; +$labels['trigger0'] = 'on time'; +$labels['triggerattime'] = 'at start time'; +$labels['triggerattimeend'] = 'at end time'; +$labels['relatedstart'] = 'start'; +$labels['relatedendevent'] = 'end'; +$labels['relatedendtask'] = 'due 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'; + +$labels['statusorganizer'] = 'Organizer'; +$labels['statusaccepted'] = 'Accepted'; +$labels['statustentative'] = 'Tentative'; +$labels['statusdeclined'] = 'Declined'; +$labels['statusdelegated'] = 'Delegated'; +$labels['statusneeds-action'] = 'Needs action'; +$labels['statusunknown'] = 'Unknown'; +$labels['statuscompleted'] = 'Completed'; +$labels['statusin-process'] = 'In process'; + +// 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.'; +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/es_AR.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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['triggerend-M'] = 'minutos antes de finalizar'; +$labels['triggerend-H'] = 'horas antes de finalizar'; +$labels['triggerend-D'] = 'dĂas antes de finalizar'; +$labels['triggerend+M'] = 'minutos despuĂ©s de finalizar'; +$labels['triggerend+H'] = 'horas despuĂ©s de finalizar'; +$labels['triggerend+D'] = 'dĂas despues de finalizar'; +$labels['trigger0'] = 'en hora'; +$labels['triggerattime'] = 'en hora de inicio'; +$labels['triggerattimeend'] = 'en hora de finalizaciĂłn'; +$labels['relatedstart'] = 'inicio'; +$labels['relatedendevent'] = 'fin'; +$labels['relatedendtask'] = 'fecha de vencimiento'; +$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['recurring'] = 'Repite'; +$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['statusorganizer'] = 'Organizador'; +$labels['statusaccepted'] = 'Aceptado'; +$labels['statustentative'] = 'Tentativo'; +$labels['statusdeclined'] = 'Rechazado'; +$labels['statusdelegated'] = 'Delegado'; +$labels['statusneeds-action'] = 'Necesita acciĂłn'; +$labels['statusunknown'] = 'Desconocido'; +$labels['statuscompleted'] = 'Completo'; +$labels['statusin-process'] = 'En proceso'; +$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['itipsubjectdelegated'] = '"$title" ha sido delegado por $name'; +$labels['itipsubjectdelegatedto'] = '"$title" ha sido delegado a usted por $name'; +$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['rsvpmodeall'] = 'La serie completa'; +$labels['rsvpmodecurrent'] = 'Esta ocurrencia sola'; +$labels['rsvpmodefuture'] = 'Esta y futuras ocurrencias'; +$labels['itipsingleoccurrence'] = 'Esta es una <em>ocurrencia individual</em> de una serie de eventos'; +$labels['itipfutureoccurrence'] = 'Refiere a <em>esta y todas las futuras ocurrencias</em> de una serie de eventos'; +$labels['itipmessagesingleoccurrence'] = 'El mensaje solamente refiere a esta ocurrencia individual'; +$labels['itipmessagefutureoccurrence'] = 'El mensaje refiere a esta y todas las futuras ocurrencias'; +$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['delegateinvitation'] = 'Delegar InvitaciĂłn'; +$labels['delegateto'] = 'Delegar a'; +$labels['delegatersvpme'] = 'Mantenerme informado acerca de las actualizaciones de esta incidencia'; +$labels['delegateinvalidaddress'] = 'Por favor, ingrese una direcciĂłn de correo electrĂłnico vĂĄlida para la delegaciĂłn'; +$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.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/es_ES.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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['alarmaudio'] = 'Reproducir sonido'; +$labels['alarmdisplayoption'] = 'Message'; +$labels['alarmemailoption'] = 'Correo electrĂłnico'; +$labels['alarmaudiooption'] = 'Sonido'; +$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['triggerend-M'] = 'minutos para finalizar'; +$labels['triggerend-H'] = 'horas para finalizar'; +$labels['triggerend-D'] = 'dĂas para finalizar'; +$labels['triggerend+M'] = 'minutos tras finalizar'; +$labels['triggerend+H'] = 'horas tras finalizar'; +$labels['triggerend+D'] = 'dĂas tras finalizar'; +$labels['trigger0'] = 'a tiempo'; +$labels['triggerattime'] = 'al iniciar'; +$labels['triggerattimeend'] = 'al finalizar'; +$labels['relatedstart'] = 'iniciar'; +$labels['relatedendevent'] = 'fin'; +$labels['relatedendtask'] = 'Plazo estipulado'; +$labels['addalarm'] = 'Añadir alarma'; +$labels['removealarm'] = 'Eliminar alarma'; +$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['recurring'] = 'Repeticiones'; +$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'] = 'año(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'] = 'hasta la fecha'; +$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'] = 'excepto'; +$labels['statusorganizer'] = 'Organizador'; +$labels['statusaccepted'] = 'Aceptada'; +$labels['statustentative'] = 'Provisional'; +$labels['statusdeclined'] = 'Rechazado'; +$labels['statusdelegated'] = 'Delgado'; +$labels['statusneeds-action'] = 'Necesita acciĂłn'; +$labels['statusunknown'] = 'Desconocido'; +$labels['statuscompleted'] = 'Realizado'; +$labels['statusin-process'] = 'En proceso'; +$labels['itipinvitation'] = 'InvitaciĂłn para'; +$labels['itipupdate'] = 'ActualizaciĂłn de'; +$labels['itipcancellation'] = 'Cancelado:'; +$labels['itipreply'] = 'Responder a'; +$labels['itipaccepted'] = 'Aceptar'; +$labels['itiptentative'] = 'Tal vez'; +$labels['itipdeclined'] = 'Declinar'; +$labels['itipdelegated'] = 'Delegado'; +$labels['itipneeds-action'] = 'Posponer'; +$labels['itipcomment'] = 'Su respuesta'; +$labels['itipeditresponse'] = 'Introducir un texto de respuesta'; +$labels['itipsendercomment'] = 'Comentario del remitente:'; +$labels['itipsuppressreply'] = 'No enviar una respuesta'; +$labels['itipobjectnotfound'] = 'El objeto referido en este mensaje no se encontrĂł en su cuenta.'; +$labels['itipsubjectaccepted'] = '$name ha aceptado el "$title"'; +$labels['itipsubjecttentative'] = '$name aceptado provisionalmente el "$title"'; +$labels['itipsubjectdeclined'] = '$name ha declinado el "$title"'; +$labels['itipsubjectin-process'] = '$name estĂĄ procesando el "$title"'; +$labels['itipsubjectcompleted'] = '$name ha completado el "$title"'; +$labels['itipsubjectcancel'] = 'Se ha cancelado su participaciĂłn en "$title'; +$labels['itipsubjectdelegated'] = '$name ha delegado el "$title"'; +$labels['itipsubjectdelegatedto'] = '$name ha delegado en ti el "$title"'; +$labels['itipnewattendee'] = 'Esta es la respuesta de un nuevo participante'; +$labels['updateattendeestatus'] = 'Actualizar el estado del participante'; +$labels['acceptinvitation'] = 'ÂżAcepta esta inviitaciĂłn?'; +$labels['acceptattendee'] = 'Aceptar participante'; +$labels['declineattendee'] = 'Declinar participante'; +$labels['declineattendeeconfirm'] = 'Introduzca un mensaje para el participante rechazado (opcional):'; +$labels['rsvpmodeall'] = 'Las series completas'; +$labels['rsvpmodecurrent'] = 'Este caso sĂłlo'; +$labels['rsvpmodefuture'] = 'Este y los futuros casos'; +$labels['itipsingleoccurrence'] = 'Se trata de un <em>caso aislado</ em> en una serie de eventos'; +$labels['itipfutureoccurrence'] = 'Se refiere a <em>este y todos los casos futuros</ em> en una serie de eventos'; +$labels['itipmessagesingleoccurrence'] = 'El mensaje solo hace referencia a este caso aislado'; +$labels['itipmessagefutureoccurrence'] = 'El mensaje hace referencia a este y todos los casos futuros'; +$labels['youhaveaccepted'] = 'Ha aceptado esta invitaciĂłn'; +$labels['youhavetentative'] = 'Ha aceptado esta invitaciĂłn provisionalmente'; +$labels['youhavedeclined'] = 'Ha declinado esta invitaciĂłn'; +$labels['youhavedelegated'] = 'Ha delegado esta invitaciĂłn'; +$labels['youhavein-process'] = 'EstĂĄ trabajando en esta asignaciĂłn'; +$labels['youhavecompleted'] = 'Ha completado esta asignaciĂłn'; +$labels['youhaveneeds-action'] = 'Sigue pendiente su respuesta a esta convitaciĂłn'; +$labels['youhavepreviouslyaccepted'] = 'Ha aceptado esta invitaciĂłn anteriormente'; +$labels['youhavepreviouslytentative'] = 'Ya ha aceptado esta invitaciĂłn provisionalmente'; +$labels['youhavepreviouslydeclined'] = 'Ha declinado esta invitaciĂłn anteriormente'; +$labels['youhavepreviouslydelegated'] = 'Ha adelegado esta invitaciĂłn anteriormente'; +$labels['youhavepreviouslyin-process'] = 'Ya ha informado anteriormente para trabajar en esta asignaciĂłn'; +$labels['youhavepreviouslycompleted'] = 'Ya ha completado esta asignaciĂłn'; +$labels['youhavepreviouslyneeds-action'] = 'Sigue pendiente su respuesta a esta invitaciĂłn'; +$labels['attendeeaccepted'] = 'El participante ha aceptado'; +$labels['attendeetentative'] = 'El participante ha aceptado provisionalmente'; +$labels['attendeedeclined'] = 'El participante ha declinado'; +$labels['attendeedelegated'] = 'El participante ha delegado en $delegatedto'; +$labels['attendeein-process'] = 'El participante estĂĄ en proceso'; +$labels['attendeecompleted'] = 'El participante ha completado'; +$labels['notanattendee'] = 'Usted no estĂĄ en la lista como asistente de este proyecto'; +$labels['outdatedinvitation'] = 'Esta invitaciĂłn se ha sustituido 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 previa'; +$labels['deleteobjectconfirm'] = 'ÂżEsta seguro de eliminar este objeto?'; +$labels['declinedeleteconfirm'] = 'ÂżTambiĂ©n desea eliminar este objeto declinado del calendario?'; +$labels['delegateinvitation'] = 'Delegar invitaciĂłn'; +$labels['delegateto'] = 'Delegar en'; +$labels['delegatersvpme'] = 'Mantenerme informado de las actualizaciones de esta incidencia'; +$labels['delegateinvalidaddress'] = 'Introduzca una direcciĂłn de correo electrĂłnico vĂĄlida para delegar'; +$labels['savingdata'] = 'Guardando datos...'; +$labels['expandattendeegroup'] = 'Sustituir con los miembros del grupo'; +$labels['expandattendeegroupnodata'] = 'No puede sustituirse este grupo. No se han encontrado miembros.'; +$labels['expandattendeegrouperror'] = 'No puede sustituirse este grupo. Tal vez tenga demasiados miembros.'; +$labels['expandattendeegroupsizelimit'] = 'Este grupo contiene demasiados miembros que sustituir.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/et_EE.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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['alarmemailoption'] = 'E-post'; +$labels['statusorganizer'] = 'Organizer'; +$labels['statustentative'] = 'Tentative'; +$labels['statusneeds-action'] = 'Needs action'; +$labels['statusunknown'] = 'Unknown'; +$labels['statuscompleted'] = 'Completed'; +$labels['statusin-process'] = 'In process';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/fi_FI.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,143 @@ +<?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['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['statusorganizer'] = 'JĂ€rjestĂ€jĂ€'; +$labels['statusaccepted'] = 'HyvĂ€ksytty'; +$labels['statustentative'] = 'Alustava'; +$labels['statusdeclined'] = 'KieltĂ€ydytty'; +$labels['statusneeds-action'] = 'Vaatii toimenpiteitĂ€'; +$labels['statusunknown'] = 'Tuntematon'; +$labels['statuscompleted'] = 'Valmis'; +$labels['statusin-process'] = 'Prosessissa'; +$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['rsvpmodeall'] = 'Koko sarja'; +$labels['rsvpmodecurrent'] = 'Vain tĂ€mĂ€ esiintymĂ€'; +$labels['rsvpmodefuture'] = 'TĂ€mĂ€ ja tulevat esiintymĂ€t'; +$labels['itipsingleoccurrence'] = 'TĂ€mĂ€ on <em>yksittĂ€inen esiintymĂ€</em> tapahtumasarjassa'; +$labels['itipfutureoccurrence'] = 'Viittaa <em>tĂ€hĂ€n ja kaikkiin tuleviin esiintymiin</em> tapahtumasarjassa'; +$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';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/fr_FR.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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 e-mail'; +$labels['alarmdisplay'] = 'Voir le message'; +$labels['alarmaudio'] = 'Alerte sonore'; +$labels['alarmdisplayoption'] = 'Message'; +$labels['alarmemailoption'] = 'E-mail'; +$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['triggerend-M'] = 'minutes avant la fin'; +$labels['triggerend-H'] = 'heures avant la fin'; +$labels['triggerend-D'] = 'jours avant la fin'; +$labels['triggerend+M'] = 'minutes aprĂšs la fin'; +$labels['triggerend+H'] = 'heures aprĂšs la fin'; +$labels['triggerend+D'] = 'jours aprĂšs la fin'; +$labels['trigger0'] = 'dans les temps'; +$labels['triggerattime'] = 'au dĂ©but'; +$labels['triggerattimeend'] = 'Ă la fin'; +$labels['relatedstart'] = 'dĂ©but'; +$labels['relatedendevent'] = 'fin'; +$labels['relatedendtask'] = 'date de fin'; +$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['recurring'] = 'RĂ©pĂ©titions'; +$labels['frequency'] = 'RĂ©pĂ©ter'; +$labels['never'] = 'jamais'; +$labels['daily'] = 'quotidienne'; +$labels['weekly'] = 'hebdomadaire'; +$labels['monthly'] = 'mensuelle'; +$labels['yearly'] = 'annuelle'; +$labels['rdate'] = 'Ă certaines 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'] = 'Ajoutez une date rĂ©pĂ©tĂ©e'; +$labels['except'] = 'sauf'; +$labels['statusorganizer'] = 'Organisateur'; +$labels['statusaccepted'] = 'AcceptĂ©'; +$labels['statustentative'] = 'Provisoire'; +$labels['statusdeclined'] = 'RefusĂ©'; +$labels['statusdelegated'] = 'DĂ©lĂ©guĂ©'; +$labels['statusneeds-action'] = 'Action exigĂ©e'; +$labels['statusunknown'] = 'Inconnu'; +$labels['statuscompleted'] = 'TerminĂ©e'; +$labels['statusin-process'] = 'En cours'; +$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 auquel il est fait rĂ©fĂ©rence dans ce message n\'a pas Ă©tĂ© trouvĂ© dans votre compte.'; +$labels['itipsubjectaccepted'] = '"$title" a Ă©tĂ© acceptĂ© par $name'; +$labels['itipsubjecttentative'] = '"$title" a provisoirement Ă©tĂ© acceptĂ© 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Ă© rempli 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'] = 'Voici la rĂ©ponse d\'un nouveau participant'; +$labels['updateattendeestatus'] = 'Modifier le statut du participant'; +$labels['acceptinvitation'] = 'Acceptez-vous cette invitation ?'; +$labels['acceptattendee'] = 'Accepter l\'utilisateur'; +$labels['declineattendee'] = 'Refuser l\'utilisateur'; +$labels['declineattendeeconfirm'] = 'Saisissez un message pour l\'utilisateur refusĂ© (optionnel) :'; +$labels['rsvpmodeall'] = 'Les sĂ©ries entiĂšres'; +$labels['rsvpmodecurrent'] = 'Cette occurrence seulement'; +$labels['rsvpmodefuture'] = 'Celle-ci et les occurrences futures'; +$labels['itipsingleoccurrence'] = 'C\'est une <em>seule occurrence</em> parmi une sĂ©rie d\'Ă©vĂ©nements'; +$labels['itipfutureoccurrence'] = 'Se rĂ©fĂšre Ă <em>celle-ci et Ă toutes les occurrences futures</em> parmi une sĂ©rie d\'Ă©vĂ©nements'; +$labels['itipmessagesingleoccurrence'] = 'Le message se rĂ©fĂšre seulement Ă cette occurrence'; +$labels['itipmessagefutureoccurrence'] = 'Le message se rĂ©fĂšre Ă celle-ci et Ă toutes les occurrences futures'; +$labels['youhaveaccepted'] = 'Vous avez acceptĂ© cette invitation'; +$labels['youhavetentative'] = 'Vous avez provisoirement acceptĂ© cette invitation'; +$labels['youhavedeclined'] = 'Vous avez refusĂ© cette invitation'; +$labels['youhavedelegated'] = 'Vous avez dĂ©lĂ©guĂ© cette invitation'; +$labels['youhavein-process'] = 'Vous traitez 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 traitiez cette affectation'; +$labels['youhavepreviouslycompleted'] = 'Vous avez dĂ©jĂ fini le traitement de cette affectation'; +$labels['youhavepreviouslyneeds-action'] = 'Vous ĂȘtes en attente de rĂ©ponse pour cette invitation'; +$labels['attendeeaccepted'] = 'Cet utilisateur a acceptĂ© l\'invitation'; +$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'] = 'Cette invitation a Ă©tĂ© remplacĂ©e par une nouvelle version'; +$labels['importtocalendar'] = 'Enregistrer dans mon calendrier'; +$labels['removefromcalendar'] = 'Supprimer de mon calendrier'; +$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 Ă jour 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Ă©.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/he.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'until'; +$labels['frequency'] = 'Repeat'; +$labels['recurrencend'] = 'until'; +$labels['savingdata'] = 'Saving data...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/hr.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'until'; +$labels['frequency'] = 'Repeat'; +$labels['recurrencend'] = 'until'; +$labels['savingdata'] = 'Saving data...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/hu_HU.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,102 @@ +<?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['statusorganizer'] = 'SzervezĆ'; +$labels['statustentative'] = 'FeltĂ©teles'; +$labels['statusneeds-action'] = 'Needs action'; +$labels['statusunknown'] = 'Ismeretlen foglaltsĂĄg'; +$labels['statuscompleted'] = 'Completed'; +$labels['statusin-process'] = 'In process'; +$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...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/it_IT.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,91 @@ +<?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['statusorganizer'] = 'Organizzatore'; +$labels['statustentative'] = 'Provvisorio'; +$labels['statusneeds-action'] = 'Necessaria azione'; +$labels['statusunknown'] = 'Sconosciuto'; +$labels['statuscompleted'] = 'Completato'; +$labels['statusin-process'] = 'In processo'; +$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...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/ja_JP.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,147 @@ +<?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['alarmaudio'] = 'ă”ăŠăłăćç'; +$labels['alarmdisplayoption'] = 'ăĄăă»ăŒăž'; +$labels['alarmemailoption'] = 'EăĄăŒă«'; +$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['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'] = 'æ„(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['untilenddate'] = 'æ„ăŸă§'; +$labels['forntimes'] = '$nr ăŸă§(s)'; +$labels['first'] = '珏1é±'; +$labels['second'] = '珏2é±'; +$labels['third'] = '珏3é±'; +$labels['fourth'] = '珏4é±'; +$labels['last'] = 'æç”é±'; +$labels['dayofmonth'] = 'æ„'; +$labels['addrdate'] = 'çč°èżăæ„èżœć '; +$labels['except'] = 'äŸć€'; +$labels['statusorganizer'] = 'ç·šæè '; +$labels['statusaccepted'] = 'æżè«ŸăăăŸăă'; +$labels['statustentative'] = '仟'; +$labels['statusdeclined'] = 'èŸéăăăŸăă'; +$labels['statusdelegated'] = 'äŸé ŒăăăŸăă'; +$labels['statusneeds-action'] = 'ăăăčăäș'; +$labels['statusunknown'] = 'äžæ'; +$labels['statuscompleted'] = 'ćźäș'; +$labels['statusin-process'] = 'éČèĄäž'; +$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'] = '$name ă "$title" ăæżè«ŸăăŸăă'; +$labels['itipsubjecttentative'] = '$name ă "$title" ă仟æżè«ŸăăŸăă'; +$labels['itipsubjectdeclined'] = '$name ă "$title" ăèŸéăăŸăă'; +$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'] = '代èĄăăăăźă«æćčăȘEăĄăŒă«ăąăăŹăčăć „ćăăŠăă ăă'; +$labels['savingdata'] = 'ăăŒăżăäżćäžâŠ'; +$labels['expandattendeegroup'] = 'ă°ă«ăŒăăĄăłăăŒăźä»Łçäșș'; +$labels['expandattendeegroupnodata'] = 'ăăźă°ă«ăŒăăźä»Łçăă§ăăŸăăăăĄăłăăŒăèŠă€ăăăŸăăă'; +$labels['expandattendeegrouperror'] = 'ăăźă°ă«ăŒăăźä»Łçăă§ăăŸăăăăăăăăĄăłăăŒăć€ăăăŸăă'; +$labels['expandattendeegroupsizelimit'] = 'ăăźă°ă«ăŒăăŻä»Łçăăă«ăŻăĄăłăăŒăć€ăăăŸăă';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/ko_KR.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'êčì§'; +$labels['frequency'] = 'ë°ëł”'; +$labels['recurrencend'] = 'êčì§'; +$labels['statusorganizer'] = 'ìŁŒì”ì'; +$labels['statustentative'] = 'ìì'; +$labels['statusunknown'] = 'ì ì ìë'; +$labels['savingdata'] = 'ìëŁ ì ì„ì€...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/ku_IQ.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,8 @@ +<?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/ + */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/nl_NL.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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'] = 'E-mail versturen'; +$labels['alarmdisplay'] = 'Bericht weergeven'; +$labels['alarmaudio'] = 'Geluid afspelen'; +$labels['alarmdisplayoption'] = 'Bericht'; +$labels['alarmemailoption'] = 'E-mail'; +$labels['alarmaudiooption'] = 'Geluid'; +$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['triggerend-M'] = 'minuten vóór einde'; +$labels['triggerend-H'] = 'uren vóór einde'; +$labels['triggerend-D'] = 'dagen vóór einde'; +$labels['triggerend+M'] = 'minuten na einde'; +$labels['triggerend+H'] = 'uren na einde'; +$labels['triggerend+D'] = 'dagen na einde'; +$labels['trigger0'] = 'op tijd'; +$labels['triggerattime'] = 'op begintijd'; +$labels['triggerattimeend'] = 'op eindtijd'; +$labels['relatedstart'] = 'begin'; +$labels['relatedendevent'] = 'einde'; +$labels['relatedendtask'] = 'vervaltijd'; +$labels['addalarm'] = 'Herinnering toevoegen'; +$labels['removealarm'] = 'Herinnering verwijderen'; +$labels['alarmtitle'] = 'Geplande activiteiten'; +$labels['dismissall'] = 'Alles negeren'; +$labels['dismiss'] = 'Negeren'; +$labels['snooze'] = 'Uitstellen'; +$labels['repeatinmin'] = 'Binnen $min minuten herhalen'; +$labels['repeatinhr'] = 'Binnen 1 uur herhalen'; +$labels['repeatinhrs'] = 'Binnen $hrs uren herhalen'; +$labels['repeattomorrow'] = 'Morgen herhalen'; +$labels['repeatinweek'] = 'Binnen een week herhalen'; +$labels['showmore'] = 'Meer tonen...'; +$labels['recurring'] = 'Herhalingen'; +$labels['frequency'] = 'Herhalen'; +$labels['never'] = 'nooit'; +$labels['daily'] = 'elke dag'; +$labels['weekly'] = 'elke week'; +$labels['monthly'] = 'elke maand'; +$labels['yearly'] = 'elk jaar'; +$labels['rdate'] = 'op datums'; +$labels['every'] = 'Elke'; +$labels['days'] = 'dag(en)'; +$labels['weeks'] = 'week / weken'; +$labels['months'] = 'maand(en)'; +$labels['years'] = 'jaar (jaren)'; +$labels['bydays'] = 'Op'; +$labels['untildate'] = 'de'; +$labels['each'] = 'Elke'; +$labels['onevery'] = 'Op elke'; +$labels['onsamedate'] = 'Op dezelfde datum'; +$labels['forever'] = 'voor altijd'; +$labels['recurrencend'] = 'tot'; +$labels['untilenddate'] = 'tot datum'; +$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'] = 'Herhaaldatum toevoegen'; +$labels['except'] = 'behalve'; +$labels['statusorganizer'] = 'Organisator'; +$labels['statusaccepted'] = 'Geaccepteerd'; +$labels['statustentative'] = 'Misschien'; +$labels['statusdeclined'] = 'Afgeslagen'; +$labels['statusdelegated'] = 'Gedelegeerd'; +$labels['statusneeds-action'] = 'Actie vereist'; +$labels['statusunknown'] = 'Onbekend'; +$labels['statuscompleted'] = 'Voltooid'; +$labels['statusin-process'] = 'In behandeling'; +$labels['itipinvitation'] = 'Uitnodiging voor'; +$labels['itipupdate'] = 'Update van'; +$labels['itipcancellation'] = 'Geannuleerd:'; +$labels['itipreply'] = 'Antwoord aan'; +$labels['itipaccepted'] = 'Accepteren'; +$labels['itiptentative'] = 'Misschien'; +$labels['itipdeclined'] = 'Afslaan'; +$labels['itipdelegated'] = 'Gedelegeerde'; +$labels['itipneeds-action'] = 'Uitstellen'; +$labels['itipcomment'] = 'Uw antwoord'; +$labels['itipeditresponse'] = 'Typ het antwoord'; +$labels['itipsendercomment'] = 'Opmerking van afzender:'; +$labels['itipsuppressreply'] = 'Geen antwoord sturen'; +$labels['itipobjectnotfound'] = 'Het object uit dit bericht is niet in uw account gevonden.'; +$labels['itipsubjectaccepted'] = '"$title" is geaccepteerd door $name'; +$labels['itipsubjecttentative'] = '"$title" is onder voorbehoud geaccepteerd door $name'; +$labels['itipsubjectdeclined'] = '"$title" is afgeslagen door $name'; +$labels['itipsubjectin-process'] = '"$title" wordt behandeld door $name'; +$labels['itipsubjectcompleted'] = '"$title" is voltooid door $name'; +$labels['itipsubjectcancel'] = 'Uw deelname aan "$title" is geannuleerd'; +$labels['itipsubjectdelegated'] = '"$title" is gedelegeerd door $name'; +$labels['itipsubjectdelegatedto'] = '"$title" is door $name gedelegeerd aan u'; +$labels['itipnewattendee'] = 'Dit is een antwoord van een nieuwe deelnemer'; +$labels['updateattendeestatus'] = 'Status van deelnemer bijwerken'; +$labels['acceptinvitation'] = 'Accepteert u deze uitnodiging?'; +$labels['acceptattendee'] = 'Deelnemer accepteren'; +$labels['declineattendee'] = 'Deelnemer afwijzen'; +$labels['declineattendeeconfirm'] = 'Voeg een bericht toe voor de afgewezen deelnemer (optioneel):'; +$labels['rsvpmodeall'] = 'De gehele reeks'; +$labels['rsvpmodecurrent'] = 'Alleen deze activiteit'; +$labels['rsvpmodefuture'] = 'Deze en toekomstige activiteiten'; +$labels['itipsingleoccurrence'] = 'Dit is <em>een enkele activiteit</em> uit een reeks activiteiten'; +$labels['itipfutureoccurrence'] = 'Verwijst naar <em>deze en alle toekomstige activiteiten</em> uit een reeks activiteiten'; +$labels['itipmessagesingleoccurrence'] = 'Het bericht verwijst alleen naar deze activiteit'; +$labels['itipmessagefutureoccurrence'] = 'Het bericht verwijst naar deze en alle toekomstige activiteiten'; +$labels['youhaveaccepted'] = 'U hebt deze uitnodiging geaccepteerd'; +$labels['youhavetentative'] = 'U hebt deze uitnodiging onder voorbehoud geaccepteerd'; +$labels['youhavedeclined'] = 'U hebt deze uitnodiging afgeslagen'; +$labels['youhavedelegated'] = 'U hebt deze uitnodiging gedelegeerd'; +$labels['youhavein-process'] = 'U werkt momenteel aan deze opdracht'; +$labels['youhavecompleted'] = 'U hebt deze opdracht voltooid'; +$labels['youhaveneeds-action'] = 'U hebt deze uitnodiging nog niet beantwoord'; +$labels['youhavepreviouslyaccepted'] = 'U hebt deze uitnodiging eerder al geaccepteerd'; +$labels['youhavepreviouslytentative'] = 'U hebt deze uitnodiging eerder al onder voorbehoud geaccepteerd'; +$labels['youhavepreviouslydeclined'] = 'U hebt deze uitnodiging eerder al afgeslagen'; +$labels['youhavepreviouslydelegated'] = 'U hebt deze uitnodiging eerder al gedelegeerd'; +$labels['youhavepreviouslyin-process'] = 'U hebt eerder al gemeld dat u aan deze opdracht werkt'; +$labels['youhavepreviouslycompleted'] = 'U hebt deze opdracht eerder al voltooid'; +$labels['youhavepreviouslyneeds-action'] = 'U hebt deze uitnodiging nog niet beantwoord'; +$labels['attendeeaccepted'] = 'Deelnemer heeft geaccepteerd'; +$labels['attendeetentative'] = 'Deelnemer heeft onder voorbehoud geaccepteerd'; +$labels['attendeedeclined'] = 'Deelnemer heeft afgeslagen'; +$labels['attendeedelegated'] = 'Deelnemer heeft gedelegeerd aan $delegatedto'; +$labels['attendeein-process'] = 'De deelnemer heeft de status gewijzigd in âIn behandelingâ'; +$labels['attendeecompleted'] = 'De deelnemer heeft de status gewijzigd in âVoltooidâ'; +$labels['notanattendee'] = 'U staat niet op de lijst met deelnemers aan deze activiteit'; +$labels['outdatedinvitation'] = 'Deze uitnodiging is vervangen door een nieuwere versie'; +$labels['importtocalendar'] = 'Opslaan in mijn agenda'; +$labels['removefromcalendar'] = 'Verwijderen uit mijn agenda'; +$labels['updatemycopy'] = 'Mijn exemplaar bijwerken'; +$labels['openpreview'] = 'Voorbeeld openen'; +$labels['deleteobjectconfirm'] = 'Weet u zeker dat u dit object wilt verwijderen?'; +$labels['declinedeleteconfirm'] = 'Wilt u dit afgewezen object ook verwijderen uit uw account?'; +$labels['delegateinvitation'] = 'Uitnodiging delegeren'; +$labels['delegateto'] = 'Delegeren aan'; +$labels['delegatersvpme'] = 'Houd me op de hoogte over updates voor deze incidentie'; +$labels['delegateinvalidaddress'] = 'Voer een geldig e-mailadres voor de gedelegeerde in'; +$labels['savingdata'] = 'Gegevens opslaan...'; +$labels['expandattendeegroup'] = 'Vervangen door groepsleden'; +$labels['expandattendeegroupnodata'] = 'Deze groep kan niet worden vervangen. Geen leden gevonden.'; +$labels['expandattendeegrouperror'] = 'Deze groep kan niet worden vervangen. De groep bevat mogelijk te veel leden.'; +$labels['expandattendeegroupsizelimit'] = 'Deze groep bevat te veel leden om te vervangen.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/pl_PL.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'dopĂłki'; +$labels['frequency'] = 'PowtĂłrz'; +$labels['recurrencend'] = 'dopĂłki'; +$labels['statusorganizer'] = 'Organizator'; +$labels['statustentative'] = 'Niepewny'; +$labels['statusunknown'] = 'Nieznany'; +$labels['savingdata'] = 'ZapisujÄ dane...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/pt_BR.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,91 @@ +<?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['statusorganizer'] = 'Organizador'; +$labels['statustentative'] = 'Tentativa'; +$labels['statusneeds-action'] = 'Needs action'; +$labels['statusunknown'] = 'Desconhecido'; +$labels['statuscompleted'] = 'Completed'; +$labels['statusin-process'] = 'In process'; +$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...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/pt_PT.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,16 @@ +<?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['alarmemailoption'] = 'Email'; +$labels['frequency'] = 'Repetir'; +$labels['recurrencend'] = 'até'; +$labels['statusorganizer'] = 'Organizador'; +$labels['statustentative'] = 'Tentativa'; +$labels['statusunknown'] = 'Desconhecido'; +$labels['savingdata'] = 'Salvando dados...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/ro.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,8 @@ +<?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/ + */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/ru_RU.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,159 @@ +<?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['triggerend-M'] = 'ĐŒĐžĐœŃŃ ĐŽĐŸ ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['triggerend-H'] = 'ŃĐ°ŃĐŸĐČ ĐŽĐŸ ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['triggerend-D'] = 'ĐŽĐœĐ”Đč ĐŽĐŸ ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['triggerend+M'] = 'ĐŒĐžĐœŃŃ ĐżĐŸŃлД ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['triggerend+H'] = 'ŃĐ°ŃĐŸĐČ ĐżĐŸŃлД ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['triggerend+D'] = 'ĐŽĐœĐ”Đč ĐżĐŸŃлД ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['trigger0'] = 'ĐČĐŸ ĐČŃĐ”ĐŒŃ'; +$labels['triggerattime'] = 'ĐČ ĐŒĐŸĐŒĐ”ĐœŃ ĐœĐ°Ńала'; +$labels['triggerattimeend'] = 'ĐČ ĐŒĐŸĐŒĐ”ĐœŃ ĐŸĐșĐŸĐœŃĐ°ĐœĐžŃ'; +$labels['relatedstart'] = 'ĐœĐ°ŃĐ°Đ»ĐŸ'; +$labels['relatedendevent'] = 'ĐŸĐșĐŸĐœŃĐ°ĐœĐžĐ”'; +$labels['relatedendtask'] = 'ŃŃĐŸĐș'; +$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['statusorganizer'] = 'ĐŃĐłĐ°ĐœĐžĐ·Đ°ŃĐŸŃ'; +$labels['statusaccepted'] = 'ĐŃĐžĐœŃŃĐŸ'; +$labels['statustentative'] = 'ĐĐ”ĐŸĐżŃДЎДлŃĐœĐœĐŸ'; +$labels['statusdeclined'] = 'ĐŃĐșĐ»ĐŸĐœĐ”ĐœĐŸ'; +$labels['statusdelegated'] = 'ĐĐŸŃŃŃĐ”ĐœĐŸ'; +$labels['statusneeds-action'] = 'ĐąŃДбŃĐ”Ń ĐŽĐ”ĐčŃŃĐČĐžŃ'; +$labels['statusunknown'] = 'ĐДОзĐČĐ”ŃŃĐœĐŸ'; +$labels['statuscompleted'] = 'ĐĐ°ĐČĐ”ŃŃĐ”ĐœĐœŃĐ”'; +$labels['statusin-process'] = 'Đ ĐżŃĐŸŃĐ”ŃŃĐ”'; +$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'] = 'ĐŃŃппа ŃĐŸĐŽĐ”ŃĐ¶ĐžŃ ŃлОŃĐșĐŸĐŒ ĐŒĐœĐŸĐłĐŸ ŃŃĐ°ŃŃĐœĐžĐșĐŸĐČ ĐŽĐ»Ń Đ·Đ°ĐŒĐ”ĐœŃ.';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/sk_SK.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'OpakovaƄ'; +$labels['statustentative'] = 'NezåvÀzne';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/sl_SI.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,21 @@ +<?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['statusorganizer'] = 'Organizator'; +$labels['statustentative'] = 'Pogojno'; +$labels['statusneeds-action'] = 'Potrebuje pozornost'; +$labels['statusunknown'] = 'Neznano'; +$labels['statuscompleted'] = 'Opravljeno'; +$labels['statusin-process'] = 'V procesu'; +$labels['itipdelegated'] = 'SodelujoÄ'; +$labels['savingdata'] = 'Shranjujem...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/sv.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,8 @@ +<?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/ + */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/sv_SE.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'tills'; +$labels['frequency'] = 'Upprepa'; +$labels['recurrencend'] = 'tills'; +$labels['statusorganizer'] = 'Organisatör'; +$labels['statustentative'] = 'PreliminÀrt'; +$labels['statusunknown'] = 'OkÀnd'; +$labels['savingdata'] = 'Sparar data ...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/th_TH.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'àžàžàžàž¶àž'; +$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['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['statusorganizer'] = 'àžàžčàčàžàž±àžàžàžČàž'; +$labels['statustentative'] = 'àčàžàž§àčàžàčàžĄ'; +$labels['statusneeds-action'] = 'àžàčàžàžàžàžłàžàž°àčàžŁàžȘàž±àžàžàžąàčàžČàž'; +$labels['statusunknown'] = 'àčàžĄàčàžàžŁàžČàž'; +$labels['statuscompleted'] = 'àčàžȘàžŁàčàžàčàžŁàž”àžąàžàžŁàčàžàžą'; +$labels['statusin-process'] = 'àžàžąàžžàčàžŁàž°àž«àž§àčàžČàžàžàžłàčàžàžŽàžàžàžČàžŁ'; +$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'] = 'àžàž±àžàžàž¶àžàžàčàžàžĄàžčàž„';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/tr_TR.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,8 @@ +<?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/ + */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/uk_UA.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,17 @@ +<?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['statusorganizer'] = 'ĐŃĐłĐ°ĐœŃĐ·Đ°ŃĐŸŃ'; +$labels['statustentative'] = 'ĐĐ”ĐČĐžĐ·ĐœĐ°ŃĐ”ĐœĐžĐč'; +$labels['statusunknown'] = 'ĐĐ”ĐČŃĐŽĐŸĐŒĐŸ'; +$labels['itipdelegated'] = 'ĐŃДЎŃŃĐ°ĐČĐœĐžĐș'; +$labels['savingdata'] = 'ĐбДŃĐ”Đ¶Đ”ĐœĐœŃ ĐŽĐ°ĐœĐžŃ ...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/vi.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'until'; +$labels['frequency'] = 'Repeat'; +$labels['recurrencend'] = 'until'; +$labels['savingdata'] = 'Saving data...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/vi_VN.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,11 @@ +<?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['statusorganizer'] = 'Organizer'; +$labels['statustentative'] = 'Tentative'; +$labels['statusunknown'] = 'Unknown';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/zh_CN.inc Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,30 @@ +<?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['alarmaudio'] = 'ææŸćŁ°éł'; +$labels['alarmdisplayoption'] = 'æ¶æŻ'; +$labels['alarmemailoption'] = 'éźä»¶'; +$labels['alarmaudiooption'] = '棰éł'; +$labels['alarmat'] = 'ćš $datetime'; +$labels['trigger-M'] = 'ćé仄ć'; +$labels['trigger-H'] = 'ć°æ¶ä»„ć'; +$labels['trigger-D'] = '怩仄ć'; +$labels['trigger+M'] = 'ćé仄ć'; +$labels['trigger+H'] = 'ć°æ¶ä»„ć'; +$labels['trigger+D'] = '怩仄ć'; +$labels['triggerend-M'] = 'èżæxxxćéç»æ # xxx was supposed to be the number before "minutes before end"'; +$labels['frequency'] = 'ćŸȘçŻ'; +$labels['recurrencend'] = 'çŽć°'; +$labels['statusorganizer'] = 'ç»ç»è '; +$labels['statustentative'] = '䞎æ¶'; +$labels['statusunknown'] = 'æȘç„'; +$labels['savingdata'] = 'äżćæ°æź...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/localization/zh_TW.inc Sat Jan 13 08:57:56 2018 -0500 @@ -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'] = 'until'; +$labels['frequency'] = 'Repeat'; +$labels['recurrencend'] = 'until'; +$labels['statusorganizer'] = 'Organizer'; +$labels['statustentative'] = 'Tentative'; +$labels['statusunknown'] = 'Unknown'; +$labels['savingdata'] = 'Saving data...';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/skins/larry/libcal.css Sat Jan 13 08:57:56 2018 -0500 @@ -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; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/libcalendaring.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,184 @@ +<?php + +/** + * libcalendaring plugin's utility functions tests + * + * @author Thomas Bruederli <bruederli@kolabsys.com> + * + * Copyright (C) 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_test extends PHPUnit_Framework_TestCase +{ + function setUp() + { + require_once __DIR__ . '/../libcalendaring.php'; + } + + /** + * libcalendaring::parse_alarm_value() + */ + function test_parse_alarm_value() + { + $alarm = libcalendaring::parse_alarm_value('-15M'); + $this->assertEquals('15', $alarm[0]); + $this->assertEquals('-M', $alarm[1]); + $this->assertEquals('-PT15M', $alarm[3]); + + $alarm = libcalendaring::parse_alarm_value('-PT5H'); + $this->assertEquals('5', $alarm[0]); + $this->assertEquals('-H', $alarm[1]); + + $alarm = libcalendaring::parse_alarm_value('P0DT1H0M0S'); + $this->assertEquals('1', $alarm[0]); + $this->assertEquals('+H', $alarm[1]); + + // FIXME: this should return something like (1140 + 120 + 30)M + $alarm = libcalendaring::parse_alarm_value('-P1DT2H30M'); + // $this->assertEquals('1590', $alarm[0]); + // $this->assertEquals('-M', $alarm[1]); + + $alarm = libcalendaring::parse_alarm_value('@1420722000'); + $this->assertInstanceOf('DateTime', $alarm[0]); + } + + /** + * libcalendaring::get_next_alarm() + */ + function test_get_next_alarm() + { + // alarm 10 minutes before event + $date = date('Ymd', strtotime('today + 2 days')); + $event = array( + 'start' => new DateTime($date . 'T160000Z'), + 'end' => new DateTime($date . 'T200000Z'), + 'valarms' => array( + array( + 'trigger' => '-PT10M', + 'action' => 'DISPLAY', + ), + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals($event['valarms'][0]['action'], $alarm['action']); + $this->assertEquals(strtotime($date . 'T155000Z'), $alarm['time']); + + // alarm 1 hour after event start + $event['valarms'] = array( + array( + 'trigger' => '+PT1H', + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals('DISPLAY', $alarm['action']); + $this->assertEquals(strtotime($date . 'T170000Z'), $alarm['time']); + + // alarm 1 hour before event end + $event['valarms'] = array( + array( + 'trigger' => '-PT1H', + 'related' => 'END', + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals('DISPLAY', $alarm['action']); + $this->assertEquals(strtotime($date . 'T190000Z'), $alarm['time']); + + // alarm 1 hour after event end + $event['valarms'] = array( + array( + 'trigger' => 'PT1H', + 'related' => 'END', + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals('DISPLAY', $alarm['action']); + $this->assertEquals(strtotime($date . 'T210000Z'), $alarm['time']); + + // ignore past alarms + $event['start'] = new DateTime('today 22:00:00'); + $event['end'] = new DateTime('today 23:00:00'); + $event['valarms'] = array( + array( + 'trigger' => '-P2D', + 'action' => 'EMAIL', + ), + array( + 'trigger' => '-PT30M', + 'action' => 'DISPLAY', + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals('DISPLAY', $alarm['action']); + $this->assertEquals(strtotime('today 21:30:00'), $alarm['time']); + + // absolute alarm date/time + $event['valarms'] = array( + array('trigger' => new DateTime('today 20:00:00')) + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals($event['valarms'][0]['trigger']->format('U'), $alarm['time']); + + // no alarms for cancelled events + $event['status'] = 'CANCELLED'; + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals(null, $alarm); + } + + /** + * libcalendaring::part_is_vcalendar() + */ + function test_part_is_vcalendar() + { + $part = new StdClass; + $part->mimetype = 'text/plain'; + $part->filename = 'event.ics'; + + $this->assertFalse(libcalendaring::part_is_vcalendar($part)); + + $part->mimetype = 'text/calendar'; + $this->assertTrue(libcalendaring::part_is_vcalendar($part)); + + $part->mimetype = 'text/x-vcalendar'; + $this->assertTrue(libcalendaring::part_is_vcalendar($part)); + + $part->mimetype = 'application/ics'; + $this->assertTrue(libcalendaring::part_is_vcalendar($part)); + + $part->mimetype = 'application/x-any'; + $this->assertTrue(libcalendaring::part_is_vcalendar($part)); + } + + /** + * libcalendaring::to_rrule() + */ + function test_to_rrule() + { + $rrule = array( + 'FREQ' => 'MONTHLY', + 'BYDAY' => '2WE', + 'INTERVAL' => 2, + 'UNTIL' => new DateTime('2025-05-01 18:00:00 CEST'), + ); + + $s = libcalendaring::to_rrule($rrule); + + $this->assertRegExp('/FREQ='.$rrule['FREQ'].'/', $s, "Recurrence Frequence"); + $this->assertRegExp('/INTERVAL='.$rrule['INTERVAL'].'/', $s, "Recurrence Interval"); + $this->assertRegExp('/BYDAY='.$rrule['BYDAY'].'/', $s, "Recurrence BYDAY"); + $this->assertRegExp('/UNTIL=20250501T160000Z/', $s, "Recurrence End date (in UTC)"); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/libvcalendar.php Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,599 @@ +<?php + +/** + * libcalendaring plugin's iCalendar functions tests + * + * @author Thomas Bruederli <bruederli@kolabsys.com> + * + * Copyright (C) 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 libvcalendar_test extends PHPUnit_Framework_TestCase +{ + function setUp() + { + require_once __DIR__ . '/../libvcalendar.php'; + require_once __DIR__ . '/../libcalendaring.php'; + } + + /** + * Simple iCal parsing test + */ + function test_import() + { + $ical = new libvcalendar(); + $ics = file_get_contents(__DIR__ . '/resources/snd.ics'); + $events = $ical->import($ics, 'UTF-8'); + + $this->assertEquals(1, count($events)); + $event = $events[0]; + + $this->assertInstanceOf('DateTime', $event['created'], "'created' property is DateTime object"); + $this->assertInstanceOf('DateTime', $event['changed'], "'changed' property is DateTime object"); + $this->assertEquals('UTC', $event['created']->getTimezone()->getName(), "'created' date is in UTC"); + + $this->assertInstanceOf('DateTime', $event['start'], "'start' property is DateTime object"); + $this->assertInstanceOf('DateTime', $event['end'], "'end' property is DateTime object"); + $this->assertEquals('08-01', $event['start']->format('m-d'), "Start date is August 1st"); + $this->assertTrue($event['allday'], "All-day event flag"); + + $this->assertEquals('B968B885-08FB-40E5-B89E-6DA05F26AA79', $event['uid'], "Event UID"); + $this->assertEquals('Swiss National Day', $event['title'], "Event title"); + $this->assertEquals('http://en.wikipedia.org/wiki/Swiss_National_Day', $event['url'], "URL property"); + $this->assertEquals(2, $event['sequence'], "Sequence number"); + + $desclines = explode("\n", $event['description']); + $this->assertEquals(4, count($desclines), "Multiline description"); + $this->assertEquals("French: FĂȘte nationale Suisse", rtrim($desclines[1]), "UTF-8 encoding"); + } + + /** + * Test parsing from files + */ + function test_import_from_file() + { + $ical = new libvcalendar(); + + $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8'); + $this->assertEquals(2, count($events)); + + $events = $ical->import_from_file(__DIR__ . '/resources/invalid.txt', 'UTF-8'); + $this->assertEmpty($events); + } + + /** + * Test parsing from files with multiple VCALENDAR blocks (#2884) + */ + function test_import_from_file_multiple() + { + $ical = new libvcalendar(); + $ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8'); + $events = array(); + foreach ($ical as $event) { + $events[] = $event; + } + + $this->assertEquals(2, count($events)); + $this->assertEquals("AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated", $events[0]['uid']); + $this->assertEquals("AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated", $events[1]['uid']); + } + + function test_invalid_dates() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/invalid-dates.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals(1, count($events), "Import event data"); + $this->assertInstanceOf('DateTime', $event['created'], "Created date field"); + $this->assertFalse(array_key_exists('changed', $event), "No changed date field"); + } + + function test_invalid_vevent() + { + $this->setExpectedException('\Sabre\VObject\ParseException'); + + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/invalid-event.ics', 'UTF-8', true); + } + + /** + * Test some extended ical properties such as attendees, recurrence rules, alarms and attachments + */ + function test_extended() + { + $ical = new libvcalendar(); + + $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8'); + $event = $events[0]; + $this->assertEquals('REQUEST', $ical->method, "iTip method"); + + // attendees + $this->assertEquals(3, count($event['attendees']), "Attendees list (including organizer)"); + $organizer = $event['attendees'][0]; + $this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE'); + $this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name'); + + $attendee = $event['attendees'][1]; + $this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE'); + $this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS'); + $this->assertEquals('rolf2@mykolab.com', $attendee['email'], 'Attendee mailto:'); + $this->assertEquals('carl@mykolab.com', $attendee['delegated-from'], 'Attendee delegated-from'); + $this->assertTrue($attendee['rsvp'], 'Attendee RSVP'); + + $delegator = $event['attendees'][2]; + $this->assertEquals('NON-PARTICIPANT', $delegator['role'], 'Delegator ROLE'); + $this->assertEquals('DELEGATED', $delegator['status'], 'Delegator STATUS'); + $this->assertEquals('INDIVIDUAL', $delegator['cutype'], 'Delegator CUTYPE'); + $this->assertEquals('carl@mykolab.com', $delegator['email'], 'Delegator mailto:'); + $this->assertEquals('rolf2@mykolab.com', $delegator['delegated-to'], 'Delegator delegated-to'); + $this->assertFalse($delegator['rsvp'], 'Delegator RSVP'); + + // attachments + $this->assertEquals(1, count($event['attachments']), "Embedded attachments"); + $attachment = $event['attachments'][0]; + $this->assertEquals('text/html', $attachment['mimetype'], "Attachment mimetype attribute"); + $this->assertEquals('calendar.html', $attachment['name'], "Attachment filename (X-LABEL) attribute"); + $this->assertContains('<title>Kalender</title>', $attachment['data'], "Attachment content (decoded)"); + + // recurrence rules + $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertTrue(is_array($event['recurrence']), 'Recurrences rule as hash array'); + $rrule = $event['recurrence']; + $this->assertEquals('MONTHLY', $rrule['FREQ'], "Recurrence frequency"); + $this->assertEquals('1', $rrule['INTERVAL'], "Recurrence interval"); + $this->assertEquals('3WE', $rrule['BYDAY'], "Recurrence frequency"); + $this->assertInstanceOf('DateTime', $rrule['UNTIL'], "Recurrence end date"); + + $this->assertEquals(2, count($rrule['EXDATE']), "Recurrence EXDATEs"); + $this->assertInstanceOf('DateTime', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime"); + + $this->assertTrue(is_array($rrule['EXCEPTIONS'])); + $this->assertEquals(1, count($rrule['EXCEPTIONS']), "Recurrence Exceptions"); + + $exception = $rrule['EXCEPTIONS'][0]; + $this->assertEquals($event['uid'], $event['uid'], "Exception UID"); + $this->assertEquals('Recurring Test (Exception)', $exception['title'], "Exception title"); + $this->assertInstanceOf('DateTime', $exception['start'], "Exception start"); + + // categories, class + $this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories"); + $this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential"); + + // parse a recurrence chain instance + $events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8'); + $this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty"); + $this->assertInstanceOf('DateTime', $events[0]['recurrence_date'], "Recurrence-ID as date"); + $this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE"); + + $this->assertEquals(count($events[0]['exceptions']), 1, "Second VEVENT as exception"); + $this->assertEquals($events[0]['exceptions'][0]['uid'], $events[0]['uid'], "Exception UID match"); + $this->assertEquals($events[0]['exceptions'][0]['sequence'], '2', "Exception sequence"); + } + + /** + * + */ + function test_alarms() + { + $ical = new libvcalendar(); + + $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string"); + $alarm = libcalendaring::parse_alarm_value($event['alarms']); + $this->assertEquals('12', $alarm[0], "Alarm value"); + $this->assertEquals('-H', $alarm[1], "Alarm unit"); + + $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)"); + $this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); + $this->assertEquals('END', $event['valarms'][0]['related'], "Full alarm item (related)"); + + // alarm trigger with 0 values + $events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals('-30M:DISPLAY', $event['alarms'], "Stripped alarm string"); + $alarm = libcalendaring::parse_alarm_value($event['alarms']); + $this->assertEquals('30', $alarm[0], "Alarm value"); + $this->assertEquals('-M', $alarm[1], "Alarm unit"); + $this->assertEquals('-30M', $alarm[2], "Alarm string"); + $this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)"); + + $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action"); + $this->assertEquals('', $event['valarms'][0]['related'], "First alarm related property"); + $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text"); + + $this->assertEquals(3, count($event['valarms']), "List all VALARM blocks"); + + $valarm = $event['valarms'][1]; + $this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees"); + $this->assertEquals('EMAIL', $valarm['action'], "Second alarm item (action)"); + $this->assertEquals('-P1D', $valarm['trigger'], "Second alarm item (trigger)"); + $this->assertEquals('This is the reminder message', $valarm['summary'], "Email alarm text"); + $this->assertInstanceOf('DateTime', $event['valarms'][2]['trigger'], "Absolute trigger date/time"); + + // test alarms export + $ics = $ical->export(array($event)); + $this->assertContains('ACTION:DISPLAY', $ics, "Display alarm block"); + $this->assertContains('ACTION:EMAIL', $ics, "Email alarm block"); + $this->assertContains('DESCRIPTION:This is the first event reminder', $ics, "Alarm description"); + $this->assertContains('SUMMARY:This is the reminder message', $ics, "Email alarm summary"); + $this->assertContains('ATTENDEE:mailto:reminder-recipient@example.org', $ics, "Email alarm recipient"); + $this->assertContains('TRIGGER;VALUE=DATE-TIME:20130812', $ics, "Date-Time trigger"); + } + + /** + * @depends test_import_from_file + */ + function test_attachment() + { + $ical = new libvcalendar(); + + $events = $ical->import_from_file(__DIR__ . '/resources/attachment.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals(2, count($events)); + $this->assertEquals(1, count($event['attachments'])); + $this->assertEquals('image/png', $event['attachments'][0]['mimetype']); + $this->assertEquals('500px-Opensource.svg.png', $event['attachments'][0]['name']); + } + + /** + * @depends test_import + */ + function test_apple_alarms() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/apple-alarms.ics', 'UTF-8'); + $event = $events[0]; + + // alarms + $this->assertEquals('-45M:AUDIO', $event['alarms'], "Relative alarm string"); + $alarm = libcalendaring::parse_alarm_value($event['alarms']); + $this->assertEquals('45', $alarm[0], "Alarm value"); + $this->assertEquals('-M', $alarm[1], "Alarm unit"); + + $this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks"); + $this->assertEquals('AUDIO', $event['valarms'][0]['action'], "Full alarm item (action)"); + $this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); + $this->assertEquals('Basso', $event['valarms'][0]['uri'], "Full alarm item (attachment)"); + } + + /** + * + */ + function test_escaped_values() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/escaped.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals("House, Street, Zip Place", $event['location'], "Decode escaped commas in location value"); + $this->assertEquals("Me, meets Them\nThem, meet Me", $event['description'], "Decode description value"); + $this->assertEquals("Kolab, Thomas", $event['attendees'][3]['name'], "Unescaped"); + + $ics = $ical->export($events); + $this->assertContains('ATTENDEE;CN="Kolab, Thomas";PARTSTAT=', $ics, "Quoted attendee parameters"); + } + + /** + * Parse RDATE properties (#2885) + */ + function test_rdate() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals(9, count($event['recurrence']['RDATE'])); + $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]); + $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][1]); + } + + /** + * @depends test_import + */ + function test_freebusy() + { + $ical = new libvcalendar(); + $ical->import_from_file(__DIR__ . '/resources/freebusy.ifb', 'UTF-8'); + $freebusy = $ical->freebusy; + + $this->assertInstanceOf('DateTime', $freebusy['start'], "'start' property is DateTime object"); + $this->assertInstanceOf('DateTime', $freebusy['end'], "'end' property is DateTime object"); + $this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined"); + $periods = $ical->get_busy_periods(); + $this->assertEquals(9, count($periods), "Number of busy periods found"); + $this->assertEquals('BUSY-TENTATIVE', $periods[8][2], "FBTYPE=BUSY-TENTATIVE"); + } + + /** + * @depends test_import + */ + function test_freebusy_dummy() + { + $ical = new libvcalendar(); + $ical->import_from_file(__DIR__ . '/resources/dummy.ifb', 'UTF-8'); + $freebusy = $ical->freebusy; + + $this->assertEquals(0, count($freebusy['periods']), "Ignore 0-length freebudy periods"); + $this->assertContains('dummy', $freebusy['comment'], "Parse comment"); + } + + function test_vtodo() + { + $ical = new libvcalendar(); + $tasks = $ical->import_from_file(__DIR__ . '/resources/vtodo.ics', 'UTF-8', true); + $task = $tasks[0]; + + $this->assertInstanceOf('DateTime', $task['start'], "'start' property is DateTime object"); + $this->assertInstanceOf('DateTime', $task['due'], "'due' property is DateTime object"); + $this->assertEquals('-1D:DISPLAY', $task['alarms'], "Taks alarm value"); + $this->assertEquals('IN-PROCESS', $task['status'], "Task status property"); + $this->assertEquals(1, count($task['x-custom']), "Custom properties"); + $this->assertEquals(4, count($task['categories'])); + $this->assertEquals('1234567890-12345678-PARENT', $task['parent_id'], "Parent Relation"); + + $completed = $tasks[1]; + $this->assertEquals('COMPLETED', $completed['status'], "Task status=completed when COMPLETED property is present"); + $this->assertEquals(100, $completed['complete'], "Task percent complete value"); + + $ics = $ical->export(array($completed)); + $this->assertRegExp('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property"); + } + + /** + * Test for iCal export from internal hash array representation + * + * + */ + function test_export() + { + $ical = new libvcalendar(); + + $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8'); + $event = $events[0]; + $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8'); + $event += $events[0]; + + $this->attachment_data = $event['attachments'][0]['data']; + unset($event['attachments'][0]['data']); + $event['attachments'][0]['id'] = '1'; + $event['description'] = '*Exported by libvcalendar*'; + + $event['start']->setTimezone(new DateTimezone('America/Montreal')); + $event['end']->setTimezone(new DateTimezone('Europe/Berlin')); + + $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'), true); + + $this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN"); + + $this->assertContains('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN"); + $this->assertContains('TZID:Europe/Berlin', $ics, "Timezone ID"); + $this->assertContains('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM"); + $this->assertContains('TZOFFSETTO:+0200', $ics, "Timzone transition TO"); + $this->assertContains('TZOFFSETFROM:-0400', $ics, "TZOFFSETFROM with negative offset (Bug T428)"); + $this->assertContains('TZOFFSETTO:-0500', $ics, "TZOFFSETTO with negative offset (Bug T428)"); + $this->assertContains('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END"); + + $this->assertContains('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN"); + $this->assertSame(2, substr_count($ics, 'DTSTAMP'), "Duplicate DTSTAMP (T1148)"); + $this->assertContains('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID"); + $this->assertContains('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number"); + $this->assertContains('CLASS:CONFIDENTIAL', $ics, "Sensitivity => Class"); + $this->assertContains('DESCRIPTION:*Exported by', $ics, "Export Description"); + $this->assertContains('ORGANIZER;CN=Rolf Test:mailto:rolf@', $ics, "Export organizer"); + $this->assertRegExp('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/', $ics, "Export Attendee ROLE"); + $this->assertRegExp('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/', $ics, "Export Attendee Status"); + $this->assertRegExp('/ATTENDEE.*;RSVP=TRUE/', $ics, "Export Attendee RSVP"); + $this->assertRegExp('/:mailto:rolf2@/', $ics, "Export Attendee mailto:"); + + $rrule = $event['recurrence']; + $this->assertRegExp('/RRULE:.*FREQ='.$rrule['FREQ'].'/', $ics, "Export Recurrence Frequence"); + $this->assertRegExp('/RRULE:.*INTERVAL='.$rrule['INTERVAL'].'/', $ics, "Export Recurrence Interval"); + $this->assertRegExp('/RRULE:.*UNTIL=20140718T215959Z/', $ics, "Export Recurrence End date"); + $this->assertRegExp('/RRULE:.*BYDAY='.$rrule['BYDAY'].'/', $ics, "Export Recurrence BYDAY"); + $this->assertRegExp('/EXDATE.*:20131218/', $ics, "Export Recurrence EXDATE"); + + $this->assertContains('BEGIN:VALARM', $ics, "Export VALARM"); + $this->assertContains('TRIGGER;RELATED=END:-PT12H', $ics, "Export Alarm trigger"); + + $this->assertRegExp('/ATTACH.*;VALUE=BINARY/', $ics, "Embed attachment"); + $this->assertRegExp('/ATTACH.*;ENCODING=BASE64/', $ics, "Attachment B64 encoding"); + $this->assertRegExp('!ATTACH.*;FMTTYPE=text/html!', $ics, "Attachment mimetype"); + $this->assertRegExp('!ATTACH.*;X-LABEL=calendar.html!', $ics, "Attachment filename with X-LABEL"); + + $this->assertContains('END:VEVENT', $ics, "VEVENT encapsulation END"); + $this->assertContains('END:VCALENDAR', $ics, "VCALENDAR encapsulation END"); + } + + /** + * @depends test_extended + * @depends test_export + */ + function test_export_multiple() + { + $ical = new libvcalendar(); + $events = array_merge( + $ical->import_from_file(__DIR__ . '/resources/snd.ics', 'UTF-8'), + $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8') + ); + + $num = count($events); + $ics = $ical->export($events, null, false); + + $this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN"); + $this->assertContains('END:VCALENDAR', $ics, "VCALENDAR encapsulation END"); + $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN"); + $this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END"); + } + + /** + * @depends test_export + */ + function test_export_recurrence_exceptions() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8'); + + // add exceptions + $event = $events[0]; + unset($event['recurrence']['EXCEPTIONS']); + + $exception1 = $event; + $exception1['start'] = clone $event['start']; + $exception1['start']->setDate(2013, 8, 14); + $exception1['end'] = clone $event['end']; + $exception1['end']->setDate(2013, 8, 14); + + $exception2 = $event; + $exception2['start'] = clone $event['start']; + $exception2['start']->setDate(2013, 11, 13); + $exception2['end'] = clone $event['end']; + $exception2['end']->setDate(2013, 11, 13); + $exception2['title'] = 'Recurring Exception'; + + $events[0]['recurrence']['EXCEPTIONS'] = array($exception1, $exception2); + + $ics = $ical->export($events, null, false); + + $num = count($events[0]['recurrence']['EXCEPTIONS']) + 1; + $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN"); + $this->assertEquals($num, substr_count($ics, 'UID:'.$event['uid']), "Recurrence Exceptions with same UID"); + $this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END"); + + $this->assertContains('RECURRENCE-ID;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date"); + $this->assertContains('RECURRENCE-ID;TZID=Europe/Zurich:20131113', $ics, "Recurrence-ID (2) being the exception date"); + $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title"); + } + + function test_export_valid_rrules() + { + $event = array( + 'uid' => '1234567890', + 'start' => new DateTime('now'), + 'end' => new DateTime('now + 30min'), + 'title' => 'test_export_valid_rrules', + 'recurrence' => array( + 'FREQ' => 'DAILY', + 'COUNT' => 5, + 'EXDATE' => array(), + 'RDATE' => array(), + ), + ); + $ical = new libvcalendar(); + $ics = $ical->export(array($event), null, false, null, false); + + $this->assertNotContains('EXDATE=', $ics); + $this->assertNotContains('RDATE=', $ics); + } + + /** + * + */ + function test_export_rdate() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8'); + $ics = $ical->export($events, null, false); + + $this->assertContains('RDATE:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values"); + } + + /** + * @depends test_export + */ + function test_export_direct() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8'); + $num = count($events); + + ob_start(); + $return = $ical->export($events, null, true); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertTrue($return, "Return true on successful writing"); + $this->assertContains('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN"); + $this->assertContains('END:VCALENDAR', $output, "VCALENDAR encapsulation END"); + $this->assertEquals($num, substr_count($output, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN"); + $this->assertEquals($num, substr_count($output, 'END:VEVENT'), "VEVENT encapsulation END"); + } + + function test_datetime() + { + $ical = new libvcalendar(); + $cal = new \Sabre\VObject\Component\VCalendar(); + $localtime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin'))); + $localdate = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true); + $utctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC'))); + $asutctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true); + + $this->assertContains('TZID=Europe/Berlin', $localtime->serialize()); + $this->assertContains('VALUE=DATE', $localdate->serialize()); + $this->assertContains('20130901T120000Z', $utctime->serialize()); + $this->assertContains('20130901T100000Z', $asutctime->serialize()); + } + + function test_get_vtimezone() + { + $vtz = libvcalendar::get_vtimezone('Europe/Berlin', strtotime('2014-08-22T15:00:00+02:00')); + $this->assertInstanceOf('\Sabre\VObject\Component', $vtz, "VTIMEZONE is a Component object"); + $this->assertEquals('Europe/Berlin', $vtz->TZID); + $this->assertEquals('4', $vtz->{'X-MICROSOFT-CDO-TZID'}); + + // check for transition to daylight saving time which is BEFORE the given date + $dst = reset($vtz->select('DAYLIGHT')); + $this->assertEquals('DAYLIGHT', $dst->name); + $this->assertEquals('20140330T010000', $dst->DTSTART); + $this->assertEquals('+0100', $dst->TZOFFSETFROM); + $this->assertEquals('+0200', $dst->TZOFFSETTO); + $this->assertEquals('CEST', $dst->TZNAME); + + // check (last) transition to standard time which is AFTER the given date + $std = end($vtz->select('STANDARD')); + $this->assertEquals('STANDARD', $std->name); + $this->assertEquals('20141026T010000', $std->DTSTART); + $this->assertEquals('+0200', $std->TZOFFSETFROM); + $this->assertEquals('+0100', $std->TZOFFSETTO); + $this->assertEquals('CET', $std->TZNAME); + + // unknown timezone + $vtz = libvcalendar::get_vtimezone('America/Foo Bar'); + $this->assertEquals(false, $vtz); + + // invalid input data + $vtz = libvcalendar::get_vtimezone(new DateTime()); + $this->assertEquals(false, $vtz); + + // DateTimezone as input data + $vtz = libvcalendar::get_vtimezone(new DateTimezone('Pacific/Chatham')); + $this->assertInstanceOf('\Sabre\VObject\Component', $vtz); + $this->assertContains('TZOFFSETFROM:+1245', $vtz->serialize()); + $this->assertContains('TZOFFSETTO:+1345', $vtz->serialize()); + } + + function get_attachment_data($id, $event) + { + return $this->attachment_data; + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/alarms.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,56 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 5.0.3//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Zurich +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:CEST +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:CET +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE + +BEGIN:VEVENT +UID:1dq52u617gkfqrr4uo1i2uh70 +CREATED:20130924T221822Z +DESCRIPTION: +DTSTART:20130818T230000Z +DTEND:20130819T010000Z +DTSTAMP:20130824T235608Z +LAST-MODIFIED:20130924T222118Z +LOCATION: +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:Alarms test +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:This is the first event reminder +TRIGGER:-P0DT0H30M0S +END:VALARM +BEGIN:VALARM +ACTION:EMAIL +DESCRIPTION:This is an event reminder +TRIGGER:-P1D +ATTENDEE:mailto:reminder-recipient@example.org +SUMMARY:This is the reminder message +DESCRIPTION:This is the second event reminder +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:An absolute reminder +TRIGGER;VALUE=DATE-TIME:20130812T160000Z +END:VALARM +END:VEVENT + +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/apple-alarms.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,50 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.9//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Zurich +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:MESZ +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:MEZ +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +TRANSP:OPAQUE +DTEND;TZID=Europe/Zurich:20131106T100000 +UID:EF185A2A-55FA-4FF3-9B02-56B0914FC79A +DTSTAMP:20131029T123927Z +LOCATION: +DESCRIPTION:With alarm +STATUS:CONFIRMED +SEQUENCE:4 +X-APPLE-TRAVEL-DURATION:PT30M +SUMMARY:Testing Bug 2415 +LAST-MODIFIED:20131029T123819Z +DTSTART;TZID=Europe/Zurich:20131106T090000 +CREATED:20131029T123819Z +BEGIN:VALARM +X-WR-ALARMUID:C4A26F1A-A433-4102-82D5-A3347FC126D4 +UID:C4A26F1A-A433-4102-82D5-A3347FC126D4 +TRIGGER;VALUE=DATE-TIME:19760401T005545Z +ACTION:NONE +END:VALARM +BEGIN:VALARM +X-WR-ALARMUID:DEF5F23D-98FC-4510-BC99-F877CD9A9F8B +UID:DEF5F23D-98FC-4510-BC99-F877CD9A9F8B +TRIGGER;X-APPLE-RELATED-TRAVEL=-PT15M:-PT45M +ATTACH;VALUE=URI:Basso +ACTION:AUDIO +END:VALARM +END:VEVENT +END:VCALENDAR \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/attachment.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,344 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube//Roundcube libcalendaring 1.0-git//Sabre//Sabre VObject + 2.1.3//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:93B331265CF47061F888BC0A3F0FBD7F-FCBB6C4091F28CA0 +DTSTAMP;VALUE=DATE-TIME:20131017T084408Z +CREATED;VALUE=DATE-TIME:20131017T083610Z +LAST-MODIFIED;VALUE=DATE-TIME:20131017T083610Z +DTSTART;VALUE=DATE-TIME;TZID=Europe/Zurich:20131017T110000 +DTEND;VALUE=DATE-TIME;TZID=Europe/Zurich:20131017T120000 +SUMMARY:Event with attachment +LOCATION:Test lab +TRANSP:OPAQUE +CLASS:PUBLIC +ORGANIZER;CN=Bruederli:mailto:thomas.bruederli@example.org +ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=500px-Opensou + rce.svg.png:iVBORw0KGgoAAAANSUhEUgAAATwAAAE8CAYAAABdBQ0GAAAACXBIWXMAAAsTAA + ALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iA + lEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIis + r74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz + /SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAY + CdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgw + ABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEO + cqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9 + eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZr + IPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFH + BPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t + 8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmS + cQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQi + iGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4 + IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDua + g3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7Ei + rAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqE + u0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBoj + k8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt + 2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX + 6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlS + aVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z + /YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1 + ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aed + pr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6 + feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8 + o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+ + eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZn + w7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt + 6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRK + dPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7 + XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/ + 0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc + 5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+ + qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpx + apLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkk + xTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty + 8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2 + pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6w + tVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm + 6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+l + Q27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHt + xwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTrado + x7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fy + z4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdO + o8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9 + zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDY + brnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bC + xh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0 + hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAOB5JREFUeNrsnXl0HFeV + /7+3etGulmRJlm15keUt9jgGR1VyrLBDMoRlfmyBDBMYfjBsA2HffmEbtgFmhgkTwjJs2YAk7B + kCwSEkECSsKtmJ7XiRJVmWtViLtbWWVlcv7/eH2x4SYtWr7uru6u77OUcn58Svuqvve/Wte9+7 + 7z4SQoBhGKYQUNgEDMOw4DEMw7DgMQzDsOAxDMOw4DEMw7DgMQzDsOAxDMOw4DEMw7DgMQzDsO + AxDMOw4DEMw4LHMAzDgscwDJPbeJO5qK6uji3HSKNpWoCIFI/H4zFNs/JJb1xFMYloAQC8Xu9C + e3u7yRZjZJiYmLB9DSVTHooFr3Bpa2vzR6PRdQA2xOPxDUS0SgixAsBf/tUm/iqT/BoTwORT/s + 4BOEdE5wCcEUKcjsVipw8ePDjJvcKCx4LHpAKpqrqBiHYB2CmE2AygCcAGAKvhrqmQOQCnAZwC + cIqIjgM4FA6Hnzh06NAidyULHgsec5Fdu3aV+v3+XQCeQUS7hBA7AfxNCt6ZW4gD6ANwCMARIj + qsKMqB/fv3D3Kvs+Cx4BUILS0taxRFeTaAKwHsFULsQpJzuTnKKIA/E9Gj8Xi8o6am5uC+ffsi + PDJY8Fjw8oDW1tZNQohrhBBtANoArGOrPIkQgC4AjwL4g8/n+2NHR8cSm4UFjwUvR0LUoqKi5w + ohXgzgbwFsYqvYYgHA74joASJ6oLOz8zSbhAWPre0i9uzZszIWi70KwN8BeDaAYraKY5wgol8L + IX5mGEYHAD71igWPyTR79+6tjUQirwJwHYDnAPCwVdLOEIAfCyHu6erq0ln8WPCYNNLW1lYeiU + SuE0JcB+AFcNlig6Io8Hq98Hq98Pl8UBQFinI+g8XjOa/HRHTx/wkhEI/HAQDxeBwXxmIsFkMs + FkMkEkE0GkU0GnVjd5wmonsB3KXr+hEenSx4jENomqYB+CchxOsAlGfjHnw+H4qKiuD3+1FUVH + Txz+fzXRS5C6LmNEIIRKNRRCIRxGIxhMNhmKaJpaUlmKaJcDiMcDiMLJ6x3AngO36//+729vZ5 + HrEseIx9kQsAuEEI8U8ALs+ksJWVlaG4uBilpaUoLS1FcXFx2sTMSVE0TROhUAiLi4tP+m8GhX + AewA8BfNswjC4exSx4jAWqqm4novcLIf4eQEm6Q9Dy8nKUl5ejoqICpaWl8Pv9eWXPeDyOUCiE + +fl5zM3NYX5+HuFwOBNf/RgRfaWqquoezvNjwWP+2qN7HoAPCCGuBUDp+A6Px4PKykpUVlaivL + wcZWVlIKKCs3U4HL4ogMFgEKFQKJ1fN0hEt8ZisW8dOHBghkc6C17BcvXVV/ump6evI6L3CSGu + SMd3lJaWIhAIIBAIoKKi4uKCAfNkAZydnb34F4vF0vE1c0T0XSHEzYZhDLDVWfAKhra2Nq9pmj + cA+ATOb8p3DCJCIBBAdXU1AoEAioqK2OA2Q+C5uTnMzMxgeno6HeFvlIhuF0J8loWPBS/fhU4x + TfN1AD4FYIuTIldZWYmamhpUV1fD5/OxsR1ACIH5+XlMTk5ienoapuloqT6TiL4Tj8e/0NXVNc + zWZsHLJ0jTtFcJIT4NYIdTIldRUYGamhrU1NSwyGVA/Obm5jA1NYXJyUkncwNDRPQtRVG+uH// + /jG2NAteTqNp2lVCiK8C2O3E5/n9fqxYsQJ1dXUoKSlhA2cp7J2amsLExASCwaBTHzsP4ItCiK + 90dXWF2MoseLkmdOuEEF8E8DqkuOp6IWStr69HVVUVLzy4iFAohImJCUxOTjoV8p4hoo/qun43 + eOsaC57bSWz/+qgQ4v1IMY/O4/Ggrq4ODQ0NvPiQA17f9PQ0RkdHMT/vyGaLR4UQ7+nq6nqMrc + uC51av7johxL8hxZpzfr8fDQ0NqKurg9frZcPmGMFgEKOjo5iZmUl1l0eciL4fjUY/wmd3sOC5 + SegahRBfB/CyVD6nrKwMq1atQk1NTUEmBOcbi4uLGBsbw7lz5y4WSUiScQDvMQzjbrYqC142w1 + fFNM23A/hXpHAGRFlZGRobG1FVVcVGzUNM08TIyAgmJiZSEj4iuh/A23VdH2KrsuBl2qvbKoT4 + DoCrkv2MkpISNDY2orq6mj26AiAcDl8UvhRC3Tki+pjP5/tGe3t7vJDtyYKXGa/Oa5rmBwF8Gk + BSKwklJSVYs2YNh64FytLSEkZGRnDu3LlUhO9RAG82DKOHBY8FLy2oqtoM4C4Ae5K53uv1Yu3a + tairq2OhY7C4uIjBwUHMzMwk/REAPmgYxjdRgCksLHjpFbs3AfgqgAq71yqKgpUrV2L16tW86s + r8FbOzsxgYGEilast9iqK8pbOzc6KQ7MaClwZaW1urhRDfEkK8Jpnrq6ursW7dOhQX87k6zKWJ + x+MYGxvDyMhIstvWRgG8yTCMB1jwWPCSQtO05wkhbgew1u61xcXF2LBhAwKBAD/NjDTRaBSDg4 + PJLmwIAP/l8/k+Wghn67LgOQepqnoTgH8BYGsfFxFh1apVWLNmDW8BY5Jmbm4Op06dwtJSUrr1 + GIBXGYbRz4LHgmfl1QWEEHciiSTisrIyNDU1oaysjJ9YxpEwd3h4GGfPnk3G25skotfruv5bFj + wWvKdlz549fxOLxX4GYLOd6xRFQWNjIxoaGnj1lXGcxcVFnDp1CgsLC3YvjQH4lGEYX0AeruKy + 4KWAqqqvBfAd2DwGsbS0FBs3bmSvjkm7tzc0NITR0dFkvL37fD7fDR0dHcF8sgkLXnKQpmmfF0 + J8zNZFRKivr8e6det4ro7JGLOzs+jv70+m9PwJAC81DKOPBa9ABW/37t1FXq/3tsQB19L4fD5s + 3LiR974yWSESiWBgYACTk7YLqIwT0d/pur6fBa/ABK+lpaWGiH4Jm3thA4EAmpubuaQ6k3XGxs + Zw5swZuwUJQkR0g67rPy1EwSvIWExV1WYi2m9X7FatWoWtW7ey2DGuYOXKlbjsssvsFoctEULc + q6rqBwrRZgXn4bW0tLQS0a8A1Mpe4/F40NTUhBUrVvBTxrgyxO3t7U3mfI1b/X7/jbladYU9PG + vP7rlE9Ds7YldcXIzt27ez2DGuxefzYdu2bWhoaLB76T9HIpE7tm7d6ikUWxWM4Kmq+mIA98NG + 2klVVRV27NiB0tJSfqoYd4dqRFi/fj2am5ttZQ0IIV5fWVn547a2Nj8LXv6I3SsA/AKAtHLV19 + djy5YtXN2EySlqa2uxbds2u/PMrzBN8xe7du3K+zd73gueqqqvB3AvAKk3GBFh7dq1aGpq4l0T + TE5SUVGB7du3263Q82K/33+/pmkV+WybvBY8TdPeCOB2AFJumqIoaG5uxurVq/mpYXKaC3PPFR + W29Ou5QogH2trayvPVLnkreKqqvkYI8V0AUhOyFyZ+eXGCyRcujOmamho7l+01TfOX+Rre5qXg + qar6EpwvxS4ldn6/H9u2bbP7NmQY9z/gioJNmzbZTSV7flFR0b35uJCRd4KnqurzAfwYknN2F8 + SOV2KZfIWI0NTUZCttRQjxEtM0f5BvKSt5JXiqqu4F8D8ASmTaFxUVYfv27SgpKeGngsl70Vu/ + fr3d+elXV1ZWfhdA3qze5Y3gaZq2E8CvIZl6UlJSgu3bt9vdlsMwOc3atWuxdq2tEwveqGnaF1 + nw3OXZ1Qshfg1A6gCJkpISbNu2DX6/n58ApuBYvXo11q9fbye8/bCqqu9gwXMBidWk+wA0yrQv + Li5msWMKnoaGBqxZs8bOJbdomvZyFrwssnXrVo/f7/8RgFaZ9hcWKFjsGAZobGzEqlWrZJt7hB + A/am1tVVnwskRlZeUtAKTeOn6/P5lSOgyT16xduxb19fWyzUvj8fjPNE1rzNXfm7OCp6rqOwFI + zStcSMDkw7AZ5skQETZs2IDaWukCQo1CiPtzNTE5JwUvkX7yn1J+uMeDLVu2cOoJwywjehs3bk + R1dbXsJZf7/f7vsOBlgD179qyEZDEAIkJzczPKy8t5VDOMxLNiY7fR9aqqvpcFL420tbV5Y7HY + vQCklpfWr19v563FMAWNx+PBpk2b7Mxzf1lV1Wex4KUJ0zS/DODZMm0bGhqwcuVKHsUMYwO/32 + +nDqQPwL2apuVMeaGcETxVVV8D4H0ybaurq7Fu3ToevQyTBKWlpdi0aZNs5eQGAPe2tbXlRKXc + nBA8VVXXA/hvmbZlZWVobm7m4p0MkwKBQAAbNmyQaiuEaDNN8xMseA6QqNZwO4AqS//a58PmzZ + vh8Xh4xDJMitTV1dnJ0bspkT3BgpcKlZWVHwLwHKt2F1aZOLGYYZxj/fr1KCsrk2nqAfCDvXv3 + VrLgJUliG8tnZNo2NjYiEAjwCGUYJwVCUbBlyxbZQ4E2RCKRr7HgJYGqqmXxePwunF8JWpbq6m + o7ewIZhrGB3++3My9+g6Zp17Pg2YSIvgxgi1W74uJibNy4kRcpGCaNBAIBNDbKbaEVQtzq1lQV + VwqeqqrPF0JY7pMlImzatInPjmWYDLBq1SrZaaNqIcS3WfAkaGlpKcH5FBRLl62xsVF2QpVhmN + SjLmzcuFF2Pu9aN4a2iguN+i8Amq3aVVZW8rwdw2QYv9+PpqYm2dD25t27d7vq3FNXCZ6mabsh + sZvC6/XyvB3DZInq6mrZ/Lx6r9f7FRa8p6Gtrc0rhPgOAMsJufXr13O+HcNkkXXr1knVlxRCvE + HTtBex4D2FSCTyAQDPlHm72ChWyDBMGvB4PGhqapKKsoQQ31RV1RWT7a4QPE3T1gkhPiljZDun + LTEMkz4qKytRV1cn03QjgI+z4P3vG+DfIHGe7Nq1azmUZRgXsXbtWtlDsd6nqmpTtu8364KXKC + D4Gqt2FRUVdjYyMwyTAbxer2zUVQTgPwpa8Nra2hQAX4FFzh0RSc8XMAyTWWpqamQri79C07Tn + FazgRSKRfwTQYtWuoaGBD+FhGBezbt06qYKhQoibEyXfCkvw9u7dWymE+LxVO5/Ph9WrV/OIYh + gXU1xcjIaGBpmmlwcCgX8qOMGLRqPvx/ny0MuyZs0a3ivLMDnA6tWrpRYwhBCfzlaaSlYEr6Wl + pUYIYbmjorS0lBcqGCZH8Hg8shVVVhLRuwpG8IjoQwAsK6OuX7+eFyoYJoeora2VOgdaCPGhbF + RHzrjgqapaD+DdVu2qq6tRWVnJI4hhcggikj0xcEUkErkx7wWPiD4MoMzKaGvWrOHRwzA5SEVF + BaqqqmSafqC1tbU6k/eWUcHTNG21EOKdVu1qamq4zh3D5DCSDkuVEOL9eSt4iYWKEvbuGCa/KS + 8vl0pGFkK854orrqjK1H1lTPASP+ptloH9ihWcZMwweeLlSSw6Vng8nrfkneAlflQFe3cMUxiU + lZWhpqZGxst7d1tbW0aSbTMieIninpYrs3V1dVJFBRmGyQ1Wr14t4+WtM03z7/NG8EzTvA7AOi + vvbuXKlTxCGCaPKC0tlT3p7L35FNJ+0KpBIBBAaWkpjxCGyTMk99g+U1XV5+e84Gma9mxIlG6X + NArDMDlGIBCQSjMjorSnqGTCw3urVYOysjJZt5dhmBxEZrpKCPG3mqY1pvM+0ip4ra2t1UKIVz + phDIZhcpcVK1bIHM/gEUK8OWcFTwhxAywSjf1+P1asWMEjgmHyGEVRZKet3pSohJ4W0pr7IoSw + TCisq6uTqpTK5A6maWJpaeniXzgcRiwWQzQahRACsVgM8XgcHo8HXq8XiqJAURT4fD4UFxc/6Y + /HRv5QW1uLwcFBxOPx5ZqtN03zagAP5JTgqap6JYCdy7UhItlj3hiXEovFsLCwgNnZWczPz2Nx + cRHRaFTq2kgkAqvxUVxcjLKyMlRWVqKiooLzNHMYr9eL6upqTE5OWvX7W3JO8IjozUKIZdsEAg + E+djEHCYVCmJqaQjAYxPz8vNUbO5UIAaFQCKFQCOfOnQMAFBUVobKyElVVVaiqqmIPMMeor6+3 + FDwhxMv27Nmzcv/+/WM5IXi7d+8uEkK8WiacZXInTJ2ensa5c+cwPz+ftfsIh8OYmJjAxMQEvF + 4vampqLhad5GKx7ueCl760tLRcM388Hn81gFtzQvC8Xu81Qohl80z8fr9szSwmiywuLmJkZART + U1Ow8tgzTTQaxfj4OMbHx1FcXIxVq1ahtraWvT4Xc2Eaa3Bw0MrLe206BC9dI+M6qwYrVqzgge + li5ufncfLkSTzxxBOYnJx0ndg9laWlJfT39+Pw4cMYHR1FLBbjTnQptbW1Mt54W0tLi+OVRBxX + nL179xYLIV4u86MZdwpHd3c3jh49iunpadcL3dOFvAMDAzh06BDGx8dz7v4LAb/fL7PRQCGi17 + he8CKRyLWwKANVUlLC+2ZdRjwex9DQEI4cOYKZmZmc/z2RSAT9/f04fvw4FhYWuINdhkzZKACO + C57jc3hE9Bqrtyp7d+5iZmYGp0+fRjgcTufXBAEsAFggolkAEEIU4fz5JlWJl6Tj43Fubg5Hjx + 5FfX091q5dC4/Hwx3uAqqrq6EoitUK/5Wapq3Tdf2MKwUvsTr7Upkfy2SfWCyGgYEBTExMOPmx + xwD8CcBxACeEECfn5uYGuru7LSfVrrjiiiqPx7NFCLENwFYAuwFcBaA8lRsSQmBsbAwzMzPYuH + Ejn4bnArxeLyorK62iCRJCvALAV10peF6v99lCiGUHZ2lpKZdwdwELCwvo7e21Sg+QYZaIfgbg + QSHEw4ZhjCb7QQcOHJgBoCf+AABXX321b3p6WiOi5ycG/zOT/fxwOIwTJ05gzZo1soUpmTSyYs + UKmemTa50UPEpmUvdS+XOapv27EOIDy127du1arF69mns7i4yPj2NgYCCVhOEYgN8CuFMI8cuu + rq5Qpu5d07SdiT3arweQ9ECqrKxEc3Mz/H4/D4gsRhgHDx60GodLQoiapxtjyUQmjnp4QogXW7 + WRnKxk0kA8Hkd/f//FXQtJenO3CCFuTcWTSwVd148A+PDWrVs/FggEXiKE+AiAvXY/JxgM4ujR + o9i8eTPKy8t5cGQBj8eDqqoqTE1NLdesmIieD+B+V4W0mqatE0JsX65NSUkJ74XM4tu0t7c32R + XYCSK62ev1fq2joyPoht+TmBO8D8B9mqY9D8BNQogX2PkM0zRx4sQJbNq0iZPgs0QgELASPBDR + NU4JnpNpKdfK/Dgm80QiERw/fjwZsQsR0SdN09yg6/oX3CJ2T+P1Pazr+gsBtAF4zO6L4OTJk6 + l4vUyKgicROV7r1Pc5JnhCiGus2vBbNPOEw2EcO3YsmVy0XwHYoev6Zw8dOrSYC7/VMIyOYDCo + ArgRwIyNsYtTp05hdHSUB0yGKSoqksnJbVZVtdk1gpco2PecZb9IUVBRUcE9nEGi0ShOnjxpdy + U2COB1hmG8zDCM/lz7zd3d3THDMG4hoh0AHrYjemfOnGFPz6VeXmIezx2CFw6HLwOwbHJdZWUl + 753NIBdCtcVFW87Z4wBaDMO4J9d/v67rI36//4VE9EmcX1WWEr3+/n7Mzs7yAMogMpGfEGKvE9 + /liAIR0VVOqDjj2PQC+vr6MDc3Z6cPv+vz+a40DKMnX+zQ3t4e13X9s0T0IgBTMtfE43H09PRk + tQRWoVFeXi6zA+YqJ77LKZfrShY89zA0NITp6Wk7l3xW1/W3dHR0LOWjPXRdfxjAswAMynrHPT + 09ME2TB1MGUBRFZvfLJlVV690ieG3L/eOFswqY9DM7O4uzZ8/KNo8DuNEwjE/mu10Mwzjm8Xja + cH7rmyWmaaK/v5+rrWTQy0tVZzIieHv27FkJYNNybcrKyngbTwYwTRN9fX2yD2kMwOsNw7ilUO + yzf//+wVgs9mwAXTLtZ2Zm7Lw8mBSQWdCUmTpLu+DFYjHLyUTerJ1+LszbWR2Mc6E5Eb3VMIy7 + C81OBw8enFQU5VoA3bLTA3bmQpnkKCsrs1zUdGLhwomQtsUhd5VJgdHRUQSD0nnB/0/X9e8Vqq + 06OzsniOiFAIZkXiSnTp3iCsppRlEUmXy8Z7S1taW0O8wJwdtt9UPKysq4R9NIOBzG8PCwVFsi + utkwjC8Wus10XR8iomsBWOagLC0tSduXSWtYWxyJRC7LtuBdvtw/lpaWcv5dmjl9+rSsB/IbXd + ffzxa7KHpHiOi1OL94Y+lB28xpZGwiGQk+I2uC19raWgeLEj1cyj29zMzMyO6RHfb5fG8AwMuO + Txa93wKw9HiFEBgYGOBV2zQioxVCiJ1ZE7x4PL7TiR/BJEcsFsPp06elmgK4vqOjg/dNPQ3BYP + CTAB6VaGd5iDSTPEVFRTIJyNkTPJkvZ8FLH2NjY1LnUBDRlw3DeJQt9vR0d3fHiOgfAFhurxga + GkqlcCqz/DiVqYaePcEjol1Wbbice3qIx+MYGxuTaToQDoc/xxazDG3PEJGlncLhMHt52Q1r17 + S0tCRdRThVD+9yKxfV6/VyL6aBiYkJ2a1P78mV8k7Zpqqq6is4f/jQsoyMjPBcXvYED0SUtJeX + kuAJIS5j7y473p3kDoAHDcP4JVtMjn379kUAvNeq3dLSEnt5aUJyC+qOjAueqqoNAEqtPDzGea + ampmTPkP0sW8sehmHsw1+cmnYpeMtZepDUjKaMCx4RbbBqwydCpQdJ7+JRXqhIemxbzuUtLi4m + U0WakRA8q333RJR5wRNCWAoeV0hxHtM0pQpUEtHn2VrJoev6rwAcsmrH1ZHT8rKxdJSEEBszLn + gALAWPQ9r0eHcSE+aPJRJqmSTf55BIRp6amuLFizQg4ShtSPazOaTNw3CWiL7LlkqNWCz2cwDT + Vt62jYINjCQSulGtaVpSFYXTFtJ6PB74fD7uPYfDWYl5I9Pr9d7D1kqNgwcPhonoXqt2SZ7zy6 + QYGco4XBkNadm7cx5Jb+I3vIXMobhWiDsd6hPGWQ8PQoh1mRa8ZbOd2btzHslClHewpZzBMIwO + AH3LtQmFQrJFVxlJJLWjLpOCRwBWLNeAd1g4j8TqbFgI8Ru2lHNOHhH9wsLT4IrIDiOpHUltL0 + tK8Hbv3l1jdS0LnrOEw2GZZOM/d3V1hdhajoa1lod5s+BlRfAy5+H5fL4VVm0kyrwwNpAsPvkw + W8rx8OpRAFEH+oaRRFI7ViTz2UkJnhCiRmKgcM+x4OU8HR0dQViccsY7Lpz38Kx2W8hokGOCJ+ + NOckjrfEhr9R7y+/2PsaWch4gOLvfvsViMD+121t4y+pE5Dw9AFQteZgmFLKfmhtrb2+fZUmnB + smTU0tISW8lhL8+C6kyGtJanbfDB25kVPCI6yVZKGz0seJn38iyoSOZz03acGC9aOEckEpE5la + ybLZUehBAnWPAyi8RJh0lpV1IXERGfrJ1hwZN4KHl3Rfo450QfMY56eElVF042pPU4cMOMJDJn + zhIRJ4OlCcMwFmBxdi0f7JNxwUtq72qyIa3l7l4+fNs5JB8mXrBIL3OpvpQYeSSmxJJaFU1Wlb + iyp8s8PCEE72JPL8vaNxqNsoUyS1kmBc8JhWYc9PA4pM2uh8eFQHNDPzjuzBOEELy1Jb2wfTM7 + nnNL8HhOI+NvO55mSC/LrgrynHXmo5pMCh6vwWcQmRVvIqpkS6WVChY8V5FUxQYlk1/GpM/DE0 + LwiUnppdQBL5xxLqRNyunikDYHkCl5TURr2VLpYc+ePSthMYfH1YFyQz+SFTzeR5NhwZMIa9ex + pdLmbVjalo8kzThJ1eRKVvAsa42zh+cckocTr2dLZU/w+NCqjHt4SeWdpk3wOBHTWSQ8iI1spb + QJXrNVG4nDoxlnBW82mc9Ndi8tC16GKS0ttWpSq2kah7XpYbeVB15SUsJWchAJ/cic4BGR5Zfx + ZuqMCx4AaGyptLCsXUtKSniV1lmP2tLDk9EgDmlzmLKyMpmBcjVbyllUVd0MoCnVvmEc9e4y6+ + EpinLOoZtmJJH0Iq5hSzkLEVnaVNL7ZhwUPCHEeMYEr7OzcxoWqSlcENHxB0/mwVqnqup2tpaj + /K0T3jcjj+SBSKOZDGkBYIQFL7NUVFiX8SeiV7GlnOGKK66oEkK8YLk2Ho8H5eVcANxJZLSDiE + YyLXijDqg0Y4OqqioZV/8NbCln8Hg818GiKEMgEODq3lkQPCHE2YwKHhGdtYrDuUaYs5SXl8sc + X7dJVdUr2VqOYPnyqKzkmg3ZEDwAmRU8IcSwxb+zl+cwRCTl5QF4E1srNVpaWrYJIfZa9Ud1dT + Uby2EkdCMei8UyG9ISUZ9Vm3A4zL3nMCtWSB24/g+tra11bK2UXi7vB7BsrFpRUcFbytKAxJGX + wwcPHkxKXFLx8PocuHHGJoFAQOYhKxFC3MjWSo7Ey+IfrNrV1fE7JUse3qlkPzutgsceXnrCWh + kvTwjxdlVVOV8iubH9HlhUOPZ4PBzOpoFoNGo5h0dEvRkXvKKiolMAYix4mae+vl5mZbAWwAfY + WvbYs2fPShnvuLa2lreTZSeclXK2HBe89vZ2E8AQh7SZp7i4GDU1NTJNP5goXslIEovFPg2Lcu + 5EhIaGBjZWGpB0kjIf0iY4biV4nJqSHiQfuIp4PP4vbC05NE27DMBbrNrV1NRwOag0EQqFLNsQ + 0bFsCd4Ri7clh7Vpory8XDYR+S0tLS2tbDHr50gI8XVYnGhPRFi9ejVbK3uCF66qqjqRLcF73A + nFZpJj3bp1MnN5HiL67u7du7kG+fLe3dsAPNeqXW1tLRcLSCOLi5bngx3ft29f0vtWUxI8Ijri + wA9gkqSkpAS1tbUyTXd4vd6vssUuKXY7hRD/Yfnm8HjQ2NjIBksTkhHhkVS+IyXBS7iWERa87N + HY2Ciz3QxCiLepqvp6ttiT2bt3b6UQ4sewOIYRAFavXs2Jxmn27qzm/GWcrLQJXsK1XHYCcX5+ + nnsyjfj9fqxbJ13Z/Vuapu1kq51n69atnkgkcieArVZtS0tLsWrVKjZaGpHRCiHEoawJXkJxDy + 7376Zp8sJFmqmtrZXdY1smhPi1pmkclwGorKy8BcDLJcY4Nm7cyFVRXCB4Pp/vYFYFD8ABqwYL + Cwvcm2mEiLBhwwbZRNhGIcS+Qt9rq6rqRwG8Q6btqlWruMhnBpDQid6Ojo5zqXxHyoInhNjvhH + IzqVFUVIT166WPpr1MCPGblpaWmkK0laqq7wDwBZm2JSUlWLNmDQ+wNCMTCRJRV6rfk7Lg+f3+ + QwCWXZmYm5vjHs0AdXV1squ2EEJcQUQPFZqnp6rqewHcCotKKMD5VdnNmzdDURQeXGlGRiNknK + u0C157e3sUQJeVq8ol3zPDhg0b7JyR+ox4PP5IoczpaZr2CQD/KSN2ANDU1MTnzWaIYDBo2YaI + /px1wUvcyH4LZWYvL0N4PB5s2rRJKlUlwXYhRKemaXl7pu3u3buLVFW9TQjxGdlrGhoaZGsPMp + kRvCWfz/e4KwQPQLsTCs44Q2lpKTZt2mQnFFsthHhE07Tr8s0Wra2tdR6P53cA3ih7TXV1tZ1U + HyZFwuGwTKGRrkTBkuwLXiwW+yMsSkWx4GWWQCCApqYmO6kUJUKIe1RV/fquXbvyYu9Ua2vrC+ + Px+CEAV8leU1FRgebmZk5BySCzs1Jnav/eie9yRPAOHDgwA4v0lFAoxPl4Gaa2thbr16+3+/C+ + w+/3G6qq7s3V393W1lauqurN8Xh8HwDpbOGysjJs3ryZ69y5L5wFET3kGsGTVeCZmRnu3Qyzcu + VKu54eAGwH8CdN0769d+/e2lz6vZqmvdo0zeMA3gPJxYkLnt22bdvg8/l40GQQIYSMh7fo8/n2 + O/F9jgkeEbHguZS6urpkRI+EEG+JRCJ9mqZ9WtO0gNvDV1VV/5zYF2tr1bmyshJbt261s9DDOM + Tc3Byi0ahVs0edmL8DLGp/2SEej/+JiEJY5iyAYDCIWCzGIUOWRM/v96O3t1dmgD1JD4QQnwJw + o6Zp/0lE3+zs7JxwSeiqRCKRFwkhborH489K1i4bNmzgXLssMT09LdPsQae+z7Fe7urqCll5ef + F4nBcvskggEMD27duTrdZbLYT4TDweH1RV9Y7W1lY1i2FrQFXV95qmeUII8QAA22JHRFi7di02 + btzIYpdFZKI+IcT9rhO8xI39j1Wbqakp7uUsUlJSgh07dkjvyHgaigDcEI/HdVVV79i9e3dGk9 + VUVX2tEKIX5xOINyfzGX6/H9u2bePKxVkmFArJpKP0dXV1nXDqOx0VPCL6HwDCyoWNx+Pc21nE + 6/WiubkZGzduTHV64Qav13sPbCwOpCh2VwP4Ec6fyJYU1dXV2LlzJyorK3kgZJnJyUmZZr9y8j + sdFTxd10cAPLZcm1gsJpt3w6SZurq6lB9+IcQLNE17aYZu+eZkxdXj8aCpqQlbtmzhxYkcEjwi + +qVrBe8vvDwnlJ3JAEVFRdi2bRuampqSFgIhxJ503+cVV1xRBWBbMteuWLECO3fuRH19PXe4S1 + hYWJAJZ2eqqqr+5GrBA3CP5a+YmUEsFuNedwlEhPr6euzatUv2kO+nXp/2szg9Ho+w690VFxdj + 27Zt2LRpE4qK+AyjHPTufp7KgT0ZETxd148DOGwV1nJOnvvwer1oamrCjh07UF1dbefSgXTfm6 + 7rswB6ZL3WDRs2YOfOnQgEAtyxLkMIIbV4KYS42+nvTtd6vOWNjo+Pc8+7lLKyMmzZskX2hK5Z + r9d7Z4Zu7UcyjS6//HKsXLmS001cyuzsrMw203G/3/97p787LSNCCGEZ1s7NzcnE8EwWkTlxjo + ju7ujoyFRH3gaLLAAigmma3Hku5tw5qSrtP03U2nS/4HV1dZ0C0Gnl1kr+cCYLRCIR2aTQ2zN1 + T4Zh9AN4lMdV7mKaptTuCiL6YTq+P20+PxHdJqP0VudQMtlhampKJl+y2zCMP2f41r7P4yp3mZ + yclBlXPbqut6fj+9M5yfEjWJx1EQ6HefHCpUxMSG2XvS3T9+X3+38CYMFqXPEWRvchhJAdV9+z + mrpwneAlVtV+atWOFy/cx+LiosyReXEiuivT99be3j5PRJbjisNa9xEMBhEKhayaRYnojnTdQ7 + qXsb5r1WB2dlZqcpxxnXf3oK7rQ1nyFCzD2unpac71dBmjo6MyzR5I7NjKPcEzDOOPAE5aublj + Y2M8GlwUdsjkSBHR7dm6R7/f/0cAp5drE4vFeEePi1haWpLaUkpE307nfaTbwxMAvmbVaHJy0m + 6NNiZNzM7OyqR1zMbj8V9k6x7b29vjAO6QGVeMe7w7iYWkMz6f71fpvI+0Z2YmVmvnrd7GPJfn + DmTmvojo7q6urlA27zORDrPsE8S5nu4gEonIjqtbEi+z3BU8XdfniMhyzmVsbIzLRrlgYMrkSA + kh7sj2vSZyPTknLwcYHx+XmU+dj8Vi30n3vWRk7w0R/ReAZdXMNE328rKMjdy7Dpfc8m0yHivn + 5GWPWCwmO0d/Z+L0w9wXvM7Ozl5IFPI7e/Yse3nuD2fvcMv9EhHn5OWAdxeJWBY8iRPRLZm4n0 + zurv6SVQPTNHmiOUssLi5ifn7ecmBCYrEgUySmSzgnz6XE43HZVJRfJKos5Y/gJcKgR6zaDQ8P + s5eXBSRfNFnLvUslrOWcvOx5d5KFHP41U/eU0fo5RPQFqzbhcFg28ZVxCBuT+3e47d59Pt8fwD + l5riMWi+Hs2bNSL1HDMLryUvB0XX8QgGHVbmRkhN/IGUQ2904I8XO33XsijcGyHh8LXmYZHR2V + 9e4+n8n7yniFRCL6nFUb0zR590UGkfTu7s127t0ycE6ei4hEIrJzd48ahvGHvBY8XdfvA2BZUm + h0dJR3X2RocEqe/n6bW3+DYRh9kMjJ46mSzDAyMiL17BLR/8v0vWWrBvbHZR7E4eFhHj1pRrI+ + 2UkX5d5d6uG5Xea3ck5eellaWpLKpyWi+3Vd/1Om7y8rgmcYxu8BPGjVbmxsjMOQDAieE2LiAn + 4MzsnLOkNDQzIvUBGPxz+RjfvL2iknRPRxWMy7CCFw5swZHkVpIhdz7y6FrutzAH7mhMAzyREM + BmXte29XV9djBSV4uq7rRGR52M/09LRUWRkmPd4dgN+5MPfuUtxm1WBqaoozANKADeckDOBj2b + rPbJ9j9xEAlit/AwMDnIychgEquTp7e678Jr/f/wgkcvJk6v0x9hgfH5epkg0ANycOYyo8wdN1 + /QyAf7dqFwqFZJMYGUkkc++Cbsy9uxTt7e1SZed5q5mzmKaJoSGpIGDU5/N9IZv36oaTir8EwH + I5dmRkhBcwHETyob/Hxbl3l/JcbwPn5GWUM2fOyKaQfbyjoyOrq0ZZFzzDMBYAfNiqXTwex8DA + AI8uB4hGozmfe7fMeJLKyWMvzxlmZmZkV/oP+P3+72f7fhWXDNIfEdFDMsblgZo6smeDuj33bh + ksV5W5Tl7qxGIxnD59WqqpEOLt6a5mnDOCd/6lK94BwDLOGBgYQDgc5tGW5nBW5iB1t0JE90Ii + J29ubo4HQwoMDg7KPou3ZrJAQC4IHgzD6JGpphKNRnH69Gl+OydJKBSSyr1TFOXOXP2NiZw8y8 + UWjhaSJxgMylYoH0rk3LoCxU1GjEajXwZgWQhwZmaGy8Gn0bsD8ND+/fsHc/ynWs4XcU5e0s8p + Tp06Jet03Jh4AbHgPZWDBw+GAbwZgOUoHBwc5AO87c8byArebbn+WxM5ecuucnFOXnL09/fLhr + I/MQzDVWlNituMaRjGn4noZqt2sVgMfX19nJBsA9ncO9M0f5HrvzWRk2cZlnNYa4/x8XHZl8Q4 + gH922/0rbjSq1+v9OIBjVu0WFxc5VcX5cPaeQ4cO5YXrzDl5zmLneSOidxqG4bp5J1cKXkdHx5 + KiKP8IwDKb0cYbp6CxkXt3e7785kROXrtDYX5BE4vF0NvbKxtR/VDX9Z+68XcobjVwZ2enAcnD + Pfr7+xEKhXhULkMB5N5dittkPF9e9XfsGTsrhHi3W3+H4mYj+/3+zwF4XMZ76enp4QrJKYazid + y7vHryEzl5y4bonJO3PKOjo7KVdQQRvbWrq8u1IZerBa+9vd0UQlwPiyRS4Hx+GefnXdo2knXv + 7sq33y5bJ4/D2qcnGAzaqUl5q67rv3Lz71HcbvCurq4TAN4lG7aNjIzwKE3uYX4oUb0m70gsXi + wL5+Q9vefb29sr60Q8FovFPuj236TkguENw7gNwA9l2g4PD8tOzhcEQgjZzd135KsNioqKHgaw + rJhzTt5f2+PkyZOIRCIyzeeJ6PpEHi0LnhP4fL53AOiTecD7+vpkQriCYHZ2ViZJNBgOh3+Wrz + ZI5ORJFRRg/vcZkk3sJ6J36rrenQu/zZsrndDR0RFUVfXVOJ9mUGr1durt7cVll12GoqKigh68 + kpPNeZN7t8xDebsQ4iYAdKk2c3NzCIfDBT9mzpw5YydK+m9d15+U4O3m4zCVXOoIwzAeB/BW2f + mHkydPFvTKbTQalQrT8jmcvUBnZ2cvJHLyCv3s2rGxMdlDtAFgv9/vf3cu/T4l1zrEMIwfyGw9 + A85nhttIlsw7pqampHLvdF1vLxCTWCZVF3JO3tTUlJ2dS2NE9Jr29nYzl36jkosdU1VV9WEAf5 + RpOzs7i76+voIcxJLeyh3Is9y7S+Hz+Tgnz5nnJEJE1+XQaXa5LXj79u2LKIryagC9dt5chSR6 + srl3hRDOXiBxngLn5D2F+fl59PT02ImE3qPr+h9z8bcqudpJnZ2dEwD+DoDUobVjY2MYHBwsmE + Es6d39Pl9z7y454BXFMqwtpJy8UCiEkydP2vm9XzIM4xs52/+53FmGYRwD8EoAUvMIZ8+exfDw + cN4PYiGE7GLF7SgwvF7v78E5eQCApaUlnDhxQjbXDgDuCwaDN+X0Cy/XO80wjN8T0btk2w8NDe + X9boxgMFjwuXeXQrZOnmQ6T86LnUR9xItBlWma13d3d+e066vkQ+fpuv5tAJ+RbT84OJjXnp5k + oYB78z33bhkP+HZYLNRIvjRyWuxs/L5eRVFelg/jRcmXTjQM41MApOcWhoaG8lL0ZHPvkEd175 + IYKz0o0Jy8UChkV+yGFEV5cWLOPOdR8qkzg8Hgu4noB3ZEb3BwMK9WbyXr3vUWUO7dpTzcgju7 + dnFxEcePH7cjdrNEdG0iaTsvyCvB6+7ujvl8vv8L4Ley14yMjORVWSnJlArLkC7f8Xq996CAcv + Lm5uZw/PhxOwsUiwCu1XX9SD71u5JvA7m9vd00TfOVAH4ve834+DhOnTqV8zsyOPdOnkLKyZud + nUV3d7edbZaLRPR/8rD6df4JHgAcOnRo0TTNlwH4gx3PyGY+Uk56d0T0cKHl3qUS1uZ6Tt7k5K + TdcR0C8Apd1x/Mxz5X8nUwJ0TvWkhuQbvwJjx27FhOrs7J1r1DHpw56xQ+n+8hSOTk5Wp9xZGR + EbtHmS4R0SsMw9iXr32u5POATiyj2xK9xcVFHDt2DAsLCzn1WyXTKOYKMfdumekPqbL2uRbWCi + HQ399vd0Hugmf323zucyXfB7VhGAumab6YiO6XvcY0TRw/fhwzMzP5Fs4WbO6dhcebNzl50WgU + 3d3dGB+3dSRsEMC1hmE8kO+drRTCiD506NCiz+d7JYB7ZK+5UOJ6eHjY9Su4NnLvOJz96xdiDx + F1WHlMueDlLS4u4ujRo5idnbVz2SSAFxiG8Ugh9LdSKAO7vb3dDAaDrwfw33ZCg6GhIfT29rp6 + 4loy966v0HPvlsEyCXtiYsLVL77p6WkcO3YMS0tLdi4bIaJnGYbRVSgdrRTSqO7u7o4ZhvF2Iv + qcneumpqZw/Phxu4PJVeGsTOhWqORyTt6Fl3JPT4/dl/JxRVHadF0/Xkh9rRTg+Ba6rn+CiN4M + QDoLc2FhAUePHnVdFQ0bZ87eCeZpSeTk/cKhF0vGME0z2WmXRxRFaevs7DxdaH2tFOog13X9ew + BeivMTtlJEo1H09PTg9OnTrklSlnwIHzEMYwDMJSGi22Q8fbdMbQSDQRw9ejSZhbW7/H7/NZ2d + nQV5lqlSyIPcMIx9RHQVAFtiMDY2lsx8SVrCGckzZ7/PkrY8iZy8ZSvEuqFOnhACw8PDdks7IT + Gd8RnDMN6Qa+dQsOA56+kdURRFhY1dGRdC3CeeeMLu8r/jb3mZ3DshxM9Z0pYnkZPn6jp5F8o6 + DQ0N2Q1h54noukRFoYKex1V4qJ8vF19dXf0iALfauS4Wi6G/vx8nT560+7bNWDhLRPcahrHAvS + yFZVibrZy88fFxPPHEEwgGg3YvPQWgTdf1n3D3suBdZN++fRHDMN6VWMywNaKnp6dx5MiRjE5q + 28i9u4N7V3qKoweAq3LywuEwuru70d/fb3v+kIgeisVimmEYh7l3WfAuFeJ+D8BVAHrsClBfXx + +6u7sz4gHYyL17lHvVlkhYenmZqJMnhMDo6CiOHDmSzMJEHMBnZ2dnrzl48OAk9yoLntWbvsvn + 87UQ0Y/tXjszM4MjR45gdHQ0rQ+FjZ0VnHtng0ROXmi5NktLSzKpQElzoVDnwMBAMqvCo0KIqw + 3D+GSunz/BgpdBOjo6grquXwfgnQBsLcfGYjEMDAwkO+diSSgUkvncuKIod3FP2u93AJaLPOko + /x6NRi+Om2SSnInoIQDP7Orqeoh7kgUvWW/vGx6PRwVwKNk3dU9Pj6NhrmzuXSEmljpERs+uFU + JgbGwMhw4dSjYyCBPRh30+39WGYYxy97HgpcT+/fuf8Pv9GoAvAbA9yqempnD48GEMDQ2l/JDY + yL27jXsuOfx+/+8ADFl58U7k5F1IID59+rSdisR/ySGPx9Oi6/q/JVJrGBa81GlvbzcNw/gogO + cA6LN7fTwex/DwMB5//HGMjo4mvVPDRu4d171Lvq/jkFjdTmW1dmFhASdOnMDx48eTrb0YA/Al + v9+v7d+//wnuNRa8dIW47QB2EdHXkcSCwIV5msOHDydVgUNy7ujHnHuX4oOhKJZh7dzcnO2piq + WlJfT29iZTxukv6QbwLMMwPlrIuyZY8DInegu6rv9zwtvrTuYzwuEwTp06dTF/T0b4otGoVLlx + Irqdeyk1Ojs7T8LBnLxQKIS+vj4cPnwYk5OTya7gRwB8QQjxTMMw/sy9xIKXaeF7NBaL7QLwWQ + BJvWn/8kEYHx9fNtSdmpri3LsMIvPisHpZXehfOy+2S6ADaDEM46aurq4Q9w4LXlY4ePBg2DCM + T3o8niusPAKrUKe/vx+HDx/G6Ojo0y5uSC5WFPyZsw6SdE7e3NwcTp486YTQzQN4r9/vv5J3TD + jwEkumI+rq6thyl7CnpmlvEEL8K4BVqXyQx+NBXV0dVq5cieLiYoRCIRw+bDne44qiNHM6inOo + qvoDAH+/XJv6+no0NTVdXEEfGxtzIjFZALhLCPGRrq6us7lks3TkKLLguZi2trZy0zQ/BeBGAP + 4UwyrU1NQgHA7LPEQPG4bxfO4B59A07RohhOXhNqtWrcLk5KQjRSSI6ACAd+m6vj8XbeZmweOQ + Ng20t7fPG4bxIUVRdkAia3/Z13zCa5D0GG5j6zuLz+d7EBY5eQBw9uxZJ8RumIje7PP5tFwVO7 + fDgpdGOjs7ew3DeCURXQkbZ+MmyRyAn7LVHX95SeXkpcgMgI8IITbruv49TiBmwctpdF3fbxjG + c4joJQDSMvFMRD/h3Lv0QETpErwQgC/F4/EmwzC+zKuvLHj5Jny/9vv9zwTwKgCPO/zxHM6mr9 + +6kcIK/NOwAODfPR5Pk2EYHz1w4MAMWzlDLy9etMie7TVNe5kQ4iYAWoqfdcowjE3gdJS0oarq + 2wB8M8WPCQL4mqIoN3d2dk7kq6140YJ5OoSu6/cZhtEK4BoAv01BsDj3Lv1h7d2wyMlbhhEiuk + kI0WQYxk35LHbs4TF2vIjtAN4L4AYAxZKXce5d5vrnhwCut3HJQSK62efz3VNIe145D4+xxd69 + e2sjkcjbALwZQJOF5/H1xL5eJs20tLTsSOTIFS3TzMT5Q72/YRjGI4VoJxY8Jina2toU0zRfSE + RvFUK8HIDvKU0eIaKX67o+x9bKDJqmvVEI8XUApU/5px4i+rYQ4nbDMMbZUu4UVha8HGHPnj0r + Y7HY+wDUEdGSEOJRwzDuAc/dZSO03QzgeiK6TAhxGsCDhmE8zH2Rp4LHMAyTi/AqLcMwLHgMwz + AseAzDMCx4DMMwLHgMwzAseAzDMCx4DMMwLHgMwzAseAzDMCx4DMMwLHgMw7DgMQzDsOAxDMOw + 4DEMw7DgMQzDsOAxDMOw4DEMw7DgMQzDsOAxDMNI8/8HAE8wFAC1H96IAAAAAElFTkSuQmCC +END:VEVENT + +BEGIN:VEVENT +CREATED:20130718T073555Z +UID:B968B885-08FB-40E5-B89E-6DA05F26AA79 +URL;VALUE=URI:http://en.wikipedia.org/wiki/Swiss_National_Day +DTEND;VALUE=DATE:20130802 +TRANSP:TRANSPARENT +SUMMARY:Swiss National Day +DTSTART;VALUE=DATE:20130801 +DTSTAMP:20130718T074538Z +SEQUENCE:2 +DESCRIPTION:German: Schweizer Bundesfeier\nFrench: Fête nationale Suisse + \nItalian: Festa nazionale svizzera\nRomansh: Fiasta naziunala Svizra +END:VEVENT + +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/dummy.ifb Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//kolab.org//NONSGML Kolab Server 3//EN +METHOD:PUBLISH +BEGIN:VFREEBUSY +ORGANIZER:MAILTO:nobody@kolabsys.com.ifb +DTSTAMP:20130824T135913Z +DTSTART:20130629T135913Z +DTEND:20131214T135913Z +COMMENT:This is a dummy vfreebusy that indicates an empty calendar +FREEBUSY:19700101T000000Z/19700101T000000Z +END:VFREEBUSY +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/escaped.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,25 @@ +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML KOrganizer 4.11.3.0//EN +VERSION:2.0 +X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0 +METHOD:REQUEST +BEGIN:VEVENT +UID:4113ab9d-ffbb-aa00-c372-deb93de6b539 +ORGANIZER;CN="Organizor":MAILTO:organizor@example.org +DTSTAMP:20140225T160532Z +ATTENDEE;CN="Master of Desaster";RSVP=TRUE;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;X-UID=100156408:mailto:master@desaster.com +ATTENDEE;CN="Doe, John";RSVP=TRUE;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;X-UID=115484392:mailto:doe@example.com +ATTENDEE;CN="Kolab\, Thomas";RSVP=TRUE;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;X-UID=115936264:mailto:thomas@kolab.org +CREATED:20140225T160335Z +LAST-MODIFIED:20140225T160335Z +DESCRIPTION:Me\, meets Them\nThem\, meet Me +SUMMARY:Meeting w/Them +LOCATION:House\, Street\, Zip Place +DTSTART:20140305T150000Z +DTEND:20140305T170000Z +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/freebusy.ifb Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,25 @@ +BEGIN:VCALENDAR +PRODID:Libkolab-0.4.2 +VERSION:2.0 +METHOD:PUBLISH +BEGIN:VFREEBUSY +CREATED:20130824T140042Z +ORGANIZER:MAILTO:somebody@somedomain.com +DTSTART:20130824T123016Z +DTEND:20131122T140026Z +FREEBUSY;FBTYPE=FREE:20130826T110000Z/20130826T150000Z +FREEBUSY:20130826T110000Z/20130826T150000Z +FREEBUSY:20130826T110000Z/20130826T150000Z +FREEBUSY:20130827T100000Z/20130827T160000Z +FREEBUSY:20130828T100000Z/20130828T120000Z +FREEBUSY:20130828T100000Z/20130828T120000Z +FREEBUSY:20130830T090000Z/20130830T093000Z +FREEBUSY:20130830T093000Z/20130830T100000Z +FREEBUSY:20130930T070000Z/20130930T160000Z +FREEBUSY:20131104T113000Z/20131104T160000Z +FREEBUSY;FBTYPE=OOF:20131104T113000Z/20131104T160000Z +FREEBUSY;FBTYPE=BUSY-TENTATIVE:20131104T113000Z/20131104T160000Z +FREEBUSY;FBTYPE=FREE:20131104T113000Z/20131104T160000Z +FREEBUSY;FBTYPE=BUSY:20131104T113000Z/20131104T160000Z +END:VFREEBUSY +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/invalid-dates.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 5.0.3//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:00001231T000000Z +LAST-MODIFIED:20130755 +UID:C968B885-08FB-40E5-B89E-6DA05F26AAFF +TRANSP:TRANSPARENT +SUMMARY:Swiss National Day +DTSTART;VALUE=DATE:20130801 +DTEND;VALUE=DATE:20130802 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/invalid-event.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 5.0.3//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20130917T000000Z +LAST-MODIFIED:20130755 +UID:C968B885-08FB-40E5-B89E-6FA05F26AACC +TRANSP:TRANSPARENT +SUMMARY:Event with no end date nor duration +DTSTART;VALUE=DATE-TIME:20131001T120000Z +SEQUENCE:2 +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/invalid.txt Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,2 @@ +Some text file that has nothing to do with the iCal format. +Just to test the sanity checks before attempting to parse iCal files. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/itip.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,151 @@ +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN +VERSION:2.0 +X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0 +METHOD:REQUEST +BEGIN:VEVENT +ORGANIZER;CN="Rolf Test":MAILTO:rolf@mykolab.com +DTSTAMP:20130628T190056Z +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT; + DELEGATED-FROM=carl@mykolab.com;X-UID=208889384:mailto:rolf2@mykolab.com +ATTENDEE;RSVP=FALSE;PARTSTAT=DELEGATED;ROLE=NON-PARTICIPANT;CUTYPE=INDIVIDUAL; + DELEGATED-TO=rolf2@mykolab.com:mailto:carl@mykolab.com +CREATED:20130628T190032Z +UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0 +LAST-MODIFIED:20130628T190032Z +SUMMARY:iTip Test +ATTACH;VALUE=BINARY;FMTTYPE=text/html;ENCODING=BASE64; + X-LABEL=calendar.html: + PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBYSFRNTCAxLjAgVHJhbnNpdGlvbm + FsLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSL3hodG1sMS9EVEQveGh0bWwxLXRyYW5zaXRp + b25hbC5kdGQiPgo8aHRtbD48aGVhZD4KICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LVR5cG + UiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD1VVEYtOCIgLz4KICA8dGl0bGU+S2FsZW5k + ZXI8L3RpdGxlPgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsgYmFja2dyb3 + VuZC1jb2xvcjp3aGl0ZTsgY29sb3I6YmxhY2sgfQogICAgdGQgeyB0ZXh0LWFsaWduOmNlbnRl + cjsgYmFja2dyb3VuZC1jb2xvcjojZWVlIH0KICAgIHRoIHsgdGV4dC1hbGlnbjpjZW50ZXI7IG + JhY2tncm91bmQtY29sb3I6IzIyODsgY29sb3I6d2hpdGUgfQogICAgdGQuc3VtIHsgdGV4dC1h + bGlnbjpsZWZ0IH0KICAgIHRkLnN1bWRvbmUgeyB0ZXh0LWFsaWduOmxlZnQ7IGJhY2tncm91bm + QtY29sb3I6I2NjYyB9CiAgICB0ZC5kb25lIHsgYmFja2dyb3VuZC1jb2xvcjojY2NjIH0KICAg + IHRkLnN1YmhlYWQgeyB0ZXh0LWFsaWduOmNlbnRlcjsgYmFja2dyb3VuZC1jb2xvcjojY2NmIH + 0KICAgIHRkLmRhdGVoZWFkIHsgdGV4dC1hbGlnbjpjZW50ZXI7IGJhY2tncm91bmQtY29sb3I6 + I2NjZiB9CiAgICB0ZC5zcGFjZSB7IGJhY2tncm91bmQtY29sb3I6d2hpdGUgfQogICAgdGQuZG + F0ZSB7IHRleHQtYWxpZ246bGVmdCB9CiAgICB0ZC5kYXRlaG9saWRheSB7IHRleHQtYWxpZ246 + bGVmdDsgY29sb3I6cmVkIH0KICA8L3N0eWxlPgo8L2hlYWQ+PGJvZHk+CjxoMT5LYWxlbmRlcj + wvaDE+CjxoMj5KdW5pIDIwMTM8L2gyPgo8dGFibGUgYm9yZGVyPSIxIj4KICA8dHI+PHRoPk1v + bnRhZzwvdGg+PHRoPkRpZW5zdGFnPC90aD48dGg+TWl0dHdvY2g8L3RoPjx0aD5Eb25uZXJzdG + FnPC90aD48dGg+RnJlaXRhZzwvdGg+PHRoPlNhbXN0YWc8L3RoPjx0aD5Tb25udGFnPC90aD48 + L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPj + x0ZCBjbGFzcz0iZGF0ZSI+Mjc8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48 + L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMC + I+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjg8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+ + PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcm + Rlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjk8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249 + InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYm + xlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MzA8L3RkPjwvdHI+PHRyPjx0ZCB2 + YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcC + I+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MzE8L3RkPjwvdHI+PHRy + Pjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ2 + 49InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTwvdGQ+PC90 + cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIH + ZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saWRh + eSI+MjwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3 + RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIw + Ij48dHI+PHRkIGNsYXNzPSJkYXRlIj4zPC90ZD48L3RyPjx0cj48dGQgdmFsaWduPSJ0b3AiPj + wvdGQ+PC90cj48L3RhYmxlPjwvdGQ+CiAgICA8dGQgdmFsaWduPSJ0b3AiPjx0YWJsZSBib3Jk + ZXI9IjAiPjx0cj48dGQgY2xhc3M9ImRhdGUiPjQ8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249In + RvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxl + IGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+NTwvdGQ+PC90cj48dHI+PHRkIHZhbG + lnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48 + dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj42PC90ZD48L3RyPjx0cj48dG + QgdmFsaWduPSJ0b3AiPjwvdGQ+PC90cj48L3RhYmxlPjwvdGQ+CiAgICA8dGQgdmFsaWduPSJ0 + b3AiPjx0YWJsZSBib3JkZXI9IjAiPjx0cj48dGQgY2xhc3M9ImRhdGUiPjc8L3RkPjwvdHI+PH + RyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxp + Z249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+ODwvdGQ+PC + 90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRk + IHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saW + RheSI+OTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48 + L3RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPS + IwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMDwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9w + Ij48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm + 9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMTwvdGQ+PC90cj48dHI+PHRkIHZhbGln + bj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dG + FibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMjwvdGQ+PC90cj48dHI+PHRk + IHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG + 9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMzwvdGQ+PC90cj48 + dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbG + lnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xNDwvdGQ+ + PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPH + RkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4x + NTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPg + ogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJk + YXRlaG9saWRheSI+MTY8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPj + wvdGFibGU+PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxl + IGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTc8L3RkPjwvdHI+PHRyPjx0ZCB2YW + xpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+ + PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTg8L3RkPjwvdHI+PHRyPj + x0ZCB2YWxpZ249InRvcCI+PHRhYmxlPiAgPHRyPgogICAgPHRkPiZuYnNwOzwvdGQ+PHRkPiZu + YnNwOzwvdGQ+CiAgICA8dGQgY2xhc3M9InN1bSI+CiAgICAgIDxiPnRlcm1pbmJldHJlZmYxPC + 9iPgogICAgPC90ZD4KICA8dGQ+CiAgICAmbmJzcDsKICA8L3RkPgogIDx0ZD4KICAgICZuYnNw + OwogIDwvdGQ+CiAgPC90cj4KPC90YWJsZT48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPH + RkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4x + OTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT + 48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNs + YXNzPSJkYXRlIj4yMDwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPj + wvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIw + Ij48dHI+PHRkIGNsYXNzPSJkYXRlIj4yMTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj + 4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFi + bGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4yMjwvdGQ+PC90cj48dHI+PHRkIH + ZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGln + bj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saWRheSI+Mj + M8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwvdGFibGU+ + PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj + 0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MjQ8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRv + cCI+PHRhYmxlPiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj4xNTowMDwvdGQ+CiAgICA8dG + QgdmFsaWduPSJ0b3AiPjAwOjMwPC90ZD4KICAgIDx0ZCBjbGFzcz0ic3VtIj4KICAgICAgPGI+ + dGVzdDwvYj4KICAgIDwvdGQ+CiAgPHRkPgogICAgJm5ic3A7CiAgPC90ZD4KICA8dGQ+CiAgIC + AmbmJzcDsKICA8L3RkPgogIDwvdHI+CjwvdGFibGU+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4K + ICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZG + F0ZSI+MjU8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwv + dGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPj + x0ZCBjbGFzcz0iZGF0ZSI+MjY8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7 + PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcm + Rlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjc8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249 + InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcC + I+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjg8L3RkPjwvdHI+PHRy + Pjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ2 + 49InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjk8L3RkPjwv + dHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZC + B2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZWhvbGlk + YXkiPjMwPC90ZD48L3RyPjx0cj48dGQgdmFsaWduPSJ0b3AiPjwvdGQ+PC90cj48L3RhYmxlPj + wvdGQ+CiAgPC90cj4KPC90YWJsZT4KPGgxPkF1ZmdhYmVubGlzdGU8L2gxPgo8dGFibGUgYm9y + ZGVyPSIwIiBjZWxscGFkZGluZz0iMyIgY2VsbHNwYWNpbmc9IjMiPgogIDx0cj4KICAgIDx0aC + BjbGFzcz0ic3VtIj5BdWZnYWJlPC90aD4KICAgIDx0aD5Qcmlvcml0w6R0PC90aD4KICAgIDx0 + aD5BYmdlc2NobG9zc2VuPC90aD4KICAgIDx0aD5Gw6RsbGlna2VpdHNkYXR1bTwvdGg+CiAgIC + A8dGg+T3J0PC90aD4KICAgIDx0aD5LYXRlZ29yaWVuPC90aD4KICA8L3RyPgo8dHI+CiAgPHRk + IGNsYXNzPSJzdW0iPgogICAgPGEgbmFtZT0iYzJmZjM1NGQtMjhlYi00YjBmLWIwYWItZDQxMT + g1Y2JlNGM3Ij48L2E+CiAgICA8Yj5UZXN0YXVmZ2FiZTE8L2I+CiAgICA8cD5BdWZnYWJlbmlu + aGFsdDE8L3A+CiAgICA8ZGl2IGFsaWduPSJyaWdodCI+PGEgaHJlZj0iI3N1YmMyZmYzNTRkLT + I4ZWItNGIwZi1iMGFiLWQ0MTE4NWNiZTRjNyI+VGVpbGF1ZmdhYmVuPC9hPjwvZGl2PgogIDwv + dGQ+CiAgPHRkPgogICAgMAogIDwvdGQ+CiAgPHRkPgogICAgMCAlCiAgPC90ZD4KICA8dGQ+Ci + AgICAyNS4wNi4yMDEzCiAgPC90ZD4KICA8dGQ+CiAgICAmbmJzcDsKICA8L3RkPgogIDx0ZD4K + ICAgICZuYnNwOwogIDwvdGQ+CjwvdHI+Cjx0cj4KICA8dGQgY2xhc3M9InN1bSI+CiAgICA8YS + BuYW1lPSI5MTc4NjRkNi1lN2ZiLTQwYTEtODEzOS05ODU3MzM0MDI3ZGYiPjwvYT4KICAgIDxi + PlRlc3RhdWZnYWJlNDg8L2I+CiAgPC90ZD4KICA8dGQ+CiAgICAwCiAgPC90ZD4KICA8dGQ+Ci + AgICAwICUKICA8L3RkPgogIDx0ZD4KICAgIDI2LjA2LjIwMTMKICA8L3RkPgogIDx0ZD4KICAg + ICZuYnNwOwogIDwvdGQ+CiAgPHRkPgogICAgdGVzdDQ4CiAgPC90ZD4KPC90cj4KICA8dHI+Ci + AgICA8dGQgY2xhc3M9InN1YmhlYWQiIGNvbHNwYW49IjYiPjxhIG5hbWU9InN1YmMyZmYzNTRk + LTI4ZWItNGIwZi1iMGFiLWQ0MTE4NWNiZTRjNyI+PC9hPlRlaWxhdWZnYWJlbiB2b246IDxhIG + hyZWY9IiNjMmZmMzU0ZC0yOGViLTRiMGYtYjBhYi1kNDExODVjYmU0YzciPjxiPlRlc3RhdWZn + YWJlMTwvYj48L2E+PC90ZD4KICA8L3RyPgo8dHI+CiAgPHRkIGNsYXNzPSJzdW0iPgogICAgPG + EgbmFtZT0iYWFlNzA2NDItNTRlMy00MTdhLTg1OGEtZmFiYjcwZTBhYmZiIj48L2E+CiAgICA8 + Yj5VbnRlcmF1ZmdhYmU8L2I+CiAgPC90ZD4KICA8dGQ+CiAgICAwCiAgPC90ZD4KICA8dGQ+Ci + AgICAwICUKICA8L3RkPgogIDx0ZD4KICAgIDI1LjA2LjIwMTMKICA8L3RkPgogIDx0ZD4KICAg + ICZuYnNwOwogIDwvdGQ+CiAgPHRkPgogICAgJm5ic3A7CiAgPC90ZD4KPC90cj4KPC90YWJsZT + 4KPHA+RGllc2UgU2VpdGUgd3VyZGUgZXJzdGVsbHQgdm9uIFJvbGYgVGVzdCAobWFpbHRvOnJv + bGZAbXlrb2xhYi5jb20pIG1pdCBLT3JnYW5pemVyIChodHRwOi8va29yZ2FuaXplci5rZGUub3 + JnKTwvcD4KPC9ib2R5PjwvaHRtbD4K +DTSTART:20130703T183000Z +DTEND:20130703T203000Z +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/multiple-rdate.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,80 @@ +BEGIN:VCALENDAR +X-LOTUS-CHARSET:UTF-8 +VERSION:2.0 +PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C +METHOD:PUBLISH +BEGIN:VTIMEZONE +TZID:W. Europe +BEGIN:STANDARD +DTSTART:19501029T020000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19500326T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VEVENT +DTSTART;TZID="W. Europe":20140520T040000 +DTEND;TZID="W. Europe":20140520T200000 +TRANSP:TRANSPARENT +RDATE;VALUE=DATE-TIME:20140520T020000Z +RDATE;VALUE=PERIOD:20150520T020000Z/20150520T180000Z + ,20160520T020000Z/20160520T180000Z,20170520T020000Z/20170520T180000Z + ,20180520T020000Z/20180520T180000Z,20190520T020000Z/20190520T180000Z + ,20200520T020000Z/20200520T180000Z,20210520T020000Z/20210520T180000Z + ,20220520T020000Z/20220520T180000Z +DTSTAMP:20140227T123549Z +CLASS:PUBLIC +SUMMARY:Feiertag - Pfingsmontag +UID:AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated +X-LOTUS-PARITAL-REPEAT:TRUE +X-LOTUS-NOTESVERSION:2 +X-LOTUS-APPTTYPE:1 +END:VEVENT + +END:VCALENDAR + +BEGIN:VCALENDAR +X-LOTUS-CHARSET:UTF-8 +VERSION:2.0 +PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C +METHOD:PUBLISH +BEGIN:VTIMEZONE +TZID:W. Europe +BEGIN:STANDARD +DTSTART:19501029T020000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19500326T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VEVENT +DTSTART;TZID="W. Europe":20120330T040000 +DTEND;TZID="W. Europe":20120330T200000 +TRANSP:TRANSPARENT +RDATE;VALUE=PERIOD:20120330T020000Z/20120330T180000Z + ,20130330T030000Z/20130330T190000Z,20140330T020000Z/20140330T180000Z + ,20150330T020000Z/20150330T180000Z,20160330T020000Z/20160330T180000Z + ,20170330T020000Z/20170330T180000Z,20180330T020000Z/20180330T180000Z + ,20190330T030000Z/20190330T190000Z,20200330T020000Z/20200330T180000Z + ,20210330T020000Z/20210330T180000Z +DTSTAMP:20140227T123547Z +CLASS:PUBLIC +SUMMARY:Another RDATE repeating event +UID:AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated +END:VEVENT + +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/multiple.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,51 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 5.0.3//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Zurich +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:CEST +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:CET +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20130530T140406Z +UID:42524DA1-8B43-4CCA-9FDE-1A8F165115C6 +DTEND;TZID=Europe/Zurich:20130607T230000 +TRANSP:OPAQUE +SUMMARY:Depeche Mode +LAST-MODIFIED:20130530T140406Z +DTSTAMP:20130530T140413Z +DTSTART;TZID=Europe/Zurich:20130607T180000 +LOCATION:Wankdorf Stadium, Bern +SEQUENCE:0 +BEGIN:VALARM +UID:E5F5C5CB-F17A-4959-A0A2-80D700197425 +X-WR-ALARMUID:E5F5C5CB-F17A-4959-A0A2-80D700197425 +DESCRIPTION:Reminder +TRIGGER:-PT1H +ACTION:DISPLAY +END:VALARM +END:VEVENT +BEGIN:VEVENT +CREATED:20130718T073555Z +UID:B968B885-08FB-40E5-B89E-6DA05F26AA79 +DTEND;VALUE=DATE:20130802 +TRANSP:TRANSPARENT +SUMMARY:Swiss National Day +DTSTART;VALUE=DATE:20130801 +DTSTAMP:20130718T074538Z +SEQUENCE:3 +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/recurrence-id.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,39 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C +METHOD:REQUEST +BEGIN:VTIMEZONE +TZID:W. Europe +BEGIN:STANDARD +DTSTART:19501029T020000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19500326T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID="W. Europe":20140230T150000 +DTEND;TZID="W. Europe":20140230T163000 +TRANSP:OPAQUE +RECURRENCE-ID;RANGE=THISANDFUTURE:20140227T130000Z +SEQUENCE:0 +UID:7e93e8e8eef16f28aa33b78cd73613ebff +DTSTAMP:20140120T105609Z +SUMMARY:Invitation with Recurrence-ID +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID="W. Europe":20140305T150000 +DTEND;TZID="W. Europe":20140305T163000 +RECURRENCE-ID;TZID="W. Europe":20140305T150000 +SEQUENCE:2 +UID:7e93e8e8eef16f28aa33b78cd73613ebff +DTSTAMP:20140120T105609Z +SUMMARY:Invitation with Recurrence-ID #2 +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/recurring.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,51 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 5.0.3//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Zurich +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:CEST +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:CET +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +PRIORITY:3 +DTEND;TZID=Europe/Zurich:20130717T130000 +TRANSP:TRANSPARENT +UID:7e93e8e8eef16f28aa33b78cd73613eb +DTSTAMP:20130718T082032Z +SEQUENCE:6 +CLASS:CONFIDENTIAL +CATEGORIES:libcalendaring tests +SUMMARY:Recurring Test +LAST-MODIFIED:20120621 +DTSTART;TZID=Europe/Zurich:20130717T080000 +CREATED:20081223T232600Z +RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20140718T215959Z;BYDAY=3WE +EXDATE;TZID=Europe/Zurich:20131218T080000 +EXDATE;TZID=Europe/Zurich:20140415T080000 +BEGIN:VALARM +TRIGGER;RELATED=END:-PT12H +ACTION:DISPLAY +END:VALARM +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID="Europe/Zurich":20140521T100000 +DTEND;TZID="Europe/Zurich":20140521T150000 +RECURRENCE-ID:20140521T080000Z +UID:7e93e8e8eef16f28aa33b78cd73613eb +DTSTAMP:20130718T082032Z +SUMMARY:Recurring Test (Exception) +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/snd.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 5.0.3//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20130718T073555Z +UID:B968B885-08FB-40E5-B89E-6DA05F26AA79 +URL;VALUE=URI:http://en.wikipedia.org/wiki/Swiss_National_Day +DTEND;VALUE=DATE:20130802 +TRANSP:TRANSPARENT +SUMMARY:Swiss National Day +DTSTART;VALUE=DATE:20130801 +DTSTAMP:20130718T074538Z +SEQUENCE:2 +DESCRIPTION:German: Schweizer Bundesfeier\nFrench: FĂȘte nationale Suisse + \nItalian: Festa nazionale svizzera\nRomansh: Fiasta naziunala Svizra +END:VEVENT +END:VCALENDAR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/tests/resources/vtodo.ics Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,55 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Zurich +X-LIC-LOCATION:Europe/Zurich +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +BEGIN:VTODO +LAST-MODIFIED:20130919T075227Z +DTSTAMP:20130919T075227Z +UID:163A577B800E62BFFEF1CEC9DDDE4E11-FCBB6C4091F28CA0 +SUMMARY:My first task today +STATUS:IN-PROCESS +DTSTART;TZID=Europe/Zurich:20130921T000000 +DUE;VALUE=DATE:20130921 +SEQUENCE:2 +CATEGORIES:Tag1,Tag2 +CATEGORIES:Tag3 +CATEGORIES:Tag4 +RELATED-TO:1234567890-12345678-PARENT +RELATED-TO;RELTYPE=CHILD:1234567890-12345678-CHILD +X-MOZ-GENERATION:1 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DURATION:-P1D +DESCRIPTION:Default Mozilla Description +END:VALARM +END:VTODO + +BEGIN:VTODO +SUMMARY:Send and receive iTip messages with task assignments +UID:7ADB358AD071AA00306962BF329EF317-FCBB6C4091F28CA0 +SEQUENCE:0 +DTSTAMP:20150527T101614Z +CREATED:20140801T091954Z +LAST-MODIFIED:20150527T101611Z +COMPLETED:20150527T101611Z +X-BUSYMAC-LASTMODBY:Thomas BrĂŒderli +END:VTODO + +END:VCALENDAR