Revert "upgrade icalparser from e6a3a285cf6e239236a40153a66b67b940220b43 to cae1631b9496a7415ef960b0dff1cd0f39fe3135"

This reverts commit e3db4803e0.
This commit is contained in:
root 2023-01-15 01:55:56 +01:00
parent a90a7b111d
commit 2395e88e36
31 changed files with 876 additions and 1328 deletions

View file

@ -1,54 +0,0 @@
<?php
namespace om;
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
class EventsList extends \ArrayObject {
/**
* Return array of Events
*
* @return array
*/
public function getArrayCopy(): array {
return array_values(parent::getArrayCopy());
}
/**
* Return sorted EventList (the newest dates are first)
*
* @return $this
*/
public function sorted(): EventsList {
$this->uasort(static function ($a, $b): int {
if ($a['DTSTART'] === $b['DTSTART']) {
return 0;
}
return ($a['DTSTART'] < $b['DTSTART']) ? -1 : 1;
});
return $this;
}
/**
* Return reversed sorted EventList (the oldest dates are first)
*
* @return $this
*/
public function reversed(): EventsList {
$this->uasort(static function ($a, $b): int {
if ($a['DTSTART'] === $b['DTSTART']) {
return 0;
}
return ($a['DTSTART'] > $b['DTSTART']) ? -1 : 1;
});
return $this;
}
}

View file

@ -1,11 +1,6 @@
<?php
namespace om;
use DateTime;
use DateTimeZone;
use Exception;
/**
* Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Freq.php)
*
@ -35,64 +30,50 @@ use Exception;
* @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
*/
class Freq {
protected array $weekdays = [
'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday',
protected $weekdays = [
'MO' => 'monday', 'TU' => 'tuesday', 'WE' => 'wednesday', 'TH' => 'thursday', 'FR' => 'friday', 'SA' => 'saturday',
'SU' => 'sunday'
];
protected array $knownRules = [
'month',
'weekno',
'day',
'monthday',
'yearday',
'hour',
'minute',
protected $knownRules = [
'month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'
]; //others : 'setpos', 'second'
protected $ruleModifiers = ['wkst'];
protected $simpleMode = true;
protected array $ruleModifiers = ['wkst'];
protected bool $simpleMode = true;
protected $rules = ['freq' => 'yearly', 'interval' => 1];
protected $start = 0;
protected $freq = '';
protected array $rules = ['freq' => 'yearly', 'interval' => 1];
protected int $start = 0;
protected string $freq = '';
protected array $excluded; //EXDATE
protected array $added; //RDATE
protected $excluded; //EXDATE
protected $added; //RDATE
protected $cache; // getAllOccurrences()
/**
* Constructs a new Frequency-rule
* Constructs a new Freqency-rule
*
* @param string|array $rule
* @param int $start Unix-timestamp (important : Need to be the start of Event)
* @param array $excluded of int (timestamps), see EXDATE documentation
* @param array $added of int (timestamps), see RDATE documentation
* @throws Exception
* @param $rule string
* @param $start int Unix-timestamp (important : Need to be the start of Event)
* @param $excluded array of int (timestamps), see EXDATE documentation
* @param $added array of int (timestamps), see RDATE documentation
*/
public function __construct($rule, int $start, array $excluded = [], array $added = []) {
public function __construct($rule, $start, $excluded = [], $added = []) {
$this->start = $start;
$this->excluded = [];
$rules = [];
foreach ($rule as $k => $v) {
foreach ($rule AS $k => $v) {
$this->rules[strtolower($k)] = $v;
}
if (isset($this->rules['until']) && is_string($this->rules['until'])) {
$this->rules['until'] = strtotime($this->rules['until']);
} elseif ($this->rules['until'] instanceof DateTime) {
} else if ($this->rules['until'] instanceof \DateTime) {
$this->rules['until'] = $this->rules['until']->getTimestamp();
}
$this->freq = strtolower($this->rules['freq']);
foreach ($this->knownRules as $rule) {
foreach ($this->knownRules AS $rule) {
if (isset($this->rules['by' . $rule])) {
if ($this->isPrerule($rule, $this->freq)) {
$this->simpleMode = false;
@ -135,34 +116,95 @@ class Freq {
$this->added = $added;
}
private function isPrerule(string $rule, string $freq): bool {
if ($rule === 'year') {
return false;
}
if ($rule === 'month' && $freq === 'yearly') {
return true;
}
if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday'])) {
return true;
}
// TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4..
if ($rule === 'yearday' && $freq === 'yearly') {
return true;
}
if ($rule === 'weekno' && $freq === 'yearly') {
return true;
}
if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly'])) {
return true;
}
if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily'])) {
return true;
}
if ($rule === 'minute') {
return true;
/**
* Returns all timestamps array(), build the cache if not made before
*
* @return array
*/
public function getAllOccurrences() {
if (empty($this->cache)) {
$cache = [];
//build cache
$next = $this->firstOccurrence();
while ($next) {
$cache[] = $next;
$next = $this->findNext($next);
}
if (!empty($this->added)) {
$cache = array_unique(array_merge($cache, $this->added));
asort($cache);
}
$this->cache = $cache;
}
return false;
return $this->cache;
}
/**
* Returns the previous (most recent) occurrence of the rule from the
* given offset
*
* @param int $offset
* @return int
*/
public function previousOccurrence($offset) {
if (!empty($this->cache)) {
$t2 = $this->start;
foreach ($this->cache as $ts) {
if ($ts >= $offset)
return $t2;
$t2 = $ts;
}
} else {
$ts = $this->start;
while (($t2 = $this->findNext($ts)) < $offset) {
if ($t2 == false) {
break;
}
$ts = $t2;
}
}
return $ts;
}
/**
* Returns the next occurrence of this rule after the given offset
*
* @param int $offset
* @return int
*/
public function nextOccurrence($offset) {
if ($offset < $this->start)
return $this->firstOccurrence();
return $this->findNext($offset);
}
/**
* Finds the first occurrence of the rule.
*
* @return int timestamp
*/
public function firstOccurrence() {
$t = $this->start;
if (in_array($t, $this->excluded))
$t = $this->findNext($t);
return $t;
}
/**
* Finds the absolute last occurrence of the rule from the given offset.
* Builds also the cache, if not set before...
*
* @return int timestamp
*/
public function lastOccurrence() {
//build cache if not done
$this->getAllOccurrences();
//return last timestamp in cache
return end($this->cache);
}
/**
@ -185,16 +227,14 @@ class Freq {
* If no new timestamps were found in the period, we try in the
* next period
*
* @param int $offset
* @return int|bool
* @throws Exception
* @param int $offset
* @return int
*/
public function findNext(int $offset) {
public function findNext($offset) {
if (!empty($this->cache)) {
foreach ($this->cache as $ts) {
if ($ts > $offset) {
if ($ts > $offset)
return $ts;
}
}
}
@ -202,7 +242,8 @@ class Freq {
//make sure the offset is valid
if ($offset === false || (isset($this->rules['until']) && $offset > $this->rules['until'])) {
if ($debug) printf("STOP: %s\n", date('r', $offset));
if ($debug) echo 'STOP: ' . date('r', $offset) . "\n";
return false;
}
@ -210,7 +251,7 @@ class Freq {
//set the timestamp of the offset (ignoring hours and minutes unless we want them to be
//part of the calculations.
if ($debug) printf("O: %s\n", date('r', $offset));
if ($debug) echo 'O: ' . date('r', $offset) . "\n";
$hour = (in_array($this->freq, ['hourly', 'minutely']) && $offset > $this->start) ? date('H', $offset) : date(
'H', $this->start
);
@ -218,14 +259,13 @@ class Freq {
'i', $offset
) : date('i', $this->start);
$t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y', $offset));
if ($debug) printf("START: %s\n", date('r', $t));
if ($debug) echo 'START: ' . date('r', $t) . "\n";
if ($this->simpleMode) {
if ($offset < $t) {
$ts = $t;
if ($ts && in_array($ts, $this->excluded, true)) {
if ($ts && in_array($ts, $this->excluded))
$ts = $this->findNext($ts);
}
} else {
$ts = $this->findStartingPoint($t, $this->rules['interval'], false);
if (!$this->validDate($ts)) {
@ -236,31 +276,22 @@ class Freq {
return $ts;
}
//EOP needs to have the same TIME as START ($t)
$tO = new DateTime('@' . $t, new DateTimeZone('UTC'));
$eop = $this->findEndOfPeriod($offset);
$eopO = new DateTime('@' . $eop, new DateTimeZone('UTC'));
$eopO->setTime($tO->format('H'), $tO->format('i'), $tO->format('s'));
$eop = $eopO->getTimestamp();
unset($eopO, $tO);
if ($debug) echo 'EOP: ' . date('r', $eop) . "\n";
if ($debug) {
echo 'EOP: ' . date('r', $eop) . "\n";
}
foreach ($this->knownRules as $rule) {
foreach ($this->knownRules AS $rule) {
if ($found && isset($this->rules['by' . $rule])) {
if ($this->isPrerule($rule, $this->freq)) {
$subRules = explode(',', $this->rules['by' . $rule]);
$subrules = explode(',', $this->rules['by' . $rule]);
$_t = null;
foreach ($subRules as $subRule) {
$imm = call_user_func_array([$this, "ruleBy$rule"], [$subRule, $t]);
foreach ($subrules AS $subrule) {
$imm = call_user_func_array([$this, 'ruleBy' . $rule], [$subrule, $t]);
if ($imm === false) {
break;
}
if ($debug) {
printf("%s: %s A: %d\n", strtoupper($rule), date('r', $imm), intval($imm > $offset && $imm < $eop));
}
if ($debug) echo strtoupper($rule) . ': ' . date(
'r', $imm
) . ' A: ' . ((int)($imm > $offset && $imm < $eop)) . "\n";
if ($imm > $offset && $imm <= $eop && ($_t == null || $imm < $_t)) {
$_t = $imm;
}
@ -288,9 +319,8 @@ class Freq {
if ($debug) echo 'Not found' . "\n";
$ts = $this->findNext($this->findStartingPoint($offset, $this->rules['interval']));
}
if ($ts && in_array($ts, $this->excluded, true)) {
if ($ts && in_array($ts, $this->excluded))
return $this->findNext($ts);
}
return $ts;
}
@ -299,17 +329,17 @@ class Freq {
* Finds the starting point for the next rule. It goes $interval
* 'freq' forward in time since the given offset
*
* @param int $offset
* @param int $interval
* @param boolean $truncate
* @param int $offset
* @param int $interval
* @param boolean $truncate
* @return int
*/
private function findStartingPoint(int $offset, int $interval, $truncate = true): int {
private function findStartingPoint($offset, $interval, $truncate = true) {
$_freq = ($this->freq === 'daily') ? 'day__' : $this->freq;
$t = '+' . $interval . ' ' . substr($_freq, 0, -2) . 's';
if ($_freq === 'monthly' && $truncate) {
if ($interval > 1) {
$offset = strtotime('+' . ($interval - 1) . ' months ', $offset); // FIXME return type int|false
$offset = strtotime('+' . ($interval - 1) . ' months ', $offset);
}
$t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days';
}
@ -323,17 +353,27 @@ class Freq {
return $sp;
}
/**
* Finds the earliest timestamp posible outside this perioid
*
* @param int $offset
* @return int
*/
public function findEndOfPeriod($offset) {
return $this->findStartingPoint($offset, 1);
}
/**
* Resets the timestamp to the beginning of the
* period specified by freq
*
* Yes - the fall-through is on purpose!
*
* @param int $time
* @param string $freq
* @param int $time
* @param int $freq
* @return int
*/
private function truncateToPeriod(int $time, string $freq): int {
private function truncateToPeriod($time, $freq) {
$date = getdate($time);
switch ($freq) {
case 'yearly':
@ -348,7 +388,7 @@ class Freq {
$date['seconds'] = 0;
break;
case 'weekly':
if (date('N', $time) == 1) { // FIXME wrong compare, date return string|false
if (date('N', $time) == 1) {
$date['hours'] = 0;
$date['minutes'] = 0;
$date['seconds'] = 0;
@ -357,179 +397,21 @@ class Freq {
}
break;
}
return mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
}
$d = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
private function validDate($t): bool {
if (isset($this->rules['until']) && $t > $this->rules['until']) {
return false;
}
if (in_array($t, $this->excluded, true)) {
return false;
}
if (isset($this->rules['bymonth'])) {
$months = explode(',', $this->rules['bymonth']);
if (!in_array(date('m', $t), $months, true)) {
return false;
}
}
if (isset($this->rules['byday'])) {
$days = explode(',', $this->rules['byday']);
foreach ($days as $i => $k) {
$days[$i] = $this->weekdays[preg_replace('/[^A-Z]/', '', $k)];
}
if (!in_array(strtolower(date('l', $t)), $days, true)) {
return false;
}
}
if (isset($this->rules['byweekno'])) {
$weeks = explode(',', $this->rules['byweekno']);
if (!in_array(date('W', $t), $weeks, true)) {
return false;
}
}
if (isset($this->rules['bymonthday'])) {
$weekdays = explode(',', $this->rules['bymonthday']);
foreach ($weekdays as $i => $k) {
if ($k < 0) {
$weekdays[$i] = date('t', $t) + $k + 1;
}
}
if (!in_array(date('d', $t), $weekdays, true)) {
return false;
}
}
if (isset($this->rules['byhour'])) {
$hours = explode(',', $this->rules['byhour']);
if (!in_array(date('H', $t), $hours, true)) {
return false;
}
}
return true;
}
/**
* Finds the earliest timestamp possible outside this period.
*
* @param int $offset
* @return int
*/
public function findEndOfPeriod($offset = 0) {
return $this->findStartingPoint($offset, 1, false);
}
/**
* Returns the previous (most recent) occurrence of the rule from the
* given offset
*
* @param int $offset
* @return int
* @throws Exception
*/
public function previousOccurrence(int $offset) {
if (!empty($this->cache)) {
$t2 = $this->start;
foreach ($this->cache as $ts) {
if ($ts >= $offset) {
return $t2;
}
$t2 = $ts;
}
} else {
$ts = $this->start;
while (($t2 = $this->findNext($ts)) < $offset) {
if ($t2 == false) {
break;
}
$ts = $t2;
}
}
return $ts;
}
/**
* Returns the next occurrence of this rule after the given offset
*
* @param int $offset
* @return int
* @throws Exception
*/
public function nextOccurrence(int $offset) {
if ($offset < $this->start) {
return $this->firstOccurrence();
}
return $this->findNext($offset);
}
/**
* Finds the first occurrence of the rule.
*
* @return int timestamp
* @throws Exception
*/
public function firstOccurrence() {
$t = $this->start;
if (in_array($t, $this->excluded)) {
$t = $this->findNext($t);
}
return $t;
}
/**
* Finds the absolute last occurrence of the rule from the given offset.
* Builds also the cache, if not set before...
*
* @return int timestamp
* @throws Exception
*/
public function lastOccurrence() {
//build cache if not done
$this->getAllOccurrences();
//return last timestamp in cache
return end($this->cache);
}
/**
* Returns all timestamps array(), build the cache if not made before
*
* @return array
* @throws Exception
*/
public function getAllOccurrences() {
if (empty($this->cache)) {
$cache = [];
//build cache
$next = $this->firstOccurrence();
while ($next) {
$cache[] = $next;
$next = $this->findNext($next);
}
if (!empty($this->added)) {
$cache = array_unique(array_merge($cache, $this->added));
asort($cache);
}
$this->cache = $cache;
}
return $this->cache;
return $d;
}
/**
* Applies the BYDAY rule to the given timestamp
*
* @param string $rule
* @param int $t
* @param string $rule
* @param int $t
* @return int
*/
private function ruleByDay(string $rule, int $t): int {
$dir = ($rule[0] === '-') ? -1 : 1;
$dir_t = ($dir === 1) ? 'next' : 'last';
private function ruleByday($rule, $t) {
$dir = ($rule{0} == '-') ? -1 : 1;
$dir_t = ($dir == 1) ? 'next' : 'last';
$d = $this->weekdays[substr($rule, -2)];
$s = $dir_t . ' ' . $d . ' ' . date('H:i:s', $t);
@ -553,7 +435,7 @@ class Freq {
if (isset($this->rules['bymonth']) && $this->freq === 'yearly') {
$this->freq = 'monthly';
}
if ($dir === -1) {
if ($dir == -1) {
$_t = $this->findEndOfPeriod($t);
} else {
$_t = $this->truncateToPeriod($t, $this->freq);
@ -565,7 +447,7 @@ class Freq {
$n = $_t;
while ($c > 0) {
if ($dir === 1 && $c == 1 && date('l', $t) == ucfirst($d)) {
if ($dir == 1 && $c == 1 && date('l', $t) == ucfirst($d)) {
$s = 'today ' . date('H:i:s', $t);
}
$n = strtotime($s, $n);
@ -576,7 +458,7 @@ class Freq {
}
}
private function ruleByMonth($rule, int $t) {
private function ruleBymonth($rule, $t) {
$_t = mktime(date('H', $t), date('i', $t), date('s', $t), $rule, date('d', $t), date('Y', $t));
if ($t == $_t && isset($this->rules['byday'])) {
// TODO: this should check if one of the by*day's exists, and have a multi-day value
@ -586,7 +468,7 @@ class Freq {
}
}
private function ruleByMonthday($rule, int $t) {
private function ruleBymonthday($rule, $t) {
if ($rule < 0) {
$rule = date('t', $t) + $rule + 1;
}
@ -594,7 +476,7 @@ class Freq {
return mktime(date('H', $t), date('i', $t), date('s', $t), date('m', $t), $rule, date('Y', $t));
}
private function ruleByYearday($rule, int $t) {
private function ruleByyearday($rule, $t) {
if ($rule < 0) {
$_t = $this->findEndOfPeriod();
$d = '-';
@ -607,7 +489,7 @@ class Freq {
return strtotime($s, $_t);
}
private function ruleByWeekno($rule, int $t) {
private function ruleByweekno($rule, $t) {
if ($rule < 0) {
$_t = $this->findEndOfPeriod();
$d = '-';
@ -623,11 +505,96 @@ class Freq {
return $_t;
}
private function ruleByHour($rule, int $t) {
return mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
private function ruleByhour($rule, $t) {
$_t = mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
return $_t;
}
private function ruleByMinute($rule, int $t) {
return mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
private function ruleByminute($rule, $t) {
$_t = mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
return $_t;
}
private function validDate($t) {
if (isset($this->rules['until']) && $t > $this->rules['until']) {
return false;
}
if (in_array($t, $this->excluded)) {
return false;
}
if (isset($this->rules['bymonth'])) {
$months = explode(',', $this->rules['bymonth']);
if (!in_array(date('m', $t), $months)) {
return false;
}
}
if (isset($this->rules['byday'])) {
$days = explode(',', $this->rules['byday']);
foreach ($days As $i => $k) {
$days[$i] = $this->weekdays[preg_replace('/[^A-Z]/', '', $k)];
}
if (!in_array(strtolower(date('l', $t)), $days)) {
return false;
}
}
if (isset($this->rules['byweekno'])) {
$weeks = explode(',', $this->rules['byweekno']);
if (!in_array(date('W', $t), $weeks)) {
return false;
}
}
if (isset($this->rules['bymonthday'])) {
$weekdays = explode(',', $this->rules['bymonthday']);
foreach ($weekdays As $i => $k) {
if ($k < 0) {
$weekdays[$i] = date('t', $t) + $k + 1;
}
}
if (!in_array(date('d', $t), $weekdays)) {
return false;
}
}
if (isset($this->rules['byhour'])) {
$hours = explode(',', $this->rules['byhour']);
if (!in_array(date('H', $t), $hours)) {
return false;
}
}
return true;
}
private function isPrerule($rule, $freq) {
if ($rule === 'year')
return false;
if ($rule === 'month' && $freq === 'yearly')
return true;
if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday']))
return true;
// TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4..
if ($rule === 'yearday' && $freq === 'yearly')
return true;
if ($rule === 'weekno' && $freq === 'yearly')
return true;
if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly']))
return true;
if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily']))
return true;
if ($rule === 'minute')
return true;
return false;
}
}

View file

@ -1,47 +1,46 @@
<?php
namespace om;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use RuntimeException;
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
* Copyright (c) 2004-2015 Roman Ožana (http://www.omdesign.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
* @author Roman Ožana <ozana@omdesign.cz>
*/
class IcalParser {
/** @var DateTimeZone */
public DateTimeZone $timezone;
/** @var array|null */
public ?array $data = null;
/** @var \DateTimeZone */
public $timezone;
/** @var array */
protected array $counters = [];
public $data = [];
/** @var array */
protected $counters = [];
/** @var array */
private $windowsTimezones;
protected $arrayKeyMappings = [
'ATTACH' => 'ATTACHMENTS',
'EXDATE' => 'EXDATES',
'RDATE' => 'RDATES',
];
public function __construct() {
$this->windowsTimezones = require __DIR__ . '/WindowsTimezones.php'; // load Windows timezones from separate file
}
/**
* @param string $file
* @param callable|null $callback
* @param null $callback
* @return array|null
* @throws Exception
* @throws \RuntimeException
* @throws \InvalidArgumentException
* @throws \Exception
*/
public function parseFile(string $file, callable $callback = null): array {
if (!$handle = fopen($file, 'rb')) {
throw new RuntimeException('Can\'t open file' . $file . ' for reading');
public function parseFile($file, $callback = null) {
if (!$handle = fopen($file, 'r')) {
throw new \RuntimeException('Can\'t open file' . $file . ' for reading');
}
fclose($handle);
@ -50,12 +49,13 @@ class IcalParser {
/**
* @param string $string
* @param callable|null $callback
* @param null $callback
* @param boolean $add if true the parsed string is added to existing data
* @return array|null
* @throws Exception
* @throws \InvalidArgumentException
* @throws \Exception
*/
public function parseString(string $string, callable $callback = null, bool $add = false): ?array {
public function parseString($string, $callback = null, $add = false) {
if ($add === false) {
// delete old data
$this->data = [];
@ -63,7 +63,7 @@ class IcalParser {
}
if (!preg_match('/BEGIN:VCALENDAR/', $string)) {
throw new InvalidArgumentException('Invalid ICAL data format');
throw new \InvalidArgumentException('Invalid ICAL data format');
}
$section = 'VCALENDAR';
@ -88,6 +88,7 @@ class IcalParser {
$section = substr($row, 6);
$this->counters[$section] = isset($this->counters[$section]) ? $this->counters[$section] + 1 : 0;
continue 2; // while
break;
case 'END:VEVENT':
$section = substr($row, 4);
$currCounter = $this->counters[$section];
@ -97,6 +98,7 @@ class IcalParser {
}
continue 2; // while
break;
case 'END:DAYLIGHT':
case 'END:VALARM':
case 'END:VTIMEZONE':
@ -105,6 +107,7 @@ class IcalParser {
case 'END:STANDARD':
case 'END:VTODO':
continue 2; // while
break;
case 'END:VCALENDAR':
$veventSection = 'VEVENT';
@ -123,9 +126,11 @@ class IcalParser {
}
}
continue 2; // while
break;
}
[$key, $middle, $value] = $this->parseRow($row);
list($key, $middle, $value) = $this->parseRow($row);
if ($callback) {
// call user function for processing line
@ -134,33 +139,15 @@ class IcalParser {
if ($section === 'VCALENDAR') {
$this->data[$key] = $value;
} else {
// use an array since there can be multiple entries for this key. This does not
// break the current implementation--it leaves the original key alone and adds
// a new one specifically for the array of values.
if ($newKey = $this->isMultipleKey($key)) {
$this->data[$section][$this->counters[$section]][$newKey][] = $value;
}
// CATEGORIES can be multiple also but there is special case that there are comma separated categories
if ($this->isMultipleKeyWithCommaSeparation($key)) {
if (strpos($value, ',') !== false) {
$values = array_map('trim', preg_split('/(?<![^\\\\]\\\\),/', $value));
} else {
$values = [$value];
}
foreach ($values as $value) {
$this->data[$section][$this->counters[$section]][$key][] = $value;
}
} else {
$this->data[$section][$this->counters[$section]][$key] = $value;
if (isset($this->arrayKeyMappings[$key])) {
// use an array since there can be multiple entries for this key. This does not
// break the current implementation--it leaves the original key alone and adds
// a new one specifically for the array of values.
$arrayKey = $this->arrayKeyMappings[$key];
$this->data[$section][$this->counters[$section]][$arrayKey][] = $value;
}
$this->data[$section][$this->counters[$section]][$key] = $value;
}
}
@ -169,12 +156,121 @@ class IcalParser {
return ($callback) ? null : $this->data;
}
/**
* @param $row
* @return array
*/
private function parseRow($row) {
preg_match('#^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$#i', $row, $matches);
$key = false;
$middle = null;
$value = null;
if ($matches) {
$key = $matches[1];
$middle = $matches[2];
$value = $matches[3];
$timezone = null;
if ($key === 'X-WR-TIMEZONE' || $key === 'TZID') {
if (preg_match('#(\w+/\w+)$#i', $value, $matches)) {
$value = $matches[1];
}
$value = $this->toTimezone($value);
$this->timezone = new \DateTimeZone($value);
}
// have some middle part ?
if ($middle && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $middle, $matches, PREG_SET_ORDER)) {
$middle = [];
foreach ($matches as $match) {
if ($match['key'] === 'TZID') {
$match['value'] = trim($match['value'], "'\"");
$match['value'] = $this->toTimezone($match['value']);
try {
$middle[$match['key']] = $timezone = new \DateTimeZone($match['value']);
} catch (\Exception $e) {
$middle[$match['key']] = $match['value'];
}
} else if ($match['key'] === 'ENCODING') {
if ($match['value'] === 'QUOTED-PRINTABLE') {
$value = quoted_printable_decode($value);
}
}
}
}
}
// process simple dates with timezone
if (in_array($key, ['DTSTAMP', 'LAST-MODIFIED', 'CREATED', 'DTSTART', 'DTEND'], true)) {
try {
$value = new \DateTime($value, ($timezone ?: $this->timezone));
} catch (\Exception $e) {
$value = null;
}
} else if (in_array($key, ['EXDATE', 'RDATE'])) {
$values = [];
foreach (explode(',', $value) as $singleValue) {
try {
$values[] = new \DateTime($singleValue, ($timezone ?: $this->timezone));
} catch (\Exception $e) {
// pass
}
}
if (count($values) === 1) {
$value = $values[0];
} else {
$value = $values;
}
}
if ($key === 'RRULE' && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $value, $matches, PREG_SET_ORDER)) {
$middle = null;
$value = [];
foreach ($matches as $match) {
if (in_array($match['key'], ['UNTIL'])) {
try {
$value[$match['key']] = new \DateTime($match['value'], ($timezone ?: $this->timezone));
} catch (\Exception $e) {
$value[$match['key']] = $match['value'];
}
} else {
$value[$match['key']] = $match['value'];
}
}
}
//split by comma, escape \,
if ($key === 'CATEGORIES') {
$value = preg_split('/(?<![^\\\\]\\\\),/', $value);
}
//implement 4.3.11 Text ESCAPED-CHAR
$text_properties = [
'CALSCALE', 'METHOD', 'PRODID', 'VERSION', 'CATEGORIES', 'CLASS', 'COMMENT', 'DESCRIPTION'
, 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'TRANSP', 'TZID', 'TZNAME', 'CONTACT', 'RELATED-TO', 'UID'
, 'ACTION', 'REQUEST-STATUS'
];
if (in_array($key, $text_properties) || strpos($key, 'X-') === 0) {
if (is_array($value)) {
foreach ($value as &$var) {
$var = strtr($var, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']);
}
} else {
$value = strtr($value, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']);
}
}
return [$key, $middle, $value];
}
/**
* @param $event
* @return array
* @throws Exception
* @throws \Exception
*/
public function parseRecurrences($event): array {
public function parseRecurrences($event) {
$recurring = new Recurrence($event['RRULE']);
$exclusions = [];
$additions = [];
@ -205,9 +301,9 @@ class IcalParser {
$until = $recurring->getUntil();
if ($until === false) {
//forever... limit to 3 years from now
$end = new DateTime('now');
$end->add(new DateInterval('P3Y')); // + 3 years
//forever... limit to 3 years
$end = clone($event['DTSTART']);
$end->add(new \DateInterval('P3Y')); // + 3 years
$recurring->setUntil($end);
$until = $recurring->getUntil();
}
@ -217,14 +313,14 @@ class IcalParser {
$recurrenceTimestamps = $frequency->getAllOccurrences();
$recurrences = [];
foreach ($recurrenceTimestamps as $recurrenceTimestamp) {
$tmp = new DateTime('now', $event['DTSTART']->getTimezone());
$tmp = new \DateTime('now', $event['DTSTART']->getTimezone());
$tmp->setTimestamp($recurrenceTimestamp);
$recurrenceIDDate = $tmp->format('Ymd');
$recurrenceIDDateTime = $tmp->format('Ymd\THis');
if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDate]) &&
empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTime])) {
$gmtCheck = new DateTime('now', new DateTimeZone('UTC'));
$gmtCheck = new \DateTime("now", new \DateTimeZone('UTC'));
$gmtCheck->setTimestamp($recurrenceTimestamp);
$recurrenceIDDateTimeZ = $gmtCheck->format('Ymd\THis\Z');
if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTimeZ])) {
@ -236,172 +332,32 @@ class IcalParser {
return $recurrences;
}
private function parseRow($row): array {
preg_match('#^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$#i', $row, $matches);
$key = false;
$middle = null;
$value = null;
if ($matches) {
$key = $matches[1];
$middle = $matches[2];
$value = $matches[3];
$timezone = null;
if ($key === 'X-WR-TIMEZONE' || $key === 'TZID') {
if (preg_match('#(\w+/\w+)$#i', $value, $matches)) {
$value = $matches[1];
}
$value = $this->toTimezone($value);
$this->timezone = new DateTimeZone($value);
}
// have some middle part ?
if ($middle && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $middle, $matches, PREG_SET_ORDER)) {
$middle = [];
foreach ($matches as $match) {
if ($match['key'] === 'TZID') {
$match['value'] = trim($match['value'], "'\"");
$match['value'] = $this->toTimezone($match['value']);
try {
$middle[$match['key']] = $timezone = new DateTimeZone($match['value']);
} catch (Exception $e) {
$middle[$match['key']] = $match['value'];
}
} elseif ($match['key'] === 'ENCODING') {
if ($match['value'] === 'QUOTED-PRINTABLE') {
$value = quoted_printable_decode($value);
}
}
}
}
}
// process simple dates with timezone
if (in_array($key, ['DTSTAMP', 'LAST-MODIFIED', 'CREATED', 'DTSTART', 'DTEND'], true)) {
try {
$value = new DateTime($value, ($timezone ?: $this->timezone));
} catch (Exception $e) {
$value = null;
}
} elseif (in_array($key, ['EXDATE', 'RDATE'])) {
$values = [];
foreach (explode(',', $value) as $singleValue) {
try {
$values[] = new DateTime($singleValue, ($timezone ?: $this->timezone));
} catch (Exception $e) {
// pass
}
}
if (count($values) === 1) {
$value = $values[0];
} else {
$value = $values;
}
}
if ($key === 'RRULE' && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $value, $matches, PREG_SET_ORDER)) {
$middle = null;
$value = [];
foreach ($matches as $match) {
if (in_array($match['key'], ['UNTIL'])) {
try {
$value[$match['key']] = new DateTime($match['value'], ($timezone ?: $this->timezone));
} catch (Exception $e) {
$value[$match['key']] = $match['value'];
}
} else {
$value[$match['key']] = $match['value'];
}
}
}
//implement 4.3.11 Text ESCAPED-CHAR
$text_properties = [
'CALSCALE', 'METHOD', 'PRODID', 'VERSION', 'CATEGORIES', 'CLASS', 'COMMENT', 'DESCRIPTION',
'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'TRANSP', 'TZID', 'TZNAME', 'CONTACT',
'RELATED-TO', 'UID', 'ACTION', 'REQUEST-STATUS', 'URL',
];
if (in_array($key, $text_properties, true) || strpos($key, 'X-') === 0) {
if (is_array($value)) {
foreach ($value as &$var) {
$var = strtr($var, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']);
}
} else {
$value = strtr($value, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']);
}
}
return [$key, $middle, $value];
}
/**
* Process timezone and return correct one...
*
* @param string $zone
* @return mixed|null
* @return array
*/
private function toTimezone(string $zone) {
return $this->windowsTimezones[$zone] ?? $zone;
}
public function isMultipleKey(string $key): ?string {
return (['ATTACH' => 'ATTACHMENTS', 'EXDATE' => 'EXDATES', 'RDATE' => 'RDATES'])[$key] ?? null;
}
/**
* @param $key
* @return string|null
*/
public function isMultipleKeyWithCommaSeparation($key): ?string {
return (['X-CATEGORIES' => 'X-CATEGORIES', 'CATEGORIES' => 'CATEGORIES'])[$key] ?? null;
}
public function getAlarms(): array {
return $this->data['VALARM'] ?? [];
}
public function getTimezone(): array {
return $this->getTimezones();
}
public function getTimezones(): array {
return $this->data['VTIMEZONE'] ?? [];
}
/**
* Return sorted event list as ArrayObject
*
* @deprecated use IcalParser::getEvents()->sorted() instead
*/
public function getSortedEvents(): \ArrayObject {
return $this->getEvents()->sorted();
}
public function getEvents(): EventsList {
$events = new EventsList();
public function getEvents() {
$events = [];
if (isset($this->data['VEVENT'])) {
foreach ($this->data['VEVENT'] as $iValue) {
$event = $iValue;
for ($i = 0; $i < count($this->data['VEVENT']); $i++) {
$event = $this->data['VEVENT'][$i];
if (empty($event['RECURRENCES'])) {
if (!empty($event['RECURRENCE-ID']) && !empty($event['UID']) && isset($event['SEQUENCE'])) {
$modifiedEventUID = $event['UID'];
$modifiedEventRecurID = $event['RECURRENCE-ID'];
$modifiedEventSeq = (int)$event['SEQUENCE'];
$modifiedEventSeq = intval($event['SEQUENCE'], 10);
if (isset($this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID])) {
$counter = $this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID];
if (isset($this->data["_RECURRENCE_COUNTERS_BY_UID"][$modifiedEventUID])) {
$counter = $this->data["_RECURRENCE_COUNTERS_BY_UID"][$modifiedEventUID];
$originalEvent = $this->data['VEVENT'][$counter];
$originalEvent = $this->data["VEVENT"][$counter];
if (isset($originalEvent['SEQUENCE'])) {
$originalEventSeq = (int)$originalEvent['SEQUENCE'];
$originalEventSeq = intval($originalEvent['SEQUENCE'], 10);
$originalEventFormattedStartDate = $originalEvent['DTSTART']->format('Ymd\THis');
if ($modifiedEventRecurID === $originalEventFormattedStartDate && $modifiedEventSeq > $originalEventSeq) {
// this modifies the original event
$modifiedEvent = array_replace_recursive($originalEvent, $event);
$this->data['VEVENT'][$counter] = $modifiedEvent;
$this->data["VEVENT"][$counter] = $modifiedEvent;
foreach ($events as $z => $event) {
if ($events[$z]['UID'] === $originalEvent['UID'] &&
$events[$z]['SEQUENCE'] === $originalEvent['SEQUENCE']) {
@ -411,13 +367,13 @@ class IcalParser {
}
}
$event = null; // don't add this to the $events[] array again
} elseif (!empty($originalEvent['RECURRENCES'])) {
} else if (!empty($originalEvent['RECURRENCES'])) {
for ($j = 0; $j < count($originalEvent['RECURRENCES']); $j++) {
$recurDate = $originalEvent['RECURRENCES'][$j];
$formattedStartDate = $recurDate->format('Ymd\THis');
if ($formattedStartDate === $modifiedEventRecurID) {
unset($this->data['VEVENT'][$counter]['RECURRENCES'][$j]);
$this->data['VEVENT'][$counter]['RECURRENCES'] = array_values($this->data['VEVENT'][$counter]['RECURRENCES']);
unset($this->data["VEVENT"][$counter]['RECURRENCES'][$j]);
$this->data["VEVENT"][$counter]['RECURRENCES'] = array_values($this->data["VEVENT"][$counter]['RECURRENCES']);
break;
}
}
@ -427,7 +383,7 @@ class IcalParser {
}
if (!empty($event)) {
$events->append($event);
$events[] = $event;
}
} else {
$recurrences = $event['RECURRENCES'];
@ -446,7 +402,7 @@ class IcalParser {
}
$newEvent['RECURRENCE_INSTANCE'] = $j;
$events->append($newEvent);
$events[] = $newEvent;
$firstEvent = false;
}
}
@ -455,12 +411,61 @@ class IcalParser {
return $events;
}
/**
* @return \ArrayObject
* @deprecated use IcalParser::getEvents->reversed();
* Process timezone and return correct one...
*
* @param string $zone
* @return mixed|null
*/
public function getReverseSortedEvents(): \ArrayObject {
return $this->getEvents()->reversed();
private function toTimezone($zone) {
return isset($this->windowsTimezones[$zone]) ? $this->windowsTimezones[$zone] : $zone;
}
/**
* @return array
*/
public function getAlarms() {
return isset($this->data['VALARM']) ? $this->data['VALARM'] : [];
}
/**
* @return array
*/
public function getTimezones() {
return isset($this->data['VTIMEZONE']) ? $this->data['VTIMEZONE'] : [];
}
/**
* Return sorted event list as array
*
* @return array
*/
public function getSortedEvents() {
if ($events = $this->getEvents()) {
usort(
$events, function ($a, $b) {
return $a['DTSTART'] > $b['DTSTART'];
}
);
return $events;
}
return [];
}
/**
* @return array
*/
public function getReverseSortedEvents() {
if ($events = $this->getEvents()) {
usort(
$events, function ($a, $b) {
return $a['DTSTART'] < $b['DTSTART'];
}
);
return $events;
}
return [];
}
}

View file

@ -2,8 +2,7 @@
namespace om;
use DateTime;
use Exception;
use \DateTime;
/**
* Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Recurrence.php)
@ -20,7 +19,6 @@ use Exception;
* @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
*/
class Recurrence {
public $rrule;
protected $freq;
protected $until;
@ -41,9 +39,9 @@ class Recurrence {
*
* @var array
*/
protected array $listProperties = [
protected $listProperties = [
'bysecond', 'byminute', 'byhour', 'byday', 'bymonthday',
'byyearday', 'byweekno', 'bymonth', 'bysetpos',
'byyearday', 'byweekno', 'bymonth', 'bysetpos'
];
/**
@ -52,7 +50,7 @@ class Recurrence {
* @param array $rrule an om\icalparser row array which will be parsed to get the
* desired information.
*/
public function __construct(array $rrule) {
public function __construct($rrule) {
$this->parseRrule($rrule);
}
@ -62,7 +60,7 @@ class Recurrence {
*
* @param $rrule
*/
protected function parseRrule($rrule): void {
protected function parseRrule($rrule) {
$this->rrule = $rrule;
//loop through the properties in the line and set their associated
//member variables
@ -77,6 +75,36 @@ class Recurrence {
}
}
/**
* Set the $until member
*
* @param mixed timestamp (int) / Valid DateTime format (string)
*/
public function setUntil($ts) {
if ($ts instanceof DateTime) {
$dt = $ts;
} else if (is_int($ts)) {
$dt = new DateTime('@' . $ts);
} else {
$dt = new DateTime($ts);
}
$this->until = $dt->format('Ymd\THisO');
$this->rrule['until'] = $this->until;
}
/**
* Retrieves the desired member variable and returns it (if it's set)
*
* @param string $member name of the member variable
* @return mixed the variable value (if set), false otherwise
*/
protected function getMember($member) {
if (isset($this->$member)) {
return $this->$member;
}
return false;
}
/**
* Returns the frequency - corresponds to FREQ in RFC 2445.
*
@ -86,16 +114,6 @@ class Recurrence {
return $this->getMember('freq');
}
/**
* Retrieves the desired member variable and returns it (if it's set)
*
* @param string $member name of the member variable
* @return mixed the variable value (if set), false otherwise
*/
protected function getMember(string $member) {
return $this->$member ?? false;
}
/**
* Returns when the event will go until - corresponds to UNTIL in RFC 2445.
*
@ -105,24 +123,6 @@ class Recurrence {
return $this->getMember('until');
}
/**
* Set the $until member
*
* @param mixed $ts timestamp (int) / Valid DateTime format (string)
* @throws Exception
*/
public function setUntil($ts): void {
if ($ts instanceof DateTime) {
$dt = $ts;
} elseif (is_int($ts)) {
$dt = new DateTime('@' . $ts);
} else {
$dt = new DateTime($ts);
}
$this->until = $dt->format('Ymd\THisO');
$this->rrule['until'] = $this->until;
}
/**
* Returns the count of the times the event will occur (should only appear if 'until'
* does not appear) - corresponds to COUNT in RFC 2445.

View file

@ -1,5 +1,4 @@
<?php
/**
* List of Windows Timezones
*/
@ -85,8 +84,6 @@ return [
'(UTC) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
'W. Europe Standard Time' => 'Europe/Berlin',
'(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
'(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rome\, Stockholm\, Vienna' => 'Europe/Berlin',
'(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\, Wien' => 'Europe/Berlin',
'Central Europe Standard Time' => 'Europe/Budapest',
'(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
'Romance Standard Time' => 'Europe/Paris',
@ -210,5 +207,4 @@ return [
'(UTC+13:00) Nuku\'alofa' => 'Pacific/Tongatapu',
'Samoa Standard Time' => 'Pacific/Apia',
'(UTC-11:00)Samoa' => 'Pacific/Apia',
'W. Europe Standard Time 1' => 'Europe/Berlin',
];
];