Projet

Général

Profil

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

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

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
}