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; }