DateIcalIcalcreatorParser.inc 17.1 KB
Newer Older
Robert Rollins's avatar
Robert Rollins committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

/**
 * @file
 *  Classes implementing Date iCal's iCalcreator-based parser functionality.
 */

class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
  
  /**
   * Output sources this parser offers.
   *
   * Includes additional field for the handler for output.
   *
   * @see DateIcalFeedsParser::getMappingSources().
   * @see DateIcalFeedsParser::getSourceElement().
   */
  static protected $sources = array(
    'summary' => array(
20
21
      'name' => t('Summary'),
      'description' => t('A short summary, or title, for the calendar component.'),
Robert Rollins's avatar
Robert Rollins committed
22
23
24
      'date_ical_parse_handler' => 'formatText',
    ),
    'description' => array(
25
26
      'name' => t('Description'),
      'description' => t('A more complete description of the calendar component than that provided by the "summary" property.'),
Robert Rollins's avatar
Robert Rollins committed
27
28
29
      'date_ical_parse_handler' => 'formatText',
    ),
    'dtstart' => array(
30
31
32
      'name' => t('Date start'),
      'description' => t('Start time for the feed item.
        If also using the "Date end" source, this MUST come before it in the mapping, due to the way iCal feeds are formatted.'),
Robert Rollins's avatar
Robert Rollins committed
33
34
35
      'date_ical_parse_handler' => 'formatDateTime',
    ),
    'dtend' => array(
36
37
      'name' => t('Date end'),
      'description' => t('End time for the feed item.'),
Robert Rollins's avatar
Robert Rollins committed
38
39
      'date_ical_parse_handler' => 'formatDateTime',
    ),
40
    'rrule' => array(
41
42
      'name' => t('Repeat rule'),
      'description' => ('Describes when and how often this calendar component should repeat.
43
        The date field for the target node must be configrred to support repeating dates, using the Date Repeat Field module (a submodule of Date).
44
        When using this source field, it MUST come after Date start (and Date end, if used) in the mapping.'),
45
46
      'date_ical_parse_handler' => 'formatRrule',
    ),
Robert Rollins's avatar
Robert Rollins committed
47
48
    'uid' => array(
      'name' => 'UID',
49
      'description' => t('UID of feed item'),
Robert Rollins's avatar
Robert Rollins committed
50
51
52
53
      'date_ical_parse_handler' => 'formatText',
    ),
    'url' => array(
      'name' => 'URL',
54
      'description' => t('URL for the feed item.'),
Robert Rollins's avatar
Robert Rollins committed
55
56
57
      'date_ical_parse_handler' => 'formatText',
    ),
    'location' => array(
58
59
      'name' => t('Location text'),
      'description' => t('Text of the location property of the feed item.'),
Robert Rollins's avatar
Robert Rollins committed
60
61
62
      'date_ical_parse_handler' => 'formatText',
    ),
    'location:altrep' => array(
63
64
      'name' => t('Location alternate representation'),
      'description' => t('Additional location information, usually a URL to a page with more info.'),
Robert Rollins's avatar
Robert Rollins committed
65
66
67
      'date_ical_parse_handler' => 'formatParamText',
    ),
    'categories' => array(
68
69
      'name' => t('Categories'),
      'description' => t('Catagories of the feed item.'),
Robert Rollins's avatar
Robert Rollins committed
70
71
72
      'date_ical_parse_handler' => 'formatCategories',
    ),
  );
73
  
Robert Rollins's avatar
Robert Rollins committed
74
75
76
77
78
79
80
  /**
   * Load and run parser implementation of FeedsParser::parse().
   *
   * @params - change these to generic required paramters.
   */
  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
    self::loadLibrary();
81
    
82
83
    // Read the iCal feed into memory.
    $ical_feed_contents = $fetcher_result->getRaw();
84
    
85
86
87
    // Create the calendar object and parse the feed.
    $calendar = new vcalendar();
    if (!$calendar->parse($ical_feed_contents)) {
88
      $url = $source->config[$source->importer->config['fetcher']['plugin_key']]['source'];
89
      throw new DateIcalParseException(t('Error parsing iCal feed: %url', array('%url' => $url)));
Robert Rollins's avatar
Robert Rollins committed
90
91
92
93
94
95
96
97
98
99
100
101
102
    }
    
    // Allow modules to alter the vcalendar object before we interpret its properties.
    $context = array(
      'source' => $source,
      'fetcher_result' => $fetcher_result,
    );
    drupal_alter('date_ical_icalcreator_calendar', $calendar, $context);
    
    //
    // Set a result object.
    //
    $result = new DateIcalParserResult();
103
    
Robert Rollins's avatar
Robert Rollins committed
104
105
106
107
108
109
    // FeedsResult properties
    $xcalname = $calendar->getProperty('X-WR-CALNAME');
    $result->title = !empty($xcalname) ? $xcalname[1] : '';
    $xcaldesc = $calendar->getProperty('X-WR-CALDESC');
    $result->description = !empty($xcaldesc) ? $xcaldesc[1] : '';
    $result->link = NULL;
110
    
Robert Rollins's avatar
Robert Rollins committed
111
112
113
114
115
116
117
118
119
120
121
122
    // Additional DateIcalParserResult properties
    $xtimezone = $calendar->getProperty('X-WR-TIMEZONE');
    if (!empty($xtimezone)) {
      try {
        $tz = new DateTimeZone($xtimezone[1]);
        $result->timezone = $tz;
      }
      catch (Exception $e) {
        $source->log('parse', 'Invalid X-WR-TIMEZONE: %error', array('%error' => $e->getMessage()), WATCHDOG_NOTICE);
      }
    }
    
123
    // DEV NOTES:
124
125
    // Due to the way that the loop after this one manipulates the $components array, all the work that gets done in here
    // gets overridden. However, we probably *should* be using this, somehow, since I think it was in the old version
126
    // in order to handle non-standard VTIMEZONES. Maybe?
Robert Rollins's avatar
Robert Rollins committed
127
    $components = array();
128
    while ($component = $calendar->getComponent('VTIMEZONE')) {
Robert Rollins's avatar
Robert Rollins committed
129
130
131
      $components[$component->getProperty('tzid')] = new DateIcalIcalcreatorComponent($component);
    }
    $result->timezones = $components;
132
    
133
    // Separate the individual feed items into DateIcalIcalcreatorComponents.
Robert Rollins's avatar
Robert Rollins committed
134
    $components = array();
135
    $component_types = array('vevent', 'vtodo', 'vjournal', 'vfreebusy', 'valarm');
Robert Rollins's avatar
Robert Rollins committed
136
137
138
139
    foreach ($component_types as $component_type) {
      while ($component = $calendar->getComponent($component_type)) {
        $component = new DateIcalIcalcreatorComponent($component);
        
140
141
        // Allow modules to alter the DateIcalIcalcreatorComponent before we
        // parse it into Feeds-readable data.
Robert Rollins's avatar
Robert Rollins committed
142
143
144
145
146
147
148
149
150
151
152
153
        $context = array(
          'calendar' => $calendar,
          'source' => $source,
          'fetcher_result' => $fetcher_result,
          'parser_result' => $result,
        );
        drupal_alter('date_ical_icalcreator_component', $component, $context);
        
        $components[] = $component;
      }
    }
    $result->items = $components;
154
    
Robert Rollins's avatar
Robert Rollins committed
155
156
    return $result;
  }
157
  
Robert Rollins's avatar
Robert Rollins committed
158
159
160
  /******
   * Source output formatters.
   *
161
   * TODO: Could be in a class of their own?
Robert Rollins's avatar
Robert Rollins committed
162
   **/
163
  
Robert Rollins's avatar
Robert Rollins committed
164
165
166
167
168
  /**
   * Format text fields.
   *
   * @todo is \n \N handling correct?
   */
169
  public function formatText($property_key, $property, DateIcalIcalcreatorComponent $item, FeedsParserResult $result, FeedsSource $source) {
Robert Rollins's avatar
Robert Rollins committed
170
171
172
173
    $context = get_defined_vars();
    $text = $property['value'];
    $text = str_replace(array("\\n", "\\N"), "\n", $text);
    
174
    // Allow modules to alter the text before it's mapped to the target.
Robert Rollins's avatar
Robert Rollins committed
175
    drupal_alter('date_ical_feeds_object', $text, $context);
176
    
Robert Rollins's avatar
Robert Rollins committed
177
178
179
180
181
182
183
184
    return $text;
  }
  
  /**
   * Format Text Parameter
   *
   * @return string.
   */
185
  public function formatParamText($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
Robert Rollins's avatar
Robert Rollins committed
186
187
188
189
190
191
    $context = get_defined_vars();
    $position = strpos($property_key, ':');
    $key = substr($property_key, 0, $position);
    $attribute = strtoupper(substr($property_key, ++$position));
    $text = $property['params'][$attribute];
    
192
    // Allow modules to alter the param text before it's mapped to the target.
Robert Rollins's avatar
Robert Rollins committed
193
    drupal_alter('date_ical_feeds_object', $text, $context);
194
    
Robert Rollins's avatar
Robert Rollins committed
195
196
    return $text;
  }
197
  
Robert Rollins's avatar
Robert Rollins committed
198
199
200
201
202
  /**
   * Format date fields.
   *
   * @return FeedsDateTime
   */
203
  public function formatDateTime($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
Robert Rollins's avatar
Robert Rollins committed
204
205
206
    $context = get_defined_vars();
    $date_array = $property['value'];
    
207
208
    // It's frustrating that iCalcreator gives us date data in a different
    // format than what it expects us to give back.
Robert Rollins's avatar
Robert Rollins committed
209
210
211
    if (isset($property['params']['TZID'])) {
      $date_array['tz'] = $property['params']['TZID'];
    }
212
    
Robert Rollins's avatar
Robert Rollins committed
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    if (isset($property['params']['VALUE']) && $property['params']['VALUE'] == 'DATE') {
      /**
       * DATE values are All Day events, with no time-of-day.
       * They can span over multiple days.
       * FeedsDateTime sets the granularity correctly.
       * However the granularity is not used yet.
       * All Day events handling is not finalized at the time of writing.
       * http://drupal.org/node/874322 To Date & All Day Date Handling
       */
      if ($property_key == 'dtend') {
        $start_prop = $item->getProperty('dtstart');
        $start_array = $start_prop['value'];
        if (isset($start_prop['params']['TZID'])) {
          $start_array['tz'] =  $start_prop['params']['TZID'];
        }
        
229
230
        // On single-day All Day events (the start date + 1 = the end date), just ignore the DTEND
        // if the Date All Day module is installed. Otherwise it'll be displayed strangely.
231
232
233
234
235
        if ($start_array['year']       == $date_array['year']
            && $start_array['month']   == $date_array['month']
            && $start_array['day'] + 1 == $date_array['day']
            && module_exists('date_all_day')) {
          return;
Robert Rollins's avatar
Robert Rollins committed
236
        }
237
238
239
240
241
242
        elseif (module_exists('date_all_day')) {
          // When we add the time of 00:00:00 to our date explicitly
          // the date that gets parsed for DTEND is 1 day ahead.
          // so taking a day away before that (if date_all_day is found)
          $date_array['day'] -= 1;
        }
Robert Rollins's avatar
Robert Rollins committed
243
      }
244
      // The order in which the source -> target mapping is set up matters here: dtstart has to come before dtend.
Robert Rollins's avatar
Robert Rollins committed
245
246
247
248
249
250
251
252
253
254
255
256
257
258
      // This has been noted in the description for this source field.
      if ($property_key == 'dtstart') {
        if ($duration = $item->getProperty('duration')) {
          $end_array = iCalUtilityFunctions::_duration2date($date_array, $duration['value']);
          $item->setProperty('dtend', $end_array);
        }
        elseif (!$item->getProperty('dtend')) {
          // For cases where a VEVENT component specifies a DTSTART property with a DATE value type
          // but no DTEND nor DURATION property, the event's duration is taken to be one day.
          $end_array = $date_array;
          $end_array['day'] += 1;
          $item->setProperty('dtend', $end_array);
        }
      }
259
260
261
262
263
264
      // FeedsDateTime's implementation of setTimezone() assumes that dates with no time element should just ignore
      // timezone changes, so I had to add the 00:00:00 explicitly, even though it shouldn't be necessary.
      $date_string = sprintf('%d-%d-%d 00:00:00', $date_array['year'], $date_array['month'], $date_array['day']);
      // Set the timezone for All Day events to the server's timezone, rather than letting them default to UTC,
      // so that they don't get improperly offset when saved to the DB.
      $parsed_datetime = new FeedsDateTime($date_string, new DateTimeZone(date_default_timezone_get()));
Robert Rollins's avatar
Robert Rollins committed
265
266
267
268
    }
    else {
      // Date with time.
      $date_string = iCalUtilityFunctions::_format_date_time($date_array);
269
      
Robert Rollins's avatar
Robert Rollins committed
270
271
      $tz = NULL;
      if (isset($date_array['tz'])) {
272
273
274
275
        // 'Z' == 'Zulu' == 'UTC'. DateTimeZone won't acept Z, so change it to UTC.
        if (strtoupper($date_array['tz']) == 'Z') {
          $date_array['tz'] = 'UTC';
        }
276
277
278
279
280
        // Allow modules to alter the timezone string before it gets converted into a DateTimeZone.
        drupal_alter('date_ical_timezone', $date_array['tz'], $context);
        
        try {
          $tz = new DateTimeZone($date_array['tz']);
Robert Rollins's avatar
Robert Rollins committed
281
        }
282
        catch (Exception $e) {
283
284
          $source->log('parse', t('"%tzid" is not recognized by PHP as a valid timezone, so UTC was used instead. Try implementing hook_date_ical_timezone_alter() in a custom module to fix this problem.',
            array('%tzid' => $date_array['tz'])), array(), WATCHDOG_WARNING);
285
          drupal_set_message(t('"%tzid" is not recognized by PHP as a valid timezone, so UTC was used instead. Try implementing hook_date_ical_timezone_alter() in a custom module to fix this problem.',
286
            array('%tzid' => $date_array['tz'])), 'warning');
Robert Rollins's avatar
Robert Rollins committed
287
288
        }
      }
289
290
291
292
293
      // If there was no timezone in the date array itself, add one if we have one.
      else if (!empty($result->timezone)) {
        // Use the timezone that was detected for the entire iCal feed.
        $tz = $result->timezone;
      }
Robert Rollins's avatar
Robert Rollins committed
294
295
      
      // If this iCal object has no DTEND, but it does have a DURATION, emulate DTEND as DTSTART + DURATION.
296
      // Again, this mechanism requires that dtstart be parsed before dtend in the source->target mapping.
Robert Rollins's avatar
Robert Rollins committed
297
298
299
300
301
302
303
304
      if ($property_key == 'dtstart' && !$item->getProperty('dtend') && ($duration = $item->getProperty('duration'))) {
        $end_array = iCalUtilityFunctions::_duration2date($date_array, $duration['value']);
        $item->setProperty('dtend', $end_array);
      }
      
      $parsed_datetime = new FeedsDateTime($date_string, $tz);
    }
    
305
    // Allow modules to alter the FeedsDateTime object before it's mapped to the target.
Robert Rollins's avatar
Robert Rollins committed
306
307
308
309
    drupal_alter('date_ical_feeds_object', $parsed_datetime, $context);
    
    return $parsed_datetime;
  }
310
  
Robert Rollins's avatar
Robert Rollins committed
311
312
313
314
315
  /**
   * Format Categories
   *
   * @return array of free tags strings.
   */
316
  public function formatCategories($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
Robert Rollins's avatar
Robert Rollins committed
317
    $context = get_defined_vars();
318
    // Allow modules to alter the categories before they're mapped to the target.
Robert Rollins's avatar
Robert Rollins committed
319
320
321
322
323
324
    drupal_alter('date_ical_feeds_object', $property['value'], $context);
    
    if (!empty($property['value'])) {
      return is_array($property['value']) ? $property['value'] : array($property['value']);
    }
  }
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
  
  /**
   * Format RRULEs, which specify when and how often the event is repeated.
   *
   * @return An RRULE string, with any EXDATE and RDATE values appended, separated by \n.
   *   This is to make the RRULE compatible with date_repeat_split_rrule().
   */
  public function formatRrule($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
    $context = get_defined_vars();
    // Allow modules to alter the RRULE before it's mapped to the target.
    drupal_alter('date_ical_feeds_object', $property, $context);
    $item->setRrule($property);
    /*
    // I'm not really sure it makes sense to let users alter these, and since you can have more than one of each,
    // it's probbaly not worth the effort to program it correctly. But here's my attempts so far, if anyone wants
    // to finish it.
    $rdate = $item->getProperty('rdate');
    if (!empty($rdate)) {
      $context['property_key'] = 'rdate';
      drupal_alter('date_ical_feeds_object', $rdate, $context);
      $item->setRdate($rdate);
    }
    $exdate = $item->getProperty('exdate');
    if (!empty($rdate)) {
      $context['property_key'] = 'exdate';
      drupal_alter('date_ical_feeds_object', $exdate, $context);
      $item->setExdate($exdate);
    }
    */
    
    $rrule = $item->getRrule();
    $exdate = $item->getExdate();
    $rdate = $item->getRdate();
    return trim("$rrule\n$exdate\n$rdate");
  }
360
  
Robert Rollins's avatar
Robert Rollins committed
361
362
363
364
365
366
367
368
369
370
371
372
373
  /**
   * Load the iCalcreator library.
   */
  static public function loadLibrary() {
    $creator_path = libraries_get_path('iCalcreator');
    require_once($creator_path . '/iCalcreator.class.php');
  }
}

/**
 * A wrapper on iCalcreator component class.
 */
class DateIcalIcalcreatorComponent implements DateIcalComponentInterface {
374
  public $component;
Robert Rollins's avatar
Robert Rollins committed
375
  private $_serialized_component;
376
  
Robert Rollins's avatar
Robert Rollins committed
377
378
379
380
381
382
383
384
385
  /**
   * Constructor.
   *
   * @param
   *   vcalendar component configured, but not yet parsed into Feeds-readable data.
   */
  public function __construct(calendarComponent $component) {
    $this->component = $component;
  }
386
  
Robert Rollins's avatar
Robert Rollins committed
387
388
389
390
391
392
393
  /**
   * Serialization helper.
   */
  public function __sleep() {
    $this->_serialized_component = serialize($this->component);
    return array('_serialized_component');
  }
394
  
Robert Rollins's avatar
Robert Rollins committed
395
396
397
398
399
400
401
  /**
   * Unserialization helper.
   */
  public function __wakeup() {
    DateIcalIcalcreatorParser::loadLibrary();
    $this->component = unserialize($this->_serialized_component);
  }
402
  
Robert Rollins's avatar
Robert Rollins committed
403
404
405
  public function getComponentType() {
    return $this->component->objName;
  }
406
  
Robert Rollins's avatar
Robert Rollins committed
407
408
409
  public function getProperty($name) {
    return $this->component->getProperty($name, FALSE, TRUE);
  }
410
  
Robert Rollins's avatar
Robert Rollins committed
411
412
413
414
415
416
417
418
419
420
421
  // $args should be an array of arguments to be sent to the set$Name()
  // function of calendarComponent. If $args is not an array, it'll be
  // wrapped as one. This is necessary because the iCalcreator API has different
  // parameter lists for the set$Name() functions of different properties.
  public function setProperty($name, $args) {
    if (!is_array($args)) {
      $args = array($args);
    }
    // Call the setProperty() method on $this->component with the arguments: $name + $args.
    call_user_func_array(array($this->component, "setProperty"), array($name) + $args);
  }
422
423
424
425
426
427
428
429
430
431
432
  
  // Special set functions, because using component->setRrule() and similar always add an *additional*
  // RRULE/RDATE/EXDATE, rather than changing the existing one. This may be a bug in iCalcreator, or
  // I may just be calling the functions wrong.
  public function setRrule($rrule) {
    return $this->component->rrule[0] = $rrule;
  }
  
  public function getRrule() {
    return trim($this->component->createRrule());
  }
433
  
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
  public function setRdate($rdate) {
    return $this->component->rdate[0] = $rdate;
  }
  
  public function getRdate() {
    return trim($this->component->createRdate());
  }
  
  public function setExdate($exdate) {
    return $this->component->exdate[0] = $exdate;
  }
  
  public function getExdate() {
    return trim($this->component->createExdate());
  }
Robert Rollins's avatar
Robert Rollins committed
449
}