diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php new file mode 100644 index 00000000..18709d9f --- /dev/null +++ b/plugins/calendar/lib/Horde_Date.php @@ -0,0 +1,775 @@ +_supportedSpecs .= 'bBpxX'; + } + + if (is_array($date) || is_object($date)) { + 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 (is_array($date) && isset($date['day']) && + is_numeric($date['day'])) { + $this->mday = (int)$date['day']; + } + // 'minute' key also from Horde_Form_datetime + if (is_array($date) && isset($date['minute'])) { + $this->min = $date['minute']; + } + } elseif (!is_null($date)) { + // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'. + if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})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]; + } else { + // 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']; + } + } + } + } + + /** + * @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 day of the year (1-366) that corresponds to the + * first day of the given week. + * + * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php + * + * @param integer $week The week of the year to find the first day of. + * @param integer $year The year to calculate for. + * + * @return integer The day of the year of the first day of the given week. + */ + function firstDayOfWeek($week, $year) + { + $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1)); + $start = $jan1->dayOfWeek(); + if ($start > HORDE_DATE_THURSDAY) { + $start -= 7; + } + return (($week * 7) - (7 + $start)) + 1; + } + + /** + * @static + */ + function daysInMonth($month, $year) + { + if ($month == 2) { + if (Horde_Date::isLeapYear($year)) { + return 29; + } else { + return 28; + } + } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) { + return 30; + } else { + return 31; + } + } + + /** + * Return the day of the week (0 = Sunday, 6 = Saturday) of this + * object's date. + * + * @return integer The day of the week. + */ + 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. + */ + function dayOfYear() + { + $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); + $dayOfYear = $this->mday + $monthTotals[$this->month - 1]; + if (Horde_Date::isLeapYear($this->year) && $this->month > 2) { + ++$dayOfYear; + } + + return $dayOfYear; + } + + /** + * Returns the week of the month. + * + * @since Horde 3.2 + * + * @return integer The week number. + */ + 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. + */ + function weekOfYear() + { + return $this->format('W'); + } + + /** + * Return the number of weeks in the given year (52 or 53). + * + * @static + * + * @param integer $year The year to count the number of weeks in. + * + * @return integer $numWeeks The number of weeks in $year. + */ + function weeksInYear($year) + { + // Find the last Thursday of the year. + $day = 31; + $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0)); + while ($date->dayOfWeek() != HORDE_DATE_THURSDAY) { + --$date->mday; + } + return $date->weekOfYear(); + } + + /** + * Set 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). + */ + function setNthWeekday($weekday, $nth = 1) + { + if ($weekday < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) { + return false; + } + + $this->mday = 1; + $first = $this->dayOfWeek(); + if ($weekday < $first) { + $this->mday = 8 + $weekday - $first; + } else { + $this->mday = $weekday - $first + 1; + } + $this->mday += 7 * $nth - 7; + + $this->correct(); + + return true; + } + + function dump($prefix = '') + { + echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "
\n"; + } + + /** + * Is the date currently represented by this object a valid date? + * + * @return boolean Validity, counting leap years, etc. + */ + function isValid() + { + if ($this->year < 0 || $this->year > 9999) { + return false; + } + return checkdate($this->month, $this->mday, $this->year); + } + + /** + * Correct any over- or underflows in any of the date's members. + * + * @param integer $mask We may not want to correct some overflows. + */ + function correct($mask = HORDE_DATE_MASK_ALLPARTS) + { + if ($mask & HORDE_DATE_MASK_SECOND) { + while ($this->sec < 0) { + --$this->min; + $this->sec += 60; + } + while ($this->sec > 59) { + ++$this->min; + $this->sec -= 60; + } + } + + if ($mask & HORDE_DATE_MASK_MINUTE) { + while ($this->min < 0) { + --$this->hour; + $this->min += 60; + } + while ($this->min > 59) { + ++$this->hour; + $this->min -= 60; + } + } + + if ($mask & HORDE_DATE_MASK_HOUR) { + while ($this->hour < 0) { + --$this->mday; + $this->hour += 24; + } + while ($this->hour > 23) { + ++$this->mday; + $this->hour -= 24; + } + } + + if ($mask & HORDE_DATE_MASK_MONTH) { + while ($this->month > 12) { + ++$this->year; + $this->month -= 12; + } + while ($this->month < 1) { + --$this->year; + $this->month += 12; + } + } + + if ($mask & HORDE_DATE_MASK_DAY) { + while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) { + $this->mday -= Horde_Date::daysInMonth($this->month, $this->year); + ++$this->month; + $this->correct(HORDE_DATE_MASK_MONTH); + } + while ($this->mday < 1) { + --$this->month; + $this->correct(HORDE_DATE_MASK_MONTH); + $this->mday += Horde_Date::daysInMonth($this->month, $this->year); + } + } + } + + /** + * Compare this date to another date object to see which one is + * greater (later). Assumes that the dates are in the same + * timezone. + * + * @param mixed $date The date to compare to. + * + * @return integer == 0 if the dates are equal + * >= 1 if this date is greater (later) + * <= -1 if the other date is greater (later) + */ + function compareDate($date) + { + if (!is_object($date) || !is_a($date, 'Horde_Date')) { + $date = new Horde_Date($date); + } + + if ($this->year != $date->year) { + return $this->year - $date->year; + } + if ($this->month != $date->month) { + return $this->month - $date->month; + } + + return $this->mday - $date->mday; + } + + /** + * Compare 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 $date The date to compare to. + * + * @return integer == 0 if the dates are equal + * >= 1 if this date is greater (later) + * <= -1 if the other date is greater (later) + */ + function compareTime($date) + { + if (!is_object($date) || !is_a($date, 'Horde_Date')) { + $date = new Horde_Date($date); + } + + if ($this->hour != $date->hour) { + return $this->hour - $date->hour; + } + if ($this->min != $date->min) { + return $this->min - $date->min; + } + + return $this->sec - $date->sec; + } + + /** + * Compare 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 $date The date to compare to. + * + * @return integer == 0 if the dates are equal + * >= 1 if this date is greater (later) + * <= -1 if the other date is greater (later) + */ + function compareDateTime($date) + { + if (!is_object($date) || !is_a($date, 'Horde_Date')) { + $date = new Horde_Date($date); + } + + if ($diff = $this->compareDate($date)) { + return $diff; + } + + return $this->compareTime($date); + } + + /** + * Get 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. + */ + function tzOffset($colon = true) + { + $secs = $this->format('Z'); + + if ($secs < 0) { + $sign = '-'; + $secs = -$secs; + } else { + $sign = '+'; + } + $colon = $colon ? ':' : ''; + $mins = intval(($secs + 30) / 60); + return sprintf('%s%02d%s%02d', + $sign, $mins / 60, $colon, $mins % 60); + } + + /** + * Return the unix timestamp representation of this date. + * + * @return integer A unix timestamp. + */ + function timestamp() + { + if (class_exists('DateTime')) { + return $this->format('U'); + } else { + return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year); + } + } + + /** + * Return the unix timestamp representation of this date, 12:00am. + * + * @return integer A unix timestamp. + */ + function datestamp() + { + if (class_exists('DateTime')) { + $dt = new DateTime(); + $dt->setDate($this->year, $this->month, $this->mday); + $dt->setTime(0, 0, 0); + return $dt->format('U'); + } else { + return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year); + } + } + + /** + * Format time using the specifiers available in date() or in the DateTime + * class' format() method. + * + * @since Horde 3.3 + * + * @param string $format + * + * @return string Formatted time. + */ + function format($format) + { + if (class_exists('DateTime')) { + $dt = new DateTime(); + $dt->setDate($this->year, $this->month, $this->mday); + $dt->setTime($this->hour, $this->min, $this->sec); + return $dt->format($format); + } else { + return date($format, $this->timestamp()); + } + } + + /** + * Format time in ISO-8601 format. Works correctly since Horde 3.2. + * + * @return string Date and time in ISO-8601 format. + */ + function iso8601DateTime() + { + return $this->rfc3339DateTime() . $this->tzOffset(); + } + + /** + * Format time in RFC 2822 format. + * + * @return string Date and time in RFC 2822 format. + */ + function rfc2822DateTime() + { + return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false); + } + + /** + * Format time in RFC 3339 format. + * + * @since Horde 3.1 + * + * @return string Date and time in RFC 3339 format. The seconds part has + * been added with Horde 3.2. + */ + function rfc3339DateTime() + { + return $this->format('Y-m-d\TH:i:s'); + } + + /** + * Format time to standard 'ctime' format. + * + * @return string Date and time. + */ + function cTime() + { + return $this->format('D M j H:i:s Y'); + } + + /** + * Format date and time using strftime() format. + * + * @since Horde 3.1 + * + * @return string strftime() formatted date and time. + */ + function strftime($format) + { + if (preg_match('/%[^' . $this->_supportedSpecs . ']/', $format)) { + return strftime($format, $this->timestamp()); + } else { + return $this->_strftime($format); + } + } + + /** + * Format date and time using a limited set of the strftime() format. + * + * @return string strftime() formatted date and time. + */ + function _strftime($format) + { + if (preg_match('/%[bBpxX]/', $format)) { + require_once 'Horde/NLS.php'; + } + + 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(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->month)))', + '$this->_strftime(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(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(NLS::getLangInfo(D_FMT))', + '$this->_strftime(NLS::getLangInfo(T_FMT))', + 'substr(sprintf(\'%04d\', $this->year), -2)', + (int)$this->year, + '%'), + $format); + } + + /** + * mktime() implementation that supports dates outside of 1970-2038, + * from http://phplens.com/phpeverywhere/adodb_date_library. + * + * @TODO remove in Horde 4 + * + * This does NOT work with pre-1970 daylight saving times. + * + * @static + */ + function _mktime($hr, $min, $sec, $mon = false, $day = false, + $year = false, $is_dst = false, $is_gmt = false) + { + if ($mon === false) { + return $is_gmt + ? @gmmktime($hr, $min, $sec) + : @mktime($hr, $min, $sec); + } + + if ($year > 1901 && $year < 2038 && + ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) { + return $is_gmt + ? @gmmktime($hr, $min, $sec, $mon, $day, $year) + : @mktime($hr, $min, $sec, $mon, $day, $year); + } + + $gmt_different = $is_gmt + ? 0 + : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0)); + + $mon = intval($mon); + $day = intval($day); + $year = intval($year); + + if ($mon > 12) { + $y = floor($mon / 12); + $year += $y; + $mon -= $y * 12; + } elseif ($mon < 1) { + $y = ceil((1 - $mon) / 12); + $year -= $y; + $mon += $y * 12; + } + + $_day_power = 86400; + $_hour_power = 3600; + $_min_power = 60; + + $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + + $_total_date = 0; + if ($year >= 1970) { + for ($a = 1970; $a <= $year; $a++) { + $leaf = Horde_Date::isLeapYear($a); + if ($leaf == true) { + $loop_table = $_month_table_leaf; + $_add_date = 366; + } else { + $loop_table = $_month_table_normal; + $_add_date = 365; + } + if ($a < $year) { + $_total_date += $_add_date; + } else { + for ($b = 1; $b < $mon; $b++) { + $_total_date += $loop_table[$b]; + } + } + } + + return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different; + } + + for ($a = 1969 ; $a >= $year; $a--) { + $leaf = Horde_Date::isLeapYear($a); + if ($leaf == true) { + $loop_table = $_month_table_leaf; + $_add_date = 366; + } else { + $loop_table = $_month_table_normal; + $_add_date = 365; + } + if ($a > $year) { + $_total_date += $_add_date; + } else { + for ($b = 12; $b > $mon; $b--) { + $_total_date += $loop_table[$b]; + } + } + } + + $_total_date += $loop_table[$mon] - $day; + $_day_time = $hr * $_hour_power + $min * $_min_power + $sec; + $_day_time = $_day_power - $_day_time; + $ret = -($_total_date * $_day_power + $_day_time - $gmt_different); + if ($ret < -12220185600) { + // If earlier than 5 Oct 1582 - gregorian correction. + return $ret + 10 * 86400; + } elseif ($ret < -12219321600) { + // If in limbo, reset to 15 Oct 1582. + return -12219321600; + } else { + return $ret; + } + } + +} + diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php index 68340ba3..fbf1d1eb 100644 --- a/plugins/calendar/lib/Horde_Date_Recurrence.php +++ b/plugins/calendar/lib/Horde_Date_Recurrence.php @@ -1,780 +1,6 @@ _supportedSpecs .= 'bBpxX'; - } - - if (is_array($date) || is_object($date)) { - 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 (is_array($date) && isset($date['day']) && - is_numeric($date['day'])) { - $this->mday = (int)$date['day']; - } - // 'minute' key also from Horde_Form_datetime - if (is_array($date) && isset($date['minute'])) { - $this->min = $date['minute']; - } - } elseif (!is_null($date)) { - // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'. - if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})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]; - } else { - // 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']; - } - } - } - } - - /** - * @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 day of the year (1-366) that corresponds to the - * first day of the given week. - * - * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php - * - * @param integer $week The week of the year to find the first day of. - * @param integer $year The year to calculate for. - * - * @return integer The day of the year of the first day of the given week. - */ - function firstDayOfWeek($week, $year) - { - $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1)); - $start = $jan1->dayOfWeek(); - if ($start > HORDE_DATE_THURSDAY) { - $start -= 7; - } - return (($week * 7) - (7 + $start)) + 1; - } - - /** - * @static - */ - function daysInMonth($month, $year) - { - if ($month == 2) { - if (Horde_Date::isLeapYear($year)) { - return 29; - } else { - return 28; - } - } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) { - return 30; - } else { - return 31; - } - } - - /** - * Return the day of the week (0 = Sunday, 6 = Saturday) of this - * object's date. - * - * @return integer The day of the week. - */ - 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. - */ - function dayOfYear() - { - $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); - $dayOfYear = $this->mday + $monthTotals[$this->month - 1]; - if (Horde_Date::isLeapYear($this->year) && $this->month > 2) { - ++$dayOfYear; - } - - return $dayOfYear; - } - - /** - * Returns the week of the month. - * - * @since Horde 3.2 - * - * @return integer The week number. - */ - 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. - */ - function weekOfYear() - { - return $this->format('W'); - } - - /** - * Return the number of weeks in the given year (52 or 53). - * - * @static - * - * @param integer $year The year to count the number of weeks in. - * - * @return integer $numWeeks The number of weeks in $year. - */ - function weeksInYear($year) - { - // Find the last Thursday of the year. - $day = 31; - $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0)); - while ($date->dayOfWeek() != HORDE_DATE_THURSDAY) { - --$date->mday; - } - return $date->weekOfYear(); - } - - /** - * Set 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). - */ - function setNthWeekday($weekday, $nth = 1) - { - if ($weekday < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) { - return false; - } - - $this->mday = 1; - $first = $this->dayOfWeek(); - if ($weekday < $first) { - $this->mday = 8 + $weekday - $first; - } else { - $this->mday = $weekday - $first + 1; - } - $this->mday += 7 * $nth - 7; - - $this->correct(); - - return true; - } - - function dump($prefix = '') - { - echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "
\n"; - } - - /** - * Is the date currently represented by this object a valid date? - * - * @return boolean Validity, counting leap years, etc. - */ - function isValid() - { - if ($this->year < 0 || $this->year > 9999) { - return false; - } - return checkdate($this->month, $this->mday, $this->year); - } - - /** - * Correct any over- or underflows in any of the date's members. - * - * @param integer $mask We may not want to correct some overflows. - */ - function correct($mask = HORDE_DATE_MASK_ALLPARTS) - { - if ($mask & HORDE_DATE_MASK_SECOND) { - while ($this->sec < 0) { - --$this->min; - $this->sec += 60; - } - while ($this->sec > 59) { - ++$this->min; - $this->sec -= 60; - } - } - - if ($mask & HORDE_DATE_MASK_MINUTE) { - while ($this->min < 0) { - --$this->hour; - $this->min += 60; - } - while ($this->min > 59) { - ++$this->hour; - $this->min -= 60; - } - } - - if ($mask & HORDE_DATE_MASK_HOUR) { - while ($this->hour < 0) { - --$this->mday; - $this->hour += 24; - } - while ($this->hour > 23) { - ++$this->mday; - $this->hour -= 24; - } - } - - if ($mask & HORDE_DATE_MASK_MONTH) { - while ($this->month > 12) { - ++$this->year; - $this->month -= 12; - } - while ($this->month < 1) { - --$this->year; - $this->month += 12; - } - } - - if ($mask & HORDE_DATE_MASK_DAY) { - while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) { - $this->mday -= Horde_Date::daysInMonth($this->month, $this->year); - ++$this->month; - $this->correct(HORDE_DATE_MASK_MONTH); - } - while ($this->mday < 1) { - --$this->month; - $this->correct(HORDE_DATE_MASK_MONTH); - $this->mday += Horde_Date::daysInMonth($this->month, $this->year); - } - } - } - - /** - * Compare this date to another date object to see which one is - * greater (later). Assumes that the dates are in the same - * timezone. - * - * @param mixed $date The date to compare to. - * - * @return integer == 0 if the dates are equal - * >= 1 if this date is greater (later) - * <= -1 if the other date is greater (later) - */ - function compareDate($date) - { - if (!is_object($date) || !is_a($date, 'Horde_Date')) { - $date = new Horde_Date($date); - } - - if ($this->year != $date->year) { - return $this->year - $date->year; - } - if ($this->month != $date->month) { - return $this->month - $date->month; - } - - return $this->mday - $date->mday; - } - - /** - * Compare 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 $date The date to compare to. - * - * @return integer == 0 if the dates are equal - * >= 1 if this date is greater (later) - * <= -1 if the other date is greater (later) - */ - function compareTime($date) - { - if (!is_object($date) || !is_a($date, 'Horde_Date')) { - $date = new Horde_Date($date); - } - - if ($this->hour != $date->hour) { - return $this->hour - $date->hour; - } - if ($this->min != $date->min) { - return $this->min - $date->min; - } - - return $this->sec - $date->sec; - } - - /** - * Compare 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 $date The date to compare to. - * - * @return integer == 0 if the dates are equal - * >= 1 if this date is greater (later) - * <= -1 if the other date is greater (later) - */ - function compareDateTime($date) - { - if (!is_object($date) || !is_a($date, 'Horde_Date')) { - $date = new Horde_Date($date); - } - - if ($diff = $this->compareDate($date)) { - return $diff; - } - - return $this->compareTime($date); - } - - /** - * Get 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. - */ - function tzOffset($colon = true) - { - $secs = $this->format('Z'); - - if ($secs < 0) { - $sign = '-'; - $secs = -$secs; - } else { - $sign = '+'; - } - $colon = $colon ? ':' : ''; - $mins = intval(($secs + 30) / 60); - return sprintf('%s%02d%s%02d', - $sign, $mins / 60, $colon, $mins % 60); - } - - /** - * Return the unix timestamp representation of this date. - * - * @return integer A unix timestamp. - */ - function timestamp() - { - if (class_exists('DateTime')) { - return $this->format('U'); - } else { - return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year); - } - } - - /** - * Return the unix timestamp representation of this date, 12:00am. - * - * @return integer A unix timestamp. - */ - function datestamp() - { - if (class_exists('DateTime')) { - $dt = new DateTime(); - $dt->setDate($this->year, $this->month, $this->mday); - $dt->setTime(0, 0, 0); - return $dt->format('U'); - } else { - return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year); - } - } - - /** - * Format time using the specifiers available in date() or in the DateTime - * class' format() method. - * - * @since Horde 3.3 - * - * @param string $format - * - * @return string Formatted time. - */ - function format($format) - { - if (class_exists('DateTime')) { - $dt = new DateTime(); - $dt->setDate($this->year, $this->month, $this->mday); - $dt->setTime($this->hour, $this->min, $this->sec); - return $dt->format($format); - } else { - return date($format, $this->timestamp()); - } - } - - /** - * Format time in ISO-8601 format. Works correctly since Horde 3.2. - * - * @return string Date and time in ISO-8601 format. - */ - function iso8601DateTime() - { - return $this->rfc3339DateTime() . $this->tzOffset(); - } - - /** - * Format time in RFC 2822 format. - * - * @return string Date and time in RFC 2822 format. - */ - function rfc2822DateTime() - { - return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false); - } - - /** - * Format time in RFC 3339 format. - * - * @since Horde 3.1 - * - * @return string Date and time in RFC 3339 format. The seconds part has - * been added with Horde 3.2. - */ - function rfc3339DateTime() - { - return $this->format('Y-m-d\TH:i:s'); - } - - /** - * Format time to standard 'ctime' format. - * - * @return string Date and time. - */ - function cTime() - { - return $this->format('D M j H:i:s Y'); - } - - /** - * Format date and time using strftime() format. - * - * @since Horde 3.1 - * - * @return string strftime() formatted date and time. - */ - function strftime($format) - { - if (preg_match('/%[^' . $this->_supportedSpecs . ']/', $format)) { - return strftime($format, $this->timestamp()); - } else { - return $this->_strftime($format); - } - } - - /** - * Format date and time using a limited set of the strftime() format. - * - * @return string strftime() formatted date and time. - */ - function _strftime($format) - { - if (preg_match('/%[bBpxX]/', $format)) { - require_once 'Horde/NLS.php'; - } - - 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(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->month)))', - '$this->_strftime(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(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(NLS::getLangInfo(D_FMT))', - '$this->_strftime(NLS::getLangInfo(T_FMT))', - 'substr(sprintf(\'%04d\', $this->year), -2)', - (int)$this->year, - '%'), - $format); - } - - /** - * mktime() implementation that supports dates outside of 1970-2038, - * from http://phplens.com/phpeverywhere/adodb_date_library. - * - * @TODO remove in Horde 4 - * - * This does NOT work with pre-1970 daylight saving times. - * - * @static - */ - function _mktime($hr, $min, $sec, $mon = false, $day = false, - $year = false, $is_dst = false, $is_gmt = false) - { - if ($mon === false) { - return $is_gmt - ? @gmmktime($hr, $min, $sec) - : @mktime($hr, $min, $sec); - } - - if ($year > 1901 && $year < 2038 && - ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) { - return $is_gmt - ? @gmmktime($hr, $min, $sec, $mon, $day, $year) - : @mktime($hr, $min, $sec, $mon, $day, $year); - } - - $gmt_different = $is_gmt - ? 0 - : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0)); - - $mon = intval($mon); - $day = intval($day); - $year = intval($year); - - if ($mon > 12) { - $y = floor($mon / 12); - $year += $y; - $mon -= $y * 12; - } elseif ($mon < 1) { - $y = ceil((1 - $mon) / 12); - $year -= $y; - $mon += $y * 12; - } - - $_day_power = 86400; - $_hour_power = 3600; - $_min_power = 60; - - $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); - $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); - - $_total_date = 0; - if ($year >= 1970) { - for ($a = 1970; $a <= $year; $a++) { - $leaf = Horde_Date::isLeapYear($a); - if ($leaf == true) { - $loop_table = $_month_table_leaf; - $_add_date = 366; - } else { - $loop_table = $_month_table_normal; - $_add_date = 365; - } - if ($a < $year) { - $_total_date += $_add_date; - } else { - for ($b = 1; $b < $mon; $b++) { - $_total_date += $loop_table[$b]; - } - } - } - - return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different; - } - - for ($a = 1969 ; $a >= $year; $a--) { - $leaf = Horde_Date::isLeapYear($a); - if ($leaf == true) { - $loop_table = $_month_table_leaf; - $_add_date = 366; - } else { - $loop_table = $_month_table_normal; - $_add_date = 365; - } - if ($a > $year) { - $_total_date += $_add_date; - } else { - for ($b = 12; $b > $mon; $b--) { - $_total_date += $loop_table[$b]; - } - } - } - - $_total_date += $loop_table[$mon] - $day; - $_day_time = $hr * $_hour_power + $min * $_min_power + $sec; - $_day_time = $_day_power - $_day_time; - $ret = -($_total_date * $_day_power + $_day_time - $gmt_different); - if ($ret < -12220185600) { - // If earlier than 5 Oct 1582 - gregorian correction. - return $ret + 10 * 86400; - } elseif ($ret < -12219321600) { - // If in limbo, reset to 15 Oct 1582. - return -12219321600; - } else { - return $ret; - } - } - -} - - -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */ +require_once(dirname(__FILE__) . '/Horde_Date.php'); // {{{ Header diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php new file mode 100644 index 00000000..5ba0f4c8 --- /dev/null +++ b/plugins/calendar/lib/Horde_iCalendar.php @@ -0,0 +1,3284 @@ + + * @since Horde 3.0 + * @package Horde_Util + */ +class String { + + /** + * Caches the result of extension_loaded() calls. + * + * @param string $ext The extension name. + * + * @return boolean Is the extension loaded? + * + * @see Util::extensionExists() + */ + function extensionExists($ext) + { + static $cache = array(); + + if (!isset($cache[$ext])) { + $cache[$ext] = extension_loaded($ext); + } + + return $cache[$ext]; + } + + /** + * Sets a default charset that the String:: methods will use if none is + * explicitly specified. + * + * @param string $charset The charset to use as the default one. + */ + function setDefaultCharset($charset) + { + $GLOBALS['_HORDE_STRING_CHARSET'] = $charset; + if (String::extensionExists('mbstring') && + function_exists('mb_regex_encoding')) { + $old_error = error_reporting(0); + mb_regex_encoding(String::_mbstringCharset($charset)); + error_reporting($old_error); + } + } + + /** + * Converts a string from one charset to another. + * + * Works only if either the iconv or the mbstring extension + * are present and best if both are available. + * The original string is returned if conversion failed or none + * of the extensions were available. + * + * @param mixed $input The data to be converted. If $input is an an array, + * the array's values get converted recursively. + * @param string $from The string's current charset. + * @param string $to The charset to convert the string to. If not + * specified, the global variable + * $_HORDE_STRING_CHARSET will be used. + * + * @return mixed The converted input data. + */ + function convertCharset($input, $from, $to = null) + { + /* Don't bother converting numbers. */ + if (is_numeric($input)) { + return $input; + } + + /* Get the user's default character set if none passed in. */ + if (is_null($to)) { + $to = $GLOBALS['_HORDE_STRING_CHARSET']; + } + + /* If the from and to character sets are identical, return now. */ + if ($from == $to) { + return $input; + } + $from = String::lower($from); + $to = String::lower($to); + if ($from == $to) { + return $input; + } + + if (is_array($input)) { + $tmp = array(); + reset($input); + while (list($key, $val) = each($input)) { + $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to); + } + return $tmp; + } + if (is_object($input)) { + // PEAR_Error objects are almost guaranteed to contain recursion, + // which will cause a segfault in PHP. We should never reach + // this line, but add a check and a log message to help the devs + // track down and fix this issue. + if (is_a($input, 'PEAR_Error')) { + Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return ''; + } + $vars = get_object_vars($input); + while (list($key, $val) = each($vars)) { + $input->$key = String::convertCharset($val, $from, $to); + } + return $input; + } + + if (!is_string($input)) { + return $input; + } + + return String::_convertCharset($input, $from, $to); + } + + /** + * Internal function used to do charset conversion. + * + * @access private + * + * @param string $input See String::convertCharset(). + * @param string $from See String::convertCharset(). + * @param string $to See String::convertCharset(). + * + * @return string The converted string. + */ + function _convertCharset($input, $from, $to) + { + $output = ''; + $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii')); + $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii')); + + /* Use utf8_[en|de]code() if possible and if the string isn't too + * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these + * functions use more memory. */ + if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) { + if ($from_check && ($to == 'utf-8')) { + return utf8_encode($input); + } + + if (($from == 'utf-8') && $to_check) { + return utf8_decode($input); + } + } + + /* First try iconv with transliteration. */ + if (($from != 'utf7-imap') && + ($to != 'utf7-imap') && + String::extensionExists('iconv')) { + /* We need to tack an extra character temporarily because of a bug + * in iconv() if the last character is not a 7 bit ASCII + * character. */ + $oldTrackErrors = ini_set('track_errors', 1); + unset($php_errormsg); + $output = @iconv($from, $to . '//TRANSLIT', $input . 'x'); + $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to); + ini_set('track_errors', $oldTrackErrors); + } + + /* Next try mbstring. */ + if (!$output && String::extensionExists('mbstring')) { + $old_error = error_reporting(0); + $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from)); + error_reporting($old_error); + } + + /* At last try imap_utf7_[en|de]code if appropriate. */ + if (!$output && String::extensionExists('imap')) { + if ($from_check && ($to == 'utf7-imap')) { + return @imap_utf7_encode($input); + } + if (($from == 'utf7-imap') && $to_check) { + return @imap_utf7_decode($input); + } + } + + return (!$output) ? $input : $output; + } + + /** + * Makes a string lowercase. + * + * @param string $string The string to be converted. + * @param boolean $locale If true the string will be converted based on a + * given charset, locale independent else. + * @param string $charset If $locale is true, the charset to use when + * converting. If not provided the current charset. + * + * @return string The string with lowercase characters + */ + function lower($string, $locale = false, $charset = null) + { + static $lowers; + + if ($locale) { + /* The existence of mb_strtolower() depends on the platform. */ + if (String::extensionExists('mbstring') && + function_exists('mb_strtolower')) { + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + $old_error = error_reporting(0); + $ret = mb_strtolower($string, String::_mbstringCharset($charset)); + error_reporting($old_error); + if (!empty($ret)) { + return $ret; + } + } + return strtolower($string); + } + + if (!isset($lowers)) { + $lowers = array(); + } + if (!isset($lowers[$string])) { + $language = setlocale(LC_CTYPE, 0); + setlocale(LC_CTYPE, 'C'); + $lowers[$string] = strtolower($string); + setlocale(LC_CTYPE, $language); + } + + return $lowers[$string]; + } + + /** + * Makes a string uppercase. + * + * @param string $string The string to be converted. + * @param boolean $locale If true the string will be converted based on a + * given charset, locale independent else. + * @param string $charset If $locale is true, the charset to use when + * converting. If not provided the current charset. + * + * @return string The string with uppercase characters + */ + function upper($string, $locale = false, $charset = null) + { + static $uppers; + + if ($locale) { + /* The existence of mb_strtoupper() depends on the + * platform. */ + if (function_exists('mb_strtoupper')) { + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + $old_error = error_reporting(0); + $ret = mb_strtoupper($string, String::_mbstringCharset($charset)); + error_reporting($old_error); + if (!empty($ret)) { + return $ret; + } + } + return strtoupper($string); + } + + if (!isset($uppers)) { + $uppers = array(); + } + if (!isset($uppers[$string])) { + $language = setlocale(LC_CTYPE, 0); + setlocale(LC_CTYPE, 'C'); + $uppers[$string] = strtoupper($string); + setlocale(LC_CTYPE, $language); + } + + return $uppers[$string]; + } + + /** + * Returns a string with the first letter capitalized if it is + * alphabetic. + * + * @param string $string The string to be capitalized. + * @param boolean $locale If true the string will be converted based on a + * given charset, locale independent else. + * @param string $charset The charset to use, defaults to current charset. + * + * @return string The capitalized string. + */ + function ucfirst($string, $locale = false, $charset = null) + { + if ($locale) { + $first = String::substr($string, 0, 1, $charset); + if (String::isAlpha($first, $charset)) { + $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset); + } + } else { + $string = String::upper(substr($string, 0, 1), false) . substr($string, 1); + } + return $string; + } + + /** + * Returns part of a string. + * + * @param string $string The string to be converted. + * @param integer $start The part's start position, zero based. + * @param integer $length The part's length. + * @param string $charset The charset to use when calculating the part's + * position and length, defaults to current + * charset. + * + * @return string The string's part. + */ + function substr($string, $start, $length = null, $charset = null) + { + if (is_null($length)) { + $length = String::length($string, $charset) - $start; + } + + if ($length == 0) { + return ''; + } + + /* Try iconv. */ + if (function_exists('iconv_substr')) { + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + + $old_error = error_reporting(0); + $ret = iconv_substr($string, $start, $length, $charset); + error_reporting($old_error); + /* iconv_substr() returns false on failure. */ + if ($ret !== false) { + return $ret; + } + } + + /* Try mbstring. */ + if (String::extensionExists('mbstring')) { + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + $old_error = error_reporting(0); + $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset)); + error_reporting($old_error); + /* mb_substr() returns empty string on failure. */ + if (strlen($ret)) { + return $ret; + } + } + + return substr($string, $start, $length); + } + + /** + * Returns the character (not byte) length of a string. + * + * @param string $string The string to return the length of. + * @param string $charset The charset to use when calculating the string's + * length. + * + * @return string The string's part. + */ + function length($string, $charset = null) + { + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + $charset = String::lower($charset); + if ($charset == 'utf-8' || $charset == 'utf8') { + return strlen(utf8_decode($string)); + } + if (String::extensionExists('mbstring')) { + $old_error = error_reporting(0); + $ret = mb_strlen($string, String::_mbstringCharset($charset)); + error_reporting($old_error); + if (!empty($ret)) { + return $ret; + } + } + return strlen($string); + } + + /** + * Returns the numeric position of the first occurrence of $needle + * in the $haystack string. + * + * @param string $haystack The string to search through. + * @param string $needle The string to search for. + * @param integer $offset Allows to specify which character in haystack + * to start searching. + * @param string $charset The charset to use when searching for the + * $needle string. + * + * @return integer The position of first occurrence. + */ + function pos($haystack, $needle, $offset = 0, $charset = null) + { + if (String::extensionExists('mbstring')) { + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + $track_errors = ini_set('track_errors', 1); + $old_error = error_reporting(0); + $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset)); + error_reporting($old_error); + ini_set('track_errors', $track_errors); + if (!isset($php_errormsg)) { + return $ret; + } + } + return strpos($haystack, $needle, $offset); + } + + /** + * Returns a string padded to a certain length with another string. + * + * This method behaves exactly like str_pad but is multibyte safe. + * + * @param string $input The string to be padded. + * @param integer $length The length of the resulting string. + * @param string $pad The string to pad the input string with. Must + * be in the same charset like the input string. + * @param const $type The padding type. One of STR_PAD_LEFT, + * STR_PAD_RIGHT, or STR_PAD_BOTH. + * @param string $charset The charset of the input and the padding + * strings. + * + * @return string The padded string. + */ + function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT, + $charset = null) + { + $mb_length = String::length($input, $charset); + $sb_length = strlen($input); + $pad_length = String::length($pad, $charset); + + /* Return if we already have the length. */ + if ($mb_length >= $length) { + return $input; + } + + /* Shortcut for single byte strings. */ + if ($mb_length == $sb_length && $pad_length == strlen($pad)) { + return str_pad($input, $length, $pad, $type); + } + + switch ($type) { + case STR_PAD_LEFT: + $left = $length - $mb_length; + $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input; + break; + case STR_PAD_BOTH: + $left = floor(($length - $mb_length) / 2); + $right = ceil(($length - $mb_length) / 2); + $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . + $input . + String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset); + break; + case STR_PAD_RIGHT: + $right = $length - $mb_length; + $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset); + break; + } + + return $output; + } + + /** + * Wraps the text of a message. + * + * @since Horde 3.2 + * + * @param string $string String containing the text to wrap. + * @param integer $width Wrap the string at this number of + * characters. + * @param string $break Character(s) to use when breaking lines. + * @param boolean $cut Whether to cut inside words if a line + * can't be wrapped. + * @param string $charset Character set to use when breaking lines. + * @param boolean $line_folding Whether to apply line folding rules per + * RFC 822 or similar. The correct break + * characters including leading whitespace + * have to be specified too. + * + * @return string String containing the wrapped text. + */ + function wordwrap($string, $width = 75, $break = "\n", $cut = false, + $charset = null, $line_folding = false) + { + /* Get the user's default character set if none passed in. */ + if (is_null($charset)) { + $charset = $GLOBALS['_HORDE_STRING_CHARSET']; + } + $charset = String::_mbstringCharset($charset); + $string = String::convertCharset($string, $charset, 'utf-8'); + $wrapped = ''; + + while (String::length($string, 'utf-8') > $width) { + $line = String::substr($string, 0, $width, 'utf-8'); + $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8'); + // Make sure didn't cut a word, unless we want hard breaks anyway. + if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) { + $line .= $match[1]; + $string = $match[2]; + } + // Wrap at existing line breaks. + if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) { + $wrapped .= $match[1] . $match[2]; + $string = $match[3] . $string; + continue; + } + // Wrap at the last colon or semicolon followed by a whitespace if + // doing line folding. + if ($line_folding && + preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) { + $wrapped .= $match[1] . $match[2] . $break; + $string = $match[3] . $string; + continue; + } + // Wrap at the last whitespace of $line. + if ($line_folding) { + $sub = '(.+[^\s])'; + } else { + $sub = '(.*)'; + } + if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) { + $wrapped .= $match[1] . $break; + $string = ($line_folding ? $match[2] : '') . $match[3] . $string; + continue; + } + // Hard wrap if necessary. + if ($cut) { + $wrapped .= $line . $break; + continue; + } + $wrapped .= $line; + } + + return String::convertCharset($wrapped . $string, 'utf-8', $charset); + } + + /** + * Wraps the text of a message. + * + * @param string $text String containing the text to wrap. + * @param integer $length Wrap $text at this number of characters. + * @param string $break_char Character(s) to use when breaking lines. + * @param string $charset Character set to use when breaking lines. + * @param boolean $quote Ignore lines that are wrapped with the '>' + * character (RFC 2646)? If true, we don't + * remove any padding whitespace at the end of + * the string. + * + * @return string String containing the wrapped text. + */ + function wrap($text, $length = 80, $break_char = "\n", $charset = null, + $quote = false) + { + $paragraphs = array(); + + foreach (preg_split('/\r?\n/', $text) as $input) { + if ($quote && (strpos($input, '>') === 0)) { + $line = $input; + } else { + /* We need to handle the Usenet-style signature line + * separately; since the space after the two dashes is + * REQUIRED, we don't want to trim the line. */ + if ($input != '-- ') { + $input = rtrim($input); + } + $line = String::wordwrap($input, $length, $break_char, false, $charset); + } + + $paragraphs[] = $line; + } + + return implode($break_char, $paragraphs); + } + + /** + * Returns true if the every character in the parameter is an alphabetic + * character. + * + * @param $string The string to test. + * @param $charset The charset to use when testing the string. + * + * @return boolean True if the parameter was alphabetic only. + */ + function isAlpha($string, $charset = null) + { + if (!String::extensionExists('mbstring')) { + return ctype_alpha($string); + } + + $charset = String::_mbstringCharset($charset); + $old_charset = mb_regex_encoding(); + $old_error = error_reporting(0); + + if ($charset != $old_charset) { + mb_regex_encoding($charset); + } + $alpha = !mb_ereg_match('[^[:alpha:]]', $string); + if ($charset != $old_charset) { + mb_regex_encoding($old_charset); + } + + error_reporting($old_error); + + return $alpha; + } + + /** + * Returns true if ever character in the parameter is a lowercase letter in + * the current locale. + * + * @param $string The string to test. + * @param $charset The charset to use when testing the string. + * + * @return boolean True if the parameter was lowercase. + */ + function isLower($string, $charset = null) + { + return ((String::lower($string, true, $charset) === $string) && + String::isAlpha($string, $charset)); + } + + /** + * Returns true if every character in the parameter is an uppercase letter + * in the current locale. + * + * @param string $string The string to test. + * @param string $charset The charset to use when testing the string. + * + * @return boolean True if the parameter was uppercase. + */ + function isUpper($string, $charset = null) + { + return ((String::upper($string, true, $charset) === $string) && + String::isAlpha($string, $charset)); + } + + /** + * Performs a multibyte safe regex match search on the text provided. + * + * @since Horde 3.1 + * + * @param string $text The text to search. + * @param array $regex The regular expressions to use, without perl + * regex delimiters (e.g. '/' or '|'). + * @param string $charset The character set of the text. + * + * @return array The matches array from the first regex that matches. + */ + function regexMatch($text, $regex, $charset = null) + { + if (!empty($charset)) { + $regex = String::convertCharset($regex, $charset, 'utf-8'); + $text = String::convertCharset($text, $charset, 'utf-8'); + } + + $matches = array(); + foreach ($regex as $val) { + if (preg_match('/' . $val . '/u', $text, $matches)) { + break; + } + } + + if (!empty($charset)) { + $matches = String::convertCharset($matches, 'utf-8', $charset); + } + + return $matches; + } + + /** + * Workaround charsets that don't work with mbstring functions. + * + * @access private + * + * @param string $charset The original charset. + * + * @return string The charset to use with mbstring functions. + */ + function _mbstringCharset($charset) + { + /* mbstring functions do not handle the 'ks_c_5601-1987' & + * 'ks_c_5601-1989' charsets. However, these charsets are used, for + * example, by various versions of Outlook to send Korean characters. + * Use UHC (CP949) encoding instead. See, e.g., + * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */ + if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) { + $charset = 'UHC'; + } + + return $charset; + } + +} + + + +/** + * @package Horde_iCalendar + */ + +/** + * String package + */ + + + +/** + * Class representing iCalendar files. + * + * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar { + + /** + * The parent (containing) iCalendar object. + * + * @var Horde_iCalendar + */ + var $_container = false; + + /** + * The name/value pairs of attributes for this object (UID, + * DTSTART, etc.). Which are present depends on the object and on + * what kind of component it is. + * + * @var array + */ + var $_attributes = array(); + + /** + * Any children (contained) iCalendar components of this object. + * + * @var array + */ + var $_components = array(); + + /** + * According to RFC 2425, we should always use CRLF-terminated lines. + * + * @var string + */ + var $_newline = "\r\n"; + + /** + * iCalendar format version (different behavior for 1.0 and 2.0 + * especially with recurring events). + * + * @var string + */ + var $_version; + + function Horde_iCalendar($version = '2.0') + { + $this->_version = $version; + $this->setAttribute('VERSION', $version); + } + + /** + * Return a reference to a new component. + * + * @param string $type The type of component to return + * @param Horde_iCalendar $container A container that this component + * will be associated with. + * + * @return object Reference to a Horde_iCalendar_* object as specified. + * + * @static + */ + function &newComponent($type, &$container) + { + $type = String::lower($type); + $class = 'Horde_iCalendar_' . $type; + if (!class_exists($class)) { + include 'Horde/iCalendar/' . $type . '.php'; + } + if (class_exists($class)) { + $component = new $class(); + if ($container !== false) { + $component->_container = &$container; + // Use version of container, not default set by component + // constructor. + $component->_version = $container->_version; + } + } else { + // Should return an dummy x-unknown type class here. + $component = false; + } + + return $component; + } + + /** + * Sets the value of an attribute. + * + * @param string $name The name of the attribute. + * @param string $value The value of the attribute. + * @param array $params Array containing any addition parameters for + * this attribute. + * @param boolean $append True to append the attribute, False to replace + * the first matching attribute found. + * @param array $values Array representation of $value. For + * comma/semicolon seperated lists of values. If + * not set use $value as single array element. + */ + function setAttribute($name, $value, $params = array(), $append = true, + $values = false) + { + // Make sure we update the internal format version if + // setAttribute('VERSION', ...) is called. + if ($name == 'VERSION') { + $this->_version = $value; + if ($this->_container !== false) { + $this->_container->_version = $value; + } + } + + if (!$values) { + $values = array($value); + } + $found = false; + if (!$append) { + foreach (array_keys($this->_attributes) as $key) { + if ($this->_attributes[$key]['name'] == String::upper($name)) { + $this->_attributes[$key]['params'] = $params; + $this->_attributes[$key]['value'] = $value; + $this->_attributes[$key]['values'] = $values; + $found = true; + break; + } + } + } + + if ($append || !$found) { + $this->_attributes[] = array( + 'name' => String::upper($name), + 'params' => $params, + 'value' => $value, + 'values' => $values + ); + } + } + + /** + * Sets parameter(s) for an (already existing) attribute. The + * parameter set is merged into the existing set. + * + * @param string $name The name of the attribute. + * @param array $params Array containing any additional parameters for + * this attribute. + * @return boolean True on success, false if no attribute $name exists. + */ + function setParameter($name, $params = array()) + { + $keys = array_keys($this->_attributes); + foreach ($keys as $key) { + if ($this->_attributes[$key]['name'] == $name) { + $this->_attributes[$key]['params'] = + array_merge($this->_attributes[$key]['params'], $params); + return true; + } + } + + return false; + } + + /** + * Get the value of an attribute. + * + * @param string $name The name of the attribute. + * @param boolean $params Return the parameters for this attribute instead + * of its value. + * + * @return mixed (object) PEAR_Error if the attribute does not exist. + * (string) The value of the attribute. + * (array) The parameters for the attribute or + * multiple values for an attribute. + */ + function getAttribute($name, $params = false) + { + $result = array(); + foreach ($this->_attributes as $attribute) { + if ($attribute['name'] == $name) { + if ($params) { + $result[] = $attribute['params']; + } else { + $result[] = $attribute['value']; + } + } + } + if (!count($result)) { + require_once 'PEAR.php'; + return PEAR::raiseError('Attribute "' . $name . '" Not Found'); + } if (count($result) == 1 && !$params) { + return $result[0]; + } else { + return $result; + } + } + + /** + * Gets the values of an attribute as an array. Multiple values + * are possible due to: + * + * a) multiplce occurences of 'name' + * b) (unsecapd) comma seperated lists. + * + * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY') + * will return array('a', 'b', 'c'). + * + * @param string $name The name of the attribute. + * @return mixed (object) PEAR_Error if the attribute does not exist. + * (array) Multiple values for an attribute. + */ + function getAttributeValues($name) + { + $result = array(); + foreach ($this->_attributes as $attribute) { + if ($attribute['name'] == $name) { + $result = array_merge($attribute['values'], $result); + } + } + if (!count($result)) { + return PEAR::raiseError('Attribute "' . $name . '" Not Found'); + } + return $result; + } + + /** + * Returns the value of an attribute, or a specified default value + * if the attribute does not exist. + * + * @param string $name The name of the attribute. + * @param mixed $default What to return if the attribute specified by + * $name does not exist. + * + * @return mixed (string) The value of $name. + * (mixed) $default if $name does not exist. + */ + function getAttributeDefault($name, $default = '') + { + $value = $this->getAttribute($name); + return is_a($value, 'PEAR_Error') ? $default : $value; + } + + /** + * Remove all occurences of an attribute. + * + * @param string $name The name of the attribute. + */ + function removeAttribute($name) + { + $keys = array_keys($this->_attributes); + foreach ($keys as $key) { + if ($this->_attributes[$key]['name'] == $name) { + unset($this->_attributes[$key]); + } + } + } + + /** + * Get attributes for all tags or for a given tag. + * + * @param string $tag Return attributes for this tag, or all attributes if + * not given. + * + * @return array An array containing all the attributes and their types. + */ + function getAllAttributes($tag = false) + { + if ($tag === false) { + return $this->_attributes; + } + $result = array(); + foreach ($this->_attributes as $attribute) { + if ($attribute['name'] == $tag) { + $result[] = $attribute; + } + } + return $result; + } + + /** + * Add a vCalendar component (eg vEvent, vTimezone, etc.). + * + * @param Horde_iCalendar $component Component (subclass) to add. + */ + function addComponent($component) + { + if (is_a($component, 'Horde_iCalendar')) { + $component->_container = &$this; + $this->_components[] = &$component; + } + } + + /** + * Retrieve all the components. + * + * @return array Array of Horde_iCalendar objects. + */ + function getComponents() + { + return $this->_components; + } + + function getType() + { + return 'vcalendar'; + } + + /** + * Return the classes (entry types) we have. + * + * @return array Hash with class names Horde_iCalendar_xxx as keys + * and number of components of this class as value. + */ + function getComponentClasses() + { + $r = array(); + foreach ($this->_components as $c) { + $cn = strtolower(get_class($c)); + if (empty($r[$cn])) { + $r[$cn] = 1; + } else { + $r[$cn]++; + } + } + + return $r; + } + + /** + * Number of components in this container. + * + * @return integer Number of components in this container. + */ + function getComponentCount() + { + return count($this->_components); + } + + /** + * Retrieve a specific component. + * + * @param integer $idx The index of the object to retrieve. + * + * @return mixed (boolean) False if the index does not exist. + * (Horde_iCalendar_*) The requested component. + */ + function getComponent($idx) + { + if (isset($this->_components[$idx])) { + return $this->_components[$idx]; + } else { + return false; + } + } + + /** + * Locates the first child component of the specified class, and returns a + * reference to it. + * + * @param string $type The type of component to find. + * + * @return boolean|Horde_iCalendar_* False if no subcomponent of the + * specified class exists or a reference + * to the requested component. + */ + function &findComponent($childclass) + { + $childclass = 'Horde_iCalendar_' . String::lower($childclass); + $keys = array_keys($this->_components); + foreach ($keys as $key) { + if (is_a($this->_components[$key], $childclass)) { + return $this->_components[$key]; + } + } + + $component = false; + return $component; + } + + /** + * Locates the first matching child component of the specified class, and + * returns a reference to it. + * + * @param string $childclass The type of component to find. + * @param string $attribute This attribute must be set in the component + * for it to match. + * @param string $value Optional value that $attribute must match. + * + * @return boolean|Horde_iCalendar_* False if no matching subcomponent of + * the specified class exists, or a + * reference to the requested component. + */ + function &findComponentByAttribute($childclass, $attribute, $value = null) + { + $childclass = 'Horde_iCalendar_' . String::lower($childclass); + $keys = array_keys($this->_components); + foreach ($keys as $key) { + if (is_a($this->_components[$key], $childclass)) { + $attr = $this->_components[$key]->getAttribute($attribute); + if (is_a($attr, 'PEAR_Error')) { + continue; + } + if ($value !== null && $value != $attr) { + continue; + } + return $this->_components[$key]; + } + } + + $component = false; + return $component; + } + + /** + * Clears the iCalendar object (resets the components and attributes + * arrays). + */ + function clear() + { + $this->_components = array(); + $this->_attributes = array(); + } + + /** + * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1. + * + * These 'old' formats are defined by www.imc.org. The 'new' (non-old) + * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445 + * respectively. + * + * @since Horde 3.1.2 + */ + function isOldFormat() + { + if ($this->getType() == 'vcard') { + return ($this->_version < 3); + } + if ($this->getType() == 'vNote') { + return ($this->_version < 2); + } + if ($this->_version >= 2) { + return false; + } + return true; + } + + /** + * Export as vCalendar format. + */ + function exportvCalendar() + { + // Default values. + $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN'; + $requiredAttributes['METHOD'] = 'PUBLISH'; + + foreach ($requiredAttributes as $name => $default_value) { + if (is_a($this->getattribute($name), 'PEAR_Error')) { + $this->setAttribute($name, $default_value); + } + } + + return $this->_exportvData('VCALENDAR'); + } + + /** + * Export this entry as a hash array with tag names as keys. + * + * @param boolean $paramsInKeys + * If false, the operation can be quite lossy as the + * parameters are ignored when building the array keys. + * So if you export a vcard with + * LABEL;TYPE=WORK:foo + * LABEL;TYPE=HOME:bar + * the resulting hash contains only one label field! + * If set to true, array keys look like 'LABEL;TYPE=WORK' + * @return array A hash array with tag names as keys. + */ + function toHash($paramsInKeys = false) + { + $hash = array(); + foreach ($this->_attributes as $a) { + $k = $a['name']; + if ($paramsInKeys && is_array($a['params'])) { + foreach ($a['params'] as $p => $v) { + $k .= ";$p=$v"; + } + } + $hash[$k] = $a['value']; + } + + return $hash; + } + + /** + * Parses a string containing vCalendar data. + * + * @todo This method doesn't work well at all, if $base is VCARD. + * + * @param string $text The data to parse. + * @param string $base The type of the base object. + * @param string $charset The encoding charset for $text. Defaults to + * utf-8 for new format, iso-8859-1 for old format. + * @param boolean $clear If true clears the iCal object before parsing. + * + * @return boolean True on successful import, false otherwise. + */ + function parsevCalendar($text, $base = 'VCALENDAR', $charset = null, + $clear = true) + { + if ($clear) { + $this->clear(); + } + if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) { + $container = true; + $vCal = $matches[1]; + } else { + // Text isn't enclosed in BEGIN:VCALENDAR + // .. END:VCALENDAR. We'll try to parse it anyway. + $container = false; + $vCal = $text; + } + $vCal = trim($vCal); + + // Extract all subcomponents. + $matches = $components = null; + if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) { + foreach ($components[0] as $key => $data) { + // Remove from the vCalendar data. + $vCal = str_replace($data, '', $vCal); + } + } elseif (!$container) { + return false; + } + + // Unfold "quoted printable" folded lines like: + // BODY;ENCODING=QUOTED-PRINTABLE:= + // another=20line= + // last=20line + while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) { + foreach ($matches[1] as $s) { + $r = preg_replace('/=\r?\n/', '', $s); + $vCal = str_replace($s, $r, $vCal); + } + } + + // Unfold any folded lines. + if ($this->isOldFormat()) { + $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal); + } else { + $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal); + } + + // Parse the remaining attributes. + if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) { + foreach ($matches[0] as $attribute) { + preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts); + $tag = trim(String::upper($parts[1])); + $value = $parts[4]; + $params = array(); + + // Parse parameters. + if (!empty($parts[2])) { + preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts); + foreach ($param_parts[2] as $key => $paramName) { + $paramName = String::upper($paramName); + $paramValue = $param_parts[4][$key]; + if ($paramName == 'TYPE') { + $paramValue = preg_split('/(? $tmp) { + if (preg_match('/"([^"]*)"/', $tmp, $parts)) { + $paramValue[$k] = $parts[1]; + } + } + } + $params[$paramName] = $paramValue; + } + } + + // Charset and encoding handling. + if ((isset($params['ENCODING']) && + String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') || + isset($params['QUOTED-PRINTABLE'])) { + + $value = quoted_printable_decode($value); + if (isset($params['CHARSET'])) { + $value = String::convertCharset($value, $params['CHARSET']); + } else { + $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset); + } + } elseif (isset($params['CHARSET'])) { + $value = String::convertCharset($value, $params['CHARSET']); + } else { + // As per RFC 2279, assume UTF8 if we don't have an + // explicit charset parameter. + $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset); + } + + // Get timezone info for date fields from $params. + $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false; + + switch ($tag) { + // Date fields. + case 'COMPLETED': + case 'CREATED': + case 'LAST-MODIFIED': + case 'X-MOZ-LASTACK': + case 'X-MOZ-SNOOZE-TIME': + $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); + break; + + case 'BDAY': + case 'X-SYNCJE-ANNIVERSARY': + case 'X-ANNIVERSARY': + $this->setAttribute($tag, $this->_parseDate($value), $params); + break; + + case 'DTEND': + case 'DTSTART': + case 'DTSTAMP': + case 'DUE': + case 'AALARM': + case 'RECURRENCE-ID': + // types like AALARM may contain additional data after a ; + // ignore these. + $ts = explode(';', $value); + if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') { + $this->setAttribute($tag, $this->_parseDate($ts[0]), $params); + } else { + $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params); + } + break; + + case 'TRIGGER': + if (isset($params['VALUE']) && + $params['VALUE'] == 'DATE-TIME') { + $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); + } else { + $this->setAttribute($tag, $this->_parseDuration($value), $params); + } + break; + + // Comma seperated dates. + case 'EXDATE': + case 'RDATE': + if (!strlen($value)) { + break; + } + $dates = array(); + $separator = $this->isOldFormat() ? ';' : ','; + preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values); + + foreach ($values[1] as $value) { + $dates[] = $this->_parseDate($value); + } + $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates); + break; + + // Duration fields. + case 'DURATION': + $this->setAttribute($tag, $this->_parseDuration($value), $params); + break; + + // Period of time fields. + case 'FREEBUSY': + $periods = array(); + preg_match_all('/,([^,]*)/', ',' . $value, $values); + foreach ($values[1] as $value) { + $periods[] = $this->_parsePeriod($value); + } + + $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods); + break; + + // UTC offset fields. + case 'TZOFFSETFROM': + case 'TZOFFSETTO': + $this->setAttribute($tag, $this->_parseUtcOffset($value), $params); + break; + + // Integer fields. + case 'PERCENT-COMPLETE': + case 'PRIORITY': + case 'REPEAT': + case 'SEQUENCE': + $this->setAttribute($tag, intval($value), $params); + break; + + // Geo fields. + case 'GEO': + if ($this->isOldFormat()) { + $floats = explode(',', $value); + $value = array('latitude' => floatval($floats[1]), + 'longitude' => floatval($floats[0])); + } else { + $floats = explode(';', $value); + $value = array('latitude' => floatval($floats[0]), + 'longitude' => floatval($floats[1])); + } + $this->setAttribute($tag, $value, $params); + break; + + // Recursion fields. + case 'EXRULE': + case 'RRULE': + $this->setAttribute($tag, trim($value), $params); + break; + + // ADR, ORG and N are lists seperated by unescaped semicolons + // with a specific number of slots. + case 'ADR': + case 'N': + case 'ORG': + $value = trim($value); + // As of rfc 2426 2.4.2 semicolon, comma, and colon must + // be escaped (comma is unescaped after splitting below). + $value = str_replace(array('\\n', '\\N', '\\;', '\\:'), + array($this->_newline, $this->_newline, ';', ':'), + $value); + + // Split by unescaped semicolons: + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + break; + + // String fields. + default: + if ($this->isOldFormat()) { + // vCalendar 1.0 and vCard 2.1 only escape semicolons + // and use unescaped semicolons to create lists. + $value = trim($value); + // Split by unescaped semicolons: + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + } else { + $value = trim($value); + // As of rfc 2426 2.4.2 semicolon, comma, and colon + // must be escaped (comma is unescaped after splitting + // below). + $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'), + array($this->_newline, $this->_newline, ';', ':', '\\'), + $value); + + // Split by unescaped commas. + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + } + break; + } + } + } + + // Process all components. + if ($components) { + // vTimezone components are processed first. They are + // needed to process vEvents that may use a TZID. + foreach ($components[0] as $key => $data) { + $type = trim($components[1][$key]); + if ($type != 'VTIMEZONE') { + continue; + } + $component = &Horde_iCalendar::newComponent($type, $this); + if ($component === false) { + return PEAR::raiseError("Unable to create object for type $type"); + } + $component->parsevCalendar($data, $type, $charset); + + $this->addComponent($component); + } + + // Now process the non-vTimezone components. + foreach ($components[0] as $key => $data) { + $type = trim($components[1][$key]); + if ($type == 'VTIMEZONE') { + continue; + } + $component = &Horde_iCalendar::newComponent($type, $this); + if ($component === false) { + return PEAR::raiseError("Unable to create object for type $type"); + } + $component->parsevCalendar($data, $type, $charset); + + $this->addComponent($component); + } + } + + return true; + } + + /** + * Export this component in vCal format. + * + * @param string $base The type of the base object. + * + * @return string vCal format data. + */ + function _exportvData($base = 'VCALENDAR') + { + $result = 'BEGIN:' . String::upper($base) . $this->_newline; + + // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR, + // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445 + if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' && + $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') { + // Ensure that version is the first attribute. + $result .= 'VERSION:' . $this->_version . $this->_newline; + } + foreach ($this->_attributes as $attribute) { + $name = $attribute['name']; + if ($name == 'VERSION') { + // Already done. + continue; + } + + $params_str = ''; + $params = $attribute['params']; + if ($params) { + foreach ($params as $param_name => $param_value) { + /* Skip CHARSET for iCalendar 2.0 data, not allowed. */ + if ($param_name == 'CHARSET' && !$this->isOldFormat()) { + continue; + } + /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */ + if ($this->isOldFormat() && + $param_name == 'VALUE' && $param_value == 'DATE') { + continue; + } + + if ($param_value === null) { + $params_str .= ";$param_name"; + } else { + $len = strlen($param_value); + $safe_value = ''; + $quote = false; + for ($i = 0; $i < $len; ++$i) { + $ord = ord($param_value[$i]); + // Accept only valid characters. + if ($ord == 9 || $ord == 32 || $ord == 33 || + ($ord >= 35 && $ord <= 126) || + $ord >= 128) { + $safe_value .= $param_value[$i]; + // Characters above 128 do not need to be + // quoted as per RFC2445 but Outlook requires + // this. + if ($ord == 44 || $ord == 58 || $ord == 59 || + $ord >= 128) { + $quote = true; + } + } + } + if ($quote) { + $safe_value = '"' . $safe_value . '"'; + } + $params_str .= ";$param_name=$safe_value"; + } + } + } + + $value = $attribute['value']; + switch ($name) { + // Date fields. + case 'COMPLETED': + case 'CREATED': + case 'DCREATED': + case 'LAST-MODIFIED': + case 'X-MOZ-LASTACK': + case 'X-MOZ-SNOOZE-TIME': + $value = $this->_exportDateTime($value); + break; + + case 'DTEND': + case 'DTSTART': + case 'DTSTAMP': + case 'DUE': + case 'AALARM': + case 'RECURRENCE-ID': + if (isset($params['VALUE'])) { + if ($params['VALUE'] == 'DATE') { + // VCALENDAR 1.0 uses T000000 - T235959 for all day events: + if ($this->isOldFormat() && $name == 'DTEND') { + $d = new Horde_Date($value); + $value = new Horde_Date(array( + 'year' => $d->year, + 'month' => $d->month, + 'mday' => $d->mday - 1)); + $value->correct(); + $value = $this->_exportDate($value, '235959'); + } else { + $value = $this->_exportDate($value, '000000'); + } + } else { + $value = $this->_exportDateTime($value); + } + } else { + $value = $this->_exportDateTime($value); + } + break; + + // Comma seperated dates. + case 'EXDATE': + case 'RDATE': + $dates = array(); + foreach ($value as $date) { + if (isset($params['VALUE'])) { + if ($params['VALUE'] == 'DATE') { + $dates[] = $this->_exportDate($date, '000000'); + } elseif ($params['VALUE'] == 'PERIOD') { + $dates[] = $this->_exportPeriod($date); + } else { + $dates[] = $this->_exportDateTime($date); + } + } else { + $dates[] = $this->_exportDateTime($date); + } + } + $value = implode($this->isOldFormat() ? ';' : ',', $dates); + break; + + case 'TRIGGER': + if (isset($params['VALUE'])) { + if ($params['VALUE'] == 'DATE-TIME') { + $value = $this->_exportDateTime($value); + } elseif ($params['VALUE'] == 'DURATION') { + $value = $this->_exportDuration($value); + } + } else { + $value = $this->_exportDuration($value); + } + break; + + // Duration fields. + case 'DURATION': + $value = $this->_exportDuration($value); + break; + + // Period of time fields. + case 'FREEBUSY': + $value_str = ''; + foreach ($value as $period) { + $value_str .= empty($value_str) ? '' : ','; + $value_str .= $this->_exportPeriod($period); + } + $value = $value_str; + break; + + // UTC offset fields. + case 'TZOFFSETFROM': + case 'TZOFFSETTO': + $value = $this->_exportUtcOffset($value); + break; + + // Integer fields. + case 'PERCENT-COMPLETE': + case 'PRIORITY': + case 'REPEAT': + case 'SEQUENCE': + $value = "$value"; + break; + + // Geo fields. + case 'GEO': + if ($this->isOldFormat()) { + $value = $value['longitude'] . ',' . $value['latitude']; + } else { + $value = $value['latitude'] . ';' . $value['longitude']; + } + break; + + // Recurrence fields. + case 'EXRULE': + case 'RRULE': + break; + + default: + if ($this->isOldFormat()) { + if (is_array($attribute['values']) && + count($attribute['values']) > 1) { + $values = $attribute['values']; + if ($name == 'N' || $name == 'ADR' || $name == 'ORG') { + $glue = ';'; + } else { + $glue = ','; + } + $values = str_replace(';', '\\;', $values); + $value = implode($glue, $values); + } else { + /* vcard 2.1 and vcalendar 1.0 escape only + * semicolons */ + $value = str_replace(';', '\\;', $value); + } + // Text containing newlines or ASCII >= 127 must be BASE64 + // or QUOTED-PRINTABLE encoded. Currently we use + // QUOTED-PRINTABLE as default. + if (preg_match("/[^\x20-\x7F]/", $value) && + empty($params['ENCODING'])) { + $params['ENCODING'] = 'QUOTED-PRINTABLE'; + $params_str .= ';ENCODING=QUOTED-PRINTABLE'; + // Add CHARSET as well. At least the synthesis client + // gets confused otherwise + if (empty($params['CHARSET'])) { + $params['CHARSET'] = 'UTF-8'; + $params_str .= ';CHARSET=' . $params['CHARSET']; + } + } + } else { + if (is_array($attribute['values']) && + count($attribute['values'])) { + $values = $attribute['values']; + if ($name == 'N' || $name == 'ADR' || $name == 'ORG') { + $glue = ';'; + } else { + $glue = ','; + } + // As of rfc 2426 2.5 semicolon and comma must be + // escaped. + $values = str_replace(array('\\', ';', ','), + array('\\\\', '\\;', '\\,'), + $values); + $value = implode($glue, $values); + } else { + // As of rfc 2426 2.5 semicolon and comma must be + // escaped. + $value = str_replace(array('\\', ';', ','), + array('\\\\', '\\;', '\\,'), + $value); + } + $value = preg_replace('/\r?\n/', '\n', $value); + } + break; + } + + $value = str_replace("\r", '', $value); + if (!empty($params['ENCODING']) && + $params['ENCODING'] == 'QUOTED-PRINTABLE' && + strlen(trim($value))) { + $result .= $name . $params_str . ':' + . str_replace('=0A', '=0D=0A', + $this->_quotedPrintableEncode($value)) + . $this->_newline; + } else { + $attr_string = $name . $params_str . ':' . $value; + if (!$this->isOldFormat()) { + $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ', + true, 'utf-8', true); + } + $result .= $attr_string . $this->_newline; + } + } + + foreach ($this->_components as $component) { + $result .= $component->exportvCalendar(); + } + + return $result . 'END:' . $base . $this->_newline; + } + + /** + * Parse a UTC Offset field. + */ + function _parseUtcOffset($text) + { + $offset = array(); + if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) { + $offset['ahead'] = (bool)($timeParts[1] == '+'); + $offset['hour'] = intval($timeParts[2]); + $offset['minute'] = intval($timeParts[3]); + if (isset($timeParts[4])) { + $offset['second'] = intval($timeParts[4]); + } + return $offset; + } else { + return false; + } + } + + /** + * Export a UTC Offset field. + */ + function _exportUtcOffset($value) + { + $offset = $value['ahead'] ? '+' : '-'; + $offset .= sprintf('%02d%02d', + $value['hour'], $value['minute']); + if (isset($value['second'])) { + $offset .= sprintf('%02d', $value['second']); + } + + return $offset; + } + + /** + * Parse a Time Period field. + */ + function _parsePeriod($text) + { + $periodParts = explode('/', $text); + + $start = $this->_parseDateTime($periodParts[0]); + + if ($duration = $this->_parseDuration($periodParts[1])) { + return array('start' => $start, 'duration' => $duration); + } elseif ($end = $this->_parseDateTime($periodParts[1])) { + return array('start' => $start, 'end' => $end); + } + } + + /** + * Export a Time Period field. + */ + function _exportPeriod($value) + { + $period = $this->_exportDateTime($value['start']); + $period .= '/'; + if (isset($value['duration'])) { + $period .= $this->_exportDuration($value['duration']); + } else { + $period .= $this->_exportDateTime($value['end']); + } + return $period; + } + + /** + * Grok the TZID and return an offset in seconds from UTC for this + * date and time. + */ + function _parseTZID($date, $time, $tzid) + { + $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid); + if (!$vtimezone) { + return false; + } + + $change_times = array(); + foreach ($vtimezone->getComponents() as $o) { + $t = $vtimezone->parseChild($o, $date['year']); + if ($t !== false) { + $change_times[] = $t; + } + } + + if (!$change_times) { + return false; + } + + sort($change_times); + + // Time is arbitrarily based on UTC for comparison. + $t = @gmmktime($time['hour'], $time['minute'], $time['second'], + $date['month'], $date['mday'], $date['year']); + + if ($t < $change_times[0]['time']) { + return $change_times[0]['from']; + } + + for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) { + if (($t >= $change_times[$i]['time']) && + ($t < $change_times[$i + 1]['time'])) { + return $change_times[$i]['to']; + } + } + + if ($t >= $change_times[$n - 1]['time']) { + return $change_times[$n - 1]['to']; + } + + return false; + } + + /** + * Parses a DateTime field and returns a unix timestamp. If the + * field cannot be parsed then the original text is returned + * unmodified. + * + * @todo This function should be moved to Horde_Date and made public. + */ + function _parseDateTime($text, $tzid = false) + { + $dateParts = explode('T', $text); + if (count($dateParts) != 2 && !empty($text)) { + // Not a datetime field but may be just a date field. + if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) { + // Or not + return $text; + } + $newtext = $text.'T000000'; + $dateParts = explode('T', $newtext); + } + + if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) { + return $text; + } + if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) { + return $text; + } + + // Get timezone info for date fields from $tzid and container. + $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar')) + ? $this->_parseTZID($date, $time, $tzid) : false; + if ($time['zone'] == 'UTC' || $tzoffset !== false) { + $result = @gmmktime($time['hour'], $time['minute'], $time['second'], + $date['month'], $date['mday'], $date['year']); + if ($tzoffset) { + $result -= $tzoffset; + } + } else { + // We don't know the timezone so assume local timezone. + // FIXME: shouldn't this be based on the user's timezone + // preference rather than the server's timezone? + $result = @mktime($time['hour'], $time['minute'], $time['second'], + $date['month'], $date['mday'], $date['year']); + } + + return ($result !== false) ? $result : $text; + } + + /** + * Export a DateTime field. + */ + function _exportDateTime($value) + { + $temp = array(); + if (!is_object($value) && !is_array($value)) { + $tz = date('O', $value); + $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2)); + $value -= $TZOffset; + + $temp['zone'] = 'UTC'; + list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value)); + } else { + $dateOb = new Horde_Date($value); + return Horde_iCalendar::_exportDateTime($dateOb->timestamp()); + } + + return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp); + } + + /** + * Parses a Time field. + * + * @static + */ + function _parseTime($text) + { + if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) { + $time['hour'] = intval($timeParts[1]); + $time['minute'] = intval($timeParts[2]); + $time['second'] = intval($timeParts[3]); + if (isset($timeParts[4])) { + $time['zone'] = 'UTC'; + } else { + $time['zone'] = 'Local'; + } + return $time; + } else { + return false; + } + } + + /** + * Exports a Time field. + */ + function _exportTime($value) + { + $time = sprintf('%02d%02d%02d', + $value['hour'], $value['minute'], $value['second']); + if ($value['zone'] == 'UTC') { + $time .= 'Z'; + } + return $time; + } + + /** + * Parses a Date field. + * + * @static + */ + function _parseDate($text) + { + $parts = explode('T', $text); + if (count($parts) == 2) { + $text = $parts[0]; + } + + if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) { + return false; + } + + return array('year' => $match[1], + 'month' => $match[2], + 'mday' => $match[3]); + } + + /** + * Exports a date field. + * + * @param object|array $value Date object or hash. + * @param string $autoconvert If set, use this as time part to export the + * date as datetime when exporting to Vcalendar + * 1.0. Examples: '000000' or '235959' + */ + function _exportDate($value, $autoconvert = false) + { + if (is_object($value)) { + $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday); + } + if ($autoconvert !== false && $this->isOldFormat()) { + return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert); + } else { + return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']); + } + } + + /** + * Parse a Duration Value field. + */ + function _parseDuration($text) + { + if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) { + // Weeks. + $duration = 7 * 86400 * intval($durvalue[3]); + + if (count($durvalue) > 4) { + // Days. + $duration += 86400 * intval($durvalue[4]); + } + if (count($durvalue) > 5) { + // Hours. + $duration += 3600 * intval($durvalue[7]); + + // Mins. + if (isset($durvalue[8])) { + $duration += 60 * intval($durvalue[8]); + } + + // Secs. + if (isset($durvalue[9])) { + $duration += intval($durvalue[9]); + } + } + + // Sign. + if ($durvalue[1] == "-") { + $duration *= -1; + } + + return $duration; + } else { + return false; + } + } + + /** + * Export a duration value. + */ + function _exportDuration($value) + { + $duration = ''; + if ($value < 0) { + $value *= -1; + $duration .= '-'; + } + $duration .= 'P'; + + $weeks = floor($value / (7 * 86400)); + $value = $value % (7 * 86400); + if ($weeks) { + $duration .= $weeks . 'W'; + } + + $days = floor($value / (86400)); + $value = $value % (86400); + if ($days) { + $duration .= $days . 'D'; + } + + if ($value) { + $duration .= 'T'; + + $hours = floor($value / 3600); + $value = $value % 3600; + if ($hours) { + $duration .= $hours . 'H'; + } + + $mins = floor($value / 60); + $value = $value % 60; + if ($mins) { + $duration .= $mins . 'M'; + } + + if ($value) { + $duration .= $value . 'S'; + } + } + + return $duration; + } + + /** + * Converts an 8bit string to a quoted-printable string according to RFC + * 2045, section 6.7. + * + * imap_8bit() does not apply all necessary rules. + * + * @param string $input The string to be encoded. + * + * @return string The quoted-printable encoded string. + */ + function _quotedPrintableEncode($input = '') + { + $output = $line = ''; + $len = strlen($input); + + for ($i = 0; $i < $len; ++$i) { + $ord = ord($input[$i]); + // Encode non-printable characters (rule 2). + if ($ord == 9 || + ($ord >= 32 && $ord <= 60) || + ($ord >= 62 && $ord <= 126)) { + $chunk = $input[$i]; + } else { + // Quoted printable encoding (rule 1). + $chunk = '=' . String::upper(sprintf('%02X', $ord)); + } + $line .= $chunk; + // Wrap long lines (rule 5) + if (strlen($line) + 1 > 76) { + $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true); + $newline = strrchr($line, "\r\n"); + if ($newline !== false) { + $output .= substr($line, 0, -strlen($newline) + 2); + $line = substr($newline, 2); + } else { + $output .= $line; + } + continue; + } + // Wrap at line breaks for better readability (rule 4). + if (substr($line, -3) == '=0A') { + $output .= $line . "=\r\n"; + $line = ''; + } + } + $output .= $line; + + // Trailing whitespace must be encoded (rule 3). + $lastpos = strlen($output) - 1; + if ($output[$lastpos] == chr(9) || + $output[$lastpos] == chr(32)) { + $output[$lastpos] = '='; + $output .= String::upper(sprintf('%02X', ord($output[$lastpos]))); + } + + return $output; + } + +} + + + +/** + * Class representing vAlarms. + * + * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar_valarm extends Horde_iCalendar { + + function getType() + { + return 'vAlarm'; + } + + function exportvCalendar() + { + return parent::_exportvData('VALARM'); + } + +} + +/** + * Class representing vEvents. + * + * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar_vevent extends Horde_iCalendar { + + function getType() + { + return 'vEvent'; + } + + function exportvCalendar() + { + // Default values. + $requiredAttributes = array(); + $requiredAttributes['DTSTAMP'] = time(); + $requiredAttributes['UID'] = $this->_exportDateTime(time()) + . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16) + . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost'); + + $method = !empty($this->_container) ? + $this->_container->getAttribute('METHOD') : 'PUBLISH'; + + switch ($method) { + case 'PUBLISH': + $requiredAttributes['DTSTART'] = time(); + $requiredAttributes['SUMMARY'] = ''; + break; + + case 'REQUEST': + $requiredAttributes['ATTENDEE'] = ''; + $requiredAttributes['DTSTART'] = time(); + $requiredAttributes['SUMMARY'] = ''; + break; + + case 'REPLY': + $requiredAttributes['ATTENDEE'] = ''; + break; + + case 'ADD': + $requiredAttributes['DTSTART'] = time(); + $requiredAttributes['SEQUENCE'] = 1; + $requiredAttributes['SUMMARY'] = ''; + break; + + case 'CANCEL': + $requiredAttributes['ATTENDEE'] = ''; + $requiredAttributes['SEQUENCE'] = 1; + break; + + case 'REFRESH': + $requiredAttributes['ATTENDEE'] = ''; + break; + } + + foreach ($requiredAttributes as $name => $default_value) { + if (is_a($this->getAttribute($name), 'PEAR_Error')) { + $this->setAttribute($name, $default_value); + } + } + + return parent::_exportvData('VEVENT'); + } + + /** + * Update the status of an attendee of an event. + * + * @param $email The email address of the attendee. + * @param $status The participant status to set. + * @param $fullname The full name of the participant to set. + */ + function updateAttendee($email, $status, $fullname = '') + { + foreach ($this->_attributes as $key => $attribute) { + if ($attribute['name'] == 'ATTENDEE' && + $attribute['value'] == 'mailto:' . $email) { + $this->_attributes[$key]['params']['PARTSTAT'] = $status; + if (!empty($fullname)) { + $this->_attributes[$key]['params']['CN'] = $fullname; + } + unset($this->_attributes[$key]['params']['RSVP']); + return; + } + } + $params = array('PARTSTAT' => $status); + if (!empty($fullname)) { + $params['CN'] = $fullname; + } + $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params); + } + + /** + * Return the organizer display name or email. + * + * @return string The organizer name to display for this event. + */ + function organizerName() + { + $organizer = $this->getAttribute('ORGANIZER', true); + if (is_a($organizer, 'PEAR_Error')) { + return _("An unknown person"); + } + + if (isset($organizer[0]['CN'])) { + return $organizer[0]['CN']; + } + + $organizer = parse_url($this->getAttribute('ORGANIZER')); + + return $organizer['path']; + } + + /** + * Update this event with details from another event. + * + * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details. + */ + function updateFromvEvent($vevent) + { + $newAttributes = $vevent->getAllAttributes(); + foreach ($newAttributes as $newAttribute) { + $currentValue = $this->getAttribute($newAttribute['name']); + if (is_a($currentValue, 'PEAR_error')) { + // Already exists so just add it. + $this->setAttribute($newAttribute['name'], + $newAttribute['value'], + $newAttribute['params']); + } else { + // Already exists so locate and modify. + $found = false; + + // Try matching the attribte name and value incase + // only the params changed (eg attendee updating + // status). + foreach ($this->_attributes as $id => $attr) { + if ($attr['name'] == $newAttribute['name'] && + $attr['value'] == $newAttribute['value']) { + // merge the params + foreach ($newAttribute['params'] as $param_id => $param_name) { + $this->_attributes[$id]['params'][$param_id] = $param_name; + } + $found = true; + break; + } + } + if (!$found) { + // Else match the first attribute with the same + // name (eg changing start time). + foreach ($this->_attributes as $id => $attr) { + if ($attr['name'] == $newAttribute['name']) { + $this->_attributes[$id]['value'] = $newAttribute['value']; + // Merge the params. + foreach ($newAttribute['params'] as $param_id => $param_name) { + $this->_attributes[$id]['params'][$param_id] = $param_name; + } + break; + } + } + } + } + } + } + + /** + * Update just the attendess of event with details from another + * event. + * + * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details + */ + function updateAttendeesFromvEvent($vevent) + { + $newAttributes = $vevent->getAllAttributes(); + foreach ($newAttributes as $newAttribute) { + if ($newAttribute['name'] != 'ATTENDEE') { + continue; + } + $currentValue = $this->getAttribute($newAttribute['name']); + if (is_a($currentValue, 'PEAR_error')) { + // Already exists so just add it. + $this->setAttribute($newAttribute['name'], + $newAttribute['value'], + $newAttribute['params']); + } else { + // Already exists so locate and modify. + $found = false; + // Try matching the attribte name and value incase + // only the params changed (eg attendee updating + // status). + foreach ($this->_attributes as $id => $attr) { + if ($attr['name'] == $newAttribute['name'] && + $attr['value'] == $newAttribute['value']) { + // Merge the params. + foreach ($newAttribute['params'] as $param_id => $param_name) { + $this->_attributes[$id]['params'][$param_id] = $param_name; + } + $found = true; + break; + } + } + + if (!$found) { + // Else match the first attribute with the same + // name (eg changing start time). + foreach ($this->_attributes as $id => $attr) { + if ($attr['name'] == $newAttribute['name']) { + $this->_attributes[$id]['value'] = $newAttribute['value']; + // Merge the params. + foreach ($newAttribute['params'] as $param_id => $param_name) { + $this->_attributes[$id]['params'][$param_id] = $param_name; + } + break; + } + } + } + } + } + } + +} + +/** + * Class representing vFreebusy components. + * + * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @todo Don't use timestamps + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar_vfreebusy extends Horde_iCalendar { + + var $_busyPeriods = array(); + var $_extraParams = array(); + + /** + * Returns the type of this calendar component. + * + * @return string The type of this component. + */ + function getType() + { + return 'vFreebusy'; + } + + /** + * Parses a string containing vFreebusy data. + * + * @param string $data The data to parse. + */ + function parsevCalendar($data, $type = null, $charset = null) + { + parent::parsevCalendar($data, 'VFREEBUSY', $charset); + + // Do something with all the busy periods. + foreach ($this->_attributes as $key => $attribute) { + if ($attribute['name'] != 'FREEBUSY') { + continue; + } + foreach ($attribute['values'] as $value) { + $params = isset($attribute['params']) + ? $attribute['params'] + : array(); + if (isset($value['duration'])) { + $this->addBusyPeriod('BUSY', $value['start'], null, + $value['duration'], $params); + } else { + $this->addBusyPeriod('BUSY', $value['start'], + $value['end'], null, $params); + } + } + unset($this->_attributes[$key]); + } + } + + /** + * Returns the component exported as string. + * + * @return string The exported vFreeBusy information according to the + * iCalender format specification. + */ + function exportvCalendar() + { + foreach ($this->_busyPeriods as $start => $end) { + $periods = array(array('start' => $start, 'end' => $end)); + $this->setAttribute('FREEBUSY', $periods, + isset($this->_extraParams[$start]) + ? $this->_extraParams[$start] : array()); + } + + $res = parent::_exportvData('VFREEBUSY'); + + foreach ($this->_attributes as $key => $attribute) { + if ($attribute['name'] == 'FREEBUSY') { + unset($this->_attributes[$key]); + } + } + + return $res; + } + + /** + * Returns a display name for this object. + * + * @return string A clear text name for displaying this object. + */ + function getName() + { + $name = ''; + $method = !empty($this->_container) ? + $this->_container->getAttribute('METHOD') : 'PUBLISH'; + + if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') { + $attr = 'ORGANIZER'; + } elseif ($method == 'REPLY') { + $attr = 'ATTENDEE'; + } + + $name = $this->getAttribute($attr, true); + if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) { + return $name[0]['CN']; + } + + $name = $this->getAttribute($attr); + if (is_a($name, 'PEAR_Error')) { + return ''; + } else { + $name = parse_url($name); + return $name['path']; + } + } + + /** + * Returns the email address for this object. + * + * @return string The email address of this object's owner. + */ + function getEmail() + { + $name = ''; + $method = !empty($this->_container) + ? $this->_container->getAttribute('METHOD') : 'PUBLISH'; + + if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') { + $attr = 'ORGANIZER'; + } elseif ($method == 'REPLY') { + $attr = 'ATTENDEE'; + } + + $name = $this->getAttribute($attr); + if (is_a($name, 'PEAR_Error')) { + return ''; + } else { + $name = parse_url($name); + return $name['path']; + } + } + + /** + * Returns the busy periods. + * + * @return array All busy periods. + */ + function getBusyPeriods() + { + return $this->_busyPeriods; + } + + /** + * Returns any additional freebusy parameters. + * + * @return array Additional parameters of the freebusy periods. + */ + function getExtraParams() + { + return $this->_extraParams; + } + + /** + * Returns all the free periods of time in a given period. + * + * @param integer $startStamp The start timestamp. + * @param integer $endStamp The end timestamp. + * + * @return array A hash with free time periods, the start times as the + * keys and the end times as the values. + */ + function getFreePeriods($startStamp, $endStamp) + { + $this->simplify(); + $periods = array(); + + // Check that we have data for some part of this period. + if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) { + return $periods; + } + + // Locate the first time in the requested period we have data for. + $nextstart = max($startStamp, $this->getStart()); + + // Check each busy period and add free periods in between. + foreach ($this->_busyPeriods as $start => $end) { + if ($start <= $endStamp && $end >= $nextstart) { + if ($nextstart <= $start) { + $periods[$nextstart] = min($start, $endStamp); + } + $nextstart = min($end, $endStamp); + } + } + + // If we didn't read the end of the requested period but still have + // data then mark as free to the end of the period or available data. + if ($nextstart < $endStamp && $nextstart < $this->getEnd()) { + $periods[$nextstart] = min($this->getEnd(), $endStamp); + } + + return $periods; + } + + /** + * Adds a busy period to the info. + * + * This function may throw away data in case you add a period with a start + * date that already exists. The longer of the two periods will be chosen + * (and all information associated with the shorter one will be removed). + * + * @param string $type The type of the period. Either 'FREE' or + * 'BUSY'; only 'BUSY' supported at the moment. + * @param integer $start The start timestamp of the period. + * @param integer $end The end timestamp of the period. + * @param integer $duration The duration of the period. If specified, the + * $end parameter will be ignored. + * @param array $extra Additional parameters for this busy period. + */ + function addBusyPeriod($type, $start, $end = null, $duration = null, + $extra = array()) + { + if ($type == 'FREE') { + // Make sure this period is not marked as busy. + return false; + } + + // Calculate the end time if duration was specified. + $tempEnd = is_null($duration) ? $end : $start + $duration; + + // Make sure the period length is always positive. + $end = max($start, $tempEnd); + $start = min($start, $tempEnd); + + if (isset($this->_busyPeriods[$start])) { + // Already a period starting at this time. Change the current + // period only if the new one is longer. This might be a problem + // if the callee assumes that there is no simplification going + // on. But since the periods are stored using the start time of + // the busy periods we have to throw away data here. + if ($end > $this->_busyPeriods[$start]) { + $this->_busyPeriods[$start] = $end; + $this->_extraParams[$start] = $extra; + } + } else { + // Add a new busy period. + $this->_busyPeriods[$start] = $end; + $this->_extraParams[$start] = $extra; + } + + return true; + } + + /** + * Returns the timestamp of the start of the time period this free busy + * information covers. + * + * @return integer A timestamp. + */ + function getStart() + { + if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) { + return $this->getAttribute('DTSTART'); + } elseif (count($this->_busyPeriods)) { + return min(array_keys($this->_busyPeriods)); + } else { + return false; + } + } + + /** + * Returns the timestamp of the end of the time period this free busy + * information covers. + * + * @return integer A timestamp. + */ + function getEnd() + { + if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) { + return $this->getAttribute('DTEND'); + } elseif (count($this->_busyPeriods)) { + return max(array_values($this->_busyPeriods)); + } else { + return false; + } + } + + /** + * Merges the busy periods of another Horde_iCalendar_vfreebusy object + * into this one. + * + * This might lead to simplification no matter what you specify for the + * "simplify" flag since periods with the same start date will lead to the + * shorter period being removed (see addBusyPeriod). + * + * @param Horde_iCalendar_vfreebusy $freebusy A freebusy object. + * @param boolean $simplify If true, simplify() will + * called after the merge. + */ + function merge($freebusy, $simplify = true) + { + if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) { + return false; + } + + $extra = $freebusy->getExtraParams(); + foreach ($freebusy->getBusyPeriods() as $start => $end) { + // This might simplify the busy periods without taking the + // "simplify" flag into account. + $this->addBusyPeriod('BUSY', $start, $end, null, + isset($extra[$start]) + ? $extra[$start] : array()); + } + + $thisattr = $this->getAttribute('DTSTART'); + $thatattr = $freebusy->getAttribute('DTSTART'); + if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) { + $this->setAttribute('DTSTART', $thatattr, array(), false); + } elseif (!is_a($thatattr, 'PEAR_Error')) { + if ($thatattr < $thisattr) { + $this->setAttribute('DTSTART', $thatattr, array(), false); + } + } + + $thisattr = $this->getAttribute('DTEND'); + $thatattr = $freebusy->getAttribute('DTEND'); + if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) { + $this->setAttribute('DTEND', $thatattr, array(), false); + } elseif (!is_a($thatattr, 'PEAR_Error')) { + if ($thatattr > $thisattr) { + $this->setAttribute('DTEND', $thatattr, array(), false); + } + } + + if ($simplify) { + $this->simplify(); + } + + return true; + } + + /** + * Removes all overlaps and simplifies the busy periods array as much as + * possible. + */ + function simplify() + { + $clean = false; + $busy = array($this->_busyPeriods, $this->_extraParams); + while (!$clean) { + $result = $this->_simplify($busy[0], $busy[1]); + $clean = $result === $busy; + $busy = $result; + } + + ksort($result[1], SORT_NUMERIC); + $this->_extraParams = $result[1]; + + ksort($result[0], SORT_NUMERIC); + $this->_busyPeriods = $result[0]; + } + + function _simplify($busyPeriods, $extraParams = array()) + { + $checked = array(); + $checkedExtra = array(); + $checkedEmpty = true; + + foreach ($busyPeriods as $start => $end) { + if ($checkedEmpty) { + $checked[$start] = $end; + $checkedExtra[$start] = isset($extraParams[$start]) + ? $extraParams[$start] : array(); + $checkedEmpty = false; + } else { + $added = false; + foreach ($checked as $testStart => $testEnd) { + // Replace old period if the new period lies around the + // old period. + if ($start <= $testStart && $end >= $testEnd) { + // Remove old period entry. + unset($checked[$testStart]); + unset($checkedExtra[$testStart]); + // Add replacing entry. + $checked[$start] = $end; + $checkedExtra[$start] = isset($extraParams[$start]) + ? $extraParams[$start] : array(); + $added = true; + } elseif ($start >= $testStart && $end <= $testEnd) { + // The new period lies fully within the old + // period. Just forget about it. + $added = true; + } elseif (($end <= $testEnd && $end >= $testStart) || + ($start >= $testStart && $start <= $testEnd)) { + // Now we are in trouble: Overlapping time periods. If + // we allow for additional parameters we cannot simply + // choose one of the two parameter sets. It's better + // to leave two separated time periods. + $extra = isset($extraParams[$start]) + ? $extraParams[$start] : array(); + $testExtra = isset($checkedExtra[$testStart]) + ? $checkedExtra[$testStart] : array(); + // Remove old period entry. + unset($checked[$testStart]); + unset($checkedExtra[$testStart]); + // We have two periods overlapping. Are their + // additional parameters the same or different? + $newStart = min($start, $testStart); + $newEnd = max($end, $testEnd); + if ($extra === $testExtra) { + // Both periods have the same information. So we + // can just merge. + $checked[$newStart] = $newEnd; + $checkedExtra[$newStart] = $extra; + } else { + // Extra parameters are different. Create one + // period at the beginning with the params of the + // first period and create a trailing period with + // the params of the second period. The break + // point will be the end of the first period. + $break = min($end, $testEnd); + $checked[$newStart] = $break; + $checkedExtra[$newStart] = + isset($extraParams[$newStart]) + ? $extraParams[$newStart] : array(); + $checked[$break] = $newEnd; + $highStart = max($start, $testStart); + $checkedExtra[$break] = + isset($extraParams[$highStart]) + ? $extraParams[$highStart] : array(); + + // Ensure we also have the extra data in the + // extraParams. + $extraParams[$break] = + isset($extraParams[$highStart]) + ? $extraParams[$highStart] : array(); + } + $added = true; + } + + if ($added) { + break; + } + } + + if (!$added) { + $checked[$start] = $end; + $checkedExtra[$start] = isset($extraParams[$start]) + ? $extraParams[$start] : array(); + } + } + } + + return array($checked, $checkedExtra); + } + +} + +/** + * Class representing vJournals. + * + * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar_vjournal extends Horde_iCalendar { + + function getType() + { + return 'vJournal'; + } + + function exportvCalendar() + { + return parent::_exportvData('VJOURNAL'); + } + +} + + + + +/** + * Class representing vNotes. + * + * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @author Karsten Fourmont + * @package Horde_iCalendar + */ +class Horde_iCalendar_vnote extends Horde_iCalendar { + + function Horde_iCalendar_vnote($version = '1.1') + { + return parent::Horde_iCalendar($version); + } + + function getType() + { + return 'vNote'; + } + + /** + * Unlike vevent and vtodo, a vnote is normally not enclosed in an + * iCalendar container. (BEGIN..END) + */ + function exportvCalendar() + { + $requiredAttributes['BODY'] = ''; + $requiredAttributes['VERSION'] = '1.1'; + + foreach ($requiredAttributes as $name => $default_value) { + if (is_a($this->getattribute($name), 'PEAR_Error')) { + $this->setAttribute($name, $default_value); + } + } + + return $this->_exportvData('VNOTE'); + } + +} + +/** + * Class representing vTimezones. + * + * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar_vtimezone extends Horde_iCalendar { + + function getType() + { + return 'vTimeZone'; + } + + function exportvCalendar() + { + return parent::_exportvData('VTIMEZONE'); + } + + /** + * Parse child components of the vTimezone component. Returns an + * array with the exact time of the time change as well as the + * 'from' and 'to' offsets around the change. Time is arbitrarily + * based on UTC for comparison. + */ + function parseChild(&$child, $year) + { + // Make sure 'time' key is first for sort(). + $result['time'] = 0; + + $t = $child->getAttribute('TZOFFSETFROM'); + if (is_a($t, 'PEAR_Error')) { + return false; + } + $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1); + + $t = $child->getAttribute('TZOFFSETTO'); + if (is_a($t, 'PEAR_Error')) { + return false; + } + $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1); + + $switch_time = $child->getAttribute('DTSTART'); + if (is_a($switch_time, 'PEAR_Error')) { + return false; + } + + $rrules = $child->getAttribute('RRULE'); + if (is_a($rrules, 'PEAR_Error')) { + if (!is_int($switch_time)) { + return false; + } + // Convert this timestamp from local time to UTC for + // comparison (All dates are compared as if they are UTC). + $t = getdate($switch_time); + $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'], + $t['mon'], $t['mday'], $t['year']); + return $result; + } + + $rrules = explode(';', $rrules); + foreach ($rrules as $rrule) { + $t = explode('=', $rrule); + switch ($t[0]) { + case 'FREQ': + if ($t[1] != 'YEARLY') { + return false; + } + break; + + case 'INTERVAL': + if ($t[1] != '1') { + return false; + } + break; + + case 'BYMONTH': + $month = intval($t[1]); + break; + + case 'BYDAY': + $len = strspn($t[1], '1234567890-+'); + if ($len == 0) { + return false; + } + $weekday = substr($t[1], $len); + $weekdays = array( + 'SU' => 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6 + ); + $weekday = $weekdays[$weekday]; + $which = intval(substr($t[1], 0, $len)); + break; + + case 'UNTIL': + if (intval($year) > intval(substr($t[1], 0, 4))) { + return false; + } + break; + } + } + + if (empty($month) || !isset($weekday)) { + return false; + } + + if (is_int($switch_time)) { + // Was stored as localtime. + $switch_time = strftime('%H:%M:%S', $switch_time); + $switch_time = explode(':', $switch_time); + } else { + $switch_time = explode('T', $switch_time); + if (count($switch_time) != 2) { + return false; + } + $switch_time[0] = substr($switch_time[1], 0, 2); + $switch_time[2] = substr($switch_time[1], 4, 2); + $switch_time[1] = substr($switch_time[1], 2, 2); + } + + // Get the timestamp for the first day of $month. + $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2], + $month, 1, $year); + // Get the day of the week for the first day of $month. + $first_of_month_weekday = intval(gmstrftime('%w', $when)); + + // Go to the first $weekday before first day of $month. + if ($weekday >= $first_of_month_weekday) { + $weekday -= 7; + } + $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24; + + // If going backwards go to the first $weekday after last day + // of $month. + if ($which < 0) { + do { + $when += 60*60*24*7; + } while (intval(gmstrftime('%m', $when)) == $month); + } + + // Calculate $weekday number $which. + $when += $which * 60 * 60 * 24 * 7; + + $result['time'] = $when; + + return $result; + } + +} + +/** + * @package Horde_iCalendar + */ +class Horde_iCalendar_standard extends Horde_iCalendar { + + function getType() + { + return 'standard'; + } + + function parsevCalendar($data) + { + parent::parsevCalendar($data, 'STANDARD'); + } + + function exportvCalendar() + { + return parent::_exportvData('STANDARD'); + } + +} + +/** + * @package Horde_iCalendar + */ +class Horde_iCalendar_daylight extends Horde_iCalendar { + + function getType() + { + return 'daylight'; + } + + function parsevCalendar($data) + { + parent::parsevCalendar($data, 'DAYLIGHT'); + } + + function exportvCalendar() + { + return parent::_exportvData('DAYLIGHT'); + } + +} + +/** + * Class representing vTodos. + * + * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $ + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Mike Cochrane + * @since Horde 3.0 + * @package Horde_iCalendar + */ +class Horde_iCalendar_vtodo extends Horde_iCalendar { + + function getType() + { + return 'vTodo'; + } + + function exportvCalendar() + { + return parent::_exportvData('VTODO'); + } + + /** + * Convert this todo to an array of attributes. + * + * @return array Array containing the details of the todo in a hash + * as used by Horde applications. + */ + function toArray() + { + $todo = array(); + + $name = $this->getAttribute('SUMMARY'); + if (!is_array($name) && !is_a($name, 'PEAR_Error')) { + $todo['name'] = $name; + } + $desc = $this->getAttribute('DESCRIPTION'); + if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) { + $todo['desc'] = $desc; + } + + $priority = $this->getAttribute('PRIORITY'); + if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) { + $todo['priority'] = $priority; + } + + $due = $this->getAttribute('DTSTAMP'); + if (!is_array($due) && !is_a($due, 'PEAR_Error')) { + $todo['due'] = $due; + } + + return $todo; + } + + /** + * Set the attributes for this todo item from an array. + * + * @param array $todo Array containing the details of the todo in + * the same format that toArray() exports. + */ + function fromArray($todo) + { + if (isset($todo['name'])) { + $this->setAttribute('SUMMARY', $todo['name']); + } + if (isset($todo['desc'])) { + $this->setAttribute('DESCRIPTION', $todo['desc']); + } + + if (isset($todo['priority'])) { + $this->setAttribute('PRIORITY', $todo['priority']); + } + + if (isset($todo['due'])) { + $this->setAttribute('DTSTAMP', $todo['due']); + } + } + +} diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index c99fab35..dd613724 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -124,7 +124,7 @@ class calendar_ical private function get_parser() { // use Horde:iCalendar to parse vcalendar file format - require_once 'Horde/iCalendar.php'; + require_once($this->cal->home . '/lib/Horde_iCalendar.php'); // set target charset for parsed events $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET; diff --git a/plugins/calendar/lib/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh new file mode 100755 index 00000000..d076af51 --- /dev/null +++ b/plugins/calendar/lib/get_horde_icalendar.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Copy Horde_iCalendar classes and dependencies to stdout. +# This will create a standalone copy of the classes requried for iCal parsing. + +SRCDIR=$1 + +if [ ! -d "$SRCDIR" ]; then + echo "Usage: get_horde_icalendar.sh SRCDIR" + echo "Please enter a valid source directory of the Horde lib" + exit 1 +fi + +echo "//' $SRCDIR/String.php +echo "\n" +sed 's///' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/" +echo "\n" + +for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do + sed 's///' $fn | sed -E "s/(include|require)_once.+//" +done; diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml index 96bbeb01..12844305 100644 --- a/plugins/calendar/package.xml +++ b/plugins/calendar/package.xml @@ -64,10 +64,9 @@ - - - - + + +