diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 775b3389f..08c0b0a2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,16 +12,22 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] coverage: ['pcov'] + code-style: ['no'] code-analysis: ['no'] include: - php-versions: '7.4' - coverage: 'none' + coverage: 'pcov' + code-style: 'yes' + code-analysis: 'yes' + - php-versions: '8.5' + coverage: 'pcov' + code-style: 'no' code-analysis: 'yes' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php @@ -36,7 +42,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} # Use composer.json for key, if composer.lock is not committed. @@ -48,8 +54,8 @@ jobs: run: composer install --no-progress --prefer-dist --optimize-autoloader - name: Code Analysis (PHP CS-Fixer) - if: matrix.code-analysis == 'yes' - run: php vendor/bin/php-cs-fixer fix --dry-run --diff + if: matrix.code-style == 'yes' + run: PHP_CS_FIXER_IGNORE_ENV=true php vendor/bin/php-cs-fixer fix --dry-run --diff - name: Code Analysis (PHPStan) if: matrix.code-analysis == 'yes' @@ -59,5 +65,5 @@ jobs: run: vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover clover.xml - name: Code Coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 if: matrix.coverage != 'none' diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index e981862b7..4d464eb3b 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -10,6 +10,10 @@ $config->setRules([ '@PSR1' => true, '@Symfony' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, ]); return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4a59661..7831feba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -178,6 +178,65 @@ ChangeLog * #3 Add daily occurrences to nextMonth and NextYear * #4 Enhance fast forward speed if no count value has been given +4.6.0 (2026-05-31) +------------------ +* #718 feat(VCard): add getByTypes method (@JimKnoxx) +* #756 fix: Update deprecated timezone names (@ralflang) + +4.5.9 (2026-05-31) +------------------ +* #759 fix(itip): handle null old calendar/event (@ChristophWurst) + +4.5.8 (2026-01-12) +------------------ + +* #721: Fix variable scope (@KristofferFM) +* #723: Broker shouldn't add EXDATE property with floating timezone (@KristofferFM) +* #726: Improved check on property name (@KristofferFM) +* #727: Improve error checks in Mimedir parser (@KristofferFM) +* #728: Removing attendee from scheduled recurring event should generate an EXDATE property (@KristofferFM) +* #735: use RDATE in time range check and use all instances (@SebastianKrupinski) +* #743: send participation reply on fresh event (@SebastianKrupinski) + +4.5.7 (2025-04-17) +------------------ + +* #618: Add new exchangetimezone 'Amsterdam, Berlin, Bern, Rom, Stockholm, Wien' (@chr-is) +* #674: Allow unknown value data types for VALUE (@heiglandreas) +* #707: add phpdoc return types for arrayaccess methods (@smhg) + +4.5.6 (2024-10-14) +------------------ + +* #662: add PHP 8.4 to CI (@phil-davis) +* #669: fix: check if event status was changed to cancelled and generate appropriate message (@SebastianKrupinski) +* #673: Fix Implicitly marking parameter $param as nullable is deprecated (@cedric-anne) +* #675: refactor: Replace dirname(__FILE__) with __DIR__ (@ChristophWurst) +* #692: Throw InvalidDataException when RRule is invalid (@mathroc) + +4.5.5 (2024-07-02) +------------------ + +* #632: Add PHP 8.3 to CI (@phil-davis) +* #649: add lineIndex and lineString properties to a Property Node (@JohnRDOrazio) +* #652: ITip\Broker: handle timezones in replies to exception events (@gharlan) +* #654: chore: stop exporting php-cs-fixer config (@phil-davis) +* #656: Yearly rrule compliance by the iterator (@kroky) +* #658: throw ParseException when null input is provided (@phil-davis) +* #653: Handle summer time jumps in event recurrences (@phil-davis) + +4.5.4 (2023-11-09) +------------------ + +* #619: fixed issue: Undefined index: plusminus (@sash04ek) +* #622: Create Issue467Test.php for 619 (@sash04ek) +* #623: docs: fix type definition for oldCalendar (@kesselb) + +4.5.3 (2023-01-22) +------------------ + +* #607: Apply 'Support sabre/xml v4' and needed code changes to 4.5 branch (@phil-davis) + 4.5.2 (2023-01-20) ------------------ diff --git a/bin/bench.php b/bin/bench.php index 0a2736fa6..98994d1b6 100755 --- a/bin/bench.php +++ b/bin/bench.php @@ -7,6 +7,6 @@ $start = microtime(true); -$lol = Sabre\VObject\Reader::read($data); +$lol = VObject\Reader::read($data); echo 'time: '.(microtime(true) - $start)."\n"; diff --git a/bin/bench_freebusygenerator.php b/bin/bench_freebusygenerator.php index ae859f487..bc2210532 100644 --- a/bin/bench_freebusygenerator.php +++ b/bin/bench_freebusygenerator.php @@ -3,7 +3,7 @@ include __DIR__.'/../vendor/autoload.php'; if ($argc < 2) { - echo 'sabre/vobject ', Sabre\VObject\Version::VERSION, " freebusy benchmark\n"; + echo 'sabre/vobject ', VObject\Version::VERSION, " freebusy benchmark\n"; echo "\n"; echo "This script can be used to measure the speed of generating a\n"; echo "free-busy report based on a calendar.\n"; @@ -19,7 +19,7 @@ $bench = new Hoa\Bench\Bench(); $bench->parse->start(); -$vcal = Sabre\VObject\Reader::read(fopen($inputFile, 'r')); +$vcal = VObject\Reader::read(fopen($inputFile, 'r')); $bench->parse->stop(); @@ -31,7 +31,7 @@ $bench->fb->start(); for ($i = 0; $i < $repeat; ++$i) { - $fb = new Sabre\VObject\FreeBusyGenerator($start, $end, $vcal, $timeZone); + $fb = new VObject\FreeBusyGenerator($start, $end, $vcal, $timeZone); $results = $fb->getResult(); } $bench->fb->stop(); diff --git a/bin/bench_manipulatevcard.php b/bin/bench_manipulatevcard.php index 96a506907..790912023 100644 --- a/bin/bench_manipulatevcard.php +++ b/bin/bench_manipulatevcard.php @@ -3,7 +3,7 @@ include __DIR__.'/../vendor/autoload.php'; if ($argc < 2) { - echo 'sabre/vobject ', Sabre\VObject\Version::VERSION, " manipulation benchmark\n"; + echo 'sabre/vobject ', VObject\Version::VERSION, " manipulation benchmark\n"; echo "\n"; echo "This script can be used to measure the speed of opening a large amount of\n"; echo "vcards, making a few alterations and serializing them again.\n"; @@ -17,7 +17,7 @@ $input = file_get_contents($inputFile); -$splitter = new Sabre\VObject\Splitter\VCard($input); +$splitter = new VObject\Splitter\VCard($input); $bench = new Hoa\Bench\Bench(); diff --git a/bin/mergeduplicates.php b/bin/mergeduplicates.php index 5e48c3c34..cd5517266 100755 --- a/bin/mergeduplicates.php +++ b/bin/mergeduplicates.php @@ -96,53 +96,52 @@ function write($vcard) $collectedNames[$fn] = $vcard; ++$stats['Unique cards']; continue; - } else { - // Starting comparison for all properties. We only check if properties - // in the current vcard exactly appear in the earlier vcard as well. - foreach ($vcard->children() as $newProp) { - if (in_array($newProp->name, $ignoredProperties)) { - // We don't care about properties such as UID and REV. - continue; - } - $ok = false; - foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { - if ($compareProp->serialize() === $newProp->serialize()) { - $ok = true; - break; - } + } + // Starting comparison for all properties. We only check if properties + // in the current vcard exactly appear in the earlier vcard as well. + foreach ($vcard->children() as $newProp) { + if (in_array($newProp->name, $ignoredProperties)) { + // We don't care about properties such as UID and REV. + continue; + } + $ok = false; + foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { + if ($compareProp->serialize() === $newProp->serialize()) { + $ok = true; + break; } + } - if (!$ok) { - if ('EMAIL' === $newProp->name || 'TEL' === $newProp->name) { - // We're going to make another attempt to find this - // property, this time just by value. If we find it, we - // consider it a success. - foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { - if ($compareProp->getValue() === $newProp->getValue()) { - $ok = true; - break; - } - } - - if (!$ok) { - // Merging the new value in the old vcard. - $collectedNames[$fn]->add(clone $newProp); + if (!$ok) { + if ('EMAIL' === $newProp->name || 'TEL' === $newProp->name) { + // We're going to make another attempt to find this + // property, this time just by value. If we find it, we + // consider it a success. + foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { + if ($compareProp->getValue() === $newProp->getValue()) { $ok = true; - ++$stats['Merged values']; + break; } } - } - if (!$ok) { - // echo $newProp->serialize() . " does not appear in earlier vcard!\n"; - ++$stats['Error']; - if ($debug) { - fwrite($debug, "Missing '".$newProp->name."' property in duplicate. Earlier vcard:\n".$collectedNames[$fn]->serialize()."\n\nLater:\n".$vcard->serialize()."\n\n"); + if (!$ok) { + // Merging the new value in the old vcard. + $collectedNames[$fn]->add(clone $newProp); + $ok = true; + ++$stats['Merged values']; } + } + } - $vcard->destroy(); - continue 2; + if (!$ok) { + // echo $newProp->serialize() . " does not appear in earlier vcard!\n"; + ++$stats['Error']; + if ($debug) { + fwrite($debug, "Missing '".$newProp->name."' property in duplicate. Earlier vcard:\n".$collectedNames[$fn]->serialize()."\n\nLater:\n".$vcard->serialize()."\n\n"); } + + $vcard->destroy(); + continue 2; } } diff --git a/bin/rrulebench.php b/bin/rrulebench.php index c8be08d46..bb31053b8 100644 --- a/bin/rrulebench.php +++ b/bin/rrulebench.php @@ -3,7 +3,7 @@ include __DIR__.'/../vendor/autoload.php'; if ($argc < 4) { - echo 'sabre/vobject ', Sabre\VObject\Version::VERSION, " RRULE benchmark\n"; + echo 'sabre/vobject ', VObject\Version::VERSION, " RRULE benchmark\n"; echo "\n"; echo "This script can be used to measure the speed of the 'recurrence expansion'\n"; echo 'system.'; @@ -18,7 +18,7 @@ $bench->parse->start(); echo "Parsing.\n"; -$vobj = Sabre\VObject\Reader::read(fopen($inputFile, 'r')); +$vobj = VObject\Reader::read(fopen($inputFile, 'r')); $bench->parse->stop(); diff --git a/composer.json b/composer.json index 65765bca3..f602f5200 100644 --- a/composer.json +++ b/composer.json @@ -38,10 +38,10 @@ "sabre/xml" : "^3.0 || ^4.0" }, "require-dev" : { - "friendsofphp/php-cs-fixer": "^3.54", + "friendsofphp/php-cs-fixer": "^3.94", "phpunit/phpunit" : "^9.6", "phpunit/php-invoker" : "^2.0 || ^3.1", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^2.1" }, "suggest" : { "hoa/bench" : "If you would like to run the benchmark scripts" @@ -94,7 +94,7 @@ "phpstan analyse lib tests --memory-limit 1G" ], "cs-fixer": [ - "php-cs-fixer fix" + "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix" ], "phpunit": [ "phpunit --configuration tests/phpunit.xml" diff --git a/lib/Component.php b/lib/Component.php index 2bcf21917..2c933ffda 100644 --- a/lib/Component.php +++ b/lib/Component.php @@ -222,16 +222,16 @@ public function select(string $name): array if (is_null($group)) { return $result; - } else { - // If we have a group filter as well, we need to narrow it down - // more. - return array_filter( - $result, - function ($child) use ($group) { - return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group; - } - ); } + + // If we have a group filter as well, we need to narrow it down + // more. + return array_filter( + $result, + function ($child) use ($group) { + return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group; + } + ); } // If we got to this point, it means there was no 'name' specified for @@ -422,13 +422,12 @@ public function __get(string $name): ?Node $matches = $this->select($name); if (0 === count($matches)) { return null; - } else { - $firstMatch = current($matches); - /* @var $firstMatch Property */ - $firstMatch->setIterator(new ElementList(array_values($matches))); - - return $firstMatch; } + $firstMatch = current($matches); + /* @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + + return $firstMatch; } /** diff --git a/lib/Component/VAlarm.php b/lib/Component/VAlarm.php index 3e5797b0a..0cb6ab1de 100644 --- a/lib/Component/VAlarm.php +++ b/lib/Component/VAlarm.php @@ -39,13 +39,7 @@ public function getEffectiveTriggerTime(): \DateTimeImmutable /** @var VEvent|VTodo $parentComponent */ $parentComponent = $this->parent; if ('START' === $related) { - if ('VTODO' === $parentComponent->name) { - $propName = 'DUE'; - } else { - $propName = 'DTSTART'; - } - - $effectiveTrigger = $parentComponent->$propName->getDateTime(); + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); } else { if ('VTODO' === $parentComponent->name) { $endProp = 'DUE'; @@ -102,9 +96,9 @@ public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end } return false; - } else { - return $start <= $effectiveTrigger && $end > $effectiveTrigger; } + + return $start <= $effectiveTrigger && $end > $effectiveTrigger; } /** diff --git a/lib/Component/VCard.php b/lib/Component/VCard.php index 6bbec5717..0dafb674b 100644 --- a/lib/Component/VCard.php +++ b/lib/Component/VCard.php @@ -409,6 +409,34 @@ public function getByType(string $propertyName, string $type) return null; } + /** + * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL). + * + * This function will return null if the exact property list does not exist. + * + * For example to get the property of `TEL;TYPE=HOME,CELL` + * you would call `getByTypes('TEL', ['HOME', 'CELL'])` + * + * @param string[] $types + * + * @return \ArrayAccess|array|null + */ + public function getByTypes(string $propertyName, array $types) + { + $types = array_map('strtolower', $types); + foreach ($this->select($propertyName) as $field) { + if (isset($field['TYPE'])) { + $parts = array_map('strtolower', $field['TYPE']->getParts()); + + if (!array_diff($types, $parts) && !array_diff($parts, $types)) { + return $field; + } + } + } + + return null; + } + /** * This method should return a list of default property values. */ diff --git a/lib/Component/VEvent.php b/lib/Component/VEvent.php index e29e56322..6ca196d66 100644 --- a/lib/Component/VEvent.php +++ b/lib/Component/VEvent.php @@ -41,7 +41,7 @@ class VEvent extends VObject\Component */ public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool { - if ($this->RRULE) { + if ($this->RRULE || $this->RDATE) { try { $it = new EventIterator($this, null, $start->getTimezone()); } catch (NoInstancesException $e) { diff --git a/lib/Component/VTodo.php b/lib/Component/VTodo.php index bda49da74..edf511ef6 100644 --- a/lib/Component/VTodo.php +++ b/lib/Component/VTodo.php @@ -54,9 +54,9 @@ public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end return ($start < $due || $start <= $dtstart) && ($end > $dtstart || $end >= $due); - } else { - return $start <= $dtstart && $end > $dtstart; } + + return $start <= $dtstart && $end > $dtstart; } if ($due) { return $start < $due && $end >= $due; @@ -104,7 +104,7 @@ public function getValidationRules(): array 'LAST-MODIFIED' => '?', 'LOCATION' => '?', 'ORGANIZER' => '?', - 'PERCENT' => '?', + 'PERCENT-COMPLETE' => '?', 'PRIORITY' => '?', 'RECURRENCE-ID' => '?', 'SEQUENCE' => '?', diff --git a/lib/DateTimeParser.php b/lib/DateTimeParser.php index a8130b8bc..712a40faf 100644 --- a/lib/DateTimeParser.php +++ b/lib/DateTimeParser.php @@ -205,9 +205,9 @@ public static function parse(string $date, $referenceTz = null) return self::parseDuration($date); } elseif (8 === strlen($date)) { return self::parseDate($date, $referenceTz); - } else { - return self::parseDateTime($date, $referenceTz); } + + return self::parseDateTime($date, $referenceTz); } /** diff --git a/lib/Document.php b/lib/Document.php index 370b02e0e..dd2faf5bf 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -119,9 +119,9 @@ public function create(string $name) { if (isset(static::$componentMap[strtoupper($name)])) { return call_user_func_array([$this, 'createComponent'], func_get_args()); - } else { - return call_user_func_array([$this, 'createProperty'], func_get_args()); } + + return call_user_func_array([$this, 'createProperty'], func_get_args()); } /** @@ -186,13 +186,17 @@ public function createProperty(string $name, $value = null, ?array $parameters = $class = $this->getClassNameForPropertyValue($valueType); } + // According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20 + // a VALUE parameter is only required when it overrides the default + // value type, so an unrecognised one falls back to the default class. + // Unlike upstream we also drop the malformed or illegal VALUE, so it is + // not serialized back out. if (is_null($class)) { - // If a VALUE parameter is supplied, we should use that. if (isset($parameters['VALUE'])) { if (is_string($parameters['VALUE'])) { $class = $this->getClassNameForPropertyValue($parameters['VALUE']); } - if (is_null($class)) { // VALUE is malformed or illegal, drop it + if (is_null($class)) { unset($parameters['VALUE']); $class = $this->getClassNameForPropertyName($name); } @@ -200,6 +204,7 @@ public function createProperty(string $name, $value = null, ?array $parameters = $class = $this->getClassNameForPropertyName($name); } } + if (is_null($parameters)) { $parameters = []; } diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php index be6ba29d7..9bcdcc2a2 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php @@ -246,16 +246,29 @@ public function parseEvent($calendar, $userHref, $oldCalendar = null): array $baseCalendar = $oldCalendar; } + // Check if the user is the organizer if (in_array($eventInfo['organizer'], $userHref)) { return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); - } elseif ($oldCalendar) { - // We need to figure out if the user is an attendee, but we're only - // doing so if there's an oldCalendar, because we only want to - // process updates, not creation of new events. - foreach ($eventInfo['attendees'] as $attendee) { - if (in_array($attendee['href'], $userHref)) { + } + + // Check if the user is an attendee + foreach ($eventInfo['attendees'] as $attendee) { + if (in_array($attendee['href'], $userHref)) { + // If this is a event update, we always generate a reply + if ($oldCalendar) { return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); } + + // If this is a new event, we only generate a reply if the participation status is set + foreach ($attendee['instances'] as $instance) { + if (isset($instance['partstat']) && 'NEEDS-ACTION' !== $instance['partstat']) { + // Attendee has responded (ACCEPTED/DECLINED/TENTATIVE) - generate REPLY + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + } + + // User is attendee but no response to process + break; } } @@ -479,7 +492,6 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, } $messages = []; - foreach ($attendees as $attendee) { // An organizer can also be an attendee. We should not generate any // messages for those. @@ -503,10 +515,11 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, $icalMsg->add(clone $timezone); } - if (!$attendee['newInstances']) { - // If there are no instances the attendee is a part of, it - // means the attendee was removed and we need to send him a - // CANCEL. + if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) { + // If there are no instances the attendee is a part of, it means + // the attendee was removed and we need to send them a CANCEL message. + // Also If the meeting STATUS property was changed to CANCELLED + // we need to send the attendee a CANCEL message. $message->method = 'CANCEL'; $icalMsg->METHOD = $message->method; @@ -579,6 +592,9 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, )); } else { $currentEvent->EXDATE = $exceptions; + if ($currentEvent->DTSTART['TZID']) { + $currentEvent->EXDATE['TZID'] = clone $currentEvent->DTSTART['TZID']; + } } } @@ -587,14 +603,14 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); - foreach ($currentEvent->ATTENDEE as $attendee) { - unset($attendee['SCHEDULE-FORCE-SEND']); - unset($attendee['SCHEDULE-STATUS']); + foreach ($currentEvent->ATTENDEE as $currentEventAttendee) { + unset($currentEventAttendee['SCHEDULE-FORCE-SEND']); + unset($currentEventAttendee['SCHEDULE-STATUS']); // We're adding PARTSTAT=NEEDS-ACTION to ensure that // iOS shows an "Inbox Item" - if (!isset($attendee['PARTSTAT'])) { - $attendee['PARTSTAT'] = 'NEEDS-ACTION'; + if (!isset($currentEventAttendee['PARTSTAT'])) { + $currentEventAttendee['PARTSTAT'] = 'NEEDS-ACTION'; } } } @@ -663,7 +679,7 @@ protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, // We only need to do that though, if the master event is not declined. if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { foreach ($eventInfo['exdate'] as $exDate) { - if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (!in_array($exDate, $oldEventInfo['exdate'] ?? [])) { if (isset($instances[$exDate])) { $instances[$exDate]['newstatus'] = 'DECLINED'; } else { @@ -773,9 +789,9 @@ protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, $message->message = $icalMsg; return [$message]; - } else { - return []; } + + return []; } /** @@ -891,6 +907,9 @@ protected function parseEventInfo(VCalendar $calendar): array $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); } } + + $instances[$recurId] = $vevent; + if (isset($vevent->ATTENDEE)) { foreach ($vevent->ATTENDEE as $attendee) { if ($this->scheduleAgentServerRules @@ -929,7 +948,6 @@ protected function parseEventInfo(VCalendar $calendar): array ]; } } - $instances[$recurId] = $vevent; } foreach ($this->significantChangeProperties as $prop) { diff --git a/lib/ITip/Message.php b/lib/ITip/Message.php index 9aa93996a..6f59eebc2 100644 --- a/lib/ITip/Message.php +++ b/lib/ITip/Message.php @@ -107,10 +107,9 @@ public function getScheduleStatus() { if (!$this->scheduleStatus) { return false; - } else { - list($scheduleStatus) = explode(';', $this->scheduleStatus); - - return $scheduleStatus; } + list($scheduleStatus) = explode(';', $this->scheduleStatus); + + return $scheduleStatus; } } diff --git a/lib/Parameter.php b/lib/Parameter.php index 1ef7da88a..f5eb58610 100644 --- a/lib/Parameter.php +++ b/lib/Parameter.php @@ -178,9 +178,9 @@ public function getValue(): ?string { if (is_array($this->value)) { return implode(',', $this->value); - } else { - return $this->value; } + + return $this->value; } /** @@ -202,9 +202,9 @@ public function getParts(): array return $this->value; } elseif (is_null($this->value)) { return []; - } else { - return [$this->value]; } + + return [$this->value]; } /** @@ -279,20 +279,19 @@ function ($out, $item) { // added + as well. if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { return $out.$item; - } else { - // Enclosing in double-quotes, and using RFC6868 for encoding any - // special characters - $out .= '"'.strtr( - $item, - [ - '^' => '^^', - "\n" => '^n', - '"' => '^\'', - ] - ).'"'; - - return $out; } + // Enclosing in double-quotes, and using RFC6868 for encoding any + // special characters + $out .= '"'.strtr( + $item, + [ + '^' => '^^', + "\n" => '^n', + '"' => '^\'', + ] + ).'"'; + + return $out; } ); } diff --git a/lib/Parser/MimeDir.php b/lib/Parser/MimeDir.php index db5ded667..f299978cd 100644 --- a/lib/Parser/MimeDir.php +++ b/lib/Parser/MimeDir.php @@ -27,6 +27,11 @@ */ class MimeDir extends Parser { + public const TOKEN_PROPNAME = 1; + public const TOKEN_PROPVALUE = 2; + public const TOKEN_PARAMNAME = 3; + public const TOKEN_PARAMVALUE = 4; + /** * The input stream. * @@ -248,16 +253,15 @@ protected function parseLine(string $line) } return $component; - } else { - // Property reader - $property = $this->readProperty($line); - if (!$property) { - // Ignored line - return false; - } - - return $property; } + // Property reader + $property = $this->readProperty($line); + if (!$property) { + // Ignored line + return false; + } + + return $property; } /** @@ -378,6 +382,12 @@ protected function readProperty(string $line) 'value' => null, ]; + /* + * Keep track on the last token we parsed in order to do + * better error checking + */ + $lastToken = null; + $lastParam = null; /* @@ -403,10 +413,16 @@ protected function readProperty(string $line) // option is set to ignore invalid lines, we ignore this line // This can happen when servers provide faulty data as iCloud // frequently does with X-APPLE-STRUCTURED-LOCATION + $lastToken = self::TOKEN_PARAMVALUE; continue; } throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); } + + if ('=' == $match[0][0] && self::TOKEN_PARAMNAME != $lastToken) { + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"'); + } + if (\is_null($property['parameters'][$lastParam])) { $property['parameters'][$lastParam] = $value; } elseif (is_array($property['parameters'][$lastParam])) { @@ -420,6 +436,7 @@ protected function readProperty(string $line) $value, ]; } + $lastToken = self::TOKEN_PARAMVALUE; continue; } if (isset($match['paramName'])) { @@ -427,14 +444,17 @@ protected function readProperty(string $line) if (!isset($property['parameters'][$lastParam])) { $property['parameters'][$lastParam] = null; } + $lastToken = self::TOKEN_PARAMNAME; continue; } if (isset($match['propValue'])) { $property['value'] = $match['propValue']; + $lastToken = self::TOKEN_PROPVALUE; continue; } - if (isset($match['name']) && $match['name']) { + if (isset($match['name']) && 0 < strlen($match['name'])) { $property['name'] = strtoupper($match['name']); + $lastToken = self::TOKEN_PROPNAME; continue; } @@ -446,7 +466,7 @@ protected function readProperty(string $line) if (\is_null($property['value'])) { $property['value'] = ''; } - if (!$property['name']) { + if (!isset($property['name']) || 0 == strlen($property['name'])) { if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { return false; } diff --git a/lib/Property.php b/lib/Property.php index f4d0bdc0b..56b571e51 100644 --- a/lib/Property.php +++ b/lib/Property.php @@ -155,9 +155,9 @@ public function getParts(): array return []; } elseif (is_array($this->value)) { return $this->value; - } else { - return [$this->value]; } + + return [$this->value]; } /** diff --git a/lib/Property/ICalendar/DateTime.php b/lib/Property/ICalendar/DateTime.php index b506bc6be..e8a60935a 100644 --- a/lib/Property/ICalendar/DateTime.php +++ b/lib/Property/ICalendar/DateTime.php @@ -262,9 +262,9 @@ public function getJsonValue(): array function (\DateTimeInterface $dt) use ($hasTime, $isUtc) { if ($hasTime) { return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); - } else { - return $dt->format('Y-m-d'); } + + return $dt->format('Y-m-d'); }, $dts ); diff --git a/lib/Property/Text.php b/lib/Property/Text.php index c59029059..363c3fa4f 100644 --- a/lib/Property/Text.php +++ b/lib/Property/Text.php @@ -241,22 +241,21 @@ public function serialize(): string } return $out; - } else { - $str .= ':'.$val; + } + $str .= ':'.$val; - $str = \preg_replace( - '/( + $str = \preg_replace( + '/( (?:^.)? # 1 additional byte in first line because of missing single space (see next line) .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) (?![\x80-\xbf]) # prevent splitting multibyte characters )/x', - "$1\r\n ", - $str - ); + "$1\r\n ", + $str + ); - // remove single space after last CRLF - return \substr($str, 0, -1); - } + // remove single space after last CRLF + return \substr($str, 0, -1); } /** diff --git a/lib/Recur/EventIterator.php b/lib/Recur/EventIterator.php index 2c93bcf4e..d0317e14b 100644 --- a/lib/Recur/EventIterator.php +++ b/lib/Recur/EventIterator.php @@ -164,8 +164,12 @@ public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZon } if (isset($this->masterEvent->RDATE)) { + $rdateValues = []; + foreach ($this->masterEvent->RDATE as $rdate) { + $rdateValues = array_merge($rdateValues, $rdate->getParts()); + } $this->recurIterator = new RDateIterator( - $this->masterEvent->RDATE->getParts(), + $rdateValues, $this->startDate ); } elseif (isset($this->masterEvent->RRULE)) { @@ -228,11 +232,10 @@ public function getDtEnd(): ?\DateTimeImmutable } if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); - } else { - $end = clone $this->currentDate; - - return $end->modify('+'.$this->eventDuration.' seconds'); } + $end = clone $this->currentDate; + + return $end->modify('+'.$this->eventDuration.' seconds'); } /** diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index e2d1bda5c..fe14f7efb 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -908,25 +908,22 @@ protected function nextYearly($amount = 1): void (int) $occurrence[0] )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); - return; - } else { - // These are the 'byMonth' rules, if there are no byDay or - // byMonthDay sub-rules. - do { - ++$currentMonth; - if ($currentMonth > 12) { - $currentYear += $this->interval; - $currentMonth = 1; - } - } while (!in_array($currentMonth, $this->byMonth)); - $this->currentDate = $this->currentDate->setDate( - (int) $currentYear, - (int) $currentMonth, - (int) $currentDayOfMonth - )->modify($this->startTime()); - return; } + // These are the 'byMonth' rules, if there are no byDay or + // byMonthDay sub-rules. + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + )->modify($this->startTime()); } /* }}} */ diff --git a/lib/Splitter/ICalendar.php b/lib/Splitter/ICalendar.php index 79f0bd443..edbba68cc 100644 --- a/lib/Splitter/ICalendar.php +++ b/lib/Splitter/ICalendar.php @@ -96,8 +96,8 @@ public function getNext(): ?Component } return $object; - } else { - return null; } + + return null; } } diff --git a/lib/TimezoneGuesser/FindFromTimezoneMap.php b/lib/TimezoneGuesser/FindFromTimezoneMap.php index 1fe680230..6df87de8d 100644 --- a/lib/TimezoneGuesser/FindFromTimezoneMap.php +++ b/lib/TimezoneGuesser/FindFromTimezoneMap.php @@ -16,13 +16,20 @@ class FindFromTimezoneMap implements TimezoneFinder '/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/', ]; + /** + * @throws void + */ public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone { $tzid = str_replace('.', '', $tzid); // Next, we check if the tzid is somewhere in our tzid map. if ($this->hasTzInMap($tzid)) { - return new \DateTimeZone($this->getTzFromMap($tzid)); + try { + return new \DateTimeZone($this->getTzFromMap($tzid)); + } catch (\Exception $e) { + return null; + } } // Some Microsoft products prefix the offset first, so let's strip that off @@ -34,7 +41,11 @@ public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZo } $tzidAlternate = $matches[3]; if ($this->hasTzInMap($tzidAlternate)) { - return new \DateTimeZone($this->getTzFromMap($tzidAlternate)); + try { + return new \DateTimeZone($this->getTzFromMap($tzidAlternate)); + } catch (\Exception $e) { + return null; + } } } diff --git a/lib/timezonedata/exchangezones.php b/lib/timezonedata/exchangezones.php index c186e492a..8651c45c6 100644 --- a/lib/timezonedata/exchangezones.php +++ b/lib/timezonedata/exchangezones.php @@ -18,6 +18,7 @@ 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', 'Amsterdam, Belgrade, Berlin, Brussels, Budapest, Madrid, Paris, Prague, Stockholm' => 'Europe/Berlin', 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Amsterdam, Berlin, Bern, Rom, Stockholm, Wien' => 'Europe/Berlin', 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', @@ -40,12 +41,12 @@ 'Kabul' => 'Asia/Kabul', 'Ekaterinburg' => 'Asia/Yekaterinburg', 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', - 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata', 'Kathmandu, Nepal' => 'Asia/Kathmandu', 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', 'Astana, Dhaka' => 'Asia/Dhaka', 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', - 'Rangoon' => 'Asia/Rangoon', + 'Rangoon' => 'Asia/Yangon', 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', 'Krasnoyarsk' => 'Asia/Krasnoyarsk', 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', diff --git a/lib/timezonedata/lotuszones.php b/lib/timezonedata/lotuszones.php index 684a0c972..881d4f5c0 100644 --- a/lib/timezonedata/lotuszones.php +++ b/lib/timezonedata/lotuszones.php @@ -71,12 +71,12 @@ 'Ekaterinburg' => 'Asia/Yekaterinburg', 'Pakistan' => 'Asia/Karachi', 'West Asia' => 'Asia/Tashkent', - 'India' => 'Asia/Calcutta', + 'India' => 'Asia/Kolkata', 'Sri Lanka' => 'Asia/Colombo', 'Nepal' => 'Asia/Kathmandu', 'Central Asia' => 'Asia/Dhaka', 'N. Central Asia' => 'Asia/Almaty', - 'Myanmar' => 'Asia/Rangoon', + 'Myanmar' => 'Asia/Yangon', 'North Asia' => 'Asia/Krasnoyarsk', 'SE Asia' => 'Asia/Bangkok', 'China' => 'Asia/Shanghai', diff --git a/lib/timezonedata/windowszones.php b/lib/timezonedata/windowszones.php index 209a9141b..81d6b7499 100644 --- a/lib/timezonedata/windowszones.php +++ b/lib/timezonedata/windowszones.php @@ -20,7 +20,7 @@ 'Arab Standard Time' => 'Asia/Riyadh', 'Arabian Standard Time' => 'Asia/Dubai', 'Arabic Standard Time' => 'Asia/Baghdad', - 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires', 'Astrakhan Standard Time' => 'Europe/Astrakhan', 'Atlantic Standard Time' => 'America/Halifax', 'Aus Central W. Standard Time' => 'Australia/Eucla', @@ -64,7 +64,7 @@ 'Greenwich Standard Time' => 'Atlantic/Reykjavik', 'Haiti Standard Time' => 'America/Port-au-Prince', 'Hawaiian Standard Time' => 'Pacific/Honolulu', - 'India Standard Time' => 'Asia/Calcutta', + 'India Standard Time' => 'Asia/Kolkata', 'Iran Standard Time' => 'Asia/Tehran', 'Israel Standard Time' => 'Asia/Jerusalem', 'Jordan Standard Time' => 'Asia/Amman', @@ -82,10 +82,10 @@ 'Morocco Standard Time' => 'Africa/Casablanca', 'Mountain Standard Time' => 'America/Denver', 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', - 'Myanmar Standard Time' => 'Asia/Rangoon', + 'Myanmar Standard Time' => 'Asia/Yangon', 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', 'Namibia Standard Time' => 'Africa/Windhoek', - 'Nepal Standard Time' => 'Asia/Katmandu', + 'Nepal Standard Time' => 'Asia/Kathmandu', 'New Zealand Standard Time' => 'Pacific/Auckland', 'Newfoundland Standard Time' => 'America/St_Johns', 'Norfolk Standard Time' => 'Pacific/Norfolk', @@ -127,7 +127,7 @@ '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 Eastern Standard Time' => 'America/Indiana/Indianapolis', 'US Mountain Standard Time' => 'America/Phoenix', 'UTC' => 'Etc/GMT', 'UTC+12' => 'Etc/GMT-12', diff --git a/tests/VObject/Component/VAlarmTest.php b/tests/VObject/Component/VAlarmTest.php index 4694d6363..a67991ff4 100644 --- a/tests/VObject/Component/VAlarmTest.php +++ b/tests/VObject/Component/VAlarmTest.php @@ -123,6 +123,19 @@ public function timeRangeTestData(): array $tests[] = [$valarm7, new \DateTime('2012-02-25 01:00:00'), new \DateTime('2012-03-05 01:00:00'), true]; $tests[] = [$valarm7, new \DateTime('2012-03-25 01:00:00'), new \DateTime('2012-04-05 01:00:00'), false]; + // Relation to start time of todo + $valarm8 = $calendar->createComponent('VALARM'); + $valarm8->TRIGGER = '-P1D'; + $valarm8->TRIGGER['VALUE'] = 'DURATION'; + + $vtodo8 = $calendar->createComponent('VTODO'); + $vtodo8->DTSTART = '20120301T130000Z'; + $vtodo8->DUE = '20120401T130000Z'; + $vtodo8->add($valarm8); + + $tests[] = [$valarm8, new \DateTime('2012-02-25 01:00:00'), new \DateTime('2012-03-05 01:00:00'), true]; + $tests[] = [$valarm8, new \DateTime('2012-03-25 01:00:00'), new \DateTime('2012-04-05 01:00:00'), false]; + return $tests; } @@ -151,7 +164,7 @@ public function testInTimeRangeBuggy(): void DTSTAMP:20121003T064931Z UID:b848cb9a7bb16e464a06c222ca1f8102@examle.com STATUS:NEEDS-ACTION -DUE:20121005T000000Z +DTSTART:20121005T000000Z SUMMARY:Task 1 CATEGORIES:AlarmCategory BEGIN:VALARM diff --git a/tests/VObject/Component/VAvailabilityTest.php b/tests/VObject/Component/VAvailabilityTest.php index 263eeebda..67499e98e 100644 --- a/tests/VObject/Component/VAvailabilityTest.php +++ b/tests/VObject/Component/VAvailabilityTest.php @@ -435,8 +435,7 @@ protected function template(array $properties) … END:VAVAILABILITY END:VCALENDAR -VCAL - , +VCAL, $properties ); } @@ -459,8 +458,7 @@ protected function templateAvailable(array $properties) END:AVAILABLE END:VAVAILABILITY END:VCALENDAR -VCAL - , +VCAL, $properties ); } diff --git a/tests/VObject/Component/VCardTest.php b/tests/VObject/Component/VCardTest.php index 3851d97d9..ff4979ed4 100644 --- a/tests/VObject/Component/VCardTest.php +++ b/tests/VObject/Component/VCardTest.php @@ -147,6 +147,27 @@ public function testGetByType(): void self::assertNull($vcard->getByType('ADR', 'non-existent')); } + public function testGetByTypes(): void + { + $vcard = <<getByTypes('TEL', ['home', 'cell'])->getValue()); + self::assertEquals('665544332211', $vcard->getByTypes('TEL', ['work', 'cell'])->getValue()); + self::assertEquals('7778889994455', $vcard->getByTypes('TEL', ['work'])->getValue()); + self::assertEquals('555555555', $vcard->getByTypes('TEL', ['external'])->getValue()); + self::assertNull($vcard->getByTypes('TEL', ['non-existent'])); + self::assertNull($vcard->getByTypes('EMAIL', ['non-existent'])); + } + public function testPreferredNoPref(): void { $vcard = <<RRULE = 'FREQ=DAILY'; $tests[] = [$vevent9, new \DateTime('2016-10-31'), new \DateTime('2016-12-12'), true]; + // Added this test to check events with RDATE property with multiple dates + $vevent10 = clone $vevent; + $vevent10->DTSTART = '20140901T000000Z'; + $vevent10->DTEND = '20140901T010000Z'; + $vevent10->add('RDATE', ['20141001T000000Z', '20141101T000000Z']); + // DTSTART is the first occurrence + $tests[] = [$vevent10, new \DateTime('2014-09-01'), new \DateTime('2014-09-02'), true]; + // RDATE adds additional occurrences on Oct 1 and Nov 1 + $tests[] = [$vevent10, new \DateTime('2014-10-01'), new \DateTime('2014-10-02'), true]; + $tests[] = [$vevent10, new \DateTime('2014-11-01'), new \DateTime('2014-11-02'), true]; + // No occurrence in December + $tests[] = [$vevent10, new \DateTime('2014-12-01'), new \DateTime('2014-12-31'), false]; + // Range that includes first occurrence + $tests[] = [$vevent10, new \DateTime('2014-08-01'), new \DateTime('2014-09-30'), true]; + // Range that spans all occurrences + $tests[] = [$vevent10, new \DateTime('2014-08-01'), new \DateTime('2014-12-31'), true]; + + // Added this test to check events with RDATE property with multiple instances + $vevent11 = clone $vevent; + $vevent11->DTSTART = '20140901T000000Z'; + $vevent11->DTEND = '20140901T010000Z'; + $vevent11->add('RDATE', '20141001T000000Z'); + $vevent11->add('RDATE', '20141101T000000Z'); + // DTSTART is the first occurrence + $tests[] = [$vevent11, new \DateTime('2014-09-01'), new \DateTime('2014-09-02'), true]; + // RDATE adds additional occurrences on Oct 1 and Nov 1 + $tests[] = [$vevent11, new \DateTime('2014-10-01'), new \DateTime('2014-10-02'), true]; + $tests[] = [$vevent11, new \DateTime('2014-11-01'), new \DateTime('2014-11-02'), true]; + // No occurrence in December + $tests[] = [$vevent11, new \DateTime('2014-12-01'), new \DateTime('2014-12-31'), false]; + // Range that includes first occurrence + $tests[] = [$vevent11, new \DateTime('2014-08-01'), new \DateTime('2014-09-30'), true]; + // Range that spans all occurrences + $tests[] = [$vevent11, new \DateTime('2014-08-01'), new \DateTime('2014-12-31'), true]; + return $tests; } } diff --git a/tests/VObject/Component/VTodoTest.php b/tests/VObject/Component/VTodoTest.php index 808675b36..899d93982 100644 --- a/tests/VObject/Component/VTodoTest.php +++ b/tests/VObject/Component/VTodoTest.php @@ -88,6 +88,31 @@ public function testValidate(): void self::assertEquals([], $messages); } + public function testValidateExtraProperty(): void + { + $input = <<validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + self::assertEquals([], $messages); + } + public function testValidateInvalid(): void { $input = <<validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + self::assertEquals([ + 'PERCENT-COMPLETE MUST NOT appear more than once in a VTODO component', + ], $messages); + } } diff --git a/tests/VObject/ITip/BrokerAttendeeReplyTest.php b/tests/VObject/ITip/BrokerAttendeeReplyTest.php index 75d2cf06f..c33a8007d 100644 --- a/tests/VObject/ITip/BrokerAttendeeReplyTest.php +++ b/tests/VObject/ITip/BrokerAttendeeReplyTest.php @@ -1204,4 +1204,125 @@ public function testPartyCrasher(): void $this->parse($oldMessage, $newMessage, $expected); } + + public function testNewEventWithReply(): void + { + $oldMessage = null; + + $newMessage = << 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<parse($oldMessage, $newMessage, $expected); + } + + /** + * Test that adding EXDATE entries when oldEventInfo doesn't have exdate + * (e.g., when updating a newly created event) doesn't cause a TypeError. + * + * This tests the scenario where an attendee receives a new recurring event + * invitation with EXDATE entries already present. Since there's no old calendar, + * oldEventInfo is initialized with only minimal keys and lacks 'exdate'. + */ + public function testAddExdateWithoutPreviousExdate(): void + { + // No old message - this is a new event invitation + $oldMessage = null; + + // New message: recurring event with EXDATE already present + $newMessage = << 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => null, + 'recipient' => 'mailto:organizer@example.org', + 'recipientName' => null, + 'message' => <<parse($oldMessage, $newMessage, $expected); + } } diff --git a/tests/VObject/ITip/BrokerDeleteEventTest.php b/tests/VObject/ITip/BrokerDeleteEventTest.php index 19f393954..2572ed284 100644 --- a/tests/VObject/ITip/BrokerDeleteEventTest.php +++ b/tests/VObject/ITip/BrokerDeleteEventTest.php @@ -2,6 +2,8 @@ namespace Sabre\VObject\ITip; +use Sabre\VObject\Version; + class BrokerDeleteEventTest extends BrokerTester { public function testOrganizerDeleteWithDtend(): void @@ -24,7 +26,7 @@ public function testOrganizerDeleteWithDtend(): void $newMessage = null; - $version = \Sabre\VObject\Version::VERSION; + $version = Version::VERSION; $expected = [ [ @@ -107,7 +109,7 @@ public function testOrganizerDeleteWithDuration(): void $newMessage = null; - $version = \Sabre\VObject\Version::VERSION; + $version = Version::VERSION; $expected = [ [ @@ -190,7 +192,7 @@ public function testAttendeeDeleteWithDtend(): void $newMessage = null; - $version = \Sabre\VObject\Version::VERSION; + $version = Version::VERSION; $expected = [ [ @@ -245,7 +247,7 @@ public function testAttendeeReplyWithDuration(): void $newMessage = null; - $version = \Sabre\VObject\Version::VERSION; + $version = Version::VERSION; $expected = [ [ diff --git a/tests/VObject/ITip/BrokerNewEventTest.php b/tests/VObject/ITip/BrokerNewEventTest.php index b0b27f41c..db8441027 100644 --- a/tests/VObject/ITip/BrokerNewEventTest.php +++ b/tests/VObject/ITip/BrokerNewEventTest.php @@ -574,4 +574,60 @@ public function testNoOrganizerHasAttendee(): void $this->parse(null, $message, [], 'mailto:strunk@example.org'); } + + public function testAttendeeRemoval(): void + { + $message = << 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:white@example.org', + 'recipientName' => 'White', + 'message' => $expectedMessage, + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + } } diff --git a/tests/VObject/ITip/BrokerUpdateEventTest.php b/tests/VObject/ITip/BrokerUpdateEventTest.php index 738c769e8..27368dd57 100644 --- a/tests/VObject/ITip/BrokerUpdateEventTest.php +++ b/tests/VObject/ITip/BrokerUpdateEventTest.php @@ -814,4 +814,246 @@ public function testInviteChangeExdateOrder(): void $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); } + + public function testInviteStatusCancelled(): void + { + $oldMessage = << 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'message' => <<parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + /* + * When EXDATE is added by Broker, it needs to be in the correct + * timezone + */ + + public function testExdateTimezone(): void + { + $oldMessage = << 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => true, + 'message' => <<parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + /* + * When EXDATE is added by Broker, it needs to be in the correct + * timezone, also in case UTC is used + */ + + public function testExdateTimezoneUTC(): void + { + $oldMessage = << 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => true, + 'message' => <<parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } } diff --git a/tests/VObject/Issue153Test.php b/tests/VObject/Issue153Test.php index 83e56cf50..9b9d7ec0e 100644 --- a/tests/VObject/Issue153Test.php +++ b/tests/VObject/Issue153Test.php @@ -8,7 +8,7 @@ class Issue153Test extends TestCase { public function testRead(): void { - $obj = Reader::read(file_get_contents(dirname(__FILE__).'/issue153.vcf')); + $obj = Reader::read(file_get_contents(__DIR__.'/issue153.vcf')); self::assertEquals('Test Benutzer', (string) $obj->FN); } } diff --git a/tests/VObject/Issue64Test.php b/tests/VObject/Issue64Test.php index 110f91fa2..f9e7dbf96 100644 --- a/tests/VObject/Issue64Test.php +++ b/tests/VObject/Issue64Test.php @@ -8,7 +8,7 @@ class Issue64Test extends TestCase { public function testRead(): void { - $vcard = Reader::read(file_get_contents(dirname(__FILE__).'/issue64.vcf')); + $vcard = Reader::read(file_get_contents(__DIR__.'/issue64.vcf')); $vcard = $vcard->convert(Document::VCARD30); $vcard = $vcard->serialize(); diff --git a/tests/VObject/Parser/MimeDirTest.php b/tests/VObject/Parser/MimeDirTest.php index c91b13e6e..f4e2ed11c 100644 --- a/tests/VObject/Parser/MimeDirTest.php +++ b/tests/VObject/Parser/MimeDirTest.php @@ -259,4 +259,38 @@ public function provideBrokenVCalendar(): array EOF, ]]; } + + public function testPropertyName0(): void + { + $iCal = <<parse($iCal); + self::assertEquals('test', $vevent->VEVENT->{0}->getValue()); + } + + public function testInvalidParameter(): void + { + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Invalid Mimedir file. Line starting at 3: Missing parameter name for parameter value "value2"'); + $vcard = <<parse($vcard); + + echo $vcard->serialize(); + } } diff --git a/tests/VObject/Parser/XmlTest.php b/tests/VObject/Parser/XmlTest.php index 2276f7fdf..6491d087b 100644 --- a/tests/VObject/Parser/XmlTest.php +++ b/tests/VObject/Parser/XmlTest.php @@ -47,8 +47,7 @@ public function testRFC6321Example1(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". // VERSION comes first because this is required by vCard 4.0. 'VERSION:2.0'."\n". @@ -270,8 +269,7 @@ public function testRFC6321Section3Part2(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'END:VCALENDAR'."\n" ); @@ -299,8 +297,7 @@ public function testRFC6321Section3Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'BEGIN:VTIMEZONE'."\n". 'END:VTIMEZONE'."\n". @@ -340,8 +337,7 @@ public function testRFC6321Section3Part4Part1Part2(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'GEO:37.386013;-122.082932'."\n". 'END:VCALENDAR'."\n" @@ -367,8 +363,7 @@ public function testRFC6321Section3Part4Part1Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'REQUEST-STATUS:2.0;Success'."\n". 'END:VCALENDAR'."\n" @@ -389,8 +384,7 @@ public function testRFC6321Section3Part4Part1Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'REQUEST-STATUS:3.1;Invalid property value;DTSTART:96-Apr-01'."\n". 'END:VCALENDAR'."\n" @@ -411,8 +405,7 @@ public function testRFC6321Section3Part4Part1Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'REQUEST-STATUS:2.8;Success\, repeating event ignored. Scheduled as a single'."\n". ' event.;RRULE:FREQ=WEEKLY\;INTERVAL=2'."\n". @@ -433,8 +426,7 @@ public function testRFC6321Section3Part4Part1Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'REQUEST-STATUS:4.1;Event conflict. Date-time is busy.'."\n". 'END:VCALENDAR'."\n" @@ -455,8 +447,7 @@ public function testRFC6321Section3Part4Part1Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'REQUEST-STATUS:3.7;Invalid calendar user;ATTENDEE:mailto:jsmith@example.com'."\n". 'END:VCALENDAR'."\n" @@ -480,8 +471,7 @@ public function testRFC6321Section3Part6Part1(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'ATTACH:SGVsbG8gV29ybGQh'."\n". 'END:VCALENDAR'."\n" @@ -500,8 +490,7 @@ public function testRFC6321Section3Part6Part1(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'ATTACH:SGVsbG8gV29ybGQh'."\n". 'END:VCALENDAR'."\n" @@ -528,8 +517,7 @@ public function testRFC6321Section3Part6Part2(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'ATTENDEE;RSVP=true:mailto:cyrus@example.com'."\n". 'END:VCALENDAR'."\n" @@ -553,8 +541,7 @@ public function testRFC6321Section3Part6Part3(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'ATTENDEE:mailto:cyrus@example.com'."\n". 'END:VCALENDAR'."\n" @@ -578,8 +565,7 @@ public function testRFC6321Section3Part6Part4(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'DTSTART;VALUE=DATE:20110517'."\n". 'END:VCALENDAR'."\n" @@ -603,8 +589,7 @@ public function testRFC6321Section3Part6Part5(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'DTSTART:20110517T120000'."\n". 'END:VCALENDAR'."\n" @@ -628,8 +613,7 @@ public function testRFC6321Section3Part6Part6(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'DURATION:P1D'."\n". 'END:VCALENDAR'."\n" @@ -662,8 +646,7 @@ public function testRFC6321Section3Part6Part8(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'FOO:42'."\n". 'END:VCALENDAR'."\n" @@ -681,8 +664,7 @@ public function testRFC6321Section3Part6Part8(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'FOO:-42'."\n". 'END:VCALENDAR'."\n" @@ -709,8 +691,7 @@ public function testRFC6321Section3Part6Part9(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'FREEBUSY:20110517T120000/P1H'."\n". 'END:VCALENDAR'."\n" @@ -731,8 +712,7 @@ public function testRFC6321Section3Part6Part9(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'FREEBUSY:20110517T120000/20120517T120000'."\n". 'END:VCALENDAR'."\n" @@ -761,8 +741,7 @@ public function testRFC6321Section3Part6Part10(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RRULE:FREQ=YEARLY;COUNT=5;BYDAY=-1SU;BYMONTH=10'."\n". 'END:VCALENDAR'."\n" @@ -786,8 +765,7 @@ public function testRFC6321Section3Part6Part11(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'CALSCALE:GREGORIAN'."\n". 'END:VCALENDAR'."\n" @@ -811,8 +789,7 @@ public function testRFC6321Section3Part6Part12(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'FOO:120000'."\n". 'END:VCALENDAR'."\n" @@ -836,8 +813,7 @@ public function testRFC6321Section3Part6Part13(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'ATTACH:http://calendar.example.com'."\n". 'END:VCALENDAR'."\n" @@ -862,8 +838,7 @@ public function testRFC6321Section3Part6Part14(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'TZOFFSETFROM:-0500'."\n". 'END:VCALENDAR'."\n" @@ -882,8 +857,7 @@ public function testRFC6321Section3Part6Part14(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'TZOFFSETFROM:+0100'."\n". 'END:VCALENDAR'."\n" @@ -907,8 +881,7 @@ public function testRFC6321Section5(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'X-PROPERTY:20110512T120000Z'."\n". 'END:VCALENDAR'."\n" @@ -931,8 +904,7 @@ public function testRFC6321Section5(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'DTSTART;X-PARAM=PT30M:20110512T130000Z'."\n". 'END:VCALENDAR'."\n" @@ -953,8 +925,7 @@ public function testRDateWithDateTime(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RDATE:20080205T191224Z'."\n". 'END:VCALENDAR'."\n" @@ -973,8 +944,7 @@ public function testRDateWithDateTime(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RDATE:20080205T191224Z,20090205T191224Z'."\n". 'END:VCALENDAR'."\n" @@ -995,8 +965,7 @@ public function testRDateWithDate(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RDATE:20081006'."\n". 'END:VCALENDAR'."\n" @@ -1016,8 +985,7 @@ public function testRDateWithDate(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RDATE:20081006,20091006,20101006'."\n". 'END:VCALENDAR'."\n" @@ -1046,8 +1014,7 @@ public function testRDateWithPeriod(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RDATE;TZID=US/Eastern;VALUE=PERIOD:20060102T150000/PT2H'."\n". 'END:VCALENDAR'."\n" @@ -1077,8 +1044,7 @@ public function testRDateWithPeriod(): void -XML - , +XML, 'BEGIN:VCALENDAR'."\n". 'RDATE;TZID=US/Eastern;VALUE=PERIOD:20060102T150000/PT2H,20080102T150000/PT1'."\n". ' H'."\n". @@ -1108,8 +1074,7 @@ public function testRFC6351Basic(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'FN:J. Doe'."\n". @@ -1149,8 +1114,7 @@ public function testRFC6351Example1(): void My web page! -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'FN:J. Doe'."\n". @@ -1183,8 +1147,7 @@ public function testRFC6351Section5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TEL;TYPE="voice,video":tel:+1-555-555-555'."\n". @@ -1207,8 +1170,7 @@ public function testRFC6351Section5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TEL;TYPE="voice,video":tel:+1-555-555-555'."\n". @@ -1244,8 +1206,7 @@ public function testRFC6351Section5Group(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TEL:tel:+1-555-555-556'."\n". @@ -1276,8 +1237,7 @@ public function testRFC6351Section5Part1NoNamespace(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'X-MY-PROP;PREF=1:value goes here'."\n". @@ -1300,8 +1260,7 @@ public function testRFC6351ValueDateWithYearMonthDay(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:20150128'."\n". @@ -1324,8 +1283,7 @@ public function testRFC6351ValueDateWithYearMonth(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:2015-01'."\n". @@ -1348,8 +1306,7 @@ public function testRFC6351ValueDateWithMonth(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:--01'."\n". @@ -1372,8 +1329,7 @@ public function testRFC6351ValueDateWithMonthDay(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:--0128'."\n". @@ -1396,8 +1352,7 @@ public function testRFC6351ValueDateWithDay(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:---28'."\n". @@ -1420,8 +1375,7 @@ public function testRFC6351ValueTimeWithHour(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:13'."\n". @@ -1444,8 +1398,7 @@ public function testRFC6351ValueTimeWithHourMinute(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:1353'."\n". @@ -1468,8 +1421,7 @@ public function testRFC6351ValueTimeWithHourMinuteSecond(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:135301'."\n". @@ -1492,8 +1444,7 @@ public function testRFC6351ValueTimeWithMinute(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:-53'."\n". @@ -1516,8 +1467,7 @@ public function testRFC6351ValueTimeWithMinuteSecond(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:-5301'."\n". @@ -1573,8 +1523,7 @@ public function testRFC6351ValueTimeWithSecondZ(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:--01Z'."\n". @@ -1597,8 +1546,7 @@ public function testRFC6351ValueTimeWithSecondTZ(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:--01+1234'."\n". @@ -1621,8 +1569,7 @@ public function testRFC6351ValueDateTimeWithYearMonthDayHour(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:20150128T13'."\n". @@ -1645,8 +1592,7 @@ public function testRFC6351ValueDateTimeWithMonthDayHour(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:--0128T13'."\n". @@ -1669,8 +1615,7 @@ public function testRFC6351ValueDateTimeWithDayHour(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:---28T13'."\n". @@ -1693,8 +1638,7 @@ public function testRFC6351ValueDateTimeWithDayHourMinute(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:---28T1353'."\n". @@ -1717,8 +1661,7 @@ public function testRFC6351ValueDateTimeWithDayHourMinuteSecond(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:---28T135301'."\n". @@ -1741,8 +1684,7 @@ public function testRFC6351ValueDateTimeWithDayHourZ(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:---28T13Z'."\n". @@ -1765,8 +1707,7 @@ public function testRFC6351ValueDateTimeWithDayHourTZ(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:---28T13+1234'."\n". @@ -1789,8 +1730,7 @@ public function testRFC6350Section6Part1Part3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'SOURCE:ldap://ldap.example.com/cn=Babs%20Jensen\,%20o=Babsco\,%20c=US'."\n". @@ -1813,8 +1753,7 @@ public function testRFC6350Section6Part1Part4(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'KIND:individual'."\n". @@ -1837,8 +1776,7 @@ public function testRFC6350Section6Part2Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'FN:Mr. John Q. Public\, Esq.'."\n". @@ -1865,8 +1803,7 @@ public function testRFC6350Section6Part2Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'N:Stevenson;John;Philip\,Paul;Dr.;Jr.\,M.D.\,A.C.P.'."\n". @@ -1890,8 +1827,7 @@ public function testRFC6350Section6Part2Part3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'NICKNAME:Jim,Jimmie'."\n". @@ -1914,8 +1850,7 @@ public function testRFC6350Section6Part2Part4(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'PHOTO:http://www.example.com/pub/photos/jqpublic.gif'."\n". @@ -1935,8 +1870,7 @@ public function testRFC6350Section6Part2Part5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'BDAY:19531015T231000Z'."\n". @@ -1956,8 +1890,7 @@ public function testRFC6350Section6Part2Part6(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'ANNIVERSARY:19960415'."\n". @@ -1981,8 +1914,7 @@ public function testRFC6350Section6Part2Part7(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'GENDER:Jim;Jimmie'."\n". @@ -2011,8 +1943,7 @@ public function testRFC6350Section6Part3Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.'."\n". @@ -2050,8 +1981,7 @@ public function testRFC6350Section6Part4Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TEL;TYPE=home:tel:+33-01-23-45-67'."\n". @@ -2073,8 +2003,7 @@ public function testRFC6350Section6Part4Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TEL;TYPE=home:tel:+33-01-23-45-67'."\n". @@ -2102,8 +2031,7 @@ public function testRFC6350Section6Part4Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'EMAIL;TYPE=work:jqpublic@xyz.example.com'."\n". @@ -2131,8 +2059,7 @@ public function testRFC6350Section6Part4Part3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'IMPP;PREF=1:xmpp:alice@example.com'."\n". @@ -2163,8 +2090,7 @@ public function testRFC6350Section6Part4Part4(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'LANG;TYPE=work;PREF=2:en'."\n". @@ -2187,8 +2113,7 @@ public function testRFC6350Section6Part5Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TZ:Raleigh/North America'."\n". @@ -2211,8 +2136,7 @@ public function testRFC6350Section6Part5Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'GEO:geo:37.386013\,-122.082932'."\n". @@ -2229,8 +2153,7 @@ public function testRFC6350Section6Part5Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'GEO:geo:37.386013\,-122.082932'."\n". @@ -2253,8 +2176,7 @@ public function testRFC6350Section6Part6Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'TITLE:Research Scientist'."\n". @@ -2277,8 +2199,7 @@ public function testRFC6350Section6Part6Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'ROLE:Project Leader'."\n". @@ -2301,8 +2222,7 @@ public function testRFC6350Section6Part6Part3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'LOGO:http://www.example.com/pub/logos/abccorp.jpg'."\n". @@ -2327,8 +2247,7 @@ public function testRFC6350Section6Part6Part4(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'ORG:ABC\, Inc.;North American Division;Marketing'."\n". @@ -2351,8 +2270,7 @@ public function testRFC6350Section6Part6Part5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'MEMBER:urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af'."\n". @@ -2378,8 +2296,7 @@ public function testRFC6350Section6Part6Part5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'MEMBER:mailto:subscriber1@example.com'."\n". @@ -2410,8 +2327,7 @@ public function testRFC6350Section6Part6Part6(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'RELATED;TYPE=friend:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6'."\n". @@ -2437,8 +2353,7 @@ public function testRFC6350Section6Part7Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY'."\n". @@ -2461,8 +2376,7 @@ public function testRFC6350Section6Part7Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'NOTE:Foo\, bar'."\n". @@ -2485,8 +2399,7 @@ public function testRFC6350Section6Part7Part3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'PRODID:-//ONLINE DIRECTORY//NONSGML Version 1//EN'."\n". @@ -2506,8 +2419,7 @@ public function testRFC6350Section6Part7Part4(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'REV:19951031T222710Z'."\n". @@ -2530,8 +2442,7 @@ public function testRFC6350Section6Part7Part5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'SOUND:CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com'."\n". @@ -2548,8 +2459,7 @@ public function testRFC6350Section6Part7Part5(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'SOUND:CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com'."\n". @@ -2572,8 +2482,7 @@ public function testRFC6350Section6Part7Part6(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'UID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6'."\n". @@ -2597,8 +2506,7 @@ public function testRFC6350Section6Part7Part7(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'CLIENTPIDMAP:1;urn:uuid:3df403f4-5924-4bb7-b077-3c711d9eb34b'."\n". @@ -2621,8 +2529,7 @@ public function testRFC6350Section6Part7Part8(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'URL:http://example.org/restaurant.french/~chezchic.html'."\n". @@ -2641,8 +2548,7 @@ public function testRFC6350Section6Part7Part9(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'END:VCARD'."\n" @@ -2669,8 +2575,7 @@ public function testRFC6350Section6Part8Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'KEY;MEDIATYPE=application/pgp-keys:ftp://example.com/keys/jdoe'."\n". @@ -2698,8 +2603,7 @@ public function testRFC6350Section6Part9Part1(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'FBURL;PREF=1:http://www.example.com/busy/janedoe'."\n". @@ -2722,8 +2626,7 @@ public function testRFC6350Section6Part9Part2(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'CALADRURI:http://example.com/calendar/jdoe'."\n". @@ -2751,8 +2654,7 @@ public function testRFC6350Section6Part9Part3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'CALURI;PREF=1:http://cal.example.com/calA'."\n". @@ -2775,8 +2677,7 @@ public function testRFC6350SectionAPart3(): void -XML - , +XML, 'BEGIN:VCARD'."\n". 'VERSION:4.0'."\n". 'CAPURI:http://cap.example.com/capA'."\n". diff --git a/tests/VObject/Property/ICalendar/DateTimeTest.php b/tests/VObject/Property/ICalendar/DateTimeTest.php index 0da557b93..3986ec32a 100644 --- a/tests/VObject/Property/ICalendar/DateTimeTest.php +++ b/tests/VObject/Property/ICalendar/DateTimeTest.php @@ -177,7 +177,7 @@ public function testGetDateTimeCached(): void { $tz = new \DateTimeZone('Europe/Amsterdam'); $dt = new \DateTimeImmutable('1985-07-04 01:30:00', $tz); - $dt->setTimeZone($tz); + $dt = $dt->setTimeZone($tz); $elem = $this->vcal->createProperty('DTSTART'); $elem->setDateTime($dt); diff --git a/tests/VObject/Property/ICalendar/RecurTest.php b/tests/VObject/Property/ICalendar/RecurTest.php index 29096e9de..99d2525c6 100644 --- a/tests/VObject/Property/ICalendar/RecurTest.php +++ b/tests/VObject/Property/ICalendar/RecurTest.php @@ -197,11 +197,10 @@ public function testValidateStripNoFreq(): void public function testUnrepairableRRule(): void { - $calendar = new VCalendar(); - $this->expectException(InvalidDataException::class); - + $calendar = new VCalendar(); $property = $calendar->createProperty('RRULE', 'IAmNotARRule'); + $property->validate(Node::REPAIR); } public function testValidateInvalidByMonthRruleWithRepair(): void diff --git a/tests/VObject/PropertyTest.php b/tests/VObject/PropertyTest.php index 5a34383f1..320618b27 100644 --- a/tests/VObject/PropertyTest.php +++ b/tests/VObject/PropertyTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\ICalendar\DateTime; class PropertyTest extends TestCase { @@ -391,4 +392,16 @@ public function testValidateBadEncodingVCard21(): void self::assertEquals('ENCODING=B is not valid for this document type.', $result[0]['message']); self::assertEquals(3, $result[0]['level']); } + + public function testUnknownValuesWillBeIgnored(): void + { + $cal = new VCalendar(); + $property = $cal->createProperty('DTSTAMP', '20240101T000000Z', ['VALUE' => 'DATETIME']); + + // Unlike upstream, Proton drops an unknown VALUE parameter. + self::assertEquals("DTSTAMP:20240101T000000Z\r\n", $property->serialize()); + + self::assertInstanceOf(DateTime::class, $property); + self::assertCount(0, $property->parameters()); + } } diff --git a/tests/VObject/TimezoneGuesser/FindFromTimezoneMapTest.php b/tests/VObject/TimezoneGuesser/FindFromTimezoneMapTest.php new file mode 100644 index 000000000..dccc5b52c --- /dev/null +++ b/tests/VObject/TimezoneGuesser/FindFromTimezoneMapTest.php @@ -0,0 +1,68 @@ +find($mapKey); + + self::assertNotNull($tz, "Expected '$mapKey' to resolve to '$expectedOlson'"); + self::assertSame($expectedOlson, $tz->getName()); + } + + public function updatedTimezoneProvider(): array + { + return [ + // windowszones.php + // Proton redirects Europe/Kyiv to Europe/Kiev, see FindFromTimezoneIdentifier::MIGRATION_TIMEZONES + ['FLE Standard Time', 'Europe/Kiev'], + ['India Standard Time', 'Asia/Kolkata'], + ['Nepal Standard Time', 'Asia/Kathmandu'], + ['Myanmar Standard Time', 'Asia/Yangon'], + // Proton maps Greenland to Atlantic/Stanley (#65) + ['Greenland Standard Time', 'Atlantic/Stanley'], + ['Argentina Standard Time', 'America/Argentina/Buenos_Aires'], + // extrazones.php overrides the windowszones.php value (#14) + ['US Eastern Standard Time', 'America/New_York'], + // lotuszones.php + ['India', 'Asia/Kolkata'], + ['Myanmar', 'Asia/Yangon'], + // exchangezones.php + ['Kolkata, Chennai, Mumbai, New Delhi, India Standard Time', 'Asia/Kolkata'], + ['Rangoon', 'Asia/Yangon'], + ]; + } + + /** + * Verify that the Microsoft-offset-prefix stripping path still works + * with updated timezone values. + */ + public function testMicrosoftOffsetPrefixStripping(): void + { + $finder = new FindFromTimezoneMap(); + $tz = $finder->find('(UTC+02:00) FLE Standard Time'); + + self::assertNotNull($tz); + self::assertSame('Europe/Kiev', $tz->getName()); + } + + public function testUnknownTimezoneReturnsNull(): void + { + $finder = new FindFromTimezoneMap(); + + self::assertNull($finder->find('This/Does_Not_Exist')); + } +}