Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge upstream 2024 06 27 #106

Merged
merged 52 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7acdf37
Bump actions/cache from 3 to 4
dependabot[bot] Feb 1, 2024
5c3a532
Apply cs-fixer changes
phil-davis Feb 2, 2024
31b90cd
Merge pull request #637 from sabre-io/dependabot/github_actions/actio…
phil-davis Feb 2, 2024
54b7952
Bump codecov/codecov-action from 3 to 4
dependabot[bot] Feb 2, 2024
c5b0742
Merge pull request #638 from sabre-io/dependabot/github_actions/codec…
phil-davis Feb 2, 2024
7f5ddc4
chore: use php-cs-fixer 3.49
phil-davis Feb 11, 2024
7968406
Merge pull request #639 from phil-davis/cs-fixer-3.49
phil-davis Feb 11, 2024
db65228
chore: use php-cs-fixer 3.51
phil-davis Mar 4, 2024
7c3a8db
Merge pull request #642 from phil-davis/cs-fixer-3.51
phil-davis Mar 4, 2024
624a4f9
chore: apply cs-fixer 3.54.0 changes
phil-davis Apr 18, 2024
b20cd3d
Merge pull request #644 from phil-davis/cs-fixer-20240418
phil-davis Apr 18, 2024
f14092f
chore: bump php-cs-fixer requirement to 3.54
phil-davis Apr 19, 2024
caa7af8
Merge pull request #645 from phil-davis/cs-fixer-3.54
phil-davis Apr 19, 2024
a848f93
add `lineIndex` and `lineString` properties to Node
JohnRDOrazio Apr 23, 2024
800aac9
creat Unit Test
JohnRDOrazio Apr 25, 2024
2a65337
ITip\Broker: handle timezones in replies to exception events
gharlan May 8, 2024
620ee98
remove comment
gharlan May 8, 2024
9192bfe
php-cs-fixer
JohnRDOrazio May 8, 2024
ec31757
remove unnecessary error descriptions
JohnRDOrazio May 8, 2024
8a2dab4
Merge pull request #649 from JohnRDOrazio/validation-fix
phil-davis May 9, 2024
bee4fa7
Add comments about use of getTimestamp
phil-davis May 9, 2024
d4bf35b
Merge pull request #652 from gharlan/recurrence-id-timezone
phil-davis May 9, 2024
e5976c9
chore: stop exporting php-cs-fixer config
phil-davis May 10, 2024
fe5d261
Merge pull request #654 from phil-davis/export-ignore-php-cs-fixer
phil-davis May 10, 2024
8bf65e2
chore: bump dev dependencies
phil-davis May 14, 2024
a865996
Merge pull request #655 from phil-davis/bump-dev-deps-20240514
phil-davis May 14, 2024
1a61550
yearly rrule compliance by the iterator when start date does not foll…
kroky May 14, 2024
72a1fe9
test: add more test scenarios for testYearlyStartDateNotOnRRuleList
phil-davis May 17, 2024
254f85d
Merge pull request #656 from kroky/bugfix/bymonth-rrule
phil-davis May 17, 2024
5d7ca00
throw ParseException when null input is provided
phil-davis May 27, 2024
227f681
Merge pull request #658 from phil-davis/issue-657
phil-davis May 27, 2024
b9c39da
Reproduce bug where dst leap is passed on to subsequent occurences
Apr 22, 2024
b37ef3d
Fix test code format
phil-davis May 9, 2024
963189b
Handle summer time start for daily recurrences
phil-davis May 9, 2024
2feab3f
Handle summer time start for weekly recurrences
phil-davis May 9, 2024
3b37fbc
Handle summer time start for monthly recurrences
phil-davis May 9, 2024
f4a0bba
Handle summer time start for yearly recurrences
phil-davis May 9, 2024
778177c
Refactor summer time start logic into advanceTheDate function
phil-davis May 9, 2024
fb5689a
Handle summer time start for hourly recurrences
phil-davis May 9, 2024
018789e
Refactor advanceTheDate
phil-davis May 9, 2024
d0cb455
fix: refactor advanceTheDate
phil-davis May 17, 2024
eef9fa6
Handle case when BYMONTHDAY falls on summer time start
phil-davis May 17, 2024
9b20d5e
Handle case when day at or near end of month falls on summer time start
phil-davis May 17, 2024
102909e
refactor hourly time jump logic into adjustForTimeJumpsOfHourlyEvent …
phil-davis May 27, 2024
85d72e0
refactor original start time calculation into startTime method
phil-davis May 27, 2024
cc112fb
refactor adjustForTimeJumpsOfHourlyEvent to be protected
phil-davis May 27, 2024
9d68c7a
Handle summer time start for weekly BYDAY recurrences
phil-davis May 30, 2024
5a3dd88
Add test case for Weekly BYDAY with BYHOUR on summer-time
phil-davis May 30, 2024
9039f90
Add test cases and fix YEARLY with BYMONTH on summer-time transition
phil-davis May 30, 2024
1d0d0bd
Add test cases and fix YEARLY with BYMONTH BYDAY on summer-time trans…
phil-davis May 30, 2024
1b388aa
Merge pull request #653 from phil-davis/dst-leap-648
phil-davis Jun 6, 2024
3f43b00
Merge branch 'master' of github.com:sabre-io/vobject into merge-upstr…
giuseppe-arcuti Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading