--- /dev/null
+<?php
+
+abstract class Horde_Icalendar_Base implements Iterator
+{
+ /**
+ * @var array
+ */
+ public $components = array();
+
+ /**
+ * @var array
+ */
+ protected $_properties = array();
+
+ public function __construct($properties = array())
+ {
+ foreach ($properties as $property => $value) {
+ $this->addProperty($property, $value);
+ }
+ }
+
+ /**
+ * Validates a property-value-pair.
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function _validate($property, &$value, &$params = array())
+ {
+ if (!isset($this->_properties[$property])) {
+ throw new InvalidArgumentException($property . ' is not a valid property');
+ }
+ $myProperty = &$this->_properties[$property];
+ if (isset($myProperty['type'])) {
+ $func = 'is_' . $myProperty['type'];
+ if (!$func) {
+ throw new InvalidArgumentException($value . ' is not a ' . $myProperty['type']);
+ }
+ } elseif (isset($myProperty['class'])) {
+ if (!($value instanceof $myProperty['class'])) {
+ throw new InvalidArgumentException($value . ' is not of class ' . $myProperty['class']);
+ }
+ }
+ if ($property == 'stamp') {
+ $value->setTimezone('UTC');
+ }
+ }
+
+ /**
+ * Setter.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __set($property, $value)
+ {
+ $this->_validate($property, $value);
+ $this->_setProperty($property, $value);
+ }
+
+ /**
+ * Sets the value of a property.
+ *
+ * @param string $property The name of the property.
+ * @param string $value The value of the property.
+ * @param array $params Array containing any addition parameters for
+ * this property.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function setProperty($property, $value, $params = array())
+ {
+ $this->_validate($property, $value);
+ $this->_setProperty($property, $value, $params);
+ }
+
+ /**
+ * Adds the value of a property.
+ *
+ * @param string $property The name of the property.
+ * @param string $value The value of the property.
+ * @param array $params Array containing any addition parameters for
+ * this property.
+ *
+ * @throws InvalidArgumentException
+ * @throws Horde_Icalendar_Exception
+ */
+ public function addProperty($property, $value, $params = array())
+ {
+ $this->_validate($property, $value);
+ if (!$this->_properties[$property]['multiple'] &&
+ !empty($this->_properties[$property]['values'])) {
+ throw new Horde_Icalendar_Exception($property . ' properties must not occur more than once.');
+ }
+ $this->_setProperty($property, $value, $params, true);
+ }
+
+ /**
+ * Sets the value of a property.
+ *
+ * @param string $property The name of the property.
+ * @param string $value The value of the property.
+ * @param array $params Array containing any addition parameters for
+ * this property.
+ * @param boolean $add Whether to add (instead of replace) the value.
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function _setProperty($property, $value, $params = array(), $add = false)
+ {
+ if ($add) {
+ if (!isset($this->_properties[$property]['values'])) {
+ $this->_properties[$property]['values'] = array();
+ $this->_properties[$property]['params'] = array();
+ }
+ $this->_properties[$property]['values'][] = $value;
+ $this->_properties[$property]['params'][] = $params;
+ } else {
+ $this->_properties[$property]['values'] = array($value);
+ $this->_properties[$property]['params'] = $params;
+ }
+ }
+
+ /**
+ * Getter.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __get($property)
+ {
+ if (!isset($this->_properties[$property])) {
+ throw new InvalidArgumentException($property . ' is not a valid property');
+ }
+ return isset($this->_properties[$property]['values'])
+ ? ($this->_properties[$property]['multiple']
+ ? $this->_properties[$property]['values']
+ : $this->_properties[$property]['values'][0])
+ : null;
+ }
+
+ /**
+ * Returns the value of an property.
+ *
+ * @param string $name The name of the property.
+ * @param boolean $params Return the parameters for this property instead
+ * of its value.
+ *
+ * @return mixed (object) PEAR_Error if the property does not exist.
+ * (string) The value of the property.
+ * (array) The parameters for the property or
+ * multiple values for an property.
+ */
+ function getProperty($name, $params = false)
+ {
+ $result = array();
+ foreach ($this->_properties as $property) {
+ if ($property['name'] == $name) {
+ if ($params) {
+ $result[] = $property['params'];
+ } else {
+ $result[] = $property['value'];
+ }
+ }
+ }
+ if (!count($result)) {
+ require_once 'PEAR.php';
+ return PEAR::raiseError('Property "' . $name . '" Not Found');
+ } if (count($result) == 1 && !$params) {
+ return $result[0];
+ } else {
+ return $result;
+ }
+ }
+
+ public function getProperties()
+ {
+ return $this->_properties;
+ }
+
+ /**
+ * Validates the complete component for missing properties or invalid
+ * property combinations.
+ *
+ * @throws Horde_Icalendar_Exception
+ */
+ public function validate()
+ {
+ foreach ($this->_properties as $name => $property) {
+ if (!empty($property['required']) && !isset($property['values'])) {
+ switch ($name) {
+ case 'uid':
+ $this->uid = (string)new Horde_Support_Guid;
+ break;
+ case 'stamp':
+ $this->stamp = new Horde_Date(time());
+ break;
+ default:
+ // @todo Use LSB (static::__CLASS__) once we require PHP 5.3
+ $component = Horde_String::upper(str_replace('Horde_Icalendar_', '', get_class($this)));
+ throw new Horde_Icalendar_Exception('This ' . $component . ' component must have a ' . $name . ' property set');
+ }
+ }
+ }
+ }
+
+ public function current()
+ {
+ return current($this->_properties);
+ }
+
+ public function key()
+ {
+ return key($this->_properties);
+ }
+
+ public function next()
+ {
+ next($this->_properties);
+ }
+
+ public function rewind()
+ {
+ reset($this->_properties);
+ }
+
+ public function valid()
+ {
+ return current($this->_properties) !== false;
+ }
+
+ /**
+ * @todo Use LSB (static::__CLASS__) once we require PHP 5.3
+ */
+ public function export()
+ {
+ $this->validate();
+ $writer = Horde_Icalendar_Writer::factory(
+ str_replace('Horde_Icalendar_', '', get_class($this)),
+ str_replace('.', '', $this->version));
+ return $writer->export($this);
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Exception extends Horde_Exception
+{
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Valarm extends Horde_Icalendar_Base
+{
+ /**
+ * Constructor.
+ */
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ 'summary' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'description' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'));
+ parent::__construct($properties);
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Vcalendar extends Horde_Icalendar_Base
+{
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ // RFC 2445 Section 4.7.1
+ 'scale' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'),
+ // RFC 2445 Section 4.7.2
+ 'method' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'),
+ // RFC 2445 Section 4.7.3
+ 'product' => array('required' => true,
+ 'multiple' => false,
+ 'type' => 'string'),
+ // RFC 2445 Section 4.7.4
+ 'version' => array('required' => true,
+ 'multiple' => false,
+ 'type' => 'string'),
+ );
+
+ $properties = array_merge(array('version' => '2.0',
+ 'product' => '-//The Horde Project//Horde_Icalendar Library//EN'),
+ $properties);
+ parent::__construct($properties);
+ }
+
+ /**
+ * Getter.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __get($property)
+ {
+ $value = parent::__get($property);
+ if ($property == 'scale' && is_null($value)) {
+ $value = 'GREGORIAN';
+ }
+ return $value;
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Vevent extends Horde_Icalendar_Base
+{
+ /**
+ * Constructor.
+ */
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ 'uid' => array('required' => true,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'start' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'startDate' => array('required' => false,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'stamp' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'summary' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'description' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'));
+ parent::__construct($properties);
+ }
+
+ /**
+ * Sets the value of a property.
+ *
+ * @param string $property The name of the property.
+ * @param string $value The value of the property.
+ * @param array $params Array containing any addition parameters for
+ * this property.
+ * @param boolean $add Whether to add (instead of replace) the value.
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function _setProperty($property, $value, $params = array(), $add = false)
+ {
+ if ($property == 'startDate') {
+ $this->_validate('start', $value);
+ $property = 'start';
+ $params['value'] = 'date';
+ }
+ parent::_setProperty($property, $value, $params, $add);
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Vfreebusy extends Horde_Icalendar_Base
+{
+ /**
+ * Constructor.
+ */
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ 'uid' => array('required' => true,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'start' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'stamp' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ );
+ parent::__construct($properties);
+ }
+
+ /**
+ * Validates a property-value-pair.
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function _validate($property, &$value)
+ {
+ parent::_validate($property, $value);
+ if ($property == 'start') {
+ $value->setTimezone('UTC');
+ }
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Vjournal extends Horde_Icalendar_Base
+{
+ /**
+ * Constructor.
+ */
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ 'uid' => array('required' => true,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'stamp' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'summary' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'description' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'));
+ parent::__construct($properties);
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Vtimezone extends Horde_Icalendar_Base
+{
+ /**
+ * Constructor.
+ */
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ /*
+ Within the "VTIMEZONE" calendar component, this property defines the
+ effective start date and time for a time zone specification. This
+ property is REQUIRED within each STANDARD and DAYLIGHT part included
+ in "VTIMEZONE" calendar components and MUST be specified as a local
+ DATE-TIME without the "TZID" property parameter.
+ */
+ 'start' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ );
+ parent::__construct($properties);
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Vtodo extends Horde_Icalendar_Base
+{
+ /**
+ * Constructor.
+ */
+ public function __construct($properties = array())
+ {
+ $this->_properties += array(
+ 'uid' => array('required' => true,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'start' => array('required' => false,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'startDate' => array('required' => false,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'stamp' => array('required' => true,
+ 'multiple' => false,
+ 'class' => 'Horde_Date'),
+ 'summary' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'),
+ 'description' => array('required' => false,
+ 'multiple' => false,
+ 'type' => 'string'));
+ parent::__construct($properties);
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Writer
+{
+
+ /**
+ * Attempts to return a concrete Horde_Icalendar_Writer instance based on
+ * $format and $version.
+ *
+ * @param mixed $format The format that the writer should output.
+ * @param array $version The format version.
+ *
+ * @return Horde_Icalendar_Writer The newly created concrete instance.
+ *
+ * @throws Horde_Icalendar_Exception
+ */
+ static public function factory($format, $version)
+ {
+ $class = 'Horde_Icalendar_Writer_' . $format . '_' . $version;
+ if (class_exists($class)) {
+ return new $class($params);
+ }
+
+ throw new Horde_Icalendar_Exception($class . ' not found.');
+ }
+
+}
--- /dev/null
+<?php
+
+abstract class Horde_Icalendar_Writer_Base
+{
+ protected $_propertyMap = array();
+
+ protected $_output = '';
+
+ public function export($object)
+ {
+ $this->_output = '';
+ $this->_exportComponent($object);
+ return $this->_output;
+ }
+
+ protected function _exportComponent($object)
+ {
+ $basename = Horde_String::upper(str_replace('Horde_Icalendar_', '', get_class($object)));
+ $this->_output .= 'BEGIN:' . $basename . "\n";
+ foreach ($object as $name => $property) {
+ $this->_exportProperty($name, $property);
+ }
+ foreach ($object->components as $component) {
+ $this->_exportComponent($component);
+ }
+ $this->_output .= 'END:' . $basename . "\n";
+ return $this->_output;
+ }
+
+ protected function _exportProperty($name, $property)
+ {
+ if (isset($property['values'])) {
+ if (isset($this->_propertyMap[$name])) {
+ $name = $this->_propertyMap[$name];
+ }
+ foreach ($property['values'] as $value) {
+ $this->_output .= Horde_String::upper($name) . ':' . $value . "\n";
+ }
+ }
+ }
+
+}
--- /dev/null
+<?php
+
+class Horde_Icalendar_Writer_Vcalendar_20 extends Horde_Icalendar_Writer_Base
+{
+ protected $_propertyMap = array('product' => 'PRODID',
+ 'start' => 'DTSTART',
+ 'stamp' => 'DTSTAMP');
+
+ protected function _exportProperty($name, $property)
+ {
+ if (!isset($property['values'])) {
+ return;
+ }
+ if (isset($property['class']) && $property['class'] == 'Horde_Date') {
+ if (isset($this->_propertyMap[$name])) {
+ $name = $this->_propertyMap[$name];
+ }
+ foreach ($property['values'] as $value) {
+ $this->_output .= Horde_String::upper($name) . ':' . $value->format('Ymd\THms\Z') . "\n";
+ }
+ } else {
+ parent::_exportProperty($name, $property);
+ }
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Icalendar
+ * @subpackage UnitTests
+ * @copyright 2009 The Horde Project (http://www.horde.org/)
+ * @license http://www.fsf.org/copyleft/lgpl.html
+ */
+
+/**
+ * Define the main method
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+ define('PHPUnit_MAIN_METHOD', 'Horde_Icalendar_AllTests::main');
+}
+
+/**
+ * Prepare the test setup.
+ */
+require_once 'Horde/Test/AllTests.php';
+
+/**
+ * @package Horde_Icalendar
+ * @subpackage UnitTests
+ */
+class Horde_Icalendar_AllTests extends Horde_Test_AllTests
+{
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Icalendar_AllTests::main') {
+ Horde_Icalendar_AllTests::main('Horde_Icalendar', __FILE__);
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Icalendar
+ * @subpackage UnitTests
+ * @copyright 2009 The Horde Project (http://www.horde.org/)
+ * @license http://www.fsf.org/copyleft/lgpl.html
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Icalendar
+ * @subpackage UnitTests
+ */
+class Horde_Icalendar_WriterTest extends Horde_Test_Case
+{
+ public function setUp()
+ {
+ }
+
+ public function testEscapes()
+ {
+ $ical = new Horde_Icalendar_Vcalendar(array('version' => '2.0'));
+ $event1 = new Horde_Icalendar_Vevent();
+ $event2 = new Horde_Icalendar_Vevent();
+
+ $event1->uid = '20041120-8550-innerjoin-org';
+ $event1->startDate = new Horde_Date(array('year' => 2005, 'month' => 5, 'mday' => 3));
+ $event1->stamp = new Horde_Date(array('year' => 2004, 'month' => 11, 'mday' => 20));
+ $event1->summary = 'Escaped Comma in Description Field';
+ $event1->description = 'There is a comma (escaped with a baskslash) in this sentence and some important words after it, see anything here?';
+
+ $event2->uid = '20041120-8549-innerjoin-org';
+ $event2->startDate = new Horde_Date(array('year' => 2005, 'month' => 5, 'mday' => 4));
+ $event2->stamp = new Horde_Date(array('year' => 2004, 'month' => 11, 'mday' => 20));
+ $event2->summary = 'Dash (rather than Comma) in the Description Field';
+ $event2->description = 'There are important words after this dash - see anything here or have the words gone?';
+
+ $ical->components[] = $event1;
+ $ical->components[] = $event2;
+
+ $this->assertEquals('BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//The Horde Project//Horde_iCalendar Library//EN
+METHOD:PUBLISH
+BEGIN:VEVENT
+UID:20041120-8550-innerjoin-org
+DTSTART;VALUE=DATE:20050503
+DTSTAMP:20041120T000000Z
+SUMMARY:Escaped Comma in Description Field
+DESCRIPTION:There is a comma (escaped with a baskslash) in this sentence
+ and some important words after it\, see anything here?
+END:VEVENT
+BEGIN:VEVENT
+UID:20041120-8549-innerjoin-org
+DTSTART;VALUE=DATE:20050504
+DTSTAMP:20041120T000000Z
+SUMMARY:Dash (rather than Comma) in the Description Field
+DESCRIPTION:There are important words after this dash - see anything here
+ or have the words gone?
+END:VEVENT
+END:VCALENDAR',
+ $ical->export());
+ }
+
+}
\ No newline at end of file