<?php
/**
* collection of a few functions for the calendar
* @package phlyMail Nahariya 4.0+ Default branch
* @subpackage Handler Calendar
* @copyright 2006-2010 phlyLabs, Berlin (http://phlylabs.de)
* @version 4.0.7 2010-08-16
*/
// Only valid within phlyMail
if (!defined('_IN_PHM_')) die();
/**
* Tries to parse a record from an iCal file
*
* @param string $data Raw entry of class VEVENT / VTODO, may contain VALARM, too
* @param VEVENT|VTODO Specify the type of the raw data for correct parsing
* @return array $return Record data matching format of calendar driver's add_event()
* @since 4.0.2
* @todo Find a better place for this function
*/
function parse_icaldata($data, $type = 'VEVENT')
{
// fix lousy Sunbird files.
$data = preg_replace('!\W+(:|;)!m', '\1', $data);
$return = array();
preg_match('!BEGIN:'.$type.'(.+)END:'.$type.'!s', $data, $event);
preg_match_all('!BEGIN:VALARM(.+)END:VALARM!Us', $data, $alarm, PREG_PATTERN_ORDER);
if (!isset($event[1])) return false; // Could not parse data
// Parse VEVENT data
// Unfolding of calendar data
$event[1] = preg_replace('!\r\n[\s\t]!', '', $event[1]);
foreach (array
(array('!^DTSTART(;.+)?:(.+)$!mi', 'start', 2)
,array('!^DTEND(;.+)?:(.+)$!mi', 'end', 2)
,array('!^DUE(;.+)?:(.+)$!mi', 'end', 2)
,array('!^COMPLETED(;.+)?:(.+)$!mi', 'completed_on', 2)
,array('!^LOCATION(;.+)?:(.+)$!mi', 'location', 2)
,array('!^DESCRIPTION(;.+)?:(.+)$!mi', 'description', 2)
,array('!^SUMMARY(;.+)?:(.+)$!mi', 'title', 2)
,array('!^STATUS(;.+)?:(.+)$!mi', 'status', 2)
,array('!^CATEGORIES(;.+)?:(.+)$!mi', 'type', 2)
,array('!^PERCENT-COMPLETE(;.+)?:(.+)$!mi', 'completion', 2)
,array('!^PRIORITY(;.+)?:(.+)$!mi', 'importance', 2)
,array('!^TRANSP(;.+)?:(.+)$!mi', 'opaque', 2)
) as $needle) {
if (preg_match($needle[0], $event[1], $found)) {
$return[$needle[1]] = trim($found[$needle[2]]);
} else {
$return[$needle[1]] = false;
}
}
// Unescape
foreach ($return as $k => $v) {
$return[$k] = str_replace(array('\N', '\n', '\,', '\:', '\;', '\"', '\\\\'), array(LF, LF, ',', ':', ';', '"', '\\'), $v);
}
$starts_unixtime = strtotime($return['start']);
$ends_unixtime = strtotime($return['end']);
$return['start'] = ($return['start']) ? date('Y-m-d H:i:s' , $starts_unixtime) : 0;
$return['end'] = ($return['end']) ? date('Y-m-d H:i:s' , $ends_unixtime) : 0;
$return['starts'] = $starts_unixtime;
$return['ends'] = $ends_unixtime;
// TRANSPARENT / OPAQUE
if (isset($return['opaque']) && strtoupper($return['opaque']) == 'TRANSPARENT') {
$return['opaque'] = 0;
} else {
$return['opaque'] = 1;
}
// For VTODO
$return['completed_on'] = ($return['completed_on']) ? date('Y-m-d H:i:s' , strtotime($return['completed_on'])) : 0;
if ($return['status']) {
$return['status'] = strtoupper($return['status']);
if ($return['status'] == 'TENTATIVE') {
$return['status'] = 10;
} elseif ($return['status'] == 'CONFIRMED') {
$return['status'] = 2;
} elseif ($return['status'] == 'CANCELLED') {
$return['status'] = 3;
} elseif ($return['status'] == 'NEEDS-ACTION') {
$return['status'] = 11;
} elseif ($return['status'] == 'COMPLETED') {
$return['status'] = 6;
} elseif ($return['status'] == 'IN-PROCESS') {
$return['status'] = 5;
} else $return['status'] = 0;
}
// iCal allows more than one category (hence the tag name), but phlyMail currently only allows exactly one
// This won't change in the near future, since it would totally break the UI and data structure
if ($return['type']) {
$return['type'] = strtoupper($return['type']);
$found = 0;
foreach ($GLOBALS['eventTypes'] as $k => $v) {
if (strstr($return['type'], strtoupper($v))) {
$found = 1;
$return['type'] = $k;
break;
}
}
if (!$found) $return['type'] = 0;
}
//
// Examination of RRULE: properties, which hold repetition rules
//
preg_match_all('!^RRULE:(.+)$!mi', $event, $rules, PREG_PATTERN_ORDER);
if (isset($rules[1]) && !empty($rules[1])) {
$return['repetitions'] = array();
foreach ($rules[1] as $raw_rule) {
$rule = array('type' => '-', 'until' => null, 'repeat' => 0);
if (preg_match('!\;UNTIL\=(.+)$!', $raw_rule, $found)) {
$rule['until'] = date('Y-m-d H:i:s' , strtotime($found[1]));
$raw_rule = str_replace($found[0], '', $raw_rule);
}
if (preg_match('!^FREQ\=YEARLY!', $raw_rule)) {
$rule['type'] = 'year';
} elseif (preg_match('!^FREQ\=MONTHLY!', $raw_rule)) {
$rule['type'] = 'month';
$day = (preg_match('!\;BYMONTHDAY\=(.+)!', $raw_rule, $found))
? intval($found[1])
: date('j', $return['starts']);
$rule['repeat'] = $day;
if (preg_match('!\;BYMONTH\=(.+)!', $raw_rule, $found)) {
$rule['extra'] = $found[1];
}
} elseif (preg_match('!^FREQ\=WEEKLY!', $raw_rule)) {
$rule['type'] = 'week';
$day = (preg_match('!\;BYDAY\=(.+)!', $raw_rule, $found)) // Cannot handle this sensibly right now
? intval($found[1])
: date('w', $return['starts']);
$rule['repeat'] = date('w', $return['starts']); // $day;
} elseif (preg_match('!^FREQ\=DAILY!', $raw_rule)) {
$rule['type'] = 'daily';
$rule['repeat'] = 0;
if (preg_match('!\;BYDAY\=(.+)!', $raw_rule, $found)) {
$rule['repeat'] = ical_parseByDay($found[1]);
}
if ($rule['repeat'] == 0) $rule['repeat'] = 127;
}
$return['repetitions'][] = $rule;
}
}
// END:RULE
//
// Examination of ATTENDEE: properties
//
preg_match_all('!^ATTENDEE(.+)$!mi', $event, $attendees, PREG_PATTERN_ORDER);
if (isset($attendees[1]) && !empty($attendees[1])) {
$return['attendees'] = array();
foreach ($attendees[1] as $raw_attendee) {
$attendee = array('name' => '', 'email' => '', 'role' => 'opt', 'type' => 'person', 'status' => 0, 'invited' => null);
if (preg_match('!\;CN\=(.+)(\;|\:|$)!U', $raw_attendee, $found)) {
$attendee['name'] = $found[1];
}
if (preg_match('!(\;|\:)MAILTO\:(.+)$!', $raw_attendee, $found)) {
$attendee['email'] = $found[2];
}
if (preg_match('!\;PARTSTAT=(ACCEPTED|DECLINED|TENTATIVE)!i', $raw_attendee, $found)) {
$found[1] = strtolower($found[1]);
if ($found[1] == 'accepted') $attendee['status'] = 1;
if ($found[1] == 'declined') $attendee['status'] = 2;
if ($found[1] == 'tentative') $attendee['status'] = 3;
}
if (preg_match('!\;ROLE=(CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT)!i', $raw_attendee, $found)) {
$found[1] = strtolower($found[1]);
if ($found[1] == 'chair') $attendee['role'] = 'chair';
if ($found[1] == 'req-participant') $attendee['role'] = 'req';
if ($found[1] == 'opt-participant') $attendee['role'] = 'opt';
if ($found[1] == 'non-participant') $attendee['role'] = 'non';
}
if (preg_match('!\;CUTYPE=(INDIVIDUAL|GROUP|RESOURCE|ROOM|UNKNOWN)!i', $raw_attendee, $found)) {
$found[1] = strtolower($found[1]);
if ($found[1] == 'individual') $attendee['type'] = 'person';
if ($found[1] == 'group') $attendee['type'] = 'group';
if ($found[1] == 'resource') $attendee['type'] = 'resource';
if ($found[1] == 'room') $attendee['type'] = 'room';
if ($found[1] == 'unknown') $attendee['type'] = 'unknown';
}
$return['attendees'][] = $attendee;
}
}
// END:ATTENDEE
// END:VEVENT
// Parse VALARM
if (!isset($alarm[1])) {
return $return; // No alarm info found
}
$return['reminders'] = array();
foreach ($alarm[1] as $raw_reminder) {
$reminder = array('trigger_value' => '');
foreach (array
(array('!^ACTION:(.+)$!mi', 'action', 1)
,array('!^SUMMARY:(.+)$!mi', 'text', 1)
,array('!^TRIGGER(;VALUE\=DURATION|VALUE\=DATE\-TIME)?:(.+)$!mi', 'trigger', 2)
,array('!^TRIGGER;RELATED\=END:(.+)$!mi', 'trigger_end', 1)
,array('!^TRIGGER;RELATED\=START:(.+)$!mi', 'trigger_start', 1)
,array('!^ATTENDEE(;.+)?:MAILTO:(.+)$!mi', 'email', 2)
) as $needle) {
if (preg_match($needle[0], $raw_reminder, $found)) {
$reminder[$needle[1]] = trim($found[$needle[2]]);
if ($needle[1] == 'trigger') $reminder['trigger_value'] = $found[1];
} else {
$reminder[$needle[1]] = false;
}
}
// We don't care much about the action types, since only the "EMAIL" one is useful to us
// The rest of the action types collide somewhat with the logic of phlyMail's calendar
if ($reminder['email'] && strtolower($reminder['action']) == 'email') {
$reminder['mailto'] = $reminder['email'];
}
if ($reminder['trigger'] && strtoupper($return['trigger_value']) == ';VALUE=DURATION') {
$reminder['trigger_start'] = $reminder['trigger'];
$reminder['trigger'] = false;
}
// Look more closely at the trigger thingy
if ($reminder['trigger'] && strtoupper($return['trigger_value']) == ';VALUE=DATE-TIME') {
$reminder['time'] = strtotime($reminder['trigger']);
if ($reminder['time'] <= $starts_unixtime) {
$reminder['mode'] = 's';
$reminder['time'] = $starts_unixtime - $reminder['time'];
} else {
$reminder['mode'] = 'e';
$reminder['time'] = $ends_unixtime - $reminder['time'];
}
} elseif ($reminder['trigger_end'] || $reminder['trigger_start']) {
$offset = 0;
$examine = ($reminder['trigger_end']) ? $reminder['trigger_end'] : $reminder['trigger_start'];
if (preg_match('!(\d+)W!', $examine, $found)) $offset += $found[1] * 604800;
if (preg_match('!(\d+)D!', $examine, $found)) $offset += $found[1] * 86400;
if (preg_match('!(\d+)H!', $examine, $found)) $offset += $found[1] * 3600;
if (preg_match('!(\d+)M!', $examine, $found)) $offset += $found[1] * 60;
if (preg_match('!(\d+)S!', $examine, $found)) $offset += $found[1] * 1;
$offset = (substr($examine, 0, 1) == '-') ? $offset*-1 : $offset;
$reminder['mode'] = ($reminder['trigger_end']) ? 'e' : 's';
$reminder['time'] = $offset;
}
$return['reminders'][] = $reminder;
}
// END:VALARM
return $return;
}
/**
* Escapes UTF-8 textual content like descriptions (summaries), which might contain
* newlines and stuff into an escaped form according to RFC 2445 so it can be safely
* put into an ICS file.
*
* @param string $text
* @return string
* @since 4.0.1
*/
function ical_escapetext($text)
{
return addcslashes($text, '",;\\');
}
function ical_foldline($text)
{
return wordwrap(str_replace(array(CRLF, LF), array('\n', '\n'), rtrim($text, "\r\n")), 75, "\r\n ", true);
}
/**
* Since PHP's date function cannot handle timestamps before 1970, we have to use
* MySQL's native DATETIME format and convert it to the format used by RFC 2445.
*
* @param string $date
* @return string
* @since 4.0.1
*/
function icalDT_fr_mysqlDT($date)
{
return preg_replace('!^(\d+)-(\d+)-(\d+)\s(\d+)\:(\d+)\:(\d+)$!', '\1\2\3T\4\5\6', $date);
}
/**
* Takes a comma separated list of week day abbr. and returns a bit field
*
* @param str $str
* @return int
* @since 4.0.3
*/
function ical_parseByDay($str)
{
$return = 0;
foreach (explode(',', $str) as $day) {
if ($day == 'SU' && !$return & 1) $return += 1;
if ($day == 'SA' && !$return & 2) $return += 2;
if ($day == 'FR' && !$return & 4) $return += 4;
if ($day == 'TH' && !$return & 8) $return += 8;
if ($day == 'WE' && !$return & 16) $return += 16;
if ($day == 'TU' && !$return & 32) $return += 32;
if ($day == 'MO' && !$return & 64) $return += 64;
}
return $return;
}
/**
* Output a given event / todo
* This function uses echo; to capture the output, use ob_start()
*
* @param array $data Event or task data from phlyMail's calendar DB
* @param VEVENT|VTODO $type Specify the data's type for correct output
* @return void
* @since 4.0.6
*/
function ical_echoEvent($data, $type = 'VEVENT')
{
if ($type != 'VEVENT' && $type != 'VTODO') return false;
$serverID = ((isset($_SERVER['SERVER_NAME']) && $_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'phlymail.local');
$wdayToIcal = array(0 => 'MO', 1 => 'TU', 2 => 'WE', 3 => 'TH', 4 => 'FR', 5 => 'SA', 6 => 'SU');
// These might confuse aggressive iCal parsers, so skip them in VEVENTs
if ($type == 'VEVENT' && $data['starts'] == '0000-00-00 00:00:00') return;
if ($type == 'VEVENT' && $data['ends'] == '0000-00-00 00:00:00') {
$data['ends'] = $data['starts'];
}
echo 'BEGIN:'.$type.CRLF;
// Event payload
echo 'UID:'.$data['id'].'-'.$data['uuid'].'@'.$serverID.CRLF;
echo ical_foldline('LOCATION:'.ical_escapetext($data['location'])).CRLF;
echo ical_foldline('SUMMARY:'.ical_escapetext($data['title'])).CRLF;
echo ical_foldline('DESCRIPTION:'.ical_escapetext($data['description'])).CRLF;
if ($data['starts']) {
echo 'DTSTART;TZID='.PHM_TIMEZONE.':'.icalDT_fr_mysqlDT($data['starts']).CRLF;
}
if ($data['ends']) {
if ($type == 'VEVENT') {
echo 'DTEND;TZID='.PHM_TIMEZONE.':'.icalDT_fr_mysqlDT($data['ends']).CRLF;
} elseif ($type == 'VTODO') {
echo 'DUE;TZID='.PHM_TIMEZONE.':'.icalDT_fr_mysqlDT($data['ends']).CRLF;
}
}
if ($data['type']) {
if (!isset($GLOBALS['eventTypes'][$data['type']])) $data['type'] = 0;
echo 'CATEGORIES:'.strtoupper($GLOBALS['eventTypes'][$data['type']]).CRLF;
}
if ($data['status']) {
if (!isset($GLOBALS['eventStatus'][$data['status']])) $data['status'] = 0;
echo 'STATUS:'.strtoupper($GLOBALS['eventStatus'][$data['status']]).CRLF;
}
if (isset($data['opaque'])) {
echo 'TRANSP:'.($data['opaque'] == 0 ? 'TRANSPARENT' : 'OPAQUE').CRLF;
}
if ($type == 'VTODO' && isset($data['completion'])) {
echo 'PERCENT-COMPLETE:'.intval($data['completion']).CRLF;
}
if ($type == 'VTODO' && isset($data['completed_on'])) {
echo 'COMPLETED;TZID='.PHM_TIMEZONE.':'.icalDT_fr_mysqlDT($data['completed_on']).CRLF;
}
if ($type == 'VTODO' && isset($data['importance'])) {
echo 'PRIORITY:'.intval($data['importance']).CRLF;
}
if (isset($GLOBALS['PHM_CAL_EX_ORGANIZER']) && $GLOBALS['PHM_CAL_EX_ORGANIZER']) {
echo 'ORGANIZER:MAILTO:'.$GLOBALS['PHM_CAL_EX_ORGANIZER'].CRLF;
}
if (!empty($data['attendees']) && !isset($GLOBALS['PHM_CAL_EX_NOATTENDEES'])) {
foreach ($data['attendees'] as $atte) {
echo 'ATTENDEE';
if ($atte['status'] == 1) {
echo ';PARTSTAT=ACCEPTED';
} elseif ($atte['status'] == 2) {
echo ';PARTSTAT=DECLINED';
} elseif ($atte['status'] == 3) {
echo ';PARTSTAT=TENTATIVE';
} else {
echo ';PARTSTAT=NEEDS-ACTION';
}
if ($atte['role'] == 'chair') {
echo ';ROLE=CHAIR';
} elseif ($atte['role'] == 'req') {
echo ';ROLE=REQ-PARTICIPANT';
} elseif ($atte['role'] == 'opt') {
echo ';ROLE=OPT-PARTICIPANT';
} elseif ($atte['role'] == 'non') {
echo ';ROLE=NON-PARTICIPANT';
}
if ($atte['type'] == 'person') {
echo ';CUTYPE=INDIVIDUAL';
} elseif ($atte['type'] == 'group') {
echo ';CUTYPE=GROUP';
} elseif ($atte['type'] == 'resource') {
echo ';CUTYPE=RESOURCE';
} elseif ($atte['type'] == 'room') {
echo ';CUTYPE=ROOM';
} elseif ($atte['type'] == 'unknown') {
echo ';CUTYPE=UNKNOWN';
}
echo ';RSVP='.(!is_null($atte['invited']) ? 'TRUE' : 'FALSE');
if ($atte['name']) echo ';CN='.ical_escapetext($atte['name']);
echo ':MAILTO:'.($atte['email'] ? $atte['email'] : 'none').CRLF;
}
}
if (!empty($data['repetitions'])) {
foreach ($data['repetitions'] as $rep) {
if ($rep['type'] == '-') continue;
$repUntil = (isset($rep['repeated_until']) && $rep['repeated_until']) ? ';UNTIL='.icalDT_fr_mysqlDT($rep['until']) : '';
if ($rep['type'] == 'year') echo 'RRULE:FREQ=YEARLY'.$repUntil.CRLF;
if ($rep['type'] == 'month') {
echo 'RRULE:FREQ=MONTHLY';
if (strlen($rep['extra'])) {
echo ';BYMONTH='.$rep['extra']; // Is comma separated list anyway
}
echo ';BYMONTHDAY='.$rep['repeat'].$repUntil.CRLF;
}
if ($rep['type'] == 'week') echo 'RRULE:FREQ=WEEKLY;BYDAY='.$wdayToIcal[$rep['repeat']].$repUntil.CRLF;
if ($rep['type'] == 'daily') {
$days = array();
if ($rep['repeat'] & 1) $days[] = $wdayToIcal[6];
if ($rep['repeat'] & 2) $days[] = $wdayToIcal[5];
if ($rep['repeat'] & 4) $days[] = $wdayToIcal[4];
if ($rep['repeat'] & 8) $days[] = $wdayToIcal[3];
if ($rep['repeat'] & 16) $days[] = $wdayToIcal[2];
if ($rep['repeat'] & 32) $days[] = $wdayToIcal[1];
if ($rep['repeat'] & 64) $days[] = $wdayToIcal[0];
echo 'RRULE:FREQ=DAILY;BYDAY='.implode(',', $days).$repUntil.CRLF;
}
}
}
if (!empty($data['reminders'])) {
foreach ($data['reminders'] as $rem) {
if ($rem['mode'] == '-') continue;
echo 'BEGIN:VALARM'.CRLF;
echo 'TRIGGER;RELATED='.($rem['mode'] == 'e' ? 'END' : 'START').':';
if ($rem['time'] >= 604800 && (intval($rem['time'] / 604800) == $rem['time'] / 604800)) {
echo '-P'.($rem['time'] / 604800).'W';
} elseif ($rem['time'] >= 86400 && (intval($rem['time'] / 86400) == $rem['time'] / 86400)) {
echo '-P'.($rem['time'] / 86400).'D';
} elseif ($rem['time'] >= 3600 && (intval($rem['time'] / 3600) == $rem['time'] / 3600)) {
echo '-PT'.($rem['time'] / 3600).'H';
} elseif ($rem['time'] >= 60 && (intval($rem['time'] / 60) == $rem['time'] / 60)) {
echo '-PT'.($rem['time'] / 60).'M';
} else {
echo 'PT'.intval($rem['time']).'S';
}
echo CRLF;
if ($rem['mailto'] && !isset($GLOBALS['PHM_CAL_EX_NOATTENDEES'])) {
echo 'ACTION:EMAIL'.CRLF.'ATTENDEE:MAILTO:'.$rem['mailto'].CRLF;
} else {
echo 'ACTION:DISPLAY'.CRLF;
}
if ($rem['text']) {
echo ical_foldline('SUMMARY:'.ical_escapetext($rem['text'])).CRLF;
} elseif ($data['title']) {
echo ical_foldline('SUMMARY:'.ical_escapetext($data['title'])).CRLF;
} elseif ($data['description']) {
echo ical_foldline('DESCRIPTION:'.ical_escapetext($data['description'])).CRLF;
} else {
echo 'DESCRIPTION:Reminder'.CRLF;
}
echo 'END:VALARM'.CRLF;
}
}
// Mandatory appendix
echo 'END:'.$type.CRLF;
}
?>