From 7acdf377d73b37c8b2dec1ac2f4c9d8dba91140e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 23:47:29 +0000 Subject: [PATCH 01/38] Bump actions/cache from 3 to 4 Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d665e841..2f5ba8c90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} # Use composer.json for key, if composer.lock is not committed. From 5c3a53299518bd7d199489dca5a97aa520a58491 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 2 Feb 2024 08:45:33 +0545 Subject: [PATCH 02/38] Apply cs-fixer changes --- bin/bench_freebusygenerator.php | 6 +- lib/Component.php | 3 +- lib/Component/VCalendar.php | 148 +++++++++++----------- lib/DateTimeParser.php | 4 +- lib/Document.php | 8 +- lib/FreeBusyGenerator.php | 4 +- lib/ITip/Broker.php | 8 +- lib/Property.php | 2 +- lib/Property/ICalendar/DateTime.php | 4 +- lib/Property/Text.php | 2 +- lib/Recur/EventIterator.php | 2 +- lib/Splitter/ICalendar.php | 2 +- lib/Splitter/VCard.php | 2 +- lib/TimeZoneUtil.php | 4 +- lib/VCardConverter.php | 12 +- tests/VObject/Component/VCalendarTest.php | 4 +- tests/VObject/Component/VCardTest.php | 4 +- tests/VObject/Component/VFreeBusyTest.php | 3 +- tests/VObject/ComponentTest.php | 12 +- tests/VObject/EmptyParameterTest.php | 2 +- tests/VObject/FreeBusyGeneratorTest.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 2 +- tests/VObject/VCardConverterTest.php | 2 +- 23 files changed, 119 insertions(+), 123 deletions(-) diff --git a/bin/bench_freebusygenerator.php b/bin/bench_freebusygenerator.php index 891335190..ae859f487 100644 --- a/bin/bench_freebusygenerator.php +++ b/bin/bench_freebusygenerator.php @@ -24,9 +24,9 @@ $bench->parse->stop(); $repeat = 100; -$start = new \DateTime('2000-01-01'); -$end = new \DateTime('2020-01-01'); -$timeZone = new \DateTimeZone('America/Toronto'); +$start = new DateTime('2000-01-01'); +$end = new DateTime('2020-01-01'); +$timeZone = new DateTimeZone('America/Toronto'); $bench->fb->start(); diff --git a/lib/Component.php b/lib/Component.php index da2c5ebd4..95bde3d40 100644 --- a/lib/Component.php +++ b/lib/Component.php @@ -2,7 +2,6 @@ namespace Sabre\VObject; -use Sabre\VObject; use Sabre\Xml; /** @@ -15,7 +14,7 @@ * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License * - * @property VObject\Property\FlatText UID + * @property Property\FlatText UID */ class Component extends Node { diff --git a/lib/Component/VCalendar.php b/lib/Component/VCalendar.php index b317e02c8..318492ce6 100644 --- a/lib/Component/VCalendar.php +++ b/lib/Component/VCalendar.php @@ -20,8 +20,8 @@ * * @property VEvent VEVENT * @property VJournal VJOURNAL - * @property VObject\Property\Text ORG - * @property VObject\Property\FlatText METHOD + * @property Property\Text ORG + * @property Property\FlatText METHOD */ class VCalendar extends VObject\Document { @@ -51,21 +51,21 @@ class VCalendar extends VObject\Document * List of value-types, and which classes they map to. */ public static array $valueMap = [ - 'BINARY' => VObject\Property\Binary::class, - 'BOOLEAN' => VObject\Property\Boolean::class, - 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, - 'DATE' => VObject\Property\ICalendar\Date::class, - 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, - 'DURATION' => VObject\Property\ICalendar\Duration::class, - 'FLOAT' => VObject\Property\FloatValue::class, - 'INTEGER' => VObject\Property\IntegerValue::class, - 'PERIOD' => VObject\Property\ICalendar\Period::class, - 'RECUR' => VObject\Property\ICalendar\Recur::class, - 'TEXT' => VObject\Property\Text::class, - 'TIME' => VObject\Property\Time::class, - 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. - 'URI' => VObject\Property\Uri::class, - 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + 'BINARY' => Property\Binary::class, + 'BOOLEAN' => Property\Boolean::class, + 'CAL-ADDRESS' => Property\ICalendar\CalAddress::class, + 'DATE' => Property\ICalendar\Date::class, + 'DATE-TIME' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FLOAT' => Property\FloatValue::class, + 'INTEGER' => Property\IntegerValue::class, + 'PERIOD' => Property\ICalendar\Period::class, + 'RECUR' => Property\ICalendar\Recur::class, + 'TEXT' => Property\Text::class, + 'TIME' => Property\Time::class, + 'UNKNOWN' => Property\Unknown::class, // jCard / jCal-only. + 'URI' => Property\Uri::class, + 'UTC-OFFSET' => Property\UtcOffset::class, ]; /** @@ -73,78 +73,78 @@ class VCalendar extends VObject\Document */ public static array $propertyMap = [ // Calendar properties - 'CALSCALE' => VObject\Property\FlatText::class, - 'METHOD' => VObject\Property\FlatText::class, - 'PRODID' => VObject\Property\FlatText::class, - 'VERSION' => VObject\Property\FlatText::class, + 'CALSCALE' => Property\FlatText::class, + 'METHOD' => Property\FlatText::class, + 'PRODID' => Property\FlatText::class, + 'VERSION' => Property\FlatText::class, // Component properties - 'ATTACH' => VObject\Property\Uri::class, - 'CATEGORIES' => VObject\Property\Text::class, - 'CLASS' => VObject\Property\FlatText::class, - 'COMMENT' => VObject\Property\FlatText::class, - 'DESCRIPTION' => VObject\Property\FlatText::class, - 'GEO' => VObject\Property\FloatValue::class, - 'LOCATION' => VObject\Property\FlatText::class, - 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, - 'PRIORITY' => VObject\Property\IntegerValue::class, - 'RESOURCES' => VObject\Property\Text::class, - 'STATUS' => VObject\Property\FlatText::class, - 'SUMMARY' => VObject\Property\FlatText::class, + 'ATTACH' => Property\Uri::class, + 'CATEGORIES' => Property\Text::class, + 'CLASS' => Property\FlatText::class, + 'COMMENT' => Property\FlatText::class, + 'DESCRIPTION' => Property\FlatText::class, + 'GEO' => Property\FloatValue::class, + 'LOCATION' => Property\FlatText::class, + 'PERCENT-COMPLETE' => Property\IntegerValue::class, + 'PRIORITY' => Property\IntegerValue::class, + 'RESOURCES' => Property\Text::class, + 'STATUS' => Property\FlatText::class, + 'SUMMARY' => Property\FlatText::class, // Date and Time Component Properties - 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, - 'DTEND' => VObject\Property\ICalendar\DateTime::class, - 'DUE' => VObject\Property\ICalendar\DateTime::class, - 'DTSTART' => VObject\Property\ICalendar\DateTime::class, - 'DURATION' => VObject\Property\ICalendar\Duration::class, - 'FREEBUSY' => VObject\Property\ICalendar\Period::class, - 'TRANSP' => VObject\Property\FlatText::class, + 'COMPLETED' => Property\ICalendar\DateTime::class, + 'DTEND' => Property\ICalendar\DateTime::class, + 'DUE' => Property\ICalendar\DateTime::class, + 'DTSTART' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FREEBUSY' => Property\ICalendar\Period::class, + 'TRANSP' => Property\FlatText::class, // Time Zone Component Properties - 'TZID' => VObject\Property\FlatText::class, - 'TZNAME' => VObject\Property\FlatText::class, - 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, - 'TZOFFSETTO' => VObject\Property\UtcOffset::class, - 'TZURL' => VObject\Property\Uri::class, + 'TZID' => Property\FlatText::class, + 'TZNAME' => Property\FlatText::class, + 'TZOFFSETFROM' => Property\UtcOffset::class, + 'TZOFFSETTO' => Property\UtcOffset::class, + 'TZURL' => Property\Uri::class, // Relationship Component Properties - 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, - 'CONTACT' => VObject\Property\FlatText::class, - 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, - 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, - 'RELATED-TO' => VObject\Property\FlatText::class, - 'URL' => VObject\Property\Uri::class, - 'UID' => VObject\Property\FlatText::class, + 'ATTENDEE' => Property\ICalendar\CalAddress::class, + 'CONTACT' => Property\FlatText::class, + 'ORGANIZER' => Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => Property\ICalendar\DateTime::class, + 'RELATED-TO' => Property\FlatText::class, + 'URL' => Property\Uri::class, + 'UID' => Property\FlatText::class, // Recurrence Component Properties - 'EXDATE' => VObject\Property\ICalendar\DateTime::class, - 'RDATE' => VObject\Property\ICalendar\DateTime::class, - 'RRULE' => VObject\Property\ICalendar\Recur::class, - 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 + 'EXDATE' => Property\ICalendar\DateTime::class, + 'RDATE' => Property\ICalendar\DateTime::class, + 'RRULE' => Property\ICalendar\Recur::class, + 'EXRULE' => Property\ICalendar\Recur::class, // Deprecated since rfc5545 // Alarm Component Properties - 'ACTION' => VObject\Property\FlatText::class, - 'REPEAT' => VObject\Property\IntegerValue::class, - 'TRIGGER' => VObject\Property\ICalendar\Duration::class, + 'ACTION' => Property\FlatText::class, + 'REPEAT' => Property\IntegerValue::class, + 'TRIGGER' => Property\ICalendar\Duration::class, // Change Management Component Properties - 'CREATED' => VObject\Property\ICalendar\DateTime::class, - 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, - 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, - 'SEQUENCE' => VObject\Property\IntegerValue::class, + 'CREATED' => Property\ICalendar\DateTime::class, + 'DTSTAMP' => Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => Property\ICalendar\DateTime::class, + 'SEQUENCE' => Property\IntegerValue::class, // Request Status - 'REQUEST-STATUS' => VObject\Property\Text::class, + 'REQUEST-STATUS' => Property\Text::class, // Additions from draft-daboo-valarm-extensions-04 - 'ALARM-AGENT' => VObject\Property\Text::class, - 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, - 'PROXIMITY' => VObject\Property\Text::class, - 'DEFAULT-ALARM' => VObject\Property\Boolean::class, + 'ALARM-AGENT' => Property\Text::class, + 'ACKNOWLEDGED' => Property\ICalendar\DateTime::class, + 'PROXIMITY' => Property\Text::class, + 'DEFAULT-ALARM' => Property\Boolean::class, // Additions from draft-daboo-calendar-availability-05 - 'BUSYTYPE' => VObject\Property\Text::class, + 'BUSYTYPE' => Property\Text::class, ]; /** @@ -166,10 +166,10 @@ public function getDocumentType(): int * * @return VObject\Component[] */ - public function getBaseComponents(string $componentName = null): array + public function getBaseComponents(?string $componentName = null): array { $isBaseComponent = function ($component): bool { - if (!$component instanceof VObject\Component) { + if (!$component instanceof Component) { return false; } if ('VTIMEZONE' === $component->name) { @@ -217,10 +217,10 @@ public function getBaseComponents(string $componentName = null): array * * @return VObject\Component|null */ - public function getBaseComponent(string $componentName = null): ?Component + public function getBaseComponent(?string $componentName = null): ?Component { $isBaseComponent = function ($component): bool { - if (!$component instanceof VObject\Component) { + if (!$component instanceof Component) { return false; } if ('VTIMEZONE' === $component->name) { @@ -275,7 +275,7 @@ public function getBaseComponent(string $componentName = null): ?Component * @throws InvalidDataException * @throws VObject\Recur\MaxInstancesExceededException */ - public function expand(\DateTimeInterface $start, \DateTimeInterface $end, \DateTimeZone $timeZone = null): VCalendar + public function expand(\DateTimeInterface $start, \DateTimeInterface $end, ?\DateTimeZone $timeZone = null): VCalendar { $newChildren = []; $recurringEvents = []; diff --git a/lib/DateTimeParser.php b/lib/DateTimeParser.php index 5ce9f207a..7e5749351 100644 --- a/lib/DateTimeParser.php +++ b/lib/DateTimeParser.php @@ -27,7 +27,7 @@ class DateTimeParser * * @throws InvalidDataException */ - public static function parseDateTime(string $dt, \DateTimeZone $tz = null): \DateTimeImmutable + public static function parseDateTime(string $dt, ?\DateTimeZone $tz = null): \DateTimeImmutable { // Format is YYYYMMDD + "T" + hhmmss $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches); @@ -54,7 +54,7 @@ public static function parseDateTime(string $dt, \DateTimeZone $tz = null): \Dat * * @throws InvalidDataException */ - public static function parseDate(string $date, \DateTimeZone $tz = null): \DateTimeImmutable + public static function parseDate(string $date, ?\DateTimeZone $tz = null): \DateTimeImmutable { // Format is YYYYMMDD $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches); diff --git a/lib/Document.php b/lib/Document.php index b311987d7..6f87b5b97 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -2,8 +2,6 @@ namespace Sabre\VObject; -use Sabre\VObject; - /** * Document. * @@ -18,7 +16,7 @@ * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License * - * @property VObject\Property\FlatText VERSION + * @property Property\FlatText VERSION */ abstract class Document extends Component { @@ -140,7 +138,7 @@ public function create(string $name) * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To * ensure that this does not happen, set $defaults to false. */ - public function createComponent(string $name, array $children = null, bool $defaults = true): Component + public function createComponent(string $name, ?array $children = null, bool $defaults = true): Component { $name = strtoupper($name); $class = Component::class; @@ -169,7 +167,7 @@ public function createComponent(string $name, array $children = null, bool $defa * * @throws InvalidDataException */ - public function createProperty(string $name, $value = null, array $parameters = null, string $valueType = null): Property + public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null): Property { // If there's a . in the name, it means it's prefixed by a group name. if (false !== ($i = strpos($name, '.'))) { diff --git a/lib/FreeBusyGenerator.php b/lib/FreeBusyGenerator.php index 2f222e4e5..424826034 100644 --- a/lib/FreeBusyGenerator.php +++ b/lib/FreeBusyGenerator.php @@ -72,7 +72,7 @@ class FreeBusyGenerator * Check the setTimeRange and setObjects methods for details about the * arguments. */ - public function __construct(\DateTimeInterface $start = null, \DateTimeInterface $end = null, $objects = null, \DateTimeZone $timeZone = null) + public function __construct(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null, $objects = null, ?\DateTimeZone $timeZone = null) { $this->setTimeRange($start, $end); @@ -138,7 +138,7 @@ public function setObjects($objects): void * * @throws \Exception */ - public function setTimeRange(\DateTimeInterface $start = null, \DateTimeInterface $end = null): void + public function setTimeRange(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): void { if (!$start) { $start = new \DateTimeImmutable(Settings::$minDate); diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php index f792fd990..9cbc3f72b 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php @@ -113,7 +113,7 @@ class Broker * @throws MaxInstancesExceededException * @throws NoInstancesException */ - public function processMessage(Message $itipMessage, VCalendar $existingObject = null) + public function processMessage(Message $itipMessage, ?VCalendar $existingObject = null) { // We only support events at the moment. if ('VEVENT' !== $itipMessage->component) { @@ -268,7 +268,7 @@ public function parseEvent($calendar, $userHref, $oldCalendar = null): array * This is message from an organizer, and is either a new event * invite, or an update to an existing one. */ - protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { if (!$existingObject) { // This is a new invite, and we're just going to copy over @@ -296,7 +296,7 @@ protected function processMessageRequest(Message $itipMessage, VCalendar $existi * attendee got removed from an event, or an event got cancelled * altogether. */ - protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { if (!$existingObject) { // The event didn't exist in the first place, so we're just @@ -321,7 +321,7 @@ protected function processMessageCancel(Message $itipMessage, VCalendar $existin * @throws MaxInstancesExceededException * @throws NoInstancesException */ - protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { // A reply can only be processed based on an existing object. // If the object is not available, the reply is ignored. diff --git a/lib/Property.php b/lib/Property.php index 53a252fb2..574099e65 100644 --- a/lib/Property.php +++ b/lib/Property.php @@ -61,7 +61,7 @@ abstract class Property extends Node * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], string $group = null) + public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null) { $this->name = $name; $this->group = $group; diff --git a/lib/Property/ICalendar/DateTime.php b/lib/Property/ICalendar/DateTime.php index 7ee907972..631a73b08 100644 --- a/lib/Property/ICalendar/DateTime.php +++ b/lib/Property/ICalendar/DateTime.php @@ -128,7 +128,7 @@ public function isFloating(): bool * * @throws InvalidDataException */ - public function getDateTime(\DateTimeZone $timeZone = null): ?\DateTimeImmutable + public function getDateTime(?\DateTimeZone $timeZone = null): ?\DateTimeImmutable { $dt = $this->getDateTimes($timeZone); if (!$dt) { @@ -149,7 +149,7 @@ public function getDateTime(\DateTimeZone $timeZone = null): ?\DateTimeImmutable * * @throws InvalidDataException */ - public function getDateTimes(\DateTimeZone $timeZone = null): array + public function getDateTimes(?\DateTimeZone $timeZone = null): array { // Does the property have a TZID? /** @var Property\FlatText $tzid */ diff --git a/lib/Property/Text.php b/lib/Property/Text.php index c606ad812..c59029059 100644 --- a/lib/Property/Text.php +++ b/lib/Property/Text.php @@ -64,7 +64,7 @@ class Text extends Property * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, string $name, $value = null, array $parameters = [], string $group = null) + public function __construct(Component $root, string $name, $value = null, array $parameters = [], ?string $group = null) { // There's two types of multi-valued text properties: // 1. multivalue properties. diff --git a/lib/Recur/EventIterator.php b/lib/Recur/EventIterator.php index d79cf3132..2c93bcf4e 100644 --- a/lib/Recur/EventIterator.php +++ b/lib/Recur/EventIterator.php @@ -89,7 +89,7 @@ class EventIterator implements \Iterator * @throws NoInstancesException * @throws InvalidDataException */ - public function __construct($input, string $uid = null, \DateTimeZone $timeZone = null) + public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZone = null) { if (is_null($timeZone)) { $timeZone = new \DateTimeZone('UTC'); diff --git a/lib/Splitter/ICalendar.php b/lib/Splitter/ICalendar.php index ccf0d1add..79f0bd443 100644 --- a/lib/Splitter/ICalendar.php +++ b/lib/Splitter/ICalendar.php @@ -46,7 +46,7 @@ public function __construct($input, int $options = 0) { $data = VObject\Reader::read($input, $options); - if (!$data instanceof VObject\Component\VCalendar) { + if (!$data instanceof VCalendar) { throw new VObject\ParseException('Supplied input could not be parsed as VCALENDAR.'); } diff --git a/lib/Splitter/VCard.php b/lib/Splitter/VCard.php index e63ae837d..40775a335 100644 --- a/lib/Splitter/VCard.php +++ b/lib/Splitter/VCard.php @@ -61,7 +61,7 @@ public function getNext(): ?Component try { $object = $this->parser->parse(); - if (!$object instanceof VObject\Component\VCard) { + if (!$object instanceof Component\VCard) { throw new VObject\ParseException('The supplied input contained non-VCARD data.'); } } catch (VObject\EofException $e) { diff --git a/lib/TimeZoneUtil.php b/lib/TimeZoneUtil.php index 8c2a374fb..2cd458578 100644 --- a/lib/TimeZoneUtil.php +++ b/lib/TimeZoneUtil.php @@ -72,7 +72,7 @@ private function addFinder(string $key, TimezoneFinder $finder): void * Alternatively, if $failIfUncertain is set to true, it will throw an * exception if we cannot accurately determine the timezone. */ - private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone { foreach ($this->timezoneFinders as $timezoneFinder) { $timezone = $timezoneFinder->find($tzid, $failIfUncertain); @@ -117,7 +117,7 @@ public static function addTimezoneFinder(string $key, TimezoneFinder $finder): v self::getInstance()->addFinder($key, $finder); } - public static function getTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + public static function getTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone { return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain); } diff --git a/lib/VCardConverter.php b/lib/VCardConverter.php index 88d74602c..74901fc1b 100644 --- a/lib/VCardConverter.php +++ b/lib/VCardConverter.php @@ -93,8 +93,8 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp ); if (Document::VCARD30 === $targetVersion) { - if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { - /** @var Property\Uri $newProperty */ + if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + /** @var Uri $newProperty */ $newProperty = $this->convertUriToBinary($output, $newProperty); } elseif ($property instanceof Property\VCard\DateAndOrTime) { // In vCard 4, the birth year may be optional. This is not the @@ -153,8 +153,8 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp return; } - if ($property instanceof Property\Binary) { - /** @var Property\Binary $newProperty */ + if ($property instanceof Binary) { + /** @var Binary $newProperty */ $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', @@ -256,7 +256,7 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp * * @throws InvalidDataException */ - protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters): Uri + protected function convertBinaryToUri(Component\VCard $output, Binary $newProperty, array &$parameters): Uri { $value = $newProperty->getValue(); /** @var Uri $newProperty */ @@ -308,7 +308,7 @@ protected function convertBinaryToUri(Component\VCard $output, Property\Binary $ * * @throws InvalidDataException */ - protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty): Property + protected function convertUriToBinary(Component\VCard $output, Uri $newProperty): Property { $value = $newProperty->getValue(); diff --git a/tests/VObject/Component/VCalendarTest.php b/tests/VObject/Component/VCalendarTest.php index 1893a9ac7..bde6a4fa5 100644 --- a/tests/VObject/Component/VCalendarTest.php +++ b/tests/VObject/Component/VCalendarTest.php @@ -756,7 +756,7 @@ public function testCalDAVMETHOD(): void ); } - public function assertValidate($ics, $options, $expectedLevel, string $expectedMessage = null): void + public function assertValidate($ics, $options, $expectedLevel, ?string $expectedMessage = null): void { $vcal = VObject\Reader::read($ics); $result = $vcal->validate($options); @@ -764,7 +764,7 @@ public function assertValidate($ics, $options, $expectedLevel, string $expectedM self::assertValidateResult($result, $expectedLevel, $expectedMessage); } - public function assertValidateResult($input, $expectedLevel, string $expectedMessage = null): void + public function assertValidateResult($input, $expectedLevel, ?string $expectedMessage = null): void { $messages = []; foreach ($input as $warning) { diff --git a/tests/VObject/Component/VCardTest.php b/tests/VObject/Component/VCardTest.php index b71b30856..3851d97d9 100644 --- a/tests/VObject/Component/VCardTest.php +++ b/tests/VObject/Component/VCardTest.php @@ -283,7 +283,7 @@ public function testVCard21NoCardDAV(): void ); } - public function assertValidate($vcf, $options, int $expectedLevel, string $expectedMessage = null): void + public function assertValidate($vcf, $options, int $expectedLevel, ?string $expectedMessage = null): void { $vcal = VObject\Reader::read($vcf); $result = $vcal->validate($options); @@ -291,7 +291,7 @@ public function assertValidate($vcf, $options, int $expectedLevel, string $expec self::assertValidateResult($result, $expectedLevel, $expectedMessage); } - public function assertValidateResult($input, int $expectedLevel, string $expectedMessage = null): void + public function assertValidateResult($input, int $expectedLevel, ?string $expectedMessage = null): void { $messages = []; foreach ($input as $warning) { diff --git a/tests/VObject/Component/VFreeBusyTest.php b/tests/VObject/Component/VFreeBusyTest.php index f3a521f95..f2f97ebe1 100644 --- a/tests/VObject/Component/VFreeBusyTest.php +++ b/tests/VObject/Component/VFreeBusyTest.php @@ -3,7 +3,6 @@ namespace Sabre\VObject\Component; use PHPUnit\Framework\TestCase; -use Sabre\VObject; use Sabre\VObject\Reader; class VFreeBusyTest extends TestCase @@ -23,7 +22,7 @@ public function testIsFree(): void END:VCALENDAR BLA; - $obj = VObject\Reader::read($input); + $obj = Reader::read($input); $vfb = $obj->VFREEBUSY; $tz = new \DateTimeZone('UTC'); diff --git a/tests/VObject/ComponentTest.php b/tests/VObject/ComponentTest.php index 4a247d906..c48480983 100644 --- a/tests/VObject/ComponentTest.php +++ b/tests/VObject/ComponentTest.php @@ -463,7 +463,7 @@ public function testRemoveNotFound(): void */ public function testValidateRules(array $componentList, int $errorCount): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'Hi', [], false); foreach ($componentList as $v) { @@ -475,7 +475,7 @@ public function testValidateRules(array $componentList, int $errorCount): void public function testValidateRepair(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'Hi', [], false); $component->validate(Component::REPAIR); @@ -484,7 +484,7 @@ public function testValidateRepair(): void public function testValidateRepairShouldNotDeduplicatePropertiesWhenValuesDiffer(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); @@ -500,7 +500,7 @@ public function testValidateRepairShouldNotDeduplicatePropertiesWhenValuesDiffer public function testValidateRepairShouldNotDeduplicatePropertiesWhenParametersDiffer(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); @@ -516,7 +516,7 @@ public function testValidateRepairShouldNotDeduplicatePropertiesWhenParametersDi public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAndParametersAreEqual(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); @@ -532,7 +532,7 @@ public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAndParame public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAreEqual(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); diff --git a/tests/VObject/EmptyParameterTest.php b/tests/VObject/EmptyParameterTest.php index ab197ab33..6739f104a 100644 --- a/tests/VObject/EmptyParameterTest.php +++ b/tests/VObject/EmptyParameterTest.php @@ -21,7 +21,7 @@ public function testRead(): void $vcard = Reader::read($input); self::assertInstanceOf(Component\VCard::class, $vcard); - $vcard = $vcard->convert(\Sabre\VObject\Document::VCARD30); + $vcard = $vcard->convert(Document::VCARD30); $vcard = $vcard->serialize(); $converted = Reader::read($vcard); diff --git a/tests/VObject/FreeBusyGeneratorTest.php b/tests/VObject/FreeBusyGeneratorTest.php index 47283eafb..a4946cc10 100644 --- a/tests/VObject/FreeBusyGeneratorTest.php +++ b/tests/VObject/FreeBusyGeneratorTest.php @@ -46,7 +46,7 @@ public function testInvalidArg(): void * * @throws ParseException */ - public function assertFreeBusyReport(string $expected, $input, \DateTimeZone $timeZone = null, string $vavailability = null): void + public function assertFreeBusyReport(string $expected, $input, ?\DateTimeZone $timeZone = null, ?string $vavailability = null): void { $gen = new FreeBusyGenerator( new \DateTime('20110101T110000Z', new \DateTimeZone('UTC')), diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 2814df66d..519f51225 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -1072,7 +1072,7 @@ public function testIteratorFunctions(): void ); } - public function parse($rule, string $start, array $expected, string $fastForward = null, string $tz = 'UTC', bool $runTillTheEnd = false): void + public function parse($rule, string $start, array $expected, ?string $fastForward = null, string $tz = 'UTC', bool $runTillTheEnd = false): void { $dt = new \DateTime($start, new \DateTimeZone($tz)); $parser = new RRuleIterator($rule, $dt); diff --git a/tests/VObject/VCardConverterTest.php b/tests/VObject/VCardConverterTest.php index 9db0ad561..db778261d 100644 --- a/tests/VObject/VCardConverterTest.php +++ b/tests/VObject/VCardConverterTest.php @@ -554,7 +554,7 @@ public function testNoLabel(): void $vcard = Reader::read($input); - self::assertInstanceOf(Component\VCard::class, $vcard); + self::assertInstanceOf(VCard::class, $vcard); /** @var VCard $vcard */ $vcard = $vcard->convert(Document::VCARD40); $vcard = $vcard->serialize(); From 54b7952b196d94902ab20d1681c3907d7f204709 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 03:03:39 +0000 Subject: [PATCH 03/38] Bump codecov/codecov-action from 3 to 4 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5ba8c90..775b3389f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,5 +59,5 @@ jobs: run: vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover clover.xml - name: Code Coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 if: matrix.coverage != 'none' From 7f5ddc4c0fb10705377c5b33e9b943f13cc50855 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Sun, 11 Feb 2024 12:55:45 +0545 Subject: [PATCH 04/38] chore: use php-cs-fixer 3.49 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 67b8b26b0..bdc8591a1 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "sabre/xml" : "^3.0 || ^4.0" }, "require-dev" : { - "friendsofphp/php-cs-fixer": "^3.38", + "friendsofphp/php-cs-fixer": "^3.49", "phpunit/phpunit" : "^9.6", "phpunit/php-invoker" : "^2.0 || ^3.1", "phpstan/phpstan": "^1.10" From db652288d9fa2c546e5ad77eeed254ea0194df31 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 4 Mar 2024 11:39:59 +0545 Subject: [PATCH 05/38] chore: use php-cs-fixer 3.51 --- composer.json | 2 +- lib/Component/VCalendar.php | 2 -- lib/VCardConverter.php | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index bdc8591a1..5c8c96d36 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "sabre/xml" : "^3.0 || ^4.0" }, "require-dev" : { - "friendsofphp/php-cs-fixer": "^3.49", + "friendsofphp/php-cs-fixer": "^3.51", "phpunit/phpunit" : "^9.6", "phpunit/php-invoker" : "^2.0 || ^3.1", "phpstan/phpstan": "^1.10" diff --git a/lib/Component/VCalendar.php b/lib/Component/VCalendar.php index 318492ce6..7b6e66e88 100644 --- a/lib/Component/VCalendar.php +++ b/lib/Component/VCalendar.php @@ -214,8 +214,6 @@ public function getBaseComponents(?string $componentName = null): array * If there is no such component, null will be returned. * * @param string|null $componentName filter by component name - * - * @return VObject\Component|null */ public function getBaseComponent(?string $componentName = null): ?Component { diff --git a/lib/VCardConverter.php b/lib/VCardConverter.php index 74901fc1b..ff8fe3f81 100644 --- a/lib/VCardConverter.php +++ b/lib/VCardConverter.php @@ -304,7 +304,7 @@ protected function convertBinaryToUri(Component\VCard $output, Binary $newProper * be valid in vCard 3.0 as well, we should convert those to BINARY if * possible, to improve compatibility. * - * @return Property\Binary|Property\Uri|null + * @return Binary|Uri|null * * @throws InvalidDataException */ From 624a4f98a83574c2328c931e4a49a6a1ee8ebac7 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 18 Apr 2024 14:07:12 +0545 Subject: [PATCH 06/38] chore: apply cs-fixer 3.54.0 changes --- lib/Component/VCard.php | 2 +- lib/DateTimeParser.php | 12 +- lib/timezonedata/windowszones.php | 276 +++++++++--------- .../VObject/ITip/BrokerAttendeeReplyTest.php | 18 +- tests/VObject/JCardTest.php | 18 +- tests/VObject/Parser/JsonTest.php | 18 +- tests/VObject/PropertyTest.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 10 +- 8 files changed, 178 insertions(+), 178 deletions(-) diff --git a/lib/Component/VCard.php b/lib/Component/VCard.php index 0f2a5e266..6bbec5717 100644 --- a/lib/Component/VCard.php +++ b/lib/Component/VCard.php @@ -82,7 +82,7 @@ class VCard extends VObject\Document 'ROLE' => VObject\Property\FlatText::class, 'LOGO' => VObject\Property\Binary::class, // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so - // not supported at the moment + // not supported at the moment 'ORG' => VObject\Property\Text::class, 'NOTE' => VObject\Property\FlatText::class, 'REV' => VObject\Property\VCard\TimeStamp::class, diff --git a/lib/DateTimeParser.php b/lib/DateTimeParser.php index 7e5749351..a8130b8bc 100644 --- a/lib/DateTimeParser.php +++ b/lib/DateTimeParser.php @@ -98,12 +98,12 @@ public static function parseDuration(string $duration): \DateInterval } $parts = [ - 'week', - 'day', - 'hour', - 'minute', - 'second', - ]; + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; foreach ($parts as $part) { $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; diff --git a/lib/timezonedata/windowszones.php b/lib/timezonedata/windowszones.php index 2049a95c1..335007983 100644 --- a/lib/timezonedata/windowszones.php +++ b/lib/timezonedata/windowszones.php @@ -11,142 +11,142 @@ */ return [ - 'AUS Central Standard Time' => 'Australia/Darwin', - 'AUS Eastern Standard Time' => 'Australia/Sydney', - 'Afghanistan Standard Time' => 'Asia/Kabul', - 'Alaskan Standard Time' => 'America/Anchorage', - 'Aleutian Standard Time' => 'America/Adak', - 'Altai Standard Time' => 'Asia/Barnaul', - 'Arab Standard Time' => 'Asia/Riyadh', - 'Arabian Standard Time' => 'Asia/Dubai', - 'Arabic Standard Time' => 'Asia/Baghdad', - 'Argentina Standard Time' => 'America/Buenos_Aires', - 'Astrakhan Standard Time' => 'Europe/Astrakhan', - 'Atlantic Standard Time' => 'America/Halifax', - 'Aus Central W. Standard Time' => 'Australia/Eucla', - 'Azerbaijan Standard Time' => 'Asia/Baku', - 'Azores Standard Time' => 'Atlantic/Azores', - 'Bahia Standard Time' => 'America/Bahia', - 'Bangladesh Standard Time' => 'Asia/Dhaka', - 'Belarus Standard Time' => 'Europe/Minsk', - 'Bougainville Standard Time' => 'Pacific/Bougainville', - 'Canada Central Standard Time' => 'America/Regina', - 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', - 'Caucasus Standard Time' => 'Asia/Yerevan', - 'Cen. Australia Standard Time' => 'Australia/Adelaide', - 'Central America Standard Time' => 'America/Guatemala', - 'Central Asia Standard Time' => 'Asia/Almaty', - 'Central Brazilian Standard Time' => 'America/Cuiaba', - 'Central Europe Standard Time' => 'Europe/Budapest', - 'Central European Standard Time' => 'Europe/Warsaw', - 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', - 'Central Standard Time' => 'America/Chicago', - 'Central Standard Time (Mexico)' => 'America/Mexico_City', - 'Chatham Islands Standard Time' => 'Pacific/Chatham', - 'China Standard Time' => 'Asia/Shanghai', - 'Cuba Standard Time' => 'America/Havana', - 'Dateline Standard Time' => 'Etc/GMT+12', - 'E. Africa Standard Time' => 'Africa/Nairobi', - 'E. Australia Standard Time' => 'Australia/Brisbane', - 'E. Europe Standard Time' => 'Europe/Chisinau', - 'E. South America Standard Time' => 'America/Sao_Paulo', - 'Easter Island Standard Time' => 'Pacific/Easter', - 'Eastern Standard Time' => 'America/New_York', - 'Eastern Standard Time (Mexico)' => 'America/Cancun', - 'Egypt Standard Time' => 'Africa/Cairo', - 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', - 'FLE Standard Time' => 'Europe/Kiev', - 'Fiji Standard Time' => 'Pacific/Fiji', - 'GMT Standard Time' => 'Europe/London', - 'GTB Standard Time' => 'Europe/Bucharest', - 'Georgian Standard Time' => 'Asia/Tbilisi', - 'Greenland Standard Time' => 'America/Godthab', - 'Greenwich Standard Time' => 'Atlantic/Reykjavik', - 'Haiti Standard Time' => 'America/Port-au-Prince', - 'Hawaiian Standard Time' => 'Pacific/Honolulu', - 'India Standard Time' => 'Asia/Calcutta', - 'Iran Standard Time' => 'Asia/Tehran', - 'Israel Standard Time' => 'Asia/Jerusalem', - 'Jordan Standard Time' => 'Asia/Amman', - 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', - 'Korea Standard Time' => 'Asia/Seoul', - 'Libya Standard Time' => 'Africa/Tripoli', - 'Line Islands Standard Time' => 'Pacific/Kiritimati', - 'Lord Howe Standard Time' => 'Australia/Lord_Howe', - 'Magadan Standard Time' => 'Asia/Magadan', - 'Magallanes Standard Time' => 'America/Punta_Arenas', - 'Marquesas Standard Time' => 'Pacific/Marquesas', - 'Mauritius Standard Time' => 'Indian/Mauritius', - 'Middle East Standard Time' => 'Asia/Beirut', - 'Montevideo Standard Time' => 'America/Montevideo', - 'Morocco Standard Time' => 'Africa/Casablanca', - 'Mountain Standard Time' => 'America/Denver', - 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', - 'Myanmar Standard Time' => 'Asia/Rangoon', - 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', - 'Namibia Standard Time' => 'Africa/Windhoek', - 'Nepal Standard Time' => 'Asia/Katmandu', - 'New Zealand Standard Time' => 'Pacific/Auckland', - 'Newfoundland Standard Time' => 'America/St_Johns', - 'Norfolk Standard Time' => 'Pacific/Norfolk', - 'North Asia East Standard Time' => 'Asia/Irkutsk', - 'North Asia Standard Time' => 'Asia/Krasnoyarsk', - 'North Korea Standard Time' => 'Asia/Pyongyang', - 'Omsk Standard Time' => 'Asia/Omsk', - 'Pacific SA Standard Time' => 'America/Santiago', - 'Pacific Standard Time' => 'America/Los_Angeles', - 'Pacific Standard Time (Mexico)' => 'America/Tijuana', - 'Pakistan Standard Time' => 'Asia/Karachi', - 'Paraguay Standard Time' => 'America/Asuncion', - 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', - 'Romance Standard Time' => 'Europe/Paris', - 'Russia Time Zone 10' => 'Asia/Srednekolymsk', - 'Russia Time Zone 11' => 'Asia/Kamchatka', - 'Russia Time Zone 3' => 'Europe/Samara', - 'Russian Standard Time' => 'Europe/Moscow', - 'SA Eastern Standard Time' => 'America/Cayenne', - 'SA Pacific Standard Time' => 'America/Bogota', - 'SA Western Standard Time' => 'America/La_Paz', - 'SE Asia Standard Time' => 'Asia/Bangkok', - 'Saint Pierre Standard Time' => 'America/Miquelon', - 'Sakhalin Standard Time' => 'Asia/Sakhalin', - 'Samoa Standard Time' => 'Pacific/Apia', - 'Sao Tome Standard Time' => 'Africa/Sao_Tome', - 'Saratov Standard Time' => 'Europe/Saratov', - 'Singapore Standard Time' => 'Asia/Singapore', - 'South Africa Standard Time' => 'Africa/Johannesburg', - 'Sri Lanka Standard Time' => 'Asia/Colombo', - 'Sudan Standard Time' => 'Africa/Khartoum', - 'Syria Standard Time' => 'Asia/Damascus', - 'Taipei Standard Time' => 'Asia/Taipei', - 'Tasmania Standard Time' => 'Australia/Hobart', - 'Tocantins Standard Time' => 'America/Araguaina', - 'Tokyo Standard Time' => 'Asia/Tokyo', - 'Tomsk Standard Time' => 'Asia/Tomsk', - 'Tonga Standard Time' => 'Pacific/Tongatapu', - 'Transbaikal Standard Time' => 'Asia/Chita', - 'Turkey Standard Time' => 'Europe/Istanbul', - 'Turks And Caicos Standard Time' => 'America/Grand_Turk', - 'US Eastern Standard Time' => 'America/Indianapolis', - 'US Mountain Standard Time' => 'America/Phoenix', - 'UTC' => 'Etc/GMT', - 'UTC+12' => 'Etc/GMT-12', - 'UTC+13' => 'Etc/GMT-13', - 'UTC-02' => 'Etc/GMT+2', - 'UTC-08' => 'Etc/GMT+8', - 'UTC-09' => 'Etc/GMT+9', - 'UTC-11' => 'Etc/GMT+11', - 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', - 'Venezuela Standard Time' => 'America/Caracas', - 'Vladivostok Standard Time' => 'Asia/Vladivostok', - 'Volgograd Standard Time' => 'Europe/Volgograd', - 'W. Australia Standard Time' => 'Australia/Perth', - 'W. Central Africa Standard Time' => 'Africa/Lagos', - 'W. Europe Standard Time' => 'Europe/Berlin', - 'W. Mongolia Standard Time' => 'Asia/Hovd', - 'West Asia Standard Time' => 'Asia/Tashkent', - 'West Bank Standard Time' => 'Asia/Hebron', - 'West Pacific Standard Time' => 'Pacific/Port_Moresby', - 'Yakutsk Standard Time' => 'Asia/Yakutsk', - 'Yukon Standard Time' => 'America/Whitehorse', + 'AUS Central Standard Time' => 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Calcutta', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', ]; diff --git a/tests/VObject/ITip/BrokerAttendeeReplyTest.php b/tests/VObject/ITip/BrokerAttendeeReplyTest.php index 98942af41..6590356bb 100644 --- a/tests/VObject/ITip/BrokerAttendeeReplyTest.php +++ b/tests/VObject/ITip/BrokerAttendeeReplyTest.php @@ -404,15 +404,15 @@ public function testRecurringAllDay(): void $version = \Sabre\VObject\Version::VERSION; $expected = [ - [ - 'uid' => 'foobar', - 'method' => 'REPLY', - 'component' => 'VEVENT', - 'sender' => 'mailto:one@example.org', - 'senderName' => 'One', - 'recipient' => 'mailto:strunk@example.org', - 'recipientName' => 'Strunk', - 'message' => << 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => << Date: Fri, 19 Apr 2024 15:19:29 +0545 Subject: [PATCH 07/38] chore: bump php-cs-fixer requirement to 3.54 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5c8c96d36..fc6911071 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "sabre/xml" : "^3.0 || ^4.0" }, "require-dev" : { - "friendsofphp/php-cs-fixer": "^3.51", + "friendsofphp/php-cs-fixer": "^3.54", "phpunit/phpunit" : "^9.6", "phpunit/php-invoker" : "^2.0 || ^3.1", "phpstan/phpstan": "^1.10" From a848f933365d7cacf39dc7361c191945dfa00c5f Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Tue, 23 Apr 2024 21:24:34 +0200 Subject: [PATCH 08/38] add `lineIndex` and `lineString` properties to Node --- lib/Document.php | 4 ++-- lib/Parser/MimeDir.php | 2 +- lib/Property.php | 24 +++++++++++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/Document.php b/lib/Document.php index 6f87b5b97..02eaf13c7 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -167,7 +167,7 @@ public function createComponent(string $name, ?array $children = null, bool $def * * @throws InvalidDataException */ - public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null): Property + public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property { // If there's a . in the name, it means it's prefixed by a group name. if (false !== ($i = strpos($name, '.'))) { @@ -201,7 +201,7 @@ public function createProperty(string $name, $value = null, ?array $parameters = $parameters = []; } - return new $class($this, $name, $value, $parameters, $group); + return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); } /** diff --git a/lib/Parser/MimeDir.php b/lib/Parser/MimeDir.php index bbef0ea6b..ddac9642b 100644 --- a/lib/Parser/MimeDir.php +++ b/lib/Parser/MimeDir.php @@ -445,7 +445,7 @@ protected function readProperty(string $line) } } - $propObj = $this->root->createProperty($property['name'], null, $namedParameters); + $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line); foreach ($namelessParameters as $namelessParameter) { $propObj->add(null, $namelessParameter); diff --git a/lib/Property.php b/lib/Property.php index 574099e65..407a14565 100644 --- a/lib/Property.php +++ b/lib/Property.php @@ -51,6 +51,20 @@ abstract class Property extends Node */ public string $delimiter = ';'; + /** + * The line number in the original iCalendar / vCard file + * that corresponds with the current node + * if the node was read from a file + */ + public ?int $lineIndex; + + /** + * The line string from the original iCalendar / vCard file + * that corresponds with the current node + * if the node was read from a file + */ + public ?string $lineString; + /** * Creates the generic property. * @@ -61,7 +75,7 @@ abstract class Property extends Node * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null) + public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null, ?int $lineIndex = null, ?string $lineString = null) { $this->name = $name; $this->group = $group; @@ -75,6 +89,14 @@ public function __construct(Component $root, ?string $name, $value = null, array if (!is_null($value)) { $this->setValue($value); } + + if (!is_null($lineIndex)) { + $this->lineIndex = $lineIndex; + } + + if (!is_null($lineString)) { + $this->lineString = $lineString; + } } /** From 800aac9bb2b0db500840a9cd2992787679070c14 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Thu, 25 Apr 2024 23:26:41 +0200 Subject: [PATCH 09/38] creat Unit Test --- tests/VObject/Component/VCalendarTest.php | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/VObject/Component/VCalendarTest.php b/tests/VObject/Component/VCalendarTest.php index bde6a4fa5..ad19b5eb8 100644 --- a/tests/VObject/Component/VCalendarTest.php +++ b/tests/VObject/Component/VCalendarTest.php @@ -756,6 +756,49 @@ public function testCalDAVMETHOD(): void ); } + public function testNodeInValidationErrorHasLineIndexAndLineStringProps(): void + { + $defectiveInput = <<validate(); + $warningMessages = []; + foreach( $result as $error ) { + $warningMessages[] = $error['message']; + } + self::assertCount(2, $result, 'We expected exactly 2 validation messages, instead we got ' . count( $result ) . ' results:' . implode(', ', $warningMessages) ); + foreach( $result as $idx => $warning ) { + self::assertArrayHasKey( 'node', $warning, 'The validation errors should contain a node key' ); + self::assertInstanceOf( VObject\Property\ICalendar\DateTime::class, $warning[ 'node' ], 'We expected the defective node to be of type Sabre\VObject\Property\ICalendar\DateTime, instead we got type ' . gettype( $warning['node'] ) ); + self::assertObjectHasProperty( 'lineIndex', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineIndex" property' ); + self::assertObjectHasProperty( 'lineString', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineString" property' ); + switch( $idx ) { + case 0: + self::assertEquals( '10', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the first defective node to be 10, instead it was ' . $warning['node']->lineIndex ); + self::assertEquals( 'CREATED:', $warning['node']->lineString, 'We expected the "lineString" property of the first defective node to be "CREATED:", instead it was ' . $warning['node']->lineString ); + break; + case 1: + self::assertEquals( '11', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the second defective node to be 11, instead it was ' . $warning['node']->lineIndex ); + self::assertEquals( 'LAST-MODIFIED:', $warning['node']->lineString, 'We expected the "lineString" property of the second defective node to be "LAST-MODIFIED:", instead it was ' . $warning['node']->lineString ); + break; + } + } + } + public function assertValidate($ics, $options, $expectedLevel, ?string $expectedMessage = null): void { $vcal = VObject\Reader::read($ics); From 2a653373602572daee01b461b38a618addbc1b9a Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Wed, 8 May 2024 10:37:50 +0200 Subject: [PATCH 10/38] ITip\Broker: handle timezones in replies to exception events Co-authored-by: Luc DUZAN --- lib/ITip/Broker.php | 6 +- tests/VObject/ITip/BrokerProcessReplyTest.php | 189 ++++++++++++++++++ 2 files changed, 192 insertions(+), 3 deletions(-) diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php index 9cbc3f72b..07f53c598 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php @@ -333,7 +333,7 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin // Finding all the instances the attendee replied to. foreach ($itipMessage->message->VEVENT as $vevent) { - $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; $attendee = $vevent->ATTENDEE; $instances[$recurId] = $attendee['PARTSTAT']->getValue(); if (isset($vevent->{'REQUEST-STATUS'})) { @@ -346,7 +346,7 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin // all the instances where we have a reply for. $masterObject = null; foreach ($existingObject->VEVENT as $vevent) { - $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; if ('master' === $recurId) { $masterObject = $vevent; } @@ -393,7 +393,7 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin $newObject = $recurrenceIterator->getEventObject(); $recurrenceIterator->next(); - if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) { + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) { $found = true; } --$iterations; diff --git a/tests/VObject/ITip/BrokerProcessReplyTest.php b/tests/VObject/ITip/BrokerProcessReplyTest.php index 064d5cb48..19b668818 100644 --- a/tests/VObject/ITip/BrokerProcessReplyTest.php +++ b/tests/VObject/ITip/BrokerProcessReplyTest.php @@ -253,6 +253,75 @@ public function testReplyPartyCrasher(): void $this->process($itip, $old, $expected); } + public function testReplyExistingExceptionRecurrenceIdInUTC(): void + { + // This is a reply to 1 instance of a recurring event. This should + // automatically create an exception. + $itip = <<process($itip, $old, $expected); + } + public function testReplyNewException(): void { // This is a reply to 1 instance of a recurring event. This should @@ -373,6 +442,126 @@ public function testReplyNewExceptionTz(): void $this->process($itip, $old, $expected); } + public function testReplyNewExceptionRecurrenceIdInDifferentTz(): void + { + // This is a reply to 1 instance of a recurring event. This should + // automatically create an exception. + $itip = <<process($itip, $old, $expected); + } + + public function testReplyNewExceptionRecurrenceIdInUTC(): void + { + // This is a reply to 1 instance of a recurring event. This should + // automatically create an exception. + $itip = <<process($itip, $old, $expected); + } + public function testReplyPartyCrashCreateException(): void { // IN this test there's a recurring event that has an exception. The From 620ee98489e91b701e7cd463a7c3bbf75816a52d Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Wed, 8 May 2024 11:02:31 +0200 Subject: [PATCH 11/38] remove comment --- tests/VObject/ITip/BrokerProcessReplyTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/VObject/ITip/BrokerProcessReplyTest.php b/tests/VObject/ITip/BrokerProcessReplyTest.php index 19b668818..1718f76d2 100644 --- a/tests/VObject/ITip/BrokerProcessReplyTest.php +++ b/tests/VObject/ITip/BrokerProcessReplyTest.php @@ -255,8 +255,6 @@ public function testReplyPartyCrasher(): void public function testReplyExistingExceptionRecurrenceIdInUTC(): void { - // This is a reply to 1 instance of a recurring event. This should - // automatically create an exception. $itip = << Date: Wed, 8 May 2024 12:28:35 +0200 Subject: [PATCH 12/38] php-cs-fixer --- lib/Property.php | 4 ++-- tests/VObject/Component/VCalendarTest.php | 24 +++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/Property.php b/lib/Property.php index 407a14565..f4d0bdc0b 100644 --- a/lib/Property.php +++ b/lib/Property.php @@ -54,14 +54,14 @@ abstract class Property extends Node /** * The line number in the original iCalendar / vCard file * that corresponds with the current node - * if the node was read from a file + * if the node was read from a file. */ public ?int $lineIndex; /** * The line string from the original iCalendar / vCard file * that corresponds with the current node - * if the node was read from a file + * if the node was read from a file. */ public ?string $lineString; diff --git a/tests/VObject/Component/VCalendarTest.php b/tests/VObject/Component/VCalendarTest.php index ad19b5eb8..67dc2fe8d 100644 --- a/tests/VObject/Component/VCalendarTest.php +++ b/tests/VObject/Component/VCalendarTest.php @@ -777,23 +777,23 @@ public function testNodeInValidationErrorHasLineIndexAndLineStringProps(): void $vcal = VObject\Reader::read($defectiveInput); $result = $vcal->validate(); $warningMessages = []; - foreach( $result as $error ) { + foreach ($result as $error) { $warningMessages[] = $error['message']; } - self::assertCount(2, $result, 'We expected exactly 2 validation messages, instead we got ' . count( $result ) . ' results:' . implode(', ', $warningMessages) ); - foreach( $result as $idx => $warning ) { - self::assertArrayHasKey( 'node', $warning, 'The validation errors should contain a node key' ); - self::assertInstanceOf( VObject\Property\ICalendar\DateTime::class, $warning[ 'node' ], 'We expected the defective node to be of type Sabre\VObject\Property\ICalendar\DateTime, instead we got type ' . gettype( $warning['node'] ) ); - self::assertObjectHasProperty( 'lineIndex', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineIndex" property' ); - self::assertObjectHasProperty( 'lineString', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineString" property' ); - switch( $idx ) { + self::assertCount(2, $result, 'We expected exactly 2 validation messages, instead we got '.count($result).' results:'.implode(', ', $warningMessages)); + foreach ($result as $idx => $warning) { + self::assertArrayHasKey('node', $warning, 'The validation errors should contain a node key'); + self::assertInstanceOf(VObject\Property\ICalendar\DateTime::class, $warning['node'], 'We expected the defective node to be of type Sabre\VObject\Property\ICalendar\DateTime, instead we got type '.gettype($warning['node'])); + self::assertObjectHasProperty('lineIndex', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineIndex" property'); + self::assertObjectHasProperty('lineString', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineString" property'); + switch ($idx) { case 0: - self::assertEquals( '10', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the first defective node to be 10, instead it was ' . $warning['node']->lineIndex ); - self::assertEquals( 'CREATED:', $warning['node']->lineString, 'We expected the "lineString" property of the first defective node to be "CREATED:", instead it was ' . $warning['node']->lineString ); + self::assertEquals('10', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the first defective node to be 10, instead it was '.$warning['node']->lineIndex); + self::assertEquals('CREATED:', $warning['node']->lineString, 'We expected the "lineString" property of the first defective node to be "CREATED:", instead it was '.$warning['node']->lineString); break; case 1: - self::assertEquals( '11', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the second defective node to be 11, instead it was ' . $warning['node']->lineIndex ); - self::assertEquals( 'LAST-MODIFIED:', $warning['node']->lineString, 'We expected the "lineString" property of the second defective node to be "LAST-MODIFIED:", instead it was ' . $warning['node']->lineString ); + self::assertEquals('11', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the second defective node to be 11, instead it was '.$warning['node']->lineIndex); + self::assertEquals('LAST-MODIFIED:', $warning['node']->lineString, 'We expected the "lineString" property of the second defective node to be "LAST-MODIFIED:", instead it was '.$warning['node']->lineString); break; } } From ec31757d19813993419dc9a98314c0c5d12f3e1e Mon Sep 17 00:00:00 2001 From: John D'Orazio Date: Wed, 8 May 2024 14:51:44 +0200 Subject: [PATCH 13/38] remove unnecessary error descriptions --- tests/VObject/Component/VCalendarTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/VObject/Component/VCalendarTest.php b/tests/VObject/Component/VCalendarTest.php index 67dc2fe8d..847e17d02 100644 --- a/tests/VObject/Component/VCalendarTest.php +++ b/tests/VObject/Component/VCalendarTest.php @@ -782,18 +782,18 @@ public function testNodeInValidationErrorHasLineIndexAndLineStringProps(): void } self::assertCount(2, $result, 'We expected exactly 2 validation messages, instead we got '.count($result).' results:'.implode(', ', $warningMessages)); foreach ($result as $idx => $warning) { - self::assertArrayHasKey('node', $warning, 'The validation errors should contain a node key'); - self::assertInstanceOf(VObject\Property\ICalendar\DateTime::class, $warning['node'], 'We expected the defective node to be of type Sabre\VObject\Property\ICalendar\DateTime, instead we got type '.gettype($warning['node'])); - self::assertObjectHasProperty('lineIndex', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineIndex" property'); - self::assertObjectHasProperty('lineString', $warning['node'], 'We expected the defective node in the validation errors array to have a "lineString" property'); + self::assertArrayHasKey('node', $warning); + self::assertInstanceOf(VObject\Property\ICalendar\DateTime::class, $warning['node']); + self::assertObjectHasProperty('lineIndex', $warning['node']); + self::assertObjectHasProperty('lineString', $warning['node']); switch ($idx) { case 0: - self::assertEquals('10', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the first defective node to be 10, instead it was '.$warning['node']->lineIndex); - self::assertEquals('CREATED:', $warning['node']->lineString, 'We expected the "lineString" property of the first defective node to be "CREATED:", instead it was '.$warning['node']->lineString); + self::assertEquals('10', $warning['node']->lineIndex); + self::assertEquals('CREATED:', $warning['node']->lineString); break; case 1: - self::assertEquals('11', $warning['node']->lineIndex, 'We expected the "lineIndex" property of the second defective node to be 11, instead it was '.$warning['node']->lineIndex); - self::assertEquals('LAST-MODIFIED:', $warning['node']->lineString, 'We expected the "lineString" property of the second defective node to be "LAST-MODIFIED:", instead it was '.$warning['node']->lineString); + self::assertEquals('11', $warning['node']->lineIndex); + self::assertEquals('LAST-MODIFIED:', $warning['node']->lineString); break; } } From bee4fa7ddab45f1a558bec4a471c8cd5bf911993 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 13:55:22 +0545 Subject: [PATCH 14/38] Add comments about use of getTimestamp --- lib/ITip/Broker.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php index 07f53c598..be6ba29d7 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php @@ -333,6 +333,9 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin // Finding all the instances the attendee replied to. foreach ($itipMessage->message->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + // The Unix timestamp will be the same for an event, even if the reply from the attendee + // used a different format/timezone to express the event date-time. $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; $attendee = $vevent->ATTENDEE; $instances[$recurId] = $attendee['PARTSTAT']->getValue(); @@ -346,6 +349,7 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin // all the instances where we have a reply for. $masterObject = null; foreach ($existingObject->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; if ('master' === $recurId) { $masterObject = $vevent; @@ -393,6 +397,9 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin $newObject = $recurrenceIterator->getEventObject(); $recurrenceIterator->next(); + // Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp. + // If they are the same, then this is a matching recurrence, even though its date-time may have + // been expressed in a different format/timezone. if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) { $found = true; } From e5976c9e2ae1d1de17dfe96f92fd2548bf5f83f3 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 10 May 2024 14:18:46 +0545 Subject: [PATCH 15/38] chore: stop exporting php-cs-fixer config --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index c717ebe6a..be96a4f14 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,6 @@ /.gitattributes export-ignore /.gitignore export-ignore /.php_cs.dist export-ignore +/.php-cs-fixer.dist.php export-ignore /CHANGELOG.md export-ignore /phpstan.neon export-ignore From 8bf65e28f082a230e0d18ae17dbe71db21c1ea65 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Tue, 14 May 2024 14:01:47 +0545 Subject: [PATCH 16/38] chore: bump dev dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fc6911071..0040076bc 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "friendsofphp/php-cs-fixer": "^3.54", "phpunit/phpunit" : "^9.6", "phpunit/php-invoker" : "^2.0 || ^3.1", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.11" }, "suggest" : { "hoa/bench" : "If you would like to run the benchmark scripts" From 1a6155035abaaceb924b38f32d168c6a696017a8 Mon Sep 17 00:00:00 2001 From: Victor Emanouilov Date: Tue, 14 May 2024 17:37:32 +0300 Subject: [PATCH 17/38] yearly rrule compliance by the iterator when start date does not follow the rrule --- lib/Recur/RRuleIterator.php | 6 +++++- tests/VObject/Recur/RRuleIteratorTest.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 888556eea..10269dafb 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -610,7 +610,11 @@ protected function nextYearly(): void // If we advanced to the next month or year, the first // occurrence is always correct. if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { - break 2; + // only consider byMonth matches, + // otherwise, we don't follow RRule correctly + if (in_array($currentMonth, $this->byMonth)) { + break 2; + } } } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 740e9fd0c..e6043d438 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -922,6 +922,23 @@ public function testYearlyBySetPosLoop(): void ); } + /** + * This caused an incorrect date to be returned by the rule iterator when + * start date was not on the rrule list. + */ + public function testYearlyStartDateNotOnRRuleList(): void + { + $this->parse( + 'FREQ=YEARLY;BYMONTH=6;BYDAY=-1FR;UNTIL=20250901T000000Z', + '2023-09-01 12:00:00', + [ + '2023-09-01 12:00:00', + '2024-06-28 12:00:00', + '2025-06-27 12:00:00', + ], + ); + } + /** * Something, somewhere produced an ics with an interval set to 0. Because * this means we increase the current day (or week, month) by 0, this also From 72a1fe9dc6d551ef05257abd90b0eaf8bb7d3fae Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 13:22:51 +0545 Subject: [PATCH 18/38] test: add more test scenarios for testYearlyStartDateNotOnRRuleList --- tests/VObject/Recur/RRuleIteratorTest.php | 43 +++++++++++++++++++---- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index e6043d438..931a79967 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -925,18 +925,47 @@ public function testYearlyBySetPosLoop(): void /** * This caused an incorrect date to be returned by the rule iterator when * start date was not on the rrule list. + * + * @dataProvider yearlyStartDateNotOnRRuleListProvider */ - public function testYearlyStartDateNotOnRRuleList(): void + public function testYearlyStartDateNotOnRRuleList(string $rule, string $start, array $expected): void { - $this->parse( - 'FREQ=YEARLY;BYMONTH=6;BYDAY=-1FR;UNTIL=20250901T000000Z', - '2023-09-01 12:00:00', + $this->parse($rule, $start, $expected); + } + + public function yearlyStartDateNotOnRRuleListProvider(): array + { + return [ [ + 'FREQ=YEARLY;BYMONTH=6;BYDAY=-1FR;UNTIL=20250901T000000Z', '2023-09-01 12:00:00', - '2024-06-28 12:00:00', - '2025-06-27 12:00:00', + [ + '2023-09-01 12:00:00', + '2024-06-28 12:00:00', + '2025-06-27 12:00:00', + ], ], - ); + [ + 'FREQ=YEARLY;BYMONTH=6;BYDAY=-1FR;UNTIL=20250901T000000Z', + '2023-06-01 12:00:00', + [ + '2023-06-01 12:00:00', + '2023-06-30 12:00:00', + '2024-06-28 12:00:00', + '2025-06-27 12:00:00', + ], + ], + [ + 'FREQ=YEARLY;BYMONTH=6;BYDAY=-1FR;UNTIL=20250901T000000Z', + '2023-05-01 12:00:00', + [ + '2023-05-01 12:00:00', + '2023-06-30 12:00:00', + '2024-06-28 12:00:00', + '2025-06-27 12:00:00', + ], + ], + ]; } /** From 5d7ca0075904c7ba8d534b4f59f5a40b00e89733 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 16:15:26 +0545 Subject: [PATCH 19/38] throw ParseException when null input is provided --- lib/Parser/MimeDir.php | 6 ++++++ tests/VObject/Parser/MimeDirTest.php | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/Parser/MimeDir.php b/lib/Parser/MimeDir.php index ddac9642b..be5d87baf 100644 --- a/lib/Parser/MimeDir.php +++ b/lib/Parser/MimeDir.php @@ -79,6 +79,12 @@ public function parse($input = null, int $options = 0): ?Document $this->setInput($input); } + if (!\is_resource($this->input)) { + // Null was passed as input, but there was no existing input buffer + // There is nothing to parse. + throw new ParseException('No input provided to parse'); + } + if (0 !== $options) { $this->options = $options; } diff --git a/tests/VObject/Parser/MimeDirTest.php b/tests/VObject/Parser/MimeDirTest.php index 24656f886..db5735b0c 100644 --- a/tests/VObject/Parser/MimeDirTest.php +++ b/tests/VObject/Parser/MimeDirTest.php @@ -101,6 +101,25 @@ public function testDecodeUnsupportedInlineCharset(): void $mimeDir->parse($vcard); } + public function provideEmptyParserInput(): array + { + return [ + [null, 'No input provided to parse'], + ['', 'End of document reached prematurely'], + ]; + } + + /** + * @dataProvider provideEmptyParserInput + */ + public function testParseEmpty($input, $expectedExceptionMessage): void + { + $this->expectException(ParseException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $mimeDir = new MimeDir(); + $mimeDir->parse($input); + } + public function testDecodeWindows1252(): void { $vcard = << Date: Mon, 22 Apr 2024 14:48:32 +0200 Subject: [PATCH 20/38] Reproduce bug where dst leap is passed on to subsequent occurences --- tests/VObject/Recur/RRuleIteratorTest.php | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 931a79967..f0f00ad41 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -162,6 +162,64 @@ public function testDailyBySetPosLoop(): void ); } + /** + * @dataProvider dstTransitionProvider + */ + public function testDailyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=DAILY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-24 02:00:00', + 'Expected' => [ + '2023-03-24 02:00:00', + '2023-03-25 02:00:00', + '2023-03-26 03:00:00', + '2023-03-27 02:00:00', + '2023-03-28 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-24 02:15:00', + 'Expected' => [ + '2023-03-24 02:15:00', + '2023-03-25 02:15:00', + '2023-03-26 03:15:00', + '2023-03-27 02:15:00', + '2023-03-28 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-24 03:00:00', + 'Expected' => [ + '2023-03-24 03:00:00', + '2023-03-25 03:00:00', + '2023-03-26 03:00:00', + '2023-03-27 03:00:00', + '2023-03-28 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-24 03:15:00', + 'Expected' => [ + '2023-03-24 03:15:00', + '2023-03-25 03:15:00', + '2023-03-26 03:15:00', + '2023-03-27 03:15:00', + '2023-03-28 03:15:00', + ], + ]; + } + public function testWeekly(): void { $this->parse( From b37ef3d72352e26e9e181bc1441c21a05c251fca Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 14:05:30 +0545 Subject: [PATCH 21/38] Fix test code format --- tests/VObject/Recur/RRuleIteratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index f0f00ad41..f850aa770 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -162,7 +162,7 @@ public function testDailyBySetPosLoop(): void ); } - /** + /** * @dataProvider dstTransitionProvider */ public function testDailyOnDstTransition(string $start, array $expected): void From 963189bb893d5b28fa6be75afa91233c41a3daf5 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:13:10 +0545 Subject: [PATCH 22/38] Handle summer time start for daily recurrences --- lib/Recur/RRuleIterator.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 10269dafb..4f25f8063 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -159,6 +159,14 @@ public function fastForward(\DateTimeInterface $dt): void */ protected ?\DateTimeInterface $currentDate; + /** + * The number of hours that the next occurrence of an event + * jumped forward, usually because summer time started and + * the requested time-of-day like 0230 did not exist on that + * day. And so the event was scheduled 1 hour later at 0330. + */ + protected int $hourJump = 0; + /** * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, * yearly. @@ -290,7 +298,23 @@ protected function nextHourly(): void protected function nextDaily(): void { if (!$this->byHour && !$this->byDay) { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } return; } From 2feab3f2e78a1240e6afcc3ac9224890a62ea043 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:37:29 +0545 Subject: [PATCH 23/38] Handle summer time start for weekly recurrences --- lib/Recur/RRuleIterator.php | 16 ++++++ tests/VObject/Recur/RRuleIteratorTest.php | 62 ++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 4f25f8063..1506f7c23 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -373,7 +373,23 @@ protected function nextDaily(): void protected function nextWeekly(): void { if (!$this->byHour && !$this->byDay) { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } return; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index f850aa770..a2518f529 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -163,7 +163,7 @@ public function testDailyBySetPosLoop(): void } /** - * @dataProvider dstTransitionProvider + * @dataProvider dstDailyTransitionProvider */ public function testDailyOnDstTransition(string $start, array $expected): void { @@ -176,7 +176,7 @@ public function testDailyOnDstTransition(string $start, array $expected): void ); } - public function dstTransitionProvider(): iterable + public function dstDailyTransitionProvider(): iterable { yield 'On transition start' => [ 'Start' => '2023-03-24 02:00:00', @@ -323,6 +323,64 @@ public function testWeeklyByDaySpecificHour(): void ); } + /** + * @dataProvider dstWeeklyTransitionProvider + */ + public function testWeeklyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=WEEKLY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstWeeklyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-12 02:00:00', + 'Expected' => [ + '2023-03-12 02:00:00', + '2023-03-19 02:00:00', + '2023-03-26 03:00:00', + '2023-04-02 02:00:00', + '2023-04-09 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-12 02:15:00', + 'Expected' => [ + '2023-03-12 02:15:00', + '2023-03-19 02:15:00', + '2023-03-26 03:15:00', + '2023-04-02 02:15:00', + '2023-04-09 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-12 03:00:00', + 'Expected' => [ + '2023-03-12 03:00:00', + '2023-03-19 03:00:00', + '2023-03-26 03:00:00', + '2023-04-02 03:00:00', + '2023-04-09 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-12 03:15:00', + 'Expected' => [ + '2023-03-12 03:15:00', + '2023-03-19 03:15:00', + '2023-03-26 03:15:00', + '2023-04-02 03:15:00', + '2023-04-09 03:15:00', + ], + ]; + } + public function testMonthly(): void { $this->parse( From 3b37fbc6a57312eaa55f200ce6d5fbeadc436f74 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:45:34 +0545 Subject: [PATCH 24/38] Handle summer time start for monthly recurrences --- lib/Recur/RRuleIterator.php | 16 +++++++ tests/VObject/Recur/RRuleIteratorTest.php | 58 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 1506f7c23..dccb4fc5d 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -448,7 +448,23 @@ protected function nextMonthly(): void // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } } else { $increase = 0; do { diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index a2518f529..21391b77c 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -530,6 +530,64 @@ public function testMonthlyByDayBySetPos(): void ); } + /** + * @dataProvider dstMonthlyTransitionProvider + */ + public function testMonthlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstMonthlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-01-26 02:00:00', + 'Expected' => [ + '2023-01-26 02:00:00', + '2023-02-26 02:00:00', + '2023-03-26 03:00:00', + '2023-04-26 02:00:00', + '2023-05-26 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-01-26 02:15:00', + 'Expected' => [ + '2023-01-26 02:15:00', + '2023-02-26 02:15:00', + '2023-03-26 03:15:00', + '2023-04-26 02:15:00', + '2023-05-26 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-01-26 03:00:00', + 'Expected' => [ + '2023-01-26 03:00:00', + '2023-02-26 03:00:00', + '2023-03-26 03:00:00', + '2023-04-26 03:00:00', + '2023-05-26 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-01-26 03:15:00', + 'Expected' => [ + '2023-01-26 03:15:00', + '2023-02-26 03:15:00', + '2023-03-26 03:15:00', + '2023-04-26 03:15:00', + '2023-05-26 03:15:00', + ], + ]; + } + public function testYearly(): void { $this->parse( From f4a0bba64f5de9ae98fdf9f93fca0e1df88afade Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 15:53:12 +0545 Subject: [PATCH 25/38] Handle summer time start for yearly recurrences --- lib/Recur/RRuleIterator.php | 16 +++++++ tests/VObject/Recur/RRuleIteratorTest.php | 58 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index dccb4fc5d..508ac002b 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -642,7 +642,23 @@ protected function nextYearly(): void } // The easiest form + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the nextDate. + // That happens if nextDate is a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) one hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } return; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 21391b77c..144f042a0 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -884,6 +884,64 @@ public function testYearlyByDayByWeekNo(): void ); } + /** + * @dataProvider dstYearlyTransitionProvider + */ + public function testYearlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=YEARLY;INTERVAL=1;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dstYearlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2021-03-26 02:00:00', + 'Expected' => [ + '2021-03-26 02:00:00', + '2022-03-26 02:00:00', + '2023-03-26 03:00:00', + '2024-03-26 02:00:00', + '2025-03-26 02:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2021-03-26 02:15:00', + 'Expected' => [ + '2021-03-26 02:15:00', + '2022-03-26 02:15:00', + '2023-03-26 03:15:00', + '2024-03-26 02:15:00', + '2025-03-26 02:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2021-03-26 03:00:00', + 'Expected' => [ + '2021-03-26 03:00:00', + '2022-03-26 03:00:00', + '2023-03-26 03:00:00', + '2024-03-26 03:00:00', + '2025-03-26 03:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2021-03-26 03:15:00', + 'Expected' => [ + '2021-03-26 03:15:00', + '2022-03-26 03:15:00', + '2023-03-26 03:15:00', + '2024-03-26 03:15:00', + '2025-03-26 03:15:00', + ], + ]; + } + public function testFastForward(): void { // The idea is that we're fast-forwarding too far in the future, so From 778177c996c9c8145b5ffc72339669f24247b1b1 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 16:07:36 +0545 Subject: [PATCH 26/38] Refactor summer time start logic into advanceTheDate function --- lib/Recur/RRuleIterator.php | 99 ++++++++++++------------------------- 1 file changed, 31 insertions(+), 68 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 508ac002b..eca07f732 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -284,6 +284,33 @@ public function fastForward(\DateTimeInterface $dt): void /* Functions that advance the iterator {{{ */ + /** + * Advances currentDate by the interval. + * Takes into account the case where summer time starts and + * the event time on that day may have had to be advanced, + * usually by 1 hour. + */ + protected function advanceTheDate(string $interval): void + { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); + $this->currentDate = $this->currentDate->modify($interval); + $hourOfNextDate = (int) $this->currentDate->format('G'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next date. + // That happens if the next date is a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + } else { + // The hour "jumped" for the previous date, to avoid the non-existent time. + // currentDate got set ahead by (usually) 1 hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } + } + /** * Does the processing for advancing the iterator for hourly frequency. */ @@ -298,23 +325,7 @@ protected function nextHourly(): void protected function nextDaily(): void { if (!$this->byHour && !$this->byDay) { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is a day when summer time starts - // and the event time is in the non-existent hour of the day. - // For example, an event that normally starts at 02:30 will - // have to start at 03:30 on that day. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one hour on that day. - // Adjust it back for this next occurrence. - $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); - $this->hourJump = 0; - } + $this->advanceTheDate('+'.$this->interval.' days'); return; } @@ -373,23 +384,7 @@ protected function nextDaily(): void protected function nextWeekly(): void { if (!$this->byHour && !$this->byDay) { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is a day when summer time starts - // and the event time is in the non-existent hour of the day. - // For example, an event that normally starts at 02:30 will - // have to start at 03:30 on that day. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one hour on that day. - // Adjust it back for this next occurrence. - $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); - $this->hourJump = 0; - } + $this->advanceTheDate('+'.$this->interval.' weeks'); return; } @@ -448,23 +443,7 @@ protected function nextMonthly(): void // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is a day when summer time starts - // and the event time is in the non-existent hour of the day. - // For example, an event that normally starts at 02:30 will - // have to start at 03:30 on that day. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one hour on that day. - // Adjust it back for this next occurrence. - $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); - $this->hourJump = 0; - } + $this->advanceTheDate('+'.$this->interval.' months'); } else { $increase = 0; do { @@ -642,23 +621,7 @@ protected function nextYearly(): void } // The easiest form - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); - $hourOfNextDate = (int) $this->currentDate->format('G'); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the nextDate. - // That happens if nextDate is a day when summer time starts - // and the event time is in the non-existent hour of the day. - // For example, an event that normally starts at 02:30 will - // have to start at 03:30 on that day. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) one hour on that day. - // Adjust it back for this next occurrence. - $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); - $this->hourJump = 0; - } + $this->advanceTheDate('+'.$this->interval.' years'); return; } From fb5689a29390488b5dc3934514b503d2859004d5 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 17:16:55 +0545 Subject: [PATCH 27/38] Handle summer time start for hourly recurrences --- lib/Recur/RRuleIterator.php | 21 ++++ tests/VObject/Recur/RRuleIteratorTest.php | 116 ++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index eca07f732..2f320036e 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -316,7 +316,28 @@ protected function advanceTheDate(string $interval): void */ protected function nextHourly(): void { + $hourOfCurrentDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next occurrence. + // That happens if the next event time is on a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + // If the interval is just 1 hour, then there is no "jumping back" to do. + // The events that day will happen, for example, at 0130 0330 0430 0530... + if ($this->interval > 1) { + $expectedHourOfNextDate = ($hourOfCurrentDate + $this->interval) % 24; + $actualHourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; + } + } else { + // The hour "jumped" for the previous occurrence, to avoid the non-existent time. + // currentDate got set ahead by (usually) 1 hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } } /** diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 144f042a0..51640a956 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -29,6 +29,122 @@ public function testHourly(): void ); } + /** + * @dataProvider dst2HourlyTransitionProvider + */ + public function test2HourlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=HOURLY;INTERVAL=2;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dst2HourlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-26 00:00:00', + 'Expected' => [ + '2023-03-26 00:00:00', + '2023-03-26 03:00:00', + '2023-03-26 04:00:00', + '2023-03-26 06:00:00', + '2023-03-26 08:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-26 00:15:00', + 'Expected' => [ + '2023-03-26 00:15:00', + '2023-03-26 03:15:00', + '2023-03-26 04:15:00', + '2023-03-26 06:15:00', + '2023-03-26 08:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-26 01:00:00', + 'Expected' => [ + '2023-03-26 01:00:00', + '2023-03-26 03:00:00', + '2023-03-26 05:00:00', + '2023-03-26 07:00:00', + '2023-03-26 09:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-26 01:15:00', + 'Expected' => [ + '2023-03-26 01:15:00', + '2023-03-26 03:15:00', + '2023-03-26 05:15:00', + '2023-03-26 07:15:00', + '2023-03-26 09:15:00', + ], + ]; + } + + /** + * @dataProvider dst6HourlyTransitionProvider + */ + public function testHourlyOnDstTransition(string $start, array $expected): void + { + $this->parse( + 'FREQ=HOURLY;INTERVAL=6;COUNT=5', + $start, + $expected, + null, + 'Europe/Zurich', + ); + } + + public function dst6HourlyTransitionProvider(): iterable + { + yield 'On transition start' => [ + 'Start' => '2023-03-25 20:00:00', + 'Expected' => [ + '2023-03-25 20:00:00', + '2023-03-26 03:00:00', + '2023-03-26 08:00:00', + '2023-03-26 14:00:00', + '2023-03-26 20:00:00', + ], + ]; + yield 'During transition' => [ + 'Start' => '2023-03-25 20:15:00', + 'Expected' => [ + '2023-03-25 20:15:00', + '2023-03-26 03:15:00', + '2023-03-26 08:15:00', + '2023-03-26 14:15:00', + '2023-03-26 20:15:00', + ], + ]; + yield 'On transition end' => [ + 'Start' => '2023-03-25 21:00:00', + 'Expected' => [ + '2023-03-25 21:00:00', + '2023-03-26 03:00:00', + '2023-03-26 09:00:00', + '2023-03-26 15:00:00', + '2023-03-26 21:00:00', + ], + ]; + yield 'After transition end' => [ + 'Start' => '2023-03-25 21:15:00', + 'Expected' => [ + '2023-03-25 21:15:00', + '2023-03-26 03:15:00', + '2023-03-26 09:15:00', + '2023-03-26 15:15:00', + '2023-03-26 21:15:00', + ], + ]; + } + public function testDaily(): void { $this->parse( From 018789e4456d07dd953133b1102ef3dee66950ad Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 9 May 2024 17:21:14 +0545 Subject: [PATCH 28/38] Refactor advanceTheDate --- lib/Recur/RRuleIterator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 2f320036e..f19ef0596 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -292,16 +292,16 @@ public function fastForward(\DateTimeInterface $dt): void */ protected function advanceTheDate(string $interval): void { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); + $hourOfPreviousDate = (int) $this->currentDate->format('G'); $this->currentDate = $this->currentDate->modify($interval); - $hourOfNextDate = (int) $this->currentDate->format('G'); if (0 === $this->hourJump) { // Remember if the clock time jumped forward on the next date. // That happens if the next date is a day when summer time starts // and the event time is in the non-existent hour of the day. // For example, an event that normally starts at 02:30 will // have to start at 03:30 on that day. - $this->hourJump = $hourOfNextDate - $hourOfCurrentDate; + $hourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $hourOfNextDate - $hourOfPreviousDate; } else { // The hour "jumped" for the previous date, to avoid the non-existent time. // currentDate got set ahead by (usually) 1 hour on that day. From d0cb455e70ff05a85dac55fe83e6b56de77cd395 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 14:23:41 +0545 Subject: [PATCH 29/38] fix: refactor advanceTheDate --- lib/Recur/RRuleIterator.php | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index f19ef0596..f562d750d 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -286,29 +286,16 @@ public function fastForward(\DateTimeInterface $dt): void /** * Advances currentDate by the interval. - * Takes into account the case where summer time starts and - * the event time on that day may have had to be advanced, - * usually by 1 hour. + * The time is set from the original startDate. + * If the recurrence is on a day when summer time started, then the + * time on that day may have jumped forward, for example, from 0230 to 0330. + * Using the original time means that the next recurrence will be calculated + * based on the original start time and the day/week/month/year interval. + * So the start time of the next occurrence can correctly revert to 0230. */ protected function advanceTheDate(string $interval): void { - $hourOfPreviousDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify($interval); - if (0 === $this->hourJump) { - // Remember if the clock time jumped forward on the next date. - // That happens if the next date is a day when summer time starts - // and the event time is in the non-existent hour of the day. - // For example, an event that normally starts at 02:30 will - // have to start at 03:30 on that day. - $hourOfNextDate = (int) $this->currentDate->format('G'); - $this->hourJump = $hourOfNextDate - $hourOfPreviousDate; - } else { - // The hour "jumped" for the previous date, to avoid the non-existent time. - // currentDate got set ahead by (usually) 1 hour on that day. - // Adjust it back for this next occurrence. - $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); - $this->hourJump = 0; - } + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startDate->format('H:i:s')); } /** From eef9fa6003d7d000d8836bc2d8dc11dc726ace63 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 15:28:30 +0545 Subject: [PATCH 30/38] Handle case when BYMONTHDAY falls on summer time start --- lib/Recur/RRuleIterator.php | 6 +++++- tests/VObject/Recur/RRuleIteratorTest.php | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index f562d750d..0c810d947 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -508,11 +508,15 @@ protected function nextMonthly(): void } } + // Set the currentDate to the year and month that we are in, and the day of the month that we have selected. + // That day could be a day when summer time starts, and if the time of the event is, for example, 0230, + // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate. + // The "modify" method will set the time forward to 0330, for example, if needed. $this->currentDate = $this->currentDate->setDate( (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), (int) $occurrence - ); + )->modify($this->startDate->format('H:i:s')); } /** diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 51640a956..d8dda554c 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -553,6 +553,26 @@ public function testMonthlyByMonthDay(): void ); } + public function testMonthlyByMonthDayDstTransition(): void + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;COUNT=8;BYMONTHDAY=1,26', + '2023-01-01 02:15:00', + [ + '2023-01-01 02:15:00', + '2023-01-26 02:15:00', + '2023-02-01 02:15:00', + '2023-02-26 02:15:00', + '2023-03-01 02:15:00', + '2023-03-26 03:15:00', + '2023-04-01 02:15:00', + '2023-04-26 02:15:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testMonthlyByDay(): void { $this->parse( From 9b20d5e927a632a7ce6dc6a61eaf4bd18c05671e Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Fri, 17 May 2024 15:50:44 +0545 Subject: [PATCH 31/38] Handle case when day at or near end of month falls on summer time start --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 0c810d947..4b19f77ca 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -457,7 +457,7 @@ protected function nextMonthly(): void do { ++$increase; $tempDate = clone $this->currentDate; - $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months'); + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startDate->format('H:i:s')); } while ($tempDate->format('j') != $currentDayOfMonth); $this->currentDate = $tempDate; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index d8dda554c..ee40c3707 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -722,6 +722,16 @@ public function dstMonthlyTransitionProvider(): iterable '2023-05-26 03:15:00', ], ]; + yield 'During transition on 31st day of month' => [ + 'Start' => '2024-01-31 02:15:00', + 'Expected' => [ + '2024-01-31 02:15:00', + '2024-03-31 03:15:00', + '2024-05-31 02:15:00', + '2024-07-31 02:15:00', + '2024-08-31 02:15:00', + ], + ]; } public function testYearly(): void From 102909efa694abd1e8574f0d2a3e9e71dcd099a0 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 14:08:12 +0545 Subject: [PATCH 32/38] refactor hourly time jump logic into adjustForTimeJumpsOfHourlyEvent private method --- lib/Recur/RRuleIterator.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 4b19f77ca..aa6261968 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -299,12 +299,10 @@ protected function advanceTheDate(string $interval): void } /** - * Does the processing for advancing the iterator for hourly frequency. + * Does the processing for adjusting the time of multi-hourly events when summer time starts. */ - protected function nextHourly(): void + private function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void { - $hourOfCurrentDate = (int) $this->currentDate->format('G'); - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); if (0 === $this->hourJump) { // Remember if the clock time jumped forward on the next occurrence. // That happens if the next event time is on a day when summer time starts @@ -312,9 +310,9 @@ protected function nextHourly(): void // For example, an event that normally starts at 02:30 will // have to start at 03:30 on that day. // If the interval is just 1 hour, then there is no "jumping back" to do. - // The events that day will happen, for example, at 0130 0330 0430 0530... + // The events that day will happen, for example, at 0030 0130 0330 0430 0530... if ($this->interval > 1) { - $expectedHourOfNextDate = ($hourOfCurrentDate + $this->interval) % 24; + $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24; $actualHourOfNextDate = (int) $this->currentDate->format('G'); $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; } @@ -327,6 +325,16 @@ protected function nextHourly(): void } } + /** + * Does the processing for advancing the iterator for hourly frequency. + */ + protected function nextHourly(): void + { + $previousEventDateTime = clone $this->currentDate; + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime); + } + /** * Does the processing for advancing the iterator for daily frequency. */ From 85d72e0d4d3e7d31149c5cc90d402c7f4fd05534 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 14:31:48 +0545 Subject: [PATCH 33/38] refactor original start time calculation into startTime method --- lib/Recur/RRuleIterator.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index aa6261968..f1fd03e3f 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -284,6 +284,16 @@ public function fastForward(\DateTimeInterface $dt): void /* Functions that advance the iterator {{{ */ + /** + * Gets the original start time of the RRULE. + * + * The value is formatted as a string with 24-hour:minute:second + */ + protected function startTime(): string + { + return $this->startDate->format('H:i:s'); + } + /** * Advances currentDate by the interval. * The time is set from the original startDate. @@ -295,7 +305,7 @@ public function fastForward(\DateTimeInterface $dt): void */ protected function advanceTheDate(string $interval): void { - $this->currentDate = $this->currentDate->modify($interval.' '.$this->startDate->format('H:i:s')); + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime()); } /** @@ -465,7 +475,7 @@ protected function nextMonthly(): void do { ++$increase; $tempDate = clone $this->currentDate; - $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startDate->format('H:i:s')); + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); } while ($tempDate->format('j') != $currentDayOfMonth); $this->currentDate = $tempDate; } @@ -524,7 +534,7 @@ protected function nextMonthly(): void (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), (int) $occurrence - )->modify($this->startDate->format('H:i:s')); + )->modify($this->startTime()); } /** From cc112fbde94450699a957bfc4be48caa8e699f77 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 27 May 2024 14:33:03 +0545 Subject: [PATCH 34/38] refactor adjustForTimeJumpsOfHourlyEvent to be protected --- lib/Recur/RRuleIterator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index f1fd03e3f..84f761bce 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -311,7 +311,7 @@ protected function advanceTheDate(string $interval): void /** * Does the processing for adjusting the time of multi-hourly events when summer time starts. */ - private function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void + protected function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void { if (0 === $this->hourJump) { // Remember if the clock time jumped forward on the next occurrence. From 9d68c7aff72db1a78330dc13e3ca175874b27ac4 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 12:31:10 +0545 Subject: [PATCH 35/38] Handle summer time start for weekly BYDAY recurrences --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 84f761bce..a90ae9087 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -432,7 +432,7 @@ protected function nextWeekly(): void if ($this->byHour) { $this->currentDate = $this->currentDate->modify('+1 hours'); } else { - $this->currentDate = $this->currentDate->modify('+1 days'); + $this->advanceTheDate('+1 days'); } // Current day of the week diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index ee40c3707..08060a450 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -439,6 +439,24 @@ public function testWeeklyByDaySpecificHour(): void ); } + public function testWeeklyByDaySpecificHourOnDstTransition(): void + { + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=SA,SU', + '2023-03-11 02:30:00', + [ + '2023-03-11 02:30:00', + '2023-03-12 02:30:00', + '2023-03-25 02:30:00', + '2023-03-26 03:30:00', + '2023-04-08 02:30:00', + '2023-04-09 02:30:00', + ], + null, + 'Europe/Zurich', + ); + } + /** * @dataProvider dstWeeklyTransitionProvider */ From 5a3dd88c084c20ee87334ba7e54434f16449dad0 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 12:52:42 +0545 Subject: [PATCH 36/38] Add test case for Weekly BYDAY with BYHOUR on summer-time --- tests/VObject/Recur/RRuleIteratorTest.php | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 08060a450..e56d0c63b 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -457,6 +457,34 @@ public function testWeeklyByDaySpecificHourOnDstTransition(): void ); } + public function testWeeklyByDayByHourOnDstTransition(): void + { + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=SA,SU;WKST=MO;BYHOUR=2,14', + '2023-03-11 02:00:00', + [ + '2023-03-11 02:00:00', + '2023-03-11 14:00:00', + '2023-03-12 02:00:00', + '2023-03-12 14:00:00', + '2023-03-25 02:00:00', + '2023-03-25 14:00:00', + // 02:00:00 does not exist on 2023-03-26 because of summer-time start. + // The current implementation logic does not schedule a recurrence on + // the morning of 2023-03-26. But maybe it should schedule one at 03:00:00. + // The RFC is silent about the required behavior in this case. + // '2023-03-26 03:00:00', + '2023-03-26 14:00:00', + '2023-04-08 02:00:00', + '2023-04-08 14:00:00', + '2023-04-09 02:00:00', + '2023-04-09 14:00:00', + ], + null, + 'Europe/Zurich', + ); + } + /** * @dataProvider dstWeeklyTransitionProvider */ From 9039f90fc6d2ee4a54ba53e841f9da651eb7bb92 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 13:49:01 +0545 Subject: [PATCH 37/38] Add test cases and fix YEARLY with BYMONTH on summer-time transition --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 45 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index a90ae9087..6ce3e609f 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -732,7 +732,7 @@ protected function nextYearly(): void (int) $currentYear, (int) $currentMonth, (int) $currentDayOfMonth - ); + )->modify($this->startTime()); return; } diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index e56d0c63b..2a206e56e 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -661,6 +661,31 @@ public function testMonthlyByDayUntil(): void ); } + public function testMonthlyByDayOnDstTransition(): void + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=2;COUNT=13;BYDAY=SU', + '2023-01-01 02:30:00', + [ + '2023-01-01 02:30:00', + '2023-01-08 02:30:00', + '2023-01-15 02:30:00', + '2023-01-22 02:30:00', + '2023-01-29 02:30:00', + '2023-03-05 02:30:00', + '2023-03-12 02:30:00', + '2023-03-19 02:30:00', + '2023-03-26 03:30:00', + '2023-05-07 02:30:00', + '2023-05-14 02:30:00', + '2023-05-21 02:30:00', + '2023-05-28 02:30:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testMonthlyByDayUntilWithImpossibleNextOccurrence(): void { $this->parse( @@ -831,6 +856,26 @@ public function testYearlyByMonth(): void ); } + public function testYearlyByMonthOnDstTransition(): void + { + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=2;BYMONTH=3,9', + '2019-03-26 02:30:00', + [ + '2019-03-26 02:30:00', + '2019-09-26 02:30:00', + '2021-03-26 02:30:00', + '2021-09-26 02:30:00', + '2023-03-26 03:30:00', + '2023-09-26 02:30:00', + '2025-03-26 02:30:00', + '2025-09-26 02:30:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testYearlyByMonthInvalidValue1(): void { $this->expectException(InvalidDataException::class); From 1d0d0bdd2ca4ca3cc71d36145c50c340d92561f8 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 30 May 2024 14:01:25 +0545 Subject: [PATCH 38/38] Add test cases and fix YEARLY with BYMONTH BYDAY on summer-time transition --- lib/Recur/RRuleIterator.php | 2 +- tests/VObject/Recur/RRuleIteratorTest.php | 25 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 6ce3e609f..ff276a697 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -715,7 +715,7 @@ protected function nextYearly(): void (int) $currentYear, (int) $currentMonth, (int) $occurrence - ); + )->modify($this->startTime()); return; } else { diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 2a206e56e..ea049a170 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -934,6 +934,31 @@ public function testYearlyByMonthByDay(): void ); } + public function testYearlyByMonthByDayOnDstTransition(): void + { + $this->parse( + 'FREQ=YEARLY;COUNT=13;INTERVAL=2;BYMONTH=3;BYDAY=SU', + '2021-03-07 02:30:00', + [ + '2021-03-07 02:30:00', + '2021-03-14 02:30:00', + '2021-03-21 02:30:00', + '2021-03-28 03:30:00', + '2023-03-05 02:30:00', + '2023-03-12 02:30:00', + '2023-03-19 02:30:00', + '2023-03-26 03:30:00', + '2025-03-02 02:30:00', + '2025-03-09 02:30:00', + '2025-03-16 02:30:00', + '2025-03-23 02:30:00', + '2025-03-30 03:30:00', + ], + null, + 'Europe/Zurich', + ); + } + public function testYearlyNewYearsDay(): void { $this->parse(