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