Update libs

This commit is contained in:
Patrick Schwarz 2024-01-12 00:49:45 +01:00
parent 3d1ca79209
commit b95de25ee8
2 changed files with 230 additions and 204 deletions

View file

@ -32,7 +32,7 @@ class Event
/** /**
* https://www.kanzaki.com/docs/ical/duration.html * https://www.kanzaki.com/docs/ical/duration.html
* *
* @var string * @var string|null
*/ */
public $duration; public $duration;
@ -81,14 +81,14 @@ class Event
/** /**
* https://www.kanzaki.com/docs/ical/description.html * https://www.kanzaki.com/docs/ical/description.html
* *
* @var string * @var string|null
*/ */
public $description; public $description;
/** /**
* https://www.kanzaki.com/docs/ical/location.html * https://www.kanzaki.com/docs/ical/location.html
* *
* @var string * @var string|null
*/ */
public $location; public $location;
@ -132,7 +132,7 @@ class Event
* *
* @var array<string, mixed> * @var array<string, mixed>
*/ */
private $additionalProperties = []; public $additionalProperties = array();
/** /**
* Creates the Event object * Creates the Event object
@ -250,10 +250,15 @@ class Event
*/ */
protected static function snakeCase($input, $glue = '_', $separator = '-') protected static function snakeCase($input, $glue = '_', $separator = '-')
{ {
$input = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input); $inputSplit = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input);
$input = implode($glue, $input);
$input = str_replace($separator, $glue, $input);
return strtolower($input); if ($inputSplit === false) {
return $input;
}
$inputSplit = implode($glue, $inputSplit);
$inputSplit = str_replace($separator, $glue, $inputSplit);
return strtolower($inputSplit);
} }
} }

View file

@ -517,7 +517,7 @@ class ICal
// Fallback to use the system default time zone // Fallback to use the system default time zone
if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) { if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) {
$this->defaultTimeZone = date_default_timezone_get(); $this->defaultTimeZone = $this->getDefaultTimeZone(true);
} }
// Ideally you would use `PHP_INT_MIN` from PHP 7 // Ideally you would use `PHP_INT_MIN` from PHP 7
@ -553,7 +553,7 @@ class ICal
{ {
$string = str_replace(array("\r\n", "\n\r", "\r"), "\n", $string); $string = str_replace(array("\r\n", "\n\r", "\r"), "\n", $string);
if (empty($this->cal)) { if ($this->cal === array()) {
$lines = explode("\n", $string); $lines = explode("\n", $string);
$this->initLines($lines); $this->initLines($lines);
@ -572,7 +572,7 @@ class ICal
*/ */
public function initFile($file) public function initFile($file)
{ {
if (empty($this->cal)) { if ($this->cal === array()) {
$lines = $this->fileOrUrl($file); $lines = $this->fileOrUrl($file);
$this->initLines($lines); $this->initLines($lines);
@ -640,15 +640,16 @@ class ICal
} }
if (!$this->disableCharacterReplacement) { if (!$this->disableCharacterReplacement) {
$line = $this->cleanData($line); $line = str_replace(array(
} '&nbsp;',
"\t",
$add = $this->keyValueFromString($line); "\xc2\xa0", // Non-breaking space
), ' ', $line);
if ($add === false) {
continue; $line = $this->cleanCharacters($line);
} }
$add = $this->keyValueFromString($line);
$keyword = $add[0]; $keyword = $add[0];
$values = $add[1]; // May be an array containing multiple values $values = $add[1]; // May be an array containing multiple values
@ -679,8 +680,8 @@ class ICal
break; break;
// https://www.kanzaki.com/docs/ical/vevent.html
case 'BEGIN:VEVENT': case 'BEGIN:VEVENT':
// https://www.kanzaki.com/docs/ical/vevent.html
if (!is_array($value)) { if (!is_array($value)) {
$this->eventCount++; $this->eventCount++;
} }
@ -689,8 +690,8 @@ class ICal
break; break;
// https://www.kanzaki.com/docs/ical/vfreebusy.html
case 'BEGIN:VFREEBUSY': case 'BEGIN:VFREEBUSY':
// https://www.kanzaki.com/docs/ical/vfreebusy.html
if (!is_array($value)) { if (!is_array($value)) {
$this->freeBusyIndex++; $this->freeBusyIndex++;
} }
@ -754,7 +755,7 @@ class ICal
$this->processRecurrences(); $this->processRecurrences();
// Apply changes to altered recurrence instances // Apply changes to altered recurrence instances
if (!empty($this->alteredRecurrenceInstances)) { if ($this->alteredRecurrenceInstances !== array()) {
$events = $this->cal['VEVENT']; $events = $this->cal['VEVENT'];
foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) { foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) {
@ -787,7 +788,7 @@ class ICal
{ {
$events = $this->cal['VEVENT']; $events = $this->cal['VEVENT'];
if (!empty($events)) { if ($events !== array()) {
$lastIndex = count($events) - 1; $lastIndex = count($events) - 1;
$lastEvent = $events[$lastIndex]; $lastEvent = $events[$lastIndex];
@ -810,7 +811,7 @@ class ICal
{ {
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
if (!empty($events)) { if ($events !== array()) {
foreach ($events as $key => $anEvent) { foreach ($events as $key => $anEvent) {
if ($anEvent === null) { if ($anEvent === null) {
unset($events[$key]); unset($events[$key]);
@ -869,9 +870,10 @@ class ICal
{ {
$string = implode(PHP_EOL, $lines); $string = implode(PHP_EOL, $lines);
$string = str_ireplace('&nbsp;', ' ', $string); $string = str_ireplace('&nbsp;', ' ', $string);
$string = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
$lines = explode(PHP_EOL, $string); $cleanedString = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
$lines = explode(PHP_EOL, $cleanedString ?: $string);
return $lines; return $lines;
} }
@ -912,7 +914,6 @@ class ICal
$this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value; $this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value;
} }
} }
break; break;
case 'VEVENT': case 'VEVENT':
@ -948,11 +949,10 @@ class ICal
} }
} }
if ($this->cal[$key1][$key2][$keyword] !== $value) { if (!is_array($value) && $this->cal[$key1][$key2][$keyword] !== $value) {
$this->cal[$key1][$key2][$keyword] .= ',' . $value; $this->cal[$key1][$key2][$keyword] .= ',' . $value;
} }
} }
break; break;
case 'VFREEBUSY': case 'VFREEBUSY':
@ -975,7 +975,6 @@ class ICal
} else { } else {
$this->cal[$key1][$key2][$key3][] = $value; $this->cal[$key1][$key2][$key3][] = $value;
} }
break; break;
case 'VTODO': case 'VTODO':
@ -989,14 +988,16 @@ class ICal
break; break;
} }
$this->lastKeyword = $keyword; if (is_string($keyword)) {
$this->lastKeyword = $keyword;
}
} }
/** /**
* Gets the key value pair from an iCal string * Gets the key value pair from an iCal string
* *
* @param string $text * @param string $text
* @return array|boolean * @return array
*/ */
public function keyValueFromString($text) public function keyValueFromString($text)
{ {
@ -1011,6 +1012,7 @@ class ICal
if ($i === 0) { if ($i === 0) {
$object[0] = $splitLine[$i]; $object[0] = $splitLine[$i];
$i++; $i++;
continue; continue;
} }
@ -1052,7 +1054,7 @@ class ICal
} }
// Object construction // Object construction
if ($paramObj !== []) { if ($paramObj !== array()) {
$object[1][0] = $valueObj; $object[1][0] = $valueObj;
$object[1][1] = $paramObj; $object[1][1] = $paramObj;
} else { } else {
@ -1103,11 +1105,29 @@ class ICal
return $words; return $words;
} }
/**
* Returns the default time zone if set.
* Falls back to the system default if not set.
*
* @param boolean $forceReturnSystemDefault
* @return string
*/
private function getDefaultTimeZone($forceReturnSystemDefault = false)
{
$systemDefault = date_default_timezone_get();
if ($forceReturnSystemDefault) {
return $systemDefault;
}
return $this->defaultTimeZone ?: $systemDefault;
}
/** /**
* Returns a `DateTime` object from an iCal date time format * Returns a `DateTime` object from an iCal date time format
* *
* @param string $icalDate * @param string $icalDate
* @return \DateTime * @return \DateTime|false
* @throws \Exception * @throws \Exception
*/ */
public function iCalDateToDateTime($icalDate) public function iCalDateToDateTime($icalDate)
@ -1131,7 +1151,7 @@ class ICal
preg_match($pattern, $icalDate, $date); preg_match($pattern, $icalDate, $date);
if (empty($date)) { if ($date === array()) {
throw new \Exception('Invalid iCal date format.'); throw new \Exception('Invalid iCal date format.');
} }
@ -1141,10 +1161,10 @@ class ICal
if ($date[4] === 'Z') { if ($date[4] === 'Z') {
$dateTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC); $dateTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
} elseif (!empty($date[1])) { } elseif (isset($date[1]) && $date[1] !== '') {
$dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]); $dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]);
} else { } else {
$dateTimeZone = new \DateTimeZone($this->defaultTimeZone); $dateTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
} }
// The exclamation mark at the start of the format string indicates that if a // The exclamation mark at the start of the format string indicates that if a
@ -1152,7 +1172,7 @@ class ICal
// set to 00:00:00. Without it, the time would be set to the current system time. // set to 00:00:00. Without it, the time would be set to the current system time.
$dateFormat = '!Ymd'; $dateFormat = '!Ymd';
$dateBasic = $date[2]; $dateBasic = $date[2];
if (!empty($date[3])) { if (isset($date[3]) && $date[3] !== '') {
$dateBasic .= "T{$date[3]}"; $dateBasic .= "T{$date[3]}";
$dateFormat .= '\THis'; $dateFormat .= '\THis';
} }
@ -1168,7 +1188,15 @@ class ICal
*/ */
public function iCalDateToUnixTimestamp($icalDate) public function iCalDateToUnixTimestamp($icalDate)
{ {
return $this->iCalDateToDateTime($icalDate)->getTimestamp(); $iCalDateToDateTime = $this->iCalDateToDateTime($icalDate);
if ($iCalDateToDateTime === false) {
trigger_error("ICal::iCalDateToUnixTimestamp: Invalid date passed ({$icalDate})", E_USER_NOTICE);
return 0;
}
return $iCalDateToDateTime->getTimestamp();
} }
/** /**
@ -1177,7 +1205,7 @@ class ICal
* @param array $event * @param array $event
* @param string $key * @param string $key
* @param string|null $format * @param string|null $format
* @return string|boolean|\DateTime * @return string|integer|boolean|\DateTime
*/ */
public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT) public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT)
{ {
@ -1188,14 +1216,24 @@ class ICal
$dateArray = $event["{$key}_array"]; $dateArray = $event["{$key}_array"];
if ($key === 'DURATION') { if ($key === 'DURATION') {
$dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2], null); $dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2]);
if ($dateTime instanceof \DateTime === false) {
trigger_error("ICal::iCalDateWithTimeZone: Invalid date passed ({$event['DTSTART']})", E_USER_NOTICE);
return false;
}
} else { } else {
// When constructing from a Unix Timestamp, no time zone needs passing. // When constructing from a Unix Timestamp, no time zone needs passing.
$dateTime = new \DateTime("@{$dateArray[2]}"); $dateTime = new \DateTime("@{$dateArray[2]}");
} }
// Set the time zone we wish to use when running `$dateTime->format`. $calendarTimeZone = $this->calendarTimeZone();
$dateTime->setTimezone(new \DateTimeZone($this->calendarTimeZone()));
if (!is_null($calendarTimeZone)) {
// Set the time zone we wish to use when running `$dateTime->format`.
$dateTime->setTimezone(new \DateTimeZone($calendarTimeZone));
}
if (is_null($format)) { if (is_null($format)) {
return $dateTime; return $dateTime;
@ -1216,7 +1254,7 @@ class ICal
$checks = null; $checks = null;
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
if (!empty($events)) { if ($events !== array()) {
foreach ($events as $key => $anEvent) { foreach ($events as $key => $anEvent) {
foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) { foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) {
if (isset($anEvent[$type])) { if (isset($anEvent[$type])) {
@ -1286,7 +1324,7 @@ class ICal
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
// If there are no events, then we have nothing to process. // If there are no events, then we have nothing to process.
if (empty($events)) { if ($events === array()) {
return; return;
} }
@ -1304,6 +1342,12 @@ class ICal
// Create new initial starting point. // Create new initial starting point.
$initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]); $initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]);
if ($initialEventDate === false) {
trigger_error("ICal::processRecurrences: Invalid date passed ({$anEvent['DTSTART_array'][3]})", E_USER_NOTICE);
continue;
}
// Separate the RRULE stanzas, and explode the values that are lists. // Separate the RRULE stanzas, and explode the values that are lists.
$rrules = array(); $rrules = array();
foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) { foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) {
@ -1315,9 +1359,14 @@ class ICal
} }
} }
// Get frequency
$frequency = $rrules['FREQ']; $frequency = $rrules['FREQ'];
if (!is_string($frequency)) {
trigger_error('ICal::processRecurrences: Invalid frequency passed', E_USER_NOTICE);
continue;
}
// Reject RRULE if BYDAY stanza is invalid: // Reject RRULE if BYDAY stanza is invalid:
// > The BYDAY rule part MUST NOT be specified with a numeric value // > The BYDAY rule part MUST NOT be specified with a numeric value
// > when the FREQ rule part is not set to MONTHLY or YEARLY. // > when the FREQ rule part is not set to MONTHLY or YEARLY.
@ -1329,21 +1378,20 @@ class ICal
return $carry && substr($weekday, -2) === $weekday; return $carry && substr($weekday, -2) === $weekday;
}; };
if (!in_array($frequency, array('MONTHLY', 'YEARLY'))) { if (!in_array($frequency, array('MONTHLY', 'YEARLY'))) {
if (!array_reduce($rrules['BYDAY'], $checkByDays, true)) { if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) {
error_log("ICal::ProcessRecurrences: A {$frequency} RRULE may not contain BYDAY values with numeric prefixes"); trigger_error("ICal::processRecurrences: A {$frequency} RRULE may not contain BYDAY values with numeric prefixes", E_USER_NOTICE);
continue; continue;
} }
} elseif ($frequency === 'YEARLY' && !empty($rrules['BYWEEKNO'])) { } elseif ($frequency === 'YEARLY' && (isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array()))) {
if (!array_reduce($rrules['BYDAY'], $checkByDays, true)) { if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) {
error_log('ICal::ProcessRecurrences: A YEARLY RRULE with a BYWEEKNO part may not contain BYDAY values with numeric prefixes'); trigger_error('ICal::processRecurrences: A YEARLY RRULE with a BYWEEKNO part may not contain BYDAY values with numeric prefixes', E_USER_NOTICE);
continue; continue;
} }
} }
} }
// Get Interval
$interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $rrules['INTERVAL']; $interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $rrules['INTERVAL'];
// Throw an error if this isn't an integer. // Throw an error if this isn't an integer.
@ -1380,22 +1428,45 @@ class ICal
*/ */
$count = 1; $count = 1;
$countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX; $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX;
$until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp(); $now = date_create();
if (isset($rrules['UNTIL'])) { $until = $now === false
$until = min($until, $this->iCalDateToUnixTimestamp($rrules['UNTIL'])); ? 0
: $now->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
$untilWhile = $until;
if (isset($rrules['UNTIL']) && is_string($rrules['UNTIL'])) {
$untilDT = $this->iCalDateToDateTime($rrules['UNTIL']);
$until = min($until, ($untilDT === false) ? $until : $untilDT->getTimestamp());
// There are certain edge cases where we need to go a little beyond the UNTIL to
// ensure we get all events. Consider:
//
// DTSTART:20200103
// RRULE:FREQ=MONTHLY;BYDAY=-5FR;UNTIL=20200502
//
// In this case the last occurrence should be 1st May, however when we transition
// from April to May:
//
// $until ~= 2nd May
// $frequencyRecurringDateTime ~= 3rd May
//
// And as the latter comes after the former, the while loop ends before any dates
// in May have the chance to be considered.
$untilWhile = min($untilWhile, ($untilDT === false) ? $untilWhile : $untilDT->modify("+1 {$this->frequencyConversion[$frequency]}")->getTimestamp());
} }
$eventRecurrences = array(); $eventRecurrences = array();
$frequencyRecurringDateTime = clone $initialEventDate; $frequencyRecurringDateTime = clone $initialEventDate;
while ($frequencyRecurringDateTime->getTimestamp() <= $until && $count < $countLimit) { while ($frequencyRecurringDateTime->getTimestamp() <= $untilWhile && $count < $countLimit) {
$candidateDateTimes = array(); $candidateDateTimes = array();
// phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
switch ($frequency) { switch ($frequency) {
case 'DAILY': case 'DAILY':
if (!empty($rrules['BYMONTHDAY'])) { if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
if (!isset($monthDays)) { if (!isset($monthDays)) {
// This variable is unset when we change months (see below) // This variable is unset when we change months (see below)
$monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime); $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
@ -1414,7 +1485,7 @@ class ICal
$initialDayOfWeek = $frequencyRecurringDateTime->format('N'); $initialDayOfWeek = $frequencyRecurringDateTime->format('N');
$matchingDays = array($initialDayOfWeek); $matchingDays = array($initialDayOfWeek);
if (!empty($rrules['BYDAY'])) { if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
// setISODate() below uses the ISO-8601 specification of weeks: start on // setISODate() below uses the ISO-8601 specification of weeks: start on
// a Monday, end on a Sunday. However, RRULEs (or the caller of the // a Monday, end on a Sunday. However, RRULEs (or the caller of the
// parser) may state an alternate WeeKSTart. // parser) may state an alternate WeeKSTart.
@ -1459,18 +1530,17 @@ class ICal
$candidateDateTimes[] = $clonedDateTime->setISODate( $candidateDateTimes[] = $clonedDateTime->setISODate(
(int) $frequencyRecurringDateTime->format('o'), (int) $frequencyRecurringDateTime->format('o'),
(int) $frequencyRecurringDateTime->format('W'), (int) $frequencyRecurringDateTime->format('W'),
$day (int) $day
); );
} }
break; break;
case 'MONTHLY': case 'MONTHLY':
$matchingDays = array(); $matchingDays = array();
if (!empty($rrules['BYMONTHDAY'])) { if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
$matchingDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
if (!empty($rrules['BYDAY'])) { if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
$matchingDays = array_filter( $matchingDays = array_filter(
$this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime), $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
function ($monthDay) use ($matchingDays) { function ($monthDay) use ($matchingDays) {
@ -1478,13 +1548,13 @@ class ICal
} }
); );
} }
} elseif (!empty($rrules['BYDAY'])) { } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
$matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
} else { } else {
$matchingDays[] = $frequencyRecurringDateTime->format('d'); $matchingDays[] = $frequencyRecurringDateTime->format('d');
} }
if (!empty($rrules['BYSETPOS'])) { if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) {
$matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays); $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
} }
@ -1501,27 +1571,26 @@ class ICal
$day $day
); );
} }
break; break;
case 'YEARLY': case 'YEARLY':
$matchingDays = array(); $matchingDays = array();
if (!empty($rrules['BYMONTH'])) { if (isset($rrules['BYMONTH']) && (is_array($rrules['BYMONTH']) && $rrules['BYMONTH'] !== array())) {
$bymonthRecurringDatetime = clone $frequencyRecurringDateTime; $bymonthRecurringDatetime = clone $frequencyRecurringDateTime;
foreach ($rrules['BYMONTH'] as $byMonth) { foreach ($rrules['BYMONTH'] as $byMonth) {
$bymonthRecurringDatetime->setDate( $bymonthRecurringDatetime->setDate(
(int) $frequencyRecurringDateTime->format('Y'), (int) $frequencyRecurringDateTime->format('Y'),
$byMonth, (int) $byMonth,
(int) $frequencyRecurringDateTime->format('d') (int) $frequencyRecurringDateTime->format('d')
); );
// Determine the days of the month affected // Determine the days of the month affected
// (The interaction between BYMONTHDAY and BYDAY is resolved later.) // (The interaction between BYMONTHDAY and BYDAY is resolved later.)
$monthDays = array(); $monthDays = array();
if (!empty($rrules['BYMONTHDAY'])) { if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
$monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $bymonthRecurringDatetime); $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $bymonthRecurringDatetime);
} elseif (!empty($rrules['BYDAY'])) { } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
$monthDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime); $monthDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime);
} else { } else {
$monthDays[] = $bymonthRecurringDatetime->format('d'); $monthDays[] = $bymonthRecurringDatetime->format('d');
@ -1536,34 +1605,34 @@ class ICal
)->format('z') + 1; )->format('z') + 1;
} }
} }
} elseif (!empty($rrules['BYWEEKNO'])) { } elseif (isset($rrules['BYWEEKNO']) && (is_array($rrules['BYWEEKNO']) && $rrules['BYWEEKNO'] !== array())) {
$matchingDays = $this->getDaysOfYearMatchingByWeekNoRRule($rrules['BYWEEKNO'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfYearMatchingByWeekNoRRule($rrules['BYWEEKNO'], $frequencyRecurringDateTime);
} elseif (!empty($rrules['BYYEARDAY'])) { } elseif (isset($rrules['BYYEARDAY']) && (is_array($rrules['BYYEARDAY']) && $rrules['BYYEARDAY'] !== array())) {
$matchingDays = $this->getDaysOfYearMatchingByYearDayRRule($rrules['BYYEARDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfYearMatchingByYearDayRRule($rrules['BYYEARDAY'], $frequencyRecurringDateTime);
} elseif (!empty($rrules['BYMONTHDAY'])) { } elseif (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
$matchingDays = $this->getDaysOfYearMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfYearMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
} }
if (!empty($rrules['BYDAY'])) { if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
if (!empty($rrules['BYYEARDAY']) || !empty($rrules['BYMONTHDAY']) || !empty($rrules['BYWEEKNO'])) { if (isset($rrules['BYYEARDAY']) && ($rrules['BYYEARDAY'] !== '' && $rrules['BYYEARDAY'] !== array()) || isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== '' && $rrules['BYMONTHDAY'] !== array()) || isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array())) {
$matchingDays = array_filter( $matchingDays = array_filter(
$this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime), $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
function ($yearDay) use ($matchingDays) { function ($yearDay) use ($matchingDays) {
return in_array($yearDay, $matchingDays); return in_array($yearDay, $matchingDays);
} }
); );
} elseif ($matchingDays === []) { } elseif ($matchingDays === array()) {
$matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
} }
} }
if ($matchingDays === []) { if ($matchingDays === array()) {
$matchingDays = array($frequencyRecurringDateTime->format('z') + 1); $matchingDays = array($frequencyRecurringDateTime->format('z') + 1);
} else { } else {
sort($matchingDays); sort($matchingDays);
} }
if (!empty($rrules['BYSETPOS'])) { if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) {
$matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays); $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
} }
@ -1575,7 +1644,6 @@ class ICal
$day $day
); );
} }
break; break;
} }
@ -1753,6 +1821,7 @@ class ICal
protected function getDaysOfMonthMatchingByDayRRule(array $byDays, $initialDateTime) protected function getDaysOfMonthMatchingByDayRRule(array $byDays, $initialDateTime)
{ {
$matchingDays = array(); $matchingDays = array();
$currentMonth = $initialDateTime->format('n');
foreach ($byDays as $weekday) { foreach ($byDays as $weekday) {
$bydayDateTime = clone $initialDateTime; $bydayDateTime = clone $initialDateTime;
@ -1771,10 +1840,14 @@ class ICal
if ($ordwk < 0) { // -ve {ordwk} if ($ordwk < 0) { // -ve {ordwk}
$bydayDateTime->modify((++$ordwk) . ' week'); $bydayDateTime->modify((++$ordwk) . ' week');
$matchingDays[] = $bydayDateTime->format('j'); if ($bydayDateTime->format('n') === $currentMonth) {
$matchingDays[] = $bydayDateTime->format('j');
}
} elseif ($ordwk > 0) { // +ve {ordwk} } elseif ($ordwk > 0) { // +ve {ordwk}
$bydayDateTime->modify((--$ordwk) . ' week'); $bydayDateTime->modify((--$ordwk) . ' week');
$matchingDays[] = $bydayDateTime->format('j'); if ($bydayDateTime->format('n') === $currentMonth) {
$matchingDays[] = $bydayDateTime->format('j');
}
} else { // No {ordwk} } else { // No {ordwk}
while ($bydayDateTime->format('n') === $initialDateTime->format('n')) { while ($bydayDateTime->format('n') === $initialDateTime->format('n')) {
$matchingDays[] = $bydayDateTime->format('j'); $matchingDays[] = $bydayDateTime->format('j');
@ -1923,9 +1996,10 @@ class ICal
protected function getDaysOfYearMatchingByWeekNoRRule(array $byWeekNums, $initialDateTime) protected function getDaysOfYearMatchingByWeekNoRRule(array $byWeekNums, $initialDateTime)
{ {
// `\DateTime::format('L')` returns 1 if leap year, 0 if not. // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
$isLeapYear = $initialDateTime->format('L'); $isLeapYear = $initialDateTime->format('L');
$firstDayOfTheYear = date_create("first day of January {$initialDateTime->format('Y')}")->format('D'); $initialYear = date_create("first day of January {$initialDateTime->format('Y')}");
$weeksInThisYear = ($firstDayOfTheYear === 'Thu' || $isLeapYear && $firstDayOfTheYear === 'Wed') ? 53 : 52; $firstDayOfTheYear = ($initialYear === false) ? null : $initialYear->format('D');
$weeksInThisYear = ($firstDayOfTheYear === 'Thu' || $isLeapYear && $firstDayOfTheYear === 'Wed') ? 53 : 52;
$matchingWeeks = $this->resolveIndicesOfRange($byWeekNums, $weeksInThisYear); $matchingWeeks = $this->resolveIndicesOfRange($byWeekNums, $weeksInThisYear);
$matchingDays = array(); $matchingDays = array();
@ -2039,7 +2113,7 @@ class ICal
{ {
$events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
if (!empty($events)) { if ($events !== array()) {
foreach ($events as $key => $anEvent) { foreach ($events as $key => $anEvent) {
if (is_null($anEvent) || !$this->isValidDate($anEvent['DTSTART'])) { if (is_null($anEvent) || !$this->isValidDate($anEvent['DTSTART'])) {
unset($events[$key]); unset($events[$key]);
@ -2077,10 +2151,8 @@ class ICal
$events = array(); $events = array();
if (!empty($array)) { foreach ($array as $event) {
foreach ($array as $event) { $events[] = new Event($event);
$events[] = new Event($event);
}
} }
return $events; return $events;
@ -2154,7 +2226,7 @@ class ICal
*/ */
public function hasEvents() public function hasEvents()
{ {
return ($this->events() !== []) ?: false; return ($this->events() !== array()) ?: false;
} }
/** /**
@ -2185,7 +2257,7 @@ class ICal
// Sort events before processing range // Sort events before processing range
$events = $this->sortEventsWithOrder($this->events()); $events = $this->sortEventsWithOrder($this->events());
if (empty($events)) { if ($events === array()) {
return array(); return array();
} }
@ -2193,34 +2265,36 @@ class ICal
if (!is_null($rangeStart)) { if (!is_null($rangeStart)) {
try { try {
$rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->defaultTimeZone)); $rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->getDefaultTimeZone()));
} catch (\Exception $exception) { } catch (\Exception $exception) {
error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})"); error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})");
$rangeStart = false; $rangeStart = false;
} }
} else { } else {
$rangeStart = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone)); $rangeStart = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone()));
} }
if (!is_null($rangeEnd)) { if (!is_null($rangeEnd)) {
try { try {
$rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->defaultTimeZone)); $rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->getDefaultTimeZone()));
} catch (\Exception $exception) { } catch (\Exception $exception) {
error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})"); error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})");
$rangeEnd = false; $rangeEnd = false;
} }
} else { } else {
$rangeEnd = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone)); $rangeEnd = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone()));
$rangeEnd->modify('+20 years'); $rangeEnd->modify('+20 years');
} }
// If start and end are identical and are dates with no times... if ($rangeEnd !== false && $rangeStart !== false) {
if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() === $rangeEnd->getTimestamp()) { // If start and end are identical and are dates with no times...
$rangeEnd->modify('+1 day'); if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() === $rangeEnd->getTimestamp()) {
} $rangeEnd->modify('+1 day');
}
$rangeStart = $rangeStart->getTimestamp(); $rangeStart = $rangeStart->getTimestamp();
$rangeEnd = $rangeEnd->getTimestamp(); $rangeEnd = $rangeEnd->getTimestamp();
}
foreach ($events as $anEvent) { foreach ($events as $anEvent) {
$eventStart = $anEvent->dtstart_array[2]; $eventStart = $anEvent->dtstart_array[2];
@ -2228,7 +2302,8 @@ class ICal
if ( if (
($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range ($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range
|| ($eventEnd !== null || (
$eventEnd !== null
&& ( && (
($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range ($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range
|| ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range || ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range
@ -2239,27 +2314,26 @@ class ICal
} }
} }
if (empty($extendedEvents)) {
return array();
}
return $extendedEvents; return $extendedEvents;
} }
/** /**
* Returns a sorted array of the events following a given string, * Returns a sorted array of the events following a given string
* or `false` if no events exist in the range.
* *
* @param string $interval * @param string $interval
* @return array * @return array
*/ */
public function eventsFromInterval($interval) public function eventsFromInterval($interval)
{ {
$rangeStart = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone)); $timeZone = $this->getDefaultTimeZone();
$rangeEnd = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone)); $rangeStart = new \DateTime('now', new \DateTimeZone($timeZone));
$rangeEnd = new \DateTime('now', new \DateTimeZone($timeZone));
$dateInterval = \DateInterval::createFromDateString($interval); $dateInterval = \DateInterval::createFromDateString($interval);
$rangeEnd->add($dateInterval);
if ($dateInterval instanceof \DateInterval) {
$rangeEnd->add($dateInterval);
}
return $this->eventsFromRange($rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d')); return $this->eventsFromRange($rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d'));
} }
@ -2358,12 +2432,16 @@ class ICal
* *
* @param string $date * @param string $date
* @param \DateInterval $duration * @param \DateInterval $duration
* @param string|null $format * @return \DateTime|false
* @return integer|\DateTime
*/ */
protected function parseDuration($date, $duration, $format = self::UNIX_FORMAT) protected function parseDuration($date, $duration)
{ {
$dateTime = date_create($date); $dateTime = date_create($date);
if ($dateTime === false) {
return false;
}
$dateTime->modify("{$duration->y} year"); $dateTime->modify("{$duration->y} year");
$dateTime->modify("{$duration->m} month"); $dateTime->modify("{$duration->m} month");
$dateTime->modify("{$duration->d} day"); $dateTime->modify("{$duration->d} day");
@ -2371,22 +2449,14 @@ class ICal
$dateTime->modify("{$duration->i} minute"); $dateTime->modify("{$duration->i} minute");
$dateTime->modify("{$duration->s} second"); $dateTime->modify("{$duration->s} second");
if (is_null($format)) { return $dateTime;
$output = $dateTime;
} elseif ($format === self::UNIX_FORMAT) {
$output = $dateTime->getTimestamp();
} else {
$output = $dateTime->format($format);
}
return $output;
} }
/** /**
* Removes unprintable ASCII and UTF-8 characters * Removes unprintable ASCII and UTF-8 characters
* *
* @param string $data * @param string $data
* @return string * @return string|null
*/ */
protected function removeUnprintableChars($data) protected function removeUnprintableChars($data)
{ {
@ -2419,53 +2489,6 @@ class ICal
} }
} }
/**
* Replace all occurrences of the search string with the replacement string.
* Multibyte safe.
*
* @param string|array $search
* @param string|array $replace
* @param string|array $subject
* @param string $encoding
* @param integer $count
* @return array|string
*/
protected static function mb_str_replace($search, $replace, $subject, $encoding = null, &$count = 0) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
{
if (is_array($subject)) {
// Call `mb_str_replace()` for each subject in the array, recursively
foreach ($subject as $key => $value) {
$subject[$key] = self::mb_str_replace($search, $replace, $value, $encoding, $count);
}
} else {
// Normalize $search and $replace so they are both arrays of the same length
$searches = is_array($search) ? array_values($search) : array($search);
$replacements = is_array($replace) ? array_values($replace) : array($replace);
$replacements = array_pad($replacements, count($searches), '');
foreach ($searches as $key => $search) {
if (is_null($encoding)) {
$encoding = mb_detect_encoding($search, 'UTF-8', true);
}
$replace = $replacements[$key];
$searchLen = mb_strlen($search, $encoding);
$sb = array();
while (($offset = mb_strpos($subject, $search, 0, $encoding)) !== false) {
$sb[] = mb_substr($subject, 0, $offset, $encoding);
$subject = mb_substr($subject, $offset + $searchLen, null, $encoding);
++$count;
}
$sb[] = $subject;
$subject = implode($replace, $sb);
}
}
return $subject;
}
/** /**
* Places double-quotes around texts that have characters not permitted * Places double-quotes around texts that have characters not permitted
* in parameter-texts, but are permitted in quoted-texts. * in parameter-texts, but are permitted in quoted-texts.
@ -2483,39 +2506,37 @@ class ICal
} }
/** /**
* Replaces curly quotes and other special characters * Replace curly quotes and other special characters with their standard equivalents
* with their standard equivalents * @see https://utf8-chartable.de/unicode-utf8-table.pl?start=8211&utf8=string-literal
* *
* @param string $data * @param string $input
* @return string * @return string
*/ */
protected function cleanData($data) protected function cleanCharacters($input)
{ {
$replacementChars = array( return strtr(
"\t" => ' ', $input,
"\xe2\x80\x98" => "'", // array(
"\xe2\x80\x99" => "'", // "\xe2\x80\x98" => "'", //
"\xe2\x80\x9a" => "'", // "\xe2\x80\x99" => "'", //
"\xe2\x80\x9b" => "'", // "\xe2\x80\x9a" => "'", //
"\xe2\x80\x9c" => '"', // “ "\xe2\x80\x9b" => "'", //
"\xe2\x80\x9d" => '"', // ” "\xe2\x80\x9c" => '"', // “
"\xe2\x80\x9e" => '"', // „ "\xe2\x80\x9d" => '"', // ”
"\xe2\x80\x9f" => '"', // ‟ "\xe2\x80\x9e" => '"', // „
"\xe2\x80\x93" => '-', // "\xe2\x80\x9f" => '"', // ‟
"\xe2\x80\x94" => '--', // — "\xe2\x80\x93" => '-', //
"\xe2\x80\xa6" => '...', // … "\xe2\x80\x94" => '--', // —
"\xc2\xa0" => ' ', // Non-breaking space "\xe2\x80\xa6" => '...', // …
$this->mb_chr(145) => "'", //
$this->mb_chr(146) => "'", //
$this->mb_chr(147) => '"', // “
$this->mb_chr(148) => '"', // ”
$this->mb_chr(150) => '-', //
$this->mb_chr(151) => '--', // —
$this->mb_chr(133) => '...', // …
)
); );
// Replace UTF-8 characters
$cleanedData = strtr($data, $replacementChars);
// Replace Windows-1252 equivalents
$charsToReplace = array_map(function ($code) {
return $this->mb_chr($code);
}, array(133, 145, 146, 147, 148, 150, 151, 194));
$cleanedData = $this->mb_str_replace($charsToReplace, $replacementChars, $cleanedData);
return $cleanedData;
} }
/** /**
@ -2534,7 +2555,7 @@ class ICal
} }
$output = array(); $output = array();
$currentTimeZone = new \DateTimeZone($this->defaultTimeZone); $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
foreach ($exdates as $subArray) { foreach ($exdates as $subArray) {
end($subArray); end($subArray);
@ -2554,7 +2575,7 @@ class ICal
if ($key === $finalKey) { if ($key === $finalKey) {
// Reset to default // Reset to default
$currentTimeZone = new \DateTimeZone($this->defaultTimeZone); $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
} }
} }
} }
@ -2609,8 +2630,8 @@ class ICal
$options['http'] = array(); $options['http'] = array();
$options['http']['header'] = array(); $options['http']['header'] = array();
if (!empty($this->httpBasicAuth) || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) { if ($this->httpBasicAuth === array() || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) {
if (!empty($this->httpBasicAuth)) { if ($this->httpBasicAuth !== array()) {
$username = $this->httpBasicAuth['username']; $username = $this->httpBasicAuth['username'];
$password = $this->httpBasicAuth['password']; $password = $this->httpBasicAuth['password'];
$basicAuth = base64_encode("{$username}:{$password}"); $basicAuth = base64_encode("{$username}:{$password}");
@ -2678,6 +2699,6 @@ class ICal
return new \DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]); return new \DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]);
} }
return new \DateTimeZone($this->defaultTimeZone); return new \DateTimeZone($this->getDefaultTimeZone());
} }
} }