1
|
<?php
|
2
|
/**
|
3
|
* @file
|
4
|
* Defines a class that parses iCalcreator vcalendar objects into
|
5
|
* Feeds-compatible data arrays.
|
6
|
*/
|
7
|
|
8
|
class ParserVcalendar {
|
9
|
/**
|
10
|
* Variables used for parsing.
|
11
|
*/
|
12
|
protected $calendar;
|
13
|
protected $source;
|
14
|
protected $fetcherResult;
|
15
|
protected $config;
|
16
|
protected $timezones = array();
|
17
|
protected $xtimezone;
|
18
|
|
19
|
/**
|
20
|
* Variables used for batch processing.
|
21
|
*/
|
22
|
protected $totalComponents = 0;
|
23
|
protected $lastComponentParsed = 0;
|
24
|
|
25
|
/**
|
26
|
* Constructor.
|
27
|
*/
|
28
|
public function __construct($calendar, $source, $fetcher_result, $config) {
|
29
|
$this->calendar = $calendar;
|
30
|
$this->source = $source;
|
31
|
$this->fetcherResult = $fetcher_result;
|
32
|
$this->config = $config;
|
33
|
}
|
34
|
|
35
|
/**
|
36
|
* Parses the vcalendar object into an array of event data arrays.
|
37
|
*
|
38
|
* @param int $offset
|
39
|
* Specifies which section of the feed to start parsing at.
|
40
|
*
|
41
|
* @param int $limit
|
42
|
* Specifies how many components to parse on this run.
|
43
|
*
|
44
|
* @return array
|
45
|
* An array of parsed event data keyed by the same strings as the array
|
46
|
* returned by DateiCalFeedsParser::getiCalMappingSources().
|
47
|
*/
|
48
|
public function parse($offset, $limit) {
|
49
|
// Sometimes, the feed will set a timezone for every event in the calendar
|
50
|
// using the non-standard X-WR-TIMEZONE property. Date iCal uses this
|
51
|
// timezone only if the date property is not in UTC and has no TZID.
|
52
|
$xtimezone = $this->calendar->getProperty('X-WR-TIMEZONE');
|
53
|
if (!empty($xtimezone[1])) {
|
54
|
// Allow modules to alter the timezone string before it gets converted
|
55
|
// into a DateTimeZone.
|
56
|
$context = array(
|
57
|
'property_key' => NULL,
|
58
|
'calendar_component' => NULL,
|
59
|
'calendar' => $this->calendar,
|
60
|
'feeeds_source' => $this->source,
|
61
|
'feeds_fetcher_result' => $this->fetcherResult,
|
62
|
);
|
63
|
drupal_alter('date_ical_import_timezone', $xtimezone[1], $context);
|
64
|
$this->xtimezone = $this->_tzid_to_datetimezone($xtimezone[1]);
|
65
|
}
|
66
|
|
67
|
// Collect the timezones into an array, for easier access.
|
68
|
while ($component = $this->calendar->getComponent('VTIMEZONE')) {
|
69
|
$this->timezones[] = $component;
|
70
|
}
|
71
|
|
72
|
// Collect each component, so we can batch them properly in the next loop.
|
73
|
$raw_components = array();
|
74
|
$types = array('VEVENT', 'VTODO', 'VJOURNAL', 'VFREEBUSY', 'VALARM');
|
75
|
foreach ($types as $type) {
|
76
|
while ($vcalendar_component = $this->calendar->getComponent($type)) {
|
77
|
// Allow modules to alter the vcalendar component before we parse it
|
78
|
// into a Feeds-compatible data array.
|
79
|
$context = array(
|
80
|
'calendar' => $this->calendar,
|
81
|
'source' => $this->source,
|
82
|
'fetcher_result' => $this->fetcherResult,
|
83
|
);
|
84
|
drupal_alter('date_ical_import_component', $vcalendar_component, $context);
|
85
|
$raw_components[] = $vcalendar_component;
|
86
|
}
|
87
|
}
|
88
|
|
89
|
// Store this for use by DateiCalFeedsParser's batch processing code.
|
90
|
$this->totalComponents = count($raw_components);
|
91
|
|
92
|
// Parse each raw component in the current batch into a Feeds-compatible
|
93
|
// event data array.
|
94
|
$events = array();
|
95
|
$sources = DateiCalFeedsParser::getiCalMappingSources();
|
96
|
$batch = array_slice($raw_components, $offset, $limit, TRUE);
|
97
|
foreach ($batch as $ndx => $raw_component) {
|
98
|
$parsed_component = array();
|
99
|
foreach ($sources as $property_key => $data) {
|
100
|
$handler = $data['date_ical_parse_handler'];
|
101
|
$parsed_component[$property_key] = $this->$handler($property_key, $raw_component);
|
102
|
}
|
103
|
$events[] = $parsed_component;
|
104
|
// The indices of the original $raw_components array are preserved in
|
105
|
// $batch, so using the $ndx value here lets us communicate our progress
|
106
|
// through the full collection of components.
|
107
|
$this->lastComponentParsed = $ndx;
|
108
|
}
|
109
|
|
110
|
return $events;
|
111
|
}
|
112
|
|
113
|
/**
|
114
|
* Getter for the protected totalComponents property.
|
115
|
*/
|
116
|
public function getTotalComponents() {
|
117
|
return $this->totalComponents;
|
118
|
}
|
119
|
|
120
|
/**
|
121
|
* Getter for the protected lastComponentParsed property.
|
122
|
*/
|
123
|
public function getLastComponentParsed() {
|
124
|
return $this->lastComponentParsed;
|
125
|
}
|
126
|
|
127
|
/**
|
128
|
* Parses text fields.
|
129
|
*
|
130
|
* @return string
|
131
|
* The parsed text property.
|
132
|
*/
|
133
|
public function parseTextProperty($property_key, $vcalendar_component) {
|
134
|
$text = $vcalendar_component->getProperty($property_key);
|
135
|
if ($text === FALSE) {
|
136
|
if ($property_key == 'SUMMARY') {
|
137
|
$uid = $vcalendar_component->getProperty('UID');
|
138
|
throw new DateIcalParseException(t('The component with UID %uid is invalid because it has no SUMMARY (nodes require a title).', array('%uid' => $uid)));
|
139
|
}
|
140
|
// If the component doesn't have this property, return NULL.
|
141
|
return NULL;
|
142
|
}
|
143
|
// Convert literal \n and \N into newline characters.
|
144
|
$text = str_replace(array('\n', '\N'), "\n", $text);
|
145
|
return $text;
|
146
|
}
|
147
|
|
148
|
/**
|
149
|
* Parses field parameters.
|
150
|
*
|
151
|
* @return string
|
152
|
* The parsed field parameter.
|
153
|
*/
|
154
|
public function parsePropertyParameter($property_key, $vcalendar_component) {
|
155
|
list($key, $attr) = explode(':', $property_key);
|
156
|
$property = $vcalendar_component->getProperty($key, FALSE, TRUE);
|
157
|
if ($property === FALSE) {
|
158
|
// If the component doesn't have this property, return NULL.
|
159
|
return NULL;
|
160
|
}
|
161
|
return isset($property['params'][$attr]) ? $property['params'][$attr] : '';
|
162
|
}
|
163
|
|
164
|
/**
|
165
|
* Parses DATE-TIME and DATE fields.
|
166
|
*
|
167
|
* @return FeedsDateTime
|
168
|
* The parsed datetime object.
|
169
|
*/
|
170
|
public function parseDateTimeProperty($property_key, $vcalendar_component) {
|
171
|
$property = $vcalendar_component->getProperty($property_key, FALSE, TRUE);
|
172
|
// Gather all the other date properties, so we can work with them later.
|
173
|
$duration = $vcalendar_component->getProperty('DURATION', FALSE, TRUE);
|
174
|
$dtstart = $vcalendar_component->getProperty('DTSTART', FALSE, TRUE);
|
175
|
$uid = $vcalendar_component->getProperty('UID');
|
176
|
|
177
|
// DATE-type properties are treated as All Day events which can span over
|
178
|
// multiple days.
|
179
|
// The Date module's All Day event handling was never finalized
|
180
|
// (http://drupal.org/node/874322), which requires us to do some some
|
181
|
// special coddling later.
|
182
|
$is_all_day = (isset($property['params']['VALUE']) && $property['params']['VALUE'] == 'DATE');
|
183
|
|
184
|
// Cover various conditions in which either DTSTART or DTEND are not set.
|
185
|
if ($property === FALSE) {
|
186
|
// When DTEND isn't defined, we may need to emulate it.
|
187
|
if ($property_key == 'DTEND') {
|
188
|
// Unset DTENDs need to emulate the DATE type from DTSTART.
|
189
|
$is_all_day = (isset($dtstart['params']['VALUE']) && $dtstart['params']['VALUE'] == 'DATE');
|
190
|
|
191
|
if ($duration !== FALSE) {
|
192
|
// If a DURATION is defined, emulate DTEND as DTSTART + DURATION.
|
193
|
$property = array(
|
194
|
'value' => iCalUtilityFunctions::_duration2date($dtstart['value'], $duration['value']),
|
195
|
'params' => $dtstart['params'],
|
196
|
);
|
197
|
}
|
198
|
elseif ($is_all_day) {
|
199
|
// If this is an all-day event with no end or duration, treat this
|
200
|
// as a single-day event by emulating DTEND as 1 day after DTSTART.
|
201
|
$property = $dtstart;
|
202
|
$property['value']['day'] = $dtstart['value']['day'] + 1;
|
203
|
}
|
204
|
else {
|
205
|
// This event has no end date.
|
206
|
return NULL;
|
207
|
}
|
208
|
}
|
209
|
elseif ($property_key == 'DTSTART') {
|
210
|
// DTSTART can only be legally unset in non-VEVENT components.
|
211
|
if ($vcalendar_component->objName == 'vevent') {
|
212
|
throw new DateIcalParseException(t('Feed import failed! The VEVENT with UID %uid is invalid: it has no DTSTART.', array('%uid' => $uid)));
|
213
|
}
|
214
|
else {
|
215
|
return NULL;
|
216
|
}
|
217
|
}
|
218
|
}
|
219
|
|
220
|
// When iCalcreator parses a UTC date (one that ends with Z) from an iCal
|
221
|
// feed, it stores that 'Z' into the $property['value']['tz'] value.
|
222
|
if (isset($property['value']['tz'])) {
|
223
|
$property['params']['TZID'] = 'UTC';
|
224
|
}
|
225
|
|
226
|
if ($is_all_day) {
|
227
|
if ($property_key == 'DTEND') {
|
228
|
if ($dtstart === FALSE) {
|
229
|
// This will almost certainly never happen, but the error message
|
230
|
// would be incomprehensible without this check.
|
231
|
throw new DateIcalParseException(t('Feed import failed! The event with UID %uid is invalid: it has a DTEND but no DTSTART!', array('%uid' => $uid)));
|
232
|
}
|
233
|
|
234
|
if (module_exists('date_all_day')) {
|
235
|
// If the Date All Day module is installed, we need to rewind the
|
236
|
// DTEND by one day, because of the problem with FeedsDateTime
|
237
|
// mentioned below.
|
238
|
$prev_day = iCalUtilityFunctions::_duration2date($property['value'], array('day' => -1));
|
239
|
$property['value'] = $prev_day;
|
240
|
}
|
241
|
}
|
242
|
|
243
|
// FeedsDateTime->setTimezone() ignores timezone changes made to dates
|
244
|
// with no time element, which means we can't compensate for the Date
|
245
|
// module's automatic timezone conversion when it writes to the DB. To
|
246
|
// get around that, we must add 00:00:00 explicitly, even though this
|
247
|
// causes other problems (see above and below).
|
248
|
$date_string = sprintf('%d-%d-%d 00:00:00', $property['value']['year'], $property['value']['month'], $property['value']['day']);
|
249
|
// Use the server's timezone rather than letting it default to UTC.
|
250
|
// This will help ensure that the date value doesn't get messed up when
|
251
|
// Date converts its timezone as the value is read from the database.
|
252
|
// This is *essential* for All Day events, because Date stores them as
|
253
|
// '2013-10-03 00:00:00' in the database, rather than doing the sensible
|
254
|
// thing and storing them as '2013-10-03'.
|
255
|
// NOTE TO MAINTAINERS:
|
256
|
// This will not work properly if the site is configured to allow users
|
257
|
// to set their own timezone. Unfortunately, there isn't anything that
|
258
|
// Date iCal can do about that, as far as I can tell.
|
259
|
$datetimezone = new DateTimeZone(date_default_timezone_get());
|
260
|
}
|
261
|
else {
|
262
|
// This is a DATE-TIME property.
|
263
|
$date_string = iCalUtilityFunctions::_format_date_time($property['value']);
|
264
|
|
265
|
// Allow modules to alter the timezone string. This also allows for
|
266
|
// setting a TZID when one was not originally set for this property.
|
267
|
$tzid = isset($property['params']['TZID']) ? $property['params']['TZID'] : NULL;
|
268
|
$context = array(
|
269
|
'property_key' => $property_key,
|
270
|
'calendar_component' => $vcalendar_component,
|
271
|
'calendar' => $this->calendar,
|
272
|
'feeeds_source' => $this->source,
|
273
|
'feeds_fetcher_result' => $this->fetcherResult,
|
274
|
);
|
275
|
drupal_alter('date_ical_import_timezone', $tzid, $context);
|
276
|
|
277
|
if (isset($tzid)) {
|
278
|
$datetimezone = $this->_tzid_to_datetimezone($tzid);
|
279
|
}
|
280
|
elseif (isset($this->xtimezone)) {
|
281
|
// No timezone was set on the parsed date property, so if a timezone
|
282
|
// was detected for the entire iCal feed, use it.
|
283
|
$datetimezone = $this->xtimezone;
|
284
|
}
|
285
|
else {
|
286
|
$msg = t("No timezone was detected for one or more of the events in this feed, forcing Date iCal to use this server's timezone as a fallback.<br>
|
287
|
To make timezone-less events use a different timezone, implement hook_date_ical_import_timezone_alter() in a custom module.");
|
288
|
drupal_set_message($msg, 'status', FALSE);
|
289
|
$this->source->log('parse', $msg, array(), WATCHDOG_NOTICE);
|
290
|
$datetimezone = new DateTimeZone(date_default_timezone_get());
|
291
|
}
|
292
|
}
|
293
|
|
294
|
return new FeedsDateTime($date_string, $datetimezone);
|
295
|
}
|
296
|
|
297
|
/**
|
298
|
* Parses multi-value fields, like the CATEGORIES component.
|
299
|
*
|
300
|
* @return array
|
301
|
* An array of strings contaning the individual values.
|
302
|
*/
|
303
|
public function parseMultivalueProperty($property_key, $vcalendar_component) {
|
304
|
// Since we're not telling it to give us the params data, $property will
|
305
|
// be either FALSE, a string, or an array of strings.
|
306
|
$property = $vcalendar_component->getProperty($property_key);
|
307
|
if (empty($property)) {
|
308
|
// If this multi-value property is being mapped to a Taxonomy field,
|
309
|
// Feeds will interpret anything besides empty array as an array of
|
310
|
// empty values (e.g. array('')). This will create a term for that
|
311
|
// empty value, rather than leaving the field blank.
|
312
|
return array();
|
313
|
}
|
314
|
if (!is_array($property)) {
|
315
|
$property = array($property);
|
316
|
}
|
317
|
return $property;
|
318
|
}
|
319
|
|
320
|
/**
|
321
|
* Format RRULEs, which specify when and how often the event is repeated.
|
322
|
*
|
323
|
* @return string
|
324
|
* An RRULE string, with EXDATE and RDATE values separated by \n.
|
325
|
* This is to make the RRULE compatible with date_repeat_split_rrule().
|
326
|
*/
|
327
|
public function parseRepeatProperty($property_key, $vcalendar_component) {
|
328
|
if ($vcalendar_component->getProperty($property_key) === FALSE) {
|
329
|
return NULL;
|
330
|
}
|
331
|
|
332
|
// Due to a few bugs and limitations with Date Repeat, we need to massage
|
333
|
// the RRULE a bit.
|
334
|
if (count($vcalendar_component->rrule) > 1) {
|
335
|
drupal_set_message(t('The event with UID %uid has multiple RRULEs, but the Date Repeat module only supports one. Only the first RRULE in the event will be used.<br>
|
336
|
If your events need to have a complex repeat pattern, using RDATEs should help.',
|
337
|
array('%uid' => $vcalendar_component->getProperty('UID'))), 'warning'
|
338
|
);
|
339
|
// Date Repeat will get extremely confused if it's sent multiple RRULE
|
340
|
// values, so we need to manually pare it down to only the first one.
|
341
|
$vcalendar_component->rrule = array($vcalendar_component->rrule[0]);
|
342
|
}
|
343
|
foreach ($vcalendar_component->rrule as &$rrule_data) {
|
344
|
// RRULEs must have an INTERVAL, or Date Repeat will throw errors.
|
345
|
if (!isset($rrule_data['value']['INTERVAL'])) {
|
346
|
$rrule_data['value']['INTERVAL'] = '1';
|
347
|
}
|
348
|
|
349
|
if (!isset($rrule_data['value']['COUNT']) && !isset($rrule_data['value']['UNTIL'])) {
|
350
|
drupal_set_message(t("The event with UID %uid has an indefinitely repeating RRULE, which the Date Repeat module doesn't support.<br>
|
351
|
As a workaround, Date iCal set the repeat count to @count. This value can be customized in the iCal parser settings.",
|
352
|
array('%uid' => $vcalendar_component->getProperty('UID'), '@count' => $this->config['indefinite_count'])), 'warning'
|
353
|
);
|
354
|
$rrule_data['value']['COUNT'] = $this->config['indefinite_count'];
|
355
|
}
|
356
|
}
|
357
|
|
358
|
$rrule = trim($vcalendar_component->createRrule());
|
359
|
$rdate = trim($vcalendar_component->createRdate());
|
360
|
$exrule = trim($vcalendar_component->createExrule());
|
361
|
$exdate = trim($vcalendar_component->createExdate());
|
362
|
return "$rrule|$rdate|$exrule|$exdate";
|
363
|
}
|
364
|
|
365
|
/**
|
366
|
* Internal helper function for creating DateTimeZone objects.
|
367
|
*/
|
368
|
protected function _tzid_to_datetimezone($tzid) {
|
369
|
try {
|
370
|
$datetimezone = new DateTimeZone($tzid);
|
371
|
}
|
372
|
catch (Exception $e) {
|
373
|
// In case this is a Windows TZID, read the mapping file to try and
|
374
|
// convert it to a real TZID.
|
375
|
$zones = file_get_contents(drupal_get_path('module', 'date_ical') . '/libraries/windowsZones.json');
|
376
|
$zones_assoc = json_decode($zones, TRUE);
|
377
|
$windows_to_olson_map = array();
|
378
|
foreach ($zones_assoc['supplemental']['windowsZones']['mapTimezones'] as $mapTimezone) {
|
379
|
if ($mapTimezone['mapZone']['_other'] == $tzid) {
|
380
|
// $mapTimezone['mapZone']['_type'] is space-separated TZIDs.
|
381
|
$tzids = preg_split('/\s/', $mapTimezone['mapZone']['_type']);
|
382
|
try {
|
383
|
// They all have the same UTC offset, so for our purposes we can
|
384
|
// just take the first one.
|
385
|
return new DateTimeZone($tzids[0]);
|
386
|
}
|
387
|
catch (Exception $e) {
|
388
|
// If this one also fails, we're out of luck, so just fall through
|
389
|
// to the regular error report code.
|
390
|
break;
|
391
|
}
|
392
|
}
|
393
|
}
|
394
|
|
395
|
$tz_wiki = l(t('here'), 'http://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List');
|
396
|
$help = l(t('README'), 'admin/help/date_ical', array('absolute' => TRUE));
|
397
|
$msg = t(
|
398
|
'"@tz" is not a valid timezone (see the TZ column !here), so Date iCal had to fall back to UTC (which is probably wrong!).<br>
|
399
|
Please read the Date iCal !readme for instructions on how to fix this.',
|
400
|
array('@tz' => $tzid, '!here' => $tz_wiki, '!readme' => $help)
|
401
|
);
|
402
|
$this->source->log('parse', $msg, array(), WATCHDOG_WARNING);
|
403
|
drupal_set_message($msg, 'warning', FALSE);
|
404
|
$datetimezone = new DateTimeZone('UTC');
|
405
|
}
|
406
|
return $datetimezone;
|
407
|
}
|
408
|
}
|