1622 lines
55 KiB
PHP
1622 lines
55 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2003-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @author Mike Cochrane <mike@graftonhall.co.nz>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Icalendar
|
|
*/
|
|
|
|
/**
|
|
* Class representing iCalendar files.
|
|
*
|
|
* @author Mike Cochrane <mike@graftonhall.co.nz>
|
|
* @category Horde
|
|
* @copyright 2003-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Icalendar
|
|
*/
|
|
class Horde_Icalendar
|
|
{
|
|
/**
|
|
* The component type of this class.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $type = 'vcalendar';
|
|
|
|
/**
|
|
* The parent (containing) iCalendar object.
|
|
*
|
|
* @var Horde_Icalendar
|
|
*/
|
|
protected $_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
|
|
*/
|
|
protected $_attributes = array();
|
|
|
|
/**
|
|
* Any children (contained) iCalendar components of this object.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_components = array();
|
|
|
|
/**
|
|
* According to RFC 2425, we should always use CRLF-terminated lines.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_newline = "\r\n";
|
|
|
|
/**
|
|
* iCalendar format version (different behavior for 1.0 and 2.0 especially
|
|
* with recurring events).
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_version;
|
|
|
|
/**
|
|
* Whether 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.
|
|
*/
|
|
protected $_oldFormat = true;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @var string $version Version.
|
|
*/
|
|
public function __construct($version = '2.0')
|
|
{
|
|
$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.
|
|
*/
|
|
public static function newComponent($type, $container)
|
|
{
|
|
$type = Horde_String::lower($type);
|
|
$class = __CLASS__ . '_' . Horde_String::ucfirst($type);
|
|
|
|
if (class_exists($class)) {
|
|
$component = new $class();
|
|
if ($container !== false) {
|
|
$component->_container = $container;
|
|
// Use version of container, not default set by component
|
|
// constructor.
|
|
$component->setVersion($container->getAttribute('VERSION'));
|
|
}
|
|
} else {
|
|
// Should return an dummy x-unknown type class here.
|
|
$component = false;
|
|
}
|
|
|
|
return $component;
|
|
}
|
|
|
|
/**
|
|
* Sets the version of this component.
|
|
*
|
|
* @see $version
|
|
* @see $oldFormat
|
|
*
|
|
* @param string $version A float-like version string.
|
|
*/
|
|
public function setVersion($version)
|
|
{
|
|
$this->_oldFormat = $version < 2;
|
|
$this->_version = $version;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public 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->setVersion($value);
|
|
if ($this->_container !== false) {
|
|
$this->_container->setVersion($value);
|
|
}
|
|
}
|
|
|
|
if (!$values) {
|
|
$values = array($value);
|
|
}
|
|
$found = false;
|
|
|
|
if (!$append) {
|
|
foreach (array_keys($this->_attributes) as $key) {
|
|
if ($this->_attributes[$key]['name'] == Horde_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' => Horde_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.
|
|
*/
|
|
public 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 (string) The value of the attribute.
|
|
* (array) The parameters for the attribute or
|
|
* multiple values for an attribute.
|
|
* @throws Horde_Icalendar_Exception
|
|
*/
|
|
public function getAttribute($name, $params = false)
|
|
{
|
|
if ($name == 'VERSION') {
|
|
return $this->_version;
|
|
}
|
|
|
|
$result = array();
|
|
foreach ($this->_attributes as $attribute) {
|
|
if ($attribute['name'] == $name) {
|
|
$result[] = $params
|
|
? $attribute['params']
|
|
: $attribute['value'];
|
|
}
|
|
}
|
|
|
|
if (!count($result)) {
|
|
throw new Horde_Icalendar_Exception('Attribute "' . $name . '" Not Found');
|
|
} elseif (count($result) == 1 && !$params) {
|
|
return $result[0];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get a single value of an attribute.
|
|
*
|
|
* If multiple values, is auto-determined by library which is preferred
|
|
* value to return.
|
|
*
|
|
* @since 2.1.0
|
|
*
|
|
* @param string $name The name of the attribute.
|
|
*
|
|
* @return string The value of the attribute.
|
|
* @throws Horde_Icalendar_Exception
|
|
*/
|
|
public function getAttributeSingle($name)
|
|
{
|
|
$out = $this->getAttribute($name, false);
|
|
return is_array($out)
|
|
? reset($out)
|
|
: $out;
|
|
}
|
|
|
|
/**
|
|
* Gets the values of an attribute as an array. Multiple values
|
|
* are possible due to:
|
|
*
|
|
* a) multiple 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 array Multiple values for an attribute.
|
|
* @throws Horde_Icalendar_Exception
|
|
*/
|
|
public function getAttributeValues($name)
|
|
{
|
|
$result = array();
|
|
foreach ($this->_attributes as $attribute) {
|
|
if ($attribute['name'] == $name) {
|
|
$result = array_merge($attribute['values'], $result);
|
|
}
|
|
}
|
|
|
|
if (!count($result)) {
|
|
throw new Horde_Icalendar_Exception('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 (mixed) The value of $name.
|
|
* (mixed) $default if $name does not exist.
|
|
*/
|
|
public function getAttributeDefault($name, $default = '')
|
|
{
|
|
try {
|
|
return $this->getAttribute($name);
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all occurences of an attribute.
|
|
*
|
|
* @param string $name The name of the attribute.
|
|
*/
|
|
public function removeAttribute($name)
|
|
{
|
|
foreach (array_keys($this->_attributes) 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.
|
|
*/
|
|
public 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 mixed Either a Horde_Icalendar component (subclass) or an array
|
|
* of them.
|
|
*/
|
|
public function addComponent($components)
|
|
{
|
|
if (!is_array($components)) {
|
|
$components = array($components);
|
|
}
|
|
|
|
foreach ($components as $component) {
|
|
if ($component instanceof Horde_Icalendar) {
|
|
$component->_container = $this;
|
|
$this->_components[] = $component;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve all the components.
|
|
*
|
|
* @return array Array of Horde_Icalendar objects.
|
|
*/
|
|
public function getComponents()
|
|
{
|
|
return $this->_components;
|
|
}
|
|
|
|
/**
|
|
* TODO
|
|
*
|
|
* @return TODO
|
|
*/
|
|
public function getType()
|
|
{
|
|
return $this->type;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public function getComponentClasses()
|
|
{
|
|
$r = array();
|
|
|
|
foreach ($this->_components as $c) {
|
|
$cn = Horde_String::lower(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.
|
|
*/
|
|
public 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.
|
|
*/
|
|
public function getComponent($idx)
|
|
{
|
|
return isset($this->_components[$idx])
|
|
? $this->_components[$idx]
|
|
: 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 the
|
|
* requested component.
|
|
*/
|
|
public function findComponent($childclass)
|
|
{
|
|
$childclass = __CLASS__ . '_' . Horde_String::lower($childclass);
|
|
|
|
foreach (array_keys($this->_components) as $key) {
|
|
if ($this->_components[$key] instanceof $childclass) {
|
|
return $this->_components[$key];
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* the requested component.
|
|
*/
|
|
public function findComponentByAttribute($childclass, $attribute,
|
|
$value = null)
|
|
{
|
|
$childclass = __CLASS__ . '_' . Horde_String::lower($childclass);
|
|
|
|
foreach (array_keys($this->_components) as $key) {
|
|
if ($this->_components[$key] instanceof $childclass) {
|
|
try {
|
|
$attr = $this->_components[$key]->getAttribute($attribute);
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
continue;
|
|
}
|
|
|
|
if (is_null($value) || $value == $attr) {
|
|
return $this->_components[$key];
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Clears the iCalendar object (resets the components and attributes
|
|
* arrays).
|
|
*/
|
|
public function clear()
|
|
{
|
|
$this->_attributes = $this->_components = array();
|
|
}
|
|
|
|
public function toString() { return $this->exportvCalendar(); }
|
|
/**
|
|
* Export as vCalendar format.
|
|
*
|
|
* @return TODO
|
|
*/
|
|
public function exportvCalendar()
|
|
{
|
|
// Default values.
|
|
// TODO: HORDE_VERSION does not exist.
|
|
$requiredAttributes['PRODID'] = '-//The Horde Project//Horde iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
|
|
|
|
foreach ($requiredAttributes as $name => $default_value) {
|
|
try {
|
|
$this->getAttribute($name);
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
$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.
|
|
*/
|
|
public 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 boolean $clear If true clears this object before parsing.
|
|
*
|
|
* @return boolean True on successful import, false otherwise.
|
|
* @throws Horde_Icalendar_Exception
|
|
*/
|
|
public function parsevCalendar($text, $base = 'VCALENDAR', $clear = true)
|
|
{
|
|
if ($clear) {
|
|
$this->clear();
|
|
}
|
|
|
|
$text = Horde_String::trimUtf8Bom($text);
|
|
|
|
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:(.*)\s*?(\r\n|\r|\n)(.*)^END:\1\s*?/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.
|
|
$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(preg_replace('/^.*\./', '', Horde_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 = Horde_String::upper($paramName);
|
|
$paramValue = $param_parts[4][$key];
|
|
if ($paramName == 'TYPE') {
|
|
$paramValue = preg_split('/(?<!\\\\),/', $paramValue);
|
|
if (count($paramValue) == 1) {
|
|
$paramValue = $paramValue[0];
|
|
}
|
|
}
|
|
if (is_string($paramValue)) {
|
|
if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
|
|
$paramValue = $parts[1];
|
|
}
|
|
} else {
|
|
foreach ($paramValue as $k => $tmp) {
|
|
if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
|
|
$paramValue[$k] = $parts[1];
|
|
}
|
|
}
|
|
}
|
|
if (isset($params[$paramName])) {
|
|
if (is_array($params[$paramName])) {
|
|
$params[$paramName][] = $paramValue;
|
|
} else {
|
|
$params[$paramName] = array($params[$paramName], $paramValue);
|
|
}
|
|
} else {
|
|
$params[$paramName] = $paramValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Charset and encoding handling.
|
|
if ((isset($params['ENCODING']) &&
|
|
Horde_String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
|
|
isset($params['QUOTED-PRINTABLE'])) {
|
|
|
|
$value = quoted_printable_decode($value);
|
|
if (isset($params['CHARSET'])) {
|
|
$value = Horde_String::convertCharset($value, $params['CHARSET'], 'UTF-8');
|
|
}
|
|
} elseif (isset($params['CHARSET'])) {
|
|
$value = Horde_String::convertCharset($value, $params['CHARSET'], 'UTF-8');
|
|
}
|
|
|
|
// 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-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->_oldFormat ? ';' : ',';
|
|
preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
|
|
|
|
foreach ($values[1] as $value) {
|
|
$stamp = $this->_parseDateTime($value);
|
|
if (!is_int($stamp)) {
|
|
continue;
|
|
}
|
|
$dates[] = array('year' => date('Y', $stamp),
|
|
'month' => date('m', $stamp),
|
|
'mday' => date('d', $stamp));
|
|
}
|
|
$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 ($value) {
|
|
if ($this->_oldFormat) {
|
|
$floats = explode(',', $value);
|
|
$value = array('latitude' => floatval($floats[1]),
|
|
'longitude' => floatval($floats[0]));
|
|
} else {
|
|
$floats = explode(';', $value);
|
|
$value = array('latitude' => floatval($floats[0]),
|
|
'longitude' => floatval($floats[1]));
|
|
}
|
|
}
|
|
$this->setAttribute($tag, $value, $params);
|
|
break;
|
|
|
|
// Recursion fields.
|
|
case 'EXRULE':
|
|
case 'RRULE':
|
|
$this->setAttribute($tag, trim($value), $params);
|
|
break;
|
|
|
|
// ADR, ORG and N are lists seperated by unescaped semicolons
|
|
// with a specific number of slots.
|
|
case 'ADR':
|
|
case 'N':
|
|
case 'ORG':
|
|
$value = trim($value);
|
|
// As of rfc 2426 2.4.2 semicolon, comma, and colon must
|
|
// be escaped (comma is unescaped after splitting below).
|
|
$value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
|
|
array($this->_newline, $this->_newline, ';', ':'),
|
|
$value);
|
|
|
|
// Split by unescaped semicolons:
|
|
$values = preg_split('/(?<!\\\\);/', $value);
|
|
$value = str_replace(
|
|
array('\\;', '\\,'), array(';', ','), $value
|
|
);
|
|
$values = str_replace(
|
|
array('\\;', '\\,'), array(';', ','), $values
|
|
);
|
|
$this->setAttribute($tag, trim($value), $params, true, $values);
|
|
break;
|
|
|
|
// String fields.
|
|
default:
|
|
if ($this->_oldFormat) {
|
|
// vCalendar 1.0 and vCard 2.1 only escape semicolons
|
|
// and use unescaped semicolons to create lists.
|
|
$value = trim($value);
|
|
// Split by unescaped semicolons:
|
|
$values = preg_split('/(?<!\\\\);/', $value);
|
|
$value = str_replace('\\;', ';', $value);
|
|
$values = str_replace('\\;', ';', $values);
|
|
$this->setAttribute($tag, trim($value), $params, true, $values);
|
|
} else {
|
|
$value = trim($value);
|
|
// As of rfc 2426 2.4.2 semicolon, comma, and colon
|
|
// must be escaped (comma is unescaped after splitting
|
|
// below).
|
|
$value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
|
|
array($this->_newline, $this->_newline, ';', ':', '\\'),
|
|
$value);
|
|
|
|
// Split by unescaped commas.
|
|
$values = preg_split('/(?<!\\\\),/', $value);
|
|
$value = str_replace('\\,', ',', $value);
|
|
$values = str_replace('\\,', ',', $values);
|
|
|
|
$this->setAttribute($tag, trim($value), $params, true, $values);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process all components.
|
|
if ($components) {
|
|
// vTimezone components are processed first. They are
|
|
// needed to process vEvents that may use a TZID.
|
|
foreach ($components[0] as $key => $data) {
|
|
$type = trim($components[1][$key]);
|
|
if ($type != 'VTIMEZONE') {
|
|
continue;
|
|
}
|
|
$component = $this->newComponent($type, $this);
|
|
if ($component === false) {
|
|
throw new Horde_Icalendar_Exception('Unable to create object for type ' . $type);
|
|
}
|
|
$component->parsevCalendar($data, $type);
|
|
|
|
$this->addComponent($component);
|
|
|
|
// Remove from the vCalendar data.
|
|
$vCal = str_replace($data, '', $vCal);
|
|
}
|
|
|
|
// Now process the non-vTimezone components.
|
|
foreach ($components[0] as $key => $data) {
|
|
$type = trim($components[1][$key]);
|
|
if ($type == 'VTIMEZONE') {
|
|
continue;
|
|
}
|
|
$component = $this->newComponent($type, $this);
|
|
if ($component === false) {
|
|
throw new Horde_Icalendar_Exception('Unable to create object for type ' . $type);
|
|
}
|
|
$component->parsevCalendar($data, $type);
|
|
|
|
$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.
|
|
*/
|
|
protected function _exportvData($base = 'VCALENDAR')
|
|
{
|
|
$result = 'BEGIN:' . Horde_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' &&
|
|
$base != 'VTIMEZONE' && $base != 'STANDARD' && $base != 'DAYLIGHT') {
|
|
// 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->_oldFormat) {
|
|
continue;
|
|
}
|
|
/* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
|
|
if ($this->_oldFormat &&
|
|
$param_name == 'VALUE' && $param_value == 'DATE') {
|
|
continue;
|
|
}
|
|
|
|
if ($param_value === null) {
|
|
$params_str .= ";$param_name";
|
|
} else {
|
|
if (!is_array($param_value)) {
|
|
$param_value = array($param_value);
|
|
}
|
|
foreach ($param_value as &$one_param_value) {
|
|
$len = strlen($one_param_value);
|
|
$safe_value = '';
|
|
$quote = false;
|
|
for ($i = 0; $i < $len; ++$i) {
|
|
$ord = ord($one_param_value[$i]);
|
|
// Accept only valid characters.
|
|
if ($ord == 9 || $ord == 32 || $ord == 33 ||
|
|
($ord >= 35 && $ord <= 126) ||
|
|
$ord >= 128) {
|
|
$safe_value .= $one_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 . '"';
|
|
}
|
|
$one_param_value = $safe_value;
|
|
}
|
|
$params_str .= ";$param_name=" . implode(',', $param_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':
|
|
$floating = $base == 'STANDARD'
|
|
|| $base == 'DAYLIGHT'
|
|
|| isset($params['TZID']);
|
|
if (isset($params['VALUE'])) {
|
|
if ($params['VALUE'] == 'DATE') {
|
|
// VCALENDAR 1.0 uses T000000 - T235959 for all day events:
|
|
if ($this->_oldFormat && $name == 'DTEND') {
|
|
$d = new Horde_Date($value);
|
|
$value = new Horde_Date(array(
|
|
'year' => $d->year,
|
|
'month' => $d->month,
|
|
'mday' => $d->mday - 1));
|
|
$value = $this->_exportDate($value, '235959');
|
|
} else {
|
|
$value = $this->_exportDate($value, '000000');
|
|
}
|
|
} else {
|
|
$value = $this->_exportDateTime($value, $floating);
|
|
}
|
|
} else {
|
|
$value = $this->_exportDateTime($value, $floating);
|
|
}
|
|
break;
|
|
|
|
// Comma seperated dates.
|
|
case 'EXDATE':
|
|
case 'RDATE':
|
|
$floating = $base == 'STANDARD' || $base == 'DAYLIGHT';
|
|
$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, $floating);
|
|
}
|
|
} else {
|
|
$dates[] = $this->_exportDateTime($date, $floating);
|
|
}
|
|
}
|
|
$value = implode($this->_oldFormat ? ';' : ',', $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->_oldFormat) {
|
|
$value = $value['longitude'] . ',' . $value['latitude'];
|
|
} else {
|
|
$value = $value['latitude'] . ';' . $value['longitude'];
|
|
}
|
|
break;
|
|
|
|
// Recurrence fields.
|
|
case 'EXRULE':
|
|
case 'RRULE':
|
|
break;
|
|
|
|
default:
|
|
if ($this->_oldFormat) {
|
|
/* vcard 2.1 and vcalendar 1.0 escape only
|
|
* semicolons */
|
|
if (is_array($attribute['values']) &&
|
|
count($attribute['values'])) {
|
|
$values = $attribute['values'];
|
|
if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
|
|
$glue = ';';
|
|
} else {
|
|
$glue = ',';
|
|
}
|
|
$values = str_replace(';', '\\;', $values);
|
|
$value = implode($glue, $values);
|
|
} else {
|
|
$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 . ':'
|
|
. preg_replace(array('/(?<!\r)\n/', '/(?<!=)\r\n/'),
|
|
array("\r\n", "=0D=0A=\r\n "),
|
|
quoted_printable_encode($value))
|
|
. $this->_newline;
|
|
} else {
|
|
$attr_string = $name . $params_str . ':' . $value;
|
|
if (!$this->_oldFormat) {
|
|
if (isset($params['ENCODING']) && $params['ENCODING'] == 'b') {
|
|
$attr_string = trim(chunk_split($attr_string, 75, $this->_newline . ' '));
|
|
} else {
|
|
$attr_string = Horde_String::wordwrap($attr_string, 75, $this->_newline . ' ', true, true);
|
|
}
|
|
}
|
|
$result .= $attr_string . $this->_newline;
|
|
}
|
|
}
|
|
|
|
$tzs = array();
|
|
foreach ($this->_components as $component) {
|
|
if (!($component instanceof Horde_Icalendar_Vtimezone) ||
|
|
!isset($tzs[$component->getAttribute('TZID')])) {
|
|
$result .= $component->exportvCalendar();
|
|
if ($component instanceof Horde_Icalendar_Vtimezone) {
|
|
$tzs[$component->getAttribute('TZID')] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result . 'END:' . $base . $this->_newline;
|
|
}
|
|
|
|
/**
|
|
* Parse a UTC Offset field.
|
|
*
|
|
* @param $text TODO
|
|
*
|
|
* @return TODO
|
|
*/
|
|
protected 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;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Export a UTC Offset field.
|
|
*
|
|
* @param $value TODO
|
|
*
|
|
* @return TODO
|
|
*/
|
|
function _exportUtcOffset($value)
|
|
{
|
|
$offset = ($value['ahead'] ? '+' : '-') .
|
|
sprintf('%02d%02d', $value['hour'], $value['minute']);
|
|
|
|
if (isset($value['second'])) {
|
|
$offset .= sprintf('%02d', $value['second']);
|
|
}
|
|
|
|
return $offset;
|
|
}
|
|
|
|
/**
|
|
* Parse a Time Period field.
|
|
*
|
|
* @param $text TODO
|
|
*
|
|
* @return array TODO
|
|
*/
|
|
protected 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.
|
|
*
|
|
* @param $value TODO
|
|
*
|
|
* @return TODO
|
|
*/
|
|
protected function _exportPeriod($value)
|
|
{
|
|
$period = $this->_exportDateTime($value['start']) . '/';
|
|
|
|
return isset($value['duration'])
|
|
? $period . $this->_exportDuration($value['duration'])
|
|
: $period . $this->_exportDateTime($value['end']);
|
|
}
|
|
|
|
/**
|
|
* Groks the TZID and returns an offset in seconds from UTC for this
|
|
* date and time.
|
|
*
|
|
* @param array $date A date hash.
|
|
* @param array $time A time hash.
|
|
* @param string $tzid A timezone ID.
|
|
*
|
|
* @return integer The offset from UTC in seconds for the provided
|
|
* timezone and date/time.
|
|
*/
|
|
protected function _parseTZID($date, $time, $tzid)
|
|
{
|
|
$vtimezone = $this->_container
|
|
->findComponentByAttribute('vtimezone', 'TZID', $tzid);
|
|
if (!$vtimezone) {
|
|
return false;
|
|
}
|
|
|
|
$change_times = array();
|
|
foreach ($vtimezone->getComponents() as $o) {
|
|
$change_times = array_merge(
|
|
$change_times,
|
|
$vtimezone->parseChild($o, $date['year'])
|
|
);
|
|
}
|
|
|
|
if (!$change_times) {
|
|
return false;
|
|
}
|
|
|
|
usort(
|
|
$change_times,
|
|
function($a, $b) {
|
|
if (!$a['end']) {
|
|
if (!$b['end']) {
|
|
return $a['time'] - $b['time'];
|
|
}
|
|
return 1;
|
|
}
|
|
if (!$b['end']) {
|
|
return -1;
|
|
}
|
|
return Horde_Icalendar::_getEndDifference($a['end'], $b['end']);
|
|
}
|
|
);
|
|
|
|
// Time is arbitrarily based on UTC for comparison.
|
|
$t = @gmmktime($time['hour'], $time['minute'], $time['second'],
|
|
$date['month'], $date['mday'], $date['year']);
|
|
|
|
// First check for the first change time that isn't expired (from POV of
|
|
// $time) and is after $t.
|
|
$n = count($change_times);
|
|
for ($i = 0, $n = count($change_times); $i < $n -1; $i++) {
|
|
if (!$this->_checkEndDate($t, $change_times[$i])) {
|
|
continue;
|
|
}
|
|
if ($t < $change_times[$i]['time']) {
|
|
return $change_times[$i]['from'];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
|
|
// See Bug: 14153. Some timezone definitions may be such that a
|
|
// transition will incorrectly match due to the way we parse the
|
|
// 'end' times. There *may* be a more correct way to do this by
|
|
// sorting the transitions/handling 'end' values differently.
|
|
if (($t >= $change_times[$i]['time']) &&
|
|
($t < $change_times[$i + 1]['time']) &&
|
|
$this->_checkEndDate($t, $change_times[$i + 1])) {
|
|
return $change_times[$i]['to'];
|
|
}
|
|
}
|
|
|
|
if ($t >= $change_times[$n - 1]['time']) {
|
|
return $change_times[$n - 1]['to'];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Utility method to aid in checking the end date of a transition.
|
|
*
|
|
* @param integer $t The timestamp of the date we are checking.
|
|
* @param array $times A transition array.
|
|
*
|
|
* @return boolean True if $t is before the end date of the transition
|
|
* otherwise false.
|
|
*/
|
|
protected function _checkEndDate($t, $times)
|
|
{
|
|
if (empty($times['end'])) {
|
|
return true;
|
|
}
|
|
if (strlen($times['end']) == 4) {
|
|
$date = @gmmktime(0, 0, 0, 1, 1, $times['end']);
|
|
return ($date && $t < $date);
|
|
}
|
|
|
|
return ($t < $times['end']);
|
|
}
|
|
|
|
/**
|
|
* Returns the difference between the datetime indicated by $a and the
|
|
* datetime indicated by $b after normalizing both values to a unix
|
|
* timestamp. Used when sorting timezone transitions that may contain
|
|
* mixed format end times.
|
|
*
|
|
* @param mixed Either a string representing a 4 digit year, or unix
|
|
* timestamp.
|
|
* @param mixed Either a string representing a 4 digit year, or unix
|
|
* timestamp.
|
|
*
|
|
* @return boolean True if $a < $b otherwise false.
|
|
* @todo This needs to be public/static due to it being called from a
|
|
* anonymous function. See PR: 213. This can be removed once we
|
|
* no longer support PHP 5.3.
|
|
*/
|
|
public static function _getEndDifference($a, $b)
|
|
{
|
|
if (strlen($a) == 4) {
|
|
$a = @gmmktime(0, 0, 0, 1, 1, $a);
|
|
}
|
|
if (strlen($b) == 4) {
|
|
$b = @gmmktime(0, 0, 0, 1, 1, $b);
|
|
}
|
|
|
|
return $a - $b;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param string $text The Icalendar datetime field value.
|
|
* @param string $tzid A timezone identifier.
|
|
*
|
|
* @return integer A unix timestamp.
|
|
*/
|
|
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)) {
|
|
// Or not
|
|
return $text;
|
|
}
|
|
$dateParts = array($text, '000000');
|
|
}
|
|
|
|
if (!($date = $this->_parseDate($dateParts[0])) ||
|
|
!($time = $this->_parseTime($dateParts[1]))) {
|
|
return $text;
|
|
}
|
|
|
|
// Get timezone info for date fields from $tzid and container.
|
|
$tzoffset = ($time['zone'] == 'Local' && $tzid &&
|
|
($this->_container instanceof 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 ($result !== false && $tzoffset) {
|
|
$result -= $tzoffset;
|
|
}
|
|
} else {
|
|
// We don't know the timezone so assume local timezone.
|
|
$result = @mktime($time['hour'], $time['minute'], $time['second'],
|
|
$date['month'], $date['mday'], $date['year']);
|
|
}
|
|
|
|
return ($result !== false) ? $result : $text;
|
|
}
|
|
|
|
/**
|
|
* Export a DateTime field.
|
|
*
|
|
* @todo A bunch of code calls this function outside this class, so it
|
|
* needs to be marked public for now.
|
|
*
|
|
* @param integer|object|array $value The time value to export (either a
|
|
* Horde_Date, array, or timestamp).
|
|
* @param boolean $floating Whether to return a floating
|
|
* date-time (without time zone
|
|
* information).
|
|
*
|
|
* @return string The string representation of the datetime value.
|
|
*/
|
|
public function _exportDateTime($value, $floating = false)
|
|
{
|
|
$date = new Horde_Date($value);
|
|
return $date->toICalendar($floating);
|
|
}
|
|
|
|
/**
|
|
* Parses a Time field.
|
|
*
|
|
* @param $text TODO
|
|
*
|
|
* @return TODO
|
|
*/
|
|
protected function _parseTime($text)
|
|
{
|
|
if (!preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
|
|
return false;
|
|
}
|
|
|
|
return array(
|
|
'hour' => $timeParts[1],
|
|
'minute' => $timeParts[2],
|
|
'second' => $timeParts[3],
|
|
'zone' => isset($timeParts[4]) ? 'UTC' : 'Local'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parses a Date field.
|
|
*
|
|
* @param $text TODO
|
|
*
|
|
* @return array TODO
|
|
*/
|
|
public 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'
|
|
*
|
|
* @return TODO
|
|
*/
|
|
protected function _exportDate($value, $autoconvert = false)
|
|
{
|
|
if (is_object($value)) {
|
|
$value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
|
|
}
|
|
|
|
return ($autoconvert !== false && $this->_oldFormat)
|
|
? sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert)
|
|
: sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
|
|
}
|
|
|
|
/**
|
|
* Parses a DURATION value field.
|
|
*
|
|
* @param string $text A DURATION value.
|
|
*
|
|
* @return integer The duration in seconds.
|
|
*/
|
|
protected 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)) {
|
|
return false;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Export a duration value.
|
|
*
|
|
* @param $value TODO
|
|
*/
|
|
protected 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';
|
|
}
|
|
} elseif ($duration === 'P') {
|
|
// Duration without time ("P") is NOT valid, append 0 seconds
|
|
// ("T0S").
|
|
$duration .= 'T0S';
|
|
}
|
|
|
|
return $duration;
|
|
}
|
|
|
|
}
|