iCalendar / vCard parser for PHP

I've just finished an iCalendar vCard parser for PHP. It's done almost completely with a 'natural' simplexml-like interface, so it should (hopefully) be just as easy to parse, and also modify iCalendar / vCard objects (ics/vcf files).

To install using pear, run the following:

  1. pear channel-discover pear.sabredav.org
  2. pear install sabredav/Sabre_VObject-alpha

Or download from pear.sabredav.org.

For testing, I used this iCalendar file: icalendartest.ics.

To load in an object, you use the Reader class:

  1. // Link to the correct path if you manually dowloaded the package
  2. include 'Sabre/VObject/includes.php';
  3.  
  4. // Reading an object
  5. $calendar = Sabre_VObject_Reader::read(file_get_contents('icalendartest.ics'));

iCalendar objects consist of components (VEVENT, VTODO, VTIMEZONE, etc), properties (SUMMARY, DESCRIPTION, DTSTART, etc) and parameters, which are to properties what attributes are to elements in XML. To show a listing of all events in a calendar, this snippet would work:

  1. echo "There are ", count($calendar->vevent), " events in this calendar\n";
  2.  
  3. // Looping through events
  4. foreach($calendar->vevent as $event) {
  5.  
  6. echo (string)$event->dtstart, ": ", $event->summary, "\n";
  7.  
  8. }

You can easily modify properties:

  1. $calendar->vevent[0]->description = "It's a birthday party";

Creating new objects uses the following syntax:

  1. $todo = new Sabre_VObject_Component('vtodo');
  2. $todo->summary = 'Take out the dog';
  3. $calendar->add($todo);

And to turn your newly modified calendar back into an ics file:

  1. file_put_contents('output.ics', $calendar->serialize());

Lastly, parameters are accessible through array-syntax:

  1. echo (string)$calendar->vevent[0]->dtstart['tzid'], "\n";

I had fun building this, I hope it's useful to you as well. It's 100% unittested, but bugs might still appear due to the complex nature of API. Use at your own risk :). This library will be part of the SabreDAV project, which is also where you can go for the source, report bugs or make suggestions.

Converting ICalendar to XML

I've started working on a CalDAV implementation, which also requires analysis of ICalendar (rfc 2554) objects.

ICalendar objects have properties, components (such as VEVENT, VTODO) and attributes. This is awfully familiar to XML. So instead of trying to come up with a complicated parser and object structure, I decided to just convert it to XML and use PHP's simplexml.

This is my current script:

  1. <?php
  2.  
  3. function iCalendarToXML($icalendarData) {
  4.  
  5. // Detecting line endings
  6. if (strpos($icalendarData,"\r\n")) $lb = "\r\n";
  7. elseif (strpos($icalendarData,"\n")) $lb = "\n";
  8. else $lb = "\r\n";
  9.  
  10. // Splitting up items per line
  11. $lines = explode($lb,$icalendarData);
  12.  
  13. // Properties can be folded over 2 lines. In this case the second
  14. // line will be preceeded by a space or tab.
  15. $lines2 = array();
  16. foreach($lines as $line) {
  17.  
  18. if ($line[0]==" " || $line[0]=="\t") {
  19. $lines2[count($lines2)-1].=substr($line,1);
  20. continue;
  21. }
  22.  
  23. $lines2[]=$line;
  24.  
  25. }
  26.  
  27. $xml = '<?xml version="1.0"?>' . "\n";
  28.  
  29. $spaces = 0;
  30. foreach($lines2 as $line) {
  31.  
  32. $matches = array();
  33. // This matches PROPERTYNAME;ATTRIBUTES:VALUE
  34. if (preg_match('/^([^:^;]*)(?:;([^:]*))?:(.*)$/',$line,$matches)) {
  35. $propertyName = strtoupper($matches[1]);
  36. $attributes = $matches[2];
  37. $value = $matches[3];
  38.  
  39. // If the line was in the format BEGIN:COMPONENT or END:COMPONENT, we need to special case it.
  40. if ($propertyName == 'BEGIN') {
  41. $xml.=str_repeat(" ",$spaces);
  42. $xml.='<' . strtoupper($value) . ">\n";
  43. $spaces+=2;
  44. continue;
  45. } elseif ($propertyName == 'END') {
  46. $spaces-=2;
  47. $xml.=str_repeat(" ",$spaces);
  48. $xml.='</' . strtoupper($value) . ">\n";
  49. continue;
  50. }
  51.  
  52. $xml.=str_repeat(" ",$spaces);
  53. $xml.='<' . $propertyName;
  54. if ($attributes) {
  55. // There can be multiple attributes
  56. $attributes = explode(';',$attributes);
  57. foreach($attributes as $att) {
  58.  
  59. list($attName,$attValue) = explode('=',$att,2);
  60. $xml.=' ' . $attName . '="' . htmlspecialchars($attValue) . '"';
  61.  
  62. }
  63. }
  64.  
  65. $xml.='>'. htmlspecialchars($value) . '</' . $propertyName . ">\n";
  66.  
  67. }
  68.  
  69. }
  70.  
  71. return $xml;
  72.  
  73. }
  74.  
  75. ?>

This will convert:

  1. BEGIN:VCALENDAR
  2. VERSION:2.0
  3. PRODID:-//Example Corp.//CalDAV Client//EN
  4. BEGIN:VTIMEZONE
  5. LAST-MODIFIED:20040110T032845Z
  6. TZID:US/Eastern
  7. BEGIN:DAYLIGHT
  8. DTSTART:20000404T020000
  9. RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
  10. TZNAME:EDT
  11. TZOFFSETFROM:-0500
  12. TZOFFSETTO:-0400
  13. END:DAYLIGHT
  14. BEGIN:STANDARD
  15. DTSTART:20001026T020000
  16. RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
  17. TZNAME:EST
  18. TZOFFSETFROM:-0400
  19. TZOFFSETTO:-0500
  20. END:STANDARD
  21. END:VTIMEZONE
  22. BEGIN:VEVENT
  23. DESCRIPTION:Hello Im evert
  24. Next line also
  25. Blabla
  26. ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
  27. ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
  28. DTSTAMP:20060206T001220Z
  29. DTSTART;TZID=US/Eastern:20060104T100000
  30. DURATION:PT1H
  31. LAST-MODIFIED:20060206T001330Z
  32. ORGANIZER:mailto:cyrus@example.com
  33. SEQUENCE:1
  34. STATUS:TENTATIVE
  35. SUMMARY:Event #3
  36. UID:DC6C50A017428C5216A2F1CD@example.com
  37. X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
  38. END:VEVENT
  39. END:VCALENDAR
To:
  1. <?xml version="1.0"?>
  2. <VCALENDAR>
  3. <VERSION>2.0</VERSION>
  4. <PRODID>-//Example Corp.//CalDAV Client//EN</PRODID>
  5. <VTIMEZONE>
  6. <LAST-MODIFIED>20040110T032845Z</LAST-MODIFIED>
  7. <TZID>US/Eastern</TZID>
  8. <DAYLIGHT>
  9. <DTSTART>20000404T020000</DTSTART>
  10. <RRULE>FREQ=YEARLY;BYDAY=1SU;BYMONTH=4</RRULE>
  11. <TZNAME>EDT</TZNAME>
  12. <TZOFFSETFROM>-0500</TZOFFSETFROM>
  13. <TZOFFSETTO>-0400</TZOFFSETTO>
  14. </DAYLIGHT>
  15. <STANDARD>
  16. <DTSTART>20001026T020000</DTSTART>
  17. <RRULE>FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10</RRULE>
  18. <TZNAME>EST</TZNAME>
  19. <TZOFFSETFROM>-0400</TZOFFSETFROM>
  20. <TZOFFSETTO>-0500</TZOFFSETTO>
  21. </STANDARD>
  22. </VTIMEZONE>
  23. <VEVENT>
  24. <DESCRIPTION>Hello Im evertNext line also Blabla</DESCRIPTION>
  25. <ATTENDEE PARTSTAT="ACCEPTED" ROLE="CHAIR">mailto:cyrus@example.com</ATTENDEE>
  26. <ATTENDEE PARTSTAT="NEEDS-ACTION">mailto:lisa@example.com</ATTENDEE>
  27. <DTSTAMP>20060206T001220Z</DTSTAMP>
  28. <DTSTART TZID="US/Eastern">20060104T100000</DTSTART>
  29. <DURATION>PT1H</DURATION>
  30. <LAST-MODIFIED>20060206T001330Z</LAST-MODIFIED>
  31. <ORGANIZER>mailto:cyrus@example.com</ORGANIZER>
  32. <SEQUENCE>1</SEQUENCE>
  33. <STATUS>TENTATIVE</STATUS>
  34. <SUMMARY>Event #3</SUMMARY>
  35. <UID>DC6C50A017428C5216A2F1CD@example.com</UID>
  36. <X-ABC-GUID>E1CX5Dr-0007ym-Hz@example.com</X-ABC-GUID>
  37. </VEVENT>
  38. </VCALENDAR>

I hope this is useful to anyone else.

 1

About

My name is Evert, and I've been writing semi-regularly on this blog since 2006.

I'm currently available for contract work.

more info.

Subscribe

Dropbox

Dropbox is a simple cross-platform online backup and sync application. The first 2GB of space is free, and both you and me get an extra 250MB extra space if you sign up through this link.