* * 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 * @version $Revision$ * @since Horde 3.0 * @package Horde_iCalendar */ class Horde_iCalendar { /** * The parent (containing) iCalendar object. * * @var object Horde_iCalendar $_container */ var $_container = false; var $_attributes = array(); var $_components = array(); /** * According to RFC 2425, we should always use CRLF-terminated * lines. * * @var string $_newline */ var $_newline = "\r\n"; /** * Return a reference to a new component. * * @param string $type The type of component to return * @param object $container A container that this component * will be associated with. * * @return object Reference to a Horde_iCalendar_* object as specified. */ function &newComponent($type, &$container) { # require_once 'Horde/String.php'; $type = strtolower($type); $class = 'Horde_iCalendar_' . strtolower($type); if (!class_exists($class)) { include_once dirname(__FILE__) . '/iCalendar/' . $type . '.php'; } if (class_exists($class)) { $component = &new $class(); if ($container !== false) { $component->_container = &$container; } return $component; } else { // Should return an dummy x-unknown type class here. return false; } } /** * Set the value of an attribute. * * @param string $name The name of the attribute. * @param string $value The value of the attribute. * @param array $params (optional) Array containing any addition * parameters for this attribute. * @param boolean $append (optional) True to append the attribute, False * to replace the first matching attribute found. * @param array $values (optional) 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) { $found = $append; if (!$values) { $values = array($value); } $keys = array_keys($this->_attributes); foreach ($keys as $key) { if ($found) break; if ($this->_attributes[$key]['name'] == $name) { $this->_attributes[$key]['params'] = $params; $this->_attributes[$key]['value'] = $value; $this->_attributes[$key]['values'] = $values; $found = true; } } if ($append || !$found) { $this->_attributes[] = array( 'name' => $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) { $keys = array_keys($this->_attributes); foreach ($keys as $key) { if ($this->_attributes[$key]['name'] == $name) { $this->_attributes[$key]['params'] = array_merge((array)$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) == 0) { 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 (optional) 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 (optional) return attributes for this tag. * or all attributes if not given * @return array 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 object 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; } /** * 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 this component. * * @param string $type The type of component to find. * * @return mixed (boolean) False if no subcomponent of the specified * class exists. * (Horde_iCalendar_*) A reference to the requested component. */ function &findComponent($childclass) { # require_once 'Horde/String.php'; # $childclass = 'Horde_iCalendar_' . String::lower($childclass); $childclass = 'Horde_iCalendar_' . strtolower($childclass); $keys = array_keys($this->_components); foreach ($keys as $key) { if (is_a($this->_components[$key], $childclass)) { return $this->_components[$key]; } } return false; } /** * Clears the iCalendar object (resets the components and * attributes arrays). */ function clear() { $this->_components = array(); $this->_attributes = array(); } /** * Export as vCalendar format. */ function exportvCalendar() { // Default values. $requiredAttributes['VERSION'] = '2.0'; $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library, Horde 3.0-cvs //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') . $this->_newline; } /** * Export this entry as a hash array with tag names as keys. * * @param boolean (optional) $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; } /** * Parse a string containing vCalendar data. * * @param string $text The data to parse. * @param string $base The type of the base object. * @param string $charset (optional) The encoding charset for $text. Defaults to utf-8 * @param boolean $clear (optional) True to clear() the iCal object before parsing. * * @return boolean True on successful import, false otherwise. */ function parsevCalendar($text, $base = 'VCALENDAR', $charset = 'utf-8', $clear = true) { if ($clear) { $this->clear(); } if (preg_match('/(BEGIN:' . $base . '\r?\n)([\W\w]*)(END:' . $base . '\r?\n?)/i', $text, $matches)) { $vCal = $matches[2]; } else { // Text isn't enclosed in BEGIN:VCALENDAR // .. END:VCALENDAR. We'll try to parse it anyway. $vCal = $text; } // All subcomponents. $matches = null; if (preg_match_all('/BEGIN:([\S]*)(\r\n|\r|\n)([\W\w]*)END:\1(\r\n|\r|\n)/U', $vCal, $matches)) { foreach ($matches[0] as $key => $data) { $type = $matches[1][$key]; $component = &Horde_iCalendar::newComponent(trim($type), $this); if ($component === false) { return PEAR::raiseError("Unable to create object for type $type"); } $component->parsevCalendar($data); $this->addComponent($component); // Remove from the vCalendar data. $vCal = str_replace($data, '', $vCal); } } // Unfold any folded lines. #$vCal = preg_replace ('/(\r|\n)+ /', ' ', $vCal); // Unfold "quoted printable" folded lines like: // BODY;ENCODING=QUOTED-PRINTABLE:= // another=20line= // last=20line # Horde::logMessage("SymcML: match 1", __FILE__, __LINE__, PEAR_LOG_DEBUG); # if (preg_match_all('/^([^:]+;\s*ENCODING=QUOTED-PRINTABLE(.*=[\r\n|\r|\n])+(.*[^=])[\r\n|\r|\n])/mU', $vCal, $matches)) { ## if (preg_match_all('/^(BODY;ENCODING=QUOTED-PRINTABLE(.*=\r\n)+(.*)?\r?\n)/mU', $vCal, $matches)) { # Horde::logMessage("SymcML: match 2", __FILE__, __LINE__, PEAR_LOG_DEBUG); # foreach ($matches[1] as $s) { # Horde::logMessage("SymcML: match 3 $s", __FILE__, __LINE__, PEAR_LOG_DEBUG); # $r = preg_replace('/=[\r\n|\r|\n]/', '', $s); # Horde::logMessage("SymcML: match 4 $r", __FILE__, __LINE__, PEAR_LOG_DEBUG); # $vCal = str_replace($s, $r, $vCal); # } # } // Unfold "quoted printable" folded lines like: // BODY;ENCODING=QUOTED-PRINTABLE:= // another=20line= // last=20line #if (preg_match_all('/^([^:]+;\s*ENCODING=QUOTED-PRINTABLE(.*=*\s))/mU', $vCal, $matches)) { # $matches = preg_split('/=+\s/',$vCal); # $vCal = implode('',$matches); #} if (preg_match_all('/^([^:]+;\s*ENCODING=QUOTED-PRINTABLE(.*=*\s))/mU', $vCal, $matches)) { $matches = preg_split('/=(\r\n|\r|\n)/',$vCal); $vCal = implode('',$matches); } // Parse the remaining attributes. if (preg_match_all('/(.*):([^\r\n]*)[\r\n]+/', $vCal, $matches)) { foreach ($matches[0] as $attribute) { preg_match('/([^;^:]*)((;[^:]*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts); $tag = $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) { $paramValue = $param_parts[4][$key]; $params[$paramName] = $paramValue; } } // Charset and encoding handling. if ((isset($params['ENCODING']) && $params['ENCODING'] == 'QUOTED-PRINTABLE') || isset($params['QUOTED-PRINTABLE'])) { $value = quoted_printable_decode($value); } if (isset($params['CHARSET'])) { $value = $GLOBALS['egw']->translation->convert($value, $params['CHARSET']); } else { // As per RFC 2279, assume UTF8 if we don't have // an explicit charset parameter. $value = $GLOBALS['egw']->translation->convert($value, 'utf-8'); } switch ($tag) { // Date fields. case 'DTSTAMP': case 'COMPLETED': case 'CREATED': case 'LAST-MODIFIED': case 'BDAY': case 'DTEND': case 'DTSTART': case 'DUE': case 'RECURRENCE-ID': $this->setAttribute($tag, $this->_parseDateTime($value), $params); break; case 'RDATE': if (isset($params['VALUE'])) { if ($params['VALUE'] == 'DATE') { $this->setAttribute($tag, $this->_parseDate($value), $params); } elseif ($params['VALUE'] == 'PERIOD') { $this->setAttribute($tag, $this->_parsePeriod($value), $params); } else { $this->setAttribute($tag, $this->_parseDateTime($value), $params); } } else { $this->setAttribute($tag, $this->_parseDateTime($value), $params); } break; case 'TRIGGER': if (isset($params['VALUE'])) { if ($params['VALUE'] == 'DATE-TIME') { $this->setAttribute($tag, $this->_parseDateTime($value), $params); } else { $this->setAttribute($tag, $this->_parseDuration($value), $params); } } else { $this->setAttribute($tag, $this->_parseDuration($value), $params); } break; // Comma and semicolon seperated dates. case 'EXDATE': $values = array(); $dates = array(); preg_match_all('/[,;]([^,;]*)/', ';' . $value, $values); foreach ($values[1] as $value) { if (isset($params['VALUE'])) { if ($params['VALUE'] == 'DATE-TIME') { $dates[] = $this->_parseDateTime($value); } elseif ($params['VALUE'] == 'DATE') { $dates[] = $this->_parseDate($value); } } else { $dates[] = $this->_parseDateTime($value); } } $this->setAttribute($tag, $dates, $params); break; // Duration fields. case 'DURATION': $this->setAttribute($tag, $this->_parseDuration($value), $params); break; // Period of time fields. case 'FREEBUSY': $values = array(); $periods = array(); preg_match_all('/[,;]([^,;]*)/', ';' . $value, $values); foreach ($values[1] as $value) { $periods[] = $this->_parsePeriod($value); } $this->setAttribute($tag, $periods, $params); 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': $floats = split(';', $value); $value['latitude'] = floatval($floats[0]); $value['longitude'] = floatval($floats[1]); $this->setAttribute($tag, $value, $params); break; // Recursion fields. case 'EXRULE': case 'RRULE': $this->setAttribute($tag, trim($value), $params); break; // ADR an N are lists seperated by unescaped semi-colons. case 'ADR': case 'N': case 'ORG': $value = trim($value); // As of rfc 2426 2.4.2 semi-colon, comma, and // colon must be escaped. $value = str_replace('\\n', $this->_newline, $value); $value = str_replace('\\,', ',', $value); $value = str_replace('\\:', ':', $value); // Split by unescaped semi-colons: $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); break; // String fields. default: $value = trim($value); // As of rfc 2426 2.4.2 semi-colon, comma, and // colon must be escaped. $value = str_replace('\\n', $this->_newline, $value); $value = str_replace('\\;', ';', $value); $value = str_replace('\\:', ':', $value); // Split by unescaped commas: $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); break; } } } return true; } /** * Export this component in vCal format. * * @param string $base (optional) The type of the base object. * * @return string vCal format data. */ function _exportvData($base = 'VCALENDAR') { $result = 'BEGIN:' . strtoupper($base) . $this->_newline; // Ensure that version is the first attribute. $v = $this->getAttributeDefault('VERSION', false); if ($v) { $result .= 'VERSION:' . $v. $this->_newline; } foreach ($this->_attributes as $attribute) { $name = $attribute['name']; if ($name == 'VERSION') { // Already done. continue; } $params = $attribute['params']; $params_str = ''; if (count($params)) { foreach ($params as $param_name => $param_value) { $params_str .= ";$param_name=$param_value"; } } $value = $attribute['value']; switch ($name) { // Date fields. case 'DTSTAMP': case 'COMPLETED': case 'CREATED': case 'DCREATED': case 'LAST-MODIFIED': $value = $this->_exportDateTime($value); break; case 'DTEND': case 'DTSTART': case 'DUE': case 'RECURRENCE-ID': if (isset($params['VALUE'])) { if ($params['VALUE'] == 'DATE') { $value = $this->_exportDate($value); } else { $value = $this->_exportDateTime($value); } } else { $value = $this->_exportDateTime($value); } break; case 'RDATE': if (isset($params['VALUE'])) { if ($params['VALUE'] == 'DATE') { $value = $this->_exportDate($value); } elseif ($params['VALUE'] == 'PERIOD') { $value = $this->_exportPeriod($value); } else { $value = $this->_exportDateTime($value); } } else { $value = $this->_exportDateTime($value); } 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': $value = $value['latitude'] . ',' . $value['longitude']; break; // Recurrence fields. case 'RRULE': break; case 'EXRULE': //Text Fields case 'SUMMARY': case 'DESCRIPTION': case 'COMMENT': $value = str_replace('\\', '\\\\', $value); #$value = str_replace($this->_newline, '\n', $value); $value = str_replace(',', '\,', $value); $value = str_replace(';', '\;', $value); $value = str_replace(':', '\:', $value); break; default: break; } if (!empty($params['ENCODING']) && $params['ENCODING'] == 'QUOTED-PRINTABLE' && strlen(trim($value)) > 0) { $value = str_replace("\r", '', $value); # $result .= "$name$params_str:=" . $this->_newline # . $this->_quotedPrintableEncode($value) # . $this->_newline; $result .= "$name$params_str:" . $this->_quotedPrintableEncode($value) . $this->_newline; } else { $attr_string = "$name$params_str:$value"; $result .= $this->_foldLine($attr_string) . $this->_newline; if (!empty($params['ENCODING']) && $params['ENCODING'] == 'BASE64' && strlen(trim($value)) > 0) { $result .= $this->_newline; } } } foreach ($this->getComponents() as $component) { $result .= $component->exportvCalendar() . $this->_newline; } $result .= 'END:' . $base; return $result; } /** * 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'] = (boolean)($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 = split('/', $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; } /** * Parse a DateTime field into a unix timestamp. */ function _parseDateTime($text) { $dateParts = split('T', $text); if (count($dateParts) != 2 && !empty($text)) { // Not a datetime field but may be just a date field. if (!$date = $this->_parseDate($text)) { return $date; } return @mktime(0, 0, 0, $date['month'], $date['mday'], $date['year']); } if (!$date = $this->_parseDate($dateParts[0])) { return $date; } if (!$time = $this->_parseTime($dateParts[1])) { return $time; } //error_log("parseDateTime: ".$text." => ".print_r($time, true)); if ($time['zone'] == 'UTC') { return @gmmktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']); } else { return @mktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']); } } /** * Export a DateTime field. */ function _exportDateTime($value) { $temp = array(); if (!is_object($value) || is_array($value)) { $TZOffset = 3600 * substr(date('O',$value), 0, 3); $TZOffset += 60 * substr(date('O',$value), 3, 2); $value -= $TZOffset; $temp['zone'] = 'UTC'; $temp['year'] = date('Y', $value); $temp['month'] = date('n', $value); $temp['mday'] = date('j', $value); $temp['hour'] = date('G', $value); $temp['minute'] = date('i', $value); $temp['second'] = date('s', $value); } else { $dateOb = (object)$value; $TZOffset = date('O',mktime($dateOb->hour,$dateOb->min,$dateOb->sec,$dateOb->month,$dateOb->mday,$dateOb->year)); // Minutes. $TZOffsetMin = substr($TZOffset, 0, 1) . substr($TZOffset, 3, 2); $thisMin = $dateOb->min - $TZOffsetMin; // Hours. $TZOffsetHour = substr($TZOffset, 0, 3); $thisHour = $dateOb->hour - $TZOffsetHour; if ($thisMin < 0) { $thisHour -= 1; $thisMin += 60; } if ($thisHour < 0) { require_once 'Date/Calc.php'; $prevday = Date_Calc::prevDay($dateOb->mday, $dateOb->month, $dateOb->year); $dateOb->mday = substr($prevday, 6, 2); $dateOb->month = substr($prevday, 4, 2); $dateOb->year = substr($prevday, 0, 4); $thisHour += 24; } $temp['zone'] = 'UTC'; $temp['year'] = $dateOb->year; $temp['month'] = $dateOb->month; $temp['mday'] = $dateOb->mday; $temp['hour'] = $thisHour; $temp['minute'] = $dateOb->min; $temp['second'] = $dateOb->sec; } return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp); } /** * Parse a Time field. */ 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; } } /** * Export 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; } /** * Parse a Date field. */ function _parseDate($text) { if (strlen($text) == 10) { $text = str_replace('-','',$text); } if (strlen($text) != 8) { return false; } $date['year'] = intval(substr($text, 0, 4)); $date['month'] = intval(substr($text, 4, 2)); $date['mday'] = intval(substr($text, 6, 2)); return $date; } /** * Export a Date field. */ function _exportDate($value) { 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; } /** * Return the folded version of a line. * JVL rewritten to fold on any ; or: or = if present before column 75 * this is still rfc2445 section 4.1 compliant */ function _foldLine($line) { $line = preg_replace("/\r\n|\n|\r/", '\n', $line); if (strlen($line) > 75) { $foldedline = ''; while (!empty($line)) { $maxLine = substr($line, 0, 75); $cutPoint = 1+max(is_numeric($p1 = strrpos($maxLine,';')) ? $p1 : -1, is_numeric($p1 = strrpos($maxLine,':')) ? $p1 : -1, is_numeric($p1 = strrpos($maxLine,'=')) ? $p1 : -1); if ($cutPoint < 1) // nothing found, then fold complete maxLine $cutPoint = 75; // now fold [0..(cutPoint-1)] $foldedline .= (empty($foldedline)) ? substr($line, 0, $cutPoint) : $this->_newline . ' ' . substr($line, 0, $cutPoint); $line = (strlen($line) <= $cutPoint) ? '' : substr($line, $cutPoint); if (strlen($line) < 75) { $foldedline .= $this->_newline . ' ' . $line; $line = ''; } } return $foldedline; } return $line; } /** * Convert an 8bit string to a quoted-printable string according * to RFC2045, section 6.7. * * Uses imap_8bit if available. * * @param string $input The string to be encoded. * * @return string The quoted-printable encoded string. */ function _quotedPrintableEncode($input = '') { return $this->EncodeQP($input); #$input = preg_replace('!(\r\n|\r|\n)!',"\n",$input); // If imap_8bit() is available, use it. if (function_exists('imap_8bit')) { $retValue = imap_8bit($input); #$retValue = preg_replace('/=0A/',"=0D=0A=\r\n",$retValue); return $retValue; } // Rather dumb replacment: just encode everything. $hex = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'); $output = ''; $len = strlen($input); for ($i = 0; $i < $len; ++$i) { $c = substr($input, $i, 1); $dec = ord($c); $output .= '=' . $hex[floor($dec / 16)] . $hex[floor($dec % 16)]; if (($i + 1) % 25 == 0) { $output .= "=\r\n"; } } return $output; } var $LE = "\r\n"; /** * Encode string to quoted-printable. * @access private * @return string */ function EncodeQP_old ($str) { $encoded = $this->FixEOL($str); #$encoded = $str; #if (substr($encoded, -(strlen($this->LE))) != $this->LE) # $encoded .= $this->LE; // Replace every high ascii, control and = characters #$encoded = preg_replace('/([\000-\010\013\014\016-\037\075\177-\377])/e', # "'='.sprintf('%02X', ord('\\1'))", $encoded); $encoded = preg_replace('/([\000-\012\015\016\020-\037\075\177-\377])/e', "'='.sprintf('%02X', ord('\\1'))", $encoded); // Replace every spaces and tabs when it's the last character on a line #$encoded = preg_replace("/([\011\040])".$this->LE."/e", # "'='.sprintf('%02X', ord('\\1')).'".$this->LE."'", $encoded); $encoded = preg_replace("/([\011\040])".$this->LE."/e", "'='.sprintf('%02X', ord('\\1')).'".$this->LE."'", $encoded); // Maximum line length of 76 characters before CRLF (74 + space + '=') $encoded = $this->WrapText($encoded, 74, true); return $encoded; } /** * Wraps message for use with mailers that do not * automatically perform wrapping and for quoted-printable. * Original written by philippe. * @access private * @return string */ function WrapText_old($message, $length, $qp_mode = false) { $soft_break = ($qp_mode) ? "=\r\n" : $this->LE; #$message = $this->FixEOL($message); if (substr($message, -1) == $this->LE) $message = substr($message, 0, -1); $line = explode("=0D=0A", $message); $message = ""; for ($i=0 ;$i < count($line); $i++) { $line_part = explode(" ", $line[$i]); $buf = ""; for ($e = 0; $e $length)) { $space_left = $length - strlen($buf) - 1; if ($e != 0) { if ($space_left > 20) { $len = $space_left; if (substr($word, $len - 1, 1) == "=") $len--; elseif (substr($word, $len - 2, 1) == "=") $len -= 2; $part = substr($word, 0, $len); $word = substr($word, $len); $buf .= " " . $part; $message .= $buf . sprintf("=%s", $this->LE); } else { $message .= $buf . $soft_break; } $buf = ""; } while (strlen($word) > 0) { $len = $length; if (substr($word, $len - 1, 1) == "=") $len--; elseif (substr($word, $len - 2, 1) == "=") $len -= 2; $part = substr($word, 0, $len); $word = substr($word, $len); if (strlen($word) > 0) $message .= $part . sprintf("=%s", $this->LE); else $buf = $part; } } else { $buf_o = $buf; $buf .= ($e == 0) ? $word : (" " . $word); if (strlen($buf) > $length and $buf_o != "") { $message .= $buf_o . $soft_break; $buf = $word; } } } $message .= $buf; if((count($line)-1) > $i) $message .= "=0D=0A=\r\n"; } return $message; } /** * Changes every end of line from CR or LF to CRLF. * @access private * @return string */ function FixEOL($str) { $str = str_replace("\r\n", "\n", $str); $str = str_replace("\r", "\n", $str); $str = str_replace("\n", $this->LE, $str); return $str; } /** * Encode string to quoted-printable. * @access private * @return string */ function EncodeQP ($str) { $encoded = $this->FixEOL($str); # see bug report http://sourceforge.net/tracker/index.php?func=detail&aid=1536674&group_id=78745&atid=554338 #if (substr($encoded, -(strlen($this->LE))) != $this->LE) # $encoded .= $this->LE; // Replace every high ascii, control and = characters #$encoded = preg_replace('/([\000-\010\013\014\016-\037\075\177-\377])/e', # "'='.sprintf('%02X', ord('\\1'))", $encoded); $encoded = preg_replace('/([\000-\012\015\016\020-\037\075\177-\377])/e', "'='.sprintf('%02X', ord('\\1'))", $encoded); // Replace every spaces and tabs when it's the last character on a line $encoded = preg_replace("/([\011\040])".$this->LE."/e", "'='.sprintf('%02X', ord('\\1')).'".$this->LE."'", $encoded); // Maximum line length of 76 characters before CRLF (74 + space + '=') #$encoded = $this->WrapText($encoded, 74, true); return $encoded; } /** * Wraps message for use with mailers that do not * automatically perform wrapping and for quoted-printable. * Original written by philippe. * @access private * @return string */ function WrapText($message, $length, $qp_mode = false) { $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE; $soft_break = "..="; $message = $this->FixEOL($message); if (substr($message, -1) == $this->LE) $message = substr($message, 0, -1); $line = explode($this->LE, $message); $message = ""; for ($i=0 ;$i < count($line); $i++) { $line_part = explode(" ", $line[$i]); $buf = ""; for ($e = 0; $e $length)) { $space_left = $length - strlen($buf) - 1; if ($e != 0) { if ($space_left > 20) { $len = $space_left; if (substr($word, $len - 1, 1) == "=") $len--; elseif (substr($word, $len - 2, 1) == "=") $len -= 2; $part = substr($word, 0, $len); $word = substr($word, $len); $buf .= " " . $part; $message .= $buf . sprintf("=%s", $this->LE); } else { $message .= $buf . $soft_break; } $buf = ""; } while (strlen($word) > 0) { $len = $length; if (substr($word, $len - 1, 1) == "=") $len--; elseif (substr($word, $len - 2, 1) == "=") $len -= 2; $part = substr($word, 0, $len); $word = substr($word, $len); if (strlen($word) > 0) $message .= $part . sprintf("=%s", $this->LE); else $buf = $part; } } else { $buf_o = $buf; $buf .= ($e == 0) ? $word : (" " . $word); if (strlen($buf) > $length and $buf_o != "") { $message .= $buf_o . $soft_break; $buf = $word; } } } $message .= $buf . $this->LE; } return $message; } }