Location: PHPKode > projects > phlyMail Lite > phlymail/handlers/calendar/functions.php
<?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;
}
?>
Return current item: phlyMail Lite