@@ -13,18 +13,18 @@ |
||
13 | 13 | */ |
14 | 14 | class DateTime extends DateAndOrTime { |
15 | 15 | |
16 | - /** |
|
17 | - * Returns the type of value. |
|
18 | - * |
|
19 | - * This corresponds to the VALUE= parameter. Every property also has a |
|
20 | - * 'default' valueType. |
|
21 | - * |
|
22 | - * @return string |
|
23 | - */ |
|
24 | - public function getValueType() { |
|
16 | + /** |
|
17 | + * Returns the type of value. |
|
18 | + * |
|
19 | + * This corresponds to the VALUE= parameter. Every property also has a |
|
20 | + * 'default' valueType. |
|
21 | + * |
|
22 | + * @return string |
|
23 | + */ |
|
24 | + public function getValueType() { |
|
25 | 25 | |
26 | - return 'DATE-TIME'; |
|
26 | + return 'DATE-TIME'; |
|
27 | 27 | |
28 | - } |
|
28 | + } |
|
29 | 29 | |
30 | 30 | } |
@@ -15,130 +15,130 @@ |
||
15 | 15 | */ |
16 | 16 | class Time extends Text { |
17 | 17 | |
18 | - /** |
|
19 | - * In case this is a multi-value property. This string will be used as a |
|
20 | - * delimiter. |
|
21 | - * |
|
22 | - * @var string|null |
|
23 | - */ |
|
24 | - public $delimiter = null; |
|
25 | - |
|
26 | - /** |
|
27 | - * Returns the type of value. |
|
28 | - * |
|
29 | - * This corresponds to the VALUE= parameter. Every property also has a |
|
30 | - * 'default' valueType. |
|
31 | - * |
|
32 | - * @return string |
|
33 | - */ |
|
34 | - public function getValueType() { |
|
35 | - |
|
36 | - return 'TIME'; |
|
37 | - |
|
38 | - } |
|
39 | - |
|
40 | - /** |
|
41 | - * Sets the JSON value, as it would appear in a jCard or jCal object. |
|
42 | - * |
|
43 | - * The value must always be an array. |
|
44 | - * |
|
45 | - * @param array $value |
|
46 | - * |
|
47 | - * @return void |
|
48 | - */ |
|
49 | - public function setJsonValue(array $value) { |
|
50 | - |
|
51 | - // Removing colons from value. |
|
52 | - $value = str_replace( |
|
53 | - ':', |
|
54 | - '', |
|
55 | - $value |
|
56 | - ); |
|
57 | - |
|
58 | - if (count($value) === 1) { |
|
59 | - $this->setValue(reset($value)); |
|
60 | - } else { |
|
61 | - $this->setValue($value); |
|
62 | - } |
|
63 | - |
|
64 | - } |
|
65 | - |
|
66 | - /** |
|
67 | - * Returns the value, in the format it should be encoded for json. |
|
68 | - * |
|
69 | - * This method must always return an array. |
|
70 | - * |
|
71 | - * @return array |
|
72 | - */ |
|
73 | - public function getJsonValue() { |
|
74 | - |
|
75 | - $parts = DateTimeParser::parseVCardTime($this->getValue()); |
|
76 | - $timeStr = ''; |
|
77 | - |
|
78 | - // Hour |
|
79 | - if (!is_null($parts['hour'])) { |
|
80 | - $timeStr .= $parts['hour']; |
|
81 | - |
|
82 | - if (!is_null($parts['minute'])) { |
|
83 | - $timeStr .= ':'; |
|
84 | - } |
|
85 | - } else { |
|
86 | - // We know either minute or second _must_ be set, so we insert a |
|
87 | - // dash for an empty value. |
|
88 | - $timeStr .= '-'; |
|
89 | - } |
|
90 | - |
|
91 | - // Minute |
|
92 | - if (!is_null($parts['minute'])) { |
|
93 | - $timeStr .= $parts['minute']; |
|
94 | - |
|
95 | - if (!is_null($parts['second'])) { |
|
96 | - $timeStr .= ':'; |
|
97 | - } |
|
98 | - } else { |
|
99 | - if (isset($parts['second'])) { |
|
100 | - // Dash for empty minute |
|
101 | - $timeStr .= '-'; |
|
102 | - } |
|
103 | - } |
|
104 | - |
|
105 | - // Second |
|
106 | - if (!is_null($parts['second'])) { |
|
107 | - $timeStr .= $parts['second']; |
|
108 | - } |
|
109 | - |
|
110 | - // Timezone |
|
111 | - if (!is_null($parts['timezone'])) { |
|
112 | - if ($parts['timezone'] === 'Z') { |
|
113 | - $timeStr .= 'Z'; |
|
114 | - } else { |
|
115 | - $timeStr .= |
|
116 | - preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); |
|
117 | - } |
|
118 | - } |
|
119 | - |
|
120 | - return [$timeStr]; |
|
121 | - |
|
122 | - } |
|
123 | - |
|
124 | - /** |
|
125 | - * Hydrate data from a XML subtree, as it would appear in a xCard or xCal |
|
126 | - * object. |
|
127 | - * |
|
128 | - * @param array $value |
|
129 | - * |
|
130 | - * @return void |
|
131 | - */ |
|
132 | - public function setXmlValue(array $value) { |
|
133 | - |
|
134 | - $value = array_map( |
|
135 | - public function($value) { |
|
136 | - return str_replace(':', '', $value); |
|
137 | - }, |
|
138 | - $value |
|
139 | - ); |
|
140 | - parent::setXmlValue($value); |
|
141 | - |
|
142 | - } |
|
18 | + /** |
|
19 | + * In case this is a multi-value property. This string will be used as a |
|
20 | + * delimiter. |
|
21 | + * |
|
22 | + * @var string|null |
|
23 | + */ |
|
24 | + public $delimiter = null; |
|
25 | + |
|
26 | + /** |
|
27 | + * Returns the type of value. |
|
28 | + * |
|
29 | + * This corresponds to the VALUE= parameter. Every property also has a |
|
30 | + * 'default' valueType. |
|
31 | + * |
|
32 | + * @return string |
|
33 | + */ |
|
34 | + public function getValueType() { |
|
35 | + |
|
36 | + return 'TIME'; |
|
37 | + |
|
38 | + } |
|
39 | + |
|
40 | + /** |
|
41 | + * Sets the JSON value, as it would appear in a jCard or jCal object. |
|
42 | + * |
|
43 | + * The value must always be an array. |
|
44 | + * |
|
45 | + * @param array $value |
|
46 | + * |
|
47 | + * @return void |
|
48 | + */ |
|
49 | + public function setJsonValue(array $value) { |
|
50 | + |
|
51 | + // Removing colons from value. |
|
52 | + $value = str_replace( |
|
53 | + ':', |
|
54 | + '', |
|
55 | + $value |
|
56 | + ); |
|
57 | + |
|
58 | + if (count($value) === 1) { |
|
59 | + $this->setValue(reset($value)); |
|
60 | + } else { |
|
61 | + $this->setValue($value); |
|
62 | + } |
|
63 | + |
|
64 | + } |
|
65 | + |
|
66 | + /** |
|
67 | + * Returns the value, in the format it should be encoded for json. |
|
68 | + * |
|
69 | + * This method must always return an array. |
|
70 | + * |
|
71 | + * @return array |
|
72 | + */ |
|
73 | + public function getJsonValue() { |
|
74 | + |
|
75 | + $parts = DateTimeParser::parseVCardTime($this->getValue()); |
|
76 | + $timeStr = ''; |
|
77 | + |
|
78 | + // Hour |
|
79 | + if (!is_null($parts['hour'])) { |
|
80 | + $timeStr .= $parts['hour']; |
|
81 | + |
|
82 | + if (!is_null($parts['minute'])) { |
|
83 | + $timeStr .= ':'; |
|
84 | + } |
|
85 | + } else { |
|
86 | + // We know either minute or second _must_ be set, so we insert a |
|
87 | + // dash for an empty value. |
|
88 | + $timeStr .= '-'; |
|
89 | + } |
|
90 | + |
|
91 | + // Minute |
|
92 | + if (!is_null($parts['minute'])) { |
|
93 | + $timeStr .= $parts['minute']; |
|
94 | + |
|
95 | + if (!is_null($parts['second'])) { |
|
96 | + $timeStr .= ':'; |
|
97 | + } |
|
98 | + } else { |
|
99 | + if (isset($parts['second'])) { |
|
100 | + // Dash for empty minute |
|
101 | + $timeStr .= '-'; |
|
102 | + } |
|
103 | + } |
|
104 | + |
|
105 | + // Second |
|
106 | + if (!is_null($parts['second'])) { |
|
107 | + $timeStr .= $parts['second']; |
|
108 | + } |
|
109 | + |
|
110 | + // Timezone |
|
111 | + if (!is_null($parts['timezone'])) { |
|
112 | + if ($parts['timezone'] === 'Z') { |
|
113 | + $timeStr .= 'Z'; |
|
114 | + } else { |
|
115 | + $timeStr .= |
|
116 | + preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); |
|
117 | + } |
|
118 | + } |
|
119 | + |
|
120 | + return [$timeStr]; |
|
121 | + |
|
122 | + } |
|
123 | + |
|
124 | + /** |
|
125 | + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal |
|
126 | + * object. |
|
127 | + * |
|
128 | + * @param array $value |
|
129 | + * |
|
130 | + * @return void |
|
131 | + */ |
|
132 | + public function setXmlValue(array $value) { |
|
133 | + |
|
134 | + $value = array_map( |
|
135 | + public function($value) { |
|
136 | + return str_replace(':', '', $value); |
|
137 | + }, |
|
138 | + $value |
|
139 | + ); |
|
140 | + parent::setXmlValue($value); |
|
141 | + |
|
142 | + } |
|
143 | 143 | |
144 | 144 | } |
@@ -16,107 +16,107 @@ |
||
16 | 16 | */ |
17 | 17 | class Uri extends Text { |
18 | 18 | |
19 | - /** |
|
20 | - * In case this is a multi-value property. This string will be used as a |
|
21 | - * delimiter. |
|
22 | - * |
|
23 | - * @var string|null |
|
24 | - */ |
|
25 | - public $delimiter = null; |
|
26 | - |
|
27 | - /** |
|
28 | - * Returns the type of value. |
|
29 | - * |
|
30 | - * This corresponds to the VALUE= parameter. Every property also has a |
|
31 | - * 'default' valueType. |
|
32 | - * |
|
33 | - * @return string |
|
34 | - */ |
|
35 | - public function getValueType() { |
|
36 | - |
|
37 | - return 'URI'; |
|
38 | - |
|
39 | - } |
|
40 | - |
|
41 | - /** |
|
42 | - * Returns an iterable list of children. |
|
43 | - * |
|
44 | - * @return array |
|
45 | - */ |
|
46 | - public function parameters() { |
|
47 | - |
|
48 | - $parameters = parent::parameters(); |
|
49 | - if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'])) { |
|
50 | - // If we are encoding a URI value, and this URI value has no |
|
51 | - // VALUE=URI parameter, we add it anyway. |
|
52 | - // |
|
53 | - // This is not required by any spec, but both Apple iCal and Apple |
|
54 | - // AddressBook (at least in version 10.8) will trip over this if |
|
55 | - // this is not set, and so it improves compatibility. |
|
56 | - // |
|
57 | - // See Issue #227 and #235 |
|
58 | - $parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI'); |
|
59 | - } |
|
60 | - return $parameters; |
|
61 | - |
|
62 | - } |
|
63 | - |
|
64 | - /** |
|
65 | - * Sets a raw value coming from a mimedir (iCalendar/vCard) file. |
|
66 | - * |
|
67 | - * This has been 'unfolded', so only 1 line will be passed. Unescaping is |
|
68 | - * not yet done, but parameters are not included. |
|
69 | - * |
|
70 | - * @param string $val |
|
71 | - * |
|
72 | - * @return void |
|
73 | - */ |
|
74 | - public function setRawMimeDirValue($val) { |
|
75 | - |
|
76 | - // Normally we don't need to do any type of unescaping for these |
|
77 | - // properties, however.. we've noticed that Google Contacts |
|
78 | - // specifically escapes the colon (:) with a blackslash. While I have |
|
79 | - // no clue why they thought that was a good idea, I'm unescaping it |
|
80 | - // anyway. |
|
81 | - // |
|
82 | - // Good thing backslashes are not allowed in urls. Makes it easy to |
|
83 | - // assume that a backslash is always intended as an escape character. |
|
84 | - if ($this->name === 'URL') { |
|
85 | - $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; |
|
86 | - $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); |
|
87 | - $newVal = ''; |
|
88 | - foreach ($matches as $match) { |
|
89 | - switch ($match) { |
|
90 | - case '\:' : |
|
91 | - $newVal .= ':'; |
|
92 | - break; |
|
93 | - default : |
|
94 | - $newVal .= $match; |
|
95 | - break; |
|
96 | - } |
|
97 | - } |
|
98 | - $this->value = $newVal; |
|
99 | - } else { |
|
100 | - $this->value = strtr($val, ['\,' => ',']); |
|
101 | - } |
|
102 | - |
|
103 | - } |
|
104 | - |
|
105 | - /** |
|
106 | - * Returns a raw mime-dir representation of the value. |
|
107 | - * |
|
108 | - * @return string |
|
109 | - */ |
|
110 | - public function getRawMimeDirValue() { |
|
111 | - |
|
112 | - if (is_array($this->value)) { |
|
113 | - $value = $this->value[0]; |
|
114 | - } else { |
|
115 | - $value = $this->value; |
|
116 | - } |
|
117 | - |
|
118 | - return strtr($value, [',' => '\,']); |
|
119 | - |
|
120 | - } |
|
19 | + /** |
|
20 | + * In case this is a multi-value property. This string will be used as a |
|
21 | + * delimiter. |
|
22 | + * |
|
23 | + * @var string|null |
|
24 | + */ |
|
25 | + public $delimiter = null; |
|
26 | + |
|
27 | + /** |
|
28 | + * Returns the type of value. |
|
29 | + * |
|
30 | + * This corresponds to the VALUE= parameter. Every property also has a |
|
31 | + * 'default' valueType. |
|
32 | + * |
|
33 | + * @return string |
|
34 | + */ |
|
35 | + public function getValueType() { |
|
36 | + |
|
37 | + return 'URI'; |
|
38 | + |
|
39 | + } |
|
40 | + |
|
41 | + /** |
|
42 | + * Returns an iterable list of children. |
|
43 | + * |
|
44 | + * @return array |
|
45 | + */ |
|
46 | + public function parameters() { |
|
47 | + |
|
48 | + $parameters = parent::parameters(); |
|
49 | + if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'])) { |
|
50 | + // If we are encoding a URI value, and this URI value has no |
|
51 | + // VALUE=URI parameter, we add it anyway. |
|
52 | + // |
|
53 | + // This is not required by any spec, but both Apple iCal and Apple |
|
54 | + // AddressBook (at least in version 10.8) will trip over this if |
|
55 | + // this is not set, and so it improves compatibility. |
|
56 | + // |
|
57 | + // See Issue #227 and #235 |
|
58 | + $parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI'); |
|
59 | + } |
|
60 | + return $parameters; |
|
61 | + |
|
62 | + } |
|
63 | + |
|
64 | + /** |
|
65 | + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. |
|
66 | + * |
|
67 | + * This has been 'unfolded', so only 1 line will be passed. Unescaping is |
|
68 | + * not yet done, but parameters are not included. |
|
69 | + * |
|
70 | + * @param string $val |
|
71 | + * |
|
72 | + * @return void |
|
73 | + */ |
|
74 | + public function setRawMimeDirValue($val) { |
|
75 | + |
|
76 | + // Normally we don't need to do any type of unescaping for these |
|
77 | + // properties, however.. we've noticed that Google Contacts |
|
78 | + // specifically escapes the colon (:) with a blackslash. While I have |
|
79 | + // no clue why they thought that was a good idea, I'm unescaping it |
|
80 | + // anyway. |
|
81 | + // |
|
82 | + // Good thing backslashes are not allowed in urls. Makes it easy to |
|
83 | + // assume that a backslash is always intended as an escape character. |
|
84 | + if ($this->name === 'URL') { |
|
85 | + $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; |
|
86 | + $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); |
|
87 | + $newVal = ''; |
|
88 | + foreach ($matches as $match) { |
|
89 | + switch ($match) { |
|
90 | + case '\:' : |
|
91 | + $newVal .= ':'; |
|
92 | + break; |
|
93 | + default : |
|
94 | + $newVal .= $match; |
|
95 | + break; |
|
96 | + } |
|
97 | + } |
|
98 | + $this->value = $newVal; |
|
99 | + } else { |
|
100 | + $this->value = strtr($val, ['\,' => ',']); |
|
101 | + } |
|
102 | + |
|
103 | + } |
|
104 | + |
|
105 | + /** |
|
106 | + * Returns a raw mime-dir representation of the value. |
|
107 | + * |
|
108 | + * @return string |
|
109 | + */ |
|
110 | + public function getRawMimeDirValue() { |
|
111 | + |
|
112 | + if (is_array($this->value)) { |
|
113 | + $value = $this->value[0]; |
|
114 | + } else { |
|
115 | + $value = $this->value; |
|
116 | + } |
|
117 | + |
|
118 | + return strtr($value, [',' => '\,']); |
|
119 | + |
|
120 | + } |
|
121 | 121 | |
122 | 122 | } |
@@ -11,183 +11,183 @@ |
||
11 | 11 | */ |
12 | 12 | class FreeBusyData { |
13 | 13 | |
14 | - /** |
|
15 | - * Start timestamp |
|
16 | - * |
|
17 | - * @var int |
|
18 | - */ |
|
19 | - protected $start; |
|
20 | - |
|
21 | - /** |
|
22 | - * End timestamp |
|
23 | - * |
|
24 | - * @var int |
|
25 | - */ |
|
26 | - protected $end; |
|
27 | - |
|
28 | - /** |
|
29 | - * A list of free-busy times. |
|
30 | - * |
|
31 | - * @var array |
|
32 | - */ |
|
33 | - protected $data; |
|
34 | - |
|
35 | - public function __construct($start, $end) { |
|
36 | - |
|
37 | - $this->start = $start; |
|
38 | - $this->end = $end; |
|
39 | - $this->data = []; |
|
40 | - |
|
41 | - $this->data[] = [ |
|
42 | - 'start' => $this->start, |
|
43 | - 'end' => $this->end, |
|
44 | - 'type' => 'FREE', |
|
45 | - ]; |
|
46 | - |
|
47 | - } |
|
48 | - |
|
49 | - /** |
|
50 | - * Adds free or busytime to the data. |
|
51 | - * |
|
52 | - * @param int $start |
|
53 | - * @param int $end |
|
54 | - * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE |
|
55 | - * @return void |
|
56 | - */ |
|
57 | - public function add($start, $end, $type) { |
|
58 | - |
|
59 | - if ($start > $this->end || $end < $this->start) { |
|
60 | - |
|
61 | - // This new data is outside our timerange. |
|
62 | - return; |
|
63 | - |
|
64 | - } |
|
65 | - |
|
66 | - if ($start < $this->start) { |
|
67 | - // The item starts before our requested time range |
|
68 | - $start = $this->start; |
|
69 | - } |
|
70 | - if ($end > $this->end) { |
|
71 | - // The item ends after our requested time range |
|
72 | - $end = $this->end; |
|
73 | - } |
|
74 | - |
|
75 | - // Finding out where we need to insert the new item. |
|
76 | - $currentIndex = 0; |
|
77 | - while ($start > $this->data[$currentIndex]['end']) { |
|
78 | - $currentIndex++; |
|
79 | - } |
|
80 | - |
|
81 | - // The standard insertion point will be one _after_ the first |
|
82 | - // overlapping item. |
|
83 | - $insertStartIndex = $currentIndex + 1; |
|
84 | - |
|
85 | - $newItem = [ |
|
86 | - 'start' => $start, |
|
87 | - 'end' => $end, |
|
88 | - 'type' => $type, |
|
89 | - ]; |
|
90 | - |
|
91 | - $preceedingItem = $this->data[$insertStartIndex - 1]; |
|
92 | - if ($this->data[$insertStartIndex - 1]['start'] === $start) { |
|
93 | - // The old item starts at the exact same point as the new item. |
|
94 | - $insertStartIndex--; |
|
95 | - } |
|
96 | - |
|
97 | - // Now we know where to insert the item, we need to know where it |
|
98 | - // starts overlapping with items on the tail end. We need to start |
|
99 | - // looking one item before the insertStartIndex, because it's possible |
|
100 | - // that the new item 'sits inside' the previous old item. |
|
101 | - if ($insertStartIndex > 0) { |
|
102 | - $currentIndex = $insertStartIndex - 1; |
|
103 | - } else { |
|
104 | - $currentIndex = 0; |
|
105 | - } |
|
106 | - |
|
107 | - while ($end > $this->data[$currentIndex]['end']) { |
|
108 | - |
|
109 | - $currentIndex++; |
|
110 | - |
|
111 | - } |
|
112 | - |
|
113 | - // What we are about to insert into the array |
|
114 | - $newItems = [ |
|
115 | - $newItem |
|
116 | - ]; |
|
117 | - |
|
118 | - // This is the amount of items that are completely overwritten by the |
|
119 | - // new item. |
|
120 | - $itemsToDelete = $currentIndex - $insertStartIndex; |
|
121 | - if ($this->data[$currentIndex]['end'] <= $end) $itemsToDelete++; |
|
122 | - |
|
123 | - // If itemsToDelete was -1, it means that the newly inserted item is |
|
124 | - // actually sitting inside an existing one. This means we need to split |
|
125 | - // the item at the current position in two and insert the new item in |
|
126 | - // between. |
|
127 | - if ($itemsToDelete === -1) { |
|
128 | - $itemsToDelete = 0; |
|
129 | - if ($newItem['end'] < $preceedingItem['end']) { |
|
130 | - $newItems[] = [ |
|
131 | - 'start' => $newItem['end'] + 1, |
|
132 | - 'end' => $preceedingItem['end'], |
|
133 | - 'type' => $preceedingItem['type'] |
|
134 | - ]; |
|
135 | - } |
|
136 | - } |
|
137 | - |
|
138 | - array_splice( |
|
139 | - $this->data, |
|
140 | - $insertStartIndex, |
|
141 | - $itemsToDelete, |
|
142 | - $newItems |
|
143 | - ); |
|
144 | - |
|
145 | - $doMerge = false; |
|
146 | - $mergeOffset = $insertStartIndex; |
|
147 | - $mergeItem = $newItem; |
|
148 | - $mergeDelete = 1; |
|
149 | - |
|
150 | - if (isset($this->data[$insertStartIndex - 1])) { |
|
151 | - // Updating the start time of the previous item. |
|
152 | - $this->data[$insertStartIndex - 1]['end'] = $start; |
|
153 | - |
|
154 | - // If the previous and the current are of the same type, we can |
|
155 | - // merge them into one item. |
|
156 | - if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) { |
|
157 | - $doMerge = true; |
|
158 | - $mergeOffset--; |
|
159 | - $mergeDelete++; |
|
160 | - $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start']; |
|
161 | - } |
|
162 | - } |
|
163 | - if (isset($this->data[$insertStartIndex + 1])) { |
|
164 | - // Updating the start time of the next item. |
|
165 | - $this->data[$insertStartIndex + 1]['start'] = $end; |
|
166 | - |
|
167 | - // If the next and the current are of the same type, we can |
|
168 | - // merge them into one item. |
|
169 | - if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) { |
|
170 | - $doMerge = true; |
|
171 | - $mergeDelete++; |
|
172 | - $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end']; |
|
173 | - } |
|
174 | - |
|
175 | - } |
|
176 | - if ($doMerge) { |
|
177 | - array_splice( |
|
178 | - $this->data, |
|
179 | - $mergeOffset, |
|
180 | - $mergeDelete, |
|
181 | - [$mergeItem] |
|
182 | - ); |
|
183 | - } |
|
184 | - |
|
185 | - } |
|
186 | - |
|
187 | - public function getData() { |
|
188 | - |
|
189 | - return $this->data; |
|
190 | - |
|
191 | - } |
|
14 | + /** |
|
15 | + * Start timestamp |
|
16 | + * |
|
17 | + * @var int |
|
18 | + */ |
|
19 | + protected $start; |
|
20 | + |
|
21 | + /** |
|
22 | + * End timestamp |
|
23 | + * |
|
24 | + * @var int |
|
25 | + */ |
|
26 | + protected $end; |
|
27 | + |
|
28 | + /** |
|
29 | + * A list of free-busy times. |
|
30 | + * |
|
31 | + * @var array |
|
32 | + */ |
|
33 | + protected $data; |
|
34 | + |
|
35 | + public function __construct($start, $end) { |
|
36 | + |
|
37 | + $this->start = $start; |
|
38 | + $this->end = $end; |
|
39 | + $this->data = []; |
|
40 | + |
|
41 | + $this->data[] = [ |
|
42 | + 'start' => $this->start, |
|
43 | + 'end' => $this->end, |
|
44 | + 'type' => 'FREE', |
|
45 | + ]; |
|
46 | + |
|
47 | + } |
|
48 | + |
|
49 | + /** |
|
50 | + * Adds free or busytime to the data. |
|
51 | + * |
|
52 | + * @param int $start |
|
53 | + * @param int $end |
|
54 | + * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE |
|
55 | + * @return void |
|
56 | + */ |
|
57 | + public function add($start, $end, $type) { |
|
58 | + |
|
59 | + if ($start > $this->end || $end < $this->start) { |
|
60 | + |
|
61 | + // This new data is outside our timerange. |
|
62 | + return; |
|
63 | + |
|
64 | + } |
|
65 | + |
|
66 | + if ($start < $this->start) { |
|
67 | + // The item starts before our requested time range |
|
68 | + $start = $this->start; |
|
69 | + } |
|
70 | + if ($end > $this->end) { |
|
71 | + // The item ends after our requested time range |
|
72 | + $end = $this->end; |
|
73 | + } |
|
74 | + |
|
75 | + // Finding out where we need to insert the new item. |
|
76 | + $currentIndex = 0; |
|
77 | + while ($start > $this->data[$currentIndex]['end']) { |
|
78 | + $currentIndex++; |
|
79 | + } |
|
80 | + |
|
81 | + // The standard insertion point will be one _after_ the first |
|
82 | + // overlapping item. |
|
83 | + $insertStartIndex = $currentIndex + 1; |
|
84 | + |
|
85 | + $newItem = [ |
|
86 | + 'start' => $start, |
|
87 | + 'end' => $end, |
|
88 | + 'type' => $type, |
|
89 | + ]; |
|
90 | + |
|
91 | + $preceedingItem = $this->data[$insertStartIndex - 1]; |
|
92 | + if ($this->data[$insertStartIndex - 1]['start'] === $start) { |
|
93 | + // The old item starts at the exact same point as the new item. |
|
94 | + $insertStartIndex--; |
|
95 | + } |
|
96 | + |
|
97 | + // Now we know where to insert the item, we need to know where it |
|
98 | + // starts overlapping with items on the tail end. We need to start |
|
99 | + // looking one item before the insertStartIndex, because it's possible |
|
100 | + // that the new item 'sits inside' the previous old item. |
|
101 | + if ($insertStartIndex > 0) { |
|
102 | + $currentIndex = $insertStartIndex - 1; |
|
103 | + } else { |
|
104 | + $currentIndex = 0; |
|
105 | + } |
|
106 | + |
|
107 | + while ($end > $this->data[$currentIndex]['end']) { |
|
108 | + |
|
109 | + $currentIndex++; |
|
110 | + |
|
111 | + } |
|
112 | + |
|
113 | + // What we are about to insert into the array |
|
114 | + $newItems = [ |
|
115 | + $newItem |
|
116 | + ]; |
|
117 | + |
|
118 | + // This is the amount of items that are completely overwritten by the |
|
119 | + // new item. |
|
120 | + $itemsToDelete = $currentIndex - $insertStartIndex; |
|
121 | + if ($this->data[$currentIndex]['end'] <= $end) $itemsToDelete++; |
|
122 | + |
|
123 | + // If itemsToDelete was -1, it means that the newly inserted item is |
|
124 | + // actually sitting inside an existing one. This means we need to split |
|
125 | + // the item at the current position in two and insert the new item in |
|
126 | + // between. |
|
127 | + if ($itemsToDelete === -1) { |
|
128 | + $itemsToDelete = 0; |
|
129 | + if ($newItem['end'] < $preceedingItem['end']) { |
|
130 | + $newItems[] = [ |
|
131 | + 'start' => $newItem['end'] + 1, |
|
132 | + 'end' => $preceedingItem['end'], |
|
133 | + 'type' => $preceedingItem['type'] |
|
134 | + ]; |
|
135 | + } |
|
136 | + } |
|
137 | + |
|
138 | + array_splice( |
|
139 | + $this->data, |
|
140 | + $insertStartIndex, |
|
141 | + $itemsToDelete, |
|
142 | + $newItems |
|
143 | + ); |
|
144 | + |
|
145 | + $doMerge = false; |
|
146 | + $mergeOffset = $insertStartIndex; |
|
147 | + $mergeItem = $newItem; |
|
148 | + $mergeDelete = 1; |
|
149 | + |
|
150 | + if (isset($this->data[$insertStartIndex - 1])) { |
|
151 | + // Updating the start time of the previous item. |
|
152 | + $this->data[$insertStartIndex - 1]['end'] = $start; |
|
153 | + |
|
154 | + // If the previous and the current are of the same type, we can |
|
155 | + // merge them into one item. |
|
156 | + if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) { |
|
157 | + $doMerge = true; |
|
158 | + $mergeOffset--; |
|
159 | + $mergeDelete++; |
|
160 | + $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start']; |
|
161 | + } |
|
162 | + } |
|
163 | + if (isset($this->data[$insertStartIndex + 1])) { |
|
164 | + // Updating the start time of the next item. |
|
165 | + $this->data[$insertStartIndex + 1]['start'] = $end; |
|
166 | + |
|
167 | + // If the next and the current are of the same type, we can |
|
168 | + // merge them into one item. |
|
169 | + if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) { |
|
170 | + $doMerge = true; |
|
171 | + $mergeDelete++; |
|
172 | + $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end']; |
|
173 | + } |
|
174 | + |
|
175 | + } |
|
176 | + if ($doMerge) { |
|
177 | + array_splice( |
|
178 | + $this->data, |
|
179 | + $mergeOffset, |
|
180 | + $mergeDelete, |
|
181 | + [$mergeItem] |
|
182 | + ); |
|
183 | + } |
|
184 | + |
|
185 | + } |
|
186 | + |
|
187 | + public function getData() { |
|
188 | + |
|
189 | + return $this->data; |
|
190 | + |
|
191 | + } |
|
192 | 192 | |
193 | 193 | } |
@@ -19,376 +19,376 @@ |
||
19 | 19 | */ |
20 | 20 | class Parameter extends Node { |
21 | 21 | |
22 | - /** |
|
23 | - * Parameter name. |
|
24 | - * |
|
25 | - * @var string |
|
26 | - */ |
|
27 | - public $name; |
|
28 | - |
|
29 | - /** |
|
30 | - * vCard 2.1 allows parameters to be encoded without a name. |
|
31 | - * |
|
32 | - * We can deduce the parameter name based on it's value. |
|
33 | - * |
|
34 | - * @var bool |
|
35 | - */ |
|
36 | - public $noName = false; |
|
37 | - |
|
38 | - /** |
|
39 | - * Parameter value. |
|
40 | - * |
|
41 | - * @var string |
|
42 | - */ |
|
43 | - protected $value; |
|
44 | - |
|
45 | - /** |
|
46 | - * Sets up the object. |
|
47 | - * |
|
48 | - * It's recommended to use the create:: factory method instead. |
|
49 | - * |
|
50 | - * @param string $name |
|
51 | - * @param string $value |
|
52 | - */ |
|
53 | - public function __construct(Document $root, $name, $value = null) { |
|
54 | - |
|
55 | - $this->name = strtoupper($name); |
|
56 | - $this->root = $root; |
|
57 | - if (is_null($name)) { |
|
58 | - $this->noName = true; |
|
59 | - $this->name = static::guessParameterNameByValue($value); |
|
60 | - } |
|
61 | - |
|
62 | - // If guessParameterNameByValue() returns an empty string |
|
63 | - // above, we're actually dealing with a parameter that has no value. |
|
64 | - // In that case we have to move the value to the name. |
|
65 | - if ($this->name === '') { |
|
66 | - $this->noName = false; |
|
67 | - $this->name = strtoupper($value); |
|
68 | - } else { |
|
69 | - $this->setValue($value); |
|
70 | - } |
|
71 | - |
|
72 | - } |
|
73 | - |
|
74 | - /** |
|
75 | - * Try to guess property name by value, can be used for vCard 2.1 nameless parameters. |
|
76 | - * |
|
77 | - * Figuring out what the name should have been. Note that a ton of |
|
78 | - * these are rather silly in 2014 and would probably rarely be |
|
79 | - * used, but we like to be complete. |
|
80 | - * |
|
81 | - * @param string $value |
|
82 | - * |
|
83 | - * @return string |
|
84 | - */ |
|
85 | - static function guessParameterNameByValue($value) { |
|
86 | - switch (strtoupper($value)) { |
|
87 | - |
|
88 | - // Encodings |
|
89 | - case '7-BIT' : |
|
90 | - case 'QUOTED-PRINTABLE' : |
|
91 | - case 'BASE64' : |
|
92 | - $name = 'ENCODING'; |
|
93 | - break; |
|
94 | - |
|
95 | - // Common types |
|
96 | - case 'WORK' : |
|
97 | - case 'HOME' : |
|
98 | - case 'PREF' : |
|
99 | - |
|
100 | - // Delivery Label Type |
|
101 | - case 'DOM' : |
|
102 | - case 'INTL' : |
|
103 | - case 'POSTAL' : |
|
104 | - case 'PARCEL' : |
|
105 | - |
|
106 | - // Telephone types |
|
107 | - case 'VOICE' : |
|
108 | - case 'FAX' : |
|
109 | - case 'MSG' : |
|
110 | - case 'CELL' : |
|
111 | - case 'PAGER' : |
|
112 | - case 'BBS' : |
|
113 | - case 'MODEM' : |
|
114 | - case 'CAR' : |
|
115 | - case 'ISDN' : |
|
116 | - case 'VIDEO' : |
|
117 | - |
|
118 | - // EMAIL types (lol) |
|
119 | - case 'AOL' : |
|
120 | - case 'APPLELINK' : |
|
121 | - case 'ATTMAIL' : |
|
122 | - case 'CIS' : |
|
123 | - case 'EWORLD' : |
|
124 | - case 'INTERNET' : |
|
125 | - case 'IBMMAIL' : |
|
126 | - case 'MCIMAIL' : |
|
127 | - case 'POWERSHARE' : |
|
128 | - case 'PRODIGY' : |
|
129 | - case 'TLX' : |
|
130 | - case 'X400' : |
|
131 | - |
|
132 | - // Photo / Logo format types |
|
133 | - case 'GIF' : |
|
134 | - case 'CGM' : |
|
135 | - case 'WMF' : |
|
136 | - case 'BMP' : |
|
137 | - case 'DIB' : |
|
138 | - case 'PICT' : |
|
139 | - case 'TIFF' : |
|
140 | - case 'PDF' : |
|
141 | - case 'PS' : |
|
142 | - case 'JPEG' : |
|
143 | - case 'MPEG' : |
|
144 | - case 'MPEG2' : |
|
145 | - case 'AVI' : |
|
146 | - case 'QTIME' : |
|
147 | - |
|
148 | - // Sound Digital Audio Type |
|
149 | - case 'WAVE' : |
|
150 | - case 'PCM' : |
|
151 | - case 'AIFF' : |
|
152 | - |
|
153 | - // Key types |
|
154 | - case 'X509' : |
|
155 | - case 'PGP' : |
|
156 | - $name = 'TYPE'; |
|
157 | - break; |
|
158 | - |
|
159 | - // Value types |
|
160 | - case 'INLINE' : |
|
161 | - case 'URL' : |
|
162 | - case 'CONTENT-ID' : |
|
163 | - case 'CID' : |
|
164 | - $name = 'VALUE'; |
|
165 | - break; |
|
166 | - |
|
167 | - default: |
|
168 | - $name = ''; |
|
169 | - } |
|
170 | - |
|
171 | - return $name; |
|
172 | - } |
|
173 | - |
|
174 | - /** |
|
175 | - * Updates the current value. |
|
176 | - * |
|
177 | - * This may be either a single, or multiple strings in an array. |
|
178 | - * |
|
179 | - * @param string|array $value |
|
180 | - * |
|
181 | - * @return void |
|
182 | - */ |
|
183 | - public function setValue($value) { |
|
184 | - |
|
185 | - $this->value = $value; |
|
186 | - |
|
187 | - } |
|
188 | - |
|
189 | - /** |
|
190 | - * Returns the current value. |
|
191 | - * |
|
192 | - * This method will always return a string, or null. If there were multiple |
|
193 | - * values, it will automatically concatenate them (separated by comma). |
|
194 | - * |
|
195 | - * @return string|null |
|
196 | - */ |
|
197 | - public function getValue() { |
|
198 | - |
|
199 | - if (is_array($this->value)) { |
|
200 | - return implode(',', $this->value); |
|
201 | - } else { |
|
202 | - return $this->value; |
|
203 | - } |
|
204 | - |
|
205 | - } |
|
206 | - |
|
207 | - /** |
|
208 | - * Sets multiple values for this parameter. |
|
209 | - * |
|
210 | - * @param array $value |
|
211 | - * |
|
212 | - * @return void |
|
213 | - */ |
|
214 | - public function setParts(array $value) { |
|
215 | - |
|
216 | - $this->value = $value; |
|
217 | - |
|
218 | - } |
|
219 | - |
|
220 | - /** |
|
221 | - * Returns all values for this parameter. |
|
222 | - * |
|
223 | - * If there were no values, an empty array will be returned. |
|
224 | - * |
|
225 | - * @return array |
|
226 | - */ |
|
227 | - public function getParts() { |
|
228 | - |
|
229 | - if (is_array($this->value)) { |
|
230 | - return $this->value; |
|
231 | - } elseif (is_null($this->value)) { |
|
232 | - return []; |
|
233 | - } else { |
|
234 | - return [$this->value]; |
|
235 | - } |
|
236 | - |
|
237 | - } |
|
238 | - |
|
239 | - /** |
|
240 | - * Adds a value to this parameter. |
|
241 | - * |
|
242 | - * If the argument is specified as an array, all items will be added to the |
|
243 | - * parameter value list. |
|
244 | - * |
|
245 | - * @param string|array $part |
|
246 | - * |
|
247 | - * @return void |
|
248 | - */ |
|
249 | - public function addValue($part) { |
|
250 | - |
|
251 | - if (is_null($this->value)) { |
|
252 | - $this->value = $part; |
|
253 | - } else { |
|
254 | - $this->value = array_merge((array)$this->value, (array)$part); |
|
255 | - } |
|
256 | - |
|
257 | - } |
|
258 | - |
|
259 | - /** |
|
260 | - * Checks if this parameter contains the specified value. |
|
261 | - * |
|
262 | - * This is a case-insensitive match. It makes sense to call this for for |
|
263 | - * instance the TYPE parameter, to see if it contains a keyword such as |
|
264 | - * 'WORK' or 'FAX'. |
|
265 | - * |
|
266 | - * @param string $value |
|
267 | - * |
|
268 | - * @return bool |
|
269 | - */ |
|
270 | - public function has($value) { |
|
271 | - |
|
272 | - return in_array( |
|
273 | - strtolower($value), |
|
274 | - array_map('strtolower', (array)$this->value) |
|
275 | - ); |
|
276 | - |
|
277 | - } |
|
278 | - |
|
279 | - /** |
|
280 | - * Turns the object back into a serialized blob. |
|
281 | - * |
|
282 | - * @return string |
|
283 | - */ |
|
284 | - public function serialize() { |
|
285 | - |
|
286 | - $value = $this->getParts(); |
|
287 | - |
|
288 | - if (count($value) === 0) { |
|
289 | - return $this->name . '='; |
|
290 | - } |
|
291 | - |
|
292 | - if ($this->root->getDocumentType() === Document::VCARD21 && $this->noName) { |
|
293 | - |
|
294 | - return implode(';', $value); |
|
295 | - |
|
296 | - } |
|
297 | - |
|
298 | - return $this->name . '=' . array_reduce( |
|
299 | - $value, |
|
300 | - public function($out, $item) { |
|
301 | - |
|
302 | - if (!is_null($out)) $out .= ','; |
|
303 | - |
|
304 | - // If there's no special characters in the string, we'll use the simple |
|
305 | - // format. |
|
306 | - // |
|
307 | - // The list of special characters is defined as: |
|
308 | - // |
|
309 | - // Any character except CONTROL, DQUOTE, ";", ":", "," |
|
310 | - // |
|
311 | - // by the iCalendar spec: |
|
312 | - // https://tools.ietf.org/html/rfc5545#section-3.1 |
|
313 | - // |
|
314 | - // And we add ^ to that because of: |
|
315 | - // https://tools.ietf.org/html/rfc6868 |
|
316 | - // |
|
317 | - // But we've found that iCal (7.0, shipped with OSX 10.9) |
|
318 | - // severaly trips on + characters not being quoted, so we |
|
319 | - // added + as well. |
|
320 | - if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { |
|
321 | - return $out . $item; |
|
322 | - } else { |
|
323 | - // Enclosing in double-quotes, and using RFC6868 for encoding any |
|
324 | - // special characters |
|
325 | - $out .= '"' . strtr( |
|
326 | - $item, |
|
327 | - [ |
|
328 | - '^' => '^^', |
|
329 | - "\n" => '^n', |
|
330 | - '"' => '^\'', |
|
331 | - ] |
|
332 | - ) . '"'; |
|
333 | - return $out; |
|
334 | - } |
|
335 | - |
|
336 | - } |
|
337 | - ); |
|
338 | - |
|
339 | - } |
|
340 | - |
|
341 | - /** |
|
342 | - * This method returns an array, with the representation as it should be |
|
343 | - * encoded in JSON. This is used to create jCard or jCal documents. |
|
344 | - * |
|
345 | - * @return array |
|
346 | - */ |
|
347 | - public function jsonSerialize() { |
|
348 | - |
|
349 | - return $this->value; |
|
350 | - |
|
351 | - } |
|
352 | - |
|
353 | - /** |
|
354 | - * This method serializes the data into XML. This is used to create xCard or |
|
355 | - * xCal documents. |
|
356 | - * |
|
357 | - * @param Xml\Writer $writer XML writer. |
|
358 | - * |
|
359 | - * @return void |
|
360 | - */ |
|
361 | - public function xmlSerialize(Xml\Writer $writer) { |
|
362 | - |
|
363 | - foreach (explode(',', $this->value) as $value) { |
|
364 | - $writer->writeElement('text', $value); |
|
365 | - } |
|
366 | - |
|
367 | - } |
|
368 | - |
|
369 | - /** |
|
370 | - * Called when this object is being cast to a string. |
|
371 | - * |
|
372 | - * @return string |
|
373 | - */ |
|
374 | - public function __toString() { |
|
375 | - |
|
376 | - return (string)$this->getValue(); |
|
377 | - |
|
378 | - } |
|
379 | - |
|
380 | - /** |
|
381 | - * Returns the iterator for this object. |
|
382 | - * |
|
383 | - * @return ElementList |
|
384 | - */ |
|
385 | - public function getIterator() { |
|
386 | - |
|
387 | - if (!is_null($this->iterator)) |
|
388 | - return $this->iterator; |
|
389 | - |
|
390 | - return $this->iterator = new ArrayIterator((array)$this->value); |
|
391 | - |
|
392 | - } |
|
22 | + /** |
|
23 | + * Parameter name. |
|
24 | + * |
|
25 | + * @var string |
|
26 | + */ |
|
27 | + public $name; |
|
28 | + |
|
29 | + /** |
|
30 | + * vCard 2.1 allows parameters to be encoded without a name. |
|
31 | + * |
|
32 | + * We can deduce the parameter name based on it's value. |
|
33 | + * |
|
34 | + * @var bool |
|
35 | + */ |
|
36 | + public $noName = false; |
|
37 | + |
|
38 | + /** |
|
39 | + * Parameter value. |
|
40 | + * |
|
41 | + * @var string |
|
42 | + */ |
|
43 | + protected $value; |
|
44 | + |
|
45 | + /** |
|
46 | + * Sets up the object. |
|
47 | + * |
|
48 | + * It's recommended to use the create:: factory method instead. |
|
49 | + * |
|
50 | + * @param string $name |
|
51 | + * @param string $value |
|
52 | + */ |
|
53 | + public function __construct(Document $root, $name, $value = null) { |
|
54 | + |
|
55 | + $this->name = strtoupper($name); |
|
56 | + $this->root = $root; |
|
57 | + if (is_null($name)) { |
|
58 | + $this->noName = true; |
|
59 | + $this->name = static::guessParameterNameByValue($value); |
|
60 | + } |
|
61 | + |
|
62 | + // If guessParameterNameByValue() returns an empty string |
|
63 | + // above, we're actually dealing with a parameter that has no value. |
|
64 | + // In that case we have to move the value to the name. |
|
65 | + if ($this->name === '') { |
|
66 | + $this->noName = false; |
|
67 | + $this->name = strtoupper($value); |
|
68 | + } else { |
|
69 | + $this->setValue($value); |
|
70 | + } |
|
71 | + |
|
72 | + } |
|
73 | + |
|
74 | + /** |
|
75 | + * Try to guess property name by value, can be used for vCard 2.1 nameless parameters. |
|
76 | + * |
|
77 | + * Figuring out what the name should have been. Note that a ton of |
|
78 | + * these are rather silly in 2014 and would probably rarely be |
|
79 | + * used, but we like to be complete. |
|
80 | + * |
|
81 | + * @param string $value |
|
82 | + * |
|
83 | + * @return string |
|
84 | + */ |
|
85 | + static function guessParameterNameByValue($value) { |
|
86 | + switch (strtoupper($value)) { |
|
87 | + |
|
88 | + // Encodings |
|
89 | + case '7-BIT' : |
|
90 | + case 'QUOTED-PRINTABLE' : |
|
91 | + case 'BASE64' : |
|
92 | + $name = 'ENCODING'; |
|
93 | + break; |
|
94 | + |
|
95 | + // Common types |
|
96 | + case 'WORK' : |
|
97 | + case 'HOME' : |
|
98 | + case 'PREF' : |
|
99 | + |
|
100 | + // Delivery Label Type |
|
101 | + case 'DOM' : |
|
102 | + case 'INTL' : |
|
103 | + case 'POSTAL' : |
|
104 | + case 'PARCEL' : |
|
105 | + |
|
106 | + // Telephone types |
|
107 | + case 'VOICE' : |
|
108 | + case 'FAX' : |
|
109 | + case 'MSG' : |
|
110 | + case 'CELL' : |
|
111 | + case 'PAGER' : |
|
112 | + case 'BBS' : |
|
113 | + case 'MODEM' : |
|
114 | + case 'CAR' : |
|
115 | + case 'ISDN' : |
|
116 | + case 'VIDEO' : |
|
117 | + |
|
118 | + // EMAIL types (lol) |
|
119 | + case 'AOL' : |
|
120 | + case 'APPLELINK' : |
|
121 | + case 'ATTMAIL' : |
|
122 | + case 'CIS' : |
|
123 | + case 'EWORLD' : |
|
124 | + case 'INTERNET' : |
|
125 | + case 'IBMMAIL' : |
|
126 | + case 'MCIMAIL' : |
|
127 | + case 'POWERSHARE' : |
|
128 | + case 'PRODIGY' : |
|
129 | + case 'TLX' : |
|
130 | + case 'X400' : |
|
131 | + |
|
132 | + // Photo / Logo format types |
|
133 | + case 'GIF' : |
|
134 | + case 'CGM' : |
|
135 | + case 'WMF' : |
|
136 | + case 'BMP' : |
|
137 | + case 'DIB' : |
|
138 | + case 'PICT' : |
|
139 | + case 'TIFF' : |
|
140 | + case 'PDF' : |
|
141 | + case 'PS' : |
|
142 | + case 'JPEG' : |
|
143 | + case 'MPEG' : |
|
144 | + case 'MPEG2' : |
|
145 | + case 'AVI' : |
|
146 | + case 'QTIME' : |
|
147 | + |
|
148 | + // Sound Digital Audio Type |
|
149 | + case 'WAVE' : |
|
150 | + case 'PCM' : |
|
151 | + case 'AIFF' : |
|
152 | + |
|
153 | + // Key types |
|
154 | + case 'X509' : |
|
155 | + case 'PGP' : |
|
156 | + $name = 'TYPE'; |
|
157 | + break; |
|
158 | + |
|
159 | + // Value types |
|
160 | + case 'INLINE' : |
|
161 | + case 'URL' : |
|
162 | + case 'CONTENT-ID' : |
|
163 | + case 'CID' : |
|
164 | + $name = 'VALUE'; |
|
165 | + break; |
|
166 | + |
|
167 | + default: |
|
168 | + $name = ''; |
|
169 | + } |
|
170 | + |
|
171 | + return $name; |
|
172 | + } |
|
173 | + |
|
174 | + /** |
|
175 | + * Updates the current value. |
|
176 | + * |
|
177 | + * This may be either a single, or multiple strings in an array. |
|
178 | + * |
|
179 | + * @param string|array $value |
|
180 | + * |
|
181 | + * @return void |
|
182 | + */ |
|
183 | + public function setValue($value) { |
|
184 | + |
|
185 | + $this->value = $value; |
|
186 | + |
|
187 | + } |
|
188 | + |
|
189 | + /** |
|
190 | + * Returns the current value. |
|
191 | + * |
|
192 | + * This method will always return a string, or null. If there were multiple |
|
193 | + * values, it will automatically concatenate them (separated by comma). |
|
194 | + * |
|
195 | + * @return string|null |
|
196 | + */ |
|
197 | + public function getValue() { |
|
198 | + |
|
199 | + if (is_array($this->value)) { |
|
200 | + return implode(',', $this->value); |
|
201 | + } else { |
|
202 | + return $this->value; |
|
203 | + } |
|
204 | + |
|
205 | + } |
|
206 | + |
|
207 | + /** |
|
208 | + * Sets multiple values for this parameter. |
|
209 | + * |
|
210 | + * @param array $value |
|
211 | + * |
|
212 | + * @return void |
|
213 | + */ |
|
214 | + public function setParts(array $value) { |
|
215 | + |
|
216 | + $this->value = $value; |
|
217 | + |
|
218 | + } |
|
219 | + |
|
220 | + /** |
|
221 | + * Returns all values for this parameter. |
|
222 | + * |
|
223 | + * If there were no values, an empty array will be returned. |
|
224 | + * |
|
225 | + * @return array |
|
226 | + */ |
|
227 | + public function getParts() { |
|
228 | + |
|
229 | + if (is_array($this->value)) { |
|
230 | + return $this->value; |
|
231 | + } elseif (is_null($this->value)) { |
|
232 | + return []; |
|
233 | + } else { |
|
234 | + return [$this->value]; |
|
235 | + } |
|
236 | + |
|
237 | + } |
|
238 | + |
|
239 | + /** |
|
240 | + * Adds a value to this parameter. |
|
241 | + * |
|
242 | + * If the argument is specified as an array, all items will be added to the |
|
243 | + * parameter value list. |
|
244 | + * |
|
245 | + * @param string|array $part |
|
246 | + * |
|
247 | + * @return void |
|
248 | + */ |
|
249 | + public function addValue($part) { |
|
250 | + |
|
251 | + if (is_null($this->value)) { |
|
252 | + $this->value = $part; |
|
253 | + } else { |
|
254 | + $this->value = array_merge((array)$this->value, (array)$part); |
|
255 | + } |
|
256 | + |
|
257 | + } |
|
258 | + |
|
259 | + /** |
|
260 | + * Checks if this parameter contains the specified value. |
|
261 | + * |
|
262 | + * This is a case-insensitive match. It makes sense to call this for for |
|
263 | + * instance the TYPE parameter, to see if it contains a keyword such as |
|
264 | + * 'WORK' or 'FAX'. |
|
265 | + * |
|
266 | + * @param string $value |
|
267 | + * |
|
268 | + * @return bool |
|
269 | + */ |
|
270 | + public function has($value) { |
|
271 | + |
|
272 | + return in_array( |
|
273 | + strtolower($value), |
|
274 | + array_map('strtolower', (array)$this->value) |
|
275 | + ); |
|
276 | + |
|
277 | + } |
|
278 | + |
|
279 | + /** |
|
280 | + * Turns the object back into a serialized blob. |
|
281 | + * |
|
282 | + * @return string |
|
283 | + */ |
|
284 | + public function serialize() { |
|
285 | + |
|
286 | + $value = $this->getParts(); |
|
287 | + |
|
288 | + if (count($value) === 0) { |
|
289 | + return $this->name . '='; |
|
290 | + } |
|
291 | + |
|
292 | + if ($this->root->getDocumentType() === Document::VCARD21 && $this->noName) { |
|
293 | + |
|
294 | + return implode(';', $value); |
|
295 | + |
|
296 | + } |
|
297 | + |
|
298 | + return $this->name . '=' . array_reduce( |
|
299 | + $value, |
|
300 | + public function($out, $item) { |
|
301 | + |
|
302 | + if (!is_null($out)) $out .= ','; |
|
303 | + |
|
304 | + // If there's no special characters in the string, we'll use the simple |
|
305 | + // format. |
|
306 | + // |
|
307 | + // The list of special characters is defined as: |
|
308 | + // |
|
309 | + // Any character except CONTROL, DQUOTE, ";", ":", "," |
|
310 | + // |
|
311 | + // by the iCalendar spec: |
|
312 | + // https://tools.ietf.org/html/rfc5545#section-3.1 |
|
313 | + // |
|
314 | + // And we add ^ to that because of: |
|
315 | + // https://tools.ietf.org/html/rfc6868 |
|
316 | + // |
|
317 | + // But we've found that iCal (7.0, shipped with OSX 10.9) |
|
318 | + // severaly trips on + characters not being quoted, so we |
|
319 | + // added + as well. |
|
320 | + if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { |
|
321 | + return $out . $item; |
|
322 | + } else { |
|
323 | + // Enclosing in double-quotes, and using RFC6868 for encoding any |
|
324 | + // special characters |
|
325 | + $out .= '"' . strtr( |
|
326 | + $item, |
|
327 | + [ |
|
328 | + '^' => '^^', |
|
329 | + "\n" => '^n', |
|
330 | + '"' => '^\'', |
|
331 | + ] |
|
332 | + ) . '"'; |
|
333 | + return $out; |
|
334 | + } |
|
335 | + |
|
336 | + } |
|
337 | + ); |
|
338 | + |
|
339 | + } |
|
340 | + |
|
341 | + /** |
|
342 | + * This method returns an array, with the representation as it should be |
|
343 | + * encoded in JSON. This is used to create jCard or jCal documents. |
|
344 | + * |
|
345 | + * @return array |
|
346 | + */ |
|
347 | + public function jsonSerialize() { |
|
348 | + |
|
349 | + return $this->value; |
|
350 | + |
|
351 | + } |
|
352 | + |
|
353 | + /** |
|
354 | + * This method serializes the data into XML. This is used to create xCard or |
|
355 | + * xCal documents. |
|
356 | + * |
|
357 | + * @param Xml\Writer $writer XML writer. |
|
358 | + * |
|
359 | + * @return void |
|
360 | + */ |
|
361 | + public function xmlSerialize(Xml\Writer $writer) { |
|
362 | + |
|
363 | + foreach (explode(',', $this->value) as $value) { |
|
364 | + $writer->writeElement('text', $value); |
|
365 | + } |
|
366 | + |
|
367 | + } |
|
368 | + |
|
369 | + /** |
|
370 | + * Called when this object is being cast to a string. |
|
371 | + * |
|
372 | + * @return string |
|
373 | + */ |
|
374 | + public function __toString() { |
|
375 | + |
|
376 | + return (string)$this->getValue(); |
|
377 | + |
|
378 | + } |
|
379 | + |
|
380 | + /** |
|
381 | + * Returns the iterator for this object. |
|
382 | + * |
|
383 | + * @return ElementList |
|
384 | + */ |
|
385 | + public function getIterator() { |
|
386 | + |
|
387 | + if (!is_null($this->iterator)) |
|
388 | + return $this->iterator; |
|
389 | + |
|
390 | + return $this->iterator = new ArrayIterator((array)$this->value); |
|
391 | + |
|
392 | + } |
|
393 | 393 | |
394 | 394 | } |
@@ -3,7 +3,7 @@ discard block |
||
3 | 3 | namespace Sabre\VObject; |
4 | 4 | |
5 | 5 | use |
6 | - InvalidArgumentException; |
|
6 | + InvalidArgumentException; |
|
7 | 7 | |
8 | 8 | /** |
9 | 9 | * This is the CLI interface for sabre-vobject. |
@@ -14,758 +14,758 @@ discard block |
||
14 | 14 | */ |
15 | 15 | class Cli { |
16 | 16 | |
17 | - /** |
|
18 | - * No output. |
|
19 | - * |
|
20 | - * @var bool |
|
21 | - */ |
|
22 | - protected $quiet = false; |
|
23 | - |
|
24 | - /** |
|
25 | - * Help display. |
|
26 | - * |
|
27 | - * @var bool |
|
28 | - */ |
|
29 | - protected $showHelp = false; |
|
30 | - |
|
31 | - /** |
|
32 | - * Wether to spit out 'mimedir' or 'json' format. |
|
33 | - * |
|
34 | - * @var string |
|
35 | - */ |
|
36 | - protected $format; |
|
37 | - |
|
38 | - /** |
|
39 | - * JSON pretty print. |
|
40 | - * |
|
41 | - * @var bool |
|
42 | - */ |
|
43 | - protected $pretty; |
|
44 | - |
|
45 | - /** |
|
46 | - * Source file. |
|
47 | - * |
|
48 | - * @var string |
|
49 | - */ |
|
50 | - protected $inputPath; |
|
51 | - |
|
52 | - /** |
|
53 | - * Destination file. |
|
54 | - * |
|
55 | - * @var string |
|
56 | - */ |
|
57 | - protected $outputPath; |
|
58 | - |
|
59 | - /** |
|
60 | - * output stream. |
|
61 | - * |
|
62 | - * @var resource |
|
63 | - */ |
|
64 | - protected $stdout; |
|
65 | - |
|
66 | - /** |
|
67 | - * stdin. |
|
68 | - * |
|
69 | - * @var resource |
|
70 | - */ |
|
71 | - protected $stdin; |
|
72 | - |
|
73 | - /** |
|
74 | - * stderr. |
|
75 | - * |
|
76 | - * @var resource |
|
77 | - */ |
|
78 | - protected $stderr; |
|
79 | - |
|
80 | - /** |
|
81 | - * Input format (one of json or mimedir). |
|
82 | - * |
|
83 | - * @var string |
|
84 | - */ |
|
85 | - protected $inputFormat; |
|
86 | - |
|
87 | - /** |
|
88 | - * Makes the parser less strict. |
|
89 | - * |
|
90 | - * @var bool |
|
91 | - */ |
|
92 | - protected $forgiving = false; |
|
93 | - |
|
94 | - /** |
|
95 | - * Main function. |
|
96 | - * |
|
97 | - * @return int |
|
98 | - */ |
|
99 | - public function main(array $argv) { |
|
100 | - |
|
101 | - // @codeCoverageIgnoreStart |
|
102 | - // We cannot easily test this, so we'll skip it. Pretty basic anyway. |
|
103 | - |
|
104 | - if (!$this->stderr) { |
|
105 | - $this->stderr = fopen('php://stderr', 'w'); |
|
106 | - } |
|
107 | - if (!$this->stdout) { |
|
108 | - $this->stdout = fopen('php://stdout', 'w'); |
|
109 | - } |
|
110 | - if (!$this->stdin) { |
|
111 | - $this->stdin = fopen('php://stdin', 'r'); |
|
112 | - } |
|
113 | - |
|
114 | - // @codeCoverageIgnoreEnd |
|
115 | - |
|
116 | - |
|
117 | - try { |
|
118 | - |
|
119 | - list($options, $positional) = $this->parseArguments($argv); |
|
120 | - |
|
121 | - if (isset($options['q'])) { |
|
122 | - $this->quiet = true; |
|
123 | - } |
|
124 | - $this->log($this->colorize('green', "sabre/vobject ") . $this->colorize('yellow', Version::VERSION)); |
|
125 | - |
|
126 | - foreach ($options as $name => $value) { |
|
127 | - |
|
128 | - switch ($name) { |
|
129 | - |
|
130 | - case 'q' : |
|
131 | - // Already handled earlier. |
|
132 | - break; |
|
133 | - case 'h' : |
|
134 | - case 'help' : |
|
135 | - $this->showHelp(); |
|
136 | - return 0; |
|
137 | - break; |
|
138 | - case 'format' : |
|
139 | - switch ($value) { |
|
140 | - |
|
141 | - // jcard/jcal documents |
|
142 | - case 'jcard' : |
|
143 | - case 'jcal' : |
|
144 | - |
|
145 | - // specific document versions |
|
146 | - case 'vcard21' : |
|
147 | - case 'vcard30' : |
|
148 | - case 'vcard40' : |
|
149 | - case 'icalendar20' : |
|
150 | - |
|
151 | - // specific formats |
|
152 | - case 'json' : |
|
153 | - case 'mimedir' : |
|
154 | - |
|
155 | - // icalendar/vcad |
|
156 | - case 'icalendar' : |
|
157 | - case 'vcard' : |
|
158 | - $this->format = $value; |
|
159 | - break; |
|
160 | - |
|
161 | - default : |
|
162 | - throw new InvalidArgumentException('Unknown format: ' . $value); |
|
163 | - |
|
164 | - } |
|
165 | - break; |
|
166 | - case 'pretty' : |
|
167 | - if (version_compare(PHP_VERSION, '5.4.0') >= 0) { |
|
168 | - $this->pretty = true; |
|
169 | - } |
|
170 | - break; |
|
171 | - case 'forgiving' : |
|
172 | - $this->forgiving = true; |
|
173 | - break; |
|
174 | - case 'inputformat' : |
|
175 | - switch ($value) { |
|
176 | - // json formats |
|
177 | - case 'jcard' : |
|
178 | - case 'jcal' : |
|
179 | - case 'json' : |
|
180 | - $this->inputFormat = 'json'; |
|
181 | - break; |
|
182 | - |
|
183 | - // mimedir formats |
|
184 | - case 'mimedir' : |
|
185 | - case 'icalendar' : |
|
186 | - case 'vcard' : |
|
187 | - case 'vcard21' : |
|
188 | - case 'vcard30' : |
|
189 | - case 'vcard40' : |
|
190 | - case 'icalendar20' : |
|
191 | - |
|
192 | - $this->inputFormat = 'mimedir'; |
|
193 | - break; |
|
194 | - |
|
195 | - default : |
|
196 | - throw new InvalidArgumentException('Unknown format: ' . $value); |
|
197 | - |
|
198 | - } |
|
199 | - break; |
|
200 | - default : |
|
201 | - throw new InvalidArgumentException('Unknown option: ' . $name); |
|
202 | - |
|
203 | - } |
|
204 | - |
|
205 | - } |
|
206 | - |
|
207 | - if (count($positional) === 0) { |
|
208 | - $this->showHelp(); |
|
209 | - return 1; |
|
210 | - } |
|
211 | - |
|
212 | - if (count($positional) === 1) { |
|
213 | - throw new InvalidArgumentException('Inputfile is a required argument'); |
|
214 | - } |
|
215 | - |
|
216 | - if (count($positional) > 3) { |
|
217 | - throw new InvalidArgumentException('Too many arguments'); |
|
218 | - } |
|
219 | - |
|
220 | - if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { |
|
221 | - throw new InvalidArgumentException('Uknown command: ' . $positional[0]); |
|
222 | - } |
|
223 | - |
|
224 | - } catch (InvalidArgumentException $e) { |
|
225 | - $this->showHelp(); |
|
226 | - $this->log('Error: ' . $e->getMessage(), 'red'); |
|
227 | - return 1; |
|
228 | - } |
|
229 | - |
|
230 | - $command = $positional[0]; |
|
231 | - |
|
232 | - $this->inputPath = $positional[1]; |
|
233 | - $this->outputPath = isset($positional[2]) ? $positional[2] : '-'; |
|
234 | - |
|
235 | - if ($this->outputPath !== '-') { |
|
236 | - $this->stdout = fopen($this->outputPath, 'w'); |
|
237 | - } |
|
238 | - |
|
239 | - if (!$this->inputFormat) { |
|
240 | - if (substr($this->inputPath, -5) === '.json') { |
|
241 | - $this->inputFormat = 'json'; |
|
242 | - } else { |
|
243 | - $this->inputFormat = 'mimedir'; |
|
244 | - } |
|
245 | - } |
|
246 | - if (!$this->format) { |
|
247 | - if (substr($this->outputPath, -5) === '.json') { |
|
248 | - $this->format = 'json'; |
|
249 | - } else { |
|
250 | - $this->format = 'mimedir'; |
|
251 | - } |
|
252 | - } |
|
253 | - |
|
254 | - |
|
255 | - $realCode = 0; |
|
256 | - |
|
257 | - try { |
|
258 | - |
|
259 | - while ($input = $this->readInput()) { |
|
260 | - |
|
261 | - $returnCode = $this->$command($input); |
|
262 | - if ($returnCode !== 0) $realCode = $returnCode; |
|
263 | - |
|
264 | - } |
|
265 | - |
|
266 | - } catch (EofException $e) { |
|
267 | - // end of file |
|
268 | - } catch (\Exception $e) { |
|
269 | - $this->log('Error: ' . $e->getMessage(), 'red'); |
|
270 | - return 2; |
|
271 | - } |
|
272 | - |
|
273 | - return $realCode; |
|
274 | - |
|
275 | - } |
|
276 | - |
|
277 | - /** |
|
278 | - * Shows the help message. |
|
279 | - * |
|
280 | - * @return void |
|
281 | - */ |
|
282 | - protected function showHelp() { |
|
283 | - |
|
284 | - $this->log('Usage:', 'yellow'); |
|
285 | - $this->log(" vobject [options] command [arguments]"); |
|
286 | - $this->log(''); |
|
287 | - $this->log('Options:', 'yellow'); |
|
288 | - $this->log($this->colorize('green', ' -q ') . "Don't output anything."); |
|
289 | - $this->log($this->colorize('green', ' -help -h ') . "Display this help message."); |
|
290 | - $this->log($this->colorize('green', ' --format ') . "Convert to a specific format. Must be one of: vcard, vcard21,"); |
|
291 | - $this->log($this->colorize('green', ' --forgiving ') . "Makes the parser less strict."); |
|
292 | - $this->log(" vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir."); |
|
293 | - $this->log($this->colorize('green', ' --inputformat ') . "If the input format cannot be guessed from the extension, it"); |
|
294 | - $this->log(" must be specified here."); |
|
295 | - // Only PHP 5.4 and up |
|
296 | - if (version_compare(PHP_VERSION, '5.4.0') >= 0) { |
|
297 | - $this->log($this->colorize('green', ' --pretty ') . "json pretty-print."); |
|
298 | - } |
|
299 | - $this->log(''); |
|
300 | - $this->log('Commands:', 'yellow'); |
|
301 | - $this->log($this->colorize('green', ' validate') . ' source_file Validates a file for correctness.'); |
|
302 | - $this->log($this->colorize('green', ' repair') . ' source_file [output_file] Repairs a file.'); |
|
303 | - $this->log($this->colorize('green', ' convert') . ' source_file [output_file] Converts a file.'); |
|
304 | - $this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.'); |
|
305 | - $this->log( |
|
306 | - <<<HELP |
|
17 | + /** |
|
18 | + * No output. |
|
19 | + * |
|
20 | + * @var bool |
|
21 | + */ |
|
22 | + protected $quiet = false; |
|
23 | + |
|
24 | + /** |
|
25 | + * Help display. |
|
26 | + * |
|
27 | + * @var bool |
|
28 | + */ |
|
29 | + protected $showHelp = false; |
|
30 | + |
|
31 | + /** |
|
32 | + * Wether to spit out 'mimedir' or 'json' format. |
|
33 | + * |
|
34 | + * @var string |
|
35 | + */ |
|
36 | + protected $format; |
|
37 | + |
|
38 | + /** |
|
39 | + * JSON pretty print. |
|
40 | + * |
|
41 | + * @var bool |
|
42 | + */ |
|
43 | + protected $pretty; |
|
44 | + |
|
45 | + /** |
|
46 | + * Source file. |
|
47 | + * |
|
48 | + * @var string |
|
49 | + */ |
|
50 | + protected $inputPath; |
|
51 | + |
|
52 | + /** |
|
53 | + * Destination file. |
|
54 | + * |
|
55 | + * @var string |
|
56 | + */ |
|
57 | + protected $outputPath; |
|
58 | + |
|
59 | + /** |
|
60 | + * output stream. |
|
61 | + * |
|
62 | + * @var resource |
|
63 | + */ |
|
64 | + protected $stdout; |
|
65 | + |
|
66 | + /** |
|
67 | + * stdin. |
|
68 | + * |
|
69 | + * @var resource |
|
70 | + */ |
|
71 | + protected $stdin; |
|
72 | + |
|
73 | + /** |
|
74 | + * stderr. |
|
75 | + * |
|
76 | + * @var resource |
|
77 | + */ |
|
78 | + protected $stderr; |
|
79 | + |
|
80 | + /** |
|
81 | + * Input format (one of json or mimedir). |
|
82 | + * |
|
83 | + * @var string |
|
84 | + */ |
|
85 | + protected $inputFormat; |
|
86 | + |
|
87 | + /** |
|
88 | + * Makes the parser less strict. |
|
89 | + * |
|
90 | + * @var bool |
|
91 | + */ |
|
92 | + protected $forgiving = false; |
|
93 | + |
|
94 | + /** |
|
95 | + * Main function. |
|
96 | + * |
|
97 | + * @return int |
|
98 | + */ |
|
99 | + public function main(array $argv) { |
|
100 | + |
|
101 | + // @codeCoverageIgnoreStart |
|
102 | + // We cannot easily test this, so we'll skip it. Pretty basic anyway. |
|
103 | + |
|
104 | + if (!$this->stderr) { |
|
105 | + $this->stderr = fopen('php://stderr', 'w'); |
|
106 | + } |
|
107 | + if (!$this->stdout) { |
|
108 | + $this->stdout = fopen('php://stdout', 'w'); |
|
109 | + } |
|
110 | + if (!$this->stdin) { |
|
111 | + $this->stdin = fopen('php://stdin', 'r'); |
|
112 | + } |
|
113 | + |
|
114 | + // @codeCoverageIgnoreEnd |
|
115 | + |
|
116 | + |
|
117 | + try { |
|
118 | + |
|
119 | + list($options, $positional) = $this->parseArguments($argv); |
|
120 | + |
|
121 | + if (isset($options['q'])) { |
|
122 | + $this->quiet = true; |
|
123 | + } |
|
124 | + $this->log($this->colorize('green', "sabre/vobject ") . $this->colorize('yellow', Version::VERSION)); |
|
125 | + |
|
126 | + foreach ($options as $name => $value) { |
|
127 | + |
|
128 | + switch ($name) { |
|
129 | + |
|
130 | + case 'q' : |
|
131 | + // Already handled earlier. |
|
132 | + break; |
|
133 | + case 'h' : |
|
134 | + case 'help' : |
|
135 | + $this->showHelp(); |
|
136 | + return 0; |
|
137 | + break; |
|
138 | + case 'format' : |
|
139 | + switch ($value) { |
|
140 | + |
|
141 | + // jcard/jcal documents |
|
142 | + case 'jcard' : |
|
143 | + case 'jcal' : |
|
144 | + |
|
145 | + // specific document versions |
|
146 | + case 'vcard21' : |
|
147 | + case 'vcard30' : |
|
148 | + case 'vcard40' : |
|
149 | + case 'icalendar20' : |
|
150 | + |
|
151 | + // specific formats |
|
152 | + case 'json' : |
|
153 | + case 'mimedir' : |
|
154 | + |
|
155 | + // icalendar/vcad |
|
156 | + case 'icalendar' : |
|
157 | + case 'vcard' : |
|
158 | + $this->format = $value; |
|
159 | + break; |
|
160 | + |
|
161 | + default : |
|
162 | + throw new InvalidArgumentException('Unknown format: ' . $value); |
|
163 | + |
|
164 | + } |
|
165 | + break; |
|
166 | + case 'pretty' : |
|
167 | + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { |
|
168 | + $this->pretty = true; |
|
169 | + } |
|
170 | + break; |
|
171 | + case 'forgiving' : |
|
172 | + $this->forgiving = true; |
|
173 | + break; |
|
174 | + case 'inputformat' : |
|
175 | + switch ($value) { |
|
176 | + // json formats |
|
177 | + case 'jcard' : |
|
178 | + case 'jcal' : |
|
179 | + case 'json' : |
|
180 | + $this->inputFormat = 'json'; |
|
181 | + break; |
|
182 | + |
|
183 | + // mimedir formats |
|
184 | + case 'mimedir' : |
|
185 | + case 'icalendar' : |
|
186 | + case 'vcard' : |
|
187 | + case 'vcard21' : |
|
188 | + case 'vcard30' : |
|
189 | + case 'vcard40' : |
|
190 | + case 'icalendar20' : |
|
191 | + |
|
192 | + $this->inputFormat = 'mimedir'; |
|
193 | + break; |
|
194 | + |
|
195 | + default : |
|
196 | + throw new InvalidArgumentException('Unknown format: ' . $value); |
|
197 | + |
|
198 | + } |
|
199 | + break; |
|
200 | + default : |
|
201 | + throw new InvalidArgumentException('Unknown option: ' . $name); |
|
202 | + |
|
203 | + } |
|
204 | + |
|
205 | + } |
|
206 | + |
|
207 | + if (count($positional) === 0) { |
|
208 | + $this->showHelp(); |
|
209 | + return 1; |
|
210 | + } |
|
211 | + |
|
212 | + if (count($positional) === 1) { |
|
213 | + throw new InvalidArgumentException('Inputfile is a required argument'); |
|
214 | + } |
|
215 | + |
|
216 | + if (count($positional) > 3) { |
|
217 | + throw new InvalidArgumentException('Too many arguments'); |
|
218 | + } |
|
219 | + |
|
220 | + if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { |
|
221 | + throw new InvalidArgumentException('Uknown command: ' . $positional[0]); |
|
222 | + } |
|
223 | + |
|
224 | + } catch (InvalidArgumentException $e) { |
|
225 | + $this->showHelp(); |
|
226 | + $this->log('Error: ' . $e->getMessage(), 'red'); |
|
227 | + return 1; |
|
228 | + } |
|
229 | + |
|
230 | + $command = $positional[0]; |
|
231 | + |
|
232 | + $this->inputPath = $positional[1]; |
|
233 | + $this->outputPath = isset($positional[2]) ? $positional[2] : '-'; |
|
234 | + |
|
235 | + if ($this->outputPath !== '-') { |
|
236 | + $this->stdout = fopen($this->outputPath, 'w'); |
|
237 | + } |
|
238 | + |
|
239 | + if (!$this->inputFormat) { |
|
240 | + if (substr($this->inputPath, -5) === '.json') { |
|
241 | + $this->inputFormat = 'json'; |
|
242 | + } else { |
|
243 | + $this->inputFormat = 'mimedir'; |
|
244 | + } |
|
245 | + } |
|
246 | + if (!$this->format) { |
|
247 | + if (substr($this->outputPath, -5) === '.json') { |
|
248 | + $this->format = 'json'; |
|
249 | + } else { |
|
250 | + $this->format = 'mimedir'; |
|
251 | + } |
|
252 | + } |
|
253 | + |
|
254 | + |
|
255 | + $realCode = 0; |
|
256 | + |
|
257 | + try { |
|
258 | + |
|
259 | + while ($input = $this->readInput()) { |
|
260 | + |
|
261 | + $returnCode = $this->$command($input); |
|
262 | + if ($returnCode !== 0) $realCode = $returnCode; |
|
263 | + |
|
264 | + } |
|
265 | + |
|
266 | + } catch (EofException $e) { |
|
267 | + // end of file |
|
268 | + } catch (\Exception $e) { |
|
269 | + $this->log('Error: ' . $e->getMessage(), 'red'); |
|
270 | + return 2; |
|
271 | + } |
|
272 | + |
|
273 | + return $realCode; |
|
274 | + |
|
275 | + } |
|
276 | + |
|
277 | + /** |
|
278 | + * Shows the help message. |
|
279 | + * |
|
280 | + * @return void |
|
281 | + */ |
|
282 | + protected function showHelp() { |
|
283 | + |
|
284 | + $this->log('Usage:', 'yellow'); |
|
285 | + $this->log(" vobject [options] command [arguments]"); |
|
286 | + $this->log(''); |
|
287 | + $this->log('Options:', 'yellow'); |
|
288 | + $this->log($this->colorize('green', ' -q ') . "Don't output anything."); |
|
289 | + $this->log($this->colorize('green', ' -help -h ') . "Display this help message."); |
|
290 | + $this->log($this->colorize('green', ' --format ') . "Convert to a specific format. Must be one of: vcard, vcard21,"); |
|
291 | + $this->log($this->colorize('green', ' --forgiving ') . "Makes the parser less strict."); |
|
292 | + $this->log(" vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir."); |
|
293 | + $this->log($this->colorize('green', ' --inputformat ') . "If the input format cannot be guessed from the extension, it"); |
|
294 | + $this->log(" must be specified here."); |
|
295 | + // Only PHP 5.4 and up |
|
296 | + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { |
|
297 | + $this->log($this->colorize('green', ' --pretty ') . "json pretty-print."); |
|
298 | + } |
|
299 | + $this->log(''); |
|
300 | + $this->log('Commands:', 'yellow'); |
|
301 | + $this->log($this->colorize('green', ' validate') . ' source_file Validates a file for correctness.'); |
|
302 | + $this->log($this->colorize('green', ' repair') . ' source_file [output_file] Repairs a file.'); |
|
303 | + $this->log($this->colorize('green', ' convert') . ' source_file [output_file] Converts a file.'); |
|
304 | + $this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.'); |
|
305 | + $this->log( |
|
306 | + <<<HELP |
|
307 | 307 | |
308 | 308 | If source_file is set as '-', STDIN will be used. |
309 | 309 | If output_file is omitted, STDOUT will be used. |
310 | 310 | All other output is sent to STDERR. |
311 | 311 | |
312 | 312 | HELP |
313 | - ); |
|
314 | - |
|
315 | - $this->log('Examples:', 'yellow'); |
|
316 | - $this->log(' vobject convert contact.vcf contact.json'); |
|
317 | - $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); |
|
318 | - $this->log(' vobject convert --inputformat=json --format=mimedir - -'); |
|
319 | - $this->log(' vobject color calendar.ics'); |
|
320 | - $this->log(''); |
|
321 | - $this->log('https://github.com/fruux/sabre-vobject', 'purple'); |
|
322 | - |
|
323 | - } |
|
324 | - |
|
325 | - /** |
|
326 | - * Validates a VObject file. |
|
327 | - * |
|
328 | - * @param Component $vObj |
|
329 | - * |
|
330 | - * @return int |
|
331 | - */ |
|
332 | - protected function validate(Component $vObj) { |
|
333 | - |
|
334 | - $returnCode = 0; |
|
335 | - |
|
336 | - switch ($vObj->name) { |
|
337 | - case 'VCALENDAR' : |
|
338 | - $this->log("iCalendar: " . (string)$vObj->VERSION); |
|
339 | - break; |
|
340 | - case 'VCARD' : |
|
341 | - $this->log("vCard: " . (string)$vObj->VERSION); |
|
342 | - break; |
|
343 | - } |
|
344 | - |
|
345 | - $warnings = $vObj->validate(); |
|
346 | - if (!count($warnings)) { |
|
347 | - $this->log(" No warnings!"); |
|
348 | - } else { |
|
349 | - |
|
350 | - $levels = [ |
|
351 | - 1 => 'REPAIRED', |
|
352 | - 2 => 'WARNING', |
|
353 | - 3 => 'ERROR', |
|
354 | - ]; |
|
355 | - $returnCode = 2; |
|
356 | - foreach ($warnings as $warn) { |
|
357 | - |
|
358 | - $extra = ''; |
|
359 | - if ($warn['node'] instanceof Property) { |
|
360 | - $extra = ' (property: "' . $warn['node']->name . '")'; |
|
361 | - } |
|
362 | - $this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); |
|
363 | - |
|
364 | - } |
|
365 | - |
|
366 | - } |
|
367 | - |
|
368 | - return $returnCode; |
|
369 | - |
|
370 | - } |
|
371 | - |
|
372 | - /** |
|
373 | - * Repairs a VObject file. |
|
374 | - * |
|
375 | - * @param Component $vObj |
|
376 | - * |
|
377 | - * @return int |
|
378 | - */ |
|
379 | - protected function repair(Component $vObj) { |
|
380 | - |
|
381 | - $returnCode = 0; |
|
382 | - |
|
383 | - switch ($vObj->name) { |
|
384 | - case 'VCALENDAR' : |
|
385 | - $this->log("iCalendar: " . (string)$vObj->VERSION); |
|
386 | - break; |
|
387 | - case 'VCARD' : |
|
388 | - $this->log("vCard: " . (string)$vObj->VERSION); |
|
389 | - break; |
|
390 | - } |
|
391 | - |
|
392 | - $warnings = $vObj->validate(Node::REPAIR); |
|
393 | - if (!count($warnings)) { |
|
394 | - $this->log(" No warnings!"); |
|
395 | - } else { |
|
396 | - |
|
397 | - $levels = [ |
|
398 | - 1 => 'REPAIRED', |
|
399 | - 2 => 'WARNING', |
|
400 | - 3 => 'ERROR', |
|
401 | - ]; |
|
402 | - $returnCode = 2; |
|
403 | - foreach ($warnings as $warn) { |
|
404 | - |
|
405 | - $extra = ''; |
|
406 | - if ($warn['node'] instanceof Property) { |
|
407 | - $extra = ' (property: "' . $warn['node']->name . '")'; |
|
408 | - } |
|
409 | - $this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); |
|
410 | - |
|
411 | - } |
|
412 | - |
|
413 | - } |
|
414 | - fwrite($this->stdout, $vObj->serialize()); |
|
415 | - |
|
416 | - return $returnCode; |
|
417 | - |
|
418 | - } |
|
419 | - |
|
420 | - /** |
|
421 | - * Converts a vObject file to a new format. |
|
422 | - * |
|
423 | - * @param Component $vObj |
|
424 | - * |
|
425 | - * @return int |
|
426 | - */ |
|
427 | - protected function convert($vObj) { |
|
428 | - |
|
429 | - $json = false; |
|
430 | - $convertVersion = null; |
|
431 | - $forceInput = null; |
|
432 | - |
|
433 | - switch ($this->format) { |
|
434 | - case 'json' : |
|
435 | - $json = true; |
|
436 | - if ($vObj->name === 'VCARD') { |
|
437 | - $convertVersion = Document::VCARD40; |
|
438 | - } |
|
439 | - break; |
|
440 | - case 'jcard' : |
|
441 | - $json = true; |
|
442 | - $forceInput = 'VCARD'; |
|
443 | - $convertVersion = Document::VCARD40; |
|
444 | - break; |
|
445 | - case 'jcal' : |
|
446 | - $json = true; |
|
447 | - $forceInput = 'VCALENDAR'; |
|
448 | - break; |
|
449 | - case 'mimedir' : |
|
450 | - case 'icalendar' : |
|
451 | - case 'icalendar20' : |
|
452 | - case 'vcard' : |
|
453 | - break; |
|
454 | - case 'vcard21' : |
|
455 | - $convertVersion = Document::VCARD21; |
|
456 | - break; |
|
457 | - case 'vcard30' : |
|
458 | - $convertVersion = Document::VCARD30; |
|
459 | - break; |
|
460 | - case 'vcard40' : |
|
461 | - $convertVersion = Document::VCARD40; |
|
462 | - break; |
|
463 | - |
|
464 | - } |
|
465 | - |
|
466 | - if ($forceInput && $vObj->name !== $forceInput) { |
|
467 | - throw new \Exception('You cannot convert a ' . strtolower($vObj->name) . ' to ' . $this->format); |
|
468 | - } |
|
469 | - if ($convertVersion) { |
|
470 | - $vObj = $vObj->convert($convertVersion); |
|
471 | - } |
|
472 | - if ($json) { |
|
473 | - $jsonOptions = 0; |
|
474 | - if ($this->pretty) { |
|
475 | - $jsonOptions = JSON_PRETTY_PRINT; |
|
476 | - } |
|
477 | - fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); |
|
478 | - } else { |
|
479 | - fwrite($this->stdout, $vObj->serialize()); |
|
480 | - } |
|
481 | - |
|
482 | - return 0; |
|
483 | - |
|
484 | - } |
|
485 | - |
|
486 | - /** |
|
487 | - * Colorizes a file. |
|
488 | - * |
|
489 | - * @param Component $vObj |
|
490 | - * |
|
491 | - * @return int |
|
492 | - */ |
|
493 | - protected function color($vObj) { |
|
494 | - |
|
495 | - fwrite($this->stdout, $this->serializeComponent($vObj)); |
|
496 | - |
|
497 | - } |
|
498 | - |
|
499 | - /** |
|
500 | - * Returns an ansi color string for a color name. |
|
501 | - * |
|
502 | - * @param string $color |
|
503 | - * |
|
504 | - * @return string |
|
505 | - */ |
|
506 | - protected function colorize($color, $str, $resetTo = 'default') { |
|
507 | - |
|
508 | - $colors = [ |
|
509 | - 'cyan' => '1;36', |
|
510 | - 'red' => '1;31', |
|
511 | - 'yellow' => '1;33', |
|
512 | - 'blue' => '0;34', |
|
513 | - 'green' => '0;32', |
|
514 | - 'default' => '0', |
|
515 | - 'purple' => '0;35', |
|
516 | - ]; |
|
517 | - return "\033[" . $colors[$color] . 'm' . $str . "\033[" . $colors[$resetTo] . "m"; |
|
518 | - |
|
519 | - } |
|
520 | - |
|
521 | - /** |
|
522 | - * Writes out a string in specific color. |
|
523 | - * |
|
524 | - * @param string $color |
|
525 | - * @param string $str |
|
526 | - * |
|
527 | - * @return void |
|
528 | - */ |
|
529 | - protected function cWrite($color, $str) { |
|
530 | - |
|
531 | - fwrite($this->stdout, $this->colorize($color, $str)); |
|
532 | - |
|
533 | - } |
|
534 | - |
|
535 | - protected function serializeComponent(Component $vObj) { |
|
536 | - |
|
537 | - $this->cWrite('cyan', 'BEGIN'); |
|
538 | - $this->cWrite('red', ':'); |
|
539 | - $this->cWrite('yellow', $vObj->name . "\n"); |
|
540 | - |
|
541 | - /** |
|
542 | - * Gives a component a 'score' for sorting purposes. |
|
543 | - * |
|
544 | - * This is solely used by the childrenSort method. |
|
545 | - * |
|
546 | - * A higher score means the item will be lower in the list. |
|
547 | - * To avoid score collisions, each "score category" has a reasonable |
|
548 | - * space to accomodate elements. The $key is added to the $score to |
|
549 | - * preserve the original relative order of elements. |
|
550 | - * |
|
551 | - * @param int $key |
|
552 | - * @param array $array |
|
553 | - * |
|
554 | - * @return int |
|
555 | - */ |
|
556 | - $sortScore = function($key, $array) { |
|
557 | - |
|
558 | - if ($array[$key] instanceof Component) { |
|
559 | - |
|
560 | - // We want to encode VTIMEZONE first, this is a personal |
|
561 | - // preference. |
|
562 | - if ($array[$key]->name === 'VTIMEZONE') { |
|
563 | - $score = 300000000; |
|
564 | - return $score + $key; |
|
565 | - } else { |
|
566 | - $score = 400000000; |
|
567 | - return $score + $key; |
|
568 | - } |
|
569 | - } else { |
|
570 | - // Properties get encoded first |
|
571 | - // VCARD version 4.0 wants the VERSION property to appear first |
|
572 | - if ($array[$key] instanceof Property) { |
|
573 | - if ($array[$key]->name === 'VERSION') { |
|
574 | - $score = 100000000; |
|
575 | - return $score + $key; |
|
576 | - } else { |
|
577 | - // All other properties |
|
578 | - $score = 200000000; |
|
579 | - return $score + $key; |
|
580 | - } |
|
581 | - } |
|
582 | - } |
|
583 | - |
|
584 | - }; |
|
585 | - |
|
586 | - $children = $vObj->children(); |
|
587 | - $tmp = $children; |
|
588 | - uksort( |
|
589 | - $children, |
|
590 | - public function($a, $b) use ($sortScore, $tmp) { |
|
591 | - |
|
592 | - $sA = $sortScore($a, $tmp); |
|
593 | - $sB = $sortScore($b, $tmp); |
|
594 | - |
|
595 | - return $sA - $sB; |
|
596 | - |
|
597 | - } |
|
598 | - ); |
|
599 | - |
|
600 | - foreach ($children as $child) { |
|
601 | - if ($child instanceof Component) { |
|
602 | - $this->serializeComponent($child); |
|
603 | - } else { |
|
604 | - $this->serializeProperty($child); |
|
605 | - } |
|
606 | - } |
|
607 | - |
|
608 | - $this->cWrite('cyan', 'END'); |
|
609 | - $this->cWrite('red', ':'); |
|
610 | - $this->cWrite('yellow', $vObj->name . "\n"); |
|
611 | - |
|
612 | - } |
|
613 | - |
|
614 | - /** |
|
615 | - * Colorizes a property. |
|
616 | - * |
|
617 | - * @param Property $property |
|
618 | - * |
|
619 | - * @return void |
|
620 | - */ |
|
621 | - protected function serializeProperty(Property $property) { |
|
622 | - |
|
623 | - if ($property->group) { |
|
624 | - $this->cWrite('default', $property->group); |
|
625 | - $this->cWrite('red', '.'); |
|
626 | - } |
|
627 | - |
|
628 | - $this->cWrite('yellow', $property->name); |
|
629 | - |
|
630 | - foreach ($property->parameters as $param) { |
|
631 | - |
|
632 | - $this->cWrite('red', ';'); |
|
633 | - $this->cWrite('blue', $param->serialize()); |
|
634 | - |
|
635 | - } |
|
636 | - $this->cWrite('red', ':'); |
|
637 | - |
|
638 | - if ($property instanceof Property\Binary) { |
|
639 | - |
|
640 | - $this->cWrite('default', 'embedded binary stripped. (' . strlen($property->getValue()) . ' bytes)'); |
|
641 | - |
|
642 | - } else { |
|
643 | - |
|
644 | - $parts = $property->getParts(); |
|
645 | - $first1 = true; |
|
646 | - // Looping through property values |
|
647 | - foreach ($parts as $part) { |
|
648 | - if ($first1) { |
|
649 | - $first1 = false; |
|
650 | - } else { |
|
651 | - $this->cWrite('red', $property->delimiter); |
|
652 | - } |
|
653 | - $first2 = true; |
|
654 | - // Looping through property sub-values |
|
655 | - foreach ((array)$part as $subPart) { |
|
656 | - if ($first2) { |
|
657 | - $first2 = false; |
|
658 | - } else { |
|
659 | - // The sub-value delimiter is always comma |
|
660 | - $this->cWrite('red', ','); |
|
661 | - } |
|
662 | - |
|
663 | - $subPart = strtr( |
|
664 | - $subPart, |
|
665 | - [ |
|
666 | - '\\' => $this->colorize('purple', '\\\\', 'green'), |
|
667 | - ';' => $this->colorize('purple', '\;', 'green'), |
|
668 | - ',' => $this->colorize('purple', '\,', 'green'), |
|
669 | - "\n" => $this->colorize('purple', "\\n\n\t", 'green'), |
|
670 | - "\r" => "", |
|
671 | - ] |
|
672 | - ); |
|
673 | - |
|
674 | - $this->cWrite('green', $subPart); |
|
675 | - } |
|
676 | - } |
|
677 | - |
|
678 | - } |
|
679 | - $this->cWrite("default", "\n"); |
|
680 | - |
|
681 | - } |
|
682 | - |
|
683 | - /** |
|
684 | - * Parses the list of arguments. |
|
685 | - * |
|
686 | - * @param array $argv |
|
687 | - * |
|
688 | - * @return void |
|
689 | - */ |
|
690 | - protected function parseArguments(array $argv) { |
|
691 | - |
|
692 | - $positional = []; |
|
693 | - $options = []; |
|
694 | - |
|
695 | - for ($ii = 0; $ii < count($argv); $ii++) { |
|
696 | - |
|
697 | - // Skipping the first argument. |
|
698 | - if ($ii === 0) continue; |
|
699 | - |
|
700 | - $v = $argv[$ii]; |
|
701 | - |
|
702 | - if (substr($v, 0, 2) === '--') { |
|
703 | - // This is a long-form option. |
|
704 | - $optionName = substr($v, 2); |
|
705 | - $optionValue = true; |
|
706 | - if (strpos($optionName, '=')) { |
|
707 | - list($optionName, $optionValue) = explode('=', $optionName); |
|
708 | - } |
|
709 | - $options[$optionName] = $optionValue; |
|
710 | - } elseif (substr($v, 0, 1) === '-' && strlen($v) > 1) { |
|
711 | - // This is a short-form option. |
|
712 | - foreach (str_split(substr($v, 1)) as $option) { |
|
713 | - $options[$option] = true; |
|
714 | - } |
|
715 | - |
|
716 | - } else { |
|
717 | - |
|
718 | - $positional[] = $v; |
|
719 | - |
|
720 | - } |
|
721 | - |
|
722 | - } |
|
723 | - |
|
724 | - return [$options, $positional]; |
|
725 | - |
|
726 | - } |
|
727 | - |
|
728 | - protected $parser; |
|
729 | - |
|
730 | - /** |
|
731 | - * Reads the input file. |
|
732 | - * |
|
733 | - * @return Component |
|
734 | - */ |
|
735 | - protected function readInput() { |
|
736 | - |
|
737 | - if (!$this->parser) { |
|
738 | - if ($this->inputPath !== '-') { |
|
739 | - $this->stdin = fopen($this->inputPath, 'r'); |
|
740 | - } |
|
741 | - |
|
742 | - if ($this->inputFormat === 'mimedir') { |
|
743 | - $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); |
|
744 | - } else { |
|
745 | - $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); |
|
746 | - } |
|
747 | - } |
|
748 | - |
|
749 | - return $this->parser->parse(); |
|
750 | - |
|
751 | - } |
|
752 | - |
|
753 | - /** |
|
754 | - * Sends a message to STDERR. |
|
755 | - * |
|
756 | - * @param string $msg |
|
757 | - * |
|
758 | - * @return void |
|
759 | - */ |
|
760 | - protected function log($msg, $color = 'default') { |
|
761 | - |
|
762 | - if (!$this->quiet) { |
|
763 | - if ($color !== 'default') { |
|
764 | - $msg = $this->colorize($color, $msg); |
|
765 | - } |
|
766 | - fwrite($this->stderr, $msg . "\n"); |
|
767 | - } |
|
768 | - |
|
769 | - } |
|
313 | + ); |
|
314 | + |
|
315 | + $this->log('Examples:', 'yellow'); |
|
316 | + $this->log(' vobject convert contact.vcf contact.json'); |
|
317 | + $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); |
|
318 | + $this->log(' vobject convert --inputformat=json --format=mimedir - -'); |
|
319 | + $this->log(' vobject color calendar.ics'); |
|
320 | + $this->log(''); |
|
321 | + $this->log('https://github.com/fruux/sabre-vobject', 'purple'); |
|
322 | + |
|
323 | + } |
|
324 | + |
|
325 | + /** |
|
326 | + * Validates a VObject file. |
|
327 | + * |
|
328 | + * @param Component $vObj |
|
329 | + * |
|
330 | + * @return int |
|
331 | + */ |
|
332 | + protected function validate(Component $vObj) { |
|
333 | + |
|
334 | + $returnCode = 0; |
|
335 | + |
|
336 | + switch ($vObj->name) { |
|
337 | + case 'VCALENDAR' : |
|
338 | + $this->log("iCalendar: " . (string)$vObj->VERSION); |
|
339 | + break; |
|
340 | + case 'VCARD' : |
|
341 | + $this->log("vCard: " . (string)$vObj->VERSION); |
|
342 | + break; |
|
343 | + } |
|
344 | + |
|
345 | + $warnings = $vObj->validate(); |
|
346 | + if (!count($warnings)) { |
|
347 | + $this->log(" No warnings!"); |
|
348 | + } else { |
|
349 | + |
|
350 | + $levels = [ |
|
351 | + 1 => 'REPAIRED', |
|
352 | + 2 => 'WARNING', |
|
353 | + 3 => 'ERROR', |
|
354 | + ]; |
|
355 | + $returnCode = 2; |
|
356 | + foreach ($warnings as $warn) { |
|
357 | + |
|
358 | + $extra = ''; |
|
359 | + if ($warn['node'] instanceof Property) { |
|
360 | + $extra = ' (property: "' . $warn['node']->name . '")'; |
|
361 | + } |
|
362 | + $this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); |
|
363 | + |
|
364 | + } |
|
365 | + |
|
366 | + } |
|
367 | + |
|
368 | + return $returnCode; |
|
369 | + |
|
370 | + } |
|
371 | + |
|
372 | + /** |
|
373 | + * Repairs a VObject file. |
|
374 | + * |
|
375 | + * @param Component $vObj |
|
376 | + * |
|
377 | + * @return int |
|
378 | + */ |
|
379 | + protected function repair(Component $vObj) { |
|
380 | + |
|
381 | + $returnCode = 0; |
|
382 | + |
|
383 | + switch ($vObj->name) { |
|
384 | + case 'VCALENDAR' : |
|
385 | + $this->log("iCalendar: " . (string)$vObj->VERSION); |
|
386 | + break; |
|
387 | + case 'VCARD' : |
|
388 | + $this->log("vCard: " . (string)$vObj->VERSION); |
|
389 | + break; |
|
390 | + } |
|
391 | + |
|
392 | + $warnings = $vObj->validate(Node::REPAIR); |
|
393 | + if (!count($warnings)) { |
|
394 | + $this->log(" No warnings!"); |
|
395 | + } else { |
|
396 | + |
|
397 | + $levels = [ |
|
398 | + 1 => 'REPAIRED', |
|
399 | + 2 => 'WARNING', |
|
400 | + 3 => 'ERROR', |
|
401 | + ]; |
|
402 | + $returnCode = 2; |
|
403 | + foreach ($warnings as $warn) { |
|
404 | + |
|
405 | + $extra = ''; |
|
406 | + if ($warn['node'] instanceof Property) { |
|
407 | + $extra = ' (property: "' . $warn['node']->name . '")'; |
|
408 | + } |
|
409 | + $this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); |
|
410 | + |
|
411 | + } |
|
412 | + |
|
413 | + } |
|
414 | + fwrite($this->stdout, $vObj->serialize()); |
|
415 | + |
|
416 | + return $returnCode; |
|
417 | + |
|
418 | + } |
|
419 | + |
|
420 | + /** |
|
421 | + * Converts a vObject file to a new format. |
|
422 | + * |
|
423 | + * @param Component $vObj |
|
424 | + * |
|
425 | + * @return int |
|
426 | + */ |
|
427 | + protected function convert($vObj) { |
|
428 | + |
|
429 | + $json = false; |
|
430 | + $convertVersion = null; |
|
431 | + $forceInput = null; |
|
432 | + |
|
433 | + switch ($this->format) { |
|
434 | + case 'json' : |
|
435 | + $json = true; |
|
436 | + if ($vObj->name === 'VCARD') { |
|
437 | + $convertVersion = Document::VCARD40; |
|
438 | + } |
|
439 | + break; |
|
440 | + case 'jcard' : |
|
441 | + $json = true; |
|
442 | + $forceInput = 'VCARD'; |
|
443 | + $convertVersion = Document::VCARD40; |
|
444 | + break; |
|
445 | + case 'jcal' : |
|
446 | + $json = true; |
|
447 | + $forceInput = 'VCALENDAR'; |
|
448 | + break; |
|
449 | + case 'mimedir' : |
|
450 | + case 'icalendar' : |
|
451 | + case 'icalendar20' : |
|
452 | + case 'vcard' : |
|
453 | + break; |
|
454 | + case 'vcard21' : |
|
455 | + $convertVersion = Document::VCARD21; |
|
456 | + break; |
|
457 | + case 'vcard30' : |
|
458 | + $convertVersion = Document::VCARD30; |
|
459 | + break; |
|
460 | + case 'vcard40' : |
|
461 | + $convertVersion = Document::VCARD40; |
|
462 | + break; |
|
463 | + |
|
464 | + } |
|
465 | + |
|
466 | + if ($forceInput && $vObj->name !== $forceInput) { |
|
467 | + throw new \Exception('You cannot convert a ' . strtolower($vObj->name) . ' to ' . $this->format); |
|
468 | + } |
|
469 | + if ($convertVersion) { |
|
470 | + $vObj = $vObj->convert($convertVersion); |
|
471 | + } |
|
472 | + if ($json) { |
|
473 | + $jsonOptions = 0; |
|
474 | + if ($this->pretty) { |
|
475 | + $jsonOptions = JSON_PRETTY_PRINT; |
|
476 | + } |
|
477 | + fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); |
|
478 | + } else { |
|
479 | + fwrite($this->stdout, $vObj->serialize()); |
|
480 | + } |
|
481 | + |
|
482 | + return 0; |
|
483 | + |
|
484 | + } |
|
485 | + |
|
486 | + /** |
|
487 | + * Colorizes a file. |
|
488 | + * |
|
489 | + * @param Component $vObj |
|
490 | + * |
|
491 | + * @return int |
|
492 | + */ |
|
493 | + protected function color($vObj) { |
|
494 | + |
|
495 | + fwrite($this->stdout, $this->serializeComponent($vObj)); |
|
496 | + |
|
497 | + } |
|
498 | + |
|
499 | + /** |
|
500 | + * Returns an ansi color string for a color name. |
|
501 | + * |
|
502 | + * @param string $color |
|
503 | + * |
|
504 | + * @return string |
|
505 | + */ |
|
506 | + protected function colorize($color, $str, $resetTo = 'default') { |
|
507 | + |
|
508 | + $colors = [ |
|
509 | + 'cyan' => '1;36', |
|
510 | + 'red' => '1;31', |
|
511 | + 'yellow' => '1;33', |
|
512 | + 'blue' => '0;34', |
|
513 | + 'green' => '0;32', |
|
514 | + 'default' => '0', |
|
515 | + 'purple' => '0;35', |
|
516 | + ]; |
|
517 | + return "\033[" . $colors[$color] . 'm' . $str . "\033[" . $colors[$resetTo] . "m"; |
|
518 | + |
|
519 | + } |
|
520 | + |
|
521 | + /** |
|
522 | + * Writes out a string in specific color. |
|
523 | + * |
|
524 | + * @param string $color |
|
525 | + * @param string $str |
|
526 | + * |
|
527 | + * @return void |
|
528 | + */ |
|
529 | + protected function cWrite($color, $str) { |
|
530 | + |
|
531 | + fwrite($this->stdout, $this->colorize($color, $str)); |
|
532 | + |
|
533 | + } |
|
534 | + |
|
535 | + protected function serializeComponent(Component $vObj) { |
|
536 | + |
|
537 | + $this->cWrite('cyan', 'BEGIN'); |
|
538 | + $this->cWrite('red', ':'); |
|
539 | + $this->cWrite('yellow', $vObj->name . "\n"); |
|
540 | + |
|
541 | + /** |
|
542 | + * Gives a component a 'score' for sorting purposes. |
|
543 | + * |
|
544 | + * This is solely used by the childrenSort method. |
|
545 | + * |
|
546 | + * A higher score means the item will be lower in the list. |
|
547 | + * To avoid score collisions, each "score category" has a reasonable |
|
548 | + * space to accomodate elements. The $key is added to the $score to |
|
549 | + * preserve the original relative order of elements. |
|
550 | + * |
|
551 | + * @param int $key |
|
552 | + * @param array $array |
|
553 | + * |
|
554 | + * @return int |
|
555 | + */ |
|
556 | + $sortScore = function($key, $array) { |
|
557 | + |
|
558 | + if ($array[$key] instanceof Component) { |
|
559 | + |
|
560 | + // We want to encode VTIMEZONE first, this is a personal |
|
561 | + // preference. |
|
562 | + if ($array[$key]->name === 'VTIMEZONE') { |
|
563 | + $score = 300000000; |
|
564 | + return $score + $key; |
|
565 | + } else { |
|
566 | + $score = 400000000; |
|
567 | + return $score + $key; |
|
568 | + } |
|
569 | + } else { |
|
570 | + // Properties get encoded first |
|
571 | + // VCARD version 4.0 wants the VERSION property to appear first |
|
572 | + if ($array[$key] instanceof Property) { |
|
573 | + if ($array[$key]->name === 'VERSION') { |
|
574 | + $score = 100000000; |
|
575 | + return $score + $key; |
|
576 | + } else { |
|
577 | + // All other properties |
|
578 | + $score = 200000000; |
|
579 | + return $score + $key; |
|
580 | + } |
|
581 | + } |
|
582 | + } |
|
583 | + |
|
584 | + }; |
|
585 | + |
|
586 | + $children = $vObj->children(); |
|
587 | + $tmp = $children; |
|
588 | + uksort( |
|
589 | + $children, |
|
590 | + public function($a, $b) use ($sortScore, $tmp) { |
|
591 | + |
|
592 | + $sA = $sortScore($a, $tmp); |
|
593 | + $sB = $sortScore($b, $tmp); |
|
594 | + |
|
595 | + return $sA - $sB; |
|
596 | + |
|
597 | + } |
|
598 | + ); |
|
599 | + |
|
600 | + foreach ($children as $child) { |
|
601 | + if ($child instanceof Component) { |
|
602 | + $this->serializeComponent($child); |
|
603 | + } else { |
|
604 | + $this->serializeProperty($child); |
|
605 | + } |
|
606 | + } |
|
607 | + |
|
608 | + $this->cWrite('cyan', 'END'); |
|
609 | + $this->cWrite('red', ':'); |
|
610 | + $this->cWrite('yellow', $vObj->name . "\n"); |
|
611 | + |
|
612 | + } |
|
613 | + |
|
614 | + /** |
|
615 | + * Colorizes a property. |
|
616 | + * |
|
617 | + * @param Property $property |
|
618 | + * |
|
619 | + * @return void |
|
620 | + */ |
|
621 | + protected function serializeProperty(Property $property) { |
|
622 | + |
|
623 | + if ($property->group) { |
|
624 | + $this->cWrite('default', $property->group); |
|
625 | + $this->cWrite('red', '.'); |
|
626 | + } |
|
627 | + |
|
628 | + $this->cWrite('yellow', $property->name); |
|
629 | + |
|
630 | + foreach ($property->parameters as $param) { |
|
631 | + |
|
632 | + $this->cWrite('red', ';'); |
|
633 | + $this->cWrite('blue', $param->serialize()); |
|
634 | + |
|
635 | + } |
|
636 | + $this->cWrite('red', ':'); |
|
637 | + |
|
638 | + if ($property instanceof Property\Binary) { |
|
639 | + |
|
640 | + $this->cWrite('default', 'embedded binary stripped. (' . strlen($property->getValue()) . ' bytes)'); |
|
641 | + |
|
642 | + } else { |
|
643 | + |
|
644 | + $parts = $property->getParts(); |
|
645 | + $first1 = true; |
|
646 | + // Looping through property values |
|
647 | + foreach ($parts as $part) { |
|
648 | + if ($first1) { |
|
649 | + $first1 = false; |
|
650 | + } else { |
|
651 | + $this->cWrite('red', $property->delimiter); |
|
652 | + } |
|
653 | + $first2 = true; |
|
654 | + // Looping through property sub-values |
|
655 | + foreach ((array)$part as $subPart) { |
|
656 | + if ($first2) { |
|
657 | + $first2 = false; |
|
658 | + } else { |
|
659 | + // The sub-value delimiter is always comma |
|
660 | + $this->cWrite('red', ','); |
|
661 | + } |
|
662 | + |
|
663 | + $subPart = strtr( |
|
664 | + $subPart, |
|
665 | + [ |
|
666 | + '\\' => $this->colorize('purple', '\\\\', 'green'), |
|
667 | + ';' => $this->colorize('purple', '\;', 'green'), |
|
668 | + ',' => $this->colorize('purple', '\,', 'green'), |
|
669 | + "\n" => $this->colorize('purple', "\\n\n\t", 'green'), |
|
670 | + "\r" => "", |
|
671 | + ] |
|
672 | + ); |
|
673 | + |
|
674 | + $this->cWrite('green', $subPart); |
|
675 | + } |
|
676 | + } |
|
677 | + |
|
678 | + } |
|
679 | + $this->cWrite("default", "\n"); |
|
680 | + |
|
681 | + } |
|
682 | + |
|
683 | + /** |
|
684 | + * Parses the list of arguments. |
|
685 | + * |
|
686 | + * @param array $argv |
|
687 | + * |
|
688 | + * @return void |
|
689 | + */ |
|
690 | + protected function parseArguments(array $argv) { |
|
691 | + |
|
692 | + $positional = []; |
|
693 | + $options = []; |
|
694 | + |
|
695 | + for ($ii = 0; $ii < count($argv); $ii++) { |
|
696 | + |
|
697 | + // Skipping the first argument. |
|
698 | + if ($ii === 0) continue; |
|
699 | + |
|
700 | + $v = $argv[$ii]; |
|
701 | + |
|
702 | + if (substr($v, 0, 2) === '--') { |
|
703 | + // This is a long-form option. |
|
704 | + $optionName = substr($v, 2); |
|
705 | + $optionValue = true; |
|
706 | + if (strpos($optionName, '=')) { |
|
707 | + list($optionName, $optionValue) = explode('=', $optionName); |
|
708 | + } |
|
709 | + $options[$optionName] = $optionValue; |
|
710 | + } elseif (substr($v, 0, 1) === '-' && strlen($v) > 1) { |
|
711 | + // This is a short-form option. |
|
712 | + foreach (str_split(substr($v, 1)) as $option) { |
|
713 | + $options[$option] = true; |
|
714 | + } |
|
715 | + |
|
716 | + } else { |
|
717 | + |
|
718 | + $positional[] = $v; |
|
719 | + |
|
720 | + } |
|
721 | + |
|
722 | + } |
|
723 | + |
|
724 | + return [$options, $positional]; |
|
725 | + |
|
726 | + } |
|
727 | + |
|
728 | + protected $parser; |
|
729 | + |
|
730 | + /** |
|
731 | + * Reads the input file. |
|
732 | + * |
|
733 | + * @return Component |
|
734 | + */ |
|
735 | + protected function readInput() { |
|
736 | + |
|
737 | + if (!$this->parser) { |
|
738 | + if ($this->inputPath !== '-') { |
|
739 | + $this->stdin = fopen($this->inputPath, 'r'); |
|
740 | + } |
|
741 | + |
|
742 | + if ($this->inputFormat === 'mimedir') { |
|
743 | + $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); |
|
744 | + } else { |
|
745 | + $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); |
|
746 | + } |
|
747 | + } |
|
748 | + |
|
749 | + return $this->parser->parse(); |
|
750 | + |
|
751 | + } |
|
752 | + |
|
753 | + /** |
|
754 | + * Sends a message to STDERR. |
|
755 | + * |
|
756 | + * @param string $msg |
|
757 | + * |
|
758 | + * @return void |
|
759 | + */ |
|
760 | + protected function log($msg, $color = 'default') { |
|
761 | + |
|
762 | + if (!$this->quiet) { |
|
763 | + if ($color !== 'default') { |
|
764 | + $msg = $this->colorize($color, $msg); |
|
765 | + } |
|
766 | + fwrite($this->stderr, $msg . "\n"); |
|
767 | + } |
|
768 | + |
|
769 | + } |
|
770 | 770 | |
771 | 771 | } |
@@ -18,37 +18,37 @@ |
||
18 | 18 | class ElementList extends ArrayIterator { |
19 | 19 | |
20 | 20 | |
21 | - /* {{{ ArrayAccess Interface */ |
|
22 | - |
|
23 | - /** |
|
24 | - * Sets an item through ArrayAccess. |
|
25 | - * |
|
26 | - * @param int $offset |
|
27 | - * @param mixed $value |
|
28 | - * |
|
29 | - * @return void |
|
30 | - */ |
|
31 | - public function offsetSet($offset, $value) { |
|
32 | - |
|
33 | - throw new LogicException('You can not add new objects to an ElementList'); |
|
34 | - |
|
35 | - } |
|
36 | - |
|
37 | - /** |
|
38 | - * Sets an item through ArrayAccess. |
|
39 | - * |
|
40 | - * This method just forwards the request to the inner iterator |
|
41 | - * |
|
42 | - * @param int $offset |
|
43 | - * |
|
44 | - * @return void |
|
45 | - */ |
|
46 | - public function offsetUnset($offset) { |
|
47 | - |
|
48 | - throw new LogicException('You can not remove objects from an ElementList'); |
|
49 | - |
|
50 | - } |
|
51 | - |
|
52 | - /* }}} */ |
|
21 | + /* {{{ ArrayAccess Interface */ |
|
22 | + |
|
23 | + /** |
|
24 | + * Sets an item through ArrayAccess. |
|
25 | + * |
|
26 | + * @param int $offset |
|
27 | + * @param mixed $value |
|
28 | + * |
|
29 | + * @return void |
|
30 | + */ |
|
31 | + public function offsetSet($offset, $value) { |
|
32 | + |
|
33 | + throw new LogicException('You can not add new objects to an ElementList'); |
|
34 | + |
|
35 | + } |
|
36 | + |
|
37 | + /** |
|
38 | + * Sets an item through ArrayAccess. |
|
39 | + * |
|
40 | + * This method just forwards the request to the inner iterator |
|
41 | + * |
|
42 | + * @param int $offset |
|
43 | + * |
|
44 | + * @return void |
|
45 | + */ |
|
46 | + public function offsetUnset($offset) { |
|
47 | + |
|
48 | + throw new LogicException('You can not remove objects from an ElementList'); |
|
49 | + |
|
50 | + } |
|
51 | + |
|
52 | + /* }}} */ |
|
53 | 53 | |
54 | 54 | } |
@@ -11,457 +11,457 @@ |
||
11 | 11 | */ |
12 | 12 | class VCardConverter { |
13 | 13 | |
14 | - /** |
|
15 | - * Converts a vCard object to a new version. |
|
16 | - * |
|
17 | - * targetVersion must be one of: |
|
18 | - * Document::VCARD21 |
|
19 | - * Document::VCARD30 |
|
20 | - * Document::VCARD40 |
|
21 | - * |
|
22 | - * Currently only 3.0 and 4.0 as input and output versions. |
|
23 | - * |
|
24 | - * 2.1 has some minor support for the input version, it's incomplete at the |
|
25 | - * moment though. |
|
26 | - * |
|
27 | - * If input and output version are identical, a clone is returned. |
|
28 | - * |
|
29 | - * @param Component\VCard $input |
|
30 | - * @param int $targetVersion |
|
31 | - */ |
|
32 | - public function convert(Component\VCard $input, $targetVersion) { |
|
33 | - |
|
34 | - $inputVersion = $input->getDocumentType(); |
|
35 | - if ($inputVersion === $targetVersion) { |
|
36 | - return clone $input; |
|
37 | - } |
|
38 | - |
|
39 | - if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { |
|
40 | - throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); |
|
41 | - } |
|
42 | - if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { |
|
43 | - throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); |
|
44 | - } |
|
45 | - |
|
46 | - $newVersion = $targetVersion === Document::VCARD40 ? '4.0' : '3.0'; |
|
47 | - |
|
48 | - $output = new Component\VCard([ |
|
49 | - 'VERSION' => $newVersion, |
|
50 | - ]); |
|
51 | - |
|
52 | - // We might have generated a default UID. Remove it! |
|
53 | - unset($output->UID); |
|
54 | - |
|
55 | - foreach ($input->children() as $property) { |
|
56 | - |
|
57 | - $this->convertProperty($input, $output, $property, $targetVersion); |
|
58 | - |
|
59 | - } |
|
60 | - |
|
61 | - return $output; |
|
62 | - |
|
63 | - } |
|
64 | - |
|
65 | - /** |
|
66 | - * Handles conversion of a single property. |
|
67 | - * |
|
68 | - * @param Component\VCard $input |
|
69 | - * @param Component\VCard $output |
|
70 | - * @param Property $property |
|
71 | - * @param int $targetVersion |
|
72 | - * |
|
73 | - * @return void |
|
74 | - */ |
|
75 | - protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) { |
|
76 | - |
|
77 | - // Skipping these, those are automatically added. |
|
78 | - if (in_array($property->name, ['VERSION', 'PRODID'])) { |
|
79 | - return; |
|
80 | - } |
|
81 | - |
|
82 | - $parameters = $property->parameters(); |
|
83 | - $valueType = null; |
|
84 | - if (isset($parameters['VALUE'])) { |
|
85 | - $valueType = $parameters['VALUE']->getValue(); |
|
86 | - unset($parameters['VALUE']); |
|
87 | - } |
|
88 | - if (!$valueType) { |
|
89 | - $valueType = $property->getValueType(); |
|
90 | - } |
|
91 | - $newProperty = $output->createProperty( |
|
92 | - $property->name, |
|
93 | - $property->getParts(), |
|
94 | - [], // parameters will get added a bit later. |
|
95 | - $valueType |
|
96 | - ); |
|
97 | - |
|
98 | - |
|
99 | - if ($targetVersion === Document::VCARD30) { |
|
100 | - |
|
101 | - if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { |
|
102 | - |
|
103 | - $newProperty = $this->convertUriToBinary($output, $newProperty); |
|
104 | - |
|
105 | - } elseif ($property instanceof Property\VCard\DateAndOrTime) { |
|
106 | - |
|
107 | - // In vCard 4, the birth year may be optional. This is not the |
|
108 | - // case for vCard 3. Apple has a workaround for this that |
|
109 | - // allows applications that support Apple's extension still |
|
110 | - // omit birthyears in vCard 3, but applications that do not |
|
111 | - // support this, will just use a random birthyear. We're |
|
112 | - // choosing 1604 for the birthyear, because that's what apple |
|
113 | - // uses. |
|
114 | - $parts = DateTimeParser::parseVCardDateTime($property->getValue()); |
|
115 | - if (is_null($parts['year'])) { |
|
116 | - $newValue = '1604-' . $parts['month'] . '-' . $parts['date']; |
|
117 | - $newProperty->setValue($newValue); |
|
118 | - $newProperty['X-APPLE-OMIT-YEAR'] = '1604'; |
|
119 | - } |
|
120 | - |
|
121 | - if ($newProperty->name == 'ANNIVERSARY') { |
|
122 | - // Microsoft non-standard anniversary |
|
123 | - $newProperty->name = 'X-ANNIVERSARY'; |
|
124 | - |
|
125 | - // We also need to add a new apple property for the same |
|
126 | - // purpose. This apple property needs a 'label' in the same |
|
127 | - // group, so we first need to find a groupname that doesn't |
|
128 | - // exist yet. |
|
129 | - $x = 1; |
|
130 | - while ($output->select('ITEM' . $x . '.')) { |
|
131 | - $x++; |
|
132 | - } |
|
133 | - $output->add('ITEM' . $x . '.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']); |
|
134 | - $output->add('ITEM' . $x . '.X-ABLABEL', '_$!<Anniversary>!$_'); |
|
135 | - } |
|
136 | - |
|
137 | - } elseif ($property->name === 'KIND') { |
|
138 | - |
|
139 | - switch (strtolower($property->getValue())) { |
|
140 | - case 'org' : |
|
141 | - // vCard 3.0 does not have an equivalent to KIND:ORG, |
|
142 | - // but apple has an extension that means the same |
|
143 | - // thing. |
|
144 | - $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); |
|
145 | - break; |
|
146 | - |
|
147 | - case 'individual' : |
|
148 | - // Individual is implicit, so we skip it. |
|
149 | - return; |
|
150 | - |
|
151 | - case 'group' : |
|
152 | - // OS X addressbook property |
|
153 | - $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); |
|
154 | - break; |
|
155 | - } |
|
156 | - |
|
157 | - |
|
158 | - } |
|
159 | - |
|
160 | - } elseif ($targetVersion === Document::VCARD40) { |
|
161 | - |
|
162 | - // These properties were removed in vCard 4.0 |
|
163 | - if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { |
|
164 | - return; |
|
165 | - } |
|
166 | - |
|
167 | - if ($property instanceof Property\Binary) { |
|
168 | - |
|
169 | - $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); |
|
170 | - |
|
171 | - } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { |
|
172 | - |
|
173 | - // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', |
|
174 | - // then we're stripping the year from the vcard 4 value. |
|
175 | - $parts = DateTimeParser::parseVCardDateTime($property->getValue()); |
|
176 | - if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) { |
|
177 | - $newValue = '--' . $parts['month'] . '-' . $parts['date']; |
|
178 | - $newProperty->setValue($newValue); |
|
179 | - } |
|
180 | - |
|
181 | - // Regardless if the year matched or not, we do need to strip |
|
182 | - // X-APPLE-OMIT-YEAR. |
|
183 | - unset($parameters['X-APPLE-OMIT-YEAR']); |
|
184 | - |
|
185 | - } |
|
186 | - switch ($property->name) { |
|
187 | - case 'X-ABSHOWAS' : |
|
188 | - if (strtoupper($property->getValue()) === 'COMPANY') { |
|
189 | - $newProperty = $output->createProperty('KIND', 'ORG'); |
|
190 | - } |
|
191 | - break; |
|
192 | - case 'X-ADDRESSBOOKSERVER-KIND' : |
|
193 | - if (strtoupper($property->getValue()) === 'GROUP') { |
|
194 | - $newProperty = $output->createProperty('KIND', 'GROUP'); |
|
195 | - } |
|
196 | - break; |
|
197 | - case 'X-ANNIVERSARY' : |
|
198 | - $newProperty->name = 'ANNIVERSARY'; |
|
199 | - // If we already have an anniversary property with the same |
|
200 | - // value, ignore. |
|
201 | - foreach ($output->select('ANNIVERSARY') as $anniversary) { |
|
202 | - if ($anniversary->getValue() === $newProperty->getValue()) { |
|
203 | - return; |
|
204 | - } |
|
205 | - } |
|
206 | - break; |
|
207 | - case 'X-ABDATE' : |
|
208 | - // Find out what the label was, if it exists. |
|
209 | - if (!$property->group) { |
|
210 | - break; |
|
211 | - } |
|
212 | - $label = $input->{$property->group . '.X-ABLABEL'}; |
|
213 | - |
|
214 | - // We only support converting anniversaries. |
|
215 | - if (!$label || $label->getValue() !== '_$!<Anniversary>!$_') { |
|
216 | - break; |
|
217 | - } |
|
218 | - |
|
219 | - // If we already have an anniversary property with the same |
|
220 | - // value, ignore. |
|
221 | - foreach ($output->select('ANNIVERSARY') as $anniversary) { |
|
222 | - if ($anniversary->getValue() === $newProperty->getValue()) { |
|
223 | - return; |
|
224 | - } |
|
225 | - } |
|
226 | - $newProperty->name = 'ANNIVERSARY'; |
|
227 | - break; |
|
228 | - // Apple's per-property label system. |
|
229 | - case 'X-ABLABEL' : |
|
230 | - if ($newProperty->getValue() === '_$!<Anniversary>!$_') { |
|
231 | - // We can safely remove these, as they are converted to |
|
232 | - // ANNIVERSARY properties. |
|
233 | - return; |
|
234 | - } |
|
235 | - break; |
|
236 | - |
|
237 | - } |
|
238 | - |
|
239 | - } |
|
240 | - |
|
241 | - // set property group |
|
242 | - $newProperty->group = $property->group; |
|
243 | - |
|
244 | - if ($targetVersion === Document::VCARD40) { |
|
245 | - $this->convertParameters40($newProperty, $parameters); |
|
246 | - } else { |
|
247 | - $this->convertParameters30($newProperty, $parameters); |
|
248 | - } |
|
249 | - |
|
250 | - // Lastly, we need to see if there's a need for a VALUE parameter. |
|
251 | - // |
|
252 | - // We can do that by instantating a empty property with that name, and |
|
253 | - // seeing if the default valueType is identical to the current one. |
|
254 | - $tempProperty = $output->createProperty($newProperty->name); |
|
255 | - if ($tempProperty->getValueType() !== $newProperty->getValueType()) { |
|
256 | - $newProperty['VALUE'] = $newProperty->getValueType(); |
|
257 | - } |
|
258 | - |
|
259 | - $output->add($newProperty); |
|
260 | - |
|
261 | - |
|
262 | - } |
|
263 | - |
|
264 | - /** |
|
265 | - * Converts a BINARY property to a URI property. |
|
266 | - * |
|
267 | - * vCard 4.0 no longer supports BINARY properties. |
|
268 | - * |
|
269 | - * @param Component\VCard $output |
|
270 | - * @param Property\Uri $property The input property. |
|
271 | - * @param $parameters List of parameters that will eventually be added to |
|
272 | - * the new property. |
|
273 | - * |
|
274 | - * @return Property\Uri |
|
275 | - */ |
|
276 | - protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) { |
|
277 | - |
|
278 | - $value = $newProperty->getValue(); |
|
279 | - $newProperty = $output->createProperty( |
|
280 | - $newProperty->name, |
|
281 | - null, // no value |
|
282 | - [], // no parameters yet |
|
283 | - 'URI' // Forcing the BINARY type |
|
284 | - ); |
|
285 | - |
|
286 | - $mimeType = 'application/octet-stream'; |
|
287 | - |
|
288 | - // See if we can find a better mimetype. |
|
289 | - if (isset($parameters['TYPE'])) { |
|
290 | - |
|
291 | - $newTypes = []; |
|
292 | - foreach ($parameters['TYPE']->getParts() as $typePart) { |
|
293 | - if (in_array( |
|
294 | - strtoupper($typePart), |
|
295 | - ['JPEG', 'PNG', 'GIF'] |
|
296 | - )) { |
|
297 | - $mimeType = 'image/' . strtolower($typePart); |
|
298 | - } else { |
|
299 | - $newTypes[] = $typePart; |
|
300 | - } |
|
301 | - } |
|
302 | - |
|
303 | - // If there were any parameters we're not converting to a |
|
304 | - // mime-type, we need to keep them. |
|
305 | - if ($newTypes) { |
|
306 | - $parameters['TYPE']->setParts($newTypes); |
|
307 | - } else { |
|
308 | - unset($parameters['TYPE']); |
|
309 | - } |
|
310 | - |
|
311 | - } |
|
312 | - |
|
313 | - $newProperty->setValue('data:' . $mimeType . ';base64,' . base64_encode($value)); |
|
314 | - return $newProperty; |
|
315 | - |
|
316 | - } |
|
317 | - |
|
318 | - /** |
|
319 | - * Converts a URI property to a BINARY property. |
|
320 | - * |
|
321 | - * In vCard 4.0 attachments are encoded as data: uri. Even though these may |
|
322 | - * be valid in vCard 3.0 as well, we should convert those to BINARY if |
|
323 | - * possible, to improve compatibility. |
|
324 | - * |
|
325 | - * @param Component\VCard $output |
|
326 | - * @param Property\Uri $property The input property. |
|
327 | - * |
|
328 | - * @return Property\Binary|null |
|
329 | - */ |
|
330 | - protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) { |
|
331 | - |
|
332 | - $value = $newProperty->getValue(); |
|
333 | - |
|
334 | - // Only converting data: uris |
|
335 | - if (substr($value, 0, 5) !== 'data:') { |
|
336 | - return $newProperty; |
|
337 | - } |
|
338 | - |
|
339 | - $newProperty = $output->createProperty( |
|
340 | - $newProperty->name, |
|
341 | - null, // no value |
|
342 | - [], // no parameters yet |
|
343 | - 'BINARY' |
|
344 | - ); |
|
345 | - |
|
346 | - $mimeType = substr($value, 5, strpos($value, ',') - 5); |
|
347 | - if (strpos($mimeType, ';')) { |
|
348 | - $mimeType = substr($mimeType, 0, strpos($mimeType, ';')); |
|
349 | - $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); |
|
350 | - } else { |
|
351 | - $newProperty->setValue(substr($value, strpos($value, ',') + 1)); |
|
352 | - } |
|
353 | - unset($value); |
|
354 | - |
|
355 | - $newProperty['ENCODING'] = 'b'; |
|
356 | - switch ($mimeType) { |
|
357 | - |
|
358 | - case 'image/jpeg' : |
|
359 | - $newProperty['TYPE'] = 'JPEG'; |
|
360 | - break; |
|
361 | - case 'image/png' : |
|
362 | - $newProperty['TYPE'] = 'PNG'; |
|
363 | - break; |
|
364 | - case 'image/gif' : |
|
365 | - $newProperty['TYPE'] = 'GIF'; |
|
366 | - break; |
|
367 | - |
|
368 | - } |
|
369 | - |
|
370 | - |
|
371 | - return $newProperty; |
|
372 | - |
|
373 | - } |
|
374 | - |
|
375 | - /** |
|
376 | - * Adds parameters to a new property for vCard 4.0. |
|
377 | - * |
|
378 | - * @param Property $newProperty |
|
379 | - * @param array $parameters |
|
380 | - * |
|
381 | - * @return void |
|
382 | - */ |
|
383 | - protected function convertParameters40(Property $newProperty, array $parameters) { |
|
384 | - |
|
385 | - // Adding all parameters. |
|
386 | - foreach ($parameters as $param) { |
|
387 | - |
|
388 | - // vCard 2.1 allowed parameters with no name |
|
389 | - if ($param->noName) $param->noName = false; |
|
390 | - |
|
391 | - switch ($param->name) { |
|
392 | - |
|
393 | - // We need to see if there's any TYPE=PREF, because in vCard 4 |
|
394 | - // that's now PREF=1. |
|
395 | - case 'TYPE' : |
|
396 | - foreach ($param->getParts() as $paramPart) { |
|
397 | - |
|
398 | - if (strtoupper($paramPart) === 'PREF') { |
|
399 | - $newProperty->add('PREF', '1'); |
|
400 | - } else { |
|
401 | - $newProperty->add($param->name, $paramPart); |
|
402 | - } |
|
403 | - |
|
404 | - } |
|
405 | - break; |
|
406 | - // These no longer exist in vCard 4 |
|
407 | - case 'ENCODING' : |
|
408 | - case 'CHARSET' : |
|
409 | - break; |
|
410 | - |
|
411 | - default : |
|
412 | - $newProperty->add($param->name, $param->getParts()); |
|
413 | - break; |
|
414 | - |
|
415 | - } |
|
416 | - |
|
417 | - } |
|
418 | - |
|
419 | - } |
|
420 | - |
|
421 | - /** |
|
422 | - * Adds parameters to a new property for vCard 3.0. |
|
423 | - * |
|
424 | - * @param Property $newProperty |
|
425 | - * @param array $parameters |
|
426 | - * |
|
427 | - * @return void |
|
428 | - */ |
|
429 | - protected function convertParameters30(Property $newProperty, array $parameters) { |
|
430 | - |
|
431 | - // Adding all parameters. |
|
432 | - foreach ($parameters as $param) { |
|
433 | - |
|
434 | - // vCard 2.1 allowed parameters with no name |
|
435 | - if ($param->noName) $param->noName = false; |
|
436 | - |
|
437 | - switch ($param->name) { |
|
438 | - |
|
439 | - case 'ENCODING' : |
|
440 | - // This value only existed in vCard 2.1, and should be |
|
441 | - // removed for anything else. |
|
442 | - if (strtoupper($param->getValue()) !== 'QUOTED-PRINTABLE') { |
|
443 | - $newProperty->add($param->name, $param->getParts()); |
|
444 | - } |
|
445 | - break; |
|
446 | - |
|
447 | - /* |
|
14 | + /** |
|
15 | + * Converts a vCard object to a new version. |
|
16 | + * |
|
17 | + * targetVersion must be one of: |
|
18 | + * Document::VCARD21 |
|
19 | + * Document::VCARD30 |
|
20 | + * Document::VCARD40 |
|
21 | + * |
|
22 | + * Currently only 3.0 and 4.0 as input and output versions. |
|
23 | + * |
|
24 | + * 2.1 has some minor support for the input version, it's incomplete at the |
|
25 | + * moment though. |
|
26 | + * |
|
27 | + * If input and output version are identical, a clone is returned. |
|
28 | + * |
|
29 | + * @param Component\VCard $input |
|
30 | + * @param int $targetVersion |
|
31 | + */ |
|
32 | + public function convert(Component\VCard $input, $targetVersion) { |
|
33 | + |
|
34 | + $inputVersion = $input->getDocumentType(); |
|
35 | + if ($inputVersion === $targetVersion) { |
|
36 | + return clone $input; |
|
37 | + } |
|
38 | + |
|
39 | + if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { |
|
40 | + throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); |
|
41 | + } |
|
42 | + if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { |
|
43 | + throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); |
|
44 | + } |
|
45 | + |
|
46 | + $newVersion = $targetVersion === Document::VCARD40 ? '4.0' : '3.0'; |
|
47 | + |
|
48 | + $output = new Component\VCard([ |
|
49 | + 'VERSION' => $newVersion, |
|
50 | + ]); |
|
51 | + |
|
52 | + // We might have generated a default UID. Remove it! |
|
53 | + unset($output->UID); |
|
54 | + |
|
55 | + foreach ($input->children() as $property) { |
|
56 | + |
|
57 | + $this->convertProperty($input, $output, $property, $targetVersion); |
|
58 | + |
|
59 | + } |
|
60 | + |
|
61 | + return $output; |
|
62 | + |
|
63 | + } |
|
64 | + |
|
65 | + /** |
|
66 | + * Handles conversion of a single property. |
|
67 | + * |
|
68 | + * @param Component\VCard $input |
|
69 | + * @param Component\VCard $output |
|
70 | + * @param Property $property |
|
71 | + * @param int $targetVersion |
|
72 | + * |
|
73 | + * @return void |
|
74 | + */ |
|
75 | + protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) { |
|
76 | + |
|
77 | + // Skipping these, those are automatically added. |
|
78 | + if (in_array($property->name, ['VERSION', 'PRODID'])) { |
|
79 | + return; |
|
80 | + } |
|
81 | + |
|
82 | + $parameters = $property->parameters(); |
|
83 | + $valueType = null; |
|
84 | + if (isset($parameters['VALUE'])) { |
|
85 | + $valueType = $parameters['VALUE']->getValue(); |
|
86 | + unset($parameters['VALUE']); |
|
87 | + } |
|
88 | + if (!$valueType) { |
|
89 | + $valueType = $property->getValueType(); |
|
90 | + } |
|
91 | + $newProperty = $output->createProperty( |
|
92 | + $property->name, |
|
93 | + $property->getParts(), |
|
94 | + [], // parameters will get added a bit later. |
|
95 | + $valueType |
|
96 | + ); |
|
97 | + |
|
98 | + |
|
99 | + if ($targetVersion === Document::VCARD30) { |
|
100 | + |
|
101 | + if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { |
|
102 | + |
|
103 | + $newProperty = $this->convertUriToBinary($output, $newProperty); |
|
104 | + |
|
105 | + } elseif ($property instanceof Property\VCard\DateAndOrTime) { |
|
106 | + |
|
107 | + // In vCard 4, the birth year may be optional. This is not the |
|
108 | + // case for vCard 3. Apple has a workaround for this that |
|
109 | + // allows applications that support Apple's extension still |
|
110 | + // omit birthyears in vCard 3, but applications that do not |
|
111 | + // support this, will just use a random birthyear. We're |
|
112 | + // choosing 1604 for the birthyear, because that's what apple |
|
113 | + // uses. |
|
114 | + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); |
|
115 | + if (is_null($parts['year'])) { |
|
116 | + $newValue = '1604-' . $parts['month'] . '-' . $parts['date']; |
|
117 | + $newProperty->setValue($newValue); |
|
118 | + $newProperty['X-APPLE-OMIT-YEAR'] = '1604'; |
|
119 | + } |
|
120 | + |
|
121 | + if ($newProperty->name == 'ANNIVERSARY') { |
|
122 | + // Microsoft non-standard anniversary |
|
123 | + $newProperty->name = 'X-ANNIVERSARY'; |
|
124 | + |
|
125 | + // We also need to add a new apple property for the same |
|
126 | + // purpose. This apple property needs a 'label' in the same |
|
127 | + // group, so we first need to find a groupname that doesn't |
|
128 | + // exist yet. |
|
129 | + $x = 1; |
|
130 | + while ($output->select('ITEM' . $x . '.')) { |
|
131 | + $x++; |
|
132 | + } |
|
133 | + $output->add('ITEM' . $x . '.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']); |
|
134 | + $output->add('ITEM' . $x . '.X-ABLABEL', '_$!<Anniversary>!$_'); |
|
135 | + } |
|
136 | + |
|
137 | + } elseif ($property->name === 'KIND') { |
|
138 | + |
|
139 | + switch (strtolower($property->getValue())) { |
|
140 | + case 'org' : |
|
141 | + // vCard 3.0 does not have an equivalent to KIND:ORG, |
|
142 | + // but apple has an extension that means the same |
|
143 | + // thing. |
|
144 | + $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); |
|
145 | + break; |
|
146 | + |
|
147 | + case 'individual' : |
|
148 | + // Individual is implicit, so we skip it. |
|
149 | + return; |
|
150 | + |
|
151 | + case 'group' : |
|
152 | + // OS X addressbook property |
|
153 | + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); |
|
154 | + break; |
|
155 | + } |
|
156 | + |
|
157 | + |
|
158 | + } |
|
159 | + |
|
160 | + } elseif ($targetVersion === Document::VCARD40) { |
|
161 | + |
|
162 | + // These properties were removed in vCard 4.0 |
|
163 | + if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { |
|
164 | + return; |
|
165 | + } |
|
166 | + |
|
167 | + if ($property instanceof Property\Binary) { |
|
168 | + |
|
169 | + $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); |
|
170 | + |
|
171 | + } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { |
|
172 | + |
|
173 | + // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', |
|
174 | + // then we're stripping the year from the vcard 4 value. |
|
175 | + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); |
|
176 | + if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) { |
|
177 | + $newValue = '--' . $parts['month'] . '-' . $parts['date']; |
|
178 | + $newProperty->setValue($newValue); |
|
179 | + } |
|
180 | + |
|
181 | + // Regardless if the year matched or not, we do need to strip |
|
182 | + // X-APPLE-OMIT-YEAR. |
|
183 | + unset($parameters['X-APPLE-OMIT-YEAR']); |
|
184 | + |
|
185 | + } |
|
186 | + switch ($property->name) { |
|
187 | + case 'X-ABSHOWAS' : |
|
188 | + if (strtoupper($property->getValue()) === 'COMPANY') { |
|
189 | + $newProperty = $output->createProperty('KIND', 'ORG'); |
|
190 | + } |
|
191 | + break; |
|
192 | + case 'X-ADDRESSBOOKSERVER-KIND' : |
|
193 | + if (strtoupper($property->getValue()) === 'GROUP') { |
|
194 | + $newProperty = $output->createProperty('KIND', 'GROUP'); |
|
195 | + } |
|
196 | + break; |
|
197 | + case 'X-ANNIVERSARY' : |
|
198 | + $newProperty->name = 'ANNIVERSARY'; |
|
199 | + // If we already have an anniversary property with the same |
|
200 | + // value, ignore. |
|
201 | + foreach ($output->select('ANNIVERSARY') as $anniversary) { |
|
202 | + if ($anniversary->getValue() === $newProperty->getValue()) { |
|
203 | + return; |
|
204 | + } |
|
205 | + } |
|
206 | + break; |
|
207 | + case 'X-ABDATE' : |
|
208 | + // Find out what the label was, if it exists. |
|
209 | + if (!$property->group) { |
|
210 | + break; |
|
211 | + } |
|
212 | + $label = $input->{$property->group . '.X-ABLABEL'}; |
|
213 | + |
|
214 | + // We only support converting anniversaries. |
|
215 | + if (!$label || $label->getValue() !== '_$!<Anniversary>!$_') { |
|
216 | + break; |
|
217 | + } |
|
218 | + |
|
219 | + // If we already have an anniversary property with the same |
|
220 | + // value, ignore. |
|
221 | + foreach ($output->select('ANNIVERSARY') as $anniversary) { |
|
222 | + if ($anniversary->getValue() === $newProperty->getValue()) { |
|
223 | + return; |
|
224 | + } |
|
225 | + } |
|
226 | + $newProperty->name = 'ANNIVERSARY'; |
|
227 | + break; |
|
228 | + // Apple's per-property label system. |
|
229 | + case 'X-ABLABEL' : |
|
230 | + if ($newProperty->getValue() === '_$!<Anniversary>!$_') { |
|
231 | + // We can safely remove these, as they are converted to |
|
232 | + // ANNIVERSARY properties. |
|
233 | + return; |
|
234 | + } |
|
235 | + break; |
|
236 | + |
|
237 | + } |
|
238 | + |
|
239 | + } |
|
240 | + |
|
241 | + // set property group |
|
242 | + $newProperty->group = $property->group; |
|
243 | + |
|
244 | + if ($targetVersion === Document::VCARD40) { |
|
245 | + $this->convertParameters40($newProperty, $parameters); |
|
246 | + } else { |
|
247 | + $this->convertParameters30($newProperty, $parameters); |
|
248 | + } |
|
249 | + |
|
250 | + // Lastly, we need to see if there's a need for a VALUE parameter. |
|
251 | + // |
|
252 | + // We can do that by instantating a empty property with that name, and |
|
253 | + // seeing if the default valueType is identical to the current one. |
|
254 | + $tempProperty = $output->createProperty($newProperty->name); |
|
255 | + if ($tempProperty->getValueType() !== $newProperty->getValueType()) { |
|
256 | + $newProperty['VALUE'] = $newProperty->getValueType(); |
|
257 | + } |
|
258 | + |
|
259 | + $output->add($newProperty); |
|
260 | + |
|
261 | + |
|
262 | + } |
|
263 | + |
|
264 | + /** |
|
265 | + * Converts a BINARY property to a URI property. |
|
266 | + * |
|
267 | + * vCard 4.0 no longer supports BINARY properties. |
|
268 | + * |
|
269 | + * @param Component\VCard $output |
|
270 | + * @param Property\Uri $property The input property. |
|
271 | + * @param $parameters List of parameters that will eventually be added to |
|
272 | + * the new property. |
|
273 | + * |
|
274 | + * @return Property\Uri |
|
275 | + */ |
|
276 | + protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) { |
|
277 | + |
|
278 | + $value = $newProperty->getValue(); |
|
279 | + $newProperty = $output->createProperty( |
|
280 | + $newProperty->name, |
|
281 | + null, // no value |
|
282 | + [], // no parameters yet |
|
283 | + 'URI' // Forcing the BINARY type |
|
284 | + ); |
|
285 | + |
|
286 | + $mimeType = 'application/octet-stream'; |
|
287 | + |
|
288 | + // See if we can find a better mimetype. |
|
289 | + if (isset($parameters['TYPE'])) { |
|
290 | + |
|
291 | + $newTypes = []; |
|
292 | + foreach ($parameters['TYPE']->getParts() as $typePart) { |
|
293 | + if (in_array( |
|
294 | + strtoupper($typePart), |
|
295 | + ['JPEG', 'PNG', 'GIF'] |
|
296 | + )) { |
|
297 | + $mimeType = 'image/' . strtolower($typePart); |
|
298 | + } else { |
|
299 | + $newTypes[] = $typePart; |
|
300 | + } |
|
301 | + } |
|
302 | + |
|
303 | + // If there were any parameters we're not converting to a |
|
304 | + // mime-type, we need to keep them. |
|
305 | + if ($newTypes) { |
|
306 | + $parameters['TYPE']->setParts($newTypes); |
|
307 | + } else { |
|
308 | + unset($parameters['TYPE']); |
|
309 | + } |
|
310 | + |
|
311 | + } |
|
312 | + |
|
313 | + $newProperty->setValue('data:' . $mimeType . ';base64,' . base64_encode($value)); |
|
314 | + return $newProperty; |
|
315 | + |
|
316 | + } |
|
317 | + |
|
318 | + /** |
|
319 | + * Converts a URI property to a BINARY property. |
|
320 | + * |
|
321 | + * In vCard 4.0 attachments are encoded as data: uri. Even though these may |
|
322 | + * be valid in vCard 3.0 as well, we should convert those to BINARY if |
|
323 | + * possible, to improve compatibility. |
|
324 | + * |
|
325 | + * @param Component\VCard $output |
|
326 | + * @param Property\Uri $property The input property. |
|
327 | + * |
|
328 | + * @return Property\Binary|null |
|
329 | + */ |
|
330 | + protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) { |
|
331 | + |
|
332 | + $value = $newProperty->getValue(); |
|
333 | + |
|
334 | + // Only converting data: uris |
|
335 | + if (substr($value, 0, 5) !== 'data:') { |
|
336 | + return $newProperty; |
|
337 | + } |
|
338 | + |
|
339 | + $newProperty = $output->createProperty( |
|
340 | + $newProperty->name, |
|
341 | + null, // no value |
|
342 | + [], // no parameters yet |
|
343 | + 'BINARY' |
|
344 | + ); |
|
345 | + |
|
346 | + $mimeType = substr($value, 5, strpos($value, ',') - 5); |
|
347 | + if (strpos($mimeType, ';')) { |
|
348 | + $mimeType = substr($mimeType, 0, strpos($mimeType, ';')); |
|
349 | + $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); |
|
350 | + } else { |
|
351 | + $newProperty->setValue(substr($value, strpos($value, ',') + 1)); |
|
352 | + } |
|
353 | + unset($value); |
|
354 | + |
|
355 | + $newProperty['ENCODING'] = 'b'; |
|
356 | + switch ($mimeType) { |
|
357 | + |
|
358 | + case 'image/jpeg' : |
|
359 | + $newProperty['TYPE'] = 'JPEG'; |
|
360 | + break; |
|
361 | + case 'image/png' : |
|
362 | + $newProperty['TYPE'] = 'PNG'; |
|
363 | + break; |
|
364 | + case 'image/gif' : |
|
365 | + $newProperty['TYPE'] = 'GIF'; |
|
366 | + break; |
|
367 | + |
|
368 | + } |
|
369 | + |
|
370 | + |
|
371 | + return $newProperty; |
|
372 | + |
|
373 | + } |
|
374 | + |
|
375 | + /** |
|
376 | + * Adds parameters to a new property for vCard 4.0. |
|
377 | + * |
|
378 | + * @param Property $newProperty |
|
379 | + * @param array $parameters |
|
380 | + * |
|
381 | + * @return void |
|
382 | + */ |
|
383 | + protected function convertParameters40(Property $newProperty, array $parameters) { |
|
384 | + |
|
385 | + // Adding all parameters. |
|
386 | + foreach ($parameters as $param) { |
|
387 | + |
|
388 | + // vCard 2.1 allowed parameters with no name |
|
389 | + if ($param->noName) $param->noName = false; |
|
390 | + |
|
391 | + switch ($param->name) { |
|
392 | + |
|
393 | + // We need to see if there's any TYPE=PREF, because in vCard 4 |
|
394 | + // that's now PREF=1. |
|
395 | + case 'TYPE' : |
|
396 | + foreach ($param->getParts() as $paramPart) { |
|
397 | + |
|
398 | + if (strtoupper($paramPart) === 'PREF') { |
|
399 | + $newProperty->add('PREF', '1'); |
|
400 | + } else { |
|
401 | + $newProperty->add($param->name, $paramPart); |
|
402 | + } |
|
403 | + |
|
404 | + } |
|
405 | + break; |
|
406 | + // These no longer exist in vCard 4 |
|
407 | + case 'ENCODING' : |
|
408 | + case 'CHARSET' : |
|
409 | + break; |
|
410 | + |
|
411 | + default : |
|
412 | + $newProperty->add($param->name, $param->getParts()); |
|
413 | + break; |
|
414 | + |
|
415 | + } |
|
416 | + |
|
417 | + } |
|
418 | + |
|
419 | + } |
|
420 | + |
|
421 | + /** |
|
422 | + * Adds parameters to a new property for vCard 3.0. |
|
423 | + * |
|
424 | + * @param Property $newProperty |
|
425 | + * @param array $parameters |
|
426 | + * |
|
427 | + * @return void |
|
428 | + */ |
|
429 | + protected function convertParameters30(Property $newProperty, array $parameters) { |
|
430 | + |
|
431 | + // Adding all parameters. |
|
432 | + foreach ($parameters as $param) { |
|
433 | + |
|
434 | + // vCard 2.1 allowed parameters with no name |
|
435 | + if ($param->noName) $param->noName = false; |
|
436 | + |
|
437 | + switch ($param->name) { |
|
438 | + |
|
439 | + case 'ENCODING' : |
|
440 | + // This value only existed in vCard 2.1, and should be |
|
441 | + // removed for anything else. |
|
442 | + if (strtoupper($param->getValue()) !== 'QUOTED-PRINTABLE') { |
|
443 | + $newProperty->add($param->name, $param->getParts()); |
|
444 | + } |
|
445 | + break; |
|
446 | + |
|
447 | + /* |
|
448 | 448 | * Converting PREF=1 to TYPE=PREF. |
449 | 449 | * |
450 | 450 | * Any other PREF numbers we'll drop. |
451 | 451 | */ |
452 | - case 'PREF' : |
|
453 | - if ($param->getValue() == '1') { |
|
454 | - $newProperty->add('TYPE', 'PREF'); |
|
455 | - } |
|
456 | - break; |
|
452 | + case 'PREF' : |
|
453 | + if ($param->getValue() == '1') { |
|
454 | + $newProperty->add('TYPE', 'PREF'); |
|
455 | + } |
|
456 | + break; |
|
457 | 457 | |
458 | - default : |
|
459 | - $newProperty->add($param->name, $param->getParts()); |
|
460 | - break; |
|
458 | + default : |
|
459 | + $newProperty->add($param->name, $param->getParts()); |
|
460 | + break; |
|
461 | 461 | |
462 | - } |
|
462 | + } |
|
463 | 463 | |
464 | - } |
|
464 | + } |
|
465 | 465 | |
466 | - } |
|
466 | + } |
|
467 | 467 | } |
@@ -18,125 +18,125 @@ |
||
18 | 18 | */ |
19 | 19 | class VAlarm extends VObject\Component { |
20 | 20 | |
21 | - /** |
|
22 | - * Returns a DateTime object when this alarm is going to trigger. |
|
23 | - * |
|
24 | - * This ignores repeated alarm, only the first trigger is returned. |
|
25 | - * |
|
26 | - * @return DateTimeImmutable |
|
27 | - */ |
|
28 | - public function getEffectiveTriggerTime() { |
|
29 | - |
|
30 | - $trigger = $this->TRIGGER; |
|
31 | - if (!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { |
|
32 | - $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); |
|
33 | - $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; |
|
34 | - |
|
35 | - $parentComponent = $this->parent; |
|
36 | - if ($related === 'START') { |
|
37 | - |
|
38 | - if ($parentComponent->name === 'VTODO') { |
|
39 | - $propName = 'DUE'; |
|
40 | - } else { |
|
41 | - $propName = 'DTSTART'; |
|
42 | - } |
|
43 | - |
|
44 | - $effectiveTrigger = $parentComponent->$propName->getDateTime(); |
|
45 | - $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
46 | - } else { |
|
47 | - if ($parentComponent->name === 'VTODO') { |
|
48 | - $endProp = 'DUE'; |
|
49 | - } elseif ($parentComponent->name === 'VEVENT') { |
|
50 | - $endProp = 'DTEND'; |
|
51 | - } else { |
|
52 | - throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); |
|
53 | - } |
|
54 | - |
|
55 | - if (isset($parentComponent->$endProp)) { |
|
56 | - $effectiveTrigger = $parentComponent->$endProp->getDateTime(); |
|
57 | - $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
58 | - } elseif (isset($parentComponent->DURATION)) { |
|
59 | - $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); |
|
60 | - $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); |
|
61 | - $effectiveTrigger = $effectiveTrigger->add($duration); |
|
62 | - $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
63 | - } else { |
|
64 | - $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); |
|
65 | - $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
66 | - } |
|
67 | - } |
|
68 | - } else { |
|
69 | - $effectiveTrigger = $trigger->getDateTime(); |
|
70 | - } |
|
71 | - return $effectiveTrigger; |
|
72 | - |
|
73 | - } |
|
74 | - |
|
75 | - /** |
|
76 | - * Returns true or false depending on if the event falls in the specified |
|
77 | - * time-range. This is used for filtering purposes. |
|
78 | - * |
|
79 | - * The rules used to determine if an event falls within the specified |
|
80 | - * time-range is based on the CalDAV specification. |
|
81 | - * |
|
82 | - * @param DateTime $start |
|
83 | - * @param DateTime $end |
|
84 | - * |
|
85 | - * @return bool |
|
86 | - */ |
|
87 | - public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { |
|
88 | - |
|
89 | - $effectiveTrigger = $this->getEffectiveTriggerTime(); |
|
90 | - |
|
91 | - if (isset($this->DURATION)) { |
|
92 | - $duration = VObject\DateTimeParser::parseDuration($this->DURATION); |
|
93 | - $repeat = (string)$this->REPEAT; |
|
94 | - if (!$repeat) { |
|
95 | - $repeat = 1; |
|
96 | - } |
|
97 | - |
|
98 | - $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat); |
|
99 | - |
|
100 | - foreach ($period as $occurrence) { |
|
101 | - |
|
102 | - if ($start <= $occurrence && $end > $occurrence) { |
|
103 | - return true; |
|
104 | - } |
|
105 | - } |
|
106 | - return false; |
|
107 | - } else { |
|
108 | - return ($start <= $effectiveTrigger && $end > $effectiveTrigger); |
|
109 | - } |
|
110 | - |
|
111 | - } |
|
112 | - |
|
113 | - /** |
|
114 | - * A simple list of validation rules. |
|
115 | - * |
|
116 | - * This is simply a list of properties, and how many times they either |
|
117 | - * must or must not appear. |
|
118 | - * |
|
119 | - * Possible values per property: |
|
120 | - * * 0 - Must not appear. |
|
121 | - * * 1 - Must appear exactly once. |
|
122 | - * * + - Must appear at least once. |
|
123 | - * * * - Can appear any number of times. |
|
124 | - * * ? - May appear, but not more than once. |
|
125 | - * |
|
126 | - * @var array |
|
127 | - */ |
|
128 | - public function getValidationRules() { |
|
129 | - |
|
130 | - return [ |
|
131 | - 'ACTION' => 1, |
|
132 | - 'TRIGGER' => 1, |
|
133 | - |
|
134 | - 'DURATION' => '?', |
|
135 | - 'REPEAT' => '?', |
|
136 | - |
|
137 | - 'ATTACH' => '?', |
|
138 | - ]; |
|
139 | - |
|
140 | - } |
|
21 | + /** |
|
22 | + * Returns a DateTime object when this alarm is going to trigger. |
|
23 | + * |
|
24 | + * This ignores repeated alarm, only the first trigger is returned. |
|
25 | + * |
|
26 | + * @return DateTimeImmutable |
|
27 | + */ |
|
28 | + public function getEffectiveTriggerTime() { |
|
29 | + |
|
30 | + $trigger = $this->TRIGGER; |
|
31 | + if (!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { |
|
32 | + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); |
|
33 | + $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; |
|
34 | + |
|
35 | + $parentComponent = $this->parent; |
|
36 | + if ($related === 'START') { |
|
37 | + |
|
38 | + if ($parentComponent->name === 'VTODO') { |
|
39 | + $propName = 'DUE'; |
|
40 | + } else { |
|
41 | + $propName = 'DTSTART'; |
|
42 | + } |
|
43 | + |
|
44 | + $effectiveTrigger = $parentComponent->$propName->getDateTime(); |
|
45 | + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
46 | + } else { |
|
47 | + if ($parentComponent->name === 'VTODO') { |
|
48 | + $endProp = 'DUE'; |
|
49 | + } elseif ($parentComponent->name === 'VEVENT') { |
|
50 | + $endProp = 'DTEND'; |
|
51 | + } else { |
|
52 | + throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); |
|
53 | + } |
|
54 | + |
|
55 | + if (isset($parentComponent->$endProp)) { |
|
56 | + $effectiveTrigger = $parentComponent->$endProp->getDateTime(); |
|
57 | + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
58 | + } elseif (isset($parentComponent->DURATION)) { |
|
59 | + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); |
|
60 | + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); |
|
61 | + $effectiveTrigger = $effectiveTrigger->add($duration); |
|
62 | + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
63 | + } else { |
|
64 | + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); |
|
65 | + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); |
|
66 | + } |
|
67 | + } |
|
68 | + } else { |
|
69 | + $effectiveTrigger = $trigger->getDateTime(); |
|
70 | + } |
|
71 | + return $effectiveTrigger; |
|
72 | + |
|
73 | + } |
|
74 | + |
|
75 | + /** |
|
76 | + * Returns true or false depending on if the event falls in the specified |
|
77 | + * time-range. This is used for filtering purposes. |
|
78 | + * |
|
79 | + * The rules used to determine if an event falls within the specified |
|
80 | + * time-range is based on the CalDAV specification. |
|
81 | + * |
|
82 | + * @param DateTime $start |
|
83 | + * @param DateTime $end |
|
84 | + * |
|
85 | + * @return bool |
|
86 | + */ |
|
87 | + public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { |
|
88 | + |
|
89 | + $effectiveTrigger = $this->getEffectiveTriggerTime(); |
|
90 | + |
|
91 | + if (isset($this->DURATION)) { |
|
92 | + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); |
|
93 | + $repeat = (string)$this->REPEAT; |
|
94 | + if (!$repeat) { |
|
95 | + $repeat = 1; |
|
96 | + } |
|
97 | + |
|
98 | + $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat); |
|
99 | + |
|
100 | + foreach ($period as $occurrence) { |
|
101 | + |
|
102 | + if ($start <= $occurrence && $end > $occurrence) { |
|
103 | + return true; |
|
104 | + } |
|
105 | + } |
|
106 | + return false; |
|
107 | + } else { |
|
108 | + return ($start <= $effectiveTrigger && $end > $effectiveTrigger); |
|
109 | + } |
|
110 | + |
|
111 | + } |
|
112 | + |
|
113 | + /** |
|
114 | + * A simple list of validation rules. |
|
115 | + * |
|
116 | + * This is simply a list of properties, and how many times they either |
|
117 | + * must or must not appear. |
|
118 | + * |
|
119 | + * Possible values per property: |
|
120 | + * * 0 - Must not appear. |
|
121 | + * * 1 - Must appear exactly once. |
|
122 | + * * + - Must appear at least once. |
|
123 | + * * * - Can appear any number of times. |
|
124 | + * * ? - May appear, but not more than once. |
|
125 | + * |
|
126 | + * @var array |
|
127 | + */ |
|
128 | + public function getValidationRules() { |
|
129 | + |
|
130 | + return [ |
|
131 | + 'ACTION' => 1, |
|
132 | + 'TRIGGER' => 1, |
|
133 | + |
|
134 | + 'DURATION' => '?', |
|
135 | + 'REPEAT' => '?', |
|
136 | + |
|
137 | + 'ATTACH' => '?', |
|
138 | + ]; |
|
139 | + |
|
140 | + } |
|
141 | 141 | |
142 | 142 | } |