From 3fcbd8e16fc8aa0e174fbc27eb02c298b270dbfc Mon Sep 17 00:00:00 2001 From: Patrick Schwarz Date: Tue, 20 Jan 2026 00:32:08 +0100 Subject: [PATCH] Move ICal module to include folder, export config to separate config file --- ICal/LICENSE | 15 ----- {ICal => include/ICal}/Event.php | 2 +- {ICal => include/ICal}/ICal.php | 102 ++++++++++++++++++++----------- include/config.sample.php | 11 ++++ index.php | 94 +++++++++++++--------------- 5 files changed, 121 insertions(+), 103 deletions(-) delete mode 100644 ICal/LICENSE rename {ICal => include/ICal}/Event.php (99%) rename {ICal => include/ICal}/ICal.php (97%) create mode 100644 include/config.sample.php diff --git a/ICal/LICENSE b/ICal/LICENSE deleted file mode 100644 index 0708674..0000000 --- a/ICal/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -The MIT License (MIT) -Copyright (c) 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, -modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ICal/Event.php b/include/ICal/Event.php similarity index 99% rename from ICal/Event.php rename to include/ICal/Event.php index 8845a66..354c7be 100644 --- a/ICal/Event.php +++ b/include/ICal/Event.php @@ -261,4 +261,4 @@ class Event return strtolower($inputSplit); } -} +} \ No newline at end of file diff --git a/ICal/ICal.php b/include/ICal/ICal.php similarity index 97% rename from ICal/ICal.php rename to include/ICal/ICal.php index 7098c3b..179923e 100644 --- a/ICal/ICal.php +++ b/include/ICal/ICal.php @@ -92,16 +92,18 @@ class ICal public $disableCharacterReplacement = false; /** - * With this being non-null the parser will ignore all events more than roughly this many days after now. + * If this value is an integer, the parser will ignore all events more than roughly this many days before now. + * If this value is a date, the parser will ignore all events occurring before this date. * - * @var integer|null + * @var \DateTimeInterface|integer|null */ public $filterDaysBefore; /** - * With this being non-null the parser will ignore all events more than roughly this many days before now. + * If this value is an integer, the parser will ignore all events more than roughly this many days after now. + * If this value is a date, the parser will ignore all events occurring after this date. * - * @var integer|null + * @var \DateTimeInterface|integer|null */ public $filterDaysAfter; @@ -523,8 +525,33 @@ class ICal // Ideally you would use `PHP_INT_MIN` from PHP 7 $php_int_min = -2147483648; - $this->windowMinTimestamp = is_null($this->filterDaysBefore) ? $php_int_min : (new \DateTime('now'))->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D'))->getTimestamp(); - $this->windowMaxTimestamp = is_null($this->filterDaysAfter) ? PHP_INT_MAX : (new \DateTime('now'))->add(new \DateInterval('P' . $this->filterDaysAfter . 'D'))->getTimestamp(); + $this->windowMinTimestamp = $php_int_min; + + if (!is_null($this->filterDaysBefore)) { + if (is_int($this->filterDaysBefore)) { + $this->windowMinTimestamp = (new \DateTime('now')) + ->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D')) + ->getTimestamp(); + } + + if ($this->filterDaysBefore instanceof \DateTimeInterface) { + $this->windowMinTimestamp = $this->filterDaysBefore->getTimestamp(); + } + } + + $this->windowMaxTimestamp = PHP_INT_MAX; + + if (!is_null($this->filterDaysAfter)) { + if (is_int($this->filterDaysAfter)) { + $this->windowMaxTimestamp = (new \DateTime('now')) + ->add(new \DateInterval('P' . $this->filterDaysAfter . 'D')) + ->getTimestamp(); + } + + if ($this->filterDaysAfter instanceof \DateTimeInterface) { + $this->windowMaxTimestamp = $this->filterDaysAfter->getTimestamp(); + } + } $this->shouldFilterByWindow = !is_null($this->filterDaysBefore) || !is_null($this->filterDaysAfter); @@ -532,7 +559,7 @@ class ICal $files = is_array($files) ? $files : array($files); foreach ($files as $file) { - if (!is_array($file) && $this->isFileOrUrl($file)) { + if (is_string($file) && $this->isFileOrUrl($file)) { $lines = $this->fileOrUrl($file); } else { $lines = is_array($file) ? $file : array($file); @@ -1144,9 +1171,9 @@ class ICal */ $pattern = '/^(?:TZID=)?([^:]*|".*")'; // [1]: Time zone $pattern .= ':?'; // Time zone delimiter - $pattern .= '([0-9]{8})'; // [2]: YYYYMMDD + $pattern .= '(\d{8})'; // [2]: YYYYMMDD $pattern .= 'T?'; // Time delimiter - $pattern .= '(?(?<=T)([0-9]{6}))'; // [3]: HHMMSS (filled if delimiter present) + $pattern .= '(?(?<=T)(\d{6}))'; // [3]: HHMMSS (filled if delimiter present) $pattern .= '(Z?)/'; // [4]: UTC flag preg_match($pattern, $icalDate, $date); @@ -1329,7 +1356,7 @@ class ICal } $allEventRecurrences = array(); - $eventKeysToRemove = array(); + $eventKeysToRemove = array(); foreach ($events as $key => $anEvent) { if (!isset($anEvent['RRULE']) || $anEvent['RRULE'] === '') { @@ -1353,7 +1380,7 @@ class ICal foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) { list($k, $v) = explode('=', $s); if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) { - $rrules[$k] = explode(',', $v); + $rrules[$k] = $v === '' ? array() : explode(',', $v); } else { $rrules[$k] = $v; } @@ -1395,7 +1422,7 @@ class ICal $interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $rrules['INTERVAL']; // Throw an error if this isn't an integer. - if (!is_int($this->defaultSpan)) { + if (!is_int($this->defaultSpan)) { // @phpstan-ignore function.alreadyNarrowedType trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE); } @@ -1567,7 +1594,7 @@ class ICal $clonedDateTime = clone $frequencyRecurringDateTime; $candidateDateTimes[] = $clonedDateTime->setDate( (int) $frequencyRecurringDateTime->format('Y'), - (int) $frequencyRecurringDateTime->format('m'), + (int) $frequencyRecurringDateTime->format('n'), $day ); } @@ -1600,7 +1627,7 @@ class ICal foreach ($monthDays as $day) { $matchingDays[] = $bymonthRecurringDatetime->setDate( (int) $frequencyRecurringDateTime->format('Y'), - (int) $bymonthRecurringDatetime->format('m'), + (int) $bymonthRecurringDatetime->format('n'), $day )->format('z') + 1; } @@ -1682,7 +1709,7 @@ class ICal } // Move forwards $interval $frequency. - $monthPreMove = $frequencyRecurringDateTime->format('m'); + $monthPreMove = (int) $frequencyRecurringDateTime->format('n'); $frequencyRecurringDateTime->modify("{$interval} {$this->frequencyConversion[$frequency]}"); // As noted in Example #2 on https://www.php.net/manual/en/datetime.modify.php, @@ -1690,7 +1717,7 @@ class ICal // expect. For instance: January 31st + 1 month == March 3rd (March 2nd on a leap // year.) The following code crudely rectifies this. if ($frequency === 'MONTHLY') { - $monthDiff = $frequencyRecurringDateTime->format('m') - $monthPreMove; + $monthDiff = (int) $frequencyRecurringDateTime->format('n') - $monthPreMove; if (($monthDiff > 0 && $monthDiff > $interval) || ($monthDiff < 0 && $monthDiff > $interval - 12)) { $frequencyRecurringDateTime->modify('-1 month'); @@ -1700,7 +1727,7 @@ class ICal // $monthDays is set in the DAILY frequency if the BYMONTHDAY stanza is present in // the RRULE. The variable only needs to be updated when we change months, so we // unset it here, prompting a recreation next iteration. - if (isset($monthDays) && $frequencyRecurringDateTime->format('m') !== $monthPreMove) { + if (isset($monthDays) && (int) $frequencyRecurringDateTime->format('n') !== $monthPreMove) { unset($monthDays); } } @@ -2050,7 +2077,7 @@ class ICal foreach ($monthDays as $day) { $matchingDays[] = $monthDateTime->setDate( (int) $initialDateTime->format('Y'), - (int) $monthDateTime->format('m'), + (int) $monthDateTime->format('n'), $day )->format('z') + 1; } @@ -2390,7 +2417,9 @@ class ICal foreach ($tza as $zone) { foreach ($zone as $item) { - $valid[$item['timezone_id']] = true; + if ($item['timezone_id'] !== null) { + $valid[$item['timezone_id']] = true; + } } } @@ -2460,7 +2489,7 @@ class ICal */ protected function removeUnprintableChars($data) { - return preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $data); + return preg_replace('/[\x00-\x1F\x7F]/u', '', $data); } /** @@ -2474,19 +2503,19 @@ class ICal { if (function_exists('mb_chr')) { return mb_chr($code); - } else { - if (($code %= 0x200000) < 0x80) { - $s = chr($code); - } elseif ($code < 0x800) { - $s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f); - } elseif ($code < 0x10000) { - $s = chr(0xe0 | $code >> 12) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f); - } else { - $s = chr(0xf0 | $code >> 18) . chr(0x80 | $code >> 12 & 0x3f) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f); - } - - return $s; } + + if (($code %= 0x200000) < 0x80) { + $s = chr($code); + } elseif ($code < 0x800) { + $s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f); + } elseif ($code < 0x10000) { + $s = chr(0xe0 | $code >> 12) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f); + } else { + $s = chr(0xf0 | $code >> 18) . chr(0x80 | $code >> 12 & 0x3f) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f); + } + + return $s; } /** @@ -2550,10 +2579,10 @@ class ICal { if (empty($event['EXDATE_array'])) { return array(); - } else { - $exdates = $event['EXDATE_array']; } + $exdates = $event['EXDATE_array']; + $output = array(); $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone()); @@ -2589,7 +2618,6 @@ class ICal * * @param string $value * @return boolean - * @throws \Exception */ public function isValidDate($value) { @@ -2630,7 +2658,7 @@ class ICal $options['http'] = array(); $options['http']['header'] = array(); - if ($this->httpBasicAuth === array() || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) { + if ($this->httpBasicAuth !== array() || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) { if ($this->httpBasicAuth !== array()) { $username = $this->httpBasicAuth['username']; $password = $this->httpBasicAuth['password']; @@ -2701,4 +2729,4 @@ class ICal return new \DateTimeZone($this->getDefaultTimeZone()); } -} +} \ No newline at end of file diff --git a/include/config.sample.php b/include/config.sample.php new file mode 100644 index 0000000..a6c5f49 --- /dev/null +++ b/include/config.sample.php @@ -0,0 +1,11 @@ +initString($ical_str); +if (!file_exists($cachefile) || (filemtime($cachefile) + $cachetime < time())) { + $context = stream_context_create(['http' => ['timeout' => 10]]); + $ical_str = @file_get_contents($ical_url, false, $context); + + if ($ical_str !== false && strpos($ical_str, 'BEGIN:VCALENDAR') !== false) { + file_put_contents($cachefile, $ical_str); + $ical->initString($ical_str); + } else { + if (file_exists($cachefile)) $ical->initFile($cachefile); + } } else { - $iCal->initFile($cachefile); + $ical->initFile($cachefile); } -//$iCal->initURL($ical); # Load calendar entries -$months = max(filter_input(INPUT_GET, 'period', FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 12))), 1); -$events = $iCal->sortEventsWithOrder($iCal->eventsFromInterval($months.' month')); - -$filter = filter_input(INPUT_GET, 'filter', FILTER_SANITIZE_SPECIAL_CHARS); +$period_val = filter_input(INPUT_GET, 'period', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 12]]) ?: 1; +$filter = filter_input(INPUT_GET, 'filter', FILTER_SANITIZE_SPECIAL_CHARS); // FILTER_UNSAFE_RAW +$events = $ical->sortEventsWithOrder($ical->eventsFromInterval($period_val . ' month')); $result = []; + foreach ($events as $event) { + $cat = $event->categories ?? ''; - if ($filter && strpos($event->categories, $filter) === false) { - continue; - } + if ($filter && stripos($cat, $filter) === false) continue; + if (stripos($cat, "hidden") !== false) continue; - if (isset($event->categories) && strpos($event->categories, "hidden")) { - continue; - } + $start = new DateTime($event->dtstart); + $end = new DateTime($event->dtend); + $uid = $event->uid; - $start = new DateTime($event->dtstart); - $end = new DateTime($event->dtend); - $uid = $event->uid; - - $interval = DateInterval::createFromDateString('1 day'); - $period = new DatePeriod($start, $interval, $end); - - foreach ($period as $dt) { - $date = $dt->format("Y-m-d"); - - $result[$date][$uid]["dtstart"] = $iCal->iCalDateToDateTime($event->dtstart_array[3])->format(DateTime::ATOM); - $result[$date][$uid]["dtend"] = $iCal->iCalDateToDateTime($event->dtend_array[3])->format(DateTime::ATOM); - $result[$date][$uid]["datestr"] = (isset($event->dtstart_array[0]["VALUE"]) && $event->dtstart_array[0]["VALUE"] == 'DATE')?'':$start->format('H:i'); - $result[$date][$uid]["summary"] = mb_strimwidth($event->summary, 0, 255, "..."); - $result[$date][$uid]["location"] = mb_strimwidth($event->location, 0, 255, "..."); - $result[$date][$uid]["description"] = mb_strimwidth($event->description, 0, 255, "..."); - if(isset($event->categories)) $result[$date][$uid]["categories"] = $event->categories; - } + $interval = new DateInterval('P1D'); + $period = new DatePeriod($start, $interval, $end); + foreach ($period as $dt) { + $date = $dt->format("Y-m-d"); + $result[$date][$uid] = [ + "dtstart" => $start->format(DateTime::ATOM), //$ical->iCalDateToDateTime($event->dtstart_array[3])->format(DateTime::ATOM); + "dtend" => $end->format(DateTime::ATOM), //$ical->iCalDateToDateTime($event->dtend_array[3])->format(DateTime::ATOM); + "datestr" => (isset($event->dtstart_array[0]["VALUE"]) && $event->dtstart_array[0]["VALUE"] == 'DATE') ? '' : $start->format('H:i'), + "summary" => mb_strimwidth($event->summary, 0, 255, "..."), + "location" => mb_strimwidth($event->location ?? '', 0, 255, "..."), + "description" => mb_strimwidth($event->description ?? '', 0, 255, "...") + ]; + if(!empty($cat)) $result[$date][$uid]["categories"] = $cat; + } } # Allow every page to load this json header("Access-Control-Allow-Origin: *"); -header("Content-Type: application/json"); - -echo json_encode($result, JSON_UNESCAPED_SLASHES); \ No newline at end of file +header("Content-Type: application/json; charset=utf-8"); +echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); \ No newline at end of file