8
* Class representing iCalendar files.
10
* Copyright 2003-2014 Horde LLC (http://www.horde.org/)
12
* See the enclosed file COPYING for license information (LGPL). If you
13
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
15
* @author Mike Cochrane <mike@graftonhall.co.nz>
17
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
23
* The component type of this class.
27
public $type = 'vcalendar';
30
* The parent (containing) iCalendar object.
32
* @var Horde_Icalendar
34
protected $_container = false;
37
* The name/value pairs of attributes for this object (UID,
38
* DTSTART, etc.). Which are present depends on the object and on
39
* what kind of component it is.
43
protected $_attributes = array();
46
* Any children (contained) iCalendar components of this object.
50
protected $_components = array();
53
* According to RFC 2425, we should always use CRLF-terminated lines.
57
protected $_newline = "\r\n";
60
* iCalendar format version (different behavior for 1.0 and 2.0 especially
61
* with recurring events).
68
* Whether entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
70
* These 'old' formats are defined by www.imc.org. The 'new' (non-old)
71
* formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
74
protected $_oldFormat = true;
79
* @var string $version Version.
81
public function __construct($version = '2.0')
83
$this->setAttribute('VERSION', $version);
87
* Return a reference to a new component.
89
* @param string $type The type of component to return
90
* @param Horde_Icalendar $container A container that this component
91
* will be associated with.
93
* @return object Reference to a Horde_Icalendar_* object as specified.
95
static public function newComponent($type, $container)
97
$type = Horde_String::lower($type);
98
$class = __CLASS__ . '_' . Horde_String::ucfirst($type);
100
if (class_exists($class)) {
101
$component = new $class();
102
if ($container !== false) {
103
$component->_container = $container;
104
// Use version of container, not default set by component
106
$component->setVersion($container->getAttribute('VERSION'));
109
// Should return an dummy x-unknown type class here.
117
* Sets the version of this component.
122
* @param string $version A float-like version string.
124
public function setVersion($version)
126
$this->_oldFormat = $version < 2;
127
$this->_version = $version;
131
* Sets the value of an attribute.
133
* @param string $name The name of the attribute.
134
* @param string $value The value of the attribute.
135
* @param array $params Array containing any addition parameters for
137
* @param boolean $append True to append the attribute, False to replace
138
* the first matching attribute found.
139
* @param array $values Array representation of $value. For
140
* comma/semicolon seperated lists of values. If
141
* not set use $value as single array element.
143
public function setAttribute($name, $value, $params = array(),
144
$append = true, $values = false)
146
// Make sure we update the internal format version if
147
// setAttribute('VERSION', ...) is called.
148
if ($name == 'VERSION') {
149
$this->setVersion($value);
150
if ($this->_container !== false) {
151
$this->_container->setVersion($value);
156
$values = array($value);
161
foreach (array_keys($this->_attributes) as $key) {
162
if ($this->_attributes[$key]['name'] == Horde_String::upper($name)) {
163
$this->_attributes[$key]['params'] = $params;
164
$this->_attributes[$key]['value'] = $value;
165
$this->_attributes[$key]['values'] = $values;
172
if ($append || !$found) {
173
$this->_attributes[] = array(
174
'name' => Horde_String::upper($name),
183
* Sets parameter(s) for an (already existing) attribute. The
184
* parameter set is merged into the existing set.
186
* @param string $name The name of the attribute.
187
* @param array $params Array containing any additional parameters for
190
* @return boolean True on success, false if no attribute $name exists.
192
public function setParameter($name, $params = array())
194
$keys = array_keys($this->_attributes);
195
foreach ($keys as $key) {
196
if ($this->_attributes[$key]['name'] == $name) {
197
$this->_attributes[$key]['params'] = array_merge($this->_attributes[$key]['params'], $params);
206
* Get the value of an attribute.
208
* @param string $name The name of the attribute.
209
* @param boolean $params Return the parameters for this attribute instead
212
* @return mixed (string) The value of the attribute.
213
* (array) The parameters for the attribute or
214
* multiple values for an attribute.
215
* @throws Horde_Icalendar_Exception
217
public function getAttribute($name, $params = false)
219
if ($name == 'VERSION') {
220
return $this->_version;
224
foreach ($this->_attributes as $attribute) {
225
if ($attribute['name'] == $name) {
227
? $attribute['params']
228
: $attribute['value'];
232
if (!count($result)) {
233
throw new Horde_Icalendar_Exception('Attribute "' . $name . '" Not Found');
234
} elseif (count($result) == 1 && !$params) {
242
* Gets the values of an attribute as an array. Multiple values
243
* are possible due to:
245
* a) multiple occurences of 'name'
246
* b) (unsecapd) comma seperated lists.
248
* So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
249
* will return array('a', 'b', 'c').
251
* @param string $name The name of the attribute.
253
* @return array Multiple values for an attribute.
254
* @throws Horde_Icalendar_Exception
256
public function getAttributeValues($name)
259
foreach ($this->_attributes as $attribute) {
260
if ($attribute['name'] == $name) {
261
$result = array_merge($attribute['values'], $result);
265
if (!count($result)) {
266
throw new Horde_Icalendar_Exception('Attribute "' . $name . '" Not Found');
273
* Returns the value of an attribute, or a specified default value
274
* if the attribute does not exist.
276
* @param string $name The name of the attribute.
277
* @param mixed $default What to return if the attribute specified by
278
* $name does not exist.
280
* @return mixed (string) The value of $name.
281
* (mixed) $default if $name does not exist.
283
public function getAttributeDefault($name, $default = '')
286
return $this->getAttribute($name);
287
} catch (Horde_Icalendar_Exception $e) {
293
* Remove all occurences of an attribute.
295
* @param string $name The name of the attribute.
297
public function removeAttribute($name)
299
foreach (array_keys($this->_attributes) as $key) {
300
if ($this->_attributes[$key]['name'] == $name) {
301
unset($this->_attributes[$key]);
307
* Get attributes for all tags or for a given tag.
309
* @param string $tag Return attributes for this tag, or all attributes
312
* @return array An array containing all the attributes and their types.
314
public function getAllAttributes($tag = false)
316
if ($tag === false) {
317
return $this->_attributes;
321
foreach ($this->_attributes as $attribute) {
322
if ($attribute['name'] == $tag) {
323
$result[] = $attribute;
331
* Add a vCalendar component (eg vEvent, vTimezone, etc.).
333
* @param mixed Either a Horde_Icalendar component (subclass) or an array
336
public function addComponent($components)
338
if (!is_array($components)) {
339
$components = array($components);
342
foreach ($components as $component) {
343
if ($component instanceof Horde_Icalendar) {
344
$component->_container = $this;
345
$this->_components[] = $component;
351
* Retrieve all the components.
353
* @return array Array of Horde_Icalendar objects.
355
public function getComponents()
357
return $this->_components;
365
public function getType()
371
* Return the classes (entry types) we have.
373
* @return array Hash with class names Horde_Icalendar_xxx as keys
374
* and number of components of this class as value.
376
public function getComponentClasses()
380
foreach ($this->_components as $c) {
381
$cn = strtolower(get_class($c));
382
if (empty($r[$cn])) {
393
* Number of components in this container.
395
* @return integer Number of components in this container.
397
public function getComponentCount()
399
return count($this->_components);
403
* Retrieve a specific component.
405
* @param integer $idx The index of the object to retrieve.
407
* @return mixed (boolean) False if the index does not exist.
408
* (Horde_Icalendar_*) The requested component.
410
public function getComponent($idx)
412
return isset($this->_components[$idx])
413
? $this->_components[$idx]
418
* Locates the first child component of the specified class, and returns a
421
* @param string $type The type of component to find.
423
* @return boolean|Horde_Icalendar_* False if no subcomponent of the
424
* specified class exists or the
425
* requested component.
427
public function findComponent($childclass)
429
$childclass = __CLASS__ . '_' . Horde_String::lower($childclass);
431
foreach (array_keys($this->_components) as $key) {
432
if ($this->_components[$key] instanceof $childclass) {
433
return $this->_components[$key];
441
* Locates the first matching child component of the specified class, and
442
* returns a reference to it.
444
* @param string $childclass The type of component to find.
445
* @param string $attribute This attribute must be set in the component
447
* @param string $value Optional value that $attribute must match.
449
* @return boolean|Horde_Icalendar_* False if no matching subcomponent
450
* of the specified class exists, or
451
* the requested component.
453
public function findComponentByAttribute($childclass, $attribute,
456
$childclass = __CLASS__ . '_' . Horde_String::lower($childclass);
458
foreach (array_keys($this->_components) as $key) {
459
if ($this->_components[$key] instanceof $childclass) {
461
$attr = $this->_components[$key]->getAttribute($attribute);
462
} catch (Horde_Icalendar_Exception $e) {
466
if (is_null($value) || $value == $attr) {
467
return $this->_components[$key];
476
* Clears the iCalendar object (resets the components and attributes
479
public function clear()
481
$this->_attributes = $this->_components = array();
484
public function toString() { return $this->exportvCalendar(); }
486
* Export as vCalendar format.
490
public function exportvCalendar()
493
// TODO: HORDE_VERSION does not exist.
494
$requiredAttributes['PRODID'] = '-//The Horde Project//Horde iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
496
foreach ($requiredAttributes as $name => $default_value) {
498
$this->getAttribute($name);
499
} catch (Horde_Icalendar_Exception $e) {
500
$this->setAttribute($name, $default_value);
504
return $this->_exportvData('VCALENDAR');
508
* Export this entry as a hash array with tag names as keys.
510
* @param boolean $paramsInKeys If false, the operation can be quite
511
* lossy as the parameters are ignored when
512
* building the array keys.
513
* So if you export a vcard with
514
* LABEL;TYPE=WORK:foo
515
* LABEL;TYPE=HOME:bar
516
* the resulting hash contains only one
518
* If set to true, array keys look like
521
* @return array A hash array with tag names as keys.
523
public function toHash($paramsInKeys = false)
527
foreach ($this->_attributes as $a) {
529
if ($paramsInKeys && is_array($a['params'])) {
530
foreach ($a['params'] as $p => $v) {
534
$hash[$k] = $a['value'];
541
* Parses a string containing vCalendar data.
543
* @todo This method doesn't work well at all, if $base is VCARD.
545
* @param string $text The data to parse.
546
* @param string $base The type of the base object.
547
* @param boolean $clear If true clears this object before parsing.
549
* @return boolean True on successful import, false otherwise.
550
* @throws Horde_Icalendar_Exception
552
public function parsevCalendar($text, $base = 'VCALENDAR', $clear = true)
558
$text = Horde_String::trimUtf8Bom($text);
560
if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
564
// Text isn't enclosed in BEGIN:VCALENDAR
565
// .. END:VCALENDAR. We'll try to parse it anyway.
571
// Extract all subcomponents.
572
$matches = $components = null;
573
if (preg_match_all('/^BEGIN:(.*)\s*?(\r\n|\r|\n)(.*)^END:\1\s*?/Uims', $vCal, $components)) {
574
foreach ($components[0] as $key => $data) {
575
// Remove from the vCalendar data.
576
$vCal = str_replace($data, '', $vCal);
578
} elseif (!$container) {
582
// Unfold "quoted printable" folded lines like:
583
// BODY;ENCODING=QUOTED-PRINTABLE:=
586
while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?(\r?\n|$))/mU', $vCal, $matches)) {
587
foreach ($matches[1] as $s) {
588
$r = preg_replace('/=\r?\n/', '', $s);
589
$vCal = str_replace($s, $r, $vCal);
593
// Unfold any folded lines.
594
$vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
596
// Parse the remaining attributes.
597
if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
598
foreach ($matches[0] as $attribute) {
599
preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
600
$tag = trim(preg_replace('/^.*\./', '', Horde_String::upper($parts[1])));
605
if (!empty($parts[2])) {
606
preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
607
foreach ($param_parts[2] as $key => $paramName) {
608
$paramName = Horde_String::upper($paramName);
609
$paramValue = $param_parts[4][$key];
610
if ($paramName == 'TYPE') {
611
$paramValue = preg_split('/(?<!\\\\),/', $paramValue);
612
if (count($paramValue) == 1) {
613
$paramValue = $paramValue[0];
616
if (is_string($paramValue)) {
617
if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
618
$paramValue = $parts[1];
621
foreach ($paramValue as $k => $tmp) {
622
if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
623
$paramValue[$k] = $parts[1];
627
if (isset($params[$paramName])) {
628
if (is_array($params[$paramName])) {
629
$params[$paramName][] = $paramValue;
631
$params[$paramName] = array($params[$paramName], $paramValue);
634
$params[$paramName] = $paramValue;
639
// Charset and encoding handling.
640
if ((isset($params['ENCODING']) &&
641
Horde_String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
642
isset($params['QUOTED-PRINTABLE'])) {
644
$value = quoted_printable_decode($value);
645
if (isset($params['CHARSET'])) {
646
$value = Horde_String::convertCharset($value, $params['CHARSET'], 'UTF-8');
648
} elseif (isset($params['CHARSET'])) {
649
$value = Horde_String::convertCharset($value, $params['CHARSET'], 'UTF-8');
652
// Get timezone info for date fields from $params.
653
$tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
659
case 'LAST-MODIFIED':
660
case 'X-MOZ-LASTACK':
661
case 'X-MOZ-SNOOZE-TIME':
662
$this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
666
case 'X-ANNIVERSARY':
667
$this->setAttribute($tag, $this->_parseDate($value), $params);
675
case 'RECURRENCE-ID':
676
// types like AALARM may contain additional data after a ;
678
$ts = explode(';', $value);
679
if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
680
$this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
682
$this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
687
if (isset($params['VALUE']) &&
688
$params['VALUE'] == 'DATE-TIME') {
689
$this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
691
$this->setAttribute($tag, $this->_parseDuration($value), $params);
695
// Comma seperated dates.
698
if (!strlen($value)) {
702
$separator = $this->_oldFormat ? ';' : ',';
703
preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
705
foreach ($values[1] as $value) {
706
$stamp = $this->_parseDateTime($value);
707
if (!is_int($stamp)) {
710
$dates[] = array('year' => date('Y', $stamp),
711
'month' => date('m', $stamp),
712
'mday' => date('d', $stamp));
714
$this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
719
$this->setAttribute($tag, $this->_parseDuration($value), $params);
722
// Period of time fields.
725
preg_match_all('/,([^,]*)/', ',' . $value, $values);
726
foreach ($values[1] as $value) {
727
$periods[] = $this->_parsePeriod($value);
730
$this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
733
// UTC offset fields.
736
$this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
740
case 'PERCENT-COMPLETE':
744
$this->setAttribute($tag, intval($value), $params);
750
if ($this->_oldFormat) {
751
$floats = explode(',', $value);
752
$value = array('latitude' => floatval($floats[1]),
753
'longitude' => floatval($floats[0]));
755
$floats = explode(';', $value);
756
$value = array('latitude' => floatval($floats[0]),
757
'longitude' => floatval($floats[1]));
760
$this->setAttribute($tag, $value, $params);
766
$this->setAttribute($tag, trim($value), $params);
769
// ADR, ORG and N are lists seperated by unescaped semicolons
770
// with a specific number of slots.
774
$value = trim($value);
775
// As of rfc 2426 2.4.2 semicolon, comma, and colon must
776
// be escaped (comma is unescaped after splitting below).
777
$value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
778
array($this->_newline, $this->_newline, ';', ':'),
781
// Split by unescaped semicolons:
782
$values = preg_split('/(?<!\\\\);/', $value);
783
$value = str_replace('\\;', ';', $value);
784
$values = str_replace('\\;', ';', $values);
785
$this->setAttribute($tag, trim($value), $params, true, $values);
790
if ($this->_oldFormat) {
791
// vCalendar 1.0 and vCard 2.1 only escape semicolons
792
// and use unescaped semicolons to create lists.
793
$value = trim($value);
794
// Split by unescaped semicolons:
795
$values = preg_split('/(?<!\\\\);/', $value);
796
$value = str_replace('\\;', ';', $value);
797
$values = str_replace('\\;', ';', $values);
798
$this->setAttribute($tag, trim($value), $params, true, $values);
800
$value = trim($value);
801
// As of rfc 2426 2.4.2 semicolon, comma, and colon
802
// must be escaped (comma is unescaped after splitting
804
$value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
805
array($this->_newline, $this->_newline, ';', ':', '\\'),
808
// Split by unescaped commas.
809
$values = preg_split('/(?<!\\\\),/', $value);
810
$value = str_replace('\\,', ',', $value);
811
$values = str_replace('\\,', ',', $values);
813
$this->setAttribute($tag, trim($value), $params, true, $values);
820
// Process all components.
822
// vTimezone components are processed first. They are
823
// needed to process vEvents that may use a TZID.
824
foreach ($components[0] as $key => $data) {
825
$type = trim($components[1][$key]);
826
if ($type != 'VTIMEZONE') {
829
$component = $this->newComponent($type, $this);
830
if ($component === false) {
831
throw new Horde_Icalendar_Exception('Unable to create object for type ' . $type);
833
$component->parsevCalendar($data, $type);
835
$this->addComponent($component);
837
// Remove from the vCalendar data.
838
$vCal = str_replace($data, '', $vCal);
841
// Now process the non-vTimezone components.
842
foreach ($components[0] as $key => $data) {
843
$type = trim($components[1][$key]);
844
if ($type == 'VTIMEZONE') {
847
$component = $this->newComponent($type, $this);
848
if ($component === false) {
849
throw new Horde_Icalendar_Exception('Unable to create object for type ' . $type);
851
$component->parsevCalendar($data, $type);
853
$this->addComponent($component);
861
* Export this component in vCal format.
863
* @param string $base The type of the base object.
865
* @return string vCal format data.
867
protected function _exportvData($base = 'VCALENDAR')
869
$result = 'BEGIN:' . Horde_String::upper($base) . $this->_newline;
871
// VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
872
// as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
873
if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
874
$base !== 'VJOURNAL' && $base !== 'VFREEBUSY' &&
875
$base != 'VTIMEZONE' && $base != 'STANDARD' && $base != 'DAYLIGHT') {
876
// Ensure that version is the first attribute.
877
$result .= 'VERSION:' . $this->_version . $this->_newline;
879
foreach ($this->_attributes as $attribute) {
880
$name = $attribute['name'];
881
if ($name == 'VERSION') {
887
$params = $attribute['params'];
889
foreach ($params as $param_name => $param_value) {
890
/* Skip CHARSET for iCalendar 2.0 data, not allowed. */
891
if ($param_name == 'CHARSET' && !$this->_oldFormat) {
894
/* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
895
if ($this->_oldFormat &&
896
$param_name == 'VALUE' && $param_value == 'DATE') {
900
if ($param_value === null) {
901
$params_str .= ";$param_name";
903
if (!is_array($param_value)) {
904
$param_value = array($param_value);
906
foreach ($param_value as &$one_param_value) {
907
$len = strlen($one_param_value);
910
for ($i = 0; $i < $len; ++$i) {
911
$ord = ord($one_param_value[$i]);
912
// Accept only valid characters.
913
if ($ord == 9 || $ord == 32 || $ord == 33 ||
914
($ord >= 35 && $ord <= 126) ||
916
$safe_value .= $one_param_value[$i];
917
// Characters above 128 do not need to be
918
// quoted as per RFC2445 but Outlook requires
920
if ($ord == 44 || $ord == 58 || $ord == 59 ||
927
$safe_value = '"' . $safe_value . '"';
929
$one_param_value = $safe_value;
931
$params_str .= ";$param_name=" . implode(',', $param_value);
936
$value = $attribute['value'];
942
case 'LAST-MODIFIED':
943
case 'X-MOZ-LASTACK':
944
case 'X-MOZ-SNOOZE-TIME':
945
$value = $this->_exportDateTime($value);
953
case 'RECURRENCE-ID':
954
$floating = $base == 'STANDARD'
955
|| $base == 'DAYLIGHT'
956
|| isset($params['TZID']);
957
if (isset($params['VALUE'])) {
958
if ($params['VALUE'] == 'DATE') {
959
// VCALENDAR 1.0 uses T000000 - T235959 for all day events:
960
if ($this->_oldFormat && $name == 'DTEND') {
961
$d = new Horde_Date($value);
962
$value = new Horde_Date(array(
964
'month' => $d->month,
965
'mday' => $d->mday - 1));
966
$value = $this->_exportDate($value, '235959');
968
$value = $this->_exportDate($value, '000000');
971
$value = $this->_exportDateTime($value, $floating);
974
$value = $this->_exportDateTime($value, $floating);
978
// Comma seperated dates.
981
$floating = $base == 'STANDARD' || $base == 'DAYLIGHT';
983
foreach ($value as $date) {
984
if (isset($params['VALUE'])) {
985
if ($params['VALUE'] == 'DATE') {
986
$dates[] = $this->_exportDate($date, '000000');
987
} elseif ($params['VALUE'] == 'PERIOD') {
988
$dates[] = $this->_exportPeriod($date);
990
$dates[] = $this->_exportDateTime($date, $floating);
993
$dates[] = $this->_exportDateTime($date, $floating);
996
$value = implode($this->_oldFormat ? ';' : ',', $dates);
1000
if (isset($params['VALUE'])) {
1001
if ($params['VALUE'] == 'DATE-TIME') {
1002
$value = $this->_exportDateTime($value);
1003
} elseif ($params['VALUE'] == 'DURATION') {
1004
$value = $this->_exportDuration($value);
1007
$value = $this->_exportDuration($value);
1013
$value = $this->_exportDuration($value);
1016
// Period of time fields.
1019
foreach ($value as $period) {
1020
$value_str .= empty($value_str) ? '' : ',';
1021
$value_str .= $this->_exportPeriod($period);
1023
$value = $value_str;
1026
// UTC offset fields.
1027
case 'TZOFFSETFROM':
1029
$value = $this->_exportUtcOffset($value);
1033
case 'PERCENT-COMPLETE':
1042
if ($this->_oldFormat) {
1043
$value = $value['longitude'] . ',' . $value['latitude'];
1045
$value = $value['latitude'] . ';' . $value['longitude'];
1049
// Recurrence fields.
1055
if ($this->_oldFormat) {
1056
if (is_array($attribute['values']) &&
1057
count($attribute['values']) > 1) {
1058
$values = $attribute['values'];
1059
if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
1064
$values = str_replace(';', '\\;', $values);
1065
$value = implode($glue, $values);
1067
/* vcard 2.1 and vcalendar 1.0 escape only
1069
$value = str_replace(';', '\\;', $value);
1071
// Text containing newlines or ASCII >= 127 must be BASE64
1072
// or QUOTED-PRINTABLE encoded. Currently we use
1073
// QUOTED-PRINTABLE as default.
1074
if (preg_match("/[^\x20-\x7F]/", $value) &&
1075
empty($params['ENCODING'])) {
1076
$params['ENCODING'] = 'QUOTED-PRINTABLE';
1077
$params_str .= ';ENCODING=QUOTED-PRINTABLE';
1078
// Add CHARSET as well. At least the synthesis client
1079
// gets confused otherwise
1080
if (empty($params['CHARSET'])) {
1081
$params['CHARSET'] = 'UTF-8';
1082
$params_str .= ';CHARSET=' . $params['CHARSET'];
1086
if (is_array($attribute['values']) &&
1087
count($attribute['values'])) {
1088
$values = $attribute['values'];
1089
if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
1094
// As of rfc 2426 2.5 semicolon and comma must be
1096
$values = str_replace(array('\\', ';', ','),
1097
array('\\\\', '\\;', '\\,'),
1099
$value = implode($glue, $values);
1101
// As of rfc 2426 2.5 semicolon and comma must be
1103
$value = str_replace(array('\\', ';', ','),
1104
array('\\\\', '\\;', '\\,'),
1107
$value = preg_replace('/\r?\n/', '\n', $value);
1112
$value = str_replace("\r", '', $value);
1113
if (!empty($params['ENCODING']) &&
1114
$params['ENCODING'] == 'QUOTED-PRINTABLE' &&
1115
strlen(trim($value))) {
1116
$result .= $name . $params_str . ':'
1117
. preg_replace(array('/(?<!\r)\n/', '/(?<!=)\r\n/'),
1118
array("\r\n", "=0D=0A=\r\n "),
1119
Horde_Mime::quotedPrintableEncode($value))
1122
$attr_string = $name . $params_str . ':' . $value;
1123
if (!$this->_oldFormat) {
1124
if (isset($params['ENCODING']) && $params['ENCODING'] == 'b') {
1125
$attr_string = trim(chunk_split($attr_string, 75, $this->_newline . ' '));
1127
$attr_string = Horde_String::wordwrap($attr_string, 75, $this->_newline . ' ', true, true);
1130
$result .= $attr_string . $this->_newline;
1135
foreach ($this->_components as $component) {
1136
if (!($component instanceof Horde_Icalendar_Vtimezone) ||
1137
!isset($tzs[$component->getAttribute('TZID')])) {
1138
$result .= $component->exportvCalendar();
1139
if ($component instanceof Horde_Icalendar_Vtimezone) {
1140
$tzs[$component->getAttribute('TZID')] = true;
1145
return $result . 'END:' . $base . $this->_newline;
1149
* Parse a UTC Offset field.
1155
protected function _parseUtcOffset($text)
1159
if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
1160
$offset['ahead'] = (bool)($timeParts[1] == '+');
1161
$offset['hour'] = intval($timeParts[2]);
1162
$offset['minute'] = intval($timeParts[3]);
1163
if (isset($timeParts[4])) {
1164
$offset['second'] = intval($timeParts[4]);
1173
* Export a UTC Offset field.
1175
* @param $value TODO
1179
function _exportUtcOffset($value)
1181
$offset = ($value['ahead'] ? '+' : '-') .
1182
sprintf('%02d%02d', $value['hour'], $value['minute']);
1184
if (isset($value['second'])) {
1185
$offset .= sprintf('%02d', $value['second']);
1192
* Parse a Time Period field.
1196
* @return array TODO
1198
protected function _parsePeriod($text)
1200
$periodParts = explode('/', $text);
1201
$start = $this->_parseDateTime($periodParts[0]);
1203
if ($duration = $this->_parseDuration($periodParts[1])) {
1204
return array('start' => $start, 'duration' => $duration);
1205
} elseif ($end = $this->_parseDateTime($periodParts[1])) {
1206
return array('start' => $start, 'end' => $end);
1211
* Export a Time Period field.
1213
* @param $value TODO
1217
protected function _exportPeriod($value)
1219
$period = $this->_exportDateTime($value['start']) . '/';
1221
return isset($value['duration'])
1222
? $period . $this->_exportDuration($value['duration'])
1223
: $period . $this->_exportDateTime($value['end']);
1227
* Grok the TZID and return an offset in seconds from UTC for this
1236
protected function _parseTZID($date, $time, $tzid)
1238
$vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
1243
$change_times = array();
1244
foreach ($vtimezone->getComponents() as $o) {
1245
$change_times = array_merge(
1247
$vtimezone->parseChild($o, $date['year'])
1251
if (!$change_times) {
1255
sort($change_times);
1257
// Time is arbitrarily based on UTC for comparison.
1258
$t = @gmmktime($time['hour'], $time['minute'], $time['second'],
1259
$date['month'], $date['mday'], $date['year']);
1261
if ($t < $change_times[0]['time']) {
1262
return $change_times[0]['from'];
1265
for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
1266
if (($t >= $change_times[$i]['time']) &&
1267
($t < $change_times[$i + 1]['time'])) {
1268
return $change_times[$i]['to'];
1272
if ($t >= $change_times[$n - 1]['time']) {
1273
return $change_times[$n - 1]['to'];
1280
* Parses a DateTime field and returns a unix timestamp. If the
1281
* field cannot be parsed then the original text is returned
1284
* @todo This function should be moved to Horde_Date and made public.
1291
public function _parseDateTime($text, $tzid = false)
1293
$dateParts = explode('T', $text);
1294
if (count($dateParts) != 2 && !empty($text)) {
1295
// Not a datetime field but may be just a date field.
1296
if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text)) {
1300
$dateParts = array($text, '000000');
1303
if (!($date = $this->_parseDate($dateParts[0])) ||
1304
!($time = $this->_parseTime($dateParts[1]))) {
1308
// Get timezone info for date fields from $tzid and container.
1309
$tzoffset = ($time['zone'] == 'Local' && $tzid &&
1310
($this->_container instanceof Horde_Icalendar))
1311
? $this->_parseTZID($date, $time, $tzid)
1313
if ($time['zone'] == 'UTC' || $tzoffset !== false) {
1314
$result = @gmmktime($time['hour'], $time['minute'], $time['second'],
1315
$date['month'], $date['mday'], $date['year']);
1316
if ($result !== false && $tzoffset) {
1317
$result -= $tzoffset;
1320
// We don't know the timezone so assume local timezone.
1321
$result = @mktime($time['hour'], $time['minute'], $time['second'],
1322
$date['month'], $date['mday'], $date['year']);
1325
return ($result !== false) ? $result : $text;
1329
* Export a DateTime field.
1331
* @todo A bunch of code calls this function outside this class, so it
1332
* needs to be marked public for now.
1334
* @param integer|object|array $value The time value to export (either a
1335
* Horde_Date, array, or timestamp).
1336
* @param boolean $floating Whether to return a floating
1337
* date-time (without time zone
1340
* @return string The string representation of the datetime value.
1342
public function _exportDateTime($value, $floating = false)
1344
$date = new Horde_Date($value);
1345
return $date->toICalendar($floating);
1349
* Parses a Time field.
1355
protected function _parseTime($text)
1357
if (!preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
1362
'hour' => $timeParts[1],
1363
'minute' => $timeParts[2],
1364
'second' => $timeParts[3],
1365
'zone' => isset($timeParts[4]) ? 'UTC' : 'Local'
1370
* Parses a Date field.
1374
* @return array TODO
1376
public function _parseDate($text)
1378
$parts = explode('T', $text);
1379
if (count($parts) == 2) {
1383
if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
1388
'year' => $match[1],
1389
'month' => $match[2],
1395
* Exports a date field.
1397
* @param object|array $value Date object or hash.
1398
* @param string $autoconvert If set, use this as time part to export the
1399
* date as datetime when exporting to Vcalendar
1400
* 1.0. Examples: '000000' or '235959'
1404
protected function _exportDate($value, $autoconvert = false)
1406
if (is_object($value)) {
1407
$value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
1410
return ($autoconvert !== false && $this->_oldFormat)
1411
? sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert)
1412
: sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
1416
* Parses a DURATION value field.
1418
* @param string $text A DURATION value.
1420
* @return integer The duration in seconds.
1422
protected function _parseDuration($text)
1424
if (!preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
1429
$duration = 7 * 86400 * intval($durvalue[3]);
1431
if (count($durvalue) > 4) {
1433
$duration += 86400 * intval($durvalue[4]);
1436
if (count($durvalue) > 5) {
1438
$duration += 3600 * intval($durvalue[7]);
1441
if (isset($durvalue[8])) {
1442
$duration += 60 * intval($durvalue[8]);
1446
if (isset($durvalue[9])) {
1447
$duration += intval($durvalue[9]);
1452
if ($durvalue[1] == "-") {
1460
* Export a duration value.
1462
* @param $value TODO
1464
protected function _exportDuration($value)
1473
$weeks = floor($value / (7 * 86400));
1474
$value = $value % (7 * 86400);
1476
$duration .= $weeks . 'W';
1479
$days = floor($value / (86400));
1480
$value = $value % (86400);
1482
$duration .= $days . 'D';
1488
$hours = floor($value / 3600);
1489
$value = $value % 3600;
1491
$duration .= $hours . 'H';
1494
$mins = floor($value / 60);
1495
$value = $value % 60;
1497
$duration .= $mins . 'M';
1501
$duration .= $value . 'S';