4 * This file incudes functions for parsing iCal data files during
7 * It will be included by import_handler.php.
9 * The iCal specification is available online at:
10 * http://www.ietf.org/rfc/rfc2445.txt
14 // Parse the ical file and return the data hash.
15 function parse_ical ( $cal_file ) {
16 global $tz, $errormsg;
20 if (!$fd=@fopen($cal_file,"r")) {
21 $errormsg .= "Can't read temporary file: $cal_file\n";
25 // Read in contents of entire file first
28 while (!feof($fd) && empty( $error ) ) {
30 $data .= fgets($fd, 4096);
33 // Now fix folding. According to RFC, lines can fold by having
34 // a CRLF and then a single white space character.
35 // We will allow it to be CRLF, CR or LF or any repeated sequence
36 // so long as there is a single white space character next.
37 //echo "Orig:<br><pre>$data</pre><br/><br/>\n";
38 $data = preg_replace ( "/[\r\n]+ /", "", $data );
39 $data = preg_replace ( "/[\r\n]+/", "\n", $data );
40 //echo "Data:<br><pre>$data</pre><p>";
42 // reflect the section where we are in the file:
43 // VEVENT, VTODO, VJORNAL, VFREEBUSY, VTIMEZONE
45 $substate = "none"; // reflect the sub section
46 $subsubstate = ""; // reflect the sub-sub section
51 $lines = explode ( "\n", $data );
52 for ( $n = 0; $n < count ( $lines ) && ! $error; $n++ ) {
56 // parser debugging code...
57 //echo "line = $line <br />";
58 //echo "state = $state <br />";
59 //echo "substate = $substate <br />";
60 //echo "subsubstate = $subsubstate <br />";
61 //echo "buff = " . htmlspecialchars ( $buff ) . "<br /><br />\n";
63 if ($state == "VEVENT") {
64 if ( ! empty ( $subsubstate ) ) {
65 if (preg_match("/^END:(.+)$/i", $buff, $match)) {
66 if ( $match[1] == $subsubstate ) {
69 } else if ( $subsubstate == "VALARM" &&
70 preg_match ( "/TRIGGER:(.+)$/i", $buff, $match ) ) {
71 // Example: TRIGGER;VALUE=DATE-TIME:19970317T133000Z
72 //echo "Set reminder to $match[1]<br />";
73 // reminder time is $match[1]
76 else if (preg_match("/^BEGIN:(.+)$/i", $buff, $match)) {
77 $subsubstate = $match[1];
79 // we suppose ":" is on the same line as property name, this can perhaps cause problems
80 else if (preg_match("/^SUMMARY.*:(.+)$/i", $buff, $match)) {
81 $substate = "summary";
82 $event[$substate] = $match[1];
83 } elseif (preg_match("/^DESCRIPTION:(.+)$/i", $buff, $match)) {
84 $substate = "description";
85 $event[$substate] = $match[1];
86 } elseif (preg_match("/^DESCRIPTION.*:(.+)$/i", $buff, $match)) {
87 $substate = "description";
88 $event[$substate] = $match[1];
89 } elseif (preg_match("/^CLASS.*:(.*)$/i", $buff, $match)) {
91 $event[$substate] = $match[1];
92 } elseif (preg_match("/^PRIORITY.*:(.*)$/i", $buff, $match)) {
93 $substate = "priority";
94 $event[$substate] = $match[1];
95 } elseif (preg_match("/^DTSTART.*:\s*(\d+T\d+Z?)\s*$/i", $buff, $match)) {
96 $substate = "dtstart";
97 $event[$substate] = $match[1];
98 } elseif (preg_match("/^DTSTART.*:\s*(\d+)\s*$/i", $buff, $match)) {
99 $substate = "dtstart";
100 $event[$substate] = $match[1];
101 } elseif (preg_match("/^DTEND.*:\s*(.*)\s*$/i", $buff, $match)) {
103 $event[$substate] = $match[1];
104 } elseif (preg_match("/^DURATION.*:(.+)\s*$/i", $buff, $match)) {
105 $substate = "duration";
107 if ( preg_match ( "/PT.*([0-9]+)H/", $match[1], $submatch ) )
108 $durH = $submatch[1];
109 if ( preg_match ( "/PT.*([0-9]+)M/", $match[1], $submatch ) )
110 $durM = $submatch[1];
111 $event[$substate] = $durH * 60 + $durM;
112 } elseif (preg_match("/^RRULE.*:(.+)$/i", $buff, $match)) {
114 $event[$substate] = $match[1];
115 } elseif (preg_match("/^EXDATE.*:(.+)$/i", $buff, $match)) {
116 $substate = "exdate";
117 $event[$substate] = $match[1];
118 } elseif (preg_match("/^CATEGORIES.*:(.+)$/i", $buff, $match)) {
119 $substate = "categories";
120 $event[$substate] = $match[1];
121 } elseif (preg_match("/^UID.*:(.+)$/i", $buff, $match)) {
123 $event[$substate] = $match[1];
124 } elseif (preg_match("/^END:VEVENT$/i", $buff, $match)) {
125 $state = "VCALENDAR";
128 if ($tmp_data = format_ical($event)) $ical_data[] = $tmp_data;
129 // clear out data for new event
132 // TODO: QUOTED-PRINTABLE descriptions
135 // TODO: This is not the best way to handle folded lines.
136 // We should fix the folding before we parse...
137 } elseif (preg_match("/^\s(\S.*)$/", $buff, $match)) {
138 if ($substate != "none") {
139 $event[$substate] .= $match[1];
141 $errormsg .= "iCal parse error on line $line:<br />$buff\n";
144 // For unsupported properties
148 } elseif ($state == "VCALENDAR") {
149 if (preg_match("/^BEGIN:VEVENT/i", $buff)) {
151 } elseif (preg_match("/^END:VCALENDAR/i", $buff)) {
153 } else if (preg_match("/^BEGIN:VTIMEZONE/i", $buff)) {
154 $state = "VTIMEZONE";
155 } else if (preg_match("/^BEGIN:VALARM/i", $buff)) {
158 } elseif ($state == "VTIMEZONE") {
159 // We don't do much with timezone info yet...
160 if (preg_match("/^END:VTIMEZONE$/i", $buff)) {
161 $state = "VCALENDAR";
163 } elseif ($state == "NONE") {
164 if (preg_match("/^BEGIN:VCALENDAR$/i", $buff))
165 $state = "VCALENDAR";
173 // Convert ical format (yyyymmddThhmmssZ) to epoch time
174 function icaldate_to_timestamp ($vdate, $plus_d = '0', $plus_m = '0',
178 $y = substr($vdate, 0, 4) + $plus_y;
179 $m = substr($vdate, 4, 2) + $plus_m;
180 $d = substr($vdate, 6, 2) + $plus_d;
181 $H = substr($vdate, 9, 2);
182 $M = substr($vdate, 11, 2);
183 $S = substr($vdate, 13, 2);
184 $Z = substr($vdate, 15, 1);
187 $TS = gmmktime($H,$M,$S,$m,$d,$y);
189 // Problem here if server in different timezone
190 $TS = mktime($H,$M,$S,$m,$d,$y);
197 // Put all ical data into import hash structure
198 function format_ical($event) {
200 // Start and end time
201 $fevent['StartTime'] = icaldate_to_timestamp($event['dtstart']);
202 if ($fevent['StartTime'] == '-1') return false;
203 if ( isset ( $event['dtend'] ) ) {
204 $fevent['EndTime'] = icaldate_to_timestamp($event['dtend']);
206 if ( isset ( $event['duration'] ) ) {
207 $fevent['EndTime'] = $fevent['StartTime'] + $event['duration'] * 60;
209 $fevent['EndTime'] = $fevent['StartTime'];
213 // Calculate duration in minutes
214 if ( isset ( $event['duration'] ) ) {
215 $fevent['Duration'] = $event['duration'];
216 } else if ( empty ( $fevent['Duration'] ) ) {
217 $fevent['Duration'] = ($fevent['EndTime'] - $fevent['StartTime']) / 60;
219 if ( $fevent['Duration'] == '1440' ) {
220 // All day event... nothing to do here :-)
221 } else if ( preg_match ( "/\d\d\d\d\d\d\d\d$/",
222 $event['dtstart'], $pmatch ) ) {
224 $fevent['Duration'] = 0;
225 $fevent['Untimed'] = 1;
227 if ( preg_match ( "/\d\d\d\d\d\d\d\d$/", $event['dtstart'],
228 $pmatch ) && preg_match ( "/\d\d\d\d\d\d\d\d$/", $event['dtend'],
229 $pmatch2 ) && $event['dtstart'] != $event['dtend'] ) {
230 $startTime = icaldate_to_timestamp($event['dtstart']);
231 $endTime = icaldate_to_timestamp($event['dtend']);
232 // Not sure... should this be untimed or allday?
233 if ( $endTime - $startTime == ( 3600 * 24 ) ) {
234 // They used a DTEND set to the next day to say this is an all day
235 // event. We will call this an untimed event.
236 $fevent['Duration'] = '0';
237 $fevent['Untimed'] = 1;
239 // Event spans multiple days. The EndTime actually represents
240 // the first day the event does _not_ take place. So,
241 // we need to back up one day since WebCalendar end date is the
242 // last day the event takes place.
243 $fevent['Repeat']['Interval'] = '1'; // 1 = daily
244 $fevent['Repeat']['Frequency'] = '1'; // 1 = every day
245 $fevent['Duration'] = '0';
246 $fevent['Untimed'] = 1;
247 $fevent['Repeat']['EndTime'] = $endTime - ( 24 * 3600 );
251 $fevent['Summary'] = $event['summary'];
252 if ( ! empty ( $event['description'] ) ) {
253 $fevent['Description'] = $event['description'];
255 $fevent['Description'] = $event['summary'];
257 if ( ! empty ( $event['class'] ) ) {
258 $fevent['Private'] = preg_match("/private|confidential/i",
259 $event['class']) ? '1' : '0';
261 $fevent['UID'] = $event['uid'];
266 if ( ! empty ( $event['rrule'] ) ) {
267 // first remove and EndTime that may have been calculated above
268 unset ( $fevent['Repeat']['EndTime'] );
270 //echo "RRULE line: $event[rrule] <br />\n";
271 $RR = explode ( ";", $event['rrule'] );
273 // create an associative array of key-value paris in $RR2[]
274 for ( $i = 0; $i < count ( $RR ); $i++ ) {
275 $ar = explode ( "=", $RR[$i] );
276 $RR2[$ar[0]] = $ar[1];
279 for ( $i = 0; $i < count ( $RR ); $i++ ) {
280 //echo "RR $i = $RR[$i] <br />";
281 if ( preg_match ( "/^FREQ=(.+)$/i", $RR[$i], $match ) ) {
282 if ( preg_match ( "/YEARLY/i", $match[1], $submatch ) ) {
283 $fevent['Repeat']['Interval'] = 5;
284 } else if ( preg_match ( "/MONTHLY/i", $match[1], $submatch ) ) {
285 $fevent['Repeat']['Interval'] = 2;
286 } else if ( preg_match ( "/WEEKLY/i", $match[1], $submatch ) ) {
287 $fevent['Repeat']['Interval'] = 2;
288 } else if ( preg_match ( "/DAILY/i", $match[1], $submatch ) ) {
289 $fevent['Repeat']['Interval'] = 1;
292 echo "Unsupported iCal FREQ value \"$match[1]\"<br />\n";
294 } else if ( preg_match ( "/^INTERVAL=(.+)$/i", $RR[$i], $match ) ) {
295 $fevent['Repeat']['Frequency'] = $match[1];
296 } else if ( preg_match ( "/^UNTIL=(.+)$/i", $RR[$i], $match ) ) {
297 // specifies an end date
298 $fevent['Repeat']['EndTime'] = icaldate_to_timestamp ( $match[1] );
299 } else if ( preg_match ( "/^COUNT=(.+)$/i", $RR[$i], $match ) ) {
300 // NOT YET SUPPORTED -- TODO
301 echo "Unsupported iCal COUNT value \"$RR[$i]\"<br />\n";
302 } else if ( preg_match ( "/^BYSECOND=(.+)$/i", $RR[$i], $match ) ) {
303 // NOT YET SUPPORTED -- TODO
304 echo "Unsupported iCal BYSECOND value \"$RR[$i]\"<br />\n";
305 } else if ( preg_match ( "/^BYMINUTE=(.+)$/i", $RR[$i], $match ) ) {
306 // NOT YET SUPPORTED -- TODO
307 echo "Unsupported iCal BYMINUTE value \"$RR[$i]\"<br />\n";
308 } else if ( preg_match ( "/^BYHOUR=(.+)$/i", $RR[$i], $match ) ) {
309 // NOT YET SUPPORTED -- TODO
310 echo "Unsupported iCal BYHOUR value \"$RR[$i]\"<br />\n";
311 } else if ( preg_match ( "/^BYMONTH=(.+)$/i", $RR[$i], $match ) ) {
312 // this event repeats during the specified months
313 $months = explode ( ",", $match[1] );
314 if ( count ( $months ) == 1 ) {
315 // Change this to a monthly event so we can support repeat by
316 // day of month (if needed)
317 // Frequency = 3 (by day), 4 (by date), 6 (by day reverse)
318 if ( ! empty ( $RR2['BYDAY'] ) ) {
319 if ( preg_match ( "/^-/", $RR2['BYDAY'], $junk ) )
320 $fevent['Repeat']['Interval'] = 6; // monthly by day reverse
322 $fevent['Repeat']['Interval'] = 3; // monthly by day
323 $fevent['Repeat']['Frequency'] = 12; // once every 12 months
325 // could convert this to monthly by date, but we will just
326 // leave it as yearly.
327 //$fevent['Repeat']['Interval'] = 4; // monthly by date
330 // WebCalendar does not support this
331 echo "Unsupported iCal BYMONTH value \"$match[1]\"<br />\n";
333 } else if ( preg_match ( "/^BYDAY=(.+)$/i", $RR[$i], $match ) ) {
334 $fevent['Repeat']['RepeatDays'] = rrule_repeat_days( $match[1] );
335 } else if ( preg_match ( "/^BYMONTHDAY=(.+)$/i", $RR[$i], $match ) ) {
336 $fevent['Repeat']['Frequency'] = 4; //monthlyByDate
337 //echo "Partially Supported iCal BYSETPOS value \"$RR[$i]\"<br />\n";
338 } else if ( preg_match ( "/^BYSETPOS=(.+)$/i", $RR[$i], $match ) ) {
339 // NOT YET SUPPORTED -- TODO
340 echo "Unsupported iCal BYSETPOS value \"$RR[$i]\"<br />\n";
344 // Repeating exceptions?
345 if ( ! empty ( $event['exdate'] ) && $event['exdate']) {
346 $fevent['Repeat']['Exceptions'] = array();
347 $EX = explode(",", $event['exdate']);
348 foreach ( $EX as $exdate ){
349 $fevent['Repeat']['Exceptions'][] = icaldate_to_timestamp($exdate);
357 // Figure out days of week for weekly repeats
358 function rrule_repeat_days($RA) {
359 $RA = explode(",", $RA );
361 $sun = $mon = $tue = $wed = $thu = $fri = $sat = 'n';
362 for ($i = 0; $i < $T; $i++) {
363 if ($RA[$i] == 'SU') {
365 } elseif ($RA[$i] == 'MO') {
367 } elseif ($RA[$i] == 'TU') {
369 } elseif ($RA[$i] == 'WE') {
371 } elseif ($RA[$i] == 'TH') {
373 } elseif ($RA[$i] == 'FR') {
375 } elseif ($RA[$i] == 'SA') {
379 return $sun.$mon.$tue.$wed.$thu.$fri.$sat;
383 // Calculate repeating ending time
384 function rrule_endtime($int,$freq,$start,$end) {
386 // if # then we have to add the difference to the start time
387 if (preg_match("/^#(.+)$/i", $end, $M)) {
389 $plus_d = $plus_m = $plus_y = '0';
392 } elseif ($int == '2') {
394 } elseif ($int == '3') {
396 } elseif ($int == '4') {
398 } elseif ($int == '5') {
400 } elseif ($int == '6') {
403 $endtime = icaldate_to_timestamp($start,$plus_d,$plus_m,$plus_y);
405 // if we have the enddate
407 $endtime = icaldate_to_timestamp($end);