Update ICal

This commit is contained in:
Patrick Schwarz 2023-04-23 20:07:52 +02:00
parent 3a59408fc5
commit 4d021eb32d
2 changed files with 240 additions and 153 deletions

View file

@ -11,122 +11,129 @@ class Event
/** /**
* https://www.kanzaki.com/docs/ical/summary.html * https://www.kanzaki.com/docs/ical/summary.html
* *
* @var $summary * @var string
*/ */
public $summary; public $summary;
/** /**
* https://www.kanzaki.com/docs/ical/dtstart.html * https://www.kanzaki.com/docs/ical/dtstart.html
* *
* @var $dtstart * @var string
*/ */
public $dtstart; public $dtstart;
/** /**
* https://www.kanzaki.com/docs/ical/dtend.html * https://www.kanzaki.com/docs/ical/dtend.html
* *
* @var $dtend * @var string
*/ */
public $dtend; public $dtend;
/** /**
* https://www.kanzaki.com/docs/ical/duration.html * https://www.kanzaki.com/docs/ical/duration.html
* *
* @var $duration * @var string
*/ */
public $duration; public $duration;
/** /**
* https://www.kanzaki.com/docs/ical/dtstamp.html * https://www.kanzaki.com/docs/ical/dtstamp.html
* *
* @var $dtstamp * @var string
*/ */
public $dtstamp; public $dtstamp;
/** /**
* When the event starts, represented as a timezone-adjusted string * When the event starts, represented as a timezone-adjusted string
* *
* @var $dtstart_tz * @var string
*/ */
public $dtstart_tz; public $dtstart_tz;
/** /**
* When the event ends, represented as a timezone-adjusted string * When the event ends, represented as a timezone-adjusted string
* *
* @var $dtend_tz * @var string
*/ */
public $dtend_tz; public $dtend_tz;
/** /**
* https://www.kanzaki.com/docs/ical/uid.html * https://www.kanzaki.com/docs/ical/uid.html
* *
* @var $uid * @var string
*/ */
public $uid; public $uid;
/** /**
* https://www.kanzaki.com/docs/ical/created.html * https://www.kanzaki.com/docs/ical/created.html
* *
* @var $created * @var string
*/ */
public $created; public $created;
/** /**
* https://www.kanzaki.com/docs/ical/lastModified.html * https://www.kanzaki.com/docs/ical/lastModified.html
* *
* @var $lastmodified * @var string
*/ */
public $lastmodified; public $last_modified;
/** /**
* https://www.kanzaki.com/docs/ical/description.html * https://www.kanzaki.com/docs/ical/description.html
* *
* @var $description * @var string
*/ */
public $description; public $description;
/** /**
* https://www.kanzaki.com/docs/ical/location.html * https://www.kanzaki.com/docs/ical/location.html
* *
* @var $location * @var string
*/ */
public $location; public $location;
/** /**
* https://www.kanzaki.com/docs/ical/sequence.html * https://www.kanzaki.com/docs/ical/sequence.html
* *
* @var $sequence * @var string
*/ */
public $sequence; public $sequence;
/** /**
* https://www.kanzaki.com/docs/ical/status.html * https://www.kanzaki.com/docs/ical/status.html
* *
* @var $status * @var string
*/ */
public $status; public $status;
/** /**
* https://www.kanzaki.com/docs/ical/transp.html * https://www.kanzaki.com/docs/ical/transp.html
* *
* @var $transp * @var string
*/ */
public $transp; public $transp;
/** /**
* https://www.kanzaki.com/docs/ical/organizer.html * https://www.kanzaki.com/docs/ical/organizer.html
* *
* @var $organizer * @var string
*/ */
public $organizer; public $organizer;
/** /**
* https://www.kanzaki.com/docs/ical/attendee.html * https://www.kanzaki.com/docs/ical/attendee.html
* *
* @var $attendee * @var string
*/ */
public $attendee; public $attendee;
/**
* Manage additional properties
*
* @var array<string, mixed>
*/
private $additionalProperties = [];
/** /**
* Creates the Event object * Creates the Event object
* *
@ -137,10 +144,40 @@ class Event
{ {
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
$variable = self::snakeCase($key); $variable = self::snakeCase($key);
$this->{$variable} = self::prepareData($value); if (property_exists($this, $variable)) {
$this->{$variable} = $this->prepareData($value);
} else {
$this->additionalProperties[$variable] = $this->prepareData($value);
}
} }
} }
/**
* Magic getter method
*
* @param string $additionalPropertyName
* @return mixed
*/
public function __get($additionalPropertyName)
{
if (array_key_exists($additionalPropertyName, $this->additionalProperties)) {
return $this->additionalProperties[$additionalPropertyName];
}
return null;
}
/**
* Magic isset method
*
* @param string $name
* @return boolean
*/
public function __isset($name)
{
return is_null($this->$name) === false;
}
/** /**
* Prepares the data for output * Prepares the data for output
* *
@ -151,8 +188,12 @@ class Event
{ {
if (is_string($value)) { if (is_string($value)) {
return stripslashes(trim(str_replace('\n', "\n", $value))); return stripslashes(trim(str_replace('\n', "\n", $value)));
} elseif (is_array($value)) { }
return array_map('self::prepareData', $value);
if (is_array($value)) {
return array_map(function ($value) {
return $this->prepareData($value);
}, $value);
} }
return $value; return $value;
@ -177,7 +218,7 @@ class Event
'DTSTAMP' => $this->dtstamp, 'DTSTAMP' => $this->dtstamp,
'UID' => $this->uid, 'UID' => $this->uid,
'CREATED' => $this->created, 'CREATED' => $this->created,
'LAST-MODIFIED' => $this->lastmodified, 'LAST-MODIFIED' => $this->last_modified,
'DESCRIPTION' => $this->description, 'DESCRIPTION' => $this->description,
'LOCATION' => $this->location, 'LOCATION' => $this->location,
'SEQUENCE' => $this->sequence, 'SEQUENCE' => $this->sequence,

View file

@ -4,11 +4,11 @@
* This PHP class will read an ICS (`.ics`, `.ical`, `.ifb`) file, parse it and return an * This PHP class will read an ICS (`.ics`, `.ical`, `.ifb`) file, parse it and return an
* array of its contents. * array of its contents.
* *
* PHP 5 ( 5.3.9) * PHP 5 ( 5.6.40)
* *
* @author Jonathan Goode <https://github.com/u01jmg3> * @author Jonathan Goode <https://github.com/u01jmg3>
* @license https://opensource.org/licenses/mit-license.php MIT License * @license https://opensource.org/licenses/mit-license.php MIT License
* @version 2.2.2 * @version 3.2.0
*/ */
namespace ICal; namespace ICal;
@ -66,7 +66,7 @@ class ICal
/** /**
* Enables customisation of the default time zone * Enables customisation of the default time zone
* *
* @var string * @var string|null
*/ */
public $defaultTimeZone; public $defaultTimeZone;
@ -94,14 +94,14 @@ class ICal
/** /**
* With this being non-null the parser will ignore all events more than roughly this many days after now. * With this being non-null the parser will ignore all events more than roughly this many days after now.
* *
* @var integer * @var integer|null
*/ */
public $filterDaysBefore; public $filterDaysBefore;
/** /**
* With this being non-null the parser will ignore all events more than roughly this many days before now. * With this being non-null the parser will ignore all events more than roughly this many days before now.
* *
* @var integer * @var integer|null
*/ */
public $filterDaysAfter; public $filterDaysAfter;
@ -190,6 +190,13 @@ class ICal
*/ */
protected $httpAcceptLanguage; protected $httpAcceptLanguage;
/**
* Holds the custom HTTP Protocol version
*
* @var string
*/
protected $httpProtocolVersion;
/** /**
* Define which variables can be configured * Define which variables can be configured
* *
@ -202,6 +209,7 @@ class ICal
'disableCharacterReplacement', 'disableCharacterReplacement',
'filterDaysAfter', 'filterDaysAfter',
'filterDaysBefore', 'filterDaysBefore',
'httpUserAgent',
'skipRecurrence', 'skipRecurrence',
); );
@ -497,7 +505,9 @@ class ICal
*/ */
public function __construct($files = false, array $options = array()) public function __construct($files = false, array $options = array())
{ {
ini_set('auto_detect_line_endings', '1'); if (\PHP_VERSION_ID < 80100) {
ini_set('auto_detect_line_endings', '1');
}
foreach ($options as $option => $value) { foreach ($options as $option => $value) {
if (in_array($option, self::$configurableOptions)) { if (in_array($option, self::$configurableOptions)) {
@ -581,9 +591,10 @@ class ICal
* @param string $password * @param string $password
* @param string $userAgent * @param string $userAgent
* @param string $acceptLanguage * @param string $acceptLanguage
* @param string $httpProtocolVersion
* @return ICal * @return ICal
*/ */
public function initUrl($url, $username = null, $password = null, $userAgent = null, $acceptLanguage = null) public function initUrl($url, $username = null, $password = null, $userAgent = null, $acceptLanguage = null, $httpProtocolVersion = null)
{ {
if (!is_null($username) && !is_null($password)) { if (!is_null($username) && !is_null($password)) {
$this->httpBasicAuth['username'] = $username; $this->httpBasicAuth['username'] = $username;
@ -598,6 +609,10 @@ class ICal
$this->httpAcceptLanguage = $acceptLanguage; $this->httpAcceptLanguage = $acceptLanguage;
} }
if (!is_null($httpProtocolVersion)) {
$this->httpProtocolVersion = $httpProtocolVersion;
}
$this->initFile($url); $this->initFile($url);
return $this; return $this;
@ -853,6 +868,7 @@ class ICal
protected function unfold(array $lines) protected function unfold(array $lines)
{ {
$string = implode(PHP_EOL, $lines); $string = implode(PHP_EOL, $lines);
$string = str_ireplace('&nbsp;', ' ', $string);
$string = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string); $string = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
$lines = explode(PHP_EOL, $string); $lines = explode(PHP_EOL, $string);
@ -865,12 +881,12 @@ class ICal
* *
* @param string $component * @param string $component
* @param string|boolean $keyword * @param string|boolean $keyword
* @param string $value * @param string|array $value
* @return void * @return void
*/ */
protected function addCalendarComponentWithKeyAndValue($component, $keyword, $value) protected function addCalendarComponentWithKeyAndValue($component, $keyword, $value)
{ {
if ($keyword == false) { if ($keyword === false) {
$keyword = $this->lastKeyword; $keyword = $this->lastKeyword;
} }
@ -896,6 +912,7 @@ class ICal
$this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value; $this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value;
} }
} }
break; break;
case 'VEVENT': case 'VEVENT':
@ -935,6 +952,7 @@ class ICal
$this->cal[$key1][$key2][$keyword] .= ',' . $value; $this->cal[$key1][$key2][$keyword] .= ',' . $value;
} }
} }
break; break;
case 'VFREEBUSY': case 'VFREEBUSY':
@ -957,6 +975,7 @@ class ICal
} else { } else {
$this->cal[$key1][$key2][$key3][] = $value; $this->cal[$key1][$key2][$key3][] = $value;
} }
break; break;
case 'VTODO': case 'VTODO':
@ -979,95 +998,109 @@ class ICal
* @param string $text * @param string $text
* @return array|boolean * @return array|boolean
*/ */
protected function keyValueFromString($text) public function keyValueFromString($text)
{ {
$text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); $splitLine = $this->parseLine($text);
$object = array();
$paramObj = array();
$valueObj = '';
$i = 0;
$colon = strpos($text, ':'); while ($i < count($splitLine)) {
$quote = strpos($text, '"'); // The first token corresponds to the property name
if ($colon === false) { if ($i === 0) {
$matches = array(); $object[0] = $splitLine[$i];
} elseif ($quote === false || $colon < $quote) { $i++;
list($before, $after) = explode(':', $text, 2); continue;
$matches = array($text, $before, $after); }
} else {
list($before, $text) = explode('"', $text, 2);
$text = '"' . $text;
$matches = str_getcsv($text, ':');
$combinedValue = '';
foreach (array_keys($matches) as $key) { // After each semicolon define the property parameters
if ($key === 0) { if ($splitLine[$i] == ';') {
if (!empty($before)) { $i++;
$matches[$key] = $before . '"' . $matches[$key] . '"'; $paramName = $splitLine[$i];
} $i += 2;
$paramValue = array();
$multiValue = false;
// A parameter can have multiple values separated by a comma
while ($i + 1 < count($splitLine) && $splitLine[$i + 1] === ',') {
$paramValue[] = $splitLine[$i];
$i += 2;
$multiValue = true;
}
if ($multiValue) {
$paramValue[] = $splitLine[$i];
} else { } else {
if ($key > 1) { $paramValue = $splitLine[$i];
$combinedValue .= ':'; }
}
$combinedValue .= $matches[$key]; // Create object with paramName => paramValue
$paramObj[$paramName] = $paramValue;
}
// After a colon all tokens are concatenated (non-standard behaviour because the property can have multiple values
// according to RFC5545)
if ($splitLine[$i] === ':') {
$i++;
while ($i < count($splitLine)) {
$valueObj .= $splitLine[$i];
$i++;
} }
} }
$matches = array_slice($matches, 0, 2); $i++;
$matches[1] = $combinedValue;
array_unshift($matches, $before . $text);
} }
if (count($matches) === 0) { // Object construction
return false; if ($paramObj !== []) {
} $object[1][0] = $valueObj;
$object[1][1] = $paramObj;
if (preg_match('/^([A-Z-]+)([;][\w\W]*)?$/', $matches[1])) {
$matches = array_splice($matches, 1, 2); // Remove first match and re-align ordering
// Process properties
if (preg_match('/([A-Z-]+)[;]([\w\W]*)/', $matches[0], $properties)) {
// Remove first match
array_shift($properties);
// Fix to ignore everything in keyword after a ; (e.g. Language, TZID, etc.)
$matches[0] = $properties[0];
array_shift($properties); // Repeat removing first match
$formatted = array();
foreach ($properties as $property) {
// Match semicolon separator outside of quoted substrings
preg_match_all('~[^' . PHP_EOL . '";]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '";]*)*~', $property, $attributes);
// Remove multi-dimensional array and use the first key
$attributes = (count($attributes) === 0) ? array($property) : reset($attributes);
if (is_array($attributes)) {
foreach ($attributes as $attribute) {
// Match equals sign separator outside of quoted substrings
preg_match_all(
'~[^' . PHP_EOL . '"=]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '"=]*)*~',
$attribute,
$values
);
// Remove multi-dimensional array and use the first key
$value = (count($values) === 0) ? null : reset($values);
if (is_array($value) && isset($value[1])) {
// Remove double quotes from beginning and end only
$formatted[$value[0]] = trim($value[1], '"');
}
}
}
}
// Assign the keyword property information
$properties[0] = $formatted;
// Add match to beginning of array
array_unshift($properties, $matches[1]);
$matches[1] = $properties;
}
return $matches;
} else { } else {
return false; // Ignore this match $object[1] = $valueObj;
} }
return $object;
}
/**
* Parses a line from an iCal file into an array of tokens
*
* @param string $line
* @return array
*/
protected function parseLine($line)
{
$words = array();
$word = '';
// The use of str_split is not a problem here even if the character set is in utf8
// Indeed we only compare the characters , ; : = " which are on a single byte
$arrayOfChar = str_split($line);
$inDoubleQuotes = false;
foreach ($arrayOfChar as $char) {
// Don't stop the word on ; , : = if it is enclosed in double quotes
if ($char === '"') {
if ($word !== '') {
$words[] = $word;
}
$word = '';
$inDoubleQuotes = !$inDoubleQuotes;
} elseif (!in_array($char, array(';', ':', ',', '=')) || $inDoubleQuotes) {
$word .= $char;
} else {
if ($word !== '') {
$words[] = $word;
}
$words[] = $char;
$word = '';
}
}
$words[] = $word;
return $words;
} }
/** /**
@ -1141,10 +1174,10 @@ class ICal
/** /**
* Returns a date adapted to the calendar time zone depending on the event `TZID` * Returns a date adapted to the calendar time zone depending on the event `TZID`
* *
* @param array $event * @param array $event
* @param string $key * @param string $key
* @param string $format * @param string|null $format
* @return string|boolean * @return string|boolean|\DateTime
*/ */
public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT) public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT)
{ {
@ -1224,7 +1257,7 @@ class ICal
$eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3]); $eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3]);
// phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
if (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']])) !== false) { if (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']], true)) !== false) {
$eventKeysToRemove[] = $alteredEventKey; $eventKeysToRemove[] = $alteredEventKey;
$alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]); $alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]);
@ -1273,7 +1306,7 @@ class ICal
// 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 (explode(';', $anEvent['RRULE']) as $s) { foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) {
list($k, $v) = explode('=', $s); list($k, $v) = explode('=', $s);
if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) { if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) {
$rrules[$k] = explode(',', $v); $rrules[$k] = explode(',', $v);
@ -1311,7 +1344,7 @@ class ICal
} }
// Get Interval // Get Interval
$interval = (empty($rrules['INTERVAL'])) ? 1 : $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.
if (!is_int($this->defaultSpan)) { if (!is_int($this->defaultSpan)) {
@ -1323,7 +1356,7 @@ class ICal
// Determine if the initial date is also an EXDATE // Determine if the initial date is also an EXDATE
$initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) { $initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) {
return $carry || $exdate->getTimestamp() == $initialEventDate->getTimestamp(); return $carry || $exdate->getTimestamp() === $initialEventDate->getTimestamp();
}, false); }, false);
if ($initialDateIsExdate) { if ($initialDateIsExdate) {
@ -1346,7 +1379,7 @@ class ICal
* enddate = <icalDate> || <icalDateTime> * enddate = <icalDate> || <icalDateTime>
*/ */
$count = 1; $count = 1;
$countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : 0; $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX;
$until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp(); $until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
if (isset($rrules['UNTIL'])) { if (isset($rrules['UNTIL'])) {
@ -1356,7 +1389,7 @@ class ICal
$eventRecurrences = array(); $eventRecurrences = array();
$frequencyRecurringDateTime = clone $initialEventDate; $frequencyRecurringDateTime = clone $initialEventDate;
while ($frequencyRecurringDateTime->getTimestamp() <= $until) { while ($frequencyRecurringDateTime->getTimestamp() <= $until && $count < $countLimit) {
$candidateDateTimes = array(); $candidateDateTimes = array();
// phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
@ -1389,15 +1422,15 @@ class ICal
if (empty($rrules['WKST'])) { if (empty($rrules['WKST'])) {
if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) { if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) {
$wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays)); $wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays), true);
} }
} elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) { } elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) {
$wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays)); $wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays), true);
} }
$matchingDays = array_map( $matchingDays = array_map(
function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) { function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) {
$day = array_search($weekday, array_keys($this->weekdays)); $day = array_search($weekday, array_keys($this->weekdays), true);
if ($day < $initialDayOfWeek) { if ($day < $initialDayOfWeek) {
$day += 7; $day += 7;
@ -1424,11 +1457,12 @@ class ICal
foreach ($matchingDays as $day) { foreach ($matchingDays as $day) {
$clonedDateTime = clone $frequencyRecurringDateTime; $clonedDateTime = clone $frequencyRecurringDateTime;
$candidateDateTimes[] = $clonedDateTime->setISODate( $candidateDateTimes[] = $clonedDateTime->setISODate(
$frequencyRecurringDateTime->format('o'), (int) $frequencyRecurringDateTime->format('o'),
$frequencyRecurringDateTime->format('W'), (int) $frequencyRecurringDateTime->format('W'),
$day $day
); );
} }
break; break;
case 'MONTHLY': case 'MONTHLY':
@ -1446,6 +1480,8 @@ class ICal
} }
} elseif (!empty($rrules['BYDAY'])) { } elseif (!empty($rrules['BYDAY'])) {
$matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
} else {
$matchingDays[] = $frequencyRecurringDateTime->format('d');
} }
if (!empty($rrules['BYSETPOS'])) { if (!empty($rrules['BYSETPOS'])) {
@ -1460,11 +1496,12 @@ class ICal
$clonedDateTime = clone $frequencyRecurringDateTime; $clonedDateTime = clone $frequencyRecurringDateTime;
$candidateDateTimes[] = $clonedDateTime->setDate( $candidateDateTimes[] = $clonedDateTime->setDate(
$frequencyRecurringDateTime->format('Y'), (int) $frequencyRecurringDateTime->format('Y'),
$frequencyRecurringDateTime->format('m'), (int) $frequencyRecurringDateTime->format('m'),
$day $day
); );
} }
break; break;
case 'YEARLY': case 'YEARLY':
@ -1474,9 +1511,9 @@ class ICal
$bymonthRecurringDatetime = clone $frequencyRecurringDateTime; $bymonthRecurringDatetime = clone $frequencyRecurringDateTime;
foreach ($rrules['BYMONTH'] as $byMonth) { foreach ($rrules['BYMONTH'] as $byMonth) {
$bymonthRecurringDatetime->setDate( $bymonthRecurringDatetime->setDate(
$frequencyRecurringDateTime->format('Y'), (int) $frequencyRecurringDateTime->format('Y'),
$byMonth, $byMonth,
$frequencyRecurringDateTime->format('d') (int) $frequencyRecurringDateTime->format('d')
); );
// Determine the days of the month affected // Determine the days of the month affected
@ -1493,8 +1530,8 @@ class ICal
// And add each of them to the list of recurrences // And add each of them to the list of recurrences
foreach ($monthDays as $day) { foreach ($monthDays as $day) {
$matchingDays[] = $bymonthRecurringDatetime->setDate( $matchingDays[] = $bymonthRecurringDatetime->setDate(
$frequencyRecurringDateTime->format('Y'), (int) $frequencyRecurringDateTime->format('Y'),
$bymonthRecurringDatetime->format('m'), (int) $bymonthRecurringDatetime->format('m'),
$day $day
)->format('z') + 1; )->format('z') + 1;
} }
@ -1515,12 +1552,12 @@ class ICal
return in_array($yearDay, $matchingDays); return in_array($yearDay, $matchingDays);
} }
); );
} elseif (count($matchingDays) === 0) { } elseif ($matchingDays === []) {
$matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime); $matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
} }
} }
if (count($matchingDays) === 0) { if ($matchingDays === []) {
$matchingDays = array($frequencyRecurringDateTime->format('z') + 1); $matchingDays = array($frequencyRecurringDateTime->format('z') + 1);
} else { } else {
sort($matchingDays); sort($matchingDays);
@ -1533,11 +1570,12 @@ class ICal
foreach ($matchingDays as $day) { foreach ($matchingDays as $day) {
$clonedDateTime = clone $frequencyRecurringDateTime; $clonedDateTime = clone $frequencyRecurringDateTime;
$candidateDateTimes[] = $clonedDateTime->setDate( $candidateDateTimes[] = $clonedDateTime->setDate(
$frequencyRecurringDateTime->format('Y'), (int) $frequencyRecurringDateTime->format('Y'),
1, 1,
$day $day
); );
} }
break; break;
} }
@ -1553,7 +1591,7 @@ class ICal
// Exclusions // Exclusions
$isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) { $isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) {
return $exdate->getTimestamp() == $timestamp; return $exdate->getTimestamp() === $timestamp;
}); });
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
@ -1567,14 +1605,11 @@ class ICal
$this->eventCount++; $this->eventCount++;
} }
// Count all evaluated candidates including excluded ones // Count all evaluated candidates including excluded ones,
if (isset($rrules['COUNT'])) { // and if RRULE[COUNT] (if set) is reached then break.
$count++; $count++;
if ($count >= $countLimit) {
// If RRULE[COUNT] is reached then break break 2;
if ($count >= $countLimit) {
break 2;
}
} }
} }
@ -1771,7 +1806,7 @@ class ICal
*/ */
protected function getDaysOfMonthMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime) protected function getDaysOfMonthMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
{ {
return $this->resolveIndicesOfRange($byMonthDays, $initialDateTime->format('t')); return $this->resolveIndicesOfRange($byMonthDays, (int) $initialDateTime->format('t'));
} }
/** /**
@ -1897,7 +1932,7 @@ class ICal
$byweekDateTime = clone $initialDateTime; $byweekDateTime = clone $initialDateTime;
foreach ($matchingWeeks as $weekNum) { foreach ($matchingWeeks as $weekNum) {
$dayNum = $byweekDateTime->setISODate( $dayNum = $byweekDateTime->setISODate(
$initialDateTime->format('Y'), (int) $initialDateTime->format('Y'),
$weekNum, $weekNum,
1 1
)->format('z') + 1; )->format('z') + 1;
@ -1932,7 +1967,7 @@ class ICal
$monthDateTime = clone $initialDateTime; $monthDateTime = clone $initialDateTime;
for ($month = 1; $month < 13; $month++) { for ($month = 1; $month < 13; $month++) {
$monthDateTime->setDate( $monthDateTime->setDate(
$initialDateTime->format('Y'), (int) $initialDateTime->format('Y'),
$month, $month,
1 1
); );
@ -1940,8 +1975,8 @@ class ICal
$monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($byMonthDays, $monthDateTime); $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($byMonthDays, $monthDateTime);
foreach ($monthDays as $day) { foreach ($monthDays as $day) {
$matchingDays[] = $monthDateTime->setDate( $matchingDays[] = $monthDateTime->setDate(
$initialDateTime->format('Y'), (int) $initialDateTime->format('Y'),
$monthDateTime->format('m'), (int) $monthDateTime->format('m'),
$day $day
)->format('z') + 1; )->format('z') + 1;
} }
@ -2075,7 +2110,7 @@ class ICal
* Returns the calendar time zone * Returns the calendar time zone
* *
* @param boolean $ignoreUtc * @param boolean $ignoreUtc
* @return string * @return string|null
*/ */
public function calendarTimeZone($ignoreUtc = false) public function calendarTimeZone($ignoreUtc = false)
{ {
@ -2119,7 +2154,7 @@ class ICal
*/ */
public function hasEvents() public function hasEvents()
{ {
return (count($this->events()) > 0) ?: false; return ($this->events() !== []) ?: false;
} }
/** /**
@ -2321,9 +2356,9 @@ class ICal
/** /**
* Parses a duration and applies it to a date * Parses a duration and applies it to a date
* *
* @param string $date * @param string $date
* @param string $duration * @param \DateInterval $duration
* @param string $format * @param string|null $format
* @return integer|\DateTime * @return integer|\DateTime
*/ */
protected function parseDuration($date, $duration, $format = self::UNIX_FORMAT) protected function parseDuration($date, $duration, $format = self::UNIX_FORMAT)
@ -2457,6 +2492,7 @@ class ICal
protected function cleanData($data) protected function cleanData($data)
{ {
$replacementChars = array( $replacementChars = array(
"\t" => ' ',
"\xe2\x80\x98" => "'", // "\xe2\x80\x98" => "'", //
"\xe2\x80\x99" => "'", // "\xe2\x80\x99" => "'", //
"\xe2\x80\x9a" => "'", // "\xe2\x80\x9a" => "'", //
@ -2468,7 +2504,7 @@ class ICal
"\xe2\x80\x93" => '-', // "\xe2\x80\x93" => '-', //
"\xe2\x80\x94" => '--', // — "\xe2\x80\x94" => '--', // —
"\xe2\x80\xa6" => '...', // … "\xe2\x80\xa6" => '...', // …
"\xc2\xa0" => ' ', "\xc2\xa0" => ' ', // Non-breaking space
); );
// Replace UTF-8 characters // Replace UTF-8 characters
$cleanedData = strtr($data, $replacementChars); $cleanedData = strtr($data, $replacementChars);
@ -2591,7 +2627,17 @@ class ICal
} }
} }
$options['http']['protocol_version'] = '1.1'; if (empty($this->httpUserAgent)) {
if (mb_stripos($filename, 'outlook.office365.com') !== false) {
$options['http']['header'][] = 'User-Agent: A User Agent';
}
}
if (!empty($this->httpProtocolVersion)) {
$options['http']['protocol_version'] = $this->httpProtocolVersion;
} else {
$options['http']['protocol_version'] = '1.1';
}
$options['http']['header'][] = 'Connection: close'; $options['http']['header'][] = 'Connection: close';