date_api_ical.inc 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. <?php
  2. /**
  3. * @file
  4. * Parse iCal data.
  5. *
  6. * This file must be included when these functions are needed.
  7. */
  8. /**
  9. * Return an array of iCalendar information from an iCalendar file.
  10. *
  11. * No timezone adjustment is performed in the import since the timezone
  12. * conversion needed will vary depending on whether the value is being
  13. * imported into the database (when it needs to be converted to UTC), is being
  14. * viewed on a site that has user-configurable timezones (when it needs to be
  15. * converted to the user's timezone), if it needs to be converted to the
  16. * site timezone, or if it is a date without a timezone which should not have
  17. * any timezone conversion applied.
  18. *
  19. * Properties that have dates and times are converted to sub-arrays like:
  20. * 'datetime' => date in YYYY-MM-DD HH:MM format, not timezone adjusted
  21. * 'all_day' => whether this is an all-day event
  22. * 'tz' => the timezone of the date, could be blank for absolute
  23. * times that should get no timezone conversion.
  24. *
  25. * Exception dates can have muliple values and are returned as arrays
  26. * like the above for each exception date.
  27. *
  28. * Most other properties are returned as PROPERTY => VALUE.
  29. *
  30. * Each item in the VCALENDAR will return an array like:
  31. * [0] => Array (
  32. * [TYPE] => VEVENT
  33. * [UID] => 104
  34. * [SUMMARY] => An example event
  35. * [URL] => http://example.com/node/1
  36. * [DTSTART] => Array (
  37. * [datetime] => 1997-09-07 09:00:00
  38. * [all_day] => 0
  39. * [tz] => US/Eastern
  40. * )
  41. * [DTEND] => Array (
  42. * [datetime] => 1997-09-07 11:00:00
  43. * [all_day] => 0
  44. * [tz] => US/Eastern
  45. * )
  46. * [RRULE] => Array (
  47. * [FREQ] => Array (
  48. * [0] => MONTHLY
  49. * )
  50. * [BYDAY] => Array (
  51. * [0] => 1SU
  52. * [1] => -1SU
  53. * )
  54. * )
  55. * [EXDATE] => Array (
  56. * [0] = Array (
  57. * [datetime] => 1997-09-21 09:00:00
  58. * [all_day] => 0
  59. * [tz] => US/Eastern
  60. * )
  61. * [1] = Array (
  62. * [datetime] => 1997-10-05 09:00:00
  63. * [all_day] => 0
  64. * [tz] => US/Eastern
  65. * )
  66. * )
  67. * [RDATE] => Array (
  68. * [0] = Array (
  69. * [datetime] => 1997-09-21 09:00:00
  70. * [all_day] => 0
  71. * [tz] => US/Eastern
  72. * )
  73. * [1] = Array (
  74. * [datetime] => 1997-10-05 09:00:00
  75. * [all_day] => 0
  76. * [tz] => US/Eastern
  77. * )
  78. * )
  79. * )
  80. *
  81. * @todo
  82. * figure out how to handle this if subgroups are nested,
  83. * like a VALARM nested inside a VEVENT.
  84. *
  85. * @param string $filename
  86. * Location (local or remote) of a valid iCalendar file.
  87. *
  88. * @return array
  89. * An array with all the elements from the ical.
  90. */
  91. function date_ical_import($filename) {
  92. // Fetch the iCal data. If file is a URL, use drupal_http_request. fopen
  93. // isn't always configured to allow network connections.
  94. if (substr($filename, 0, 4) == 'http') {
  95. // Fetch the ical data from the specified network location.
  96. $icaldatafetch = drupal_http_request($filename);
  97. // Check the return result.
  98. if ($icaldatafetch->error) {
  99. watchdog('date ical', 'HTTP Request Error importing %filename: @error', array('%filename' => $filename, '@error' => $icaldatafetch->error));
  100. return FALSE;
  101. }
  102. // Break the return result into one array entry per lines.
  103. $icaldatafolded = explode("\n", $icaldatafetch->data);
  104. }
  105. else {
  106. $icaldatafolded = @file($filename, FILE_IGNORE_NEW_LINES);
  107. if ($icaldatafolded === FALSE) {
  108. watchdog('date ical', 'Failed to open file: %filename', array('%filename' => $filename));
  109. return FALSE;
  110. }
  111. }
  112. // Verify this is iCal data.
  113. if (trim($icaldatafolded[0]) != 'BEGIN:VCALENDAR') {
  114. watchdog('date ical', 'Invalid calendar file: %filename', array('%filename' => $filename));
  115. return FALSE;
  116. }
  117. return date_ical_parse($icaldatafolded);
  118. }
  119. /**
  120. * Returns an array of iCalendar information from an iCalendar file.
  121. *
  122. * As date_ical_import() but different param.
  123. *
  124. * @param array $icaldatafolded
  125. * An array of lines from an ical feed.
  126. *
  127. * @return array
  128. * An array with all the elements from the ical.
  129. */
  130. function date_ical_parse($icaldatafolded = array()) {
  131. $items = array();
  132. // Verify this is iCal data.
  133. if (trim($icaldatafolded[0]) != 'BEGIN:VCALENDAR') {
  134. watchdog('date ical', 'Invalid calendar file.');
  135. return FALSE;
  136. }
  137. // "Unfold" wrapped lines.
  138. $icaldata = array();
  139. foreach ($icaldatafolded as $line) {
  140. $out = array();
  141. // See if this looks like the beginning of a new property or value. If not,
  142. // it is a continuation of the previous line. The regex is to ensure that
  143. // wrapped QUOTED-PRINTABLE data is kept intact.
  144. if (!preg_match('/([A-Z]+)[:;](.*)/', $line, $out)) {
  145. // Trim up to 1 leading space from wrapped line per iCalendar standard.
  146. $line = array_pop($icaldata) . (ltrim(substr($line, 0, 1)) . substr($line, 1));
  147. }
  148. $icaldata[] = $line;
  149. }
  150. unset($icaldatafolded);
  151. // Parse the iCal information.
  152. $parents = array();
  153. $subgroups = array();
  154. $vcal = '';
  155. foreach ($icaldata as $line) {
  156. $line = trim($line);
  157. $vcal .= $line . "\n";
  158. // Deal with begin/end tags separately.
  159. if (preg_match('/(BEGIN|END):V(\S+)/', $line, $matches)) {
  160. $closure = $matches[1];
  161. $type = 'V' . $matches[2];
  162. if ($closure == 'BEGIN') {
  163. array_push($parents, $type);
  164. array_push($subgroups, array());
  165. }
  166. elseif ($closure == 'END') {
  167. end($subgroups);
  168. $subgroup = &$subgroups[key($subgroups)];
  169. switch ($type) {
  170. case 'VCALENDAR':
  171. if (prev($subgroups) == FALSE) {
  172. $items[] = array_pop($subgroups);
  173. }
  174. else {
  175. $parent[array_pop($parents)][] = array_pop($subgroups);
  176. }
  177. break;
  178. // Add the timezones in with their index their TZID.
  179. case 'VTIMEZONE':
  180. $subgroup = end($subgroups);
  181. $id = $subgroup['TZID'];
  182. unset($subgroup['TZID']);
  183. // Append this subgroup onto the one above it.
  184. prev($subgroups);
  185. $parent = &$subgroups[key($subgroups)];
  186. $parent[$type][$id] = $subgroup;
  187. array_pop($subgroups);
  188. array_pop($parents);
  189. break;
  190. // Do some fun stuff with durations and all_day events and then append
  191. // to parent.
  192. case 'VEVENT':
  193. case 'VALARM':
  194. case 'VTODO':
  195. case 'VJOURNAL':
  196. case 'VVENUE':
  197. case 'VFREEBUSY':
  198. default:
  199. // Can't be sure whether DTSTART is before or after DURATION, so
  200. // parse DURATION at the end.
  201. if (isset($subgroup['DURATION'])) {
  202. date_ical_parse_duration($subgroup, 'DURATION');
  203. }
  204. // Add a top-level indication for the 'All day' condition. Leave it
  205. // in the individual date components, too, so it is always available
  206. // even when you are working with only a portion of the VEVENT
  207. // array, like in Feed API parsers.
  208. $subgroup['all_day'] = FALSE;
  209. // iCal spec states 'The "DTEND" property for a "VEVENT" calendar
  210. // component specifies the non-inclusive end of the event'. Adjust
  211. // multi-day events to remove the extra day because the Date code
  212. // assumes the end date is inclusive.
  213. if (!empty($subgroup['DTEND']) && (!empty($subgroup['DTEND']['all_day']))) {
  214. // Make the end date one day earlier.
  215. $date = new DateObject ($subgroup['DTEND']['datetime'] . ' 00:00:00', $subgroup['DTEND']['tz']);
  216. date_modify($date, '-1 day');
  217. $subgroup['DTEND']['datetime'] = date_format($date, 'Y-m-d');
  218. }
  219. // If a start datetime is defined AND there is no definition for
  220. // the end datetime THEN make the end datetime equal the start
  221. // datetime and if it is an all day event define the entire event
  222. // as a single all day event.
  223. if (!empty($subgroup['DTSTART']) &&
  224. (empty($subgroup['DTEND']) && empty($subgroup['RRULE']) && empty($subgroup['RRULE']['COUNT']))) {
  225. $subgroup['DTEND'] = $subgroup['DTSTART'];
  226. }
  227. // Add this element to the parent as an array under the component
  228. // name.
  229. if (!empty($subgroup['DTSTART']['all_day'])) {
  230. $subgroup['all_day'] = TRUE;
  231. }
  232. // Add this element to the parent as an array under the
  233. prev($subgroups);
  234. $parent = &$subgroups[key($subgroups)];
  235. $parent[$type][] = $subgroup;
  236. array_pop($subgroups);
  237. array_pop($parents);
  238. break;
  239. }
  240. }
  241. }
  242. // Handle all other possibilities.
  243. else {
  244. // Grab current subgroup.
  245. end($subgroups);
  246. $subgroup = &$subgroups[key($subgroups)];
  247. // Split up the line into nice pieces for PROPERTYNAME,
  248. // PROPERTYATTRIBUTES, and PROPERTYVALUE.
  249. preg_match('/([^;:]+)(?:;([^:]*))?:(.+)/', $line, $matches);
  250. $name = !empty($matches[1]) ? strtoupper(trim($matches[1])) : '';
  251. $field = !empty($matches[2]) ? $matches[2] : '';
  252. $data = !empty($matches[3]) ? $matches[3] : '';
  253. $parse_result = '';
  254. switch ($name) {
  255. // Keep blank lines out of the results.
  256. case '':
  257. break;
  258. // Lots of properties have date values that must be parsed out.
  259. case 'CREATED':
  260. case 'LAST-MODIFIED':
  261. case 'DTSTART':
  262. case 'DTEND':
  263. case 'DTSTAMP':
  264. case 'FREEBUSY':
  265. case 'DUE':
  266. case 'COMPLETED':
  267. $parse_result = date_ical_parse_date($field, $data);
  268. break;
  269. case 'EXDATE':
  270. case 'RDATE':
  271. $parse_result = date_ical_parse_exceptions($field, $data);
  272. break;
  273. case 'TRIGGER':
  274. // A TRIGGER can either be a date or in the form -PT1H.
  275. if (!empty($field)) {
  276. $parse_result = date_ical_parse_date($field, $data);
  277. }
  278. else {
  279. $parse_result = array('DATA' => $data);
  280. }
  281. break;
  282. case 'DURATION':
  283. // Can't be sure whether DTSTART is before or after DURATION in
  284. // the VEVENT, so store the data and parse it at the end.
  285. $parse_result = array('DATA' => $data);
  286. break;
  287. case 'RRULE':
  288. case 'EXRULE':
  289. $parse_result = date_ical_parse_rrule($field, $data);
  290. break;
  291. case 'STATUS':
  292. case 'SUMMARY':
  293. case 'DESCRIPTION':
  294. $parse_result = date_ical_parse_text($field, $data);
  295. break;
  296. case 'LOCATION':
  297. $parse_result = date_ical_parse_location($field, $data);
  298. break;
  299. // For all other properties, just store the property and the value.
  300. // This can be expanded on in the future if other properties should
  301. // be given special treatment.
  302. default:
  303. $parse_result = $data;
  304. break;
  305. }
  306. // Store the result of our parsing.
  307. $subgroup[$name] = $parse_result;
  308. }
  309. }
  310. return $items;
  311. }
  312. /**
  313. * Parses a ical date element.
  314. *
  315. * Possible formats to parse include:
  316. * PROPERTY:YYYYMMDD[T][HH][MM][SS][Z]
  317. * PROPERTY;VALUE=DATE:YYYYMMDD[T][HH][MM][SS][Z]
  318. * PROPERTY;VALUE=DATE-TIME:YYYYMMDD[T][HH][MM][SS][Z]
  319. * PROPERTY;TZID=XXXXXXXX;VALUE=DATE:YYYYMMDD[T][HH][MM][SS]
  320. * PROPERTY;TZID=XXXXXXXX:YYYYMMDD[T][HH][MM][SS]
  321. *
  322. * The property and the colon before the date are removed in the import
  323. * process above and we are left with $field and $data.
  324. *
  325. * @param string $field
  326. * The text before the colon and the date, i.e.
  327. * ';VALUE=DATE:', ';VALUE=DATE-TIME:', ';TZID='
  328. * @param string $data
  329. * The date itself, after the colon, in the format YYYYMMDD[T][HH][MM][SS][Z]
  330. * 'Z', if supplied, means the date is in UTC.
  331. *
  332. * @return array
  333. * $items array, consisting of:
  334. * 'datetime' => date in YYYY-MM-DD HH:MM format, not timezone adjusted
  335. * 'all_day' => whether this is an all-day event with no time
  336. * 'tz' => the timezone of the date, could be blank if the ical
  337. * has no timezone; the ical specs say no timezone
  338. * conversion should be done if no timezone info is
  339. * supplied
  340. * @todo
  341. * Another option for dates is the format PROPERTY;VALUE=PERIOD:XXXX. The
  342. * period may include a duration, or a date and a duration, or two dates, so
  343. * would have to be split into parts and run through date_ical_parse_date()
  344. * and date_ical_parse_duration(). This is not commonly used, so ignored for
  345. * now. It will take more work to figure how to support that.
  346. */
  347. function date_ical_parse_date($field, $data) {
  348. $items = array('datetime' => '', 'all_day' => '', 'tz' => '');
  349. if (empty($data)) {
  350. return $items;
  351. }
  352. // Make this a little more whitespace independent.
  353. $data = trim($data);
  354. // Turn the properties into a nice indexed array of
  355. // array(PROPERTYNAME => PROPERTYVALUE);
  356. $field_parts = preg_split('/[;:]/', $field);
  357. $properties = array();
  358. foreach ($field_parts as $part) {
  359. if (strpos($part, '=') !== FALSE) {
  360. $tmp = explode('=', $part);
  361. $properties[$tmp[0]] = $tmp[1];
  362. }
  363. }
  364. // Make this a little more whitespace independent.
  365. $data = trim($data);
  366. // Record if a time has been found.
  367. $has_time = FALSE;
  368. // If a format is specified, parse it according to that format.
  369. if (isset($properties['VALUE'])) {
  370. switch ($properties['VALUE']) {
  371. case 'DATE':
  372. preg_match(DATE_REGEX_ICAL_DATE, $data, $regs);
  373. // Date.
  374. $datetime = date_pad($regs[1]) . '-' . date_pad($regs[2]) . '-' . date_pad($regs[3]);
  375. break;
  376. case 'DATE-TIME':
  377. preg_match(DATE_REGEX_ICAL_DATETIME, $data, $regs);
  378. // Date.
  379. $datetime = date_pad($regs[1]) . '-' . date_pad($regs[2]) . '-' . date_pad($regs[3]);
  380. // Time.
  381. $datetime .= ' ' . date_pad($regs[4]) . ':' . date_pad($regs[5]) . ':' . date_pad($regs[6]);
  382. $has_time = TRUE;
  383. break;
  384. }
  385. }
  386. // If no format is specified, attempt a loose match.
  387. else {
  388. preg_match(DATE_REGEX_LOOSE, $data, $regs);
  389. if (!empty($regs) && count($regs) > 2) {
  390. // Date.
  391. $datetime = date_pad($regs[1]) . '-' . date_pad($regs[2]) . '-' . date_pad($regs[3]);
  392. if (isset($regs[4])) {
  393. $has_time = TRUE;
  394. // Time.
  395. $datetime .= ' ' . (!empty($regs[5]) ? date_pad($regs[5]) : '00') .
  396. ':' . (!empty($regs[6]) ? date_pad($regs[6]) : '00') .
  397. ':' . (!empty($regs[7]) ? date_pad($regs[7]) : '00');
  398. }
  399. }
  400. }
  401. // Use timezone if explicitly declared.
  402. if (isset($properties['TZID'])) {
  403. $tz = $properties['TZID'];
  404. // Fix alternatives like US-Eastern which should be US/Eastern.
  405. $tz = str_replace('-', '/', $tz);
  406. // Unset invalid timezone names.
  407. module_load_include('inc', 'date_api', 'date_api.admin');
  408. $tz = _date_timezone_replacement($tz);
  409. if (!date_timezone_is_valid($tz)) {
  410. $tz = '';
  411. }
  412. }
  413. // If declared as UTC with terminating 'Z', use that timezone.
  414. elseif (strpos($data, 'Z') !== FALSE) {
  415. $tz = 'UTC';
  416. }
  417. // Otherwise this date is floating.
  418. else {
  419. $tz = '';
  420. }
  421. $items['datetime'] = $datetime;
  422. $items['all_day'] = $has_time ? FALSE : TRUE;
  423. $items['tz'] = $tz;
  424. return $items;
  425. }
  426. /**
  427. * Parse an ical repeat rule.
  428. *
  429. * @return array
  430. * Array in the form of PROPERTY => array(VALUES)
  431. * PROPERTIES include FREQ, INTERVAL, COUNT, BYDAY, BYMONTH, BYYEAR, UNTIL
  432. */
  433. function date_ical_parse_rrule($field, $data) {
  434. $data = preg_replace("/RRULE.*:/", '', $data);
  435. $items = array('DATA' => $data);
  436. $rrule = explode(';', $data);
  437. foreach ($rrule as $key => $value) {
  438. $param = explode('=', $value);
  439. // Must be some kind of invalid data.
  440. if (count($param) != 2) {
  441. continue;
  442. }
  443. if ($param[0] == 'UNTIL') {
  444. $values = date_ical_parse_date('', $param[1]);
  445. }
  446. else {
  447. $values = explode(',', $param[1]);
  448. }
  449. // Treat items differently if they have multiple or single values.
  450. if (in_array($param[0], array('FREQ', 'INTERVAL', 'COUNT', 'WKST'))) {
  451. $items[$param[0]] = $param[1];
  452. }
  453. else {
  454. $items[$param[0]] = $values;
  455. }
  456. }
  457. return $items;
  458. }
  459. /**
  460. * Parse exception dates (can be multiple values).
  461. *
  462. * @return array
  463. * an array of date value arrays.
  464. */
  465. function date_ical_parse_exceptions($field, $data) {
  466. $data = str_replace($field . ':', '', $data);
  467. $items = array('DATA' => $data);
  468. $ex_dates = explode(',', $data);
  469. foreach ($ex_dates as $ex_date) {
  470. $items[] = date_ical_parse_date('', $ex_date);
  471. }
  472. return $items;
  473. }
  474. /**
  475. * Parses the duration of the event.
  476. *
  477. * Example:
  478. * DURATION:PT1H30M
  479. * DURATION:P1Y2M
  480. *
  481. * @param array $subgroup
  482. * Array of other values in the vevent so we can check for DTSTART.
  483. */
  484. function date_ical_parse_duration(&$subgroup, $field = 'DURATION') {
  485. $items = $subgroup[$field];
  486. $data = $items['DATA'];
  487. preg_match('/^P(\d{1,4}[Y])?(\d{1,2}[M])?(\d{1,2}[W])?(\d{1,2}[D])?([T]{0,1})?(\d{1,2}[H])?(\d{1,2}[M])?(\d{1,2}[S])?/', $data, $duration);
  488. $items['year'] = isset($duration[1]) ? str_replace('Y', '', $duration[1]) : '';
  489. $items['month'] = isset($duration[2]) ?str_replace('M', '', $duration[2]) : '';
  490. $items['week'] = isset($duration[3]) ?str_replace('W', '', $duration[3]) : '';
  491. $items['day'] = isset($duration[4]) ?str_replace('D', '', $duration[4]) : '';
  492. $items['hour'] = isset($duration[6]) ?str_replace('H', '', $duration[6]) : '';
  493. $items['minute'] = isset($duration[7]) ?str_replace('M', '', $duration[7]) : '';
  494. $items['second'] = isset($duration[8]) ?str_replace('S', '', $duration[8]) : '';
  495. $start_date = array_key_exists('DTSTART', $subgroup) ? $subgroup['DTSTART']['datetime'] : date_format(date_now(), DATE_FORMAT_ISO);
  496. $timezone = array_key_exists('DTSTART', $subgroup) ? $subgroup['DTSTART']['tz'] : variable_get('date_default_timezone');
  497. if (empty($timezone)) {
  498. $timezone = 'UTC';
  499. }
  500. $date = new DateObject($start_date, $timezone);
  501. $date2 = clone($date);
  502. foreach ($items as $item => $count) {
  503. if ($count > 0) {
  504. date_modify($date2, '+' . $count . ' ' . $item);
  505. }
  506. }
  507. $format = isset($subgroup['DTSTART']['type']) && $subgroup['DTSTART']['type'] == 'DATE' ? 'Y-m-d' : 'Y-m-d H:i:s';
  508. $subgroup['DTEND'] = array(
  509. 'datetime' => date_format($date2, DATE_FORMAT_DATETIME),
  510. 'all_day' => isset($subgroup['DTSTART']['all_day']) ? $subgroup['DTSTART']['all_day'] : 0,
  511. 'tz' => $timezone,
  512. );
  513. $duration = date_format($date2, 'U') - date_format($date, 'U');
  514. $subgroup['DURATION'] = array('DATA' => $data, 'DURATION' => $duration);
  515. }
  516. /**
  517. * Parse and clean up ical text elements.
  518. */
  519. function date_ical_parse_text($field, $data) {
  520. if (strstr($field, 'QUOTED-PRINTABLE')) {
  521. $data = quoted_printable_decode($data);
  522. }
  523. // Strip line breaks within element.
  524. $data = str_replace(array("\r\n ", "\n ", "\r "), '', $data);
  525. // Put in line breaks where encoded.
  526. $data = str_replace(array("\\n", "\\N"), "\n", $data);
  527. // Remove other escaping.
  528. $data = stripslashes($data);
  529. return $data;
  530. }
  531. /**
  532. * Parse location elements.
  533. *
  534. * Catch situations like the upcoming.org feed that uses
  535. * LOCATION;VENUE-UID="http://upcoming.yahoo.com/venue/104/":111 First Street...
  536. * or more normal LOCATION;UID=123:111 First Street...
  537. * Upcoming feed would have been improperly broken on the ':' in http://
  538. * so we paste the $field and $data back together first.
  539. *
  540. * Use non-greedy check for ':' in case there are more of them in the address.
  541. */
  542. function date_ical_parse_location($field, $data) {
  543. if (preg_match('/UID=[?"](.+)[?"][*?:](.+)/', $field . ':' . $data, $matches)) {
  544. $location = array();
  545. $location['UID'] = $matches[1];
  546. $location['DESCRIPTION'] = stripslashes($matches[2]);
  547. return $location;
  548. }
  549. else {
  550. // Remove other escaping.
  551. $location = stripslashes($data);
  552. return $location;
  553. }
  554. }
  555. /**
  556. * Return a date object for the ical date, adjusted to its local timezone.
  557. *
  558. * @param array $ical_date
  559. * An array of ical date information created in the ical import.
  560. * @param string $to_tz
  561. * The timezone to convert the date's value to.
  562. *
  563. * @return object
  564. * A timezone-adjusted date object.
  565. */
  566. function date_ical_date($ical_date, $to_tz = FALSE) {
  567. // If the ical date has no timezone, must assume it is stateless
  568. // so treat it as a local date.
  569. if (empty($ical_date['datetime'])) {
  570. return NULL;
  571. }
  572. elseif (empty($ical_date['tz'])) {
  573. $from_tz = date_default_timezone();
  574. }
  575. else {
  576. $from_tz = $ical_date['tz'];
  577. }
  578. if (strlen($ical_date['datetime']) < 11) {
  579. $ical_date['datetime'] .= ' 00:00:00';
  580. }
  581. $date = new DateObject($ical_date['datetime'], new DateTimeZone($from_tz));
  582. if ($to_tz && $ical_date['tz'] != '' && $to_tz != $ical_date['tz']) {
  583. date_timezone_set($date, timezone_open($to_tz));
  584. }
  585. return $date;
  586. }
  587. /**
  588. * Escape #text elements for safe iCal use.
  589. *
  590. * @param string $text
  591. * Text to escape
  592. *
  593. * @return string
  594. * Escaped text
  595. *
  596. */
  597. function date_ical_escape_text($text) {
  598. $text = drupal_html_to_text($text);
  599. $text = trim($text);
  600. // TODO Per #38130 the iCal specs don't want : and " escaped
  601. // but there was some reason for adding this in. Need to watch
  602. // this and see if anything breaks.
  603. // $text = str_replace('"', '\"', $text);
  604. // $text = str_replace(":", "\:", $text);
  605. $text = preg_replace("/\\\b/", "\\\\", $text);
  606. $text = str_replace(",", "\,", $text);
  607. $text = str_replace(";", "\;", $text);
  608. $text = str_replace("\n", "\\n ", $text);
  609. return trim($text);
  610. }
  611. /**
  612. * Build an iCal RULE from $form_values.
  613. *
  614. * @param array $form_values
  615. * An array constructed like the one created by date_ical_parse_rrule().
  616. * [RRULE] => Array (
  617. * [FREQ] => Array (
  618. * [0] => MONTHLY
  619. * )
  620. * [BYDAY] => Array (
  621. * [0] => 1SU
  622. * [1] => -1SU
  623. * )
  624. * [UNTIL] => Array (
  625. * [datetime] => 1997-21-31 09:00:00
  626. * [all_day] => 0
  627. * [tz] => US/Eastern
  628. * )
  629. * )
  630. * [EXDATE] => Array (
  631. * [0] = Array (
  632. * [datetime] => 1997-09-21 09:00:00
  633. * [all_day] => 0
  634. * [tz] => US/Eastern
  635. * )
  636. * [1] = Array (
  637. * [datetime] => 1997-10-05 09:00:00
  638. * [all_day] => 0
  639. * [tz] => US/Eastern
  640. * )
  641. * )
  642. * [RDATE] => Array (
  643. * [0] = Array (
  644. * [datetime] => 1997-09-21 09:00:00
  645. * [all_day] => 0
  646. * [tz] => US/Eastern
  647. * )
  648. * [1] = Array (
  649. * [datetime] => 1997-10-05 09:00:00
  650. * [all_day] => 0
  651. * [tz] => US/Eastern
  652. * )
  653. * )
  654. */
  655. function date_api_ical_build_rrule($form_values) {
  656. $RRULE = '';
  657. if (empty($form_values) || !is_array($form_values)) {
  658. return $RRULE;
  659. }
  660. // Grab the RRULE data and put them into iCal RRULE format.
  661. $RRULE .= 'RRULE:FREQ=' . (!array_key_exists('FREQ', $form_values) ? 'DAILY' : $form_values['FREQ']);
  662. $RRULE .= ';INTERVAL=' . (!array_key_exists('INTERVAL', $form_values) ? 1 : $form_values['INTERVAL']);
  663. // Unset the empty 'All' values.
  664. if (array_key_exists('BYDAY', $form_values) && is_array($form_values['BYDAY'])) {
  665. unset($form_values['BYDAY']['']);
  666. }
  667. if (array_key_exists('BYMONTH', $form_values) && is_array($form_values['BYMONTH'])) {
  668. unset($form_values['BYMONTH']['']);
  669. }
  670. if (array_key_exists('BYMONTHDAY', $form_values) && is_array($form_values['BYMONTHDAY'])) {
  671. unset($form_values['BYMONTHDAY']['']);
  672. }
  673. if (array_key_exists('BYDAY', $form_values) && is_array($form_values['BYDAY']) && $BYDAY = implode(",", $form_values['BYDAY'])) {
  674. $RRULE .= ';BYDAY=' . $BYDAY;
  675. }
  676. if (array_key_exists('BYMONTH', $form_values) && is_array($form_values['BYMONTH']) && $BYMONTH = implode(",", $form_values['BYMONTH'])) {
  677. $RRULE .= ';BYMONTH=' . $BYMONTH;
  678. }
  679. if (array_key_exists('BYMONTHDAY', $form_values) && is_array($form_values['BYMONTHDAY']) && $BYMONTHDAY = implode(",", $form_values['BYMONTHDAY'])) {
  680. $RRULE .= ';BYMONTHDAY=' . $BYMONTHDAY;
  681. }
  682. // The UNTIL date is supposed to always be expressed in UTC.
  683. // The input date values may already have been converted to a date object on a
  684. // previous pass, so check for that.
  685. if (array_key_exists('UNTIL', $form_values) && array_key_exists('datetime', $form_values['UNTIL']) && !empty($form_values['UNTIL']['datetime'])) {
  686. // We only collect a date for UNTIL, but we need it to be inclusive, so
  687. // force it to a full datetime element at the last second of the day.
  688. if (!is_object($form_values['UNTIL']['datetime'])) {
  689. // If this is a date without time, give it time.
  690. if (strlen($form_values['UNTIL']['datetime']) < 11) {
  691. $form_values['UNTIL']['datetime'] .= ' 23:59:59';
  692. $form_values['UNTIL']['granularity'] = serialize(drupal_map_assoc(array('year', 'month', 'day', 'hour', 'minute', 'second')));
  693. $form_values['UNTIL']['all_day'] = FALSE;
  694. }
  695. $until = date_ical_date($form_values['UNTIL'], 'UTC');
  696. }
  697. else {
  698. $until = $form_values['UNTIL']['datetime'];
  699. }
  700. $RRULE .= ';UNTIL=' . date_format($until, DATE_FORMAT_ICAL) . 'Z';
  701. }
  702. // Our form doesn't allow a value for COUNT, but it may be needed by
  703. // modules using the API, so add it to the rule.
  704. if (array_key_exists('COUNT', $form_values)) {
  705. $RRULE .= ';COUNT=' . $form_values['COUNT'];
  706. }
  707. // iCal rules presume the week starts on Monday unless otherwise specified,
  708. // so we'll specify it.
  709. if (array_key_exists('WKST', $form_values)) {
  710. $RRULE .= ';WKST=' . $form_values['WKST'];
  711. }
  712. else {
  713. $RRULE .= ';WKST=' . date_repeat_dow2day(variable_get('date_first_day', 0));
  714. }
  715. // Exceptions dates go last, on their own line.
  716. // The input date values may already have been converted to a date
  717. // object on a previous pass, so check for that.
  718. if (isset($form_values['EXDATE']) && is_array($form_values['EXDATE'])) {
  719. $ex_dates = array();
  720. foreach ($form_values['EXDATE'] as $value) {
  721. if (!empty($value['datetime'])) {
  722. $date = !is_object($value['datetime']) ? date_ical_date($value, 'UTC') : $value['datetime'];
  723. $ex_date = !empty($date) ? date_format($date, DATE_FORMAT_ICAL) . 'Z': '';
  724. if (!empty($ex_date)) {
  725. $ex_dates[] = $ex_date;
  726. }
  727. }
  728. }
  729. if (!empty($ex_dates)) {
  730. sort($ex_dates);
  731. $RRULE .= chr(13) . chr(10) . 'EXDATE:' . implode(',', $ex_dates);
  732. }
  733. }
  734. elseif (!empty($form_values['EXDATE'])) {
  735. $RRULE .= chr(13) . chr(10) . 'EXDATE:' . $form_values['EXDATE'];
  736. }
  737. // Exceptions dates go last, on their own line.
  738. if (isset($form_values['RDATE']) && is_array($form_values['RDATE'])) {
  739. $ex_dates = array();
  740. foreach ($form_values['RDATE'] as $value) {
  741. $date = !is_object($value['datetime']) ? date_ical_date($value, 'UTC') : $value['datetime'];
  742. $ex_date = !empty($date) ? date_format($date, DATE_FORMAT_ICAL) . 'Z': '';
  743. if (!empty($ex_date)) {
  744. $ex_dates[] = $ex_date;
  745. }
  746. }
  747. if (!empty($ex_dates)) {
  748. sort($ex_dates);
  749. $RRULE .= chr(13) . chr(10) . 'RDATE:' . implode(',', $ex_dates);
  750. }
  751. }
  752. elseif (!empty($form_values['RDATE'])) {
  753. $RRULE .= chr(13) . chr(10) . 'RDATE:' . $form_values['RDATE'];
  754. }
  755. return $RRULE;
  756. }