Skip to content

Commit

Permalink
Merge upstream 2024 06 27 (#106)
Browse files Browse the repository at this point in the history
* 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](actions/cache@v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>

* Apply cs-fixer changes

* 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](codecov/codecov-action@v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>

* chore: use php-cs-fixer 3.49

* chore: use php-cs-fixer 3.51

* chore: apply cs-fixer 3.54.0 changes

* chore: bump php-cs-fixer requirement to 3.54

* add `lineIndex` and `lineString` properties to Node

* creat Unit Test

* ITip\Broker: handle timezones in replies to exception events

Co-authored-by: Luc DUZAN <[email protected]>

* remove comment

* php-cs-fixer

* remove unnecessary error descriptions

* Add comments about use of getTimestamp

* chore: stop exporting php-cs-fixer config

* chore: bump dev dependencies

* yearly rrule compliance by the iterator when start date does not follow the rrule

* test: add more test scenarios for testYearlyStartDateNotOnRRuleList

* throw ParseException when null input is provided

* Reproduce bug where dst leap is passed on to subsequent occurences

* Fix test code format

* Handle summer time start for daily recurrences

* Handle summer time start for weekly recurrences

* Handle summer time start for monthly recurrences

* Handle summer time start for yearly recurrences

* Refactor summer time start logic into advanceTheDate function

* Handle summer time start for hourly recurrences

* Refactor advanceTheDate

* fix: refactor advanceTheDate

* Handle case when BYMONTHDAY falls on summer time start

* Handle case when day at or near end of month falls on summer time start

* refactor hourly time jump logic into adjustForTimeJumpsOfHourlyEvent private method

* refactor original start time calculation into startTime method

* refactor adjustForTimeJumpsOfHourlyEvent to be protected

* Handle summer time start for weekly BYDAY recurrences

* Add test case for Weekly BYDAY with BYHOUR on summer-time

* Add test cases and fix YEARLY with BYMONTH on summer-time transition

* Add test cases and fix YEARLY with BYMONTH BYDAY on summer-time transition

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Phil Davis <[email protected]>
Co-authored-by: John R. D'Orazio <[email protected]>
Co-authored-by: Gregor Harlan <[email protected]>
Co-authored-by: Luc DUZAN <[email protected]>
Co-authored-by: John D'Orazio <[email protected]>
Co-authored-by: Victor Emanouilov <[email protected]>
Co-authored-by: Cyril van Schreven <[email protected]>
  • Loading branch information
9 people authored Jun 28, 2024
1 parent ac1421c commit df90391
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
/.gitignore export-ignore
/.php_cs.dist export-ignore
/.travis.yml export-ignore
/.php-cs-fixer.dist.php export-ignore
/CHANGELOG.md export-ignore
/phpstan.neon export-ignore
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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'
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"sabre/xml" : "^3.0 || ^4.0"
},
"require-dev" : {
"friendsofphp/php-cs-fixer": "^3.38",
"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"
Expand Down
4 changes: 2 additions & 2 deletions lib/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, '.'))) {
Expand Down Expand Up @@ -204,7 +204,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);
}

/**
Expand Down
13 changes: 10 additions & 3 deletions lib/ITip/Broker.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,10 @@ 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';
// 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();
if (isset($vevent->{'REQUEST-STATUS'})) {
Expand All @@ -346,7 +349,8 @@ 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';
// 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;
}
Expand Down Expand Up @@ -393,7 +397,10 @@ protected function processMessageReply(Message $itipMessage, ?VCalendar $existin
$newObject = $recurrenceIterator->getEventObject();
$recurrenceIterator->next();

if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
// 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;
}
--$iterations;
Expand Down
8 changes: 7 additions & 1 deletion lib/Parser/MimeDir.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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;
}
Expand Down Expand Up @@ -463,7 +469,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);
Expand Down
24 changes: 23 additions & 1 deletion lib/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
Expand All @@ -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;
}
}

/**
Expand Down
79 changes: 72 additions & 7 deletions lib/Recur/RRuleIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ private function jumpForward(\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.
Expand Down Expand Up @@ -427,12 +435,65 @@ private function jumpForward(\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.
* 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
{
$this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime());
}

/**
* Does the processing for adjusting the time of multi-hourly events when summer time starts.
*/
protected function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void
{
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 0030 0130 0330 0430 0530...
if ($this->interval > 1) {
$expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $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;
}
}

/**
* Does the processing for advancing the iterator for hourly frequency.
*/
protected function nextHourly($amount = 1): void
{
$previousEventDateTime = clone $this->currentDate;
$this->currentDate = $this->currentDate->modify('+'.$amount * $this->interval.' hours');
$this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime);
}

/**
Expand All @@ -441,7 +502,7 @@ protected function nextHourly($amount = 1): void
protected function nextDaily($amount = 1): void
{
if (!$this->byHour && !$this->byDay) {
$this->currentDate = $this->currentDate->modify('+'.$amount * $this->interval.' days');
$this->advanceTheDate('+'.$amount * $this->interval.' days');

return;
}
Expand Down Expand Up @@ -502,7 +563,7 @@ protected function nextDaily($amount = 1): void
protected function nextWeekly($amount = 1): void
{
if (!$this->byHour && !$this->byDay) {
$this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' weeks');
$this->advanceTheDate('+'.$amount * $this->interval.' weeks');

return;
}
Expand All @@ -524,7 +585,7 @@ protected function nextWeekly($amount = 1): 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
Expand Down Expand Up @@ -564,13 +625,13 @@ protected function nextMonthly($amount = 1): void
// occur to the next month. We Must skip these invalid
// entries.
if ($currentDayOfMonth < 29) {
$this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' months');
$this->advanceTheDate('+'.($amount * $this->interval).' months');
} else {
$increase = $amount - 1;
do {
++$increase;
$tempDate = clone $this->currentDate;
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months');
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime());
} while ($tempDate->format('j') != $currentDayOfMonth);
$this->currentDate = $tempDate;
}
Expand Down Expand Up @@ -641,6 +702,10 @@ protected function nextMonthly($amount = 1): 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'),
Expand Down Expand Up @@ -767,7 +832,7 @@ protected function nextYearly($amount = 1): void
}

// The easiest form
$this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' years');
$this->advanceTheDate('+'.($amount * $this->interval).' years');

return;
}
Expand Down Expand Up @@ -858,7 +923,7 @@ protected function nextYearly($amount = 1): void
(int) $currentYear,
(int) $currentMonth,
(int) $currentDayOfMonth
);
)->modify($this->startTime());

return;
}
Expand Down
43 changes: 43 additions & 0 deletions tests/VObject/Component/VCalendarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,49 @@ public function testCalDAVMETHOD(): void
);
}

public function testNodeInValidationErrorHasLineIndexAndLineStringProps(): void
{
$defectiveInput = <<<ICS
BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
PRODID:vobject
BEGIN:VEVENT
UID:foo
CLASS:PUBLIC
DTSTART;VALUE=DATE:19931231
DTSTAMP:20240422T070855Z
CREATED:
LAST-MODIFIED:
DESCRIPTION:bar
END:VEVENT
ICS;

$vcal = VObject\Reader::read($defectiveInput);
$result = $vcal->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);
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);
self::assertEquals('CREATED:', $warning['node']->lineString);
break;
case 1:
self::assertEquals('11', $warning['node']->lineIndex);
self::assertEquals('LAST-MODIFIED:', $warning['node']->lineString);
break;
}
}
}

public function assertValidate($ics, $options, $expectedLevel, ?string $expectedMessage = null): void
{
$vcal = VObject\Reader::read($ics);
Expand Down
Loading

0 comments on commit df90391

Please sign in to comment.