Branch 'dev/kolab3' - plugins/calendar

Thomas Brüderli bruederli at kolabsys.com
Tue May 1 22:27:54 CEST 2012


 plugins/calendar/lib/Horde_Date.php            |  775 +++++
 plugins/calendar/lib/Horde_Date_Recurrence.php |  776 -----
 plugins/calendar/lib/Horde_iCalendar.php       | 3284 +++++++++++++++++++++++++
 plugins/calendar/lib/calendar_ical.php         |    2 
 plugins/calendar/lib/get_horde_icalendar.sh    |   24 
 plugins/calendar/package.xml                   |    7 
 6 files changed, 4088 insertions(+), 780 deletions(-)

New commits:
commit 584c4c9bc456eb99eebe24410d10e3169abb04cd
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 1 22:27:29 2012 +0200

    Drop dependency to external Horde libs. Add a local and independent copy of these files to the plugin package

diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
new file mode 100644
index 0000000..18709d9
--- /dev/null
+++ b/plugins/calendar/lib/Horde_Date.php
@@ -0,0 +1,775 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/Date.php, PEAR/Date/Calc.php, Horde/Date/Recurrence.php
+ */
+
+define('HORDE_DATE_SUNDAY',    0);
+define('HORDE_DATE_MONDAY',    1);
+define('HORDE_DATE_TUESDAY',   2);
+define('HORDE_DATE_WEDNESDAY', 3);
+define('HORDE_DATE_THURSDAY',  4);
+define('HORDE_DATE_FRIDAY',    5);
+define('HORDE_DATE_SATURDAY',  6);
+
+define('HORDE_DATE_MASK_SUNDAY',    1);
+define('HORDE_DATE_MASK_MONDAY',    2);
+define('HORDE_DATE_MASK_TUESDAY',   4);
+define('HORDE_DATE_MASK_WEDNESDAY', 8);
+define('HORDE_DATE_MASK_THURSDAY', 16);
+define('HORDE_DATE_MASK_FRIDAY',   32);
+define('HORDE_DATE_MASK_SATURDAY', 64);
+define('HORDE_DATE_MASK_WEEKDAYS', 62);
+define('HORDE_DATE_MASK_WEEKEND',  65);
+define('HORDE_DATE_MASK_ALLDAYS', 127);
+
+define('HORDE_DATE_MASK_SECOND',    1);
+define('HORDE_DATE_MASK_MINUTE',    2);
+define('HORDE_DATE_MASK_HOUR',      4);
+define('HORDE_DATE_MASK_DAY',       8);
+define('HORDE_DATE_MASK_MONTH',    16);
+define('HORDE_DATE_MASK_YEAR',     32);
+define('HORDE_DATE_MASK_ALLPARTS', 63);
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * $Horde: framework/Date/Date.php,v 1.8.10.18 2008/09/17 08:46:04 jan Exp $
+ *
+ * @package Horde_Date
+ */
+class Horde_Date {
+
+    /**
+     * Year
+     *
+     * @var integer
+     */
+    var $year;
+
+    /**
+     * Month
+     *
+     * @var integer
+     */
+    var $month;
+
+    /**
+     * Day
+     *
+     * @var integer
+     */
+    var $mday;
+
+    /**
+     * Hour
+     *
+     * @var integer
+     */
+    var $hour = 0;
+
+    /**
+     * Minute
+     *
+     * @var integer
+     */
+    var $min = 0;
+
+    /**
+     * Second
+     *
+     * @var integer
+     */
+    var $sec = 0;
+
+    /**
+     * Internally supported strftime() specifiers.
+     *
+     * @var string
+     */
+    var $_supportedSpecs = '%CdDeHImMnRStTyY';
+
+    /**
+     * Build a new date object. If $date contains date parts, use them to
+     * initialize the object.
+     *
+     * Recognized formats:
+     * - arrays with keys 'year', 'month', 'mday', 'day' (since Horde 3.2),
+     *   'hour', 'min', 'minute' (since Horde 3.2), 'sec'
+     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+     * - yyyy-mm-dd hh:mm:ss (since Horde 3.1)
+     * - yyyymmddhhmmss (since Horde 3.1)
+     * - yyyymmddThhmmssZ (since Horde 3.1.4)
+     * - unix timestamps
+     */
+    function Horde_Date($date = null)
+    {
+        if (function_exists('nl_langinfo')) {
+            $this->_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 . "<br />\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 68340ba..fbf1d1e 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -1,780 +1,6 @@
 <?php
 
-/**
- * This is a concatenated copy of the following files:
- *   Horde/Date.php, PEAR/Date/Calc.php, Horde/Date/Recurrence.php
- */
-
-define('HORDE_DATE_SUNDAY',    0);
-define('HORDE_DATE_MONDAY',    1);
-define('HORDE_DATE_TUESDAY',   2);
-define('HORDE_DATE_WEDNESDAY', 3);
-define('HORDE_DATE_THURSDAY',  4);
-define('HORDE_DATE_FRIDAY',    5);
-define('HORDE_DATE_SATURDAY',  6);
-
-define('HORDE_DATE_MASK_SUNDAY',    1);
-define('HORDE_DATE_MASK_MONDAY',    2);
-define('HORDE_DATE_MASK_TUESDAY',   4);
-define('HORDE_DATE_MASK_WEDNESDAY', 8);
-define('HORDE_DATE_MASK_THURSDAY', 16);
-define('HORDE_DATE_MASK_FRIDAY',   32);
-define('HORDE_DATE_MASK_SATURDAY', 64);
-define('HORDE_DATE_MASK_WEEKDAYS', 62);
-define('HORDE_DATE_MASK_WEEKEND',  65);
-define('HORDE_DATE_MASK_ALLDAYS', 127);
-
-define('HORDE_DATE_MASK_SECOND',    1);
-define('HORDE_DATE_MASK_MINUTE',    2);
-define('HORDE_DATE_MASK_HOUR',      4);
-define('HORDE_DATE_MASK_DAY',       8);
-define('HORDE_DATE_MASK_MONTH',    16);
-define('HORDE_DATE_MASK_YEAR',     32);
-define('HORDE_DATE_MASK_ALLPARTS', 63);
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * $Horde: framework/Date/Date.php,v 1.8.10.18 2008/09/17 08:46:04 jan Exp $
- *
- * @package Horde_Date
- */
-class Horde_Date {
-
-    /**
-     * Year
-     *
-     * @var integer
-     */
-    var $year;
-
-    /**
-     * Month
-     *
-     * @var integer
-     */
-    var $month;
-
-    /**
-     * Day
-     *
-     * @var integer
-     */
-    var $mday;
-
-    /**
-     * Hour
-     *
-     * @var integer
-     */
-    var $hour = 0;
-
-    /**
-     * Minute
-     *
-     * @var integer
-     */
-    var $min = 0;
-
-    /**
-     * Second
-     *
-     * @var integer
-     */
-    var $sec = 0;
-
-    /**
-     * Internally supported strftime() specifiers.
-     *
-     * @var string
-     */
-    var $_supportedSpecs = '%CdDeHImMnRStTyY';
-
-    /**
-     * Build a new date object. If $date contains date parts, use them to
-     * initialize the object.
-     *
-     * Recognized formats:
-     * - arrays with keys 'year', 'month', 'mday', 'day' (since Horde 3.2),
-     *   'hour', 'min', 'minute' (since Horde 3.2), 'sec'
-     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
-     * - yyyy-mm-dd hh:mm:ss (since Horde 3.1)
-     * - yyyymmddhhmmss (since Horde 3.1)
-     * - yyyymmddThhmmssZ (since Horde 3.1.4)
-     * - unix timestamps
-     */
-    function Horde_Date($date = null)
-    {
-        if (function_exists('nl_langinfo')) {
-            $this->_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 . "<br />\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 0000000..5ba0f4c
--- /dev/null
+++ b/plugins/calendar/lib/Horde_iCalendar.php
@@ -0,0 +1,3284 @@
+<?php
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+
+$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
+
+/**
+ * The String:: class provides static methods for charset and locale safe
+ * string manipulation.
+ *
+ * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 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  Jan Schneider <jan at horde.org>
+ * @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 <mike at graftonhall.co.nz>
+ * @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('/(?<!\\\\),/', $paramValue);
+                            if (count($paramValue) == 1) {
+                                $paramValue = $paramValue[0];
+                            }
+                        }
+                        if (is_string($paramValue)) {
+                            if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
+                                $paramValue = $parts[1];
+                            }
+                        } else {
+                            foreach ($paramValue as $k => $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('/(?<!\\\\);/', $value);
+                    $value = str_replace('\\;', ';', $value);
+                    $values = str_replace('\\;', ';', $values);
+                    $this->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('/(?<!\\\\);/', $value);
+                        $value = str_replace('\\;', ';', $value);
+                        $values = str_replace('\\;', ';', $values);
+                        $this->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('/(?<!\\\\),/', $value);
+                        $value = str_replace('\\,', ',', $value);
+                        $values = str_replace('\\,', ',', $values);
+
+                        $this->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 <mike at graftonhall.co.nz>
+ * @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 <mike at graftonhall.co.nz>
+ * @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 <mike at graftonhall.co.nz>
+ * @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 <mike at graftonhall.co.nz>
+ * @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 <mike at graftonhall.co.nz>
+ * @author  Karsten Fourmont <fourmont at gmx.de>
+ * @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 <mike at graftonhall.co.nz>
+ * @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 <mike at graftonhall.co.nz>
+ * @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 c99fab3..dd61372 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 0000000..d076af5
--- /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 "<?php\n"
+echo "require_once(dirname(__FILE__) . '/Horde_Date.php');"
+
+sed 's/<?php//; s/?>//' $SRCDIR/String.php
+echo "\n"
+sed 's/<?php//; 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/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
+done;
diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml
index 96bbeb0..1284430 100644
--- a/plugins/calendar/package.xml
+++ b/plugins/calendar/package.xml
@@ -64,10 +64,9 @@
 				<tasks:replace from="@name@" to="name" type="package-info"/>
 				<tasks:replace from="@package_version@" to="version" type="package-info"/>
 			</file>
-			<file name="lib/Horde_Date_Recurrence.php" role="php">
-				<tasks:replace from="@name@" to="name" type="package-info"/>
-				<tasks:replace from="@package_version@" to="version" type="package-info"/>
-			</file>
+			<file name="lib/Horde_Date.php" role="php"></file>
+			<file name="lib/Horde_Date_Recurrence.php" role="php"></file>
+			<file name="lib/Horde_iCalendar.php" role="php"></file>
 			<file name="lib/fullcalendar-rc.patch" role="data">
 				<tasks:replace from="@name@" to="name" type="package-info"/>
 				<tasks:replace from="@package_version@" to="version" type="package-info"/>





More information about the commits mailing list