egroupware/phpgwapi/inc/horde/Horde/iCalendar.php
Klaus Leithoff cb8cd0f3e6 when having multiple lines descriptions in contacts or calendar, these are not correctly encoded in vcard.
The problem seems to be line [784] of trunk/phpgwapi/inc/horde/Horde/iCalendar.php
$value = str_replace($this->_newline, '\n', $value);
When removing this line, the description value is correct on the client.

I could not find any sideffects during my tests, if some clients have
problems with this, I assume this would then have to be fixed at a higher
level, because the current state with this line just causes broken output.

From wikipedia regarding Linebreaks in QuotedPrintable:
If the data being encoded contains meaningful line breaks, they must be encoded as an ASCII CR LF sequence, not as their original byte values

Vcard extract without the reported line code:
DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Desc 1=0D=0ADesk 2=0D=0A=0D=0A

Vcard extract with the reported line of code
DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Desc 1\nDesk 2\n\n

pointed out by Philip Herbert. Carl Knauber Holding GmbH & Co KG
2008-08-04 08:42:19 +00:00

1434 lines
47 KiB
PHP

<?php
/**
* Class representing iCalendar files.
*
* $Horde: framework/iCalendar/iCalendar.php,v 1.53 2004/09/24 03:34:43 chuck Exp $
*
* Copyright 2003-2004 Mike Cochrane <mike@graftonhall.co.nz>
*
* See the enclosed file COPYING for license information (LGPL). If you
* did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
*
* @author Mike Cochrane <mike@graftonhall.co.nz>
* @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('/(?<!\\\\);/',$value);
$value = str_replace('\\;', ';', $value);
$values = str_replace('\\;', ';', $values);
$this->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('/(?<!\\\\),/',$value);
$value = str_replace('\\,', ',', $value);
$values = str_replace('\\,', ',', $values);
$this->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<count($line_part); $e++)
{
$word = $line_part[$e];
if ($qp_mode and (strlen($word) > $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<count($line_part); $e++)
{
$word = $line_part[$e];
if ($qp_mode and (strlen($word) > $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;
}
}