From 50569114acdc64e7c7cae1498635d3f821517c30 Mon Sep 17 00:00:00 2001 From: Daniel Lange Date: Mon, 7 Mar 2016 15:53:16 +0100 Subject: Initial commit of the Faster IT roundcube_calendar plugin distribution This includes: * Kolab plugins 3.2.9 (calendar and libcalendaring) * CalDAV driver 3.2.8 * .htaccess files for at least some security * SabreDAV updated to 1.8.12 (Jan 2015 release) * Support for CURLOPT_SSL_* settings to allow self-signed certificates * Small fixes & improved documentation --- libcalendaring/lib/.htaccess | 2 + libcalendaring/lib/Horde_Date.php | 1304 +++++++++++++++ libcalendaring/lib/Horde_Date_Recurrence.php | 1705 ++++++++++++++++++++ libcalendaring/lib/OldSabre/VObject/Component.php | 405 +++++ .../lib/OldSabre/VObject/Component/VAlarm.php | 108 ++ .../lib/OldSabre/VObject/Component/VCalendar.php | 244 +++ .../lib/OldSabre/VObject/Component/VCard.php | 107 ++ .../lib/OldSabre/VObject/Component/VEvent.php | 70 + .../lib/OldSabre/VObject/Component/VFreeBusy.php | 68 + .../lib/OldSabre/VObject/Component/VJournal.php | 46 + .../lib/OldSabre/VObject/Component/VTodo.php | 68 + .../lib/OldSabre/VObject/DateTimeParser.php | 181 +++ libcalendaring/lib/OldSabre/VObject/Document.php | 109 ++ .../lib/OldSabre/VObject/ElementList.php | 172 ++ .../lib/OldSabre/VObject/FreeBusyGenerator.php | 322 ++++ libcalendaring/lib/OldSabre/VObject/Node.php | 187 +++ libcalendaring/lib/OldSabre/VObject/Parameter.php | 91 ++ .../lib/OldSabre/VObject/ParseException.php | 12 + libcalendaring/lib/OldSabre/VObject/Property.php | 453 ++++++ .../lib/OldSabre/VObject/Property/Compound.php | 125 ++ .../lib/OldSabre/VObject/Property/DateTime.php | 245 +++ .../OldSabre/VObject/Property/MultiDateTime.php | 180 +++ libcalendaring/lib/OldSabre/VObject/Reader.php | 223 +++ .../lib/OldSabre/VObject/RecurrenceIterator.php | 1144 +++++++++++++ .../lib/OldSabre/VObject/Splitter/ICalendar.php | 111 ++ .../VObject/Splitter/SplitterInterface.php | 39 + .../lib/OldSabre/VObject/Splitter/VCard.php | 76 + libcalendaring/lib/OldSabre/VObject/StringUtil.php | 61 + .../lib/OldSabre/VObject/TimeZoneUtil.php | 527 ++++++ libcalendaring/lib/OldSabre/VObject/Version.php | 24 + libcalendaring/lib/OldSabre/VObject/includes.php | 41 + libcalendaring/lib/libcalendaring_itip.php | 817 ++++++++++ libcalendaring/lib/libcalendaring_recurrence.php | 155 ++ 33 files changed, 9422 insertions(+) create mode 100644 libcalendaring/lib/.htaccess create mode 100644 libcalendaring/lib/Horde_Date.php create mode 100644 libcalendaring/lib/Horde_Date_Recurrence.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VCard.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VEvent.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VJournal.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VTodo.php create mode 100644 libcalendaring/lib/OldSabre/VObject/DateTimeParser.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Document.php create mode 100644 libcalendaring/lib/OldSabre/VObject/ElementList.php create mode 100644 libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Node.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Parameter.php create mode 100644 libcalendaring/lib/OldSabre/VObject/ParseException.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property/Compound.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property/DateTime.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Reader.php create mode 100644 libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php create mode 100644 libcalendaring/lib/OldSabre/VObject/StringUtil.php create mode 100644 libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Version.php create mode 100644 libcalendaring/lib/OldSabre/VObject/includes.php create mode 100644 libcalendaring/lib/libcalendaring_itip.php create mode 100644 libcalendaring/lib/libcalendaring_recurrence.php (limited to 'libcalendaring/lib') diff --git a/libcalendaring/lib/.htaccess b/libcalendaring/lib/.htaccess new file mode 100644 index 0000000..c0800be --- /dev/null +++ b/libcalendaring/lib/.htaccess @@ -0,0 +1,2 @@ +# Deny all for risky include files of this __CENSORED__ quality plugin +Require all denied diff --git a/libcalendaring/lib/Horde_Date.php b/libcalendaring/lib/Horde_Date.php new file mode 100644 index 0000000..9197f84 --- /dev/null +++ b/libcalendaring/lib/Horde_Date.php @@ -0,0 +1,1304 @@ + 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 + * @author Pierre-Alain Joye + * @author Daniel Convissor + * @author C.A. Woodcock + * + * @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 + * @author Pierre-Alain Joye + * @author Daniel Convissor + * @author C.A. Woodcock + * + * @param int $days the number of days since 24th November, 4714 B.C. + * @param string $format the string indicating how to format the output + * + * @return Horde_Date A Horde_Date object representing the date. + */ + public static function fromDays($days) + { + if (function_exists('JDToGregorian')) { + list($month, $day, $year) = explode('/', JDToGregorian($days)); + } else { + $days = intval($days); + + $days -= 1721119; + $century = floor((4 * $days - 1) / 146097); + $days = floor(4 * $days - 1 - 146097 * $century); + $day = floor($days / 4); + + $year = floor((4 * $day + 3) / 1461); + $day = floor(4 * $day + 3 - 1461 * $year); + $day = floor(($day + 4) / 4); + + $month = floor((5 * $day - 3) / 153); + $day = floor(5 * $day - 3 - 153 * $month); + $day = floor(($day + 5) / 5); + + $year = $century * 100 + $year; + if ($month < 10) { + $month +=3; + } else { + $month -=9; + ++$year; + } + } + + return new Horde_Date($year, $month, $day); + } + + /** + * Getter for the date and time properties. + * + * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or + * 'sec'. + * + * @return integer The property value, or null if not set. + */ + public function __get($name) + { + if ($name == 'day') { + $name = 'mday'; + } + + return $this->{'_' . $name}; + } + + /** + * Setter for the date and time properties. + * + * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or + * 'sec'. + * @param integer $value The property value. + */ + public function __set($name, $value) + { + if ($name == 'timezone') { + $this->_initializeTimezone($value); + return; + } + if ($name == 'day') { + $name = 'mday'; + } + + if ($name != 'year' && $name != 'month' && $name != 'mday' && + $name != 'hour' && $name != 'min' && $name != 'sec') { + throw new InvalidArgumentException('Undefined property ' . $name); + } + + $down = $value < $this->{'_' . $name}; + $this->{'_' . $name} = $value; + $this->_correct(self::$_corrections[$name], $down); + $this->_formatCache = array(); + } + + /** + * Returns whether a date or time property exists. + * + * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or + * 'sec'. + * + * @return boolen True if the property exists and is set. + */ + public function __isset($name) + { + if ($name == 'day') { + $name = 'mday'; + } + return ($name == 'year' || $name == 'month' || $name == 'mday' || + $name == 'hour' || $name == 'min' || $name == 'sec') && + isset($this->{'_' . $name}); + } + + /** + * Adds a number of seconds or units to this date, returning a new Date + * object. + */ + public function add($factor) + { + $d = clone($this); + if (is_array($factor) || is_object($factor)) { + foreach ($factor as $property => $value) { + $d->$property += $value; + } + } else { + $d->sec += $factor; + } + + return $d; + } + + /** + * Subtracts a number of seconds or units from this date, returning a new + * Horde_Date object. + */ + public function sub($factor) + { + if (is_array($factor)) { + foreach ($factor as &$value) { + $value *= -1; + } + } else { + $factor *= -1; + } + + return $this->add($factor); + } + + /** + * Converts this object to a different timezone. + * + * @param string $timezone The new timezone. + * + * @return Horde_Date This object. + */ + public function setTimezone($timezone) + { + $date = $this->toDateTime(); + $date->setTimezone(new DateTimeZone($timezone)); + $this->_timezone = $timezone; + $this->_year = (int)$date->format('Y'); + $this->_month = (int)$date->format('m'); + $this->_mday = (int)$date->format('d'); + $this->_hour = (int)$date->format('H'); + $this->_min = (int)$date->format('i'); + $this->_sec = (int)$date->format('s'); + $this->_formatCache = array(); + return $this; + } + + /** + * Sets the default date format used in __toString() + * + * @param string $format + */ + public function setDefaultFormat($format) + { + $this->_defaultFormat = $format; + } + + /** + * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date. + * + * @return integer The day of the week. + */ + public function dayOfWeek() + { + if ($this->_month > 2) { + $month = $this->_month - 2; + $year = $this->_year; + } else { + $month = $this->_month + 10; + $year = $this->_year - 1; + } + + $day = (floor((13 * $month - 1) / 5) + + $this->_mday + ($year % 100) + + floor(($year % 100) / 4) + + floor(($year / 100) / 4) - 2 * + floor($year / 100) + 77); + + return (int)($day - 7 * floor($day / 7)); + } + + /** + * Returns the day number of the year (1 to 365/366). + * + * @return integer The day of the year. + */ + public function dayOfYear() + { + return $this->format('z') + 1; + } + + /** + * Returns the week of the month. + * + * @return integer The week number. + */ + public function weekOfMonth() + { + return ceil($this->_mday / 7); + } + + /** + * Returns the week of the year, first Monday is first day of first week. + * + * @return integer The week number. + */ + public function weekOfYear() + { + return $this->format('W'); + } + + /** + * Returns the number of weeks in the given year (52 or 53). + * + * @param integer $year The year to count the number of weeks in. + * + * @return integer $numWeeks The number of weeks in $year. + */ + public static function weeksInYear($year) + { + // Find the last Thursday of the year. + $date = new Horde_Date($year . '-12-31'); + while ($date->dayOfWeek() != self::DATE_THURSDAY) { + --$date->mday; + } + return $date->weekOfYear(); + } + + /** + * Sets the date of this object to the $nth weekday of $weekday. + * + * @param integer $weekday The day of the week (0 = Sunday, etc). + * @param integer $nth The $nth $weekday to set to (defaults to 1). + */ + public function setNthWeekday($weekday, $nth = 1) + { + if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) { + return; + } + + if ($nth < 0) { // last $weekday of month + $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year); + $last = $this->dayOfWeek(); + $this->_mday += ($weekday - $last); + if ($this->_mday > $lastday) + $this->_mday -= 7; + } + else { + $this->_mday = 1; + $first = $this->dayOfWeek(); + if ($weekday < $first) { + $this->_mday = 8 + $weekday - $first; + } else { + $this->_mday = $weekday - $first + 1; + } + $diff = 7 * $nth - 7; + $this->_mday += $diff; + $this->_correct(self::MASK_DAY, $diff < 0); + } + } + + /** + * Is the date currently represented by this object a valid date? + * + * @return boolean Validity, counting leap years, etc. + */ + public function isValid() + { + return ($this->_year >= 0 && $this->_year <= 9999); + } + + /** + * Compares this date to another date object to see which one is + * greater (later). Assumes that the dates are in the same + * timezone. + * + * @param mixed $other The date to compare to. + * + * @return integer == 0 if they are on the same date + * >= 1 if $this is greater (later) + * <= -1 if $other is greater (later) + */ + public function compareDate($other) + { + if (!($other instanceof Horde_Date)) { + $other = new Horde_Date($other); + } + + if ($this->_year != $other->year) { + return $this->_year - $other->year; + } + if ($this->_month != $other->month) { + return $this->_month - $other->month; + } + + return $this->_mday - $other->mday; + } + + /** + * Returns whether this date is after the other. + * + * @param mixed $other The date to compare to. + * + * @return boolean True if this date is after the other. + */ + public function after($other) + { + return $this->compareDate($other) > 0; + } + + /** + * Returns whether this date is before the other. + * + * @param mixed $other The date to compare to. + * + * @return boolean True if this date is before the other. + */ + public function before($other) + { + return $this->compareDate($other) < 0; + } + + /** + * Returns whether this date is the same like the other. + * + * @param mixed $other The date to compare to. + * + * @return boolean True if this date is the same like the other. + */ + public function equals($other) + { + return $this->compareDate($other) == 0; + } + + /** + * Compares this to another date object by time, to see which one + * is greater (later). Assumes that the dates are in the same + * timezone. + * + * @param mixed $other The date to compare to. + * + * @return integer == 0 if they are at the same time + * >= 1 if $this is greater (later) + * <= -1 if $other is greater (later) + */ + public function compareTime($other) + { + if (!($other instanceof Horde_Date)) { + $other = new Horde_Date($other); + } + + if ($this->_hour != $other->hour) { + return $this->_hour - $other->hour; + } + if ($this->_min != $other->min) { + return $this->_min - $other->min; + } + + return $this->_sec - $other->sec; + } + + /** + * Compares this to another date object, including times, to see + * which one is greater (later). Assumes that the dates are in the + * same timezone. + * + * @param mixed $other The date to compare to. + * + * @return integer == 0 if they are equal + * >= 1 if $this is greater (later) + * <= -1 if $other is greater (later) + */ + public function compareDateTime($other) + { + if (!($other instanceof Horde_Date)) { + $other = new Horde_Date($other); + } + + if ($diff = $this->compareDate($other)) { + return $diff; + } + + return $this->compareTime($other); + } + + /** + * Returns number of days between this date and another. + * + * @param Horde_Date $other The other day to diff with. + * + * @return integer The absolute number of days between the two dates. + */ + public function diff($other) + { + return abs($this->toDays() - $other->toDays()); + } + + /** + * Returns the time offset for local time zone. + * + * @param boolean $colon Place a colon between hours and minutes? + * + * @return string Timezone offset as a string in the format +HH:MM. + */ + public function tzOffset($colon = true) + { + return $colon ? $this->format('P') : $this->format('O'); + } + + /** + * Returns the unix timestamp representation of this date. + * + * @return integer A unix timestamp. + */ + public function timestamp() + { + if ($this->_year >= 1970 && $this->_year < 2038) { + return mktime($this->_hour, $this->_min, $this->_sec, + $this->_month, $this->_mday, $this->_year); + } + return $this->format('U'); + } + + /** + * Returns the unix timestamp representation of this date, 12:00am. + * + * @return integer A unix timestamp. + */ + public function datestamp() + { + if ($this->_year >= 1970 && $this->_year < 2038) { + return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year); + } + $date = new DateTime($this->format('Y-m-d')); + return $date->format('U'); + } + + /** + * Formats date and time to be passed around as a short url parameter. + * + * @return string Date and time. + */ + public function dateString() + { + return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday); + } + + /** + * Formats date and time to the ISO format used by JSON. + * + * @return string Date and time. + */ + public function toJson() + { + return $this->format(self::DATE_JSON); + } + + /** + * Formats date and time to the RFC 2445 iCalendar DATE-TIME format. + * + * @param boolean $floating Whether to return a floating date-time + * (without time zone information). + * + * @return string Date and time. + */ + public function toiCalendar($floating = false) + { + if ($floating) { + return $this->format('Ymd\THis'); + } + $dateTime = $this->toDateTime(); + $dateTime->setTimezone(new DateTimeZone('UTC')); + return $dateTime->format('Ymd\THis\Z'); + } + + /** + * Formats time using the specifiers available in date() or in the DateTime + * class' format() method. + * + * To format in languages other than English, use strftime() instead. + * + * @param string $format + * + * @return string Formatted time. + */ + public function format($format) + { + if (!isset($this->_formatCache[$format])) { + $this->_formatCache[$format] = $this->toDateTime()->format($format); + } + return $this->_formatCache[$format]; + } + + /** + * Formats date and time using strftime() format. + * + * @return string strftime() formatted date and time. + */ + public function strftime($format) + { + if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) { + return strftime($format, $this->timestamp()); + } else { + return $this->_strftime($format); + } + } + + /** + * Formats date and time using a limited set of the strftime() format. + * + * @return string strftime() formatted date and time. + */ + protected function _strftime($format) + { + return preg_replace( + array('/%b/e', + '/%B/e', + '/%C/e', + '/%d/e', + '/%D/e', + '/%e/e', + '/%H/e', + '/%I/e', + '/%m/e', + '/%M/e', + '/%n/', + '/%p/e', + '/%R/e', + '/%S/e', + '/%t/', + '/%T/e', + '/%x/e', + '/%X/e', + '/%y/e', + '/%Y/', + '/%%/'), + array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))', + '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))', + '(int)($this->_year / 100)', + 'sprintf(\'%02d\', $this->_mday)', + '$this->_strftime(\'%m/%d/%y\')', + 'sprintf(\'%2d\', $this->_mday)', + 'sprintf(\'%02d\', $this->_hour)', + 'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))', + 'sprintf(\'%02d\', $this->_month)', + 'sprintf(\'%02d\', $this->_min)', + "\n", + '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))', + '$this->_strftime(\'%H:%M\')', + 'sprintf(\'%02d\', $this->_sec)', + "\t", + '$this->_strftime(\'%H:%M:%S\')', + '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))', + '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))', + 'substr(sprintf(\'%04d\', $this->_year), -2)', + (int)$this->_year, + '%'), + $format); + } + + /** + * Corrects any over- or underflows in any of the date's members. + * + * @param integer $mask We may not want to correct some overflows. + * @param integer $down Whether to correct the date up or down. + */ + protected function _correct($mask = self::MASK_ALLPARTS, $down = false) + { + if ($mask & self::MASK_SECOND) { + if ($this->_sec < 0 || $this->_sec > 59) { + $mask |= self::MASK_MINUTE; + + $this->_min += (int)($this->_sec / 60); + $this->_sec %= 60; + if ($this->_sec < 0) { + $this->_min--; + $this->_sec += 60; + } + } + } + + if ($mask & self::MASK_MINUTE) { + if ($this->_min < 0 || $this->_min > 59) { + $mask |= self::MASK_HOUR; + + $this->_hour += (int)($this->_min / 60); + $this->_min %= 60; + if ($this->_min < 0) { + $this->_hour--; + $this->_min += 60; + } + } + } + + if ($mask & self::MASK_HOUR) { + if ($this->_hour < 0 || $this->_hour > 23) { + $mask |= self::MASK_DAY; + + $this->_mday += (int)($this->_hour / 24); + $this->_hour %= 24; + if ($this->_hour < 0) { + $this->_mday--; + $this->_hour += 24; + } + } + } + + if ($mask & self::MASK_MONTH) { + $this->_correctMonth($down); + /* When correcting the month, always correct the day too. Months + * have different numbers of days. */ + $mask |= self::MASK_DAY; + } + + if ($mask & self::MASK_DAY) { + while ($this->_mday > 28 && + $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) { + if ($down) { + $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year); + } else { + $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year); + $this->_month++; + } + $this->_correctMonth($down); + } + while ($this->_mday < 1) { + --$this->_month; + $this->_correctMonth($down); + $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year); + } + } + } + + /** + * Corrects the current month. + * + * This cannot be done in _correct() because that would also trigger a + * correction of the day, which would result in an infinite loop. + * + * @param integer $down Whether to correct the date up or down. + */ + protected function _correctMonth($down = false) + { + $this->_year += (int)($this->_month / 12); + $this->_month %= 12; + if ($this->_month < 1) { + $this->_year--; + $this->_month += 12; + } + } + + /** + * Handles args in order: year month day hour min sec tz + */ + protected function _initializeFromArgs($args) + { + $tz = (isset($args[6])) ? array_pop($args) : null; + $this->_initializeTimezone($tz); + + $args = array_slice($args, 0, 6); + $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0); + $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args); + $date = array_merge($keys, $date); + + $this->_initializeFromArray($date); + } + + protected function _initializeFromArray($date) + { + if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) { + if ($date['year'] > 70) { + $date['year'] += 1900; + } else { + $date['year'] += 2000; + } + } + + foreach ($date as $key => $val) { + if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) { + $this->{'_'. $key} = (int)$val; + } + } + + // If $date['day'] is present and numeric we may have been passed + // a Horde_Form_datetime array. + if (isset($date['day']) && + (string)(int)$date['day'] == $date['day']) { + $this->_mday = (int)$date['day']; + } + // 'minute' key also from Horde_Form_datetime + if (isset($date['minute']) && + (string)(int)$date['minute'] == $date['minute']) { + $this->_min = (int)$date['minute']; + } + + $this->_correct(); + } + + protected function _initializeFromObject($date) + { + if ($date instanceof DateTime) { + $this->_year = (int)$date->format('Y'); + $this->_month = (int)$date->format('m'); + $this->_mday = (int)$date->format('d'); + $this->_hour = (int)$date->format('H'); + $this->_min = (int)$date->format('i'); + $this->_sec = (int)$date->format('s'); + $this->_initializeTimezone($date->getTimezone()->getName()); + } else { + $is_horde_date = $date instanceof Horde_Date; + foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) { + if ($is_horde_date || isset($date->$key)) { + $this->{'_' . $key} = (int)$date->$key; + } + } + if (!$is_horde_date) { + $this->_correct(); + } else { + $this->_initializeTimezone($date->timezone); + } + } + } + + protected function _initializeTimezone($timezone) + { + if (empty($timezone)) { + $timezone = date_default_timezone_get(); + } + $this->_timezone = $timezone; + } + +} + +/** + * @category Horde + * @package Date + */ + +/** + * Horde Date wrapper/logic class, including some calculation + * functions. + * + * @category Horde + * @package Date + */ +class Horde_Date_Utils +{ + /** + * Returns whether a year is a leap year. + * + * @param integer $year The year. + * + * @return boolean True if the year is a leap year. + */ + public static function isLeapYear($year) + { + if (strlen($year) != 4 || preg_match('/\D/', $year)) { + return false; + } + + return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0); + } + + /** + * Returns the date of the year that corresponds to the first day of the + * given week. + * + * @param integer $week The week of the year to find the first day of. + * @param integer $year The year to calculate for. + * + * @return Horde_Date The date of the first day of the given week. + */ + public static function firstDayOfWeek($week, $year) + { + return new Horde_Date(sprintf('%04dW%02d', $year, $week)); + } + + /** + * Returns the number of days in the specified month. + * + * @param integer $month The month + * @param integer $year The year. + * + * @return integer The number of days in the month. + */ + public static function daysInMonth($month, $year) + { + static $cache = array(); + if (!isset($cache[$year][$month])) { + $date = new DateTime(sprintf('%04d-%02d-01', $year, $month)); + $cache[$year][$month] = $date->format('t'); + } + return $cache[$year][$month]; + } + + /** + * Returns a relative, natural language representation of a timestamp + * + * @todo Wider range of values ... maybe future time as well? + * @todo Support minimum resolution parameter. + * + * @param mixed $time The time. Any format accepted by Horde_Date. + * @param string $date_format Format to display date if timestamp is + * more then 1 day old. + * @param string $time_format Format to display time if timestamp is 1 + * day old. + * + * @return string The relative time (i.e. 2 minutes ago) + */ + public static function relativeDateTime($time, $date_format = '%x', + $time_format = '%X') + { + $date = new Horde_Date($time); + + $delta = time() - $date->timestamp(); + if ($delta < 60) { + return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta); + } + + $delta = round($delta / 60); + if ($delta < 60) { + return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta); + } + + $delta = round($delta / 60); + if ($delta < 24) { + return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta); + } + + if ($delta > 24 && $delta < 48) { + $date = new Horde_Date($time); + return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format)); + } + + $delta = round($delta / 24); + if ($delta < 7) { + return sprintf(Horde_Date_Translation::t("%d days ago"), $delta); + } + + if (round($delta / 7) < 5) { + $delta = round($delta / 7); + return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta); + } + + // Default to the user specified date format. + return $date->strftime($date_format); + } + + /** + * Tries to convert strftime() formatters to date() formatters. + * + * Unsupported formatters will be removed. + * + * @param string $format A strftime() formatting string. + * + * @return string A date() formatting string. + */ + public static function strftime2date($format) + { + $replace = array( + '/%a/' => 'D', + '/%A/' => 'l', + '/%d/' => 'd', + '/%e/' => 'j', + '/%j/' => 'z', + '/%u/' => 'N', + '/%w/' => 'w', + '/%U/' => '', + '/%V/' => 'W', + '/%W/' => '', + '/%b/' => 'M', + '/%B/' => 'F', + '/%h/' => 'M', + '/%m/' => 'm', + '/%C/' => '', + '/%g/' => '', + '/%G/' => 'o', + '/%y/' => 'y', + '/%Y/' => 'Y', + '/%H/' => 'H', + '/%I/' => 'h', + '/%i/' => 'g', + '/%M/' => 'i', + '/%p/' => 'A', + '/%P/' => 'a', + '/%r/' => 'h:i:s A', + '/%R/' => 'H:i', + '/%S/' => 's', + '/%T/' => 'H:i:s', + '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))', + '/%z/' => 'O', + '/%Z/' => '', + '/%c/' => '', + '/%D/' => 'm/d/y', + '/%F/' => 'Y-m-d', + '/%s/' => 'U', + '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))', + '/%n/' => "\n", + '/%t/' => "\t", + '/%%/' => '%' + ); + + return preg_replace(array_keys($replace), array_values($replace), $format); + } + +} diff --git a/libcalendaring/lib/Horde_Date_Recurrence.php b/libcalendaring/lib/Horde_Date_Recurrence.php new file mode 100644 index 0000000..19e372c --- /dev/null +++ b/libcalendaring/lib/Horde_Date_Recurrence.php @@ -0,0 +1,1705 @@ + 1 ? $plur : $sing); } +} + + +/** + * This file contains the Horde_Date_Recurrence class and according constants. + * + * Copyright 2007-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @category Horde + * @package Date + */ + +/** + * The Horde_Date_Recurrence class implements algorithms for calculating + * recurrences of events, including several recurrence types, intervals, + * exceptions, and conversion from and to vCalendar and iCalendar recurrence + * rules. + * + * All methods expecting dates as parameters accept all values that the + * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date + * object, an ISO time string or a hash. + * + * @author Jan Schneider + * @category Horde + * @package Date + */ +class Horde_Date_Recurrence +{ + /** No Recurrence **/ + const RECUR_NONE = 0; + + /** Recurs daily. */ + const RECUR_DAILY = 1; + + /** Recurs weekly. */ + const RECUR_WEEKLY = 2; + + /** Recurs monthly on the same date. */ + const RECUR_MONTHLY_DATE = 3; + + /** Recurs monthly on the same week day. */ + const RECUR_MONTHLY_WEEKDAY = 4; + + /** Recurs yearly on the same date. */ + const RECUR_YEARLY_DATE = 5; + + /** Recurs yearly on the same day of the year. */ + const RECUR_YEARLY_DAY = 6; + + /** Recurs yearly on the same week day. */ + const RECUR_YEARLY_WEEKDAY = 7; + + /** + * The start time of the event. + * + * @var Horde_Date + */ + public $start; + + /** + * The end date of the recurrence interval. + * + * @var Horde_Date + */ + public $recurEnd = null; + + /** + * The number of recurrences. + * + * @var integer + */ + public $recurCount = null; + + /** + * The type of recurrence this event follows. RECUR_* constant. + * + * @var integer + */ + public $recurType = self::RECUR_NONE; + + /** + * The length of time between recurrences. The time unit depends on the + * recurrence type. + * + * @var integer + */ + public $recurInterval = 1; + + /** + * Any additional recurrence data. + * + * @var integer + */ + public $recurData = null; + + /** + * BYDAY recurrence number + * + * @var integer + */ + public $recurNthDay = null; + + /** + * BYMONTH recurrence data + * + * @var array + */ + public $recurMonths = array(); + + /** + * RDATE recurrence values + * + * @var array + */ + public $rdates = array(); + + /** + * All the exceptions from recurrence for this event. + * + * @var array + */ + public $exceptions = array(); + + /** + * All the dates this recurrence has been marked as completed. + * + * @var array + */ + public $completions = array(); + + /** + * Constructor. + * + * @param Horde_Date $start Start of the recurring event. + */ + public function __construct($start) + { + $this->start = new Horde_Date($start); + } + + /** + * Resets the class properties. + */ + public function reset() + { + $this->recurEnd = null; + $this->recurCount = null; + $this->recurType = self::RECUR_NONE; + $this->recurInterval = 1; + $this->recurData = null; + $this->exceptions = array(); + $this->completions = array(); + } + + /** + * Checks if this event recurs on a given day of the week. + * + * @param integer $dayMask A mask consisting of Horde_Date::MASK_* + * constants specifying the day(s) to check. + * + * @return boolean True if this event recurs on the given day(s). + */ + public function recurOnDay($dayMask) + { + return ($this->recurData & $dayMask); + } + + /** + * Specifies the days this event recurs on. + * + * @param integer $dayMask A mask consisting of Horde_Date::MASK_* + * constants specifying the day(s) to recur on. + */ + public function setRecurOnDay($dayMask) + { + $this->recurData = $dayMask; + } + + /** + * + * @param integer $nthDay The nth weekday of month to repeat events on + */ + public function setRecurNthWeekday($nth) + { + $this->recurNthDay = (int)$nth; + } + + /** + * + * @return integer The nth weekday of month to repeat events. + */ + public function getRecurNthWeekday() + { + return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7); + } + + /** + * Specifies the months for yearly (weekday) recurrence + * + * @param array $months List of months (integers) this event recurs on. + */ + function setRecurByMonth($months) + { + $this->recurMonths = (array)$months; + } + + /** + * Returns a list of months this yearly event recurs on + * + * @return array List of months (integers) this event recurs on. + */ + function getRecurByMonth() + { + return $this->recurMonths; + } + + /** + * Returns the days this event recurs on. + * + * @return integer A mask consisting of Horde_Date::MASK_* constants + * specifying the day(s) this event recurs on. + */ + public function getRecurOnDays() + { + return $this->recurData; + } + + /** + * Returns whether this event has a specific recurrence type. + * + * @param integer $recurrence RECUR_* constant of the + * recurrence type to check for. + * + * @return boolean True if the event has the specified recurrence type. + */ + public function hasRecurType($recurrence) + { + return ($recurrence == $this->recurType); + } + + /** + * Sets a recurrence type for this event. + * + * @param integer $recurrence A RECUR_* constant. + */ + public function setRecurType($recurrence) + { + $this->recurType = $recurrence; + } + + /** + * Returns recurrence type of this event. + * + * @return integer A RECUR_* constant. + */ + public function getRecurType() + { + return $this->recurType; + } + + /** + * Returns a description of this event's recurring type. + * + * @return string Human readable recurring type. + */ + public function getRecurName() + { + switch ($this->getRecurType()) { + case self::RECUR_NONE: return Horde_Date_Translation::t("No recurrence"); + case self::RECUR_DAILY: return Horde_Date_Translation::t("Daily"); + case self::RECUR_WEEKLY: return Horde_Date_Translation::t("Weekly"); + case self::RECUR_MONTHLY_DATE: + case self::RECUR_MONTHLY_WEEKDAY: return Horde_Date_Translation::t("Monthly"); + case self::RECUR_YEARLY_DATE: + case self::RECUR_YEARLY_DAY: + case self::RECUR_YEARLY_WEEKDAY: return Horde_Date_Translation::t("Yearly"); + } + } + + /** + * Sets the length of time between recurrences of this event. + * + * @param integer $interval The time between recurrences. + */ + public function setRecurInterval($interval) + { + if ($interval > 0) { + $this->recurInterval = $interval; + } + } + + /** + * Retrieves the length of time between recurrences of this event. + * + * @return integer The number of seconds between recurrences. + */ + public function getRecurInterval() + { + return $this->recurInterval; + } + + /** + * Sets the number of recurrences of this event. + * + * @param integer $count The number of recurrences. + */ + public function setRecurCount($count) + { + if ($count > 0) { + $this->recurCount = (int)$count; + // Recurrence counts and end dates are mutually exclusive. + $this->recurEnd = null; + } else { + $this->recurCount = null; + } + } + + /** + * Retrieves the number of recurrences of this event. + * + * @return integer The number recurrences. + */ + public function getRecurCount() + { + return $this->recurCount; + } + + /** + * Returns whether this event has a recurrence with a fixed count. + * + * @return boolean True if this recurrence has a fixed count. + */ + public function hasRecurCount() + { + return isset($this->recurCount); + } + + /** + * Sets the start date of the recurrence interval. + * + * @param Horde_Date $start The recurrence start. + */ + public function setRecurStart($start) + { + $this->start = clone $start; + } + + /** + * Retrieves the start date of the recurrence interval. + * + * @return Horde_Date The recurrence start. + */ + public function getRecurStart() + { + return $this->start; + } + + /** + * Sets the end date of the recurrence interval. + * + * @param Horde_Date $end The recurrence end. + */ + public function setRecurEnd($end) + { + if (!empty($end)) { + // Recurrence counts and end dates are mutually exclusive. + $this->recurCount = null; + $this->recurEnd = clone $end; + } else { + $this->recurEnd = $end; + } + } + + /** + * Retrieves the end date of the recurrence interval. + * + * @return Horde_Date The recurrence end. + */ + public function getRecurEnd() + { + return $this->recurEnd; + } + + /** + * Returns whether this event has a recurrence end. + * + * @return boolean True if this recurrence ends. + */ + public function hasRecurEnd() + { + return isset($this->recurEnd) && isset($this->recurEnd->year) && + $this->recurEnd->year != 9999; + } + + /** + * Finds the next recurrence of this event that's after $afterDate. + * + * @param Horde_Date|string $after Return events after this date. + * + * @return Horde_Date|boolean The date of the next recurrence or false + * if the event does not recur after + * $afterDate. + */ + public function nextRecurrence($after) + { + if (!($after instanceof Horde_Date)) { + $after = new Horde_Date($after); + } else { + $after = clone($after); + } + + // Make sure $after and $this->start are in the same TZ + $after->setTimezone($this->start->timezone); + if ($this->start->compareDateTime($after) >= 0) { + return clone $this->start; + } + + if ($this->recurInterval == 0 && empty($this->rdates)) { + return false; + } + + switch ($this->getRecurType()) { + case self::RECUR_DAILY: + $diff = $this->start->diff($after); + $recur = ceil($diff / $this->recurInterval); + if ($this->recurCount && $recur >= $this->recurCount) { + return false; + } + + $recur *= $this->recurInterval; + $next = $this->start->add(array('day' => $recur)); + if ((!$this->hasRecurEnd() || + $next->compareDateTime($this->recurEnd) <= 0) && + $next->compareDateTime($after) >= 0) { + return $next; + } + break; + + case self::RECUR_WEEKLY: + if (empty($this->recurData)) { + return false; + } + + $start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'), + $this->start->year); + $start_week->timezone = $this->start->timezone; + $start_week->hour = $this->start->hour; + $start_week->min = $this->start->min; + $start_week->sec = $this->start->sec; + + // Make sure we are not at the ISO-8601 first week of year while + // still in month 12...OR in the ISO-8601 last week of year while + // in month 1 and adjust the year accordingly. + $week = $after->format('W'); + if ($week == 1 && $after->month == 12) { + $theYear = $after->year + 1; + } elseif ($week >= 52 && $after->month == 1) { + $theYear = $after->year - 1; + } else { + $theYear = $after->year; + } + + $after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear); + $after_week->timezone = $this->start->timezone; + $after_week_end = clone $after_week; + $after_week_end->mday += 7; + + $diff = $start_week->diff($after_week); + $interval = $this->recurInterval * 7; + $repeats = floor($diff / $interval); + if ($diff % $interval < 7) { + $recur = $diff; + } else { + /** + * If the after_week is not in the first week interval the + * search needs to skip ahead a complete interval. The way it is + * calculated here means that an event that occurs every second + * week on Monday and Wednesday with the event actually starting + * on Tuesday or Wednesday will only have one incidence in the + * first week. + */ + $recur = $interval * ($repeats + 1); + } + + if ($this->hasRecurCount()) { + $recurrences = 0; + /** + * Correct the number of recurrences by the number of events + * that lay between the start of the start week and the + * recurrence start. + */ + $next = clone $start_week; + while ($next->compareDateTime($this->start) < 0) { + if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) { + $recurrences--; + } + ++$next->mday; + } + if ($repeats > 0) { + $weekdays = $this->recurData; + $total_recurrences_per_week = 0; + while ($weekdays > 0) { + if ($weekdays % 2) { + $total_recurrences_per_week++; + } + $weekdays = ($weekdays - ($weekdays % 2)) / 2; + } + $recurrences += $total_recurrences_per_week * $repeats; + } + } + + $next = clone $start_week; + $next->mday += $recur; + while ($next->compareDateTime($after) < 0 && + $next->compareDateTime($after_week_end) < 0) { + if ($this->hasRecurCount() + && $next->compareDateTime($after) < 0 + && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) { + $recurrences++; + } + ++$next->mday; + } + if ($this->hasRecurCount() && + $recurrences >= $this->recurCount) { + return false; + } + if (!$this->hasRecurEnd() || + $next->compareDateTime($this->recurEnd) <= 0) { + if ($next->compareDateTime($after_week_end) >= 0) { + return $this->nextRecurrence($after_week_end); + } + while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) && + $next->compareDateTime($after_week_end) < 0) { + ++$next->mday; + } + if (!$this->hasRecurEnd() || + $next->compareDateTime($this->recurEnd) <= 0) { + if ($next->compareDateTime($after_week_end) >= 0) { + return $this->nextRecurrence($after_week_end); + } else { + return $next; + } + } + } + break; + + case self::RECUR_MONTHLY_DATE: + $start = clone $this->start; + if ($after->compareDateTime($start) < 0) { + $after = clone $start; + } else { + $after = clone $after; + } + + // If we're starting past this month's recurrence of the event, + // look in the next month on the day the event recurs. + if ($after->mday > $start->mday) { + ++$after->month; + $after->mday = $start->mday; + } + + // Adjust $start to be the first match. + $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12; + $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; + + if ($this->recurCount && + ($offset / $this->recurInterval) >= $this->recurCount) { + return false; + } + $start->month += $offset; + $count = $offset / $this->recurInterval; + + do { + if ($this->recurCount && + $count++ >= $this->recurCount) { + return false; + } + + // Bail if we've gone past the end of recurrence. + if ($this->hasRecurEnd() && + $this->recurEnd->compareDateTime($start) < 0) { + return false; + } + if ($start->isValid()) { + return $start; + } + + // If the interval is 12, and the date isn't valid, then we + // need to see if February 29th is an option. If not, then the + // event will _never_ recur, and we need to stop checking to + // avoid an infinite loop. + if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) { + return false; + } + + // Add the recurrence interval. + $start->month += $this->recurInterval; + } while (true); + + break; + + case self::RECUR_MONTHLY_WEEKDAY: + // Start with the start date of the event. + $estart = clone $this->start; + + // What day of the week, and week of the month, do we recur on? + if (isset($this->recurNthDay)) { + $nth = $this->recurNthDay; + $weekday = log($this->recurData, 2); + } else { + $nth = ceil($this->start->mday / 7); + $weekday = $estart->dayOfWeek(); + } + + // Adjust $estart to be the first candidate. + $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12; + $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; + + // Adjust our working date until it's after $after. + $estart->month += $offset - $this->recurInterval; + + $count = $offset / $this->recurInterval; + do { + if ($this->recurCount && + $count++ >= $this->recurCount) { + return false; + } + + $estart->month += $this->recurInterval; + + $next = clone $estart; + $next->setNthWeekday($weekday, $nth); + + if ($next->compareDateTime($after) < 0) { + // We haven't made it past $after yet, try again. + continue; + } + if ($this->hasRecurEnd() && + $next->compareDateTime($this->recurEnd) > 0) { + // We've gone past the end of recurrence; we can give up + // now. + return false; + } + + // We have a candidate to return. + break; + } while (true); + + return $next; + + case self::RECUR_YEARLY_DATE: + // Start with the start date of the event. + $estart = clone $this->start; + $after = clone $after; + + if ($after->month > $estart->month || + ($after->month == $estart->month && $after->mday > $estart->mday)) { + ++$after->year; + $after->month = $estart->month; + $after->mday = $estart->mday; + } + + // Seperate case here for February 29th + if ($estart->month == 2 && $estart->mday == 29) { + while (!Horde_Date_Utils::isLeapYear($after->year)) { + ++$after->year; + } + } + + // Adjust $estart to be the first candidate. + $offset = $after->year - $estart->year; + if ($offset > 0) { + $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; + $estart->year += $offset; + } + + // We've gone past the end of recurrence; give up. + if ($this->recurCount && + $offset >= $this->recurCount) { + return false; + } + if ($this->hasRecurEnd() && + $this->recurEnd->compareDateTime($estart) < 0) { + return false; + } + + return $estart; + + case self::RECUR_YEARLY_DAY: + // Check count first. + $dayofyear = $this->start->dayOfYear(); + $count = ($after->year - $this->start->year) / $this->recurInterval + 1; + if ($this->recurCount && + ($count > $this->recurCount || + ($count == $this->recurCount && + $after->dayOfYear() > $dayofyear))) { + return false; + } + + // Start with a rough interval. + $estart = clone $this->start; + $estart->year += floor($count - 1) * $this->recurInterval; + + // Now add the difference to the required day of year. + $estart->mday += $dayofyear - $estart->dayOfYear(); + + // Add an interval if the estimation was wrong. + if ($estart->compareDate($after) < 0) { + $estart->year += $this->recurInterval; + $estart->mday += $dayofyear - $estart->dayOfYear(); + } + + // We've gone past the end of recurrence; give up. + if ($this->hasRecurEnd() && + $this->recurEnd->compareDateTime($estart) < 0) { + return false; + } + + return $estart; + + case self::RECUR_YEARLY_WEEKDAY: + // Start with the start date of the event. + $estart = clone $this->start; + + // What day of the week, and week of the month, do we recur on? + if (isset($this->recurNthDay)) { + $nth = $this->recurNthDay; + $weekday = log($this->recurData, 2); + } else { + $nth = ceil($this->start->mday / 7); + $weekday = $estart->dayOfWeek(); + } + + // Adjust $estart to be the first candidate. + $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval; + + // Adjust our working date until it's after $after. + $estart->year += $offset - $this->recurInterval; + + $count = $offset / $this->recurInterval; + do { + if ($this->recurCount && + $count++ >= $this->recurCount) { + return false; + } + + $estart->year += $this->recurInterval; + + $next = clone $estart; + $next->setNthWeekday($weekday, $nth); + + if ($next->compareDateTime($after) < 0) { + // We haven't made it past $after yet, try again. + continue; + } + if ($this->hasRecurEnd() && + $next->compareDateTime($this->recurEnd) > 0) { + // We've gone past the end of recurrence; we can give up + // now. + return false; + } + + // We have a candidate to return. + break; + } while (true); + + return $next; + } + + // fall-back to RDATE properties + if (!empty($this->rdates)) { + $next = clone $this->start; + foreach ($this->rdates as $rdate) { + $next->year = $rdate->year; + $next->month = $rdate->month; + $next->mday = $rdate->mday; + if ($next->compareDateTime($after) >= 0) { + return $next; + } + } + } + + // We didn't find anything, the recurType was bad, or something else + // went wrong - return false. + return false; + } + + /** + * Returns whether this event has any date that matches the recurrence + * rules and is not an exception. + * + * @return boolean True if an active recurrence exists. + */ + public function hasActiveRecurrence() + { + if (!$this->hasRecurEnd()) { + return true; + } + + $next = $this->nextRecurrence(new Horde_Date($this->start)); + while (is_object($next)) { + if (!$this->hasException($next->year, $next->month, $next->mday) && + !$this->hasCompletion($next->year, $next->month, $next->mday)) { + return true; + } + + $next = $this->nextRecurrence($next->add(array('day' => 1))); + } + + return false; + } + + /** + * Returns the next active recurrence. + * + * @param Horde_Date $afterDate Return events after this date. + * + * @return Horde_Date|boolean The date of the next active + * recurrence or false if the event + * has no active recurrence after + * $afterDate. + */ + public function nextActiveRecurrence($afterDate) + { + $next = $this->nextRecurrence($afterDate); + while (is_object($next)) { + if (!$this->hasException($next->year, $next->month, $next->mday) && + !$this->hasCompletion($next->year, $next->month, $next->mday)) { + return $next; + } + $next->mday++; + $next = $this->nextRecurrence($next); + } + + return false; + } + + /** + * Adds an absolute recurrence date. + * + * @param integer $year The year of the instance. + * @param integer $month The month of the instance. + * @param integer $mday The day of the month of the instance. + */ + public function addRDate($year, $month, $mday) + { + $this->rdates[] = new Horde_Date($year, $month, $mday); + } + + /** + * Adds an exception to a recurring event. + * + * @param integer $year The year of the execption. + * @param integer $month The month of the execption. + * @param integer $mday The day of the month of the exception. + */ + public function addException($year, $month, $mday) + { + $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday); + } + + /** + * Deletes an exception from a recurring event. + * + * @param integer $year The year of the execption. + * @param integer $month The month of the execption. + * @param integer $mday The day of the month of the exception. + */ + public function deleteException($year, $month, $mday) + { + $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions); + if ($key !== false) { + unset($this->exceptions[$key]); + } + } + + /** + * Checks if an exception exists for a given reccurence of an event. + * + * @param integer $year The year of the reucrance. + * @param integer $month The month of the reucrance. + * @param integer $mday The day of the month of the reucrance. + * + * @return boolean True if an exception exists for the given date. + */ + public function hasException($year, $month, $mday) + { + return in_array(sprintf('%04d%02d%02d', $year, $month, $mday), + $this->getExceptions()); + } + + /** + * Retrieves all the exceptions for this event. + * + * @return array Array containing the dates of all the exceptions in + * YYYYMMDD form. + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Adds a completion to a recurring event. + * + * @param integer $year The year of the execption. + * @param integer $month The month of the execption. + * @param integer $mday The day of the month of the completion. + */ + public function addCompletion($year, $month, $mday) + { + $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday); + } + + /** + * Deletes a completion from a recurring event. + * + * @param integer $year The year of the execption. + * @param integer $month The month of the execption. + * @param integer $mday The day of the month of the completion. + */ + public function deleteCompletion($year, $month, $mday) + { + $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions); + if ($key !== false) { + unset($this->completions[$key]); + } + } + + /** + * Checks if a completion exists for a given reccurence of an event. + * + * @param integer $year The year of the reucrance. + * @param integer $month The month of the recurrance. + * @param integer $mday The day of the month of the recurrance. + * + * @return boolean True if a completion exists for the given date. + */ + public function hasCompletion($year, $month, $mday) + { + return in_array(sprintf('%04d%02d%02d', $year, $month, $mday), + $this->getCompletions()); + } + + /** + * Retrieves all the completions for this event. + * + * @return array Array containing the dates of all the completions in + * YYYYMMDD form. + */ + public function getCompletions() + { + return $this->completions; + } + + /** + * Parses a vCalendar 1.0 recurrence rule. + * + * @link http://www.imc.org/pdi/vcal-10.txt + * @link http://www.shuchow.com/vCalAddendum.html + * + * @param string $rrule A vCalendar 1.0 conform RRULE value. + */ + public function fromRRule10($rrule) + { + $this->reset(); + + if (!$rrule) { + return; + } + + if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) { + // No recurrence data - event does not recur. + $this->setRecurType(self::RECUR_NONE); + } + + // Always default the recurInterval to 1. + $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1); + + $remainder = trim($matches[3]); + + switch ($matches[1]) { + case 'D': + $this->setRecurType(self::RECUR_DAILY); + break; + + case 'W': + $this->setRecurType(self::RECUR_WEEKLY); + if (!empty($remainder)) { + $mask = 0; + while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) { + $day = trim($matches[0]); + $remainder = substr($remainder, strlen($matches[0])); + $mask |= $maskdays[$day]; + } + $this->setRecurOnDay($mask); + } else { + // Recur on the day of the week of the original recurrence. + $maskdays = array( + Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY, + Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY, + Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY, + Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY, + Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY, + Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY, + Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY, + ); + $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]); + } + break; + + case 'MP': + $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY); + break; + + case 'MD': + $this->setRecurType(self::RECUR_MONTHLY_DATE); + break; + + case 'YM': + $this->setRecurType(self::RECUR_YEARLY_DATE); + break; + + case 'YD': + $this->setRecurType(self::RECUR_YEARLY_DAY); + break; + } + + // We don't support modifiers at the moment, strip them. + while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) { + $remainder = substr($remainder, 1); + } + if (!empty($remainder)) { + if (strpos($remainder, '#') === 0) { + $this->setRecurCount(substr($remainder, 1)); + } else { + list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d'); + $this->setRecurEnd(new Horde_Date(array('year' => $year, + 'month' => $month, + 'mday' => $mday, + 'hour' => 23, + 'min' => 59, + 'sec' => 59))); + } + } + } + + /** + * Creates a vCalendar 1.0 recurrence rule. + * + * @link http://www.imc.org/pdi/vcal-10.txt + * @link http://www.shuchow.com/vCalAddendum.html + * + * @param Horde_Icalendar $calendar A Horde_Icalendar object instance. + * + * @return string A vCalendar 1.0 conform RRULE value. + */ + public function toRRule10($calendar) + { + switch ($this->recurType) { + case self::RECUR_NONE: + return ''; + + case self::RECUR_DAILY: + $rrule = 'D' . $this->recurInterval; + break; + + case self::RECUR_WEEKLY: + $rrule = 'W' . $this->recurInterval; + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + + for ($i = 0; $i <= 7; ++$i) { + if ($this->recurOnDay(pow(2, $i))) { + $rrule .= ' ' . $vcaldays[$i]; + } + } + break; + + case self::RECUR_MONTHLY_DATE: + $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday); + break; + + case self::RECUR_MONTHLY_WEEKDAY: + $nth_weekday = (int)($this->start->mday / 7); + if (($this->start->mday % 7) > 0) { + $nth_weekday++; + } + + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()]; + + break; + + case self::RECUR_YEARLY_DATE: + $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month); + break; + + case self::RECUR_YEARLY_DAY: + $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear(); + break; + + default: + return ''; + } + + if ($this->hasRecurEnd()) { + $recurEnd = clone $this->recurEnd; + return $rrule . ' ' . $calendar->_exportDateTime($recurEnd); + } + + return $rrule . ' #' . (int)$this->getRecurCount(); + } + + /** + * Parses an iCalendar 2.0 recurrence rule. + * + * @link http://rfc.net/rfc2445.html#s4.3.10 + * @link http://rfc.net/rfc2445.html#s4.8.5 + * @link http://www.shuchow.com/vCalAddendum.html + * + * @param string $rrule An iCalendar 2.0 conform RRULE value. + */ + public function fromRRule20($rrule) + { + $this->reset(); + + // Parse the recurrence rule into keys and values. + $rdata = array(); + $parts = explode(';', $rrule); + foreach ($parts as $part) { + list($key, $value) = explode('=', $part, 2); + $rdata[strtoupper($key)] = $value; + } + + if (isset($rdata['FREQ'])) { + // Always default the recurInterval to 1. + $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1); + + $maskdays = array( + 'SU' => Horde_Date::MASK_SUNDAY, + 'MO' => Horde_Date::MASK_MONDAY, + 'TU' => Horde_Date::MASK_TUESDAY, + 'WE' => Horde_Date::MASK_WEDNESDAY, + 'TH' => Horde_Date::MASK_THURSDAY, + 'FR' => Horde_Date::MASK_FRIDAY, + 'SA' => Horde_Date::MASK_SATURDAY, + ); + + switch (strtoupper($rdata['FREQ'])) { + case 'DAILY': + $this->setRecurType(self::RECUR_DAILY); + break; + + case 'WEEKLY': + $this->setRecurType(self::RECUR_WEEKLY); + if (isset($rdata['BYDAY'])) { + $days = explode(',', $rdata['BYDAY']); + $mask = 0; + foreach ($days as $day) { + $mask |= $maskdays[$day]; + } + $this->setRecurOnDay($mask); + } else { + // Recur on the day of the week of the original + // recurrence. + $maskdays = array( + Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY, + Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY, + Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY, + Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY, + Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY, + Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY, + Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY); + $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]); + } + break; + + case 'MONTHLY': + if (isset($rdata['BYDAY'])) { + $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY); + if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) { + $this->setRecurOnDay($maskdays[$m[2]]); + $this->setRecurNthWeekday($m[1]); + } + } else { + $this->setRecurType(self::RECUR_MONTHLY_DATE); + } + break; + + case 'YEARLY': + if (isset($rdata['BYYEARDAY'])) { + $this->setRecurType(self::RECUR_YEARLY_DAY); + } elseif (isset($rdata['BYDAY'])) { + $this->setRecurType(self::RECUR_YEARLY_WEEKDAY); + if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) { + $this->setRecurOnDay($maskdays[$m[2]]); + $this->setRecurNthWeekday($m[1]); + } + if ($rdata['BYMONTH']) { + $months = explode(',', $rdata['BYMONTH']); + $this->setRecurByMonth($months); + } + } else { + $this->setRecurType(self::RECUR_YEARLY_DATE); + } + break; + } + + if (isset($rdata['UNTIL'])) { + list($year, $month, $mday) = sscanf($rdata['UNTIL'], + '%04d%02d%02d'); + $this->setRecurEnd(new Horde_Date(array('year' => $year, + 'month' => $month, + 'mday' => $mday, + 'hour' => 23, + 'min' => 59, + 'sec' => 59))); + } + if (isset($rdata['COUNT'])) { + $this->setRecurCount($rdata['COUNT']); + } + } else { + // No recurrence data - event does not recur. + $this->setRecurType(self::RECUR_NONE); + } + } + + /** + * Creates an iCalendar 2.0 recurrence rule. + * + * @link http://rfc.net/rfc2445.html#s4.3.10 + * @link http://rfc.net/rfc2445.html#s4.8.5 + * @link http://www.shuchow.com/vCalAddendum.html + * + * @param Horde_Icalendar $calendar A Horde_Icalendar object instance. + * + * @return string An iCalendar 2.0 conform RRULE value. + */ + public function toRRule20($calendar) + { + switch ($this->recurType) { + case self::RECUR_NONE: + return ''; + + case self::RECUR_DAILY: + $rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval; + break; + + case self::RECUR_WEEKLY: + $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY='; + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + + for ($i = $flag = 0; $i <= 7; ++$i) { + if ($this->recurOnDay(pow(2, $i))) { + if ($flag) { + $rrule .= ','; + } + $rrule .= $vcaldays[$i]; + $flag = true; + } + } + break; + + case self::RECUR_MONTHLY_DATE: + $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval; + break; + + case self::RECUR_MONTHLY_WEEKDAY: + if (isset($this->recurNthDay)) { + $nth_weekday = $this->recurNthDay; + $day_of_week = log($this->recurData, 2); + } else { + $day_of_week = $this->start->dayOfWeek(); + $nth_weekday = (int)($this->start->mday / 7); + if (($this->start->mday % 7) > 0) { + $nth_weekday++; + } + } + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval + . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week]; + break; + + case self::RECUR_YEARLY_DATE: + $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval; + break; + + case self::RECUR_YEARLY_DAY: + $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval + . ';BYYEARDAY=' . $this->start->dayOfYear(); + break; + + case self::RECUR_YEARLY_WEEKDAY: + if (isset($this->recurNthDay)) { + $nth_weekday = $this->recurNthDay; + $day_of_week = log($this->recurData, 2); + } else { + $day_of_week = $this->start->dayOfWeek(); + $nth_weekday = (int)($this->start->mday / 7); + if (($this->start->mday % 7) > 0) { + $nth_weekday++; + } + } + $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month; + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval + . ';BYDAY=' + . $nth_weekday + . $vcaldays[$day_of_week] + . ';BYMONTH=' . $this->start->month; + break; + } + + if ($this->hasRecurEnd()) { + $recurEnd = clone $this->recurEnd; + $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd); + } + if ($count = $this->getRecurCount()) { + $rrule .= ';COUNT=' . $count; + } + return $rrule; + } + + /** + * Parses the recurrence data from a hash. + * + * @param array $hash The hash to convert. + * + * @return boolean True if the hash seemed valid, false otherwise. + */ + public function fromHash($hash) + { + $this->reset(); + + if (!isset($hash['interval']) || !isset($hash['cycle'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + $this->setRecurInterval((int)$hash['interval']); + + $month2number = array( + 'january' => 1, + 'february' => 2, + 'march' => 3, + 'april' => 4, + 'may' => 5, + 'june' => 6, + 'july' => 7, + 'august' => 8, + 'september' => 9, + 'october' => 10, + 'november' => 11, + 'december' => 12, + ); + + $parse_day = false; + $set_daymask = false; + $update_month = false; + $update_daynumber = false; + $update_weekday = false; + $nth_weekday = -1; + + switch ($hash['cycle']) { + case 'daily': + $this->setRecurType(self::RECUR_DAILY); + break; + + case 'weekly': + $this->setRecurType(self::RECUR_WEEKLY); + $parse_day = true; + $set_daymask = true; + break; + + case 'monthly': + if (!isset($hash['daynumber'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + switch ($hash['type']) { + case 'daynumber': + $this->setRecurType(self::RECUR_MONTHLY_DATE); + $update_daynumber = true; + break; + + case 'weekday': + $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY); + $this->setRecurNthWeekday($hash['daynumber']); + $parse_day = true; + $set_daymask = true; + break; + } + break; + + case 'yearly': + if (!isset($hash['type'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + switch ($hash['type']) { + case 'monthday': + $this->setRecurType(self::RECUR_YEARLY_DATE); + $update_month = true; + $update_daynumber = true; + break; + + case 'yearday': + if (!isset($hash['month'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + $this->setRecurType(self::RECUR_YEARLY_DAY); + // Start counting days in January. + $hash['month'] = 'january'; + $update_month = true; + $update_daynumber = true; + break; + + case 'weekday': + if (!isset($hash['daynumber'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + $this->setRecurType(self::RECUR_YEARLY_WEEKDAY); + $this->setRecurNthWeekday($hash['daynumber']); + $parse_day = true; + $set_daymask = true; + + if ($hash['month'] && isset($month2number[$hash['month']])) { + $this->setRecurByMonth($month2number[$hash['month']]); + } + break; + } + } + + if (isset($hash['range-type']) && isset($hash['range'])) { + switch ($hash['range-type']) { + case 'number': + $this->setRecurCount((int)$hash['range']); + break; + + case 'date': + $recur_end = new Horde_Date($hash['range']); + $recur_end->hour = 23; + $recur_end->min = 59; + $recur_end->sec = 59; + $this->setRecurEnd($recur_end); + break; + } + } + + // Need to parse ? + $last_found_day = -1; + if ($parse_day) { + if (!isset($hash['day'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + $mask = 0; + $bits = array( + 'monday' => Horde_Date::MASK_MONDAY, + 'tuesday' => Horde_Date::MASK_TUESDAY, + 'wednesday' => Horde_Date::MASK_WEDNESDAY, + 'thursday' => Horde_Date::MASK_THURSDAY, + 'friday' => Horde_Date::MASK_FRIDAY, + 'saturday' => Horde_Date::MASK_SATURDAY, + 'sunday' => Horde_Date::MASK_SUNDAY, + ); + $days = array( + 'monday' => Horde_Date::DATE_MONDAY, + 'tuesday' => Horde_Date::DATE_TUESDAY, + 'wednesday' => Horde_Date::DATE_WEDNESDAY, + 'thursday' => Horde_Date::DATE_THURSDAY, + 'friday' => Horde_Date::DATE_FRIDAY, + 'saturday' => Horde_Date::DATE_SATURDAY, + 'sunday' => Horde_Date::DATE_SUNDAY, + ); + + foreach ($hash['day'] as $day) { + // Validity check. + if (empty($day) || !isset($bits[$day])) { + continue; + } + + $mask |= $bits[$day]; + $last_found_day = $days[$day]; + } + + if ($set_daymask) { + $this->setRecurOnDay($mask); + } + } + + if ($update_month || $update_daynumber || $update_weekday) { + if ($update_month) { + if (isset($month2number[$hash['month']])) { + $this->start->month = $month2number[$hash['month']]; + } + } + + if ($update_daynumber) { + if (!isset($hash['daynumber'])) { + $this->setRecurType(self::RECUR_NONE); + return false; + } + + $this->start->mday = $hash['daynumber']; + } + + if ($update_weekday) { + $this->setNthWeekday($nth_weekday); + } + } + + // Exceptions. + if (isset($hash['exceptions'])) { + $this->exceptions = $hash['exceptions']; + } + + if (isset($hash['completions'])) { + $this->completions = $hash['completions']; + } + + return true; + } + + /** + * Export this object into a hash. + * + * @return array The recurrence hash. + */ + public function toHash() + { + if ($this->getRecurType() == self::RECUR_NONE) { + return array(); + } + + $day2number = array( + 0 => 'sunday', + 1 => 'monday', + 2 => 'tuesday', + 3 => 'wednesday', + 4 => 'thursday', + 5 => 'friday', + 6 => 'saturday' + ); + $month2number = array( + 1 => 'january', + 2 => 'february', + 3 => 'march', + 4 => 'april', + 5 => 'may', + 6 => 'june', + 7 => 'july', + 8 => 'august', + 9 => 'september', + 10 => 'october', + 11 => 'november', + 12 => 'december' + ); + + $hash = array('interval' => $this->getRecurInterval()); + $start = $this->getRecurStart(); + + switch ($this->getRecurType()) { + case self::RECUR_DAILY: + $hash['cycle'] = 'daily'; + break; + + case self::RECUR_WEEKLY: + $hash['cycle'] = 'weekly'; + $bits = array( + 'monday' => Horde_Date::MASK_MONDAY, + 'tuesday' => Horde_Date::MASK_TUESDAY, + 'wednesday' => Horde_Date::MASK_WEDNESDAY, + 'thursday' => Horde_Date::MASK_THURSDAY, + 'friday' => Horde_Date::MASK_FRIDAY, + 'saturday' => Horde_Date::MASK_SATURDAY, + 'sunday' => Horde_Date::MASK_SUNDAY, + ); + $days = array(); + foreach ($bits as $name => $bit) { + if ($this->recurOnDay($bit)) { + $days[] = $name; + } + } + $hash['day'] = $days; + break; + + case self::RECUR_MONTHLY_DATE: + $hash['cycle'] = 'monthly'; + $hash['type'] = 'daynumber'; + $hash['daynumber'] = $start->mday; + break; + + case self::RECUR_MONTHLY_WEEKDAY: + $hash['cycle'] = 'monthly'; + $hash['type'] = 'weekday'; + $hash['daynumber'] = $start->weekOfMonth(); + $hash['day'] = array ($day2number[$start->dayOfWeek()]); + break; + + case self::RECUR_YEARLY_DATE: + $hash['cycle'] = 'yearly'; + $hash['type'] = 'monthday'; + $hash['daynumber'] = $start->mday; + $hash['month'] = $month2number[$start->month]; + break; + + case self::RECUR_YEARLY_DAY: + $hash['cycle'] = 'yearly'; + $hash['type'] = 'yearday'; + $hash['daynumber'] = $start->dayOfYear(); + break; + + case self::RECUR_YEARLY_WEEKDAY: + $hash['cycle'] = 'yearly'; + $hash['type'] = 'weekday'; + $hash['daynumber'] = $start->weekOfMonth(); + $hash['day'] = array ($day2number[$start->dayOfWeek()]); + $hash['month'] = $month2number[$start->month]; + } + + if ($this->hasRecurCount()) { + $hash['range-type'] = 'number'; + $hash['range'] = $this->getRecurCount(); + } elseif ($this->hasRecurEnd()) { + $date = $this->getRecurEnd(); + $hash['range-type'] = 'date'; + $hash['range'] = $date->datestamp(); + } else { + $hash['range-type'] = 'none'; + $hash['range'] = ''; + } + + // Recurrence exceptions + $hash['exceptions'] = $this->exceptions; + $hash['completions'] = $this->completions; + + return $hash; + } + + /** + * Returns a simple object suitable for json transport representing this + * object. + * + * Possible properties are: + * - t: type + * - i: interval + * - e: end date + * - c: count + * - d: data + * - co: completions + * - ex: exceptions + * + * @return object A simple object. + */ + public function toJson() + { + $json = new stdClass; + $json->t = $this->recurType; + $json->i = $this->recurInterval; + if ($this->hasRecurEnd()) { + $json->e = $this->recurEnd->toJson(); + } + if ($this->recurCount) { + $json->c = $this->recurCount; + } + if ($this->recurData) { + $json->d = $this->recurData; + } + if ($this->completions) { + $json->co = $this->completions; + } + if ($this->exceptions) { + $json->ex = $this->exceptions; + } + return $json; + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Component.php b/libcalendaring/lib/OldSabre/VObject/Component.php new file mode 100644 index 0000000..ba41176 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component.php @@ -0,0 +1,405 @@ + 'OldSabre\\VObject\\Component\\VAlarm', + 'VCALENDAR' => 'OldSabre\\VObject\\Component\\VCalendar', + 'VCARD' => 'OldSabre\\VObject\\Component\\VCard', + 'VEVENT' => 'OldSabre\\VObject\\Component\\VEvent', + 'VJOURNAL' => 'OldSabre\\VObject\\Component\\VJournal', + 'VTODO' => 'OldSabre\\VObject\\Component\\VTodo', + 'VFREEBUSY' => 'OldSabre\\VObject\\Component\\VFreeBusy', + ); + + /** + * Creates the new component by name, but in addition will also see if + * there's a class mapped to the property name. + * + * @param string $name + * @param string $value + * @return Component + */ + static public function create($name, $value = null) { + + $name = strtoupper($name); + + if (isset(self::$classMap[$name])) { + return new self::$classMap[$name]($name, $value); + } else { + return new self($name, $value); + } + + } + + /** + * Creates a new component. + * + * By default this object will iterate over its own children, but this can + * be overridden with the iterator argument + * + * @param string $name + * @param ElementList $iterator + */ + public function __construct($name, ElementList $iterator = null) { + + $this->name = strtoupper($name); + if (!is_null($iterator)) $this->iterator = $iterator; + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + $str = "BEGIN:" . $this->name . "\r\n"; + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accomodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * @return int + */ + $sortScore = function($key, $array) { + + if ($array[$key] instanceof Component) { + + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ($array[$key]->name === 'VTIMEZONE') { + $score=300000000; + return $score+$key; + } else { + $score=400000000; + return $score+$key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ($array[$key]->name === 'VERSION') { + $score=100000000; + return $score+$key; + } else { + // All other properties + $score=200000000; + return $score+$key; + } + } + } + + }; + + $tmp = $this->children; + uksort($this->children, function($a, $b) use ($sortScore, $tmp) { + + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + if ($sA === $sB) return 0; + + return ($sA < $sB) ? -1 : 1; + + }); + + foreach($this->children as $child) $str.=$child->serialize(); + $str.= "END:" . $this->name . "\r\n"; + + return $str; + + } + + /** + * Adds a new component or element + * + * You can call this method with the following syntaxes: + * + * add(Node $node) + * add(string $name, $value, array $parameters = array()) + * + * The first version adds an Element + * The second adds a property as a string. + * + * @param mixed $item + * @param mixed $itemValue + * @return void + */ + public function add($item, $itemValue = null, array $parameters = array()) { + + if ($item instanceof Node) { + if (!is_null($itemValue)) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); + } + $item->parent = $this; + $this->children[] = $item; + } elseif(is_string($item)) { + + $item = Property::create($item,$itemValue, $parameters); + $item->parent = $this; + $this->children[] = $item; + + } else { + + throw new \InvalidArgumentException('The first argument must either be a \\OldSabre\\VObject\\Node or a string'); + + } + + } + + /** + * Returns an iterable list of children + * + * @return ElementList + */ + public function children() { + + return new ElementList($this->children); + + } + + /** + * Returns an array with elements that match the specified name. + * + * This function is also aware of MIME-Directory groups (as they appear in + * vcards). This means that if a property is grouped as "HOME.EMAIL", it + * will also be returned when searching for just "EMAIL". If you want to + * search for a property in a specific group, you can select on the entire + * string ("HOME.EMAIL"). If you want to search on a specific property that + * has not been assigned a group, specify ".EMAIL". + * + * Keys are retained from the 'children' array, which may be confusing in + * certain cases. + * + * @param string $name + * @return array + */ + public function select($name) { + + $group = null; + $name = strtoupper($name); + if (strpos($name,'.')!==false) { + list($group,$name) = explode('.', $name, 2); + } + + $result = array(); + foreach($this->children as $key=>$child) { + + if ( + strtoupper($child->name) === $name && + (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group)) + ) { + + $result[$key] = $child; + + } + } + + reset($result); + return $result; + + } + + /** + * This method only returns a list of sub-components. Properties are + * ignored. + * + * @return array + */ + public function getComponents() { + + $result = array(); + foreach($this->children as $child) { + if ($child instanceof Component) { + $result[] = $child; + } + } + + return $result; + + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $result = array(); + foreach($this->children as $child) { + $result = array_merge($result, $child->validate($options)); + } + return $result; + + } + + /* Magic property accessors {{{ */ + + /** + * Using 'get' you will either get a property or component, + * + * If there were no child-elements found with the specified name, + * null is returned. + * + * @param string $name + * @return Property + */ + public function __get($name) { + + $matches = $this->select($name); + if (count($matches)===0) { + return null; + } else { + $firstMatch = current($matches); + /** @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + return $firstMatch; + } + + } + + /** + * This method checks if a sub-element with the specified name exists. + * + * @param string $name + * @return bool + */ + public function __isset($name) { + + $matches = $this->select($name); + return count($matches)>0; + + } + + /** + * Using the setter method you can add properties or subcomponents + * + * You can either pass a Component, Property + * object, or a string to automatically create a Property. + * + * If the item already exists, it will be removed. If you want to add + * a new item with the same name, always use the add() method. + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) { + + $matches = $this->select($name); + $overWrite = count($matches)?key($matches):null; + + if ($value instanceof Component || $value instanceof Property) { + $value->parent = $this; + if (!is_null($overWrite)) { + $this->children[$overWrite] = $value; + } else { + $this->children[] = $value; + } + } elseif (is_scalar($value)) { + $property = Property::create($name,$value); + $property->parent = $this; + if (!is_null($overWrite)) { + $this->children[$overWrite] = $property; + } else { + $this->children[] = $property; + } + } else { + throw new \InvalidArgumentException('You must pass a \\OldSabre\\VObject\\Component, \\OldSabre\\VObject\\Property or scalar type'); + } + + } + + /** + * Removes all properties and components within this component. + * + * @param string $name + * @return void + */ + public function __unset($name) { + + $matches = $this->select($name); + foreach($matches as $k=>$child) { + + unset($this->children[$k]); + $child->parent = null; + + } + + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + * + * @return void + */ + public function __clone() { + + foreach($this->children as $key=>$child) { + $this->children[$key] = clone $child; + $this->children[$key]->parent = $this; + } + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php b/libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php new file mode 100644 index 0000000..4cae670 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php @@ -0,0 +1,108 @@ +TRIGGER; + if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; + + $parentComponent = $this->parent; + if ($related === 'START') { + + if ($parentComponent->name === 'VTODO') { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = clone $parentComponent->$propName->getDateTime(); + $effectiveTrigger->add($triggerDuration); + } else { + if ($parentComponent->name === 'VTODO') { + $endProp = 'DUE'; + } elseif ($parentComponent->name === 'VEVENT') { + $endProp = 'DTEND'; + } else { + throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + } + + if (isset($parentComponent->$endProp)) { + $effectiveTrigger = clone $parentComponent->$endProp->getDateTime(); + $effectiveTrigger->add($triggerDuration); + } elseif (isset($parentComponent->DURATION)) { + $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); + $effectiveTrigger->add($duration); + $effectiveTrigger->add($triggerDuration); + } else { + $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); + $effectiveTrigger->add($triggerDuration); + } + } + } else { + $effectiveTrigger = $trigger->getDateTime(); + } + return $effectiveTrigger; + + } + + /** + * Returns true or false depending on if the event falls in the specified + * time-range. This is used for filtering purposes. + * + * The rules used to determine if an event falls within the specified + * time-range is based on the CalDAV specification. + * + * @param \DateTime $start + * @param \DateTime $end + * @return bool + */ + public function isInTimeRange(\DateTime $start, \DateTime $end) { + + $effectiveTrigger = $this->getEffectiveTriggerTime(); + + if (isset($this->DURATION)) { + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); + $repeat = (string)$this->repeat; + if (!$repeat) { + $repeat = 1; + } + + $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat); + + foreach($period as $occurrence) { + + if ($start <= $occurrence && $end > $occurrence) { + return true; + } + } + return false; + } else { + return ($start <= $effectiveTrigger && $end > $effectiveTrigger); + } + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php b/libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php new file mode 100644 index 0000000..2b1446f --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php @@ -0,0 +1,244 @@ +children as $component) { + + if (!$component instanceof VObject\Component) + continue; + + if (isset($component->{'RECURRENCE-ID'})) + continue; + + if ($componentName && $component->name !== strtoupper($componentName)) + continue; + + if ($component->name === 'VTIMEZONE') + continue; + + $components[] = $component; + + } + + return $components; + + } + + /** + * If this calendar object, has events with recurrence rules, this method + * can be used to expand the event into multiple sub-events. + * + * Each event will be stripped from it's recurrence information, and only + * the instances of the event in the specified timerange will be left + * alone. + * + * In addition, this method will cause timezone information to be stripped, + * and normalized to UTC. + * + * This method will alter the VCalendar. This cannot be reversed. + * + * This functionality is specifically used by the CalDAV standard. It is + * possible for clients to request expand events, if they are rather simple + * clients and do not have the possibility to calculate recurrences. + * + * @param DateTime $start + * @param DateTime $end + * @return void + */ + public function expand(\DateTime $start, \DateTime $end) { + + $newEvents = array(); + + foreach($this->select('VEVENT') as $key=>$vevent) { + + if (isset($vevent->{'RECURRENCE-ID'})) { + unset($this->children[$key]); + continue; + } + + + if (!$vevent->rrule) { + unset($this->children[$key]); + if ($vevent->isInTimeRange($start, $end)) { + $newEvents[] = $vevent; + } + continue; + } + + $uid = (string)$vevent->uid; + if (!$uid) { + throw new \LogicException('Event did not have a UID!'); + } + + $it = new VObject\RecurrenceIterator($this, $vevent->uid); + $it->fastForward($start); + + while($it->valid() && $it->getDTStart() < $end) { + + if ($it->getDTEnd() > $start) { + + $newEvents[] = $it->getEventObject(); + + } + $it->next(); + + } + unset($this->children[$key]); + + } + + foreach($newEvents as $newEvent) { + + foreach($newEvent->children as $child) { + if ($child instanceof VObject\Property\DateTime && + $child->getDateType() == VObject\Property\DateTime::LOCALTZ) { + $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC); + } + } + + $this->add($newEvent); + + } + + // Removing all VTIMEZONE components + unset($this->VTIMEZONE); + + } + + /** + * Validates the node for correctness. + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @return array + */ + /* + public function validate() { + + $warnings = array(); + + $version = $this->select('VERSION'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time', + 'node' => $this, + ); + } else { + if ((string)$this->VERSION !== '2.0') { + $warnings[] = array( + 'level' => 1, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ); + } + } + $version = $this->select('PRODID'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time', + 'node' => $this, + ); + } + if (count($this->CALSCALE) > 1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The CALSCALE property must not be specified more than once.', + 'node' => $this, + ); + } + if (count($this->METHOD) > 1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The METHOD property must not be specified more than once.', + 'node' => $this, + ); + } + + $allowedComponents = array( + 'VEVENT', + 'VTODO', + 'VJOURNAL', + 'VFREEBUSY', + 'VTIMEZONE', + ); + $allowedProperties = array( + 'PRODID', + 'VERSION', + 'CALSCALE', + 'METHOD', + ); + $componentsFound = 0; + foreach($this->children as $child) { + if($child instanceof Component) { + $componentsFound++; + if (!in_array($child->name, $allowedComponents)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component", + 'node' => $this, + ); + } + } + if ($child instanceof Property) { + if (!in_array($child->name, $allowedProperties)) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component", + 'node' => $this, + ); + } + } + } + + if ($componentsFound===0) { + $warnings[] = array( + 'level' => 1, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ); + } + + return array_merge( + $warnings, + parent::validate() + ); + + } + */ + +} + diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VCard.php b/libcalendaring/lib/OldSabre/VObject/Component/VCard.php new file mode 100644 index 0000000..93ef03f --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VCard.php @@ -0,0 +1,107 @@ +select('VERSION'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = self::DEFAULT_VERSION; + } + } else { + $version = (string)$this->VERSION; + if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') { + $warnings[] = array( + 'level' => 1, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = '4.0'; + } + } + + } + $fn = $this->select('FN'); + if (count($fn)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if (($options & self::REPAIR) && count($fn) === 0) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string)$this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1] . ' ' . $value[0]; + } else { + $this->FN = $value[0]; + } + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string)$this->ORG; + } + + } + } + + return array_merge( + parent::validate($options), + $warnings + ); + + } + +} + diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VEvent.php b/libcalendaring/lib/OldSabre/VObject/Component/VEvent.php new file mode 100644 index 0000000..1532110 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VEvent.php @@ -0,0 +1,70 @@ +RRULE) { + $it = new VObject\RecurrenceIterator($this); + $it->fastForward($start); + + // We fast-forwarded to a spot where the end-time of the + // recurrence instance exceeded the start of the requested + // time-range. + // + // If the starttime of the recurrence did not exceed the + // end of the time range as well, we have a match. + return ($it->getDTStart() < $end && $it->getDTEnd() > $start); + + } + + $effectiveStart = $this->DTSTART->getDateTime(); + if (isset($this->DTEND)) { + + // The DTEND property is considered non inclusive. So for a 3 day + // event in july, dtstart and dtend would have to be July 1st and + // July 4th respectively. + // + // See: + // http://tools.ietf.org/html/rfc5545#page-54 + $effectiveEnd = $this->DTEND->getDateTime(); + + } elseif (isset($this->DURATION)) { + $effectiveEnd = clone $effectiveStart; + $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) ); + } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) { + $effectiveEnd = clone $effectiveStart; + $effectiveEnd->modify('+1 day'); + } else { + $effectiveEnd = clone $effectiveStart; + } + return ( + ($start <= $effectiveEnd) && ($end > $effectiveStart) + ); + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php b/libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php new file mode 100644 index 0000000..a8aa370 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php @@ -0,0 +1,68 @@ +select('FREEBUSY') as $freebusy) { + + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string)$freebusy); + + foreach($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $tmp = clone $busyStart; + $tmp->add($busyEnd); + $busyEnd = $tmp; + } + + if($start < $busyEnd && $end > $busyStart) { + return false; + } + + } + + } + + return true; + + } + +} + diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VJournal.php b/libcalendaring/lib/OldSabre/VObject/Component/VJournal.php new file mode 100644 index 0000000..c9b0692 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VJournal.php @@ -0,0 +1,46 @@ +DTSTART)?$this->DTSTART->getDateTime():null; + if ($dtstart) { + $effectiveEnd = clone $dtstart; + if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) { + $effectiveEnd->modify('+1 day'); + } + + return ($start <= $effectiveEnd && $end > $dtstart); + + } + return false; + + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Component/VTodo.php b/libcalendaring/lib/OldSabre/VObject/Component/VTodo.php new file mode 100644 index 0000000..06bac4f --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Component/VTodo.php @@ -0,0 +1,68 @@ +DTSTART)?$this->DTSTART->getDateTime():null; + $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null; + $due = isset($this->DUE)?$this->DUE->getDateTime():null; + $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null; + $created = isset($this->CREATED)?$this->CREATED->getDateTime():null; + + if ($dtstart) { + if ($duration) { + $effectiveEnd = clone $dtstart; + $effectiveEnd->add($duration); + return $start <= $effectiveEnd && $end > $dtstart; + } elseif ($due) { + return + ($start < $due || $start <= $dtstart) && + ($end > $dtstart || $end >= $due); + } else { + return $start <= $dtstart && $end > $dtstart; + } + } + if ($due) { + return ($start < $due && $end >= $due); + } + if ($completed && $created) { + return + ($start <= $created || $start <= $completed) && + ($end >= $created || $end >= $completed); + } + if ($completed) { + return ($start <= $completed && $end >= $completed); + } + if ($created) { + return ($end > $created); + } + return true; + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/DateTimeParser.php b/libcalendaring/lib/OldSabre/VObject/DateTimeParser.php new file mode 100644 index 0000000..a8cfca5 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/DateTimeParser.php @@ -0,0 +1,181 @@ +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\+|-)?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration); + } + + if (!$asString) { + $invert = false; + if ($matches['plusminus']==='-') { + $invert = true; + } + + + $parts = array( + 'week', + 'day', + 'hour', + 'minute', + 'second', + ); + foreach($parts as $part) { + $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0; + } + + + // We need to re-construct the $duration string, because weeks and + // days are not supported by DateInterval in the same string. + $duration = 'P'; + $days = $matches['day']; + if ($matches['week']) { + $days+=$matches['week']*7; + } + if ($days) + $duration.=$days . 'D'; + + if ($matches['minute'] || $matches['second'] || $matches['hour']) { + $duration.='T'; + + if ($matches['hour']) + $duration.=$matches['hour'].'H'; + + if ($matches['minute']) + $duration.=$matches['minute'].'M'; + + if ($matches['second']) + $duration.=$matches['second'].'S'; + + } + + if ($duration==='P') { + $duration = 'PT0S'; + } + $iv = new \DateInterval($duration); + if ($invert) $iv->invert = true; + + return $iv; + + } + + + + $parts = array( + 'week', + 'day', + 'hour', + 'minute', + 'second', + ); + + $newDur = ''; + foreach($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur.=' '.$matches[$part] . ' ' . $part . 's'; + } + } + + $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur); + if ($newDur === '+') { $newDur = '+0 seconds'; }; + return $newDur; + + } + + /** + * Parses either a Date or DateTime, or Duration value. + * + * @param string $date + * @param DateTimeZone|string $referenceTZ + * @return DateTime|DateInterval + */ + static public function parse($date, $referenceTZ = null) { + + if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) { + return self::parseDuration($date); + } elseif (strlen($date)===8) { + return self::parseDate($date); + } else { + return self::parseDateTime($date, $referenceTZ); + } + + } + + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Document.php b/libcalendaring/lib/OldSabre/VObject/Document.php new file mode 100644 index 0000000..5b5e2e6 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Document.php @@ -0,0 +1,109 @@ +value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * @param string $name + * @param array $children + * @return Component + */ + public function createComponent($name, array $children = array()) { + + $component = Component::create($name); + foreach($children as $k=>$v) { + + if ($v instanceof Node) { + $component->add($v); + } else { + $component->add($k, $v); + } + + } + return $component; + + } + + /** + * Factory method for creating new properties + * + * This method automatically searches for the correct property class, based + * on its name. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param string $name + * @param mixed $value + * @param array $parameters + * @return Property + */ + public function createProperty($name, $value = null, array $parameters = array()) { + + return Property::create($name, $value, $parameters); + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/ElementList.php b/libcalendaring/lib/OldSabre/VObject/ElementList.php new file mode 100644 index 0000000..da2aa34 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/ElementList.php @@ -0,0 +1,172 @@ +vevent where there's multiple VEVENT objects. + * + * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ElementList implements \Iterator, \Countable, \ArrayAccess { + + /** + * Inner elements + * + * @var array + */ + protected $elements = array(); + + /** + * Creates the element list. + * + * @param array $elements + */ + public function __construct(array $elements) { + + $this->elements = $elements; + + } + + /* {{{ Iterator interface */ + + /** + * Current position + * + * @var int + */ + private $key = 0; + + /** + * Returns current item in iteration + * + * @return Element + */ + public function current() { + + return $this->elements[$this->key]; + + } + + /** + * To the next item in the iterator + * + * @return void + */ + public function next() { + + $this->key++; + + } + + /** + * Returns the current iterator key + * + * @return int + */ + public function key() { + + return $this->key; + + } + + /** + * Returns true if the current position in the iterator is a valid one + * + * @return bool + */ + public function valid() { + + return isset($this->elements[$this->key]); + + } + + /** + * Rewinds the iterator + * + * @return void + */ + public function rewind() { + + $this->key = 0; + + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements + * + * @return int + */ + public function count() { + + return count($this->elements); + + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + + /** + * Checks if an item exists through ArrayAccess. + * + * @param int $offset + * @return bool + */ + public function offsetExists($offset) { + + return isset($this->elements[$offset]); + + } + + /** + * Gets an item through ArrayAccess. + * + * @param int $offset + * @return mixed + */ + public function offsetGet($offset) { + + return $this->elements[$offset]; + + } + + /** + * Sets an item through ArrayAccess. + * + * @param int $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset,$value) { + + throw new \LogicException('You can not add new objects to an ElementList'); + + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return void + */ + public function offsetUnset($offset) { + + throw new \LogicException('You can not remove objects from an ElementList'); + + } + + /* }}} */ + +} diff --git a/libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php b/libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php new file mode 100644 index 0000000..7671cd0 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php @@ -0,0 +1,322 @@ +setTimeRange($start, $end); + } + + if ($objects) { + $this->setObjects($objects); + } + + } + + /** + * Sets the VCALENDAR object. + * + * If this is set, it will not be generated for you. You are responsible + * for setting things like the METHOD, CALSCALE, VERSION, etc.. + * + * The VFREEBUSY object will be automatically added though. + * + * @param Component $vcalendar + * @return void + */ + public function setBaseObject(Component $vcalendar) { + + $this->baseObject = $vcalendar; + + } + + /** + * Sets the input objects + * + * You must either specify a valendar object as a strong, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + * + * @param mixed $objects + * @return void + */ + public function setObjects($objects) { + + if (!is_array($objects)) { + $objects = array($objects); + } + + $this->objects = array(); + foreach($objects as $object) { + + if (is_string($object)) { + $this->objects[] = Reader::read($object); + } elseif ($object instanceof Component) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\OldSabre\\VObject\\Component arguments to setObjects'); + } + + } + + } + + /** + * Sets the time range + * + * Any freebusy object falling outside of this time range will be ignored. + * + * @param DateTime $start + * @param DateTime $end + * @return void + */ + public function setTimeRange(\DateTime $start = null, \DateTime $end = null) { + + $this->start = $start; + $this->end = $end; + + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + * + * @return Component + */ + public function getResult() { + + $busyTimes = array(); + + foreach($this->objects as $object) { + + foreach($object->getBaseComponents() as $component) { + + switch($component->name) { + + case 'VEVENT' : + + $FBTYPE = 'BUSY'; + if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ($status==='CANCELLED') { + break; + } + if ($status==='TENTATIVE') { + $FBTYPE = 'BUSY-TENTATIVE'; + } + } + + $times = array(); + + if ($component->RRULE) { + + $iterator = new RecurrenceIterator($object, (string)$component->uid); + if ($this->start) { + $iterator->fastForward($this->start); + } + + $maxRecurrences = 200; + + while($iterator->valid() && --$maxRecurrences) { + + $startTime = $iterator->getDTStart(); + if ($this->end && $startTime > $this->end) { + break; + } + $times[] = array( + $iterator->getDTStart(), + $iterator->getDTEnd(), + ); + + $iterator->next(); + + } + + } else { + + $startTime = $component->DTSTART->getDateTime(); + if ($this->end && $startTime > $this->end) { + break; + } + $endTime = null; + if (isset($component->DTEND)) { + $endTime = $component->DTEND->getDateTime(); + } elseif (isset($component->DURATION)) { + $duration = DateTimeParser::parseDuration((string)$component->DURATION); + $endTime = clone $startTime; + $endTime->add($duration); + } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) { + $endTime = clone $startTime; + $endTime->modify('+1 day'); + } else { + // The event had no duration (0 seconds) + break; + } + + $times[] = array($startTime, $endTime); + + } + + foreach($times as $time) { + + if ($this->end && $time[0] > $this->end) break; + if ($this->start && $time[1] < $this->start) break; + + $busyTimes[] = array( + $time[0], + $time[1], + $FBTYPE, + ); + } + break; + + case 'VFREEBUSY' : + foreach($component->FREEBUSY as $freebusy) { + + $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY'; + + // Skipping intervals marked as 'free' + if ($fbType==='FREE') + continue; + + $values = explode(',', $freebusy); + foreach($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') { + $duration = DateTimeParser::parseDuration($endTime); + $endTime = clone $startTime; + $endTime->add($duration); + } else { + $endTime = DateTimeParser::parseDateTime($endTime); + } + + if($this->start && $this->start > $endTime) continue; + if($this->end && $this->end < $startTime) continue; + $busyTimes[] = array( + $startTime, + $endTime, + $fbType + ); + + } + + + } + break; + + + + } + + + } + + } + + if ($this->baseObject) { + $calendar = $this->baseObject; + } else { + $calendar = Component::create('VCALENDAR'); + $calendar->version = '2.0'; + $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN'; + $calendar->calscale = 'GREGORIAN'; + } + + $vfreebusy = Component::create('VFREEBUSY'); + $calendar->add($vfreebusy); + + if ($this->start) { + $dtstart = Property::create('DTSTART'); + $dtstart->setDateTime($this->start,Property\DateTime::UTC); + $vfreebusy->add($dtstart); + } + if ($this->end) { + $dtend = Property::create('DTEND'); + $dtend->setDateTime($this->end,Property\DateTime::UTC); + $vfreebusy->add($dtend); + } + $dtstamp = Property::create('DTSTAMP'); + $dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC); + $vfreebusy->add($dtstamp); + + foreach($busyTimes as $busyTime) { + + $busyTime[0]->setTimeZone(new \DateTimeZone('UTC')); + $busyTime[1]->setTimeZone(new \DateTimeZone('UTC')); + + $prop = Property::create( + 'FREEBUSY', + $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z') + ); + $prop['FBTYPE'] = $busyTime[2]; + $vfreebusy->add($prop); + + } + + return $calendar; + + } + +} + diff --git a/libcalendaring/lib/OldSabre/VObject/Node.php b/libcalendaring/lib/OldSabre/VObject/Node.php new file mode 100644 index 0000000..e86f29e --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Node.php @@ -0,0 +1,187 @@ +iterator)) + return $this->iterator; + + return new ElementList(array($this)); + + } + + /** + * Sets the overridden iterator + * + * Note that this is not actually part of the iterator interface + * + * @param ElementList $iterator + * @return void + */ + public function setIterator(ElementList $iterator) { + + $this->iterator = $iterator; + + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements + * + * @return int + */ + public function count() { + + $it = $this->getIterator(); + return $it->count(); + + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + + /** + * Checks if an item exists through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return bool + */ + public function offsetExists($offset) { + + $iterator = $this->getIterator(); + return $iterator->offsetExists($offset); + + } + + /** + * Gets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return mixed + */ + public function offsetGet($offset) { + + $iterator = $this->getIterator(); + return $iterator->offsetGet($offset); + + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset,$value) { + + $iterator = $this->getIterator(); + $iterator->offsetSet($offset,$value); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + // @codeCoverageIgnoreEnd + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @return void + */ + public function offsetUnset($offset) { + + $iterator = $this->getIterator(); + $iterator->offsetUnset($offset); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + // @codeCoverageIgnoreEnd + + /* }}} */ + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Parameter.php b/libcalendaring/lib/OldSabre/VObject/Parameter.php new file mode 100644 index 0000000..19fc024 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Parameter.php @@ -0,0 +1,91 @@ +name = strtoupper($name); + $this->value = $value; + + } + + /** + * Returns the parameter's internal value. + * + * @return string + */ + public function getValue() { + + return $this->value; + + } + + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + if (is_null($this->value)) { + return $this->name; + } + $value = str_replace("\n", '\n', $this->value); + if (preg_match('#(?: [:;\\\\])#x', $value)) { + $value = '"' . $value . '"'; + } + return $this->name . '=' . $value; + + } + + /** + * Called when this object is being cast to a string + * + * @return string + */ + public function __toString() { + + return $this->value; + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/ParseException.php b/libcalendaring/lib/OldSabre/VObject/ParseException.php new file mode 100644 index 0000000..7bbdd74 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/ParseException.php @@ -0,0 +1,12 @@ + 'OldSabre\\VObject\\Property\\DateTime', + 'CREATED' => 'OldSabre\\VObject\\Property\\DateTime', + 'DTEND' => 'OldSabre\\VObject\\Property\\DateTime', + 'DTSTAMP' => 'OldSabre\\VObject\\Property\\DateTime', + 'DTSTART' => 'OldSabre\\VObject\\Property\\DateTime', + 'DUE' => 'OldSabre\\VObject\\Property\\DateTime', + 'EXDATE' => 'OldSabre\\VObject\\Property\\MultiDateTime', + 'LAST-MODIFIED' => 'OldSabre\\VObject\\Property\\DateTime', + 'RECURRENCE-ID' => 'OldSabre\\VObject\\Property\\DateTime', + 'TRIGGER' => 'OldSabre\\VObject\\Property\\DateTime', + 'N' => 'OldSabre\\VObject\\Property\\Compound', + 'ORG' => 'OldSabre\\VObject\\Property\\Compound', + 'ADR' => 'OldSabre\\VObject\\Property\\Compound', + 'CATEGORIES' => 'OldSabre\\VObject\\Property\\Compound', + ); + + /** + * Creates the new property by name, but in addition will also see if + * there's a class mapped to the property name. + * + * Parameters can be specified with the optional third argument. Parameters + * must be a key->value map of the parameter name, and value. If the value + * is specified as an array, it is assumed that multiple parameters with + * the same name should be added. + * + * @param string $name + * @param string $value + * @param array $parameters + * @return Property + */ + static public function create($name, $value = null, array $parameters = array()) { + + $name = strtoupper($name); + $shortName = $name; + $group = null; + if (strpos($shortName,'.')!==false) { + list($group, $shortName) = explode('.', $shortName); + } + + if (isset(self::$classMap[$shortName])) { + return new self::$classMap[$shortName]($name, $value, $parameters); + } else { + return new self($name, $value, $parameters); + } + + } + + /** + * Creates a new property object + * + * Parameters can be specified with the optional third argument. Parameters + * must be a key->value map of the parameter name, and value. If the value + * is specified as an array, it is assumed that multiple parameters with + * the same name should be added. + * + * @param string $name + * @param string $value + * @param array $parameters + */ + public function __construct($name, $value = null, array $parameters = array()) { + + if (!is_scalar($value) && !is_null($value)) { + throw new \InvalidArgumentException('The value argument must be scalar or null'); + } + + $name = strtoupper($name); + $group = null; + if (strpos($name,'.')!==false) { + list($group, $name) = explode('.', $name); + } + $this->name = $name; + $this->group = $group; + $this->setValue($value); + + foreach($parameters as $paramName => $paramValues) { + + if (!is_array($paramValues)) { + $paramValues = array($paramValues); + } + + foreach($paramValues as $paramValue) { + $this->add($paramName, $paramValue); + } + + } + + } + + /** + * Updates the internal value + * + * @param string $value + * @return void + */ + public function setValue($value) { + + $this->value = $value; + + } + + /** + * Returns the internal value + * + * @param string $value + * @return string + */ + public function getValue() { + + return $this->value; + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + $str = $this->name; + if ($this->group) $str = $this->group . '.' . $this->name; + + foreach($this->parameters as $param) { + + $str.=';' . $param->serialize(); + + } + + $src = array( + '\\', + "\n", + "\r", + ); + $out = array( + '\\\\', + '\n', + '', + ); + + // avoid double-escaping of \, and \; from Compound properties + if (method_exists($this, 'setParts')) { + $src[] = '\\\\,'; + $out[] = '\\,'; + $src[] = '\\\\;'; + $out[] = '\\;'; + } + + $str.=':' . str_replace($src, $out, $this->value); + + $out = ''; + while(strlen($str)>0) { + if (strlen($str)>75) { + $out.= mb_strcut($str,0,75,'utf-8') . "\r\n"; + $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8'); + } else { + $out.=$str . "\r\n"; + $str=''; + break; + } + } + + return $out; + + } + + /** + * Adds a new componenten or element + * + * You can call this method with the following syntaxes: + * + * add(Parameter $element) + * add(string $name, $value) + * + * The first version adds an Parameter + * The second adds a property as a string. + * + * @param mixed $item + * @param mixed $itemValue + * @return void + */ + public function add($item, $itemValue = null) { + + if ($item instanceof Parameter) { + if (!is_null($itemValue)) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject'); + } + $item->parent = $this; + $this->parameters[] = $item; + } elseif(is_string($item)) { + + $parameter = new Parameter($item,$itemValue); + $parameter->parent = $this; + $this->parameters[] = $parameter; + + } else { + + throw new \InvalidArgumentException('The first argument must either be a Node a string'); + + } + + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists + * + * @param mixed $name + * @return bool + */ + public function offsetExists($name) { + + if (is_int($name)) return parent::offsetExists($name); + + $name = strtoupper($name); + + foreach($this->parameters as $parameter) { + if ($parameter->name == $name) return true; + } + return false; + + } + + /** + * Returns a parameter, or parameter list. + * + * @param string $name + * @return Node + */ + public function offsetGet($name) { + + if (is_int($name)) return parent::offsetGet($name); + $name = strtoupper($name); + + $result = array(); + foreach($this->parameters as $parameter) { + if ($parameter->name == $name) + $result[] = $parameter; + } + + if (count($result)===0) { + return null; + } elseif (count($result)===1) { + return $result[0]; + } else { + $result[0]->setIterator(new ElementList($result)); + return $result[0]; + } + + } + + /** + * Creates a new parameter + * + * @param string $name + * @param mixed $value + * @return void + */ + public function offsetSet($name, $value) { + + if (is_int($name)) parent::offsetSet($name, $value); + + if (is_scalar($value)) { + if (!is_string($name)) + throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.'); + + $this->offsetUnset($name); + $parameter = new Parameter($name, $value); + $parameter->parent = $this; + $this->parameters[] = $parameter; + + } elseif ($value instanceof Parameter) { + if (!is_null($name)) + throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\OldSabre\\VObject\\Parameter. Add using $array[]=$parameterObject.'); + + $value->parent = $this; + $this->parameters[] = $value; + } else { + throw new \InvalidArgumentException('You can only add parameters to the property object'); + } + + } + + /** + * Removes one or more parameters with the specified name + * + * @param string $name + * @return void + */ + public function offsetUnset($name) { + + if (is_int($name)) parent::offsetUnset($name); + $name = strtoupper($name); + + foreach($this->parameters as $key=>$parameter) { + if ($parameter->name == $name) { + $parameter->parent = null; + unset($this->parameters[$key]); + } + + } + + } + + /* }}} */ + + /** + * Called when this object is being cast to a string + * + * @return string + */ + public function __toString() { + + return (string)$this->value; + + } + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + * + * @return void + */ + public function __clone() { + + foreach($this->parameters as $key=>$child) { + $this->parameters[$key] = clone $child; + $this->parameters[$key]->parent = $this; + } + + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $warnings = array(); + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->value)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'Property is not valid UTF-8!', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->value = StringUtil::convertToUTF8($this->value); + } + } + + // Checking if the propertyname does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed', + 'node' => $this, + ); + if ($options & self::REPAIR) { + // Uppercasing and converting underscores to dashes. + $this->name = strtoupper( + str_replace('_', '-', $this->name) + ); + // Removing every other invalid character + $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); + + } + + } + + // Validating inner parameters + foreach($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Property/Compound.php b/libcalendaring/lib/OldSabre/VObject/Property/Compound.php new file mode 100644 index 0000000..1300807 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Property/Compound.php @@ -0,0 +1,125 @@ + ';', + '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("/(?value); + + // remove slashes from any semicolon and comma left escaped in the single values + $compoundValues = array_map( + function($val) { + return strtr($val, array('\,' => ',', '\;' => ';')); + }, $compoundValues); + + return $compoundValues; + + } + + /** + * Returns the delimiter for this property. + * + * @return string + */ + public function getDelimiter() { + + if (!$this->delimiter) { + if (isset(self::$delimiterMap[$this->name])) { + $this->delimiter = self::$delimiterMap[$this->name]; + } else { + // To be a bit future proof, we are going to default the + // delimiter to ; + $this->delimiter = ';'; + } + } + return $this->delimiter; + + } + + /** + * Set a compound value as an array. + * + * + * @param $name string + * @return array + */ + public function setParts(array $values) { + + // add slashes to all semicolons and commas in the single values + $values = array_map( + function($val) { + return strtr($val, array(',' => '\,', ';' => '\;')); + }, $values); + + $this->setValue( + implode($this->getDelimiter(), $values) + ); + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Property/DateTime.php b/libcalendaring/lib/OldSabre/VObject/Property/DateTime.php new file mode 100644 index 0000000..60a3781 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Property/DateTime.php @@ -0,0 +1,245 @@ +setValue($dt->format('Ymd\\THis')); + $this->offsetUnset('VALUE'); + $this->offsetUnset('TZID'); + $this->offsetSet('VALUE','DATE-TIME'); + break; + case self::UTC : + $dt->setTimeZone(new \DateTimeZone('UTC')); + $this->setValue($dt->format('Ymd\\THis\\Z')); + $this->offsetUnset('VALUE'); + $this->offsetUnset('TZID'); + $this->offsetSet('VALUE','DATE-TIME'); + break; + case self::LOCALTZ : + $this->setValue($dt->format('Ymd\\THis')); + $this->offsetUnset('VALUE'); + $this->offsetUnset('TZID'); + $this->offsetSet('VALUE','DATE-TIME'); + $this->offsetSet('TZID', $dt->getTimeZone()->getName()); + break; + case self::DATE : + $this->setValue($dt->format('Ymd')); + $this->offsetUnset('VALUE'); + $this->offsetUnset('TZID'); + $this->offsetSet('VALUE','DATE'); + break; + default : + throw new \InvalidArgumentException('You must pass a valid dateType constant'); + + } + $this->dateTime = $dt; + $this->dateType = $dateType; + + } + + /** + * Returns the current DateTime value. + * + * If no value was set, this method returns null. + * + * @return \DateTime|null + */ + public function getDateTime() { + + if ($this->dateTime) + return $this->dateTime; + + list( + $this->dateType, + $this->dateTime + ) = self::parseData($this->value, $this); + return $this->dateTime; + + } + + /** + * Returns the type of Date format. + * + * This method returns one of the format constants. If no date was set, + * this method will return null. + * + * @return int|null + */ + public function getDateType() { + + if ($this->dateType) + return $this->dateType; + + list( + $this->dateType, + $this->dateTime, + ) = self::parseData($this->value, $this); + return $this->dateType; + + } + + /** + * This method will return true, if the property had a date and a time, as + * opposed to only a date. + * + * @return bool + */ + public function hasTime() { + + return $this->getDateType()!==self::DATE; + + } + + /** + * Parses the internal data structure to figure out what the current date + * and time is. + * + * The returned array contains two elements: + * 1. A 'DateType' constant (as defined on this class), or null. + * 2. A DateTime object (or null) + * + * @param string|null $propertyValue The string to parse (yymmdd or + * ymmddThhmmss, etc..) + * @param \OldSabre\VObject\Property|null $property The instance of the + * property we're parsing. + * @return array + */ + static public function parseData($propertyValue, VObject\Property $property = null) { + + if (is_null($propertyValue)) { + return array(null, null); + } + + $date = '(?P[1-2][0-9]{3})(?P[0-1][0-9])(?P[0-3][0-9])'; + $time = '(?P[0-2][0-9])(?P[0-5][0-9])(?P[0-5][0-9])'; + $regex = "/^$date(T$time(?PZ)?)?$/"; + + if (!preg_match($regex, $propertyValue, $matches)) { + throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string'); + } + + if (!isset($matches['hour'])) { + // Date-only + return array( + self::DATE, + new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')), + ); + } + + $dateStr = + $matches['year'] .'-' . + $matches['month'] . '-' . + $matches['date'] . ' ' . + $matches['hour'] . ':' . + $matches['minute'] . ':' . + $matches['second']; + + if (isset($matches['isutc'])) { + $dt = new \DateTime($dateStr,new \DateTimeZone('UTC')); + $dt->setTimeZone(new \DateTimeZone('UTC')); + return array( + self::UTC, + $dt + ); + } + + // Finding the timezone. + $tzid = $property['TZID']; + if (!$tzid) { + // This was a floating time string. This implies we use the + // timezone from date_default_timezone_set / date.timezone ini + // setting. + return array( + self::LOCAL, + new \DateTime($dateStr) + ); + } + + // To look up the timezone, we must first find the VCALENDAR component. + $root = $property; + while($root->parent) { + $root = $root->parent; + } + if ($root->name === 'VCALENDAR') { + $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root); + } else { + $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid); + } + + $dt = new \DateTime($dateStr, $tz); + $dt->setTimeZone($tz); + + return array( + self::LOCALTZ, + $dt + ); + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php b/libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php new file mode 100644 index 0000000..45ee811 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php @@ -0,0 +1,180 @@ +offsetUnset('VALUE'); + $this->offsetUnset('TZID'); + switch($dateType) { + + case DateTime::LOCAL : + $val = array(); + foreach($dt as $i) { + $val[] = $i->format('Ymd\\THis'); + } + $this->setValue(implode(',',$val)); + $this->offsetSet('VALUE','DATE-TIME'); + break; + case DateTime::UTC : + $val = array(); + foreach($dt as $i) { + $i->setTimeZone(new \DateTimeZone('UTC')); + $val[] = $i->format('Ymd\\THis\\Z'); + } + $this->setValue(implode(',',$val)); + $this->offsetSet('VALUE','DATE-TIME'); + break; + case DateTime::LOCALTZ : + $val = array(); + foreach($dt as $i) { + $val[] = $i->format('Ymd\\THis'); + } + $this->setValue(implode(',',$val)); + $this->offsetSet('VALUE','DATE-TIME'); + $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName()); + break; + case DateTime::DATE : + $val = array(); + foreach($dt as $i) { + $val[] = $i->format('Ymd'); + } + $this->setValue(implode(',',$val)); + $this->offsetSet('VALUE','DATE'); + break; + default : + throw new \InvalidArgumentException('You must pass a valid dateType constant'); + + } + $this->dateTimes = $dt; + $this->dateType = $dateType; + + } + + /** + * Returns the current DateTime value. + * + * If no value was set, this method returns null. + * + * @return array|null + */ + public function getDateTimes() { + + if ($this->dateTimes) + return $this->dateTimes; + + $dts = array(); + + if (!$this->value) { + $this->dateTimes = null; + $this->dateType = null; + return null; + } + + foreach(explode(',',$this->value) as $val) { + list( + $type, + $dt + ) = DateTime::parseData($val, $this); + $dts[] = $dt; + $this->dateType = $type; + } + $this->dateTimes = $dts; + return $this->dateTimes; + + } + + /** + * Returns the type of Date format. + * + * This method returns one of the format constants. If no date was set, + * this method will return null. + * + * @return int|null + */ + public function getDateType() { + + if ($this->dateType) + return $this->dateType; + + if (!$this->value) { + $this->dateTimes = null; + $this->dateType = null; + return null; + } + + $dts = array(); + foreach(explode(',',$this->value) as $val) { + list( + $type, + $dt + ) = DateTime::parseData($val, $this); + $dts[] = $dt; + $this->dateType = $type; + } + $this->dateTimes = $dts; + return $this->dateType; + + } + + /** + * This method will return true, if the property had a date and a time, as + * opposed to only a date. + * + * @return bool + */ + public function hasTime() { + + return $this->getDateType()!==DateTime::DATE; + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Reader.php b/libcalendaring/lib/OldSabre/VObject/Reader.php new file mode 100644 index 0000000..5bea834 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Reader.php @@ -0,0 +1,223 @@ +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[A-Z0-9-]+)(?:;(?P^(?([^:^\"]|\"([^\"]*)\")*))?"; + $regex = "/^(?P$token)$parameters:(?P.*)$/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[^\"^;]*|"[^"]*")'; + + $regex = "/(?<=^|;)(?P$token)(=$paramValue(?=$|;))?/i"; + preg_match_all($regex, $parameters, $matches, PREG_SET_ORDER); + + $params = array(); + foreach($matches as $match) { + + if (!isset($match['paramValue'])) { + + $value = null; + + } else { + + $value = $match['paramValue']; + + if (isset($value[0]) && $value[0]==='"') { + // Stripping quotes, if needed + $value = substr($value,1,strlen($value)-2); + } + + $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) { + if ($matches[2]==='n' || $matches[2]==='N') { + return "\n"; + } else { + return $matches[2]; + } + }, $value); + + } + + $params[] = new Parameter($match['paramName'], $value); + + } + + return $params; + + } + + +} diff --git a/libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php b/libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php new file mode 100644 index 0000000..dcef36c --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php @@ -0,0 +1,1144 @@ + 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + ); + + /** + * Mappings between the day number and english day name. + * + * @var array + */ + private $dayNames = array( + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ); + + /** + * If the current iteration of the event is an overriden event, this + * property will hold the VObject + * + * @var Component + */ + private $currentOverriddenEvent; + + /** + * This property may contain the date of the next not-overridden event. + * This date is calculated sometimes a bit early, before overridden events + * are evaluated. + * + * @var DateTime + */ + private $nextDate; + + /** + * This counts the number of overridden events we've handled so far + * + * @var int + */ + private $handledOverridden = 0; + + /** + * Creates the iterator + * + * You should pass a VCALENDAR component, as well as the UID of the event + * we're going to traverse. + * + * @param Component $vcal + * @param string|null $uid + */ + public function __construct(Component $vcal, $uid=null) { + + if (is_null($uid)) { + if ($vcal->name === 'VCALENDAR') { + throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well'); + } + $components = array($vcal); + $uid = (string)$vcal->uid; + } else { + $components = $vcal->select('VEVENT'); + } + foreach($components as $component) { + if ((string)$component->uid == $uid) { + if (isset($component->{'RECURRENCE-ID'})) { + $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component; + $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime(); + } else { + $this->baseEvent = $component; + } + } + } + + ksort($this->overriddenEvents); + + if (!$this->baseEvent) { + throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid); + } + + $this->startDate = clone $this->baseEvent->DTSTART->getDateTime(); + + $this->endDate = null; + if (isset($this->baseEvent->DTEND)) { + $this->endDate = clone $this->baseEvent->DTEND->getDateTime(); + } else { + $this->endDate = clone $this->startDate; + if (isset($this->baseEvent->DURATION)) { + $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value)); + } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) { + $this->endDate->modify('+1 day'); + } + } + $this->currentDate = clone $this->startDate; + + $rrule = (string)$this->baseEvent->RRULE; + + $parts = explode(';', $rrule); + + // If no rrule was specified, we create a default setting + if (!$rrule) { + $this->frequency = 'daily'; + $this->count = 1; + } else foreach($parts as $part) { + + list($key, $value) = explode('=', $part, 2); + + switch(strtoupper($key)) { + + case 'FREQ' : + if (!in_array( + strtolower($value), + array('secondly','minutely','hourly','daily','weekly','monthly','yearly') + )) { + throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); + + } + $this->frequency = strtolower($value); + break; + + case 'UNTIL' : + $this->until = DateTimeParser::parse($value); + + // In some cases events are generated with an UNTIL= + // parameter before the actual start of the event. + // + // Not sure why this is happening. We assume that the + // intention was that the event only recurs once. + // + // So we are modifying the parameter so our code doesn't + // break. + if($this->until < $this->baseEvent->DTSTART->getDateTime()) { + $this->until = $this->baseEvent->DTSTART->getDateTime(); + } + break; + + case 'COUNT' : + $this->count = (int)$value; + break; + + case 'INTERVAL' : + $this->interval = (int)$value; + if ($this->interval < 1) { + throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!'); + } + break; + + case 'BYSECOND' : + $this->bySecond = explode(',', $value); + break; + + case 'BYMINUTE' : + $this->byMinute = explode(',', $value); + break; + + case 'BYHOUR' : + $this->byHour = explode(',', $value); + break; + + case 'BYDAY' : + $this->byDay = explode(',', strtoupper($value)); + break; + + case 'BYMONTHDAY' : + $this->byMonthDay = explode(',', $value); + break; + + case 'BYYEARDAY' : + $this->byYearDay = explode(',', $value); + break; + + case 'BYWEEKNO' : + $this->byWeekNo = explode(',', $value); + break; + + case 'BYMONTH' : + $this->byMonth = explode(',', $value); + break; + + case 'BYSETPOS' : + $this->bySetPos = explode(',', $value); + break; + + case 'WKST' : + $this->weekStart = strtoupper($value); + break; + + } + + } + + // Parsing exception dates + if (isset($this->baseEvent->EXDATE)) { + foreach($this->baseEvent->EXDATE as $exDate) { + + foreach(explode(',', (string)$exDate) as $exceptionDate) { + + $this->exceptionDates[] = + DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone()); + + } + + } + + } + + } + + /** + * Returns the current item in the list + * + * @return DateTime + */ + public function current() { + + if (!$this->valid()) return null; + return clone $this->currentDate; + + } + + /** + * This method returns the startdate for the current iteration of the + * event. + * + * @return DateTime + */ + public function getDtStart() { + + if (!$this->valid()) return null; + return clone $this->currentDate; + + } + + /** + * This method returns the enddate for the current iteration of the + * event. + * + * @return DateTime + */ + public function getDtEnd() { + + if (!$this->valid()) return null; + $dtEnd = clone $this->currentDate; + $dtEnd->add( $this->startDate->diff( $this->endDate ) ); + return clone $dtEnd; + + } + + /** + * Returns a VEVENT object with the updated start and end date. + * + * Any recurrence information is removed, and this function may return an + * 'overridden' event instead. + * + * This method always returns a cloned instance. + * + * @return Component\VEvent + */ + public function getEventObject() { + + if ($this->currentOverriddenEvent) { + return clone $this->currentOverriddenEvent; + } + $event = clone $this->baseEvent; + unset($event->RRULE); + unset($event->EXDATE); + unset($event->RDATE); + unset($event->EXRULE); + + $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType()); + if (isset($event->DTEND)) { + $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType()); + } + if ($this->counter > 0) { + $event->{'RECURRENCE-ID'} = (string)$event->DTSTART; + } + + return $event; + + } + + /** + * Returns the current item number + * + * @return int + */ + public function key() { + + return $this->counter; + + } + + /** + * Whether or not there is a 'next item' + * + * @return bool + */ + public function valid() { + + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + if (!is_null($this->until) && $this->currentDate > $this->until) { + + // Need to make sure there's no overridden events past the + // until date. + foreach($this->overriddenEvents as $overriddenEvent) { + + if ($overriddenEvent->DTSTART->getDateTime() >= $this->currentDate) { + + return true; + } + } + return false; + } + return true; + + } + + /** + * Resets the iterator + * + * @return void + */ + public function rewind() { + + $this->currentDate = clone $this->startDate; + $this->counter = 0; + + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + * + * Note that this checks the current 'endDate', not the 'stardDate'. This + * means that if you forward to January 1st, the iterator will stop at the + * first event that ends *after* January 1st. + * + * @param DateTime $dt + * @return void + */ + public function fastForward(\DateTime $dt) { + + while($this->valid() && $this->getDTEnd() <= $dt) { + $this->next(); + } + + } + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() { + + return !$this->count && !$this->until; + + } + + /** + * Goes on to the next iteration + * + * @return void + */ + public function next() { + + $previousStamp = $this->currentDate->getTimeStamp(); + + // Finding the next overridden event in line, and storing that for + // later use. + $overriddenEvent = null; + $overriddenDate = null; + $this->currentOverriddenEvent = null; + + foreach($this->overriddenEvents as $index=>$event) { + if ($index > $previousStamp) { + $overriddenEvent = $event; + $overriddenDate = clone $event->DTSTART->getDateTime(); + break; + } + } + + // If we have a stored 'next date', we will use that. + if ($this->nextDate) { + if (!$overriddenDate || $this->nextDate < $overriddenDate) { + $this->currentDate = $this->nextDate; + $currentStamp = $this->currentDate->getTimeStamp(); + $this->nextDate = null; + } else { + $this->currentDate = clone $overriddenDate; + $this->currentOverriddenEvent = $overriddenEvent; + } + $this->counter++; + return; + } + + while(true) { + + // Otherwise, we find the next event in the normal RRULE + // sequence. + switch($this->frequency) { + + case 'hourly' : + $this->nextHourly(); + break; + + case 'daily' : + $this->nextDaily(); + break; + + case 'weekly' : + $this->nextWeekly(); + break; + + case 'monthly' : + $this->nextMonthly(); + break; + + case 'yearly' : + $this->nextYearly(); + break; + + } + $currentStamp = $this->currentDate->getTimeStamp(); + + + // Checking exception dates + foreach($this->exceptionDates as $exceptionDate) { + if ($this->currentDate == $exceptionDate) { + $this->counter++; + continue 2; + } + } + foreach($this->overriddenDates as $check) { + if ($this->currentDate == $check) { + continue 2; + } + } + break; + + } + + + + // Is the date we have actually higher than the next overiddenEvent? + if ($overriddenDate && $this->currentDate > $overriddenDate) { + $this->nextDate = clone $this->currentDate; + $this->currentDate = clone $overriddenDate; + $this->currentOverriddenEvent = $overriddenEvent; + $this->handledOverridden++; + } + $this->counter++; + + + /* + * If we have overridden events left in the queue, but our counter is + * running out, we should grab one of those. + */ + if (!is_null($overriddenEvent) && !is_null($this->count) && count($this->overriddenEvents) - $this->handledOverridden >= ($this->count - $this->counter)) { + + $this->currentOverriddenEvent = $overriddenEvent; + $this->currentDate = clone $overriddenDate; + $this->handledOverridden++; + + } + + } + + /** + * Does the processing for advancing the iterator for hourly frequency. + * + * @return void + */ + protected function nextHourly() { + + if (!$this->byHour) { + $this->currentDate->modify('+' . $this->interval . ' hours'); + return; + } + } + + /** + * Does the processing for advancing the iterator for daily frequency. + * + * @return void + */ + protected function nextDaily() { + + if (!$this->byHour && !$this->byDay) { + $this->currentDate->modify('+' . $this->interval . ' days'); + return; + } + + if (isset($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + if (isset($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + do { + + if ($this->byHour) { + if ($this->currentDate->format('G') == '23') { + // to obey the interval rule + $this->currentDate->modify('+' . $this->interval-1 . ' days'); + } + + $this->currentDate->modify('+1 hours'); + + } else { + $this->currentDate->modify('+' . $this->interval . ' days'); + + } + + // Current day of the week + $currentDay = $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = $this->currentDate->format('G'); + + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + + } + + /** + * Does the processing for advancing the iterator for weekly frequency. + * + * @return void + */ + protected function nextWeekly() { + + if (!$this->byHour && !$this->byDay) { + $this->currentDate->modify('+' . $this->interval . ' weeks'); + return; + } + + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + if ($this->byDay) { + $recurrenceDays = $this->getDays(); + } + + // First day of the week: + $firstDay = $this->dayMap[$this->weekStart]; + + do { + + if ($this->byHour) { + $this->currentDate->modify('+1 hours'); + } else { + $this->currentDate->modify('+1 days'); + } + + // Current day of the week + $currentDay = (int) $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = (int) $this->currentDate->format('G'); + + // We need to roll over to the next week + if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) { + $this->currentDate->modify('+' . $this->interval-1 . ' weeks'); + + // We need to go to the first day of this week, but only if we + // are not already on this first day of this week. + if($this->currentDate->format('w') != $firstDay) { + $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]); + } + } + + // We have a match + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + } + + /** + * Does the processing for advancing the iterator for monthly frequency. + * + * @return void + */ + protected function nextMonthly() { + + $currentDayOfMonth = $this->currentDate->format('j'); + if (!$this->byMonthDay && !$this->byDay) { + + // If the current day is higher than the 28th, rollover can + // occur to the next month. We Must skip these invalid + // entries. + if ($currentDayOfMonth < 29) { + $this->currentDate->modify('+' . $this->interval . ' months'); + } else { + $increase = 0; + do { + $increase++; + $tempDate = clone $this->currentDate; + $tempDate->modify('+ ' . ($this->interval*$increase) . ' months'); + } while ($tempDate->format('j') != $currentDayOfMonth); + $this->currentDate = $tempDate; + } + return; + } + + while(true) { + + $occurrences = $this->getMonthlyOccurrences(); + + foreach($occurrences as $occurrence) { + + // The first occurrence thats higher than the current + // day of the month wins. + if ($occurrence > $currentDayOfMonth) { + break 2; + } + + } + + // If we made it all the way here, it means there were no + // valid occurrences, and we need to advance to the next + // month. + $this->currentDate->modify('first day of this month'); + $this->currentDate->modify('+ ' . $this->interval . ' months'); + + // This goes to 0 because we need to start counting at hte + // beginning. + $currentDayOfMonth = 0; + + } + + $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence); + + } + + /** + * Does the processing for advancing the iterator for yearly frequency. + * + * @return void + */ + protected function nextYearly() { + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + // No sub-rules, so we just advance by year + if (!$this->byMonth) { + + // Unless it was a leap day! + if ($currentMonth==2 && $currentDayOfMonth==29) { + + $counter = 0; + do { + $counter++; + // Here we increase the year count by the interval, until + // we hit a date that's also in a leap year. + // + // We could just find the next interval that's dividable by + // 4, but that would ignore the rule that there's no leap + // year every year that's dividable by a 100, but not by + // 400. (1800, 1900, 2100). So we just rely on the datetime + // functions instead. + $nextDate = clone $this->currentDate; + $nextDate->modify('+ ' . ($this->interval*$counter) . ' years'); + } while ($nextDate->format('n')!=2); + $this->currentDate = $nextDate; + + return; + + } + + // The easiest form + $this->currentDate->modify('+' . $this->interval . ' years'); + return; + + } + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + $advancedToNewMonth = false; + + // If we got a byDay or getMonthDay filter, we must first expand + // further. + if ($this->byDay || $this->byMonthDay) { + + while(true) { + + $occurrences = $this->getMonthlyOccurrences(); + + foreach($occurrences as $occurrence) { + + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { + break 2; + } + + } + + // If we made it here, it means we need to advance to + // the next month or year. + $currentDayOfMonth = 1; + $advancedToNewMonth = true; + do { + + $currentMonth++; + if ($currentMonth>12) { + $currentYear+=$this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + + $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); + + } + + // If we made it here, it means we got a valid occurrence + $this->currentDate->setDate($currentYear, $currentMonth, $occurrence); + return; + + } else { + + // These are the 'byMonth' rules, if there are no byDay or + // byMonthDay sub-rules. + do { + + $currentMonth++; + if ($currentMonth>12) { + $currentYear+=$this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); + + return; + + } + + } + + /** + * Returns all the occurrences for a monthly frequency with a 'byDay' or + * 'byMonthDay' expansion for the current month. + * + * The returned list is an array of integers with the day of month (1-31). + * + * @return array + */ + protected function getMonthlyOccurrences() { + + $startDate = clone $this->currentDate; + + $byDayResults = array(); + + // Our strategy is to simply go through the byDays, advance the date to + // that point and add it to the results. + if ($this->byDay) foreach($this->byDay as $day) { + + $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]]; + + // Dayname will be something like 'wednesday'. Now we need to find + // all wednesdays in this month. + $dayHits = array(); + + $checkDate = clone $startDate; + $checkDate->modify('first day of this month'); + $checkDate->modify($dayName); + + do { + $dayHits[] = $checkDate->format('j'); + $checkDate->modify('next ' . $dayName); + } while ($checkDate->format('n') === $startDate->format('n')); + + // So now we have 'all wednesdays' for month. It is however + // possible that the user only really wanted the 1st, 2nd or last + // wednesday. + if (strlen($day)>2) { + $offset = (int)substr($day,0,-2); + + if ($offset>0) { + // It is possible that the day does not exist, such as a + // 5th or 6th wednesday of the month. + if (isset($dayHits[$offset-1])) { + $byDayResults[] = $dayHits[$offset-1]; + } + } else { + + // if it was negative we count from the end of the array + $byDayResults[] = $dayHits[count($dayHits) + $offset]; + } + } else { + // There was no counter (first, second, last wednesdays), so we + // just need to add the all to the list). + $byDayResults = array_merge($byDayResults, $dayHits); + + } + + } + + $byMonthDayResults = array(); + if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) { + + // Removing values that are out of range for this month + if ($monthDay > $startDate->format('t') || + $monthDay < 0-$startDate->format('t')) { + continue; + } + if ($monthDay>0) { + $byMonthDayResults[] = $monthDay; + } else { + // Negative values + $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; + } + } + + // If there was just byDay or just byMonthDay, they just specify our + // (almost) final list. If both were provided, then byDay limits the + // list. + if ($this->byMonthDay && $this->byDay) { + $result = array_intersect($byMonthDayResults, $byDayResults); + } elseif ($this->byMonthDay) { + $result = $byMonthDayResults; + } else { + $result = $byDayResults; + } + $result = array_unique($result); + sort($result, SORT_NUMERIC); + + // The last thing that needs checking is the BYSETPOS. If it's set, it + // means only certain items in the set survive the filter. + if (!$this->bySetPos) { + return $result; + } + + $filteredResult = array(); + foreach($this->bySetPos as $setPos) { + + if ($setPos<0) { + $setPos = count($result)-($setPos+1); + } + if (isset($result[$setPos-1])) { + $filteredResult[] = $result[$setPos-1]; + } + } + + sort($filteredResult, SORT_NUMERIC); + return $filteredResult; + + } + + protected function getHours() + { + $recurrenceHours = array(); + foreach($this->byHour as $byHour) { + $recurrenceHours[] = $byHour; + } + + return $recurrenceHours; + } + + protected function getDays() + { + $recurrenceDays = array(); + foreach($this->byDay as $byDay) { + + // The day may be preceeded with a positive (+n) or + // negative (-n) integer. However, this does not make + // sense in 'weekly' so we ignore it here. + $recurrenceDays[] = $this->dayMap[substr($byDay,-2)]; + + } + + return $recurrenceDays; + } +} + diff --git a/libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php b/libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php new file mode 100644 index 0000000..5fc1e68 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php @@ -0,0 +1,111 @@ +children as $component) { + if (!$component instanceof VObject\Component) { + continue; + } + + // Get all timezones + if ($component->name === 'VTIMEZONE') { + $this->vtimezones[(string)$component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if($component->UID) { + $uid = (string)$component->UID; + } else { + // Generating a random UID + $uid = sha1(microtime()) . '-vobjectimport'; + } + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = VObject\Component::create('VCALENDAR'); + } + + $this->objects[$uid]->add(clone $component); + } + + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return OldSabre\VObject\Component|null + */ + public function getNext() { + + if($object=array_shift($this->objects)) { + + // create our baseobject + $object->version = '2.0'; + $object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN'; + $object->calscale = 'GREGORIAN'; + + // add vtimezone information to obj (if we have it) + foreach ($this->vtimezones as $vtimezone) { + $object->add($vtimezone); + } + + return $object; + + } else { + + return null; + + } + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php b/libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php new file mode 100644 index 0000000..ed73c39 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php @@ -0,0 +1,39 @@ +input = $input; + + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return OldSabre\VObject\Component|null + */ + public function getNext() { + + $vcard = ''; + + do { + + if (feof($this->input)) { + return false; + } + + $line = fgets($this->input); + $vcard .= $line; + + } while(strtoupper(substr($line,0,4))!=="END:"); + + $object = VObject\Reader::read($vcard); + + if($object->name !== 'VCARD') { + throw new \InvalidArgumentException("Thats no vCard!", 1); + } + + return $object; + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/StringUtil.php b/libcalendaring/lib/OldSabre/VObject/StringUtil.php new file mode 100644 index 0000000..e6e2aa3 --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/StringUtil.php @@ -0,0 +1,61 @@ +'Australia/Darwin', + 'AUS Eastern Standard Time'=>'Australia/Sydney', + 'Afghanistan Standard Time'=>'Asia/Kabul', + 'Alaskan Standard Time'=>'America/Anchorage', + 'Arab Standard Time'=>'Asia/Riyadh', + 'Arabian Standard Time'=>'Asia/Dubai', + 'Arabic Standard Time'=>'Asia/Baghdad', + 'Argentina Standard Time'=>'America/Buenos_Aires', + 'Armenian Standard Time'=>'Asia/Yerevan', + 'Atlantic Standard Time'=>'America/Halifax', + 'Azerbaijan Standard Time'=>'Asia/Baku', + 'Azores Standard Time'=>'Atlantic/Azores', + 'Bangladesh Standard Time'=>'Asia/Dhaka', + 'Canada Central Standard Time'=>'America/Regina', + 'Cape Verde Standard Time'=>'Atlantic/Cape_Verde', + 'Caucasus Standard Time'=>'Asia/Yerevan', + 'Cen. Australia Standard Time'=>'Australia/Adelaide', + 'Central America Standard Time'=>'America/Guatemala', + 'Central Asia Standard Time'=>'Asia/Almaty', + 'Central Brazilian Standard Time'=>'America/Cuiaba', + 'Central Europe Standard Time'=>'Europe/Budapest', + 'Central European Standard Time'=>'Europe/Warsaw', + 'Central Pacific Standard Time'=>'Pacific/Guadalcanal', + 'Central Standard Time'=>'America/Chicago', + 'Central Standard Time (Mexico)'=>'America/Mexico_City', + 'China Standard Time'=>'Asia/Shanghai', + 'Dateline Standard Time'=>'Etc/GMT+12', + 'E. Africa Standard Time'=>'Africa/Nairobi', + 'E. Australia Standard Time'=>'Australia/Brisbane', + 'E. Europe Standard Time'=>'Europe/Minsk', + 'E. South America Standard Time'=>'America/Sao_Paulo', + 'Eastern Standard Time'=>'America/New_York', + 'Egypt Standard Time'=>'Africa/Cairo', + 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg', + 'FLE Standard Time'=>'Europe/Kiev', + 'Fiji Standard Time'=>'Pacific/Fiji', + 'GMT Standard Time'=>'Europe/London', + 'GTB Standard Time'=>'Europe/Istanbul', + 'Georgian Standard Time'=>'Asia/Tbilisi', + 'Greenland Standard Time'=>'America/Godthab', + 'Greenwich Standard Time'=>'Atlantic/Reykjavik', + 'Hawaiian Standard Time'=>'Pacific/Honolulu', + 'India Standard Time'=>'Asia/Calcutta', + 'Iran Standard Time'=>'Asia/Tehran', + 'Israel Standard Time'=>'Asia/Jerusalem', + 'Jordan Standard Time'=>'Asia/Amman', + 'Kamchatka Standard Time'=>'Asia/Kamchatka', + 'Korea Standard Time'=>'Asia/Seoul', + 'Magadan Standard Time'=>'Asia/Magadan', + 'Mauritius Standard Time'=>'Indian/Mauritius', + 'Mexico Standard Time'=>'America/Mexico_City', + 'Mexico Standard Time 2'=>'America/Chihuahua', + 'Mid-Atlantic Standard Time'=>'Etc/GMT-2', + 'Middle East Standard Time'=>'Asia/Beirut', + 'Montevideo Standard Time'=>'America/Montevideo', + 'Morocco Standard Time'=>'Africa/Casablanca', + 'Mountain Standard Time'=>'America/Denver', + 'Mountain Standard Time (Mexico)'=>'America/Chihuahua', + 'Myanmar Standard Time'=>'Asia/Rangoon', + 'N. Central Asia Standard Time'=>'Asia/Novosibirsk', + 'Namibia Standard Time'=>'Africa/Windhoek', + 'Nepal Standard Time'=>'Asia/Katmandu', + 'New Zealand Standard Time'=>'Pacific/Auckland', + 'Newfoundland Standard Time'=>'America/St_Johns', + 'North Asia East Standard Time'=>'Asia/Irkutsk', + 'North Asia Standard Time'=>'Asia/Krasnoyarsk', + 'Pacific SA Standard Time'=>'America/Santiago', + 'Pacific Standard Time'=>'America/Los_Angeles', + 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel', + 'Pakistan Standard Time'=>'Asia/Karachi', + 'Paraguay Standard Time'=>'America/Asuncion', + 'Romance Standard Time'=>'Europe/Paris', + 'Russian Standard Time'=>'Europe/Moscow', + 'SA Eastern Standard Time'=>'America/Cayenne', + 'SA Pacific Standard Time'=>'America/Bogota', + 'SA Western Standard Time'=>'America/La_Paz', + 'SE Asia Standard Time'=>'Asia/Bangkok', + 'Samoa Standard Time'=>'Pacific/Apia', + 'Singapore Standard Time'=>'Asia/Singapore', + 'South Africa Standard Time'=>'Africa/Johannesburg', + 'Sri Lanka Standard Time'=>'Asia/Colombo', + 'Syria Standard Time'=>'Asia/Damascus', + 'Taipei Standard Time'=>'Asia/Taipei', + 'Tasmania Standard Time'=>'Australia/Hobart', + 'Tokyo Standard Time'=>'Asia/Tokyo', + 'Tonga Standard Time'=>'Pacific/Tongatapu', + 'US Eastern Standard Time'=>'America/Indianapolis', + 'US Mountain Standard Time'=>'America/Phoenix', + 'UTC+12'=>'Etc/GMT-12', + 'UTC-02'=>'Etc/GMT+2', + 'UTC-11'=>'Etc/GMT+11', + 'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar', + 'Venezuela Standard Time'=>'America/Caracas', + 'Vladivostok Standard Time'=>'Asia/Vladivostok', + 'W. Australia Standard Time'=>'Australia/Perth', + 'W. Central Africa Standard Time'=>'Africa/Lagos', + 'W. Europe Standard Time'=>'Europe/Berlin', + 'West Asia Standard Time'=>'Asia/Tashkent', + 'West Pacific Standard Time'=>'Pacific/Port_Moresby', + 'Yakutsk Standard Time'=>'Asia/Yakutsk', + + // Microsoft exchange timezones + // Source: + // http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx + // + // Correct timezones deduced with help from: + // http://en.wikipedia.org/wiki/List_of_tz_database_time_zones + 'Universal Coordinated Time' => 'UTC', + 'Casablanca, Monrovia' => 'Africa/Casablanca', + 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', + 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', + 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', + 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', + 'Prague, Central Europe' => 'Europe/Prague', + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', + 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Athens, Istanbul, Minsk' => 'Europe/Athens', + 'Bucharest' => 'Europe/Bucharest', + 'Cairo' => 'Africa/Cairo', + 'Harare, Pretoria' => 'Africa/Harare', + 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', + 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', + 'Baghdad' => 'Asia/Baghdad', + 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'East Africa, Nairobi' => 'Africa/Nairobi', + 'Tehran' => 'Asia/Tehran', + 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', + 'Kabul' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kathmandu, Nepal' => 'Asia/Kathmandu', + 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', + 'Astana, Dhaka' => 'Asia/Dhaka', + 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', + 'Rangoon' => 'Asia/Rangoon', + 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', + 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', + 'Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'Perth, Western Australia' => 'Australia/Perth', + 'Taipei' => 'Asia/Taipei', + 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Seoul, Korea Standard time' => 'Asia/Seoul', + 'Yakutsk' => 'Asia/Yakutsk', + 'Adelaide, Central Australia' => 'Australia/Adelaide', + 'Darwin' => 'Australia/Darwin', + 'Brisbane, East Australia' => 'Australia/Brisbane', + 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', + 'Guam, Port Moresby' => 'Pacific/Guam', + 'Hobart, Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', + 'Auckland, Wellington' => 'Pacific/Auckland', + 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', + 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde Is.' => 'Atlantic/Cape_Verde', + 'Mid-Atlantic' => 'America/Noronha', + 'Brasilia' => 'America/Sao_Paulo', // Best guess + 'Buenos Aires' => 'America/Argentina/Buenos_Aires', + 'Greenland' => 'America/Godthab', + 'Newfoundland' => 'America/St_Johns', + 'Atlantic Time (Canada)' => 'America/Halifax', + 'Caracas, La Paz' => 'America/Caracas', + 'Santiago' => 'America/Santiago', + 'Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis', + 'Central America' => 'America/Guatemala', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Mexico City, Tegucigalpa' => 'America/Mexico_City', + 'Saskatchewan' => 'America/Edmonton', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', + + // The following list are timezone names that could be generated by + // Lotus / Domino + 'Dateline' => 'Etc/GMT-12', + 'Samoa' => 'Pacific/Apia', + 'Hawaiian' => 'Pacific/Honolulu', + 'Alaskan' => 'America/Anchorage', + 'Pacific' => 'America/Los_Angeles', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Mexico Standard Time 2' => 'America/Chihuahua', + 'Mountain' => 'America/Denver', + 'Mountain Standard Time' => 'America/Chihuahua', + 'US Mountain' => 'America/Phoenix', + 'Canada Central' => 'America/Edmonton', + 'Central America' => 'America/Guatemala', + 'Central' => 'America/Chicago', + 'Central Standard Time' => 'America/Mexico_City', + 'Mexico' => 'America/Mexico_City', + 'Eastern' => 'America/New_York', + 'SA Pacific' => 'America/Bogota', + 'US Eastern' => 'America/Indiana/Indianapolis', + 'Venezuela' => 'America/Caracas', + 'Atlantic' => 'America/Halifax', + 'Central Brazilian' => 'America/Manaus', + 'Pacific SA' => 'America/Santiago', + 'SA Western' => 'America/La_Paz', + 'Newfoundland' => 'America/St_Johns', + 'Argentina' => 'America/Argentina/Buenos_Aires', + 'E. South America' => 'America/Belem', + 'Greenland' => 'America/Godthab', + 'Montevideo' => 'America/Montevideo', + 'SA Eastern' => 'America/Belem', + 'Mid-Atlantic' => 'Etc/GMT-2', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde' => 'Atlantic/Cape_Verde', + 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT. + 'Morocco' => 'Africa/Casablanca', + 'Central Europe' => 'Europe/Prague', + 'Central European' => 'Europe/Sarajevo', + 'Romance' => 'Europe/Paris', + 'W. Central Africa' => 'Africa/Lagos', // Best guess + 'W. Europe' => 'Europe/Amsterdam', + 'E. Europe' => 'Europe/Minsk', + 'Egypt' => 'Africa/Cairo', + 'FLE' => 'Europe/Helsinki', + 'GTB' => 'Europe/Athens', + 'Israel' => 'Asia/Jerusalem', + 'Jordan' => 'Asia/Amman', + 'Middle East' => 'Asia/Beirut', + 'Namibia' => 'Africa/Windhoek', + 'South Africa' => 'Africa/Harare', + 'Arab' => 'Asia/Kuwait', + 'Arabic' => 'Asia/Baghdad', + 'E. Africa' => 'Africa/Nairobi', + 'Georgian' => 'Asia/Tbilisi', + 'Russian' => 'Europe/Moscow', + 'Iran' => 'Asia/Tehran', + 'Arabian' => 'Asia/Muscat', + 'Armenian' => 'Asia/Yerevan', + 'Azerbijan' => 'Asia/Baku', + 'Caucasus' => 'Asia/Yerevan', + 'Mauritius' => 'Indian/Mauritius', + 'Afghanistan' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Pakistan' => 'Asia/Karachi', + 'West Asia' => 'Asia/Tashkent', + 'India' => 'Asia/Calcutta', + 'Sri Lanka' => 'Asia/Colombo', + 'Nepal' => 'Asia/Kathmandu', + 'Central Asia' => 'Asia/Dhaka', + 'N. Central Asia' => 'Asia/Almaty', + 'Myanmar' => 'Asia/Rangoon', + 'North Asia' => 'Asia/Krasnoyarsk', + 'SE Asia' => 'Asia/Bangkok', + 'China' => 'Asia/Shanghai', + 'North Asia East' => 'Asia/Irkutsk', + 'Singapore' => 'Asia/Singapore', + 'Taipei' => 'Asia/Taipei', + 'W. Australia' => 'Australia/Perth', + 'Korea' => 'Asia/Seoul', + 'Tokyo' => 'Asia/Tokyo', + 'Yakutsk' => 'Asia/Yakutsk', + 'AUS Central' => 'Australia/Darwin', + 'Cen. Australia' => 'Australia/Adelaide', + 'AUS Eastern' => 'Australia/Sydney', + 'E. Australia' => 'Australia/Brisbane', + 'Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'West Pacific' => 'Pacific/Guam', + 'Central Pacific' => 'Asia/Magadan', + 'Fiji' => 'Pacific/Fiji', + 'New Zealand' => 'Pacific/Auckland', + 'Tonga' => 'Pacific/Tongatapu', + + // PHP 5.5.10 failed on a few timezones that were valid before. We're + // normalizing them here. + 'CST6CDT' => 'America/Chicago', + 'Cuba' => 'America/Havana', + 'Egypt' => 'Africa/Cairo', + 'Eire' => 'Europe/Dublin', + 'EST5EDT' => 'America/New_York', + 'Factory' => 'UTC', + 'GB-Eire' => 'Europe/London', + 'GMT0' => 'UTC', + 'Greenwich' => 'UTC', + 'Hongkong' => 'Asia/Hong_Kong', + 'Iceland' => 'Atlantic/Reykjavik', + 'Iran' => 'Asia/Tehran', + 'Israel' => 'Asia/Jerusalem', + 'Jamaica' => 'America/Jamaica', + 'Japan' => 'Asia/Tokyo', + 'Kwajalein' => 'Pacific/Kwajalein', + 'Libya' => 'Africa/Tripoli', + 'MST7MDT' => 'America/Denver', + 'Navajo' => 'America/Denver', + 'NZ-CHAT' => 'Pacific/Chatham', + 'Poland' => 'Europe/Warsaw', + 'Portugal' => 'Europe/Lisbon', + 'PST8PDT' => 'America/Los_Angeles', + 'Singapore' => 'Asia/Singapore', + 'Turkey' => 'Europe/Istanbul', + 'Universal' => 'UTC', + 'W-SU' => 'Europe/Moscow', + ); + + /** + * List of microsoft exchange timezone ids. + * + * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + */ + public static $microsoftExchangeMap = array( + 0 => 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ); + + /** + * This method will try to find out the correct timezone for an iCalendar + * date-time value. + * + * You must pass the contents of the TZID parameter, as well as the full + * calendar. + * + * If the lookup fails, this method will return the default PHP timezone + * (as configured using date_default_timezone_set, or the date.timezone ini + * setting). + * + * Alternatively, if $failIfUncertain is set to true, it will throw an + * exception if we cannot accurately determine the timezone. + * + * @param string $tzid + * @param OldSabre\VObject\Component $vcalendar + * @return DateTimeZone + */ + static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) { + + // First we will just see if the tzid is a support timezone identifier. + // + // The only exception is if the timezone starts with (. This is to + // handle cases where certain microsoft products generate timezone + // identifiers that for instance look like: + // + // (GMT+01.00) Sarajevo/Warsaw/Zagreb + // + // Since PHP 5.5.10, the first bit will be used as the timezone and + // this method will return just GMT+01:00. This is wrong, because it + // doesn't take DST into account. + if ($tzid[0]!=='(') { + try { + return new \DateTimeZone($tzid); + } catch (\Exception $e) { + } + } + + // Next, we check if the tzid is somewhere in our tzid map. + if (isset(self::$map[$tzid])) { + return new \DateTimeZone(self::$map[$tzid]); + } + + // Maybe the author was hyper-lazy and just included an offset. We + // support it, but we aren't happy about it. + // + // Note that the path in the source will never be taken from PHP 5.5.10 + // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it + // already gets returned early in this function. Once we drop support + // for versions under PHP 5.5.10, this bit can be taken out of the + // source. + if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { + return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0')); + } + + if ($vcalendar) { + + // If that didn't work, we will scan VTIMEZONE objects + foreach($vcalendar->select('VTIMEZONE') as $vtimezone) { + + if ((string)$vtimezone->TZID === $tzid) { + + // Some clients add 'X-LIC-LOCATION' with the olson name. + if (isset($vtimezone->{'X-LIC-LOCATION'})) { + + $lic = (string)$vtimezone->{'X-LIC-LOCATION'}; + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if (substr($lic,0,8)==='SystemV/') { + $lic = substr($lic,8); + } + + return self::getTimeZone($lic, null, $failIfUncertain); + + } + // Microsoft may add a magic number, which we also have an + // answer for. + if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value; + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) { + return new \DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + } + + } + + } + + } + + if ($failIfUncertain) { + throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid); + } + + // If we got all the way here, we default to UTC. + return new \DateTimeZone(date_default_timezone_get()); + + } + +} diff --git a/libcalendaring/lib/OldSabre/VObject/Version.php b/libcalendaring/lib/OldSabre/VObject/Version.php new file mode 100644 index 0000000..7c3becb --- /dev/null +++ b/libcalendaring/lib/OldSabre/VObject/Version.php @@ -0,0 +1,24 @@ + + * + * Copyright (C) 2011-2014, Kolab Systems AG + * + * 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 . + */ +class libcalendaring_itip +{ + protected $rc; + protected $lib; + protected $plugin; + protected $sender; + protected $domain; + protected $itip_send = false; + protected $rsvp_actions = array('accepted','tentative','declined','delegated'); + protected $rsvp_status = array('accepted','tentative','declined','delegated'); + + function __construct($plugin, $domain = 'libcalendaring') + { + $this->plugin = $plugin; + $this->rc = rcube::get_instance(); + $this->lib = libcalendaring::get_instance(); + $this->domain = $domain; + + $hook = $this->rc->plugins->exec_hook('calendar_load_itip', + array('identity' => $this->rc->user->list_emails(true))); + $this->sender = $hook['identity']; + + $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook')); + $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook')); + } + + public function set_sender_email($email) + { + if (!empty($email)) + $this->sender['email'] = $email; + } + + public function set_rsvp_actions($actions) + { + $this->rsvp_actions = (array)$actions; + $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated')); + } + + public function set_rsvp_status($status) + { + $this->rsvp_status = $status; + } + + /** + * Wrapper for rcube_plugin::gettext() + * Checking for a label in different domains + * + * @see rcube::gettext() + */ + public function gettext($p) + { + $label = is_array($p) ? $p['name'] : $p; + $domain = $this->domain; + if (!$this->rc->text_exists($label, $domain)) { + $domain = 'libcalendaring'; + } + return $this->rc->gettext($p, $domain); + } + + /** + * Send an iTip mail message + * + * @param array Event object to send + * @param string iTip method (REQUEST|REPLY|CANCEL) + * @param array Hash array with recipient data (name, email) + * @param string Mail subject + * @param string Mail body text label + * @param object Mail_mime object with message data + * @param boolean Request RSVP + * @return boolean True on success, false on failure + */ + public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true) + { + if (!$this->sender['name']) + $this->sender['name'] = $this->sender['email']; + + if (!$message) { + libcalendaring::identify_recurrence_instance($event); + $message = $this->compose_itip_message($event, $method, $rsvp); + } + + $mailto = rcube_idn_to_ascii($recipient['email']); + + $headers = $message->headers(); + $headers['To'] = format_email_recipient($mailto, $recipient['name']); + $headers['Subject'] = $this->gettext(array( + 'name' => $subject, + 'vars' => array( + 'title' => $event['title'], + 'name' => $this->sender['name'] + ) + )); + + // compose a list of all event attendees + $attendees_list = array(); + foreach ((array)$event['attendees'] as $attendee) { + $attendees_list[] = ($attendee['name'] && $attendee['email']) ? + $attendee['name'] . ' <' . $attendee['email'] . '>' : + ($attendee['name'] ? $attendee['name'] : $attendee['email']); + } + + $recurrence_info = ''; + if (!empty($event['recurrence_id'])) { + $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **'; + } + else if (!empty($event['recurrence'])) { + $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence'])); + } + + $mailbody = $this->gettext(array( + 'name' => $bodytext, + 'vars' => array( + 'title' => $event['title'], + 'date' => $this->lib->event_date_text($event, true) . $recurrence_info, + 'attendees' => join(",\n ", $attendees_list), + 'sender' => $this->sender['name'], + 'organizer' => $this->sender['name'], + ) + )); + + // if (!empty($event['comment'])) { + // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment']; + // } + + // append links for direct invitation replies + if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) { + $mailbody .= "\n\n" . $this->gettext(array( + 'name' => 'invitationattendlinks', + 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))), + )); + } + else if ($method == 'CANCEL' && $event['cancelled']) { + $this->cancel_itip_invitation($event); + } + + $message->headers($headers, true); + $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79)); + + if ($this->rc->config->get('libcalendaring_itip_debug', false)) { + console('iTip ' . $method, $message->txtHeaders() . "\n\r" . $message->get()); + } + + // finally send the message + $this->itip_send = true; + $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error); + $this->itip_send = false; + + return $sent; + } + + /** + * Plugin hook triggered by rcube::deliver_message() before delivering a message. + * Here we can set the 'smtp_server' config option to '' in order to use + * PHP's mail() function for unauthenticated email sending. + */ + public function before_send_hook($p) + { + if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') { + $this->rc->config->set('smtp_server', ''); + } + + return $p; + } + + /** + * Plugin hook to alter SMTP authentication. + * This is used if iTip messages are to be sent from an unauthenticated session + */ + public function smtp_connect_hook($p) + { + // replace smtp auth settings if we're not in an authenticated session + if ($this->itip_send && !$this->rc->user->ID) { + foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) { + $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]); + } + } + + return $p; + } + + /** + * Helper function to build a Mail_mime object to send an iTip message + * + * @param array Event object to send + * @param string iTip method (REQUEST|REPLY|CANCEL) + * @param boolean Request RSVP + * @return object Mail_mime object with message data + */ + public function compose_itip_message($event, $method, $rsvp = true) + { + $from = rcube_idn_to_ascii($this->sender['email']); + $from_utf = rcube_utils::idn_to_utf8($from); + $sender = format_email_recipient($from, $this->sender['name']); + + // truncate list attendees down to the recipient of the iTip Reply. + // constraints for a METHOD:REPLY according to RFC 5546 + if ($method == 'REPLY') { + $replying_attendee = null; + $reply_attendees = array(); + foreach ($event['attendees'] as $attendee) { + if ($attendee['role'] == 'ORGANIZER') { + $reply_attendees[] = $attendee; + } + else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) { + $replying_attendee = $attendee; + if ($attendee['status'] != 'DELEGATED') { + unset($replying_attendee['rsvp']); // unset the RSVP attribute + } + } + // include attendees relevant for delegation (RFC 5546, Section 4.2.5) + else if ((!empty($attendee['delegated-to']) && + (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) || + (!empty($attendee['delegated-from']) && + (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) { + $reply_attendees[] = $attendee; + } + } + if ($replying_attendee) { + array_unshift($reply_attendees, $replying_attendee); + $event['attendees'] = $reply_attendees; + } + if ($event['recurrence']) { + unset($event['recurrence']['EXCEPTIONS']); + } + } + // set RSVP for every attendee + else if ($method == 'REQUEST') { + foreach ($event['attendees'] as $i => $attendee) { + if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) { + $event['attendees'][$i]['rsvp']= (bool)$rsvp; + } + } + } + else if ($method == 'CANCEL') { + if ($event['recurrence']) { + unset($event['recurrence']['EXCEPTIONS']); + } + } + + // compose multipart message using PEAR:Mail_Mime + $message = new Mail_mime("\r\n"); + $message->setParam('text_encoding', 'quoted-printable'); + $message->setParam('head_encoding', 'quoted-printable'); + $message->setParam('head_charset', RCMAIL_CHARSET); + $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed"); + $message->setContentType('multipart/alternative'); + + // compose common headers array + $headers = array( + 'From' => $sender, + 'Date' => $this->rc->user_date(), + 'Message-ID' => $this->rc->gen_message_id(), + 'X-Sender' => $from, + ); + if ($agent = $this->rc->config->get('useragent')) { + $headers['User-Agent'] = $agent; + } + + $message->headers($headers); + + // attach ics file for this event + $ical = libcalendaring::get_ical(); + $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false); + $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics'; + $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method); + + return $message; + } + + /** + * Forward the given iTip event as delegation to another person + * + * @param array Event object to delegate + * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto' + * @param boolean The delegator's RSVP flag + * @param array List with indexes of new/updated attendees + * @return boolean True on success, False on failure + */ + public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array()) + { + if (is_string($delegate)) { + $delegates = rcube_mime::decode_address_list($delegate, 1, false); + if (count($delegates) > 0) { + $delegate = reset($delegates); + } + } + + $emails = $this->lib->get_user_emails(); + $me = $this->rc->user->list_emails(true); + + // find/create the delegate attendee + $delegate_attendee = array( + 'email' => $delegate['mailto'], + 'name' => $delegate['name'], + 'role' => 'REQ-PARTICIPANT', + ); + $delegate_index = count($event['attendees']); + + foreach ($event['attendees'] as $i => $attendee) { + // set myself the DELEGATED-TO parameter + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $event['attendees'][$i]['delegated-to'] = $delegate['mailto']; + $event['attendees'][$i]['status'] = 'DELEGATED'; + $event['attendees'][$i]['role'] = 'NON-PARTICIPANT'; + $event['attendees'][$i]['rsvp'] = $rsvp; + + $me['email'] = $attendee['email']; + $delegate_attendee['role'] = $attendee['role']; + } + // the disired delegatee is already listed as an attendee + else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') { + $delegate_attendee = $attendee; + $delegate_index = $i; + break; + } + // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me) + } + + // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter + $delegate_attendee['rsvp'] = true; + $delegate_attendee['status'] = 'NEEDS-ACTION'; + $delegate_attendee['delegated-from'] = $me['email']; + $event['attendees'][$delegate_index] = $delegate_attendee; + + $attendees[] = $delegate_index; + + $this->set_sender_email($me['email']); + return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto'); + } + + /** + * Handler for calendar/itip-status requests + */ + public function get_itip_status($event, $existing = null) + { + $action = $event['rsvp'] ? 'rsvp' : ''; + $status = $event['fallback']; + $latest = false; + $html = ''; + + if (is_numeric($event['changed'])) + $event['changed'] = new DateTime('@'.$event['changed']); + + // check if the given itip object matches the last state + if ($existing) { + $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) || + (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']); + } + + // determine action for REQUEST + if ($event['method'] == 'REQUEST') { + $html = html::div('rsvp-status', $this->gettext('acceptinvitation')); + + if ($existing) { + $rsvp = $event['rsvp']; + $emails = $this->lib->get_user_emails(); + foreach ($existing['attendees'] as $attendee) { + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $status = strtoupper($attendee['status']); + break; + } + } + } + else { + $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true); + } + + $status_lc = strtolower($status); + + if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { + $html = html::div('rsvp-status', $this->gettext('notanattendee')); + $action = 'import'; + } + else if (in_array($status_lc, $this->rsvp_status)) { + $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc); + + if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) { + $action = ''; // nothing to do here, outdated invitation + if ($status_lc == 'needs-action') + $status_text = $this->gettext('outdatedinvitation'); + } + else if (!$existing && !$rsvp) { + $action = 'import'; + } + else if ($latest && $status_lc != 'needs-action') { + $action = 'update'; + } + + $html = html::div('rsvp-status ' . $status_lc, $status_text); + } + } + // determine action for REPLY + else if ($event['method'] == 'REPLY') { + // check whether the sender already is an attendee + if ($existing) { + $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : ''; + $listed = false; + foreach ($existing['attendees'] as $attendee) { + if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) { + $status_lc = strtolower($status); + if (in_array($status_lc, $this->rsvp_status)) { + $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array( + 'name' => 'attendee' . $status_lc, + 'vars' => array( + 'delegatedto' => Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')), + ) + ))); + } + $action = $attendee['status'] == $status || !$latest ? '' : 'update'; + $listed = true; + break; + } + } + + if (!$listed) { + $html = html::div('rsvp-status', $this->gettext('itipnewattendee')); + } + } + else { + $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); + $action = ''; + } + } + else if ($event['method'] == 'CANCEL') { + if (!$existing) { + $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); + $action = ''; + } + } + + return array( + 'uid' => $event['uid'], + 'id' => asciiwords($event['uid'], true), + 'existing' => $existing ? true : false, + 'saved' => $existing ? true : false, + 'latest' => $latest, + 'status' => $status, + 'action' => $action, + 'html' => $html, + ); + } + + /** + * Build inline UI elements for iTip messages + */ + public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null) + { + $buttons = array(); + $dom_id = asciiwords($event['uid'], true); + $rsvp_status = 'unknown'; + + // pass some metadata about the event and trigger the asynchronous status check + $changed = is_object($event['changed']) ? $event['changed'] : $message_date; + $metadata = array( + 'uid' => $event['uid'], + '_instance' => $event['_instance'], + 'changed' => $changed ? $changed->format('U') : 0, + 'sequence' => intval($event['sequence']), + 'method' => $method, + 'task' => $task, + ); + + // create buttons to be activated from async request checking existence of this event in local calendars + $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading')); + + // on iTip REPLY we have two options: + if ($method == 'REPLY') { + $title = $this->gettext('itipreply'); + + foreach ($event['attendees'] as $attendee) { + if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER' && + (empty($event['_sender']) || ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf']))) { + $metadata['attendee'] = $attendee['email']; + $rsvp_status = strtoupper($attendee['status']); + if ($attendee['delegated-to']) + $metadata['delegated-to'] = $attendee['delegated-to']; + break; + } + } + + // 1. update the attendee status on our copy + $update_button = html::tag('input', array( + 'type' => 'button', + 'class' => 'button', + 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", + 'value' => $this->gettext('updateattendeestatus'), + )); + + // 2. accept or decline a new or delegate attendee + $accept_buttons = html::tag('input', array( + 'type' => 'button', + 'class' => "button accept", + 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", + 'value' => $this->gettext('acceptattendee'), + )); + $accept_buttons .= html::tag('input', array( + 'type' => 'button', + 'class' => "button decline", + 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . JQ($mime_id) . "', '$task')", + 'value' => $this->gettext('declineattendee'), + )); + + $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); + $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons); + } + // when receiving iTip REQUEST messages: + else if ($method == 'REQUEST') { + $emails = $this->lib->get_user_emails(); + $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation'); + $metadata['rsvp'] = true; + $metadata['sensitivity'] = $event['sensitivity']; + + if (is_object($event['start'])) { + $metadata['date'] = $event['start']->format('U'); + } + + // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons + if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') { + $this->rsvp_actions = array('accepted','declined'); + $metadata['nosave'] = true; + } + + // 1. display RSVP buttons (if the user was invited) + foreach ($this->rsvp_actions as $method) { + $rsvp_buttons .= html::tag('input', array( + 'type' => 'button', + 'class' => "button $method", + 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task', '$method', '$dom_id')", + 'value' => $this->gettext('itip' . $method), + )); + } + + // add button to open calendar/preview + if (!empty($preview_url)) { + $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id; + $rsvp_buttons .= html::tag('input', array( + 'type' => 'button', + 'class' => "button preview", + 'onclick' => "rcube_libcalendaring.open_itip_preview('" . JQ($preview_url) . "', '" . JQ($msgref) . "')", + 'value' => $this->gettext('openpreview'), + )); + } + + // 2. update the local copy with minor changes + $update_button = html::tag('input', array( + 'type' => 'button', + 'class' => 'button', + 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", + 'value' => $this->gettext('updatemycopy'), + )); + + // 3. Simply import the event without replying + $import_button = html::tag('input', array( + 'type' => 'button', + 'class' => 'button', + 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", + 'value' => $this->gettext('importtocalendar'), + )); + + // check my status + foreach ($event['attendees'] as $attendee) { + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $metadata['attendee'] = $attendee['email']; + $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; + $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION'; + break; + } + } + + // add itip reply message controls + $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave'])); + + $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons); + $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); + + // prepare autocompletion for delegation dialog + if (in_array('delegated', $this->rsvp_actions)) { + $this->rc->autocomplete_init(); + } + } + // for CANCEL messages, we can: + else if ($method == 'CANCEL') { + $title = $this->gettext('itipcancellation'); + $event_prop = array_filter(array( + 'uid' => $event['uid'], + '_instance' => $event['_instance'], + '_savemode' => $event['_savemode'], + )); + + // 1. remove the event from our calendar + $button_remove = html::tag('input', array( + 'type' => 'button', + 'class' => 'button', + 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . JQ($event['title']) . "')", + 'value' => $this->gettext('removefromcalendar'), + )); + + // 2. update our copy with status=cancelled + $button_update = html::tag('input', array( + 'type' => 'button', + 'class' => 'button', + 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", + 'value' => $this->gettext('updatemycopy'), + )); + + $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update); + + $rsvp_status = 'CANCELLED'; + $metadata['rsvp'] = true; + } + + // append generic import button + if ($import_button) { + $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button); + } + + // pass some metadata about the event and trigger the asynchronous status check + $metadata['fallback'] = $rsvp_status; + $metadata['rsvp'] = intval($metadata['rsvp']); + + $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . json_serialize($metadata) . ")", 'docready'); + + // get localized texts from the right domain + foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee', + 'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation', + 'delegateto','delegatersvpme','delegateinvalidaddress') as $label) { + $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); + } + + // show event details with buttons + return $this->itip_object_details_table($event, $title) . + html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons)); + } + + /** + * Render an RSVP UI widget with buttons to respond on iTip invitations + */ + function itip_rsvp_buttons($attrib = array(), $actions = null) + { + $attrib += array('type' => 'button'); + + if (!$actions) + $actions = $this->rsvp_actions; + + foreach ($actions as $method) { + $buttons .= html::tag('input', array( + 'type' => $attrib['type'], + 'name' => $attrib['iname'], + 'class' => 'button', + 'rel' => $method, + 'value' => $this->gettext('itip' . $method), + )); + } + + // add localized texts for the delegation dialog + if (in_array('delegated', $actions)) { + foreach (array('itipdelegated','itipcomment','delegateinvitation', + 'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) { + $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); + } + } + + foreach (array('all','current','future') as $mode) { + $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode")); + } + + $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode')); + + return html::div($attrib, + html::div('label', $this->gettext('acceptinvitation')) . + html::div('rsvp-buttons', + $buttons . + html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id'])) + ) + ); + } + + /** + * Render UI elements to control iTip reply message sending + */ + public function itip_rsvp_options_ui($dom_id, $disable = false) + { + $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3); + + // itip sending is entirely disabled + if ($itip_sending === 0) { + return ''; + } + // add checkbox to suppress itip reply message + else if ($itip_sending >= 2) { + $rsvp_additions = html::label(array('class' => 'noreply-toggle'), + html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0)) + . ' ' . $this->gettext('itipsuppressreply') + ); + } + + // add input field for reply comment + $rsvp_additions .= html::a(array('href' => '#toggle', 'class' => 'reply-comment-toggle'), $this->gettext('itipeditresponse')); + $rsvp_additions .= html::div('itip-reply-comment', + html::tag('textarea', array('id' => 'reply-comment-'.$dom_id, 'name' => '_comment', 'cols' => 40, 'rows' => 6, 'style' => 'display:none', 'placeholder' => $this->gettext('itipcomment')), '') + ); + + return $rsvp_additions; + } + + /** + * Render event/task details in a table + */ + function itip_object_details_table($event, $title) + { + $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); + $table->add('ititle', $title); + $table->add('title', Q($event['title'])); + if ($event['start'] && $event['end']) { + $table->add('label', $this->gettext('date')); + $table->add('date', Q($this->lib->event_date_text($event))); + } + else if ($event['due'] && $event['_type'] == 'task') { + $table->add('label', $this->gettext('date')); + $table->add('date', Q($this->lib->event_date_text($event))); + } + if (!empty($event['recurrence_date'])) { + $table->add('label', ''); + $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence')); + } + else if (!empty($event['recurrence'])) { + $table->add('label', $this->gettext('recurring')); + $table->add('recurrence', $this->lib->recurrence_text($event['recurrence'])); + } + if ($event['location']) { + $table->add('label', $this->gettext('location')); + $table->add('location', Q($event['location'])); + } + if ($event['sensitivity'] && $event['sensitivity'] != 'public') { + $table->add('label', $this->gettext('sensitivity')); + $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!'); + } + if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') { + $table->add('label', $this->gettext('status')); + $table->add('status', $this->gettext('status-' . strtolower($event['status']))); + } + if ($event['comment']) { + $table->add('label', $this->gettext('comment')); + $table->add('location', Q($event['comment'])); + } + + return $table->show(); + } + + + /** + * Create iTIP invitation token for later replies via URL + * + * @param array Hash array with event properties + * @param string Attendee email address + * @return string Invitation token + */ + public function store_invitation($event, $attendee) + { + // empty stub + return false; + } + + /** + * Mark invitations for the given event as cancelled + * + * @param array Hash array with event properties + */ + public function cancel_itip_invitation($event) + { + // empty stub + return false; + } + + /** + * Utility function to get the value of a custom property + */ + public static function get_custom_property($event, $name) + { + $ret = false; + + if (is_array($event['x-custom'])) { + array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) { + if (strcasecmp($prop[0], $name) === 0) { + $ret = $prop[1]; + } + }); + } + + return $ret; + } + +} diff --git a/libcalendaring/lib/libcalendaring_recurrence.php b/libcalendaring/lib/libcalendaring_recurrence.php new file mode 100644 index 0000000..bbc4976 --- /dev/null +++ b/libcalendaring/lib/libcalendaring_recurrence.php @@ -0,0 +1,155 @@ + + * + * Copyright (C) 2012-2014, Kolab Systems AG + * + * 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 . + */ +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; + } + +} -- cgit v1.2.3