From a628b10f3faaab7b6276a70250242ae2d70b8a91 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 20 Sep 2018 15:55:51 -0500 Subject: [PATCH] Add ical Library --- ical_library.php | 684 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 684 insertions(+) create mode 100644 ical_library.php diff --git a/ical_library.php b/ical_library.php new file mode 100644 index 0000000..a8bca6e --- /dev/null +++ b/ical_library.php @@ -0,0 +1,684 @@ +calURL = $calURL; + $this->fetchCalendar(); + $this->createCalendarFromRaw(); + } + } + + function fetchCalendar() { + $ch = curl_init($this->calURL); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $this->rawCalendar = curl_exec($ch); + curl_close($ch); + } + + function createCalendarFromRaw() { + $this->iCal = new ICal($this->rawCalendar); + } + + function getCalendar() { + return $this->iCal; + } + + function getTimezone() { + return new DateTimeZone($this->iCal->cal['VCALENDAR']['X-WR-TIMEZONE']); + } + + function getEvents() { + if(isset($this->iCal->cal['VEVENT'])) { + return $this->iCal->cal['VEVENT']; + } + return []; + } + + function getUpcomingEvents() { + $ret = []; + $earliest = new DateTime(date('Y-m-d H:i:s', strtotime('now'))); + $allEvents = $this->iCal->cal['VEVENT']; + foreach($allEvents as $anEvent) { + $tstDt = new DateTime(date('Y-m-d H:i:s', strtotime($anEvent['DTSTART']))); + if($tstDt <= $earliest) { continue; } + $ret[] = $anEvent; + } + return $ret; + } + + function getNextEvent() { + $earliest = new DateTime(date('Y-m-d H:i:s', strtotime('2016-04-21 17:00:00'))); + $dt = new DateTime(); + foreach($this->getEvents() as $anEv) { + $tstDt = new DateTime(date('Y-m-d H:i:s', strtotime($anEv['DTSTART']))); + // $tstDt is the _STARTING_ date of the event + if($tstDt <= $earliest) { continue; } + $diff = $dt->diff($tstDt); + if(!isset($nextDt)) { + $nextDt = $tstDt; + $ret = $anEv; + } else if($dt < $tstDt && $tstDt < $nextDt) { + $nextDt = $tstDt; + $ret = $anEv; + } + } + if(isset($ret)) { + return $ret; + } + return false; + } + + // getNextEventWithCurrent will get the next event, including ones that + // are occurring right now, but haven't ended yet. + function getNextEventWithCurrent() { + $dt = new DateTime(); + foreach($this->getEvents() as $anEv) { + $tstDt = new DateTime(date('Y-m-d H:i:s', strtotime($anEv['DTSTART']))); + // $tstDt is the _STARTING_ date of the event + $diff = $dt->diff($tstDt); + if(!isset($nextDt)) { + $nextDt = $tstDt; + } else if($dt < $tstDt && $tstDt < $nextDt) { + $nextDt = $tstDt; + } + } + } +} + +class ICal { + private $timezone = 'UTC'; + private $events = []; + private $recEvents = []; + + public function __construct($ICalString) { + if(!$ICalString) { + return false; + } + $lines = explode("\r\n", $ICalString); + if(stristr($lines[0], 'BEGIN:VCALENDAR') === false) { + return false; + } + + // First, fix all multiline values + $badIdx = []; + foreach($lines as $idx => &$line) { + $line = ltrim(str_replace('\,',',',$line)); + //$line = ltrim(str_replace('','
',$line)); + $add = $this->keyValueFromString($line); + if ($add === false) { + $badIdx[] = $idx; + } + } + + $newLines = []; + $lastGood = ''; + foreach($lines as $idx => $aLine) { + if(in_array($idx, $badIdx)) { + $lastGood .= $aLine; + } else { + if(!empty($lastGood)) { + $newLines[] = $lastGood; + $lastGood = ''; + } + $lastGood = $aLine; + } + } + $newLines[] = $lastGood; + $lines = $newLines; + + for($idx = 0; $idx < count($lines); $idx++) { + $line = $lines[$idx]; + if($line == 'BEGIN:VTIMEZONE') { + $i = $this->_parseFindNext('END:VTIMEZONE', $lines, $idx); + $this->timezone = $this->parseTimezone($i); + } + if($line == 'BEGIN:VEVENT') { + $i = $this->_parseFindNext('END:VEVENT', $lines, $idx); + if($i !== false) { + $newEvent = $this->parseEvent($i); + if($newEvent->isValid()) { + $this->events[] = $newEvent; + } + } + } + } + } + + public function getEvents() { + return $this->events; + } + + public function getEventsInRange($stDtTm, $endDtTm) { + $ret = []; + foreach($this->events as $anEv) { + + $evStTm = $anEv->getStartDttm(); + $evEndTm = $anEv->getEndDttm(); + if($anEv->getRecurFlag() && $evStTm < $endDtTm) { + $evLength = $evStTm->diff($evEndTm); + // Recurrences are defined by the original startDttm + $recurUntil = $anEv->getRecurUntil(); + if(!empty($recurUntil)) { + // So there is an end date + if($recurUntil < $stDtTm) { + // Past the recur until + continue; + } + } + $recurFreq = $anEv->getRecurFrequency(); + if(!empty($recurFreq)) { + switch($recurFreq) { + case 'WEEKLY': + $dow = $anEv->getRecurByDay(); + $newArr = []; + if(empty($dow)) { + // If 'recurByDay' is blank, just recur on the event start date + array_push($newArr, $evStTm->format('w')); + } else { + if(is_array($dow)) { + foreach($dow as &$ad) { + $ad = $this->shortDayToDOWNum($ad); + array_push($newArr, $ad); + } + } else { + $ad = $this->shortDayToDOWNum($dow); + array_push($newArr, $ad); + } + } + $dow = $newArr; + $evStDay = new DateTime($evStTm->format('Y-m-d')); + for($tstDt = new DateTime($stDtTm->format(DATE_RFC3339)); + $tstDt <= $endDtTm; + $tstDt->add(new DateInterval('P1D'))) { + if(!in_array($tstDt->format('w'), $dow)) { continue; } // Same day of week + if($tstDt < $evStDay) { continue; } // After the recurrence started + if(!empty($recurUntil) && $tstDt > $recurUntil) { continue; } // Before it ended + $recurInt = $anEv->getRecurInterval(); + if($recurInt != 0) { + // Figure out how many 'FREQ's have passed (weeks in this case) + // 7 Days in a Week, recurring every $recurInt weeks + $recurInt *= 7; + $daysSince = $tstDt->diff($evStTm)->days; + if($daysSince%$recurInt != 0) { + continue; + } + } + $recurCnt = $anEv->getRecurCount(); + if($recurCnt != 0) { + // Recurring weekly + $recurInt = 7; + $daysSince = $tstDt->diff($evStDay)->days + 1; + if($daysSince/$recurInt > $recurCnt) { + // Too many occurrences already + continue; + } + } + $tstDt->setTime($evStTm->format('H'), $evStTm->format('i'), $evStTm->format('s')); + // The recurrence appears to be valid, check the exception dates + if($anEv->isExceptionDate($tstDt)) { + // An exception for this date + continue; + } + $rEv = new ICalEvent(); + $rEv->setStartDttm(new DateTime($tstDt->format(DATE_RFC3339))); + $rEv->setEndDttm(new DateTime($tstDt->add($evLength)->format(DATE_RFC3339))); + $rEv->setDescription($anEv->getDescription()); + $rEv->setSummary($anEv->getSummary()); + $rEv->setLocation($anEv->getLocation()); + $rEv->setICalTag("RecurringCopy", "true"); + $ret[] = $rEv; + } + break; + case 'MONTHLY': + // TODO: TEST + $dom = $evStTm->format('d'); + for($tstDt = new DateTime($stDtTm->format(DATE_RFC3339)); + $tstDt <= $endDtTm; + $tstDt->add(new DateInterval('P1M'))) { + if($tstDt->format('d') != $dom) { continue; } // Same day of month + if($tstDt < $evStTm) { continue; } // After the recurrence started + if(!empty($recurUntil) && $tstDt > $recurUntil) { continue; } // Before it ended + // TODO: Check Recurrence Interval + $tstDt->setTime($evStTm->format('H'), $evStTm->format('i'), $evStTm->format('s')); + $evLength = $evStTm->diff($evEndTm); + $rEv = new ICalEvent(); + $rEv->setStartDttm(new DateTime($tstDt->format(DATE_RFC3339))); + $rEv->setEndDttm(new DateTime($tstDt->add($evLength)->format(DATE_RFC3339))); + $rEv->setDescription($anEv->getDescription()); + $rEv->setSummary($anEv->getSummary()); + $rEv->setLocation($anEv->getLocation()); + $rEv->setICalTag("RecurringCopy", "true"); + $ret[] = $rEv; + } + break; + case 'YEARLY': + // TODO: TEST + $doy = $evStTm->format('z'); + for($tstDt = new DateTime($stDtTm->format(DATE_RFC3339)); + $tstDt <= $endDtTm; + $tstDt->add(new DateInterval('P1Y'))) { + if($tstDt->format('z') != $doy) { continue; } // Same day of year + if($tstDt < $evStTm) { continue; } // After the recurrence started + if(!empty($recurUntil) && $tstDt > $recurUntil) { continue; } // Before it ended + // TODO: Check Recurrence Interval + $tstDt->setTime($evStTm->format('H'), $evStTm->format('i'), $evStTm->format('s')); + $evLength = $evStTm->diff($evEndTm); + $rEv = new ICalEvent(); + $rEv->setStartDttm(new DateTime($tstDt->format(DATE_RFC3339))); + $rEv->setEndDttm(new DateTime($tstDt->add($evLength)->format(DATE_RFC3339))); + $rEv->setDescription($anEv->getDescription()); + $rEv->setSummary($anEv->getSummary()); + $rEv->setLocation($anEv->getLocation()); + $rEv->setICalTag("RecurringCopy", "true"); + $ret[] = $rEv; + } + break; + case 'DAILY': + // TODO: TEST + for($tstDt = new DateTime($stDtTm->format(DATE_RFC3339)); + $tstDt <= $endDtTm; + $tstDt->add(new DateInterval('P1D'))) { + if($tstDt < $evStTm) { continue; } // After the recurrence started + if(!empty($recurUntil) && $tstDt > $recurUntil) { continue; } // Before it ended + // TODO: Check Recurrence Interval + $tstDt->setTime($evStTm->format('H'), $evStTm->format('i'), $evStTm->format('s')); + $evLength = $evStTm->diff($evEndTm); + $rEv = new ICalEvent(); + $rEv->setStartDttm(new DateTime($tstDt->format(DATE_RFC3339))); + $rEv->setEndDttm(new DateTime($tstDt->add($evLength)->format(DATE_RFC3339))); + $rEv->setDescription($anEv->getDescription()); + $rEv->setSummary($anEv->getSummary()); + $rEv->setLocation($anEv->getLocation()); + $rEv->setICalTag("RecurringCopy", "true"); + $ret[] = $rEv; + } + break; + case 'HOURLY': + // TODO: TEST + for($tstDt = new DateTime($stDtTm->format(DATE_RFC3339)); + $tstDt <= $endDtTm; + $tstDt->add(new DateInterval('PT1H'))) { + if($tstDt < $evStTm) { continue; } // After the recurrence started + if(!empty($recurUntil) && $tstDt > $recurUntil) { continue; } // Before it ended + // TODO: Check Recurrence Interval + $evLength = $evStTm->diff($evEndTm); + $rEv = new ICalEvent(); + $rEv->setStartDttm(new DateTime($tstDt->format(DATE_RFC3339))); + $rEv->setEndDttm(new DateTime($tstDt->add($evLength)->format(DATE_RFC3339))); + $rEv->setDescription($anEv->getDescription()); + $rEv->setSummary($anEv->getSummary()); + $rEv->setLocation($anEv->getLocation()); + $rEv->setICalTag("RecurringCopy", "true"); + $ret[] = $rEv; + } + break; + case 'MINUTELY': + // TODO: TEST + for($tstDt = new DateTime($stDtTm->format(DATE_RFC3339)); + $tstDt <= $endDtTm; + $tstDt->add(new DateInterval('PT1M'))) { + if($tstDt < $evStTm) { continue; } // After the recurrence started + if(!empty($recurUntil) && $tstDt > $recurUntil) { continue; } // Before it ended + // TODO: Check Recurrence Interval + $evLength = $evStTm->diff($evEndTm); + $rEv = new ICalEvent(); + $rEv->setStartDttm(new DateTime($tstDt->format(DATE_RFC3339))); + $rEv->setEndDttm(new DateTime($tstDt->add($evLength)->format(DATE_RFC3339))); + $rEv->setDescription($anEv->getDescription()); + $rEv->setSummary($anEv->getSummary()); + $rEv->setLocation($anEv->getLocation()); + $rEv->setICalTag("RecurringCopy", "true"); + $ret[] = $rEv; + } + break; + } + } + } else { + // Normal Event, just check the dates + // If any part of the event takes place in the range, append it + if(($evStTm >= $stDtTm && $evStTm < $endDtTm) + || ($evEndTm > $stDtTm && $evEndTm <= $endDtTm)) { + $ret[] = $anEv; + } + } + } + usort($ret, "_eventSortHelper"); + return $ret; + } + + + public function parseTimezone($lines) { + $defaultTZ = 'UTC'; + foreach($lines as $line) { + $line = trim($line); + if($line == "END:VTIMEZONE") { + break; + } + $add = $this->keyValueFromString($line); + if ($add === false) { + continue; + } + list($keyword, $value) = $add; + if($keyword == 'TZID') { + return $value; + } + } + return $defaultTZ; + } + + + // Parses and returns the first event out of $lines + public function parseEvent($lines) { + $newEvent = new ICalEvent(); + foreach($lines as $line) { + $line = trim($line); + if($line == 'END:VEVENT') { + break; + } + $add = $this->keyValueFromString($line); + if($add === FALSE) { + continue; + } + list($keyword, $value) = $add; + $newEvent->setICalTag($keyword, $value); + if(stristr($keyword, "DTSTART") + || stristr($keyword, "DTEND") + || stristr($keyword, "EXDATE")) { + $tz = 'UTC'; + if(stristr($keyword, ';')) { + $keywordPts = explode(';', $keyword); + $keyword = $keywordPts[0]; + $tzPts = $keywordPts[1]; + $tzPts = explode('=', $tzPts); + if($tzPts[0] == 'TZID') { + $tz = $tzPts[1]; + } + } + if($keyword == 'EXDATE') { + // Exception Dates (could be an array) + if(stristr($value, ',')) { + $exDates = explode(',', $value); + foreach($exDates as $exDt) { + $date = new DateTime($exDt, new DateTimeZone($tz)); + $date->setTimezone(new DateTimeZone($this->timezone)); + $newEvent->addExceptionDate($date); + } + } else { + $date = new DateTime($value, new DateTimeZone($tz)); + $date->setTimezone(new DateTimeZone($this->timezone)); + $newEvent->addExceptionDate($date); + } + } else { + $date = new DateTime($value, new DateTimeZone($tz)); + $date->setTimezone(new DateTimeZone($this->timezone)); + if($keyword == 'DTSTART') { + $newEvent->setStartDttm($date); + } else if($keyword == 'DTEND') { + $newEvent->setEndDttm($date); + } + } + continue; + } + switch($keyword) { + case 'RRULE': // Recurrence Rule + // http://www.kanzaki.com/docs/ical/rrule.html + $pts = explode(';', $value); + foreach($pts as $recRules) { + $recRulePts = explode('=', $recRules); + switch($recRulePts[0]) { + case 'FREQ': // (MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY) + $newEvent->setRecurFrequency($recRulePts[1]); + break; + case 'INTERVAL': // 1 out of every x of FREQ that it recurs + $newEvent->setRecurInterval($recRulePts[1]); + break; + case 'COUNT': // Number of times it recurs + $newEvent->setRecurCount($recRulePts[1]); + break; + case 'UNTIL': // Recur until this date + // TODO: Create a DateTime + $date = new DateTime($recRulePts[1], new DateTimeZone('UTC')); + $date->setTimezone(new DateTimeZone($this->timezone)); + $newEvent->setRecurUntil($date); + break; + case 'BYMONTH': // idx of months that it recurs (1 indexed) + $newEvent->setRecurByMonth($recRulePts[1]); + break; + case 'BYDAY': // (SU,MO,TU,WE,TH,FR,SA) + $newEvent->setRecurByDay($recRulePts[1]); + break; + case 'BYMONTHDAY': // idx of day of month, 1 indexed, <0 is from end + $newEvent->setRecurByMonthDay($recRulePts[1]); + break; + case 'BYYEARDAY': // idx of day of year + $newEvent->setRecurByYearDay($recRulePts[1]); + break; + case 'WKST': // Week Start? + $newEvent->setRecurWkSt($recRulePts[1]); + break; + } + } + break; + case 'DESCRIPTION': // Event Description + $newEvent->setDescription($value); + break; + case 'LOCATION': // Event Location + $newEvent->setLocation($value); + break; + case 'SUMMARY': // Event Summary + $newEvent->setSummary($value); + break; + } + } + return $newEvent; + } + + public function _parseFindNext($needle, $haystack, $start=0) { + $ret = []; + for($i = $start; $i < count($haystack); $i++) { + if(!empty(trim($haystack[$i]))) { + $ret[] = $haystack[$i]; + if($haystack[$i] == $needle) { + return $ret; + } + } + } + // Didn't find it. + return false; + } + + public function keyValueFromString($text) { + preg_match("/([^:]+)[:]([\w\W]*)/", $text, $matches); + if (count($matches) == 0) { + return false; + } + $matches = array_splice($matches, 1, 2); + return $matches; + } + + // shortDayToDOWNum takes a 'Recur By Day' value (SU,MO,TU,WE,TH,FR,SA) + public function shortDayToDOWNum($shortDay) { + switch($shortDay) { + case 'SU': return 0; + case 'MO': return 1; + case 'TU': return 2; + case 'WE': return 3; + case 'TH': return 4; + case 'FR': return 5; + case 'SA': return 6; + } + return -1; + } +} + +class ICalEvent { + private $summary = ''; + private $description = ''; + private $location = ''; + private $isRecurring = false; + private $_recurRules = []; + private $exceptionDates = []; + private $stDtTm; + private $endDtTm; + private $valid = false; + + private $ICalTags = []; + + public function __construct() { } + + public function setSummary($s) { $this->summary = $s; } + public function getSummary() { return $this->summary; } + + public function setDescription($s) { $this->description = $s; } + public function getDescription() { return $this->description; } + + public function setStartDttm($dttm) { $this->stDtTm = $dttm; } + public function getStartDttm() { return $this->stDtTm; } + + public function setEndDttm($dttm) { $this->endDtTm = $dttm; } + public function getEndDttm() { return $this->endDtTm; } + + public function setLocation($loc) { $this->location = $loc; } + public function getLocation() { return $this->location; } + + // getDuration returns a DateInterval for how long this event lasts + public function getDuration() { + return $this->stDtTm->diff($this->endDtTm); + } + + public function isValid() { + return ($this->stDtTm < $this->endDtTm); + } + + public function getRecurFlag() { + return $this->isRecurring; + } + + public function getRecurRules() { + return $this->_recurRules; + } + + public function getRecurRule($rule) { + if(isset($this->_recurRules[$rule])) { + return $this->_recurRules[$rule]; + } + return ''; + } + + // Frequency to recur + public function setRecurFrequency($fr) { + if($fr == 'MINUTELY' || $fr == 'HOURLY' || $fr == 'DAILY' + || $fr == 'WEEKLY' || $fr == 'MONTHLY' || $fr == 'YEARLY') { + $this->_recurRules['frequency'] = $fr; + $this->isRecurring = true; + return true; + } + return false; + } + public function getRecurFrequency() { + return $this->getRecurRule('frequency'); + } + + // One out of every $i of Frequency to occur + public function setRecurInterval($i) { + $this->_recurRules['interval'] = $i; + } + public function getRecurInterval() { + return $this->getRecurRule('interval'); + } + + // How many times it should recur + public function setRecurCount($i) { + $this->_recurRules['count'] = $i; + } + public function getRecurCount() { + return $this->getRecurRule('count'); + } + + // Date to stop recurring + public function setRecurUntil($dt) { + $this->_recurRules['until'] = $dt; + } + public function getRecurUntil() { + return $this->getRecurRule('until'); + } + + // array of 1-indexed months to recur on + public function setRecurByMonth($mos) { + $mos = is_array($mos)?$mos:[$mos]; + $this->_recurRules['byMonth'] = $mos; + } + public function getRecurByMonth() { + return $this->getRecurRule('byMonth'); + } + + // array of abbreviated days to recur on (SU,MO,TU,WE,TH,FR,SA) + public function setRecurByDay($dys) { + $dys = is_array($dys)?$dys:[$dys]; + $this->_recurRules['byDay'] = explode(',',$dys[0]); + } + public function getRecurByDay() { + return $this->getRecurRule('byDay'); + } + + // arary of 1-indexed day-of-months to recur on + public function setRecurByMonthDay($doms) { + $doms = is_array($doms)?$doms:[$doms]; + $this->_recurRules['byMonthDay'] = $doms; + } + public function getRecurByMonthDay() { + return $this->getRecurRule('byMonthDay'); + } + + // array of 1-indexed day-of-years to recur on + public function setRecurByYearDay($doys) { + $doys = is_array($doys)?$doys:[$doys]; + $this->_recurRules['byYearDay'] = $doys; + } + public function getRecurByYearDay() { + return $this->getRecurRule('byYearDay'); + } + + // Set the week start day for the recurrence + public function setRecurWkSt($wkst) { + $this->_recurRules['wkSt'] = $wkst; + } + public function getRecurWkSt() { + return $this->getRecurRule('wkSt'); + } + + public function setICalTag($key, $val) { + $this->ICalTags[$key] = $val; + } + + public function addExceptionDate($dy) { + array_push($this->exceptionDates, $dy); + } + + public function isExceptionDate($dy) { + foreach($this->exceptionDates as $exDt) { + if($dy == $exDt) { + return true; + } + } + return false; + } +} + +function _eventSortHelper($a, $b) { + if($a->getStartDttm() == $b->getStartDttm()) { + return 0; + } + return ($a->getStartDttm() > $b->getStartDttm())?1:-1; +} + +