2011-05-20 19:04:25 +02:00
|
|
|
<?php
|
|
|
|
/*
|
|
|
|
+-------------------------------------------------------------------------+
|
|
|
|
| iCalendar functions for the Calendar Plugin |
|
2011-07-06 22:42:23 +02:00
|
|
|
| Version 0.3 beta |
|
2011-05-20 19:04:25 +02:00
|
|
|
| |
|
|
|
|
| This program is free software; you can redistribute it and/or modify |
|
|
|
|
| it under the terms of the GNU General Public License version 2 |
|
|
|
|
| as published by the Free Software Foundation. |
|
|
|
|
| |
|
|
|
|
| This program is distributed in the hope that it will be useful, |
|
|
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
|
|
| GNU General Public License for more details. |
|
|
|
|
| |
|
|
|
|
| You should have received a copy of the GNU General Public License along |
|
|
|
|
| with this program; if not, write to the Free Software Foundation, Inc., |
|
|
|
|
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
|
|
|
|
| |
|
|
|
|
+-------------------------------------------------------------------------+
|
|
|
|
| Author: Lazlo Westerhof <hello@lazlo.me> |
|
2011-07-29 09:19:18 +02:00
|
|
|
| Thomas Bruederli <roundcube@gmail.com> |
|
|
|
|
| Bogomil "Bogo" Shopov <shopov@kolabsys.com> |
|
2011-05-20 19:04:25 +02:00
|
|
|
+-------------------------------------------------------------------------+
|
|
|
|
*/
|
|
|
|
|
2011-07-29 09:19:18 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class to parse and build vCalendar (iCalendar) files
|
|
|
|
*
|
|
|
|
* Uses the Horde:iCalendar class for parsing. To install:
|
|
|
|
* > pear channel-discover pear.horde.org
|
|
|
|
* > pear install horde/Horde_Icalendar
|
|
|
|
*
|
|
|
|
*/
|
2011-05-20 19:04:25 +02:00
|
|
|
class calendar_ical
|
|
|
|
{
|
2011-07-27 17:42:45 +02:00
|
|
|
const EOL = "\r\n";
|
|
|
|
|
2011-05-20 19:04:25 +02:00
|
|
|
private $rc;
|
2011-07-29 09:19:18 +02:00
|
|
|
private $cal;
|
2011-08-06 16:12:34 +02:00
|
|
|
private $timezone = 'Z';
|
2011-05-20 19:04:25 +02:00
|
|
|
|
2011-07-29 09:19:18 +02:00
|
|
|
function __construct($cal)
|
2011-07-06 22:42:23 +02:00
|
|
|
{
|
2011-07-29 09:19:18 +02:00
|
|
|
$this->cal = $cal;
|
|
|
|
$this->rc = $cal->rc;
|
2011-08-06 16:12:34 +02:00
|
|
|
|
|
|
|
// compose timezone string
|
|
|
|
if ($cal->timezone) {
|
|
|
|
$hours = floor($cal->timezone);
|
|
|
|
$this->timezone = sprintf('%s%02d:%02d', ($hours >= 0 ? '+' : ''), $hours, ($cal->timezone - $hours) * 60);
|
|
|
|
}
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
2011-07-29 09:19:18 +02:00
|
|
|
|
2011-05-20 19:04:25 +02:00
|
|
|
/**
|
|
|
|
* Import events from iCalendar format
|
|
|
|
*
|
2011-07-29 09:19:18 +02:00
|
|
|
* @param string vCalendar input
|
2011-08-04 23:54:29 +02:00
|
|
|
* @param string Input charset (from envelope)
|
2011-07-29 09:19:18 +02:00
|
|
|
* @return array List of events extracted from the input
|
2011-05-20 19:04:25 +02:00
|
|
|
*/
|
2011-08-04 23:54:29 +02:00
|
|
|
public function import($vcal, $charset = RCMAIL_CHARSET)
|
2011-07-06 22:42:23 +02:00
|
|
|
{
|
2011-07-29 09:19:18 +02:00
|
|
|
// use Horde:iCalendar to parse vcalendar file format
|
|
|
|
require_once 'Horde/iCalendar.php';
|
|
|
|
|
2011-08-04 23:54:29 +02:00
|
|
|
// set target charset for parsed events
|
|
|
|
$GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
|
|
|
|
|
2011-07-29 09:19:18 +02:00
|
|
|
$parser = new Horde_iCalendar;
|
2011-08-04 23:54:29 +02:00
|
|
|
$parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
|
2011-07-29 09:19:18 +02:00
|
|
|
$events = array();
|
|
|
|
if ($data = $parser->getComponents()) {
|
|
|
|
foreach ($data as $comp) {
|
|
|
|
if ($comp->getType() == 'vEvent')
|
|
|
|
$events[] = $this->_to_rcube_format($comp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $events;
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
2011-07-29 09:19:18 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
|
|
|
|
*/
|
|
|
|
private function _to_rcube_format($ve)
|
|
|
|
{
|
|
|
|
$event = array(
|
|
|
|
'uid' => $ve->getAttributeDefault('UID'),
|
2011-07-29 17:51:04 +02:00
|
|
|
'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
|
2011-07-29 09:19:18 +02:00
|
|
|
'title' => $ve->getAttributeDefault('SUMMARY'),
|
|
|
|
'start' => $ve->getAttribute('DTSTART'),
|
|
|
|
'end' => $ve->getAttribute('DTEND'),
|
2011-07-29 17:51:04 +02:00
|
|
|
// set defaults
|
|
|
|
'free_busy' => 'busy',
|
|
|
|
'priority' => 1,
|
2011-07-29 09:19:18 +02:00
|
|
|
);
|
2011-07-29 17:51:04 +02:00
|
|
|
|
2011-07-29 09:19:18 +02:00
|
|
|
// check for all-day dates
|
|
|
|
if (is_array($event['start'])) {
|
2011-08-06 16:12:34 +02:00
|
|
|
// create timestamp at 00:00 in user's timezone
|
|
|
|
$event['start'] = strtotime(sprintf('%04d%02d%02dT000000%s', $event['start']['year'], $event['start']['month'], $event['start']['mday'], $this->timezone));
|
2011-07-29 09:19:18 +02:00
|
|
|
$event['allday'] = true;
|
|
|
|
}
|
|
|
|
if (is_array($event['end'])) {
|
2011-08-06 16:12:34 +02:00
|
|
|
$event['end'] = strtotime(sprintf('%04d%02d%02dT000000%s', $event['end']['year'], $event['end']['month'], $event['end']['mday'], $this->timezone)) - 60;
|
2011-07-29 09:19:18 +02:00
|
|
|
}
|
2011-08-06 16:12:34 +02:00
|
|
|
|
2011-07-29 17:51:04 +02:00
|
|
|
// map other attributes to internal fields
|
|
|
|
$_attendees = array();
|
|
|
|
foreach ($ve->getAllAttributes() as $attr) {
|
|
|
|
switch ($attr['name']) {
|
|
|
|
case 'ORGANIZER':
|
|
|
|
$organizer = array(
|
|
|
|
'name' => $attr['params']['CN'],
|
|
|
|
'email' => preg_replace('/^mailto:/', '', $attr['value']),
|
|
|
|
'role' => 'ORGANIZER',
|
|
|
|
'status' => 'ACCEPTED',
|
|
|
|
);
|
|
|
|
if (isset($_attendees[$organizer['email']])) {
|
|
|
|
$i = $_attendees[$organizer['email']];
|
|
|
|
$event['attendees'][$i]['role'] = $organizer['role'];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'ATTENDEE':
|
|
|
|
$attendee = array(
|
|
|
|
'name' => $attr['params']['CN'],
|
|
|
|
'email' => preg_replace('/^mailto:/', '', $attr['value']),
|
|
|
|
'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
|
|
|
|
'status' => $attr['params']['PARTSTAT'],
|
|
|
|
'rsvp' => $attr['params']['RSVP'] == 'TRUE',
|
|
|
|
);
|
|
|
|
if ($organizer && $organizer['email'] == $attendee['email'])
|
|
|
|
$attendee['role'] = 'ORGANIZER';
|
|
|
|
|
|
|
|
$event['attendees'][] = $attendee;
|
|
|
|
$_attendees[$attendee['email']] = count($event['attendees']) - 1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'TRANSP':
|
|
|
|
$event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'STATUS':
|
|
|
|
if ($attr['value'] == 'TENTATIVE')
|
|
|
|
$event['free_busy'] == 'tentative';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'PRIORITY':
|
|
|
|
if (is_numeric($attr['value'])) {
|
|
|
|
$event['priority'] = $attr['value'] <= 4 ? 2 /* high */ :
|
|
|
|
($attr['value'] == 5 ? 1 /* normal */ : 0 /* low */);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'RRULE':
|
|
|
|
// parse recurrence rule attributes
|
|
|
|
foreach (explode(';', $attr['value']) as $par) {
|
|
|
|
list($k, $v) = explode('=', $par);
|
|
|
|
$params[$k] = $v;
|
|
|
|
}
|
|
|
|
if ($params['UNTIL'])
|
|
|
|
$params['UNTIL'] = $ve->_parseDateTime($params['UNTIL']);
|
|
|
|
if (!$params['INTERVAL'])
|
|
|
|
$params['INTERVAL'] = 1;
|
|
|
|
|
|
|
|
$event['recurrence'] = $params;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'EXDATE':
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'DESCRIPTION':
|
|
|
|
case 'LOCATION':
|
|
|
|
$event[strtolower($attr['name'])] = $attr['value'];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'CLASS':
|
|
|
|
case 'X-CALENDARSERVER-ACCESS':
|
|
|
|
$sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
|
|
|
|
$event['sensitivity'] = $sensitivity_map[$attr['value']];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'X-MICROSOFT-CDO-BUSYSTATUS':
|
|
|
|
if ($attr['value'] == 'OOF')
|
|
|
|
$event['free_busy'] == 'outofoffice';
|
|
|
|
else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
|
|
|
|
$event['free_busy'] = strtolower($attr['value']);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2011-07-29 09:19:18 +02:00
|
|
|
|
2011-07-29 17:51:04 +02:00
|
|
|
// add organizer to attendees list if not already present
|
|
|
|
if ($organizer && !isset($_attendees[$organizer['email']]))
|
|
|
|
array_unshift($event['attendees'], $organizer);
|
|
|
|
|
|
|
|
// make sure the event has an UID
|
2011-07-29 09:19:18 +02:00
|
|
|
if (!$event['uid'])
|
|
|
|
$event['uid'] = $this->cal->$this->generate_uid();
|
|
|
|
|
|
|
|
return $event;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-05-20 19:04:25 +02:00
|
|
|
/**
|
|
|
|
* Export events to iCalendar format
|
|
|
|
*
|
2011-07-29 17:51:04 +02:00
|
|
|
* @param array Events as array
|
|
|
|
* @param string VCalendar method to advertise
|
|
|
|
* @param boolean Directly send data to stdout instead of returning
|
2011-05-20 19:04:25 +02:00
|
|
|
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
|
|
|
|
*/
|
2011-07-29 17:51:04 +02:00
|
|
|
public function export($events, $method = null, $write = false)
|
2011-07-06 22:42:23 +02:00
|
|
|
{
|
2011-05-20 19:04:25 +02:00
|
|
|
if (!empty($this->rc->user->ID)) {
|
2011-07-27 17:42:45 +02:00
|
|
|
$ical = "BEGIN:VCALENDAR" . self::EOL;
|
|
|
|
$ical .= "VERSION:2.0" . self::EOL;
|
|
|
|
$ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
|
|
|
|
$ical .= "CALSCALE:GREGORIAN" . self::EOL;
|
|
|
|
|
|
|
|
if ($method)
|
|
|
|
$ical .= "METHOD:" . strtoupper($method) . self::EOL;
|
2011-07-29 17:51:04 +02:00
|
|
|
|
|
|
|
if ($write) {
|
|
|
|
echo $ical;
|
|
|
|
$ical = '';
|
|
|
|
}
|
2011-07-06 22:42:23 +02:00
|
|
|
|
2011-05-20 19:04:25 +02:00
|
|
|
foreach ($events as $event) {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent = "BEGIN:VEVENT" . self::EOL;
|
|
|
|
$vevent .= "UID:" . self::escpape($event['uid']) . self::EOL;
|
|
|
|
$vevent .= "DTSTAMP:" . gmdate('Ymd\THis\Z', $event['changed'] ? $event['changed'] : time()) . self::EOL;
|
2011-07-29 09:19:18 +02:00
|
|
|
// correctly set all-day dates
|
|
|
|
if ($event['allday']) {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL;
|
|
|
|
$vevent .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 60) . self::EOL; // ends the next day
|
2011-07-29 09:19:18 +02:00
|
|
|
}
|
|
|
|
else {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL;
|
|
|
|
$vevent .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL;
|
2011-07-29 09:19:18 +02:00
|
|
|
}
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "SUMMARY:" . self::escpape($event['title']) . self::EOL;
|
|
|
|
$vevent .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL;
|
2011-07-27 17:42:45 +02:00
|
|
|
|
|
|
|
if (!empty($event['attendees'])){
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= $this->_get_attendees($event['attendees']);
|
2011-07-27 17:42:45 +02:00
|
|
|
}
|
|
|
|
|
2011-05-20 19:04:25 +02:00
|
|
|
if (!empty($event['location'])) {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "LOCATION:" . self::escpape($event['location']) . self::EOL;
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
2011-07-06 22:42:23 +02:00
|
|
|
if ($event['recurrence']) {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "RRULE:" . calendar::to_rrule($event['recurrence'], self::EOL) . self::EOL;
|
2011-07-06 22:42:23 +02:00
|
|
|
}
|
2011-05-20 19:04:25 +02:00
|
|
|
if(!empty($event['categories'])) {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL;
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
2011-07-06 22:42:23 +02:00
|
|
|
if ($event['sensitivity'] > 0) {
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
|
2011-07-06 22:42:23 +02:00
|
|
|
}
|
|
|
|
if ($event['alarms']) {
|
|
|
|
list($trigger, $action) = explode(':', $event['alarms']);
|
|
|
|
$val = calendar::parse_alaram_value($trigger);
|
|
|
|
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "BEGIN:VALARM\n";
|
|
|
|
if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
|
|
|
|
else $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
|
|
|
|
if ($action) $vevent .= "ACTION:" . self::escpape(strtoupper($action)) . self::EOL;
|
|
|
|
$vevent .= "END:VALARM\n";
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
2011-07-29 18:41:43 +02:00
|
|
|
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
|
2011-07-06 22:42:23 +02:00
|
|
|
|
2011-08-04 23:54:29 +02:00
|
|
|
if ($event['priority'] != 1) {
|
|
|
|
$vevent .= "PRIORITY:" . ($event['priority'] == 2 ? '1' : '9') . self::EOL;
|
|
|
|
}
|
|
|
|
|
2011-07-29 18:41:43 +02:00
|
|
|
if ($event['cancelled'])
|
|
|
|
$vevent .= "STATUS:CANCELLED" . self::EOL;
|
|
|
|
else if ($event['free_busy'] == 'tentative')
|
|
|
|
$vevent .= "STATUS:TENTATIVE" . self::EOL;
|
|
|
|
|
2011-07-06 22:42:23 +02:00
|
|
|
// TODO: export attachments
|
|
|
|
|
2011-07-29 17:51:04 +02:00
|
|
|
$vevent .= "END:VEVENT" . self::EOL;
|
|
|
|
|
|
|
|
if ($write)
|
|
|
|
echo rcube_vcard::rfc2425_fold($vevent);
|
|
|
|
else
|
|
|
|
$ical .= $vevent;
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
2011-07-06 22:42:23 +02:00
|
|
|
|
2011-07-27 17:42:45 +02:00
|
|
|
$ical .= "END:VCALENDAR" . self::EOL;
|
2011-07-29 17:51:04 +02:00
|
|
|
|
|
|
|
if ($write) {
|
|
|
|
echo $ical;
|
|
|
|
return true;
|
|
|
|
}
|
2011-05-20 19:04:25 +02:00
|
|
|
|
2011-07-27 17:42:45 +02:00
|
|
|
// fold lines to 75 chars
|
|
|
|
return rcube_vcard::rfc2425_fold($ical);
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|
|
|
|
}
|
2011-07-06 22:47:36 +02:00
|
|
|
|
|
|
|
private function escpape($str)
|
|
|
|
{
|
|
|
|
return preg_replace('/(?<!\\\\)([\:\;\,\\n\\r])/', '\\\$1', $str);
|
|
|
|
}
|
2011-07-19 17:11:33 +03:00
|
|
|
|
2011-07-27 17:42:45 +02:00
|
|
|
/**
|
2011-07-19 17:11:33 +03:00
|
|
|
* Construct the orginizer of the event.
|
|
|
|
* @param Array Attendees and roles
|
|
|
|
*
|
|
|
|
*/
|
2011-07-27 17:42:45 +02:00
|
|
|
private function _get_attendees($ats)
|
|
|
|
{
|
|
|
|
$organizer = "";
|
|
|
|
$attendees = "";
|
|
|
|
foreach ($ats as $at) {
|
|
|
|
if ($at['role']=="ORGANIZER") {
|
|
|
|
//I am an orginizer
|
|
|
|
$organizer .= "ORGANIZER;";
|
|
|
|
if (!empty($at['name']))
|
2011-07-27 18:55:51 +02:00
|
|
|
$organizer .= 'CN="' . $at['name'] . '"';
|
|
|
|
$organizer .= ":mailto:". $at['email'] . self::EOL;
|
2011-07-27 17:42:45 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
//I am an attendee
|
|
|
|
$attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
|
|
|
|
if (!empty($at['name']))
|
2011-07-27 18:55:51 +02:00
|
|
|
$attendees .= ';CN="' . $at['name'] . '"';
|
|
|
|
$attendees .= ":mailto:" . $at['email'] . self::EOL;
|
2011-07-27 17:42:45 +02:00
|
|
|
}
|
2011-07-19 17:11:33 +03:00
|
|
|
}
|
2011-07-27 17:42:45 +02:00
|
|
|
|
|
|
|
return $organizer . $attendees;
|
2011-07-19 17:11:33 +03:00
|
|
|
}
|
2011-07-27 17:42:45 +02:00
|
|
|
|
2011-05-20 19:04:25 +02:00
|
|
|
}
|