Projet

Général

Profil

Paste
Télécharger (20,5 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date_ical / libraries / ParserVcalendar.inc @ f4462ddf

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