You are here

public function DateiCalParse::parse in Date 8

Returns an array of iCalendar information from an iCalendar file.

As date_ical_import() but different param.

Parameters

array $icaldatafolded: An array of lines from an ical feed.

Return value

array An array with all the elements from the ical.

1 call to DateiCalParse::parse()
DateiCalParse::import in date_api/lib/Drupal/date_api/DateiCalParse.php

File

date_api/lib/Drupal/date_api/DateiCalParse.php, line 163
Parse iCal data.

Class

DateiCalParse
Return an array of iCalendar information from an iCalendar file.

Namespace

Drupal\date_api

Code

public function parse($icaldatafolded = array()) {
  $items = array();

  // Verify this is iCal data.
  if (trim($icaldatafolded[0]) != 'BEGIN:VCALENDAR') {
    watchdog('date ical', 'Invalid calendar file.');
    return FALSE;
  }

  // "Unfold" wrapped lines.
  $icaldata = array();
  foreach ($icaldatafolded as $line) {
    $out = array();

    // See if this looks like the beginning of a new property or value. If not,
    // it is a continuation of the previous line. The regex is to ensure that
    // wrapped QUOTED-PRINTABLE data is kept intact.
    if (!preg_match('/([A-Z]+)[:;](.*)/', $line, $out)) {

      // Trim up to 1 leading space from wrapped line per iCalendar standard.
      $line = array_pop($icaldata) . (ltrim(substr($line, 0, 1)) . substr($line, 1));
    }
    $icaldata[] = $line;
  }
  unset($icaldatafolded);

  // Parse the iCal information.
  $parents = array();
  $subgroups = array();
  $vcal = '';
  foreach ($icaldata as $line) {
    $line = trim($line);
    $vcal .= $line . "\n";

    // Deal with begin/end tags separately.
    if (preg_match('/(BEGIN|END):V(\\S+)/', $line, $matches)) {
      $closure = $matches[1];
      $type = 'V' . $matches[2];
      if ($closure == 'BEGIN') {
        array_push($parents, $type);
        array_push($subgroups, array());
      }
      elseif ($closure == 'END') {
        end($subgroups);
        $subgroup =& $subgroups[key($subgroups)];
        switch ($type) {
          case 'VCALENDAR':
            if (prev($subgroups) == FALSE) {
              $items[] = array_pop($subgroups);
            }
            else {
              $parent[array_pop($parents)][] = array_pop($subgroups);
            }
            break;

          // Add the timezones in with their index their TZID.
          case 'VTIMEZONE':
            $subgroup = end($subgroups);
            $id = $subgroup['TZID'];
            unset($subgroup['TZID']);

            // Append this subgroup onto the one above it.
            prev($subgroups);
            $parent =& $subgroups[key($subgroups)];
            $parent[$type][$id] = $subgroup;
            array_pop($subgroups);
            array_pop($parents);
            break;

          // Do some fun stuff with durations and all_day events and then append
          // to parent.
          case 'VEVENT':
          case 'VALARM':
          case 'VTODO':
          case 'VJOURNAL':
          case 'VVENUE':
          case 'VFREEBUSY':
          default:

            // Can't be sure whether DTSTART is before or after DURATION, so
            // parse DURATION at the end.
            if (isset($subgroup['DURATION'])) {
              self::parse_duration($subgroup, 'DURATION');
            }

            // Add a top-level indication for the 'All day' condition. Leave it
            // in the individual date components, too, so it is always available
            // even when you are working with only a portion of the VEVENT
            // array, like in Feed API parsers.
            $subgroup['all_day'] = FALSE;

            // iCal spec states 'The "DTEND" property for a "VEVENT" calendar
            // component specifies the non-inclusive end of the event'. Adjust
            // multi-day events to remove the extra day because the Date code
            // assumes the end date is inclusive.
            if (!empty($subgroup['DTEND']) && !empty($subgroup['DTEND']['all_day'])) {

              // Make the end date one day earlier.
              $date = new DrupalDateTime($subgroup['DTEND']['datetime'] . ' 00:00:00', $subgroup['DTEND']['tz']);
              date_modify($date, '-1 day');
              $subgroup['DTEND']['datetime'] = date_format($date, 'Y-m-d');
            }

            // If a start datetime is defined AND there is no definition for
            // the end datetime THEN make the end datetime equal the start
            // datetime and if it is an all day event define the entire event
            // as a single all day event.
            if (!empty($subgroup['DTSTART']) && (empty($subgroup['DTEND']) && empty($subgroup['RRULE']) && empty($subgroup['RRULE']['COUNT']))) {
              $subgroup['DTEND'] = $subgroup['DTSTART'];
            }

            // Add this element to the parent as an array under the component
            // name.
            if (!empty($subgroup['DTSTART']['all_day'])) {
              $subgroup['all_day'] = TRUE;
            }

            // Add this element to the parent as an array under the
            prev($subgroups);
            $parent =& $subgroups[key($subgroups)];
            $parent[$type][] = $subgroup;
            array_pop($subgroups);
            array_pop($parents);
            break;
        }
      }
    }
    else {

      // Grab current subgroup.
      end($subgroups);
      $subgroup =& $subgroups[key($subgroups)];

      // Split up the line into nice pieces for PROPERTYNAME,
      // PROPERTYATTRIBUTES, and PROPERTYVALUE.
      preg_match('/([^;:]+)(?:;([^:]*))?:(.+)/', $line, $matches);
      $name = !empty($matches[1]) ? strtoupper(trim($matches[1])) : '';
      $field = !empty($matches[2]) ? $matches[2] : '';
      $data = !empty($matches[3]) ? $matches[3] : '';
      $parse_result = '';
      switch ($name) {

        // Keep blank lines out of the results.
        case '':
          break;

        // Lots of properties have date values that must be parsed out.
        case 'CREATED':
        case 'LAST-MODIFIED':
        case 'DTSTART':
        case 'DTEND':
        case 'DTSTAMP':
        case 'FREEBUSY':
        case 'DUE':
        case 'COMPLETED':
          $parse_result = self::parse_date($data, $field);
          break;
        case 'EXDATE':
        case 'RDATE':
          $parse_result = self::parse_exceptions($data, $field);
          break;
        case 'TRIGGER':

          // A TRIGGER can either be a date or in the form -PT1H.
          if (!empty($field)) {
            $parse_result = self::parse_date($data, $field);
          }
          else {
            $parse_result = array(
              'DATA' => $data,
            );
          }
          break;
        case 'DURATION':

          // Can't be sure whether DTSTART is before or after DURATION in
          // the VEVENT, so store the data and parse it at the end.
          $parse_result = array(
            'DATA' => $data,
          );
          break;
        case 'RRULE':
        case 'EXRULE':
          $parse_result = self::parse_rrule($data, $field);
          break;
        case 'STATUS':
        case 'SUMMARY':
        case 'DESCRIPTION':
          $parse_result = self::parse_text($data, $field);
          break;
        case 'LOCATION':
          $parse_result = self::parse_location($data, $field);
          break;

        // For all other properties, just store the property and the value.
        // This can be expanded on in the future if other properties should
        // be given special treatment.
        default:
          $parse_result = $data;
          break;
      }

      // Store the result of our parsing.
      $subgroup[$name] = $parse_result;
    }
  }
  return $items;
}