date_ical.utils.inc 7.45 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php
/**
 * @file
 * Utility functions for Date iCal. Many of these are re-writes of buggy Date
 * module code.
 */

/**
 * Parse the repeat data into date values.
 *
 * This is a re-write of date_repeat_build_dates() which fixes it's bugs
 * regarding multi-property RDATEs and EXDATEs.
 */
function _date_ical_get_repeat_dates($field_name, $repeat_data, $item, $source) {
  module_load_include('inc', 'date_api', 'date_api_ical');
  $field_info = field_info_field($field_name);
17

18 19 20 21
  $rrule_values = _date_ical_parse_repeat_rule($repeat_data['RRULE']);
  //$exrule_values = _date_ical_parse_repeat_rule($repeat_data['EXRULE']);
  $rdates = _date_ical_parse_repeat_dates($repeat_data['RDATE']);
  $exdates = _date_ical_parse_repeat_dates($repeat_data['EXDATE']);
22

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
  // By the time we get here, the start and end dates have been
  // adjusted back to UTC, but we want localtime dates to do
  // things like '+1 Tuesday', so adjust back to localtime.
  $timezone = date_get_timezone($field_info['settings']['tz_handling'], $item['timezone']);
  $timezone_db = date_get_timezone_db($field_info['settings']['tz_handling']);
  $start = new DateObject($item['value'], $timezone_db, date_type_format($field_info['type']));
  $start->limitGranularity($field_info['settings']['granularity']);
  if ($timezone != $timezone_db) {
    date_timezone_set($start, timezone_open($timezone));
  }
  if (!empty($item['value2']) && $item['value2'] != $item['value']) {
    $end = new DateObject($item['value2'], date_get_timezone_db($field_info['settings']['tz_handling']), date_type_format($field_info['type']));
    $end->limitGranularity($field_info['settings']['granularity']);
    date_timezone_set($end, timezone_open($timezone));
  }
  else {
    $end = $start;
  }
  $duration = $start->difference($end);
  $start_datetime = date_format($start, DATE_FORMAT_DATETIME);
43

44
  if (!empty($rrule_values['UNTIL']['datetime'])) {
45 46 47 48 49 50 51 52
    // The spec says that UNTIL must be in UTC, but not all feed creators
    // follow that rule. If the user specified that he wanted to overcome this
    // problem, use $timezone for the $final_repeat, and then convert the UNTIL
    // in the unparsed RRULE to UTC.
    if (!empty($source->importer->config['parser']['config']['until_not_utc'])) {
      // Change the parsed UNTIL from UTC to $timezone, so that the
      // date_ical_date() won't convert it.
      $rrule_values['UNTIL']['tz'] = $timezone;
53

54
      // Convert the unparsed UNTIL to UTC, since the Date code will use it.
55 56
      // It may currently have a Z on it, but only because iCalcreator blindly
      // adds one to DATETIME-type UNTILs if it's not there.
57
      $matches = array();
58
      if (preg_match('/^(.*?)UNTIL=([\dT]+)Z?(.*?)$/', $repeat_data['RRULE'], $matches)) {
59 60 61 62 63 64 65 66
        // If the UNTIL value doesn't have a "T", it's a DATE, making timezone
        // fixes irrelvant.
        if (strpos($matches[2], 'T') !== FALSE) {
          $until_date = new DateObject($matches[2], $timezone);
          $until_date->setTimezone(new DateTimeZone('UTC'));
          $matches[2] = $until_date->format('Ymd\THis');
          $repeat_data['RRULE'] = "{$matches[1]}UNTIL={$matches[2]}Z{$matches[3]}";
        }
67 68
      }
      else {
69
        watchdog('date_ical', 'The RRULE string "%rrule" could not be parsed to fix the UNTIL value. Date repeats may not be calculated correctly.',
70 71 72
          array('%rrule' => $repeat_data['RRULE']), WATCHDOG_WARNING
        );
      }
73 74 75
    }
    $final_repeat = date_ical_date($rrule_values['UNTIL'], $timezone);
    $final_repeat_datetime = date_format($final_repeat, DATE_FORMAT_DATETIME);
76 77
  }
  elseif (!empty($rrule_values['COUNT'])) {
78
    $final_repeat_datetime = NULL;
79 80
  }
  else {
81
    // No UNTIL and no COUNT? This is an illegal RRULE.
82 83
    return array();
  }
84

85 86 87 88 89 90 91 92 93 94 95 96 97
  // Convert the EXDATE and RDATE values to datetime strings.
  // Even though exdates and rdates can be specified to the second, Date
  // Repeat's code checks them by comparing them to the date value only.
  $exceptions = array();
  foreach ($exdates as $exception) {
    $date = date_ical_date($exception, $timezone);
    $exceptions[] = date_format($date, 'Y-m-d');
  }
  $additions = array();
  foreach ($rdates as $rdate) {
    $date = date_ical_date($rdate, $timezone);
    $additions[] = date_format($date, 'Y-m-d');
  }
98

99
  // TODO: EXRULEs.
100

101
  $date_repeat_compatible_rrule = "{$repeat_data['RRULE']}\n{$repeat_data['RDATE']}\n{$repeat_data['EXDATE']}";
102
  $calculated_dates = date_repeat_calc($date_repeat_compatible_rrule, $start_datetime, $final_repeat_datetime, $exceptions, $timezone, $additions);
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
  $repeat_dates = array();
  foreach ($calculated_dates as $delta => $date) {
    // date_repeat_calc always returns DATE_DATETIME dates, which is
    // not necessarily $field_info['type'] dates.
    // Convert returned dates back to db timezone before storing.
    $date_start = new DateObject($date, $timezone, DATE_FORMAT_DATETIME);
    $date_start->limitGranularity($field_info['settings']['granularity']);
    date_timezone_set($date_start, timezone_open($timezone_db));
    $date_end = clone($date_start);
    date_modify($date_end, '+' . $duration . ' seconds');
    $repeat_dates[$delta] = array(
      'value' => date_format($date_start, date_type_format($field_info['type'])),
      'value2' => date_format($date_end, date_type_format($field_info['type'])),
      'offset' => date_offset_get($date_start),
      'offset2' => date_offset_get($date_end),
      'timezone' => $timezone,
      'rrule' => $date_repeat_compatible_rrule,
    );
  }
  return $repeat_dates;
}

/**
 * Parse an rrule or exrule string.
 *
 * @return array
 *   Array in the form of PROPERTY => array(VALUES)
 *   PROPERTIES include FREQ, INTERVAL, COUNT, BYDAY, BYMONTH, BYYEAR, UNTIL.
 */
function _date_ical_parse_repeat_rule($repeat_rule_string) {
  module_load_include('inc', 'date_api', 'date_api_ical');
134

135 136 137 138 139 140 141 142
  $repeat_rule_string = preg_replace('/(R|EX)RULE.*:/', '', $repeat_rule_string);
  $items = array('DATA' => $repeat_rule_string);
  foreach (explode(';', $repeat_rule_string) as $recur_val) {
    list($key, $value) = explode('=', $recur_val);
    // Must be some kind of invalid data.
    if (empty($key) || empty($value)) {
      continue;
    }
143

144
    // The following keys never have multiple values.
145 146 147 148 149 150 151 152 153 154
    if (in_array($key, array('UNTIL', 'FREQ', 'INTERVAL', 'COUNT', 'WKST'))) {
      if ($key == 'UNTIL') {
        // This is a function from the date_api module, not date_ical.
        $value = date_ical_parse_date('', $value);
      }
    }
    else {
      // The rest can be multi-value csv strings.
      $value = explode(',', $value);
    }
155

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
    $items[$key] = $value;
  }
  return $items;
}

/**
 * Parses an iCal RDATE or EXDATE, including multi-property rules.
 *
 * @param string $repeat_date_string
 *   An RDATE or EXDATE property string, e.g.
 *   "EXDATE;TZID=America/Los_Angeles:20130415T180000,20130422T180000"
 *   Can be multiple EXDATE or RDATE properties, separated by newlines.
 *
 * @return array
 *   An array of dates returned by date_ical_parse_date().
 */
function _date_ical_parse_repeat_dates($repeat_date_string) {
  module_load_include('inc', 'date_api', 'date_api_ical');
174

175 176
  $properties = explode("\n", str_replace("\r\n", "\n", $repeat_date_string));
  $parsed_dates = array();
177

178 179 180 181 182 183 184 185 186 187 188
  foreach ($properties as $property) {
    $matches = array();
    if (preg_match('/(R|EX)DATE([^:]*):(.*)/', $property, $matches)) {
      $params = $matches[2];
      foreach (explode(',', $matches[3]) as $date) {
        $parsed_dates[] = date_ical_parse_date($params, $date);
      }
    }
  }
  return $parsed_dates;
}