Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/calendar/parser/ics/PhutilICSWriter.php
12262 views
1
<?php
2
3
final class PhutilICSWriter extends Phobject {
4
5
public function writeICSDocument(PhutilCalendarRootNode $node) {
6
$out = array();
7
8
foreach ($node->getChildren() as $child) {
9
$out[] = $this->writeNode($child);
10
}
11
12
return implode('', $out);
13
}
14
15
private function writeNode(PhutilCalendarNode $node) {
16
if (!$this->getICSNodeType($node)) {
17
return null;
18
}
19
20
$out = array();
21
22
$out[] = $this->writeBeginNode($node);
23
$out[] = $this->writeNodeProperties($node);
24
25
if ($node instanceof PhutilCalendarContainerNode) {
26
foreach ($node->getChildren() as $child) {
27
$out[] = $this->writeNode($child);
28
}
29
}
30
31
$out[] = $this->writeEndNode($node);
32
33
return implode('', $out);
34
}
35
36
private function writeBeginNode(PhutilCalendarNode $node) {
37
$type = $this->getICSNodeType($node);
38
return $this->wrapICSLine("BEGIN:{$type}");
39
}
40
41
private function writeEndNode(PhutilCalendarNode $node) {
42
$type = $this->getICSNodeType($node);
43
return $this->wrapICSLine("END:{$type}");
44
}
45
46
private function writeNodeProperties(PhutilCalendarNode $node) {
47
$properties = $this->getNodeProperties($node);
48
49
$out = array();
50
foreach ($properties as $property) {
51
$propname = $property['name'];
52
$propvalue = $property['value'];
53
54
$propline = array();
55
$propline[] = $propname;
56
57
foreach ($property['parameters'] as $parameter) {
58
$paramname = $parameter['name'];
59
$paramvalue = $parameter['value'];
60
$propline[] = ";{$paramname}={$paramvalue}";
61
}
62
63
$propline[] = ":{$propvalue}";
64
$propline = implode('', $propline);
65
66
$out[] = $this->wrapICSLine($propline);
67
}
68
69
return implode('', $out);
70
}
71
72
private function getICSNodeType(PhutilCalendarNode $node) {
73
switch ($node->getNodeType()) {
74
case PhutilCalendarDocumentNode::NODETYPE:
75
return 'VCALENDAR';
76
case PhutilCalendarEventNode::NODETYPE:
77
return 'VEVENT';
78
default:
79
return null;
80
}
81
}
82
83
private function wrapICSLine($line) {
84
$out = array();
85
$buf = '';
86
87
// NOTE: The line may contain sequences of combining characters which are
88
// more than 80 bytes in length. If it does, we'll split them in the
89
// middle of the sequence. This is okay and generally anticipated by
90
// RFC5545, which even allows implementations to split multibyte
91
// characters. The sequence will be stitched back together properly by
92
// whatever is parsing things.
93
94
foreach (phutil_utf8v($line) as $character) {
95
// If adding this character would bring the line over 75 bytes, start
96
// a new line.
97
if (strlen($buf) + strlen($character) > 75) {
98
$out[] = $buf."\r\n";
99
$buf = ' ';
100
}
101
102
$buf .= $character;
103
}
104
105
$out[] = $buf."\r\n";
106
107
return implode('', $out);
108
}
109
110
private function getNodeProperties(PhutilCalendarNode $node) {
111
switch ($node->getNodeType()) {
112
case PhutilCalendarDocumentNode::NODETYPE:
113
return $this->getDocumentNodeProperties($node);
114
case PhutilCalendarEventNode::NODETYPE:
115
return $this->getEventNodeProperties($node);
116
default:
117
return array();
118
}
119
}
120
121
private function getDocumentNodeProperties(
122
PhutilCalendarDocumentNode $event) {
123
$properties = array();
124
125
$properties[] = $this->newTextProperty(
126
'VERSION',
127
'2.0');
128
129
$properties[] = $this->newTextProperty(
130
'PRODID',
131
self::getICSPRODID());
132
133
return $properties;
134
}
135
136
public static function getICSPRODID() {
137
return '-//Phacility//Phabricator//EN';
138
}
139
140
private function getEventNodeProperties(PhutilCalendarEventNode $event) {
141
$properties = array();
142
143
$uid = $event->getUID();
144
if (!strlen($uid)) {
145
throw new Exception(
146
pht(
147
'Unable to write ICS document: event has no UID, but each event '.
148
'MUST have a UID.'));
149
}
150
$properties[] = $this->newTextProperty(
151
'UID',
152
$uid);
153
154
$created = $event->getCreatedDateTime();
155
if ($created) {
156
$properties[] = $this->newDateTimeProperty(
157
'CREATED',
158
$event->getCreatedDateTime());
159
}
160
161
$dtstamp = $event->getModifiedDateTime();
162
if (!$dtstamp) {
163
throw new Exception(
164
pht(
165
'Unable to write ICS document: event has no modified time, but '.
166
'each event MUST have a modified time.'));
167
}
168
$properties[] = $this->newDateTimeProperty(
169
'DTSTAMP',
170
$dtstamp);
171
172
$dtstart = $event->getStartDateTime();
173
if ($dtstart) {
174
$properties[] = $this->newDateTimeProperty(
175
'DTSTART',
176
$dtstart);
177
}
178
179
$dtend = $event->getEndDateTime();
180
if ($dtend) {
181
$properties[] = $this->newDateTimeProperty(
182
'DTEND',
183
$event->getEndDateTime());
184
}
185
186
$name = $event->getName();
187
if (phutil_nonempty_string($name)) {
188
$properties[] = $this->newTextProperty(
189
'SUMMARY',
190
$name);
191
}
192
193
$description = $event->getDescription();
194
if (phutil_nonempty_string($description)) {
195
$properties[] = $this->newTextProperty(
196
'DESCRIPTION',
197
$description);
198
}
199
200
$organizer = $event->getOrganizer();
201
if ($organizer) {
202
$properties[] = $this->newUserProperty(
203
'ORGANIZER',
204
$organizer);
205
}
206
207
$attendees = $event->getAttendees();
208
if ($attendees) {
209
foreach ($attendees as $attendee) {
210
$properties[] = $this->newUserProperty(
211
'ATTENDEE',
212
$attendee);
213
}
214
}
215
216
$rrule = $event->getRecurrenceRule();
217
if ($rrule) {
218
$properties[] = $this->newRRULEProperty(
219
'RRULE',
220
$rrule);
221
}
222
223
$recurrence_id = $event->getRecurrenceID();
224
if ($recurrence_id) {
225
$properties[] = $this->newTextProperty(
226
'RECURRENCE-ID',
227
$recurrence_id);
228
}
229
230
$exdates = $event->getRecurrenceExceptions();
231
if ($exdates) {
232
$properties[] = $this->newDateTimesProperty(
233
'EXDATE',
234
$exdates);
235
}
236
237
$rdates = $event->getRecurrenceDates();
238
if ($rdates) {
239
$properties[] = $this->newDateTimesProperty(
240
'RDATE',
241
$rdates);
242
}
243
244
return $properties;
245
}
246
247
private function newTextProperty(
248
$name,
249
$value,
250
array $parameters = array()) {
251
252
$map = array(
253
'\\' => '\\\\',
254
',' => '\\,',
255
"\n" => '\\n',
256
);
257
258
$value = (array)$value;
259
foreach ($value as $k => $v) {
260
$v = str_replace(array_keys($map), array_values($map), $v);
261
$value[$k] = $v;
262
}
263
264
$value = implode(',', $value);
265
266
return $this->newProperty($name, $value, $parameters);
267
}
268
269
private function newDateTimeProperty(
270
$name,
271
PhutilCalendarDateTime $value,
272
array $parameters = array()) {
273
274
return $this->newDateTimesProperty($name, array($value), $parameters);
275
}
276
277
private function newDateTimesProperty(
278
$name,
279
array $values,
280
array $parameters = array()) {
281
assert_instances_of($values, 'PhutilCalendarDateTime');
282
283
if (head($values)->getIsAllDay()) {
284
$parameters[] = array(
285
'name' => 'VALUE',
286
'values' => array(
287
'DATE',
288
),
289
);
290
}
291
292
$datetimes = array();
293
foreach ($values as $value) {
294
$datetimes[] = $value->getISO8601();
295
}
296
$datetimes = implode(';', $datetimes);
297
298
return $this->newProperty($name, $datetimes, $parameters);
299
}
300
301
private function newUserProperty(
302
$name,
303
PhutilCalendarUserNode $value,
304
array $parameters = array()) {
305
306
$parameters[] = array(
307
'name' => 'CN',
308
'values' => array(
309
$value->getName(),
310
),
311
);
312
313
$partstat = null;
314
switch ($value->getStatus()) {
315
case PhutilCalendarUserNode::STATUS_INVITED:
316
$partstat = 'NEEDS-ACTION';
317
break;
318
case PhutilCalendarUserNode::STATUS_ACCEPTED:
319
$partstat = 'ACCEPTED';
320
break;
321
case PhutilCalendarUserNode::STATUS_DECLINED:
322
$partstat = 'DECLINED';
323
break;
324
}
325
326
if ($partstat !== null) {
327
$parameters[] = array(
328
'name' => 'PARTSTAT',
329
'values' => array(
330
$partstat,
331
),
332
);
333
}
334
335
// TODO: We could reasonably fill in "ROLE" and "RSVP" here too, but it
336
// isn't clear if these are important to external programs or not.
337
338
return $this->newProperty($name, $value->getURI(), $parameters);
339
}
340
341
private function newRRULEProperty(
342
$name,
343
PhutilCalendarRecurrenceRule $rule,
344
array $parameters = array()) {
345
346
$value = $rule->toRRULE();
347
return $this->newProperty($name, $value, $parameters);
348
}
349
350
private function newProperty(
351
$name,
352
$value,
353
array $parameters = array()) {
354
355
$map = array(
356
'^' => '^^',
357
"\n" => '^n',
358
'"' => "^'",
359
);
360
361
$writable_params = array();
362
foreach ($parameters as $k => $parameter) {
363
$value_list = array();
364
foreach ($parameter['values'] as $v) {
365
$v = str_replace(array_keys($map), array_values($map), $v);
366
367
// If the parameter value isn't a very simple one, quote it.
368
369
// RFC5545 says that we MUST quote it if it has a colon, a semicolon,
370
// or a comma, and that we MUST quote it if it's a URI.
371
if (!preg_match('/^[A-Za-z0-9-]*\z/', $v)) {
372
$v = '"'.$v.'"';
373
}
374
375
$value_list[] = $v;
376
}
377
378
$writable_params[] = array(
379
'name' => $parameter['name'],
380
'value' => implode(',', $value_list),
381
);
382
}
383
384
return array(
385
'name' => $name,
386
'value' => $value,
387
'parameters' => $writable_params,
388
);
389
}
390
391
}
392
393