forked from friendica/friendica-addons
389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
# SabreTooth VObject library
|
|
|
|
[![Build Status](https://secure.travis-ci.org/evert/sabre-vobject.png?branch=master)](http://travis-ci.org/evert/sabre-vobject)
|
|
|
|
The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545)
|
|
and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP.
|
|
The goal of the VObject library is to create a very complete library, with an easy to use API.
|
|
|
|
This project is a spin-off from [SabreDAV](http://code.google.com/p/sabredav/), where it has
|
|
been used for several years. The VObject library has 100% unittest coverage.
|
|
|
|
# Installation
|
|
|
|
VObject requires PHP 5.3, and should be installed using composer.
|
|
The general composer instructions can be found on the [composer website](http://getcomposer.org/doc/00-intro.md composer website).
|
|
|
|
After that, just declare the vobject dependency as follows:
|
|
|
|
```
|
|
"require" : {
|
|
"sabre/vobject" : "dev-master"
|
|
}
|
|
```
|
|
|
|
Then, run `composer.phar update` and you should be good. As soon as the first release is out, you should switch `dev-master` to `2.0.*` though.
|
|
|
|
# Usage
|
|
|
|
## Parsing
|
|
|
|
For our example, we will be using the following vcard:
|
|
|
|
```
|
|
BEGIN:VCARD
|
|
VERSION:3.0
|
|
PRODID:-//Sabre//Sabre VObject 2.0//EN
|
|
N:Planck;Max;;;
|
|
FN:Max Planck
|
|
EMAIL;TYPE=WORK:mplanck@example.org
|
|
item1.TEL;TYPE=CELL:(+49)3144435678
|
|
item1.X-ABLabel:Private cell
|
|
item2.TEL;TYPE=WORK:(+49)5554564744
|
|
item2.X-ABLabel:Work
|
|
END:VCARD
|
|
```
|
|
|
|
|
|
If we want to just print out Max' full name, you can just use property access:
|
|
|
|
|
|
```php
|
|
|
|
use Sabre\VObject;
|
|
|
|
$card = VObject\Reader::read($data);
|
|
echo $card->FN;
|
|
|
|
```
|
|
|
|
## Changing properties
|
|
|
|
Creating properties is pretty similar. If we like to add his birthday, we just
|
|
set the property:
|
|
|
|
```php
|
|
|
|
$card->BDAY = '1858-04-23';
|
|
|
|
```
|
|
|
|
Note that in the previous example, we're actually updating any existing BDAY that
|
|
may already exist. If we want to add a new property, without overwriting the previous
|
|
we can do this with the `add` method.
|
|
|
|
```php
|
|
|
|
$card->add('EMAIL','max@example.org');
|
|
|
|
```
|
|
|
|
## Parameters
|
|
|
|
If we want to also specify that this is max' home email addresses, we can do this with
|
|
a third parameter:
|
|
|
|
```
|
|
|
|
$card->add('EMAIL', 'max@example'org', array('type' => 'HOME'));
|
|
|
|
```
|
|
|
|
If we want to read out all the email addresses and their type, this would look something
|
|
like this:
|
|
|
|
```
|
|
foreach($card->EMAIL as $email) {
|
|
|
|
echo $email['TYPE'], ' - ', $email;
|
|
|
|
}
|
|
```
|
|
|
|
## Groups
|
|
|
|
In our example, you can see that the TEL properties are prefixed. These are 'groups' and
|
|
allow you to group multiple related properties together. The group can be any user-defined
|
|
name.
|
|
|
|
This particular example as generated by the OS X addressbook, which uses the `X-ABLabel`
|
|
to allow the user to specify custom labels for properties. OS X addressbook uses groups
|
|
to tie the label to the property.
|
|
|
|
The VObject library simply ignores the group if you don't specify it, so this will work:
|
|
|
|
```php
|
|
|
|
foreach($card->TEL as $tel) {
|
|
echo $tel, "\n";
|
|
}
|
|
```
|
|
|
|
But if you would like to target a specific group + property, this is possible too:
|
|
|
|
```php
|
|
|
|
echo $card->{'ITEM1.TEL'};
|
|
|
|
```
|
|
|
|
So if you would like to show all the phone numbers, along with their custom label, the
|
|
following syntax is used:
|
|
|
|
```
|
|
foreach($card->TEL as $tel) {
|
|
|
|
echo $card->{$tel->group . '.X-ABLABEL'}, ": ";
|
|
echo $tel, "\n";
|
|
|
|
}
|
|
```
|
|
|
|
## Serializing / Saving
|
|
|
|
If you want to generate your updated VObject, you can simply call the serialize() method.
|
|
|
|
```
|
|
|
|
echo $card->serialize();
|
|
|
|
```
|
|
|
|
## Components
|
|
|
|
iCalendar, unlike vCards always have sub-components. Where vCards are often just a flat
|
|
list, iCalendar objects tend to have a tree-like structure. For the following paragraphs,
|
|
we will use the following object as the example:
|
|
|
|
```
|
|
BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//Sabre//Sabre VObject 2.0//EN
|
|
BEGIN:VEVENT
|
|
SUMMARY:Curiosity landing
|
|
DTSTART:20120806T051439Z
|
|
LOCATION:Mars
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
```
|
|
|
|
Since events, tasks and journals are always in a sub component, this is also how we
|
|
access them.
|
|
|
|
```php
|
|
|
|
use Sabre\VObject;
|
|
|
|
$calendar = VObject\Reader::read($data);
|
|
echo $calendar->VEVENT->SUMMARY;
|
|
|
|
```
|
|
|
|
Adding components to a calendar is done with a factory method:
|
|
|
|
```php
|
|
|
|
$event = VObject\Component::create('VEVENT');
|
|
$calendar->add($event);
|
|
|
|
$event->SUMMARY = 'Curiosity launch';
|
|
$event->DTSTART = '20111126T150202Z';
|
|
$event->LOCATION = 'Cape Carnival';
|
|
|
|
```
|
|
|
|
By the way.. cloning also works as expected, as the entire structure is cloned along with it:
|
|
|
|
```php
|
|
|
|
$clonedEvent = clone $calendar->VEVENT[0];
|
|
$calendar->add($clonedEvent);
|
|
|
|
```
|
|
|
|
## Date and time handling
|
|
|
|
If you ever had to deal with iCalendar timezones, you know it can be complicated.
|
|
The way timezones are specified is flawed, which is something I may write an essay about some
|
|
day. VObject does its best to determine the correct timezone. Many standard formats
|
|
have been tested and verified, and special code has been implemented for special-casing
|
|
microsoft generated timezone information, and others.
|
|
|
|
To get a real php `DateTime` object, you can request this as follows:
|
|
|
|
```
|
|
$event = $calendar->VEVENT;
|
|
$start = $event->DTSTART->getDateTime();
|
|
echo $start->format(\DateTime::W3C);
|
|
```
|
|
|
|
To set the property with a DateTime object, you can use the following syntax:
|
|
|
|
```
|
|
$dateTime = new \DateTime('2012-08-07 23:53:00', new DateTimeZone('Europe/Amsterdam'));
|
|
$event->DTSTART->setDateTime($dateTime, VObject\Property\DateTime::DATE);
|
|
```
|
|
|
|
The second argument specifies the type of date you're setting. The following three
|
|
options exist:
|
|
|
|
1. `LOCAL` This is a floating time, with no timezone information. This basically specifies that the event happens in whatever the timezone's currently in. This would be encoded as `DTSTART;VALUE=DATE-TIME:20120807235300`
|
|
2. `UTC` This specifies that the time should be encoded as a UTC time. This is encoded as `DTSTART;VALUE=DATE-TIME:20120807205300Z`. Note the extra Z and the fact that it's two hours 'earlier'.
|
|
3. `LOCALTZ` specifies that it's supposed to be encoded in its local timezone. For example `DTSTART;VALUE=DATE-TIME;TZID=Europe/Amsterdam:20120807235300`.
|
|
4. `DATE` This is a date-only, and does not contain the time. In this case this would be encoded as `DTSTART;VALUE=DATE:20120807`.
|
|
|
|
A few important notes:
|
|
|
|
* When a `TZID` is specified, there should also be a matching `VTIMEZONE` object with all the timezone information. VObject cannot currently automatically embed this. However, in reality other clients seem to do fine without this information. Yet, for completeness, this will be added in the future.
|
|
* As mentioned, the timezone-determination process may sometimes fail. Report any issues you find, and I'll be quick to add workarounds!
|
|
|
|
## Recurrence rules
|
|
|
|
Recurrence rules allow events to recur, for example for a weekly meeting, or an anniversary.
|
|
This is done with the `RRULE` property. The `RRULE` property allows for a LOT of different
|
|
rules. VObject only implements the ones that actually appear in calendar software.
|
|
|
|
To read more about `RRULE` and all the options, check out [RFC5545](https://tools.ietf.org/html/rfc5545#section-3.8.5).
|
|
VObject supports the following options:
|
|
|
|
1. `UNTIL` for an end date.
|
|
2. `INTERVAL` for for example "every 2 days".
|
|
3. `COUNT` to stop recurring after x items.
|
|
4. `FREQ=DAILY` to recur every day, and `BYDAY` to limit it to certain days.
|
|
5. `FREQ=WEEKLY` to recur every week, `BYDAY` to expand this to multiple weekdays in every week and `WKST` to specify on which day the week starts.
|
|
6. `FREQ=MONTHLY` to recur every month, `BYMONTHDAY` to expand this to certain days in a month, `BYDAY` to expand it to certain weekdays occuring in a month, and `BYSETPOS` to limit the last two expansions.
|
|
7. `FREQ=YEARLY` to recur every year, `BYMONTH` to expand that to certain months in a year, and `BYDAY` and `BYWEEKDAY` to expand the `BYMONTH` rule even further.
|
|
|
|
VObject supports the `EXDATE` property for exclusions, but not yet the `RDATE` and `EXRULE`
|
|
properties. If you're interested in this, please file a github issue, as this will put it
|
|
on my radar.
|
|
|
|
This is a bit of a complex subject to go in excruciating detail. The
|
|
[RFC](https://tools.ietf.org/html/rfc5545#section-3.8.5) has a lot of examples though.
|
|
|
|
The hard part is not to write the RRULE, it is to expand them. The most complex and
|
|
hard-to-read code is hidden in this component. Dragons be here.
|
|
|
|
So, if we have a meeting every 2nd monday of the month, this would be specified as such:
|
|
|
|
```
|
|
BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:1102c450-e0d7-11e1-9b23-0800200c9a66
|
|
DTSTART:20120109T140000Z
|
|
RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=2
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
```
|
|
|
|
Note that normally it's not allowed to indent the object like this, but it does make
|
|
it easier to read. This is also the first time I added in a UID, which is required
|
|
for all VEVENT, VTODO and VJOURNAL objects!
|
|
|
|
To figure out all the meetings for this year, we can use the following syntax:
|
|
|
|
```php
|
|
use Sabre\VObject;
|
|
|
|
$calendar = VObject\Reader::read($data);
|
|
$calendar->expand(new DateTime('2012-01-01'), new DateTime('2012-12-31'));
|
|
```
|
|
|
|
What the expand method does, is look at its inner events, and expand the recurring
|
|
rule. Our calendar now contains 12 events. The first will have its RRULE stripped,
|
|
and every subsequent VEVENT has the correct meeting date and a `RECURRENCE-ID` set.
|
|
|
|
This results in something like this:
|
|
|
|
```
|
|
BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:1102c450-e0d7-11e1-9b23-0800200c9a66
|
|
DTSTART:20120109T140000Z
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:1102c450-e0d7-11e1-9b23-0800200c9a66
|
|
RECURRENCE-ID:20120213T140000Z
|
|
DTSTART:20120213T140000Z
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:1102c450-e0d7-11e1-9b23-0800200c9a66
|
|
RECURRENCE-ID:20120312T140000Z
|
|
DTSTART:20120312T140000Z
|
|
END:VEVENT
|
|
..etc..
|
|
END:VCALENDAR
|
|
```
|
|
|
|
To show the list of dates, we would do this as such:
|
|
|
|
```
|
|
foreach($calendar->VEVENT as $event) {
|
|
echo $event->DTSTART->getDateTime()->format(\DateTime::ATOM);
|
|
}
|
|
```
|
|
|
|
In a recurring event, single instances can also be overriden. VObject also takes these
|
|
into consideration. The reason we needed to specify a start and end-date, is because
|
|
some recurrence rules can be 'never ending'.
|
|
|
|
You should make sure you pick a sane date-range. Because if you pick a 50 year
|
|
time-range, for a daily recurring event; this would result in over 18K objects.
|
|
|
|
# Free-busy report generation
|
|
|
|
Some calendaring software can make use of FREEBUSY reports to show when people are
|
|
available.
|
|
|
|
You can automatically generate these reports from calendars using the `FreeBusyGenerator`.
|
|
|
|
Example based on our last event:
|
|
|
|
```
|
|
|
|
// We're giving it the calendar object. It's also possible to specify multiple objects,
|
|
// by setting them as an array.
|
|
//
|
|
// We must also specify a start and end date, because recurring events are expanded.
|
|
$fbGenerator = new VObject\FreeBusyGenerator(
|
|
new DateTime('2012-01-01'),
|
|
new DateTime('2012-12-31'),
|
|
$calendar
|
|
);
|
|
|
|
// Grabbing the report
|
|
$freebusy = $fbGenerator->result();
|
|
|
|
// The freebusy report is another VCALENDAR object, so we can serialize it as usual:
|
|
echo $freebusy->serialize();
|
|
```
|
|
|
|
The output of this script will look like this:
|
|
|
|
```
|
|
BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//Sabre//Sabre VObject 2.0//EN
|
|
CALSCALE:GREGORIAN
|
|
BEGIN:VFREEBUSY
|
|
DTSTART;VALUE=DATE-TIME:20111231T230000Z
|
|
DTEND;VALUE=DATE-TIME:20111231T230000Z
|
|
DTSTAMP;VALUE=DATE-TIME:20120808T131628Z
|
|
FREEBUSY;FBTYPE=BUSY:20120109T140000Z/20120109T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120213T140000Z/20120213T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120312T140000Z/20120312T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120409T140000Z/20120409T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120514T140000Z/20120514T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120611T140000Z/20120611T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120709T140000Z/20120709T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120813T140000Z/20120813T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20120910T140000Z/20120910T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20121008T140000Z/20121008T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20121112T140000Z/20121112T140000Z
|
|
FREEBUSY;FBTYPE=BUSY:20121210T140000Z/20121210T140000Z
|
|
END:VFREEBUSY
|
|
END:VCALENDAR
|
|
```
|