diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f95a950f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php_cs.dist export-ignore +/.travis.yml export-ignore +/CHANGELOG.md export-ignore +/phpstan.neon export-ignore diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml new file mode 100644 index 000000000..d1c7c97b2 --- /dev/null +++ b/.github/workflows/actions.yml @@ -0,0 +1,59 @@ +name: Tests + +on: [push] + +jobs: + phpunit: + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['7.4', '8.0', '8.1'] + + runs-on: ${{ matrix.operating-system }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring + coverage: pcov + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + # Use composer.json for key, if composer.lock is not committed. + # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + 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 + + - name: Code Analysis (PHPStan) + if: matrix.code-analysis == 'yes' + run: composer phpstan + + - name: Test with phpunit + run: vendor/bin/phpunit --configuration ./tests/phpunit.xml --coverage-text --coverage-clover=coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: tests + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index f08b31359..82b7dad3f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,20 +3,7 @@ vendor/ composer.lock tests/cov/ tests/temp - -#vim -.*.swp - -#binaries -bin/phpunit -bin/phpcs -bin/php-cs-fixer -bin/sabre-cs-fixer -bin/hoa +tests/.phpunit.result.cache # Development stuff -testdata/ .php_cs.cache - -# OS X -.DS_Store diff --git a/.php_cs.dist b/.php_cs.dist index 8d61ee259..4aaf1d90f 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -6,7 +6,7 @@ $config->getFinder() ->in(__DIR__); $config->setRules([ '@PSR1' => true, - '@Symfony' =>true + '@Symfony' => true ]); -return $config; \ No newline at end of file +return $config; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 686982b66..000000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: php -sudo: required -php: - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - -env: - global: - - RUN_PHPSTAN="FALSE" - -matrix: - include: - - name: 'PHPStan' - php: 7.2 - env: RUN_PHPSTAN="TRUE" - fast_finish: true - allow_failures: - - php: 5.5 - -install: - - if [ $RUN_PHPSTAN == "TRUE" ]; then wget https://github.com/phpstan/phpstan/releases/download/0.11.8/phpstan.phar; fi - -before_script: - - composer install - -script: - - if [ $RUN_PHPSTAN == "FALSE" ]; then ./bin/phpunit --configuration tests/phpunit.xml --coverage-clover=coverage.xml; fi - - if [ $RUN_PHPSTAN == "TRUE" ]; then php phpstan.phar analyse -c phpstan.neon lib; fi - -after_success: - - bash <(curl -s https://codecov.io/bash) - -cache: - directories: - - $HOME/.composer/cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2c935fb..edf59eb78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,134 @@ ChangeLog ========= +4.22.0 (2023-03-01) +------------------- +* #80 Allow newly deprecated timezones + +4.21.0 (2022-12-28) +------------------- +* #76 Add mapping for PST timezone + +4.20.0 (2022-12-23) +------------------- +* #74 Handle null tzid + +4.19.0 (2022-12-21) +------------------- +* #72 Deprecate Enderbury timezone + +4.18.0 (2022-09-01) +------------------- +* #69 Add mapping for CDT timezone + +4.17.0 (2022-07-29) +------------------- +* #65 [Calendar] Replace Godthab timezone + +4.16.0 (2022-07-06) +------------------- +* #63 Fix customized timezone guesser + +4.15.0 (2022-06-23) +------------------- +* #57 Handle customized timezone + +4.14.0 (2022-05-31) +------------------- +* #54 Add FindFromOffsetName + +4.13.0 (2022-05-27) +------------------- +* #56 Merge upstream changes from sabre-io/vobject@a595790 into protonlabs/vobject +* #58 Handle version timezone + +4.12.0 (2022-05-05) +------------------- +* #52 Add FindFromOutlookCities timezone finder + +4.11.0 (2022-04-22) +------------------- +* #50 Add lowercase timezone finder + +4.10.0 (2022-03-08) +------------------- +* #48 Block invalid combinations of FREQ with BY rules + +4.9.0 (2022-02-15) +------------------ +* #46 Add support UTC-05:00 timezone + +4.8.0 (2022-02-10) +------------------ +* #42 Add option to fix unfolding issues in ICS + +4.7.2 (2022-01-21) +------------------ +* #41 Add missing microsoft timezones and test with confluence file + +4.7.1 (2022-01-10) +------------------ +* #39 Add support for lowercase timezones + +4.7.0 (2021-12-15) +------------------ +* #34 Merge upstream changes from sabre-io/vobject:4.4.0 into protonlabs/vobject +* #36 Merge upstream changes from sabre-io/vobject:4.4.1 into protonlabs/vobject + +4.6.1 (2021-11-04) +------------------ +* #29 Fix timezone name prefixed with / +* #30 Missing EDT TZID conversion + +4.5.1 (2021-10-11) +------------------ +* #25 Fix duplicate value +* #26 Add php unsupport timezone + +4.4.2 (2021-07-15) +------------------ +* #23 Add microsoft timezone map + +4.4.1 (2021-01-18) +------------------ +* #19 Validate count and until property + +4.4.0 (2020-11-23) +------------------ + +* #18 Merge upstream changes from sabre-io/vobject:4.3.3 into protonlabs/vobject +* #17 Throw exception when getting invalid timezone + +4.3.4 (2020-07-27) +------------------ + +* #16 Merge upstream changes from sabre-io/vobject:4.3.1 into protonlabs/vobject + +4.3.3 (2020-07-22) +------------------ + +* #15 Expose RRULE properties + +4.3.2 (2020-05-20) +------------------ + +* #14 Add timezones data mapping. + +4.3.1 (2020-01-27) +------------------ + +* #6 Add FastForward Before +* #7 Add FastForward to end +* #11 FREQ is mandatory in the RRule +* #12 Validate BYMONTHDAY + +4.2.1 (2019-09-10) +------------------ + +* #2 Fix bug in by year day +* #3 Add daily occurrences to nextMonth and NextYear +* #4 Enhance fast forward speed if no count value has been given + 4.2.0 (2019-02-19) ------------------ @@ -84,7 +212,7 @@ ChangeLog * #306: iTip REPLYs to the first instance of a recurring event was not handled correctly. * Slightly better error message during validation of `N` and `ADR` properties. -* #312: Correctly extracing timezone in the iTip broker, even when we don't +* #312: Correctly extracting timezone in the iTip broker, even when we don't have a master event. (@vkomrakov-sugar). * When validating a component's property that must appear once and which could automatically be repaired, make sure we report the change as 'repaired'. @@ -406,7 +534,7 @@ ChangeLog * #114: VTIMEZONE is retained when generating new REQUEST objects. * #114: Support for 'MAILTO:' style email addresses (in uppercase) in the iTip broker. This improves evolution support. -* #115: Using REQUEST-STATUS from REPLY messages and now propegating that into +* #115: Using REQUEST-STATUS from REPLY messages and now propagating that into SCHEDULE-STATUS. @@ -643,7 +771,7 @@ ChangeLog 3.0.0-alpha2 (2013-05-22) ------------------------- -* Fixed: vCard URL properties were referencing a non-existant class. +* Fixed: vCard URL properties were referencing a non-existent class. 3.0.0-alpha1 (2013-05-21) @@ -801,7 +929,7 @@ ChangeLog properties such as N, ADR, ORG and CATEGORIES. * Added: Splitter classes, that can split up large objects (such as exports) into individual objects (thanks @DominikTo and @armin-hackmann). -* Added: VFREEBUSY component, which allows easily checking wether timeslots are +* Added: VFREEBUSY component, which allows easily checking whether timeslots are available. * Added: The Reader class now has a 'FORGIVING' option, which allows it to parse properties with incorrect characters in the name (at this time, it just allows diff --git a/README.md b/README.md index 5030cf276..cc5f34fb9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -sabre/vobject +protonlabs/vobject ============= +![Build Status](https://github.com/ProtonMail/vobject/actions/workflows/actions.yml/badge.svg) +[![codecov](https://codecov.io/gh/ProtonMail/vobject/branch/master/graph/badge.svg?token=ARcwkxCKZn)](https://codecov.io/gh/ProtonMail/vobject) + +Forked from [sabre/vobject](https://github.com/sabre-io/vobject). The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545) and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP. -The goal of the VObject library is to create a very complete library, with an easy to use API. +The goal of the VObject library is to create a very complete library, with an easy-to-use API. Installation @@ -12,12 +16,12 @@ Installation Make sure you have [Composer][1] installed, and then run: - composer require sabre/vobject "^4.0" + composer require protonlabs/vobject "^4.0" This package requires PHP 5.5. If you need the PHP 5.3/5.4 version of this package instead, use: - composer require sabre/vobject "^3.4" + composer require protonlabs/vobject "^3.4" Usage @@ -27,21 +31,6 @@ Usage * [Working with iCalendar](http://sabre.io/vobject/icalendar/) - -Build status ------------- - -| branch | status | -| ------ | ------ | -| master | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=master)](https://travis-ci.org/sabre-io/vobject) | -| 3.5 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.5)](https://travis-ci.org/sabre-io/vobject) | -| 3.4 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.4)](https://travis-ci.org/sabre-io/vobject) | -| 3.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.1)](https://travis-ci.org/sabre-io/vobject) | -| 2.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.1)](https://travis-ci.org/sabre-io/vobject) | -| 2.0 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.0)](https://travis-ci.org/sabre-io/vobject) | - - - Support ------- diff --git a/bin/bench_freebusygenerator.php b/bin/bench_freebusygenerator.php index 1299c14fb..963623d18 100644 --- a/bin/bench_freebusygenerator.php +++ b/bin/bench_freebusygenerator.php @@ -11,7 +11,7 @@ echo "The process will be repeated 100 times to get accurate stats\n"; echo "\n"; echo 'Usage: '.$argv[0]." inputfile.ics\n"; - die(); + exit(); } list(, $inputFile) = $argv; diff --git a/bin/bench_manipulatevcard.php b/bin/bench_manipulatevcard.php index f229091db..df6d9f23d 100644 --- a/bin/bench_manipulatevcard.php +++ b/bin/bench_manipulatevcard.php @@ -10,7 +10,7 @@ echo 'system.'; echo "\n"; echo 'Usage: '.$argv[0]." inputfile.vcf\n"; - die(); + exit(); } list(, $inputFile) = $argv; diff --git a/bin/fetch_windows_zones.php b/bin/fetch_windows_zones.php index 9c4e51abd..2361dc309 100755 --- a/bin/fetch_windows_zones.php +++ b/bin/fetch_windows_zones.php @@ -1,13 +1,12 @@ #!/usr/bin/env php =5.5", + "php" : "^7.1 || ^8.0", "ext-mbstring" : "*", - "sabre/xml" : ">=1.5 <3.0" + "sabre/xml" : "^2.1" }, "require-dev" : { - "phpunit/phpunit" : "> 4.8.35, <6.0.0" + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.0", + "phpunit/php-invoker" : "^2.0 || ^3.1", + "phpstan/phpstan": "^0.12" }, "suggest" : { "hoa/bench" : "If you would like to run the benchmark scripts" @@ -71,6 +74,11 @@ "Sabre\\VObject\\" : "lib/" } }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\VObject\\" : "tests/VObject" + } + }, "bin" : [ "bin/vobject", "bin/generate_vcards" @@ -80,7 +88,20 @@ "dev-master" : "4.0.x-dev" } }, - "config" : { - "bin-dir" : "bin" + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] } } diff --git a/lib/Cli.php b/lib/Cli.php index 70b5e8d6e..816e2cb31 100644 --- a/lib/Cli.php +++ b/lib/Cli.php @@ -2,8 +2,7 @@ namespace Sabre\VObject; -use - InvalidArgumentException; +use InvalidArgumentException; /** * This is the CLI interface for sabre-vobject. @@ -29,7 +28,7 @@ class Cli protected $showHelp = false; /** - * Wether to spit out 'mimedir' or 'json' format. + * Whether to spit out 'mimedir' or 'json' format. * * @var string */ @@ -137,17 +136,14 @@ public function main(array $argv) // jcard/jcal documents case 'jcard': case 'jcal': - // specific document versions case 'vcard21': case 'vcard30': case 'vcard40': case 'icalendar20': - // specific formats case 'json': case 'mimedir': - // icalendar/vcad case 'icalendar': case 'vcard': @@ -183,7 +179,6 @@ public function main(array $argv) case 'vcard30': case 'vcard40': case 'icalendar20': - $this->inputFormat = 'mimedir'; break; @@ -211,7 +206,7 @@ public function main(array $argv) } if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { - throw new InvalidArgumentException('Uknown command: '.$positional[0]); + throw new InvalidArgumentException('Unknown command: '.$positional[0]); } } catch (InvalidArgumentException $e) { $this->showHelp(); @@ -289,7 +284,7 @@ protected function showHelp() $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.'); $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.'); $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.'); - $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debbugging.'); + $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.'); $this->log( <<group) === $group; + return $child instanceof Property && strtoupper($child->group ?? '') === $group; } ); } @@ -251,7 +249,7 @@ function ($child) use ($group) { $result = []; foreach ($this->children as $childGroup) { foreach ($childGroup as $child) { - if ($child instanceof Property && strtoupper($child->group) === $group) { + if ($child instanceof Property && $child->group && strtoupper($child->group) === $group) { $result[] = $child; } } @@ -276,7 +274,7 @@ public function serialize() * * A higher score means the item will be lower in the list. * To avoid score collisions, each "score category" has a reasonable - * space to accomodate elements. The $key is added to the $score to + * space to accommodate elements. The $key is added to the $score to * preserve the original relative order of elements. * * @param int $key @@ -341,6 +339,7 @@ function ($a, $b) use ($sortScore, $tmp) { * * @return array */ + #[\ReturnTypeWillChange] public function jsonSerialize() { $components = []; @@ -433,7 +432,7 @@ protected function getDefaults() * * @param string $name * - * @return Property + * @return Property|null */ public function __get($name) { diff --git a/lib/Component/VAvailability.php b/lib/Component/VAvailability.php index 6f3e7f13c..04ec38dcb 100644 --- a/lib/Component/VAvailability.php +++ b/lib/Component/VAvailability.php @@ -26,9 +26,6 @@ class VAvailability extends VObject\Component * * https://tools.ietf.org/html/draft-daboo-calendar-availability-05#section-3.1 * - * @param DateTimeInterface $start - * @param DateTimeInterface $end - * * @return bool */ public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) diff --git a/lib/Component/VCalendar.php b/lib/Component/VCalendar.php index e21ae077a..4db318135 100644 --- a/lib/Component/VCalendar.php +++ b/lib/Component/VCalendar.php @@ -37,15 +37,15 @@ class VCalendar extends VObject\Document * @var array */ public static $componentMap = [ - 'VCALENDAR' => 'Sabre\\VObject\\Component\\VCalendar', - 'VALARM' => 'Sabre\\VObject\\Component\\VAlarm', - 'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', - 'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', - 'VAVAILABILITY' => 'Sabre\\VObject\\Component\\VAvailability', - 'AVAILABLE' => 'Sabre\\VObject\\Component\\Available', - 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', - 'VTIMEZONE' => 'Sabre\\VObject\\Component\\VTimeZone', - 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', + 'VCALENDAR' => self::class, + 'VALARM' => VAlarm::class, + 'VEVENT' => VEvent::class, + 'VFREEBUSY' => VFreeBusy::class, + 'VAVAILABILITY' => VAvailability::class, + 'AVAILABLE' => Available::class, + 'VJOURNAL' => VJournal::class, + 'VTIMEZONE' => VTimeZone::class, + 'VTODO' => VTodo::class, ]; /** @@ -54,21 +54,21 @@ class VCalendar extends VObject\Document * @var array */ public static $valueMap = [ - 'BINARY' => 'Sabre\\VObject\\Property\\Binary', - 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', - 'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', - 'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date', - 'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', - 'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', - 'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue', - 'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period', - 'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', - 'TEXT' => 'Sabre\\VObject\\Property\\Text', - 'TIME' => 'Sabre\\VObject\\Property\\Time', - 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. - 'URI' => 'Sabre\\VObject\\Property\\Uri', - 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, + 'DATE' => VObject\Property\ICalendar\Date::class, + 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'PERIOD' => VObject\Property\ICalendar\Period::class, + 'RECUR' => VObject\Property\ICalendar\Recur::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, ]; /** @@ -78,78 +78,78 @@ class VCalendar extends VObject\Document */ public static $propertyMap = [ // Calendar properties - 'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText', - 'METHOD' => 'Sabre\\VObject\\Property\\FlatText', - 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', - 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', + 'CALSCALE' => VObject\Property\FlatText::class, + 'METHOD' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, // Component properties - 'ATTACH' => 'Sabre\\VObject\\Property\\Uri', - 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', - 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', - 'COMMENT' => 'Sabre\\VObject\\Property\\FlatText', - 'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText', - 'GEO' => 'Sabre\\VObject\\Property\\FloatValue', - 'LOCATION' => 'Sabre\\VObject\\Property\\FlatText', - 'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\IntegerValue', - 'PRIORITY' => 'Sabre\\VObject\\Property\\IntegerValue', - 'RESOURCES' => 'Sabre\\VObject\\Property\\Text', - 'STATUS' => 'Sabre\\VObject\\Property\\FlatText', - 'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText', + 'ATTACH' => VObject\Property\Uri::class, + 'CATEGORIES' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, + 'COMMENT' => VObject\Property\FlatText::class, + 'DESCRIPTION' => VObject\Property\FlatText::class, + 'GEO' => VObject\Property\FloatValue::class, + 'LOCATION' => VObject\Property\FlatText::class, + 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, + 'PRIORITY' => VObject\Property\IntegerValue::class, + 'RESOURCES' => VObject\Property\Text::class, + 'STATUS' => VObject\Property\FlatText::class, + 'SUMMARY' => VObject\Property\FlatText::class, // Date and Time Component Properties - 'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', - 'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period', - 'TRANSP' => 'Sabre\\VObject\\Property\\FlatText', + 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, + 'DTEND' => VObject\Property\ICalendar\DateTime::class, + 'DUE' => VObject\Property\ICalendar\DateTime::class, + 'DTSTART' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FREEBUSY' => VObject\Property\ICalendar\Period::class, + 'TRANSP' => VObject\Property\FlatText::class, // Time Zone Component Properties - 'TZID' => 'Sabre\\VObject\\Property\\FlatText', - 'TZNAME' => 'Sabre\\VObject\\Property\\FlatText', - 'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset', - 'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset', - 'TZURL' => 'Sabre\\VObject\\Property\\Uri', + 'TZID' => VObject\Property\FlatText::class, + 'TZNAME' => VObject\Property\FlatText::class, + 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, + 'TZOFFSETTO' => VObject\Property\UtcOffset::class, + 'TZURL' => VObject\Property\Uri::class, // Relationship Component Properties - 'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', - 'CONTACT' => 'Sabre\\VObject\\Property\\FlatText', - 'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', - 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText', - 'URL' => 'Sabre\\VObject\\Property\\Uri', - 'UID' => 'Sabre\\VObject\\Property\\FlatText', + 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, + 'CONTACT' => VObject\Property\FlatText::class, + 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, + 'RELATED-TO' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, // Recurrence Component Properties - 'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', - 'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545 + 'EXDATE' => VObject\Property\ICalendar\DateTime::class, + 'RDATE' => VObject\Property\ICalendar\DateTime::class, + 'RRULE' => VObject\Property\ICalendar\Recur::class, + 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 // Alarm Component Properties - 'ACTION' => 'Sabre\\VObject\\Property\\FlatText', - 'REPEAT' => 'Sabre\\VObject\\Property\\IntegerValue', - 'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + 'ACTION' => VObject\Property\FlatText::class, + 'REPEAT' => VObject\Property\IntegerValue::class, + 'TRIGGER' => VObject\Property\ICalendar\Duration::class, // Change Management Component Properties - 'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'SEQUENCE' => 'Sabre\\VObject\\Property\\IntegerValue', + 'CREATED' => VObject\Property\ICalendar\DateTime::class, + 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, + 'SEQUENCE' => VObject\Property\IntegerValue::class, // Request Status - 'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text', + 'REQUEST-STATUS' => VObject\Property\Text::class, // Additions from draft-daboo-valarm-extensions-04 - 'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text', - 'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', - 'PROXIMITY' => 'Sabre\\VObject\\Property\\Text', - 'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean', + 'ALARM-AGENT' => VObject\Property\Text::class, + 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, + 'PROXIMITY' => VObject\Property\Text::class, + 'DEFAULT-ALARM' => VObject\Property\Boolean::class, // Additions from draft-daboo-calendar-availability-05 - 'BUSYTYPE' => 'Sabre\\VObject\\Property\\Text', + 'BUSYTYPE' => VObject\Property\Text::class, ]; /** @@ -276,10 +276,8 @@ public function getBaseComponent($componentName = null) * In addition, this method will cause timezone information to be stripped, * and normalized to UTC. * - * @param DateTimeInterface $start - * @param DateTimeInterface $end - * @param DateTimeZone $timeZone reference timezone for floating dates and - * times + * @param DateTimeZone $timeZone reference timezone for floating dates and + * times * * @return VCalendar */ @@ -311,7 +309,7 @@ public function expand(DateTimeInterface $start, DateTimeInterface $end, DateTim foreach ($this->children() as $child) { if ($child instanceof Property && 'PRODID' !== $child->name) { - // We explictly want to ignore PRODID, because we want to + // We explicitly want to ignore PRODID, because we want to // overwrite it with our own. $newChildren[] = clone $child; } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) { diff --git a/lib/Component/VCard.php b/lib/Component/VCard.php index 860e45ffa..eac789842 100644 --- a/lib/Component/VCard.php +++ b/lib/Component/VCard.php @@ -39,7 +39,7 @@ class VCard extends VObject\Document * @var array */ public static $componentMap = [ - 'VCARD' => 'Sabre\\VObject\\Component\\VCard', + 'VCARD' => VCard::class, ]; /** @@ -48,22 +48,23 @@ class VCard extends VObject\Document * @var array */ public static $valueMap = [ - 'BINARY' => 'Sabre\\VObject\\Property\\Binary', - 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', - 'CONTENT-ID' => 'Sabre\\VObject\\Property\\FlatText', // vCard 2.1 only - 'DATE' => 'Sabre\\VObject\\Property\\VCard\\Date', - 'DATE-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateTime', - 'DATE-AND-OR-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', // vCard only - 'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', - 'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue', - 'LANGUAGE-TAG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', - 'TIMESTAMP' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', - 'TEXT' => 'Sabre\\VObject\\Property\\Text', - 'TIME' => 'Sabre\\VObject\\Property\\Time', - 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. - 'URI' => 'Sabre\\VObject\\Property\\Uri', - 'URL' => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only - 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only + 'DATE' => VObject\Property\VCard\Date::class, + 'DATE-TIME' => VObject\Property\VCard\DateTime::class, + 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class, + 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only + 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'URL' => VObject\Property\Uri::class, // vCard 2.1 only + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, ]; /** @@ -73,68 +74,68 @@ class VCard extends VObject\Document */ public static $propertyMap = [ // vCard 2.1 properties and up - 'N' => 'Sabre\\VObject\\Property\\Text', - 'FN' => 'Sabre\\VObject\\Property\\FlatText', - 'PHOTO' => 'Sabre\\VObject\\Property\\Binary', - 'BDAY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', - 'ADR' => 'Sabre\\VObject\\Property\\Text', - 'LABEL' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 - 'TEL' => 'Sabre\\VObject\\Property\\FlatText', - 'EMAIL' => 'Sabre\\VObject\\Property\\FlatText', - 'MAILER' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 - 'GEO' => 'Sabre\\VObject\\Property\\FlatText', - 'TITLE' => 'Sabre\\VObject\\Property\\FlatText', - 'ROLE' => 'Sabre\\VObject\\Property\\FlatText', - 'LOGO' => 'Sabre\\VObject\\Property\\Binary', + 'N' => VObject\Property\Text::class, + 'FN' => VObject\Property\FlatText::class, + 'PHOTO' => VObject\Property\Binary::class, + 'BDAY' => VObject\Property\VCard\DateAndOrTime::class, + 'ADR' => VObject\Property\Text::class, + 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'TEL' => VObject\Property\FlatText::class, + 'EMAIL' => VObject\Property\FlatText::class, + 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'GEO' => VObject\Property\FlatText::class, + 'TITLE' => VObject\Property\FlatText::class, + 'ROLE' => VObject\Property\FlatText::class, + 'LOGO' => VObject\Property\Binary::class, // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so // not supported at the moment - 'ORG' => 'Sabre\\VObject\\Property\\Text', - 'NOTE' => 'Sabre\\VObject\\Property\\FlatText', - 'REV' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', - 'SOUND' => 'Sabre\\VObject\\Property\\FlatText', - 'URL' => 'Sabre\\VObject\\Property\\Uri', - 'UID' => 'Sabre\\VObject\\Property\\FlatText', - 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', - 'KEY' => 'Sabre\\VObject\\Property\\FlatText', - 'TZ' => 'Sabre\\VObject\\Property\\Text', + 'ORG' => VObject\Property\Text::class, + 'NOTE' => VObject\Property\FlatText::class, + 'REV' => VObject\Property\VCard\TimeStamp::class, + 'SOUND' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + 'KEY' => VObject\Property\FlatText::class, + 'TZ' => VObject\Property\Text::class, // vCard 3.0 properties - 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', - 'SORT-STRING' => 'Sabre\\VObject\\Property\\FlatText', - 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', - 'NICKNAME' => 'Sabre\\VObject\\Property\\Text', - 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + 'CATEGORIES' => VObject\Property\Text::class, + 'SORT-STRING' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'NICKNAME' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0 // rfc2739 properties - 'FBURL' => 'Sabre\\VObject\\Property\\Uri', - 'CAPURI' => 'Sabre\\VObject\\Property\\Uri', - 'CALURI' => 'Sabre\\VObject\\Property\\Uri', - 'CALADRURI' => 'Sabre\\VObject\\Property\\Uri', + 'FBURL' => VObject\Property\Uri::class, + 'CAPURI' => VObject\Property\Uri::class, + 'CALURI' => VObject\Property\Uri::class, + 'CALADRURI' => VObject\Property\Uri::class, // rfc4770 properties - 'IMPP' => 'Sabre\\VObject\\Property\\Uri', + 'IMPP' => VObject\Property\Uri::class, // vCard 4.0 properties - 'SOURCE' => 'Sabre\\VObject\\Property\\Uri', - 'XML' => 'Sabre\\VObject\\Property\\FlatText', - 'ANNIVERSARY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', - 'CLIENTPIDMAP' => 'Sabre\\VObject\\Property\\Text', - 'LANG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', - 'GENDER' => 'Sabre\\VObject\\Property\\Text', - 'KIND' => 'Sabre\\VObject\\Property\\FlatText', - 'MEMBER' => 'Sabre\\VObject\\Property\\Uri', - 'RELATED' => 'Sabre\\VObject\\Property\\Uri', + 'SOURCE' => VObject\Property\Uri::class, + 'XML' => VObject\Property\FlatText::class, + 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class, + 'CLIENTPIDMAP' => VObject\Property\Text::class, + 'LANG' => VObject\Property\VCard\LanguageTag::class, + 'GENDER' => VObject\Property\Text::class, + 'KIND' => VObject\Property\FlatText::class, + 'MEMBER' => VObject\Property\Uri::class, + 'RELATED' => VObject\Property\Uri::class, // rfc6474 properties - 'BIRTHPLACE' => 'Sabre\\VObject\\Property\\FlatText', - 'DEATHPLACE' => 'Sabre\\VObject\\Property\\FlatText', - 'DEATHDATE' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', + 'BIRTHPLACE' => VObject\Property\FlatText::class, + 'DEATHPLACE' => VObject\Property\FlatText::class, + 'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class, // rfc6715 properties - 'EXPERTISE' => 'Sabre\\VObject\\Property\\FlatText', - 'HOBBY' => 'Sabre\\VObject\\Property\\FlatText', - 'INTEREST' => 'Sabre\\VObject\\Property\\FlatText', - 'ORG-DIRECTORY' => 'Sabre\\VObject\\Property\\FlatText', + 'EXPERTISE' => VObject\Property\FlatText::class, + 'HOBBY' => VObject\Property\FlatText::class, + 'INTEREST' => VObject\Property\FlatText::class, + 'ORG-DIRECTORY' => VObject\Property\FlatText::class, ]; /** @@ -372,7 +373,7 @@ public function getValidationRules() /** * Returns a preferred field. * - * VCards can indicate wether a field such as ADR, TEL or EMAIL is + * VCards can indicate whether a field such as ADR, TEL or EMAIL is * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x * being a number between 1 and 100). * @@ -444,6 +445,7 @@ protected function getDefaults() * * @return array */ + #[\ReturnTypeWillChange] public function jsonSerialize() { // A vcard does not have sub-components, so we're overriding this @@ -525,8 +527,8 @@ public function getClassNameForPropertyName($propertyName) $className = parent::getClassNameForPropertyName($propertyName); // In vCard 4, BINARY no longer exists, and we need URI instead. - if ('Sabre\\VObject\\Property\\Binary' == $className && self::VCARD40 === $this->getDocumentType()) { - return 'Sabre\\VObject\\Property\\Uri'; + if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { + return VObject\Property\Uri::class; } return $className; diff --git a/lib/Component/VEvent.php b/lib/Component/VEvent.php index 09f37033c..6ea93ed5e 100644 --- a/lib/Component/VEvent.php +++ b/lib/Component/VEvent.php @@ -25,9 +25,6 @@ class VEvent extends VObject\Component * The rules used to determine if an event falls within the specified * time-range is based on the CalDAV specification. * - * @param DateTimeInterface $start - * @param DateTimeInterface $end - * * @return bool */ public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) @@ -36,7 +33,7 @@ public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) try { $it = new EventIterator($this, null, $start->getTimezone()); } catch (NoInstancesException $e) { - // If we've catched this exception, there are no instances + // If we've caught this exception, there are no instances // for the event that fall into the specified time-range. return false; } diff --git a/lib/Component/VFreeBusy.php b/lib/Component/VFreeBusy.php index 558a85233..fef418b53 100644 --- a/lib/Component/VFreeBusy.php +++ b/lib/Component/VFreeBusy.php @@ -21,9 +21,6 @@ class VFreeBusy extends VObject\Component * Checks based on the contained FREEBUSY information, if a timeslot is * available. * - * @param DateTimeInterface $start - * @param DateTimeInterface $end - * * @return bool */ public function isFree(DateTimeInterface $start, DatetimeInterface $end) diff --git a/lib/Component/VJournal.php b/lib/Component/VJournal.php index 9bd336776..9b7f1b873 100644 --- a/lib/Component/VJournal.php +++ b/lib/Component/VJournal.php @@ -23,9 +23,6 @@ class VJournal extends VObject\Component * The rules used to determine if an event falls within the specified * time-range is based on the CalDAV specification. * - * @param DateTimeInterface $start - * @param DateTimeInterface $end - * * @return bool */ public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) diff --git a/lib/Component/VTodo.php b/lib/Component/VTodo.php index 9de77e841..6f022ba6d 100644 --- a/lib/Component/VTodo.php +++ b/lib/Component/VTodo.php @@ -23,9 +23,6 @@ class VTodo extends VObject\Component * The rules used to determine if an event falls within the specified * time-range is based on the CalDAV specification. * - * @param DateTimeInterface $start - * @param DateTimeInterface $end - * * @return bool */ public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) diff --git a/lib/Document.php b/lib/Document.php index 0cb2e0978..14a77c911 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -160,7 +160,7 @@ public function create($name) public function createComponent($name, array $children = null, $defaults = true) { $name = strtoupper($name); - $class = 'Sabre\\VObject\\Component'; + $class = Component::class; if (isset(static::$componentMap[$name])) { $class = static::$componentMap[$name]; @@ -258,7 +258,7 @@ public function getClassNameForPropertyName($propertyName) if (isset(static::$propertyMap[$propertyName])) { return static::$propertyMap[$propertyName]; } else { - return 'Sabre\\VObject\\Property\\Unknown'; + return Property\Unknown::class; } } } diff --git a/lib/ElementList.php b/lib/ElementList.php index 56058cbd5..860512649 100644 --- a/lib/ElementList.php +++ b/lib/ElementList.php @@ -25,6 +25,7 @@ class ElementList extends ArrayIterator * @param int $offset * @param mixed $value */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { throw new LogicException('You can not add new objects to an ElementList'); @@ -37,6 +38,7 @@ public function offsetSet($offset, $value) * * @param int $offset */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { throw new LogicException('You can not remove objects from an ElementList'); diff --git a/lib/FreeBusyData.php b/lib/FreeBusyData.php index d05dfc799..4d9f441ce 100644 --- a/lib/FreeBusyData.php +++ b/lib/FreeBusyData.php @@ -84,7 +84,7 @@ public function add($start, $end, $type) 'type' => $type, ]; - $preceedingItem = $this->data[$insertStartIndex - 1]; + $precedingItem = $this->data[$insertStartIndex - 1]; if ($this->data[$insertStartIndex - 1]['start'] === $start) { // The old item starts at the exact same point as the new item. --$insertStartIndex; @@ -122,11 +122,11 @@ public function add($start, $end, $type) // between. if (-1 === $itemsToDelete) { $itemsToDelete = 0; - if ($newItem['end'] < $preceedingItem['end']) { + if ($newItem['end'] < $precedingItem['end']) { $newItems[] = [ 'start' => $newItem['end'] + 1, - 'end' => $preceedingItem['end'], - 'type' => $preceedingItem['type'], + 'end' => $precedingItem['end'], + 'type' => $precedingItem['type'], ]; } } diff --git a/lib/FreeBusyGenerator.php b/lib/FreeBusyGenerator.php index adb214c08..81b8126d5 100644 --- a/lib/FreeBusyGenerator.php +++ b/lib/FreeBusyGenerator.php @@ -109,8 +109,6 @@ public function __construct(DateTimeInterface $start = null, DateTimeInterface $ * for setting things like the METHOD, CALSCALE, VERSION, etc.. * * The VFREEBUSY object will be automatically added though. - * - * @param Document $vcalendar */ public function setBaseObject(Document $vcalendar) { @@ -119,8 +117,6 @@ public function setBaseObject(Document $vcalendar) /** * Sets a VAVAILABILITY document. - * - * @param Document $vcalendar */ public function setVAvailability(Document $vcalendar) { @@ -130,7 +126,7 @@ public function setVAvailability(Document $vcalendar) /** * Sets the input objects. * - * You must either specify a valendar object as a string, or as the parse + * You must either specify a vcalendar object as a string, or as the parse * Component. * It's also possible to specify multiple objects as an array. * @@ -176,8 +172,6 @@ public function setTimeRange(DateTimeInterface $start = null, DateTimeInterface /** * Sets the reference timezone for floating times. - * - * @param DateTimeZone $timeZone */ public function setTimeZone(DateTimeZone $timeZone) { @@ -208,9 +202,6 @@ public function getResult() /** * This method takes a VAVAILABILITY component and figures out all the * available times. - * - * @param FreeBusyData $fbData - * @param VCalendar $vavailability */ protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability) { @@ -363,8 +354,7 @@ function ($a, $b) { * This method takes an array of iCalendar objects and applies its busy * times on fbData. * - * @param FreeBusyData $fbData - * @param VCalendar[] $objects + * @param VCalendar[] $objects */ protected function calculateBusy(FreeBusyData $fbData, array $objects) { @@ -372,7 +362,6 @@ protected function calculateBusy(FreeBusyData $fbData, array $objects) foreach ($object->getBaseComponents() as $component) { switch ($component->name) { case 'VEVENT': - $FBTYPE = 'BUSY'; if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) { break; diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php index 4f37b75d0..b66a59f54 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php @@ -104,7 +104,6 @@ class Broker * * If the iTip message was not supported, we will always return false. * - * @param Message $itipMessage * @param VCalendar $existingObject * * @return VCalendar|null @@ -163,7 +162,7 @@ public function processMessage(Message $itipMessage, VCalendar $existingObject = * * @return array */ - public function parseEvent($calendar = null, $userHref, $oldCalendar = null) + public function parseEvent($calendar, $userHref, $oldCalendar = null) { if ($oldCalendar) { if (is_string($oldCalendar)) { @@ -263,8 +262,6 @@ public function parseEvent($calendar = null, $userHref, $oldCalendar = null) * This is message from an organizer, and is either a new event * invite, or an update to an existing one. * - * - * @param Message $itipMessage * @param VCalendar $existingObject * * @return VCalendar|null @@ -300,7 +297,6 @@ protected function processMessageRequest(Message $itipMessage, VCalendar $existi * attendee got removed from an event, or an event got cancelled * altogether. * - * @param Message $itipMessage * @param VCalendar $existingObject * * @return VCalendar|null @@ -326,7 +322,6 @@ protected function processMessageCancel(Message $itipMessage, VCalendar $existin * The message is a reply. This is for example an attendee telling * an organizer he accepted the invite, or declined it. * - * @param Message $itipMessage * @param VCalendar $existingObject * * @return VCalendar|null @@ -452,10 +447,6 @@ protected function processMessageReply(Message $itipMessage, VCalendar $existing * We will detect which attendees got added, which got removed and create * specific messages for these situations. * - * @param VCalendar $calendar - * @param array $eventInfo - * @param array $oldEventInfo - * * @return array */ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) @@ -505,20 +496,21 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, $message->recipient = $attendee['href']; $message->recipientName = $attendee['name']; + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $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. $message->method = 'CANCEL'; - // Creating the new iCalendar body. - $icalMsg = new VCalendar(); $icalMsg->METHOD = $message->method; - foreach ($calendar->select('VTIMEZONE') as $timezone) { - $icalMsg->add(clone $timezone); - } - $event = $icalMsg->add('VEVENT', [ 'UID' => $message->uid, 'SEQUENCE' => $message->sequence, @@ -545,14 +537,8 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, // The attendee gets the updated event body $message->method = 'REQUEST'; - // Creating the new iCalendar body. - $icalMsg = new VCalendar(); $icalMsg->METHOD = $message->method; - foreach ($calendar->select('VTIMEZONE') as $timezone) { - $icalMsg->add(clone $timezone); - } - // We need to find out that this change is significant. If it's // not, systems may opt to not send messages. // @@ -561,9 +547,13 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, // properties changed in the event, or simply if there's a // difference in instances that the attendee is invited to. + $oldAttendeeInstances = array_keys($attendee['oldInstances']); + $newAttendeeInstances = array_keys($attendee['newInstances']); + $message->significantChange = 'REQUEST' === $attendee['forceSend'] || - array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) || + count($oldAttendeeInstances) != count($newAttendeeInstances) || + count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { @@ -625,10 +615,7 @@ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, * * This function figures out if we need to send a reply to an organizer. * - * @param VCalendar $calendar - * @param array $eventInfo - * @param array $oldEventInfo - * @param string $attendee + * @param string $attendee * * @return Message[] */ @@ -711,6 +698,10 @@ protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, $icalMsg = new VCalendar(); $icalMsg->METHOD = 'REPLY'; + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + $hasReply = false; foreach ($instances as $instance) { @@ -829,7 +820,10 @@ protected function parseEventInfo(VCalendar $calendar = null) $instances = []; $exdate = []; + $significantChangeEventProperties = []; + foreach ($calendar->VEVENT as $vevent) { + $eventSignificantChangeHash = ''; $rrule = []; if (is_null($uid)) { @@ -943,19 +937,26 @@ protected function parseEventInfo(VCalendar $calendar = null) if (isset($vevent->$prop)) { $propertyValues = $vevent->select($prop); - $significantChangeHash .= $prop.':'; + $eventSignificantChangeHash .= $prop.':'; if ('EXDATE' === $prop) { - $significantChangeHash .= implode(',', $exdate).';'; + $eventSignificantChangeHash .= implode(',', $exdate).';'; } elseif ('RRULE' === $prop) { - $significantChangeHash .= implode(',', $rrule).';'; + $eventSignificantChangeHash .= implode(',', $rrule).';'; } else { foreach ($propertyValues as $val) { - $significantChangeHash .= $val->getValue().';'; + $eventSignificantChangeHash .= $val->getValue().';'; } } } } + $significantChangeEventProperties[] = $eventSignificantChangeHash; + } + + asort($significantChangeEventProperties); + + foreach ($significantChangeEventProperties as $eventSignificantChangeHash) { + $significantChangeHash .= $eventSignificantChangeHash; } $significantChangeHash = md5($significantChangeHash); diff --git a/lib/Node.php b/lib/Node.php index 154a7fac5..2041b2ac7 100644 --- a/lib/Node.php +++ b/lib/Node.php @@ -73,6 +73,7 @@ abstract public function serialize(); * * @return array */ + #[\ReturnTypeWillChange] abstract public function jsonSerialize(); /** @@ -102,6 +103,7 @@ public function destroy() * * @return ElementList */ + #[\ReturnTypeWillChange] public function getIterator() { if (!is_null($this->iterator)) { @@ -115,8 +117,6 @@ public function getIterator() * Sets the overridden iterator. * * Note that this is not actually part of the iterator interface - * - * @param ElementList $iterator */ public function setIterator(ElementList $iterator) { @@ -159,6 +159,7 @@ public function validate($options = 0) * * @return int */ + #[\ReturnTypeWillChange] public function count() { $it = $this->getIterator(); @@ -179,6 +180,7 @@ public function count() * * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { $iterator = $this->getIterator(); @@ -195,6 +197,7 @@ public function offsetExists($offset) * * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { $iterator = $this->getIterator(); @@ -210,6 +213,7 @@ public function offsetGet($offset) * @param int $offset * @param mixed $value */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { $iterator = $this->getIterator(); @@ -230,6 +234,7 @@ public function offsetSet($offset, $value) * * @param int $offset */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { $iterator = $this->getIterator(); diff --git a/lib/PHPUnitAssertions.php b/lib/PHPUnitAssertions.php index d77e4b1ed..45c0a21c6 100644 --- a/lib/PHPUnitAssertions.php +++ b/lib/PHPUnitAssertions.php @@ -15,7 +15,7 @@ trait PHPUnitAssertions { /** - * This method tests wether two vcards or icalendar objects are + * This method tests whether two vcards or icalendar objects are * semantically identical. * * It supports objects being supplied as strings, streams or @@ -34,8 +34,7 @@ trait PHPUnitAssertions */ public function assertVObjectEqualsVObject($expected, $actual, $message = '') { - $self = $this; - $getObj = function ($input) use ($self) { + $getObj = function ($input) { if (is_resource($input)) { $input = stream_get_contents($input); } diff --git a/lib/Parameter.php b/lib/Parameter.php index 2c9a8e7fd..5df6238e0 100644 --- a/lib/Parameter.php +++ b/lib/Parameter.php @@ -47,12 +47,12 @@ class Parameter extends Node * * It's recommended to use the create:: factory method instead. * - * @param string $name + * @param string|null $name * @param string $value */ public function __construct(Document $root, $name, $value = null) { - $this->name = strtoupper($name); + $this->name = is_null($name) ? '' : strtoupper($name); $this->root = $root; if (is_null($name)) { $this->noName = true; @@ -95,13 +95,11 @@ public static function guessParameterNameByValue($value) case 'WORK': case 'HOME': case 'PREF': - // Delivery Label Type case 'DOM': case 'INTL': case 'POSTAL': case 'PARCEL': - // Telephone types case 'VOICE': case 'FAX': @@ -113,7 +111,6 @@ public static function guessParameterNameByValue($value) case 'CAR': case 'ISDN': case 'VIDEO': - // EMAIL types (lol) case 'AOL': case 'APPLELINK': @@ -127,7 +124,6 @@ public static function guessParameterNameByValue($value) case 'PRODIGY': case 'TLX': case 'X400': - // Photo / Logo format types case 'GIF': case 'CGM': @@ -143,12 +139,10 @@ public static function guessParameterNameByValue($value) case 'MPEG2': case 'AVI': case 'QTIME': - // Sound Digital Audio Type case 'WAVE': case 'PCM': case 'AIFF': - // Key types case 'X509': case 'PGP': @@ -201,8 +195,6 @@ public function getValue() /** * Sets multiple values for this parameter. - * - * @param array $value */ public function setParts(array $value) { @@ -301,7 +293,7 @@ function ($out, $item) { // https://tools.ietf.org/html/rfc6868 // // But we've found that iCal (7.0, shipped with OSX 10.9) - // severaly trips on + characters not being quoted, so we + // severely trips on + characters not being quoted, so we // added + as well. if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { return $out.$item; @@ -329,6 +321,7 @@ function ($out, $item) { * * @return array */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->value; @@ -362,6 +355,7 @@ public function __toString() * * @return ElementList */ + #[\ReturnTypeWillChange] public function getIterator() { if (!is_null($this->iterator)) { diff --git a/lib/Parser/Json.php b/lib/Parser/Json.php index 3fd307e97..f33603207 100644 --- a/lib/Parser/Json.php +++ b/lib/Parser/Json.php @@ -7,6 +7,8 @@ use Sabre\VObject\Document; use Sabre\VObject\EofException; use Sabre\VObject\ParseException; +use Sabre\VObject\Property\FlatText; +use Sabre\VObject\Property\Text; /** * Json Parser. @@ -87,8 +89,6 @@ public function parse($input = null, $options = 0) /** * Parses a component. * - * @param array $jComp - * * @return \Sabre\VObject\Component */ public function parseComponent(array $jComp) @@ -124,8 +124,6 @@ function ($jComp) use ($self) { /** * Parses properties. * - * @param array $jProp - * * @return \Sabre\VObject\Property */ public function parseProperty(array $jProp) @@ -160,8 +158,8 @@ public function parseProperty(array $jProp) // represents TEXT values. We have to normalize these here. In the // future we can get rid of FlatText once we're allowed to break BC // again. - if ('Sabre\VObject\Property\FlatText' === $defaultPropertyClass) { - $defaultPropertyClass = 'Sabre\VObject\Property\Text'; + if (FlatText::class === $defaultPropertyClass) { + $defaultPropertyClass = Text::class; } // If the value type we received (e.g.: TEXT) was not the default value diff --git a/lib/Parser/MimeDir.php b/lib/Parser/MimeDir.php index 26a7101e5..b0b52d223 100644 --- a/lib/Parser/MimeDir.php +++ b/lib/Parser/MimeDir.php @@ -9,6 +9,7 @@ use Sabre\VObject\EofException; use Sabre\VObject\Node; use Sabre\VObject\ParseException; +use Sabre\VObject\Reader; /** * MimeDir parser. @@ -124,7 +125,7 @@ public function setInput($input) $this->startLine = 0; if (is_string($input)) { - // Convering to a stream. + // Converting to a stream. $stream = fopen('php://temp', 'r+'); fwrite($stream, $input); rewind($stream); @@ -200,15 +201,32 @@ protected function parseLine($line) } $component = $this->root->createComponent(substr($line, 6), [], false); + $prevNode = null; while (true) { // Reading until we hit END: $line = $this->readLine(); if ('END:' === strtoupper(substr($line, 0, 4))) { break; } - $result = $this->parseLine($line); + try { + $result = $this->parseLine($line); + } catch (\Exception $e) { + if (isset($prevNode) + && $e instanceof ParseException && str_contains($e->getMessage(), 'Invalid Mimedir file. Line starting at') + && ($this->options & Reader::OPTION_FIX_UNFOLDING) + ) { + // Fix unfolding + $component->remove($prevNode); + $value = $prevNode->getValue() . ' ' . $line . PHP_EOL; + $prevNode->offsetSet('VALUE', $value); + $prevNode->setValue($value); + $component->add($prevNode); + continue; + } + throw $e; + } if ($result) { - $component->add($result); + $prevNode = $component->add($result); } } @@ -343,7 +361,7 @@ protected function readProperty($line) ) (?=[;:,]) /xi"; - //echo $regex, "\n"; die(); + //echo $regex, "\n"; exit(); preg_match_all($regex, $line, $matches, PREG_SET_ORDER); $property = [ @@ -378,7 +396,7 @@ protected function readProperty($line) $property['parameters'][$lastParam] = $value; } elseif (is_array($property['parameters'][$lastParam])) { $property['parameters'][$lastParam][] = $value; - } else { + } elseif ($property['parameters'][$lastParam] !== $value) { $property['parameters'][$lastParam] = [ $property['parameters'][$lastParam], $value, @@ -439,7 +457,7 @@ protected function readProperty($line) $propObj->add(null, $namelessParameter); } - if ('QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) { + if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) { $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); } else { $charset = $this->charset; @@ -480,7 +498,7 @@ protected function readProperty($line) * vCard 3.0 says: * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be * escaped, all time time. - * * Comma's are used for delimeters in multiple values + * * Comma's are used for delimiters in multiple values * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, * as in some properties semi-colon is used for separators. * * Properties using semi-colons: N, ADR, GEO, ORG @@ -518,7 +536,7 @@ protected function readProperty($line) * * Now for the parameters * - * If delimiter is not set (null) this method will just return a string. + * If delimiter is not set (empty string) this method will just return a string. * If it's a comma or a semi-colon the string will be split on those * characters, and always return an array. * diff --git a/lib/Parser/XML.php b/lib/Parser/XML.php index 90f262d9e..78773173d 100644 --- a/lib/Parser/XML.php +++ b/lib/Parser/XML.php @@ -112,8 +112,6 @@ public function parse($input = null, $options = 0) /** * Parse a xCalendar component. - * - * @param Component $parentComponent */ protected function parseVCalendarComponents(Component $parentComponent) { @@ -134,8 +132,6 @@ protected function parseVCalendarComponents(Component $parentComponent) /** * Parse a xCard component. - * - * @param Component $parentComponent */ protected function parseVCardComponents(Component $parentComponent) { @@ -146,8 +142,7 @@ protected function parseVCardComponents(Component $parentComponent) /** * Parse xCalendar and xCard properties. * - * @param Component $parentComponent - * @param string $propertyNamePrefix + * @param string $propertyNamePrefix */ protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') { @@ -302,8 +297,6 @@ protected function parseProperties(Component $parentComponent, $propertyNamePref /** * Parse a component. - * - * @param Component $parentComponent */ protected function parseComponent(Component $parentComponent) { @@ -327,11 +320,10 @@ protected function parseComponent(Component $parentComponent) /** * Create a property. * - * @param Component $parentComponent - * @param string $name - * @param array $parameters - * @param string $type - * @param mixed $value + * @param string $name + * @param array $parameters + * @param string $type + * @param mixed $value */ protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) { @@ -359,9 +351,9 @@ public function setInput($input) if (is_string($input)) { $reader = new SabreXml\Reader(); $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period'] - = 'Sabre\VObject\Parser\XML\Element\KeyValue'; + = XML\Element\KeyValue::class; $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur'] - = 'Sabre\VObject\Parser\XML\Element\KeyValue'; + = XML\Element\KeyValue::class; $reader->xml($input); $input = $reader->parse(); } diff --git a/lib/Parser/XML/Element/KeyValue.php b/lib/Parser/XML/Element/KeyValue.php index e26540036..c0bbf0d9b 100644 --- a/lib/Parser/XML/Element/KeyValue.php +++ b/lib/Parser/XML/Element/KeyValue.php @@ -18,7 +18,7 @@ class KeyValue extends SabreXml\Element\KeyValue /** * The deserialize method is called during xml parsing. * - * This method is called staticly, this is because in theory this method + * This method is called statically, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are diff --git a/lib/Property.php b/lib/Property.php index 6105cb0f0..56096dafe 100644 --- a/lib/Property.php +++ b/lib/Property.php @@ -30,7 +30,7 @@ abstract class Property extends Node * * This is only used in vcards * - * @var string + * @var string|null */ public $group; @@ -52,7 +52,7 @@ abstract class Property extends Node * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ public $delimiter = ';'; @@ -123,8 +123,6 @@ public function getValue() /** * Sets a multi-valued property. - * - * @param array $parts */ public function setParts(array $parts) { @@ -262,8 +260,6 @@ public function getJsonValue() * Sets the JSON value, as it would appear in a jCard or jCal object. * * The value must always be an array. - * - * @param array $value */ public function setJsonValue(array $value) { @@ -280,6 +276,7 @@ public function setJsonValue(array $value) * * @return array */ + #[\ReturnTypeWillChange] public function jsonSerialize() { $parameters = []; @@ -309,8 +306,6 @@ public function jsonSerialize() /** * Hydrate data from a XML subtree, as it would appear in a xCard or xCal * object. - * - * @param array $value */ public function setXmlValue(array $value) { @@ -393,6 +388,7 @@ public function __toString() * * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($name) { if (is_int($name)) { @@ -419,6 +415,7 @@ public function offsetExists($name) * * @return Node */ + #[\ReturnTypeWillChange] public function offsetGet($name) { if (is_int($name)) { @@ -439,6 +436,7 @@ public function offsetGet($name) * @param string $name * @param mixed $value */ + #[\ReturnTypeWillChange] public function offsetSet($name, $value) { if (is_int($name)) { @@ -459,6 +457,7 @@ public function offsetSet($name, $value) * * @param string $name */ + #[\ReturnTypeWillChange] public function offsetUnset($name) { if (is_int($name)) { diff --git a/lib/Property/Binary.php b/lib/Property/Binary.php index 830dd9028..1262dd054 100644 --- a/lib/Property/Binary.php +++ b/lib/Property/Binary.php @@ -24,9 +24,9 @@ class Binary extends Property * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Updates the current value. @@ -100,8 +100,6 @@ public function getJsonValue() * Sets the json value, as it would appear in a jCard or jCal object. * * The value must always be an array. - * - * @param array $value */ public function setJsonValue(array $value) { diff --git a/lib/Property/Boolean.php b/lib/Property/Boolean.php index 1b219bb8c..4bd6ffdfe 100644 --- a/lib/Property/Boolean.php +++ b/lib/Property/Boolean.php @@ -2,13 +2,12 @@ namespace Sabre\VObject\Property; -use - Sabre\VObject\Property; +use Sabre\VObject\Property; /** * Boolean property. * - * This object represents BOOLEAN values. These are always the case-insenstive + * This object represents BOOLEAN values. These are always the case-insensitive * string TRUE or FALSE. * * Automatic conversion to PHP's true and false are done. @@ -59,8 +58,6 @@ public function getValueType() /** * Hydrate data from a XML subtree, as it would appear in a xCard or xCal * object. - * - * @param array $value */ public function setXmlValue(array $value) { diff --git a/lib/Property/FloatValue.php b/lib/Property/FloatValue.php index 208d74516..e780ae6c1 100644 --- a/lib/Property/FloatValue.php +++ b/lib/Property/FloatValue.php @@ -21,7 +21,7 @@ class FloatValue extends Property * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ public $delimiter = ';'; @@ -93,8 +93,6 @@ public function getJsonValue() /** * Hydrate data from a XML subtree, as it would appear in a xCard or xCal * object. - * - * @param array $value */ public function setXmlValue(array $value) { diff --git a/lib/Property/ICalendar/CalAddress.php b/lib/Property/ICalendar/CalAddress.php index e89bb31f9..2dbbc6eaf 100644 --- a/lib/Property/ICalendar/CalAddress.php +++ b/lib/Property/ICalendar/CalAddress.php @@ -2,8 +2,7 @@ namespace Sabre\VObject\Property\ICalendar; -use - Sabre\VObject\Property\Text; +use Sabre\VObject\Property\Text; /** * CalAddress property. @@ -20,9 +19,9 @@ class CalAddress extends Text * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Returns the type of value. diff --git a/lib/Property/ICalendar/DateTime.php b/lib/Property/ICalendar/DateTime.php index 7eb3e0bb7..afeb8ff81 100644 --- a/lib/Property/ICalendar/DateTime.php +++ b/lib/Property/ICalendar/DateTime.php @@ -38,8 +38,6 @@ class DateTime extends Property * Sets a multi-valued property. * * You may also specify DateTime objects here. - * - * @param array $parts */ public function setParts(array $parts) { @@ -133,9 +131,9 @@ public function isFloating() * * @return \DateTimeImmutable */ - public function getDateTime(DateTimeZone $timeZone = null) + public function getDateTime(DateTimeZone $timeZone = null, bool $activeCustomizedGuesser = true) { - $dt = $this->getDateTimes($timeZone); + $dt = $this->getDateTimes($timeZone, $activeCustomizedGuesser); if (!$dt) { return; } @@ -155,13 +153,13 @@ public function getDateTime(DateTimeZone $timeZone = null) * @return \DateTimeImmutable[] * @return \DateTime[] */ - public function getDateTimes(DateTimeZone $timeZone = null) + public function getDateTimes(DateTimeZone $timeZone = null, bool $activeCustomizedGuesser = true) { // Does the property have a TZID? $tzid = $this['TZID']; if ($tzid) { - $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); + $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root, true, $activeCustomizedGuesser); } $dts = []; @@ -175,7 +173,6 @@ public function getDateTimes(DateTimeZone $timeZone = null) /** * Sets the property as a DateTime object. * - * @param DateTimeInterface $dt * @param bool isFloating If set to true, timezones will be ignored */ public function setDateTime(DateTimeInterface $dt, $isFloating = false) @@ -187,7 +184,7 @@ public function setDateTime(DateTimeInterface $dt, $isFloating = false) * Sets the property as multiple date-time objects. * * The first value will be used as a reference for the timezones, and all - * the otehr values will be adjusted for that timezone + * the other values will be adjusted for that timezone * * @param DateTimeInterface[] $dt * @param bool isFloating If set to true, timezones will be ignored @@ -279,8 +276,6 @@ function (DateTimeInterface $dt) use ($hasTime, $isUtc) { * Sets the json value, as it would appear in a jCard or jCal object. * * The value must always be an array. - * - * @param array $value */ public function setJsonValue(array $value) { @@ -305,6 +300,7 @@ function ($item) { * @param string $name * @param mixed $value */ + #[\ReturnTypeWillChange] public function offsetSet($name, $value) { parent::offsetSet($name, $value); @@ -343,8 +339,8 @@ public function validate($options = 0) $messages = parent::validate($options); $valueType = $this->getValueType(); $values = $this->getParts(); - try { - foreach ($values as $value) { + foreach ($values as $value) { + try { switch ($valueType) { case 'DATE': DateTimeParser::parseDate($value); @@ -353,13 +349,14 @@ public function validate($options = 0) DateTimeParser::parseDateTime($value); break; } + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, + 'node' => $this, + ]; + break; } - } catch (InvalidDataException $e) { - $messages[] = [ - 'level' => 3, - 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, - 'node' => $this, - ]; } return $messages; diff --git a/lib/Property/ICalendar/Duration.php b/lib/Property/ICalendar/Duration.php index 87f008160..e18fe191e 100644 --- a/lib/Property/ICalendar/Duration.php +++ b/lib/Property/ICalendar/Duration.php @@ -22,7 +22,7 @@ class Duration extends Property * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ public $delimiter = ','; diff --git a/lib/Property/ICalendar/Period.php b/lib/Property/ICalendar/Period.php index 17bfa5c5c..ae8a78911 100644 --- a/lib/Property/ICalendar/Period.php +++ b/lib/Property/ICalendar/Period.php @@ -23,7 +23,7 @@ class Period extends Property * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ public $delimiter = ','; @@ -67,8 +67,6 @@ public function getValueType() * Sets the json value, as it would appear in a jCard or jCal object. * * The value must always be an array. - * - * @param array $value */ public function setJsonValue(array $value) { diff --git a/lib/Property/ICalendar/Recur.php b/lib/Property/ICalendar/Recur.php index baeda781e..3d632fec1 100644 --- a/lib/Property/ICalendar/Recur.php +++ b/lib/Property/ICalendar/Recur.php @@ -88,8 +88,6 @@ public function getValue() /** * Sets a multi-valued property. - * - * @param array $parts */ public function setParts(array $parts) { diff --git a/lib/Property/IntegerValue.php b/lib/Property/IntegerValue.php index ddd71d731..3ae775214 100644 --- a/lib/Property/IntegerValue.php +++ b/lib/Property/IntegerValue.php @@ -2,14 +2,13 @@ namespace Sabre\VObject\Property; -use - Sabre\VObject\Property; +use Sabre\VObject\Property; /** * Integer property. * * This object represents INTEGER values. These are always a single integer. - * They may be preceeded by either + or -. + * They may be preceded by either + or -. * * @copyright Copyright (C) fruux GmbH (https://fruux.com/) * @author Evert Pot (http://evertpot.com/) @@ -68,8 +67,6 @@ public function getJsonValue() /** * Hydrate data from a XML subtree, as it would appear in a xCard or xCal * object. - * - * @param array $value */ public function setXmlValue(array $value) { diff --git a/lib/Property/Text.php b/lib/Property/Text.php index 23c945551..ac8aa066b 100644 --- a/lib/Property/Text.php +++ b/lib/Property/Text.php @@ -111,7 +111,7 @@ public function setQuotedPrintableValue($val) // that. // // We also don't have to unescape \\, so all we need to look for is a ; - // that's not preceeded with a \. + // that's not preceded with a \. $regex = '# (?setValue($matches); diff --git a/lib/Property/Time.php b/lib/Property/Time.php index 7aeafc8d0..1b81609aa 100644 --- a/lib/Property/Time.php +++ b/lib/Property/Time.php @@ -19,9 +19,9 @@ class Time extends Text * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Returns the type of value. @@ -40,8 +40,6 @@ public function getValueType() * Sets the JSON value, as it would appear in a jCard or jCal object. * * The value must always be an array. - * - * @param array $value */ public function setJsonValue(array $value) { @@ -119,8 +117,6 @@ public function getJsonValue() /** * Hydrate data from a XML subtree, as it would appear in a xCard or xCal * object. - * - * @param array $value */ public function setXmlValue(array $value) { diff --git a/lib/Property/Uri.php b/lib/Property/Uri.php index 3449ba1f2..1ad1fb199 100644 --- a/lib/Property/Uri.php +++ b/lib/Property/Uri.php @@ -20,9 +20,9 @@ class Uri extends Text * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Returns the type of value. @@ -72,7 +72,7 @@ public function setRawMimeDirValue($val) { // Normally we don't need to do any type of unescaping for these // properties, however.. we've noticed that Google Contacts - // specifically escapes the colon (:) with a blackslash. While I have + // specifically escapes the colon (:) with a backslash. While I have // no clue why they thought that was a good idea, I'm unescaping it // anyway. // diff --git a/lib/Property/UtcOffset.php b/lib/Property/UtcOffset.php index 732239e23..04b88447f 100644 --- a/lib/Property/UtcOffset.php +++ b/lib/Property/UtcOffset.php @@ -17,9 +17,9 @@ class UtcOffset extends Text * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Returns the type of value. @@ -38,8 +38,6 @@ public function getValueType() * Sets the JSON value, as it would appear in a jCard or jCal object. * * The value must always be an array. - * - * @param array $value */ public function setJsonValue(array $value) { diff --git a/lib/Property/VCard/Date.php b/lib/Property/VCard/Date.php index a018ccbb8..fc679d572 100644 --- a/lib/Property/VCard/Date.php +++ b/lib/Property/VCard/Date.php @@ -28,8 +28,6 @@ public function getValueType() /** * Sets the property as a DateTime object. - * - * @param \DateTimeInterface $dt */ public function setDateTime(\DateTimeInterface $dt) { diff --git a/lib/Property/VCard/DateAndOrTime.php b/lib/Property/VCard/DateAndOrTime.php index b7e17492a..7bf79c48c 100644 --- a/lib/Property/VCard/DateAndOrTime.php +++ b/lib/Property/VCard/DateAndOrTime.php @@ -24,9 +24,9 @@ class DateAndOrTime extends Property /** * Field separator. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Returns the type of value. @@ -45,8 +45,6 @@ public function getValueType() * Sets a multi-valued property. * * You may also specify DateTimeInterface objects here. - * - * @param array $parts */ public function setParts(array $parts) { @@ -80,8 +78,6 @@ public function setValue($value) /** * Sets the property as a DateTime object. - * - * @param DateTimeInterface $dt */ public function setDateTime(DateTimeInterface $dt) { diff --git a/lib/Property/VCard/LanguageTag.php b/lib/Property/VCard/LanguageTag.php index 697273989..318ea0231 100644 --- a/lib/Property/VCard/LanguageTag.php +++ b/lib/Property/VCard/LanguageTag.php @@ -2,8 +2,7 @@ namespace Sabre\VObject\Property\VCard; -use - Sabre\VObject\Property; +use Sabre\VObject\Property; /** * LanguageTag property. diff --git a/lib/Property/VCard/PhoneNumber.php b/lib/Property/VCard/PhoneNumber.php new file mode 100644 index 000000000..b714ffd03 --- /dev/null +++ b/lib/Property/VCard/PhoneNumber.php @@ -0,0 +1,30 @@ + + */ +class PhoneNumber extends Property\Text +{ + protected $structuredValues = []; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PHONE-NUMBER'; + } +} diff --git a/lib/Property/VCard/TimeStamp.php b/lib/Property/VCard/TimeStamp.php index fccf2d600..da6ea3d44 100644 --- a/lib/Property/VCard/TimeStamp.php +++ b/lib/Property/VCard/TimeStamp.php @@ -21,9 +21,9 @@ class TimeStamp extends Text * In case this is a multi-value property. This string will be used as a * delimiter. * - * @var string|null + * @var string */ - public $delimiter = null; + public $delimiter = ''; /** * Returns the type of value. diff --git a/lib/Reader.php b/lib/Reader.php index 055d546a5..a68c1af47 100644 --- a/lib/Reader.php +++ b/lib/Reader.php @@ -26,6 +26,11 @@ class Reader */ const OPTION_IGNORE_INVALID_LINES = 2; + /** + * If this option is turned on, it will fix unfolding parse error by adding empty space. + */ + const OPTION_FIX_UNFOLDING = 4; + /** * Parses a vCard or iCalendar object, and returns the top component. * diff --git a/lib/Recur/EventIterator.php b/lib/Recur/EventIterator.php index 135ecf00e..61f05d7de 100644 --- a/lib/Recur/EventIterator.php +++ b/lib/Recur/EventIterator.php @@ -83,7 +83,7 @@ class EventIterator implements \Iterator * 2. You can pass an array of VEVENTs (all UIDS should match). * 3. You can pass a single VEVENT component. * - * Only the second method is recomended. The other 1 and 3 will be removed + * Only the second method is recommended. The other 1 and 3 will be removed * at some point in the future. * * The $uid parameter is only required for the first method. @@ -198,6 +198,7 @@ public function __construct($input, $uid = null, DateTimeZone $timeZone = null) * * @return DateTimeImmutable */ + #[\ReturnTypeWillChange] public function current() { if ($this->currentDate) { @@ -229,9 +230,13 @@ public function getDtEnd() if (!$this->valid()) { return; } - $end = clone $this->currentDate; + if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { + return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); + } else { + $end = clone $this->currentDate; - return $end->modify('+'.$this->eventDuration.' seconds'); + return $end->modify('+'.$this->eventDuration.' seconds'); + } } /** @@ -281,6 +286,7 @@ public function getEventObject() * * @return int */ + #[\ReturnTypeWillChange] public function key() { // The counter is always 1 ahead. @@ -293,6 +299,7 @@ public function key() * * @return bool */ + #[\ReturnTypeWillChange] public function valid() { if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) { @@ -304,7 +311,10 @@ public function valid() /** * Sets the iterator back to the starting point. + * + * @return void */ + #[\ReturnTypeWillChange] public function rewind() { $this->recurIterator->rewind(); @@ -327,7 +337,10 @@ public function rewind() /** * Advances the iterator with one step. + * + * @return void */ + #[\ReturnTypeWillChange] public function next() { $this->currentOverriddenEvent = null; @@ -380,8 +393,6 @@ public function next() /** * Quickly jump to a date in the future. - * - * @param DateTimeInterface $dateTime */ public function fastForward(DateTimeInterface $dateTime) { diff --git a/lib/Recur/NoInstancesException.php b/lib/Recur/NoInstancesException.php index b55af567d..348c02306 100644 --- a/lib/Recur/NoInstancesException.php +++ b/lib/Recur/NoInstancesException.php @@ -7,7 +7,7 @@ /** * This exception gets thrown when a recurrence iterator produces 0 instances. * - * This may happen when every occurence in a rrule is also in EXDATE. + * This may happen when every occurrence in a rrule is also in EXDATE. * * @copyright Copyright (C) fruux GmbH (https://fruux.com/) * @author Evert Pot (http://evertpot.com/) diff --git a/lib/Recur/RDateIterator.php b/lib/Recur/RDateIterator.php index 013694b95..5d56657fa 100644 --- a/lib/Recur/RDateIterator.php +++ b/lib/Recur/RDateIterator.php @@ -24,8 +24,7 @@ class RDateIterator implements Iterator /** * Creates the Iterator. * - * @param string|array $rrule - * @param DateTimeInterface $start + * @param string|array $rrule */ public function __construct($rrule, DateTimeInterface $start) { @@ -36,6 +35,7 @@ public function __construct($rrule, DateTimeInterface $start) /* Implementation of the Iterator interface {{{ */ + #[\ReturnTypeWillChange] public function current() { if (!$this->valid()) { @@ -50,6 +50,7 @@ public function current() * * @return int */ + #[\ReturnTypeWillChange] public function key() { return $this->counter; @@ -61,6 +62,7 @@ public function key() * * @return bool */ + #[\ReturnTypeWillChange] public function valid() { return $this->counter <= count($this->dates); @@ -68,7 +70,10 @@ public function valid() /** * Resets the iterator. + * + * @return void */ + #[\ReturnTypeWillChange] public function rewind() { $this->currentDate = clone $this->startDate; @@ -77,7 +82,10 @@ public function rewind() /** * Goes on to the next iteration. + * + * @return void */ + #[\ReturnTypeWillChange] public function next() { ++$this->counter; @@ -107,8 +115,6 @@ public function isInfinite() /** * This method allows you to quickly go to the next occurrence after the * specified date. - * - * @param DateTimeInterface $dt */ public function fastForward(DateTimeInterface $dt) { diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 75342a2a8..d09838398 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -2,7 +2,6 @@ namespace Sabre\VObject\Recur; -use DateTimeImmutable; use DateTimeInterface; use Iterator; use Sabre\VObject\DateTimeParser; @@ -27,8 +26,7 @@ class RRuleIterator implements Iterator /** * Creates the Iterator. * - * @param string|array $rrule - * @param DateTimeInterface $start + * @param string|array $rrule */ public function __construct($rrule, DateTimeInterface $start) { @@ -39,6 +37,7 @@ public function __construct($rrule, DateTimeInterface $start) /* Implementation of the Iterator interface {{{ */ + #[\ReturnTypeWillChange] public function current() { if (!$this->valid()) { @@ -53,6 +52,7 @@ public function current() * * @return int */ + #[\ReturnTypeWillChange] public function key() { return $this->counter; @@ -65,6 +65,7 @@ public function key() * * @return bool */ + #[\ReturnTypeWillChange] public function valid() { if (null === $this->currentDate) { @@ -79,7 +80,10 @@ public function valid() /** * Resets the iterator. + * + * @return void */ + #[\ReturnTypeWillChange] public function rewind() { $this->currentDate = clone $this->startDate; @@ -88,30 +92,30 @@ public function rewind() /** * Goes on to the next iteration. + * + * @param int $amount + * @return void */ - public function next() + #[\ReturnTypeWillChange] + public function next(int $amount = 1) { // Otherwise, we find the next event in the normal RRULE // sequence. switch ($this->frequency) { case 'hourly': - $this->nextHourly(); + $this->nextHourly($amount); break; - case 'daily': - $this->nextDaily(); + $this->nextDaily($amount); break; - case 'weekly': - $this->nextWeekly(); + $this->nextWeekly($amount); break; - case 'monthly': - $this->nextMonthly(); + $this->nextMonthly($amount); break; - case 'yearly': - $this->nextYearly(); + $this->nextYearly($amount); break; } ++$this->counter; @@ -132,13 +136,165 @@ public function isInfinite() /** * This method allows you to quickly go to the next occurrence after the * specified date. - * - * @param DateTimeInterface $dt */ public function fastForward(DateTimeInterface $dt) { + // We don't do any jumps if we have a count limit as we have to keep track of the number of occurrences + if (!isset($this->count)) { + $this->jumpForward($dt); + } + + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * This method allows you to quickly go to the next occurrence before the specified date. + */ + public function fastForwardBefore(DateTimeInterface $dt) + { + $hasCount = isset($this->count); + + // We don't do any jumps if we have a count limit as we have to keep track of the number of occurrences + if (!$hasCount) { + $this->jumpForward($dt); + } + + $previousDate = null; while ($this->valid() && $this->currentDate < $dt) { + $previousDate = clone $this->currentDate; + $this->next(); + } + + if (isset($previousDate)) { + $this->currentDate = $previousDate; + $hasCount && $this->counter--; + } + } + + /** + * This method allows you to quickly go to the last occurrence. + */ + public function fastForwardToEnd() + { + if ($this->isInfinite()) { + throw new \LogicException('Cannot fast forward to the end an infinite event.'); + } + + $hasCount = isset($this->count); + + if (isset($this->until) && !$hasCount) { + $this->jumpForward($this->until); + } + + // We fast forward until the last event occurrence + $previous = clone $this->currentDate; + while ($this->valid()) { + $previous = clone $this->currentDate; + $this->next(); + } + + $hasCount && $this->counter--; + $this->currentDate = $previous; + } + + public function getCount() + { + return $this->count; + } + + public function getInterval() + { + return $this->interval; + } + + public function getUntil() + { + return $this->until; + } + + public function getFrequency() + { + return $this->frequency; + } + + /** + * Return the frequency in number of days. + * + * @return float|int|null + */ + private function getFrequencyCoeff() + { + $frequencyCoeff = null; + + switch ($this->frequency) { + case 'hourly': + $frequencyCoeff = 1 / 24; + break; + case 'daily': + $frequencyCoeff = 1; + break; + case 'weekly': + $frequencyCoeff = 7; + break; + case 'monthly': + $frequencyCoeff = 30; + break; + case 'yearly': + $frequencyCoeff = 365; + break; + } + + return $frequencyCoeff; + } + + /** + * Perform a fast forward by doing jumps based on the distance of the requested date and the frequency of the + * recurrence rule. Will set the position of the iterator to the last occurrence before the requested date. If the + * fast forwarding failed, the position will be reset. + */ + private function jumpForward(DateTimeInterface $dt) + { + $frequencyCoeff = $this->getFrequencyCoeff(); + + do { + // We estimate the number of jumps to reach $dt. This is an estimate as the number of generated event within + // a frequency interval is assumed to be 1 (in reality, it could be anything >= 0) + $diff = $this->currentDate->diff($dt); + $estimatedOccurrences = $diff->days / $frequencyCoeff; + $estimatedOccurrences /= $this->interval; + + // We want to do small jumps to not overshot + $jumpSize = floor($estimatedOccurrences / 4); + $jumpSize = (int) max(1, $jumpSize); + + // If we are too close to the desired occurrence, we abort the jumping + if ($jumpSize <= 4) { + break; + } + + do { + $previousDate = clone $this->currentDate; + $this->next($jumpSize); + } while ($this->valid() && $this->currentDate < $dt); + + $this->currentDate = clone $previousDate; + // Do one step to avoid deadlock $this->next(); + } while ($this->valid() && $this->currentDate < $dt); + + // We undo the last next as it made the $this->currentDate < $dt false + // we want the last that validate it. + isset($previousDate) && $this->currentDate = clone $previousDate; + + // We don't know the counter at this point anymore + $this->counter = NAN; + + // It's possible that we miss the previous occurrence by jumping too much, in this case we reset the rrule and + // do the normal forward. + if ($this->currentDate >= $dt) { + $this->rewind(); } } @@ -232,7 +388,7 @@ public function fastForward(DateTimeInterface $dt) * * This is an array of weekdays * - * This may also be preceeded by a positive or negative integer. If present, + * This may also be preceded by a positive or negative integer. If present, * this indicates the nth occurrence of a specific day within the monthly or * yearly rrule. For instance, -2TU indicates the second-last tuesday of * the month, or year. @@ -309,30 +465,33 @@ public function fastForward(DateTimeInterface $dt) /** * Does the processing for advancing the iterator for hourly frequency. */ - protected function nextHourly() + protected function nextHourly($amount = 1) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->currentDate = $this->currentDate->modify('+'.$amount * $this->interval.' hours'); } /** * Does the processing for advancing the iterator for daily frequency. */ - protected function nextDaily() + protected function nextDaily($amount = 1) { if (!$this->byHour && !$this->byDay) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $this->currentDate = $this->currentDate->modify('+'.$amount * $this->interval.' days'); return; } + $recurrenceHours = []; if (!empty($this->byHour)) { $recurrenceHours = $this->getHours(); } + $recurrenceDays = []; if (!empty($this->byDay)) { $recurrenceDays = $this->getDays(); } + $recurrenceMonths = []; if (!empty($this->byMonth)) { $recurrenceMonths = $this->getMonths(); } @@ -341,12 +500,14 @@ protected function nextDaily() if ($this->byHour) { if ('23' == $this->currentDate->format('G')) { // to obey the interval rule - $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); + $this->currentDate = $this->currentDate->modify('+'.(($amount * $this->interval) - 1).' days'); + $amount = 1; } $this->currentDate = $this->currentDate->modify('+1 hours'); } else { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' days'); + $amount = 1; } // Current month of the year @@ -367,18 +528,20 @@ protected function nextDaily() /** * Does the processing for advancing the iterator for weekly frequency. */ - protected function nextWeekly() + protected function nextWeekly($amount = 1) { if (!$this->byHour && !$this->byDay) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' weeks'); return; } + $recurrenceHours = []; if ($this->byHour) { $recurrenceHours = $this->getHours(); } + $recurrenceDays = []; if ($this->byDay) { $recurrenceDays = $this->getDays(); } @@ -401,8 +564,8 @@ protected function nextWeekly() // We need to roll over to the next week if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { - $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); - + $this->currentDate = $this->currentDate->modify('+'.(($amount * $this->interval) - 1).' weeks'); + $amount = 1; // We need to go to the first day of this week, but only if we // are not already on this first day of this week. if ($this->currentDate->format('w') != $firstDay) { @@ -417,17 +580,20 @@ protected function nextWeekly() /** * Does the processing for advancing the iterator for monthly frequency. */ - protected function nextMonthly() + protected function nextMonthly($amount = 1) { $currentDayOfMonth = $this->currentDate->format('j'); + $currentHourOfMonth = $this->currentDate->format('G'); + $currentMinuteOfMonth = $this->currentDate->format('i'); + $currentSecondOfMonth = $this->currentDate->format('s'); if (!$this->byMonthDay && !$this->byDay) { // If the current day is higher than the 28th, rollover can // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' months'); } else { - $increase = 0; + $increase = $amount - 1; do { ++$increase; $tempDate = clone $this->currentDate; @@ -439,13 +605,30 @@ protected function nextMonthly() return; } + $occurrence = -1; while (true) { $occurrences = $this->getMonthlyOccurrences(); foreach ($occurrences as $occurrence) { - // The first occurrence thats higher than the current + // The first occurrence that's higher than the current // day of the month wins. - if ($occurrence > $currentDayOfMonth) { + if ($occurrence[0] > $currentDayOfMonth) { + break 2; + } elseif ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } elseif ($occurrence[1] < $currentHourOfMonth) { + continue; + } + + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } elseif ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { break 2; } } @@ -453,18 +636,36 @@ protected function nextMonthly() // If we made it all the way here, it means there were no // valid occurrences, and we need to advance to the next // month. - // - // This line does not currently work in hhvm. Temporary workaround - // follows: - // $this->currentDate->modify('first day of this month'); - $this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); + $this->currentDate = $this->currentDate->setDate( + (int) $this->currentDate->format('Y'), + (int) $this->currentDate->format('n'), + 1 + ); // end of workaround - $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months'); + $this->currentDate = $this->currentDate->modify('+ '.($amount * $this->interval).' months'); + $amount = 1; // This goes to 0 because we need to start counting at the // beginning. $currentDayOfMonth = 0; + // For some reason the "until" parameter was not being used here, + // that's why the workaround of the 10000 year bug was needed at all + $currentHourOfMonth = 0; + $currentMinuteOfMonth = 0; + $currentSecondOfMonth = 0; + + // For some reason the "until" parameter was not being used here, + // that's why the workaround of the 10000 year bug was needed at all + // let's stop it before the "until" parameter date + if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) { + return; + } + // let's stop it before the "until" parameter date + if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) { + return; + } + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... if ($this->currentDate->getTimestamp() > 253402300799) { @@ -477,18 +678,21 @@ protected function nextMonthly() $this->currentDate = $this->currentDate->setDate( (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), - (int) $occurrence - ); + $occurrence[0] + )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); } /** * Does the processing for advancing the iterator for yearly frequency. */ - protected function nextYearly() + protected function nextYearly($amount = 1) { - $currentMonth = $this->currentDate->format('n'); $currentYear = $this->currentDate->format('Y'); + $currentMonth = $this->currentDate->format('n'); $currentDayOfMonth = $this->currentDate->format('j'); + $currentHourOfMonth = $this->currentDate->format('G'); + $currentMinuteOfMonth = $this->currentDate->format('i'); + $currentSecondOfMonth = $this->currentDate->format('s'); // No sub-rules, so we just advance by year if (empty($this->byMonth)) { @@ -533,7 +737,7 @@ protected function nextYearly() foreach ($this->byWeekNo as $byWeekNo) { foreach ($dayOffsets as $dayOffset) { $date = clone $this->currentDate; - $date->setISODate($currentYear, $byWeekNo, $dayOffset); + $date = $date->setISODate($currentYear, $byWeekNo, $dayOffset); if ($date > $this->currentDate) { $checkDates[] = $date; @@ -548,7 +752,8 @@ protected function nextYearly() } // if there is no date found, check the next year - $currentYear += $this->interval; + $currentYear += $amount * $this->interval; + $amount = 1; } } @@ -570,11 +775,12 @@ protected function nextYearly() // loop through all YearDay and Days to check all the combinations foreach ($this->byYearDay as $byYearDay) { $date = clone $this->currentDate; - $date = $date->setDate($currentYear, 1, 1); if ($byYearDay > 0) { - $date = $date->add(new \DateInterval('P'.$byYearDay.'D')); + $date = $date->setDate($currentYear, 1, 1); + $date = $date->add(new \DateInterval('P'.($byYearDay - 1).'D')); } else { - $date = $date->sub(new \DateInterval('P'.abs($byYearDay).'D')); + $date = $date->setDate($currentYear, 12, 31); + $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D')); } if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) { @@ -589,35 +795,50 @@ protected function nextYearly() } // if there is no date found, check the next year - $currentYear += $this->interval; + $currentYear += ($amount * $this->interval); + $amount = 1; } } // The easiest form - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' years'); return; } - $currentMonth = $this->currentDate->format('n'); - $currentYear = $this->currentDate->format('Y'); - $currentDayOfMonth = $this->currentDate->format('j'); - $advancedToNewMonth = false; // If we got a byDay or getMonthDay filter, we must first expand // further. if ($this->byDay || $this->byMonthDay) { + $occurrence = -1; while (true) { - $occurrences = $this->getMonthlyOccurrences(); - - foreach ($occurrences as $occurrence) { - // The first occurrence that's higher than the current - // day of the month wins. - // If we advanced to the next month or year, the first - // occurrence is always correct. - if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { - break 2; + // If the start date is incorrect we must directly jump to the next value + if (in_array($currentMonth, $this->byMonth)) { + $occurrences = $this->getMonthlyOccurrences(); + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence[0] > $currentDayOfMonth || $advancedToNewMonth) { + break 2; + } elseif ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } elseif ($occurrence[1] < $currentHourOfMonth) { + continue; + } + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } elseif ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { + break 2; + } } } @@ -628,7 +849,8 @@ protected function nextYearly() do { ++$currentMonth; if ($currentMonth > 12) { - $currentYear += $this->interval; + $currentYear += ($amount * $this->interval); + $amount = 1; $currentMonth = 1; } } while (!in_array($currentMonth, $this->byMonth)); @@ -644,8 +866,8 @@ protected function nextYearly() $this->currentDate = $this->currentDate->setDate( (int) $currentYear, (int) $currentMonth, - (int) $occurrence - ); + (int) $occurrence[0] + )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); return; } else { @@ -713,7 +935,6 @@ protected function parseRRule($rrule) break; case 'INTERVAL': - case 'COUNT': $val = (int) $value; if ($val < 1) { @@ -747,6 +968,15 @@ protected function parseRRule($rrule) case 'BYMONTHDAY': $this->byMonthDay = (array) $value; + foreach ($this->byMonthDay as $byMonthDay) { + if (!is_numeric($byMonthDay)) { + throw new InvalidDataException('BYMONTHDAY in RRULE has a not numeric value(s)!'); + } + $byMonthDay = (int) $byMonthDay; + if ($byMonthDay < -31 || 0 === $byMonthDay || $byMonthDay > 31) { + throw new InvalidDataException('BYMONTHDAY in RRULE must have value(s) from 1 to 31, or -31 to -1!'); + } + } break; case 'BYYEARDAY': @@ -771,7 +1001,7 @@ protected function parseRRule($rrule) $this->byMonth = (array) $value; foreach ($this->byMonth as $byMonth) { if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) { - throw new InvalidDataException('BYMONTH in RRULE must have value(s) betweeen 1 and 12!'); + throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!'); } } break; @@ -788,6 +1018,23 @@ protected function parseRRule($rrule) throw new InvalidDataException('Not supported: '.strtoupper($key)); } } + + // FREQ is mandatory + if (!isset($this->frequency)) { + throw new InvalidDataException('Unknown value for FREQ'); + } + + if (isset($this->count) && isset($this->until)) { + throw new InvalidDataException('Can not have both UNTIL and COUNT property at the same time'); + } + + if ( + (isset($this->byWeekNo) && $this->frequency !== 'yearly') || + (isset($this->byYearDay) && in_array($this->frequency, ['daily', 'weekly', 'monthly'], true)) || + (isset($this->byMonthDay) && $this->frequency === 'weekly') + ) { + throw new InvalidDataException('Invalid combination of FREQ with BY rules'); + } } /** @@ -809,7 +1056,8 @@ protected function parseRRule($rrule) * Returns all the occurrences for a monthly frequency with a 'byDay' or * 'byMonthDay' expansion for the current month. * - * The returned list is an array of integers with the day of month (1-31). + * The returned list is an array of arrays with as first element the day of month (1-31); + * the hour; the minute and second of the occurence * * @return array */ @@ -873,7 +1121,7 @@ protected function getMonthlyOccurrences() foreach ($this->byMonthDay as $monthDay) { // Removing values that are out of range for this month if ($monthDay > $startDate->format('t') || - $monthDay < 0 - $startDate->format('t')) { + $monthDay < 0 - $startDate->format('t')) { continue; } if ($monthDay > 0) { @@ -895,8 +1143,23 @@ protected function getMonthlyOccurrences() } else { $result = $byDayResults; } - $result = array_unique($result); - sort($result, SORT_NUMERIC); + + $result = $this->addDailyOccurences($result); + $result = array_unique($result, SORT_REGULAR); + $sortLex = function ($a, $b) { + if ($a[0] != $b[0]) { + return $a[0] - $b[0]; + } + if ($a[1] != $b[1]) { + return $a[1] - $b[1]; + } + if ($a[2] != $b[2]) { + return $a[2] - $b[2]; + } + + return $a[3] - $b[3]; + }; + usort($result, $sortLex); // The last thing that needs checking is the BYSETPOS. If it's set, it // means only certain items in the set survive the filter. @@ -914,11 +1177,40 @@ protected function getMonthlyOccurrences() } } - sort($filteredResult, SORT_NUMERIC); + usort($result, $sortLex); return $filteredResult; } + /** + * Expends daily occurrences to an array of days that an event occurs on. + * + * @param array $result an array of integers with the day of month (1-31); + * + * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence + */ + protected function addDailyOccurences(array $result) + { + $output = []; + $hour = (int) $this->currentDate->format('G'); + $minute = (int) $this->currentDate->format('i'); + $second = (int) $this->currentDate->format('s'); + foreach ($result as $day) { + $seconds = $this->bySecond ? $this->bySecond : [$second]; + $minutes = $this->byMinute ? $this->byMinute : [$minute]; + $hours = $this->byHour ? $this->byHour : [$hour]; + foreach ($hours as $h) { + foreach ($minutes as $m) { + foreach ($seconds as $s) { + $output[] = [(int) $day, (int) $h, (int) $m, (int) $s]; + } + } + } + } + + return $output; + } + /** * Simple mapping from iCalendar day names to day numbers. * @@ -948,7 +1240,7 @@ protected function getDays() { $recurrenceDays = []; foreach ($this->byDay as $byDay) { - // The day may be preceeded with a positive (+n) or + // The day may be preceded with a positive (+n) or // negative (-n) integer. However, this does not make // sense in 'weekly' so we ignore it here. $recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; diff --git a/lib/Settings.php b/lib/Settings.php index afc586b0c..b0bb80a82 100644 --- a/lib/Settings.php +++ b/lib/Settings.php @@ -46,7 +46,7 @@ class Settings * specific events that recur many, many times, potentially DDOSing the * server. * - * The default (3500) allows creation of a dialy event that goes on for 10 + * The default (3500) allows creation of a daily event that goes on for 10 * years, which is hopefully long enough for most. * * Set this value to -1 to disable this control altogether. diff --git a/lib/TimeZoneUtil.php b/lib/TimeZoneUtil.php index 5b1a775c2..c81b96a1e 100644 --- a/lib/TimeZoneUtil.php +++ b/lib/TimeZoneUtil.php @@ -2,6 +2,21 @@ namespace Sabre\VObject; +use DateTimeZone; +use InvalidArgumentException; +use Sabre\VObject\TimezoneGuesser\FindFromMzVersionTimezone; +use Sabre\VObject\TimezoneGuesser\FindFromOffset; +use Sabre\VObject\TimezoneGuesser\FindFromOffsetName; +use Sabre\VObject\TimezoneGuesser\FindFromOutlookCities; +use Sabre\VObject\TimezoneGuesser\FindFromTimezoneIdentifier; +use Sabre\VObject\TimezoneGuesser\FindFromTimezoneMap; +use Sabre\VObject\TimezoneGuesser\GuessFromLicEntry; +use Sabre\VObject\TimezoneGuesser\GuessFromMsTzId; +use Sabre\VObject\TimezoneGuesser\GuessFromCustomizedTimeZone; +use Sabre\VObject\TimezoneGuesser\LowercaseTimezoneIdentifier; +use Sabre\VObject\TimezoneGuesser\TimezoneFinder; +use Sabre\VObject\TimezoneGuesser\TimezoneGuesser; + /** * Time zone name translation. * @@ -14,17 +29,150 @@ */ class TimeZoneUtil { + /** @var self */ + private static $instance = null; + + /** @var TimezoneGuesser[] */ + private $timezoneGuessers = []; + + /** @var TimezoneFinder[] */ + private $timezoneFinders = []; + + private function __construct() + { + $this->addGuesser('lic', new GuessFromLicEntry()); + $this->addGuesser('msTzId', new GuessFromMsTzId()); + $this->addFinder('tzid', new FindFromTimezoneIdentifier()); + $this->addFinder('tzmap', new FindFromTimezoneMap()); + $this->addFinder('offset', new FindFromOffset()); + $this->addFinder('lowercase', new LowercaseTimezoneIdentifier()); + $this->addFinder('outlookCities', new FindFromOutlookCities()); + $this->addFinder('version', new FindFromMzVersionTimezone()); + $this->addFinder('offsetName', new FindFromOffsetName()); + } + + private static function getInstance(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + private function addGuesser(string $key, TimezoneGuesser $guesser): void + { + $this->timezoneGuessers[$key] = $guesser; + } + + private function addFinder(string $key, TimezoneFinder $finder): void + { + $this->timezoneFinders[$key] = $finder; + } + + /** + * This method will try to find out the correct timezone for an iCalendar + * date-time value. + * + * You must pass the contents of the TZID parameter, as well as the full + * calendar. + * + * If the lookup fails, this method will return the default PHP timezone + * (as configured using date_default_timezone_set, or the date.timezone ini + * setting). + * + * Alternatively, if $failIfUncertain is set to true, it will throw an + * exception if we cannot accurately determine the timezone. + */ + private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false, bool $activeCustomizedGuesser = false): DateTimeZone + { + foreach ($this->timezoneFinders as $timezoneFinder) { + $timezone = $timezoneFinder->find($tzid, $failIfUncertain); + if (!$timezone instanceof DateTimeZone) { + continue; + } + + return $timezone; + } + + if (!$activeCustomizedGuesser) { + unset($this->timezoneGuessers['customized']); + } + + if ($vcalendar) { + // We temporary add the customized timezone guesser if needed + $guessers = $this->timezoneGuessers; + if ($activeCustomizedGuesser) { + $guessers[] = new GuessFromCustomizedTimeZone(); + } + + // If that didn't work, we will scan VTIMEZONE objects + foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { + if ((string) $vtimezone->TZID === $tzid) { + foreach ($guessers as $timezoneGuesser) { + $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain); + if (!$timezone instanceof DateTimeZone) { + continue; + } + + return $timezone; + } + } + } + } + + if ($failIfUncertain) { + throw new InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid); + } + + // If we got all the way here, we default to whatever has been set as the PHP default timezone. + return new DateTimeZone(date_default_timezone_get()); + } + + public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void + { + self::getInstance()->addGuesser($key, $guesser); + } + + public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void + { + self::getInstance()->addFinder($key, $finder); + } + + /** + * @param string $tzid + * @param false $failIfUncertain + * + * @return DateTimeZone + */ + public static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false, bool $activeCustomizedGuesser = true) + { + return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain, $activeCustomizedGuesser); + } + + public static function clean(): void + { + self::$instance = null; + } + + // Keeping things for backwards compatibility + /** + * @var array|null + * + * @deprecated + */ public static $map = null; /** * List of microsoft exchange timezone ids. * * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + * + * @deprecated */ public static $microsoftExchangeMap = [ 0 => 'UTC', 31 => 'Africa/Casablanca', - // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. // I'm not even kidding.. We handle this special case in the // getTimeZone method. @@ -80,11 +228,11 @@ class TimeZoneUtil 29 => 'Atlantic/Azores', 53 => 'Atlantic/Cape_Verde', 30 => 'America/Noronha', - 8 => 'America/Sao_Paulo', // Best guess + 8 => 'America/Sao_Paulo', // Best guess 32 => 'America/Argentina/Buenos_Aires', - 60 => 'America/Godthab', + 60 => 'America/Nuuk', 28 => 'America/St_Johns', - 9 => 'America/Halifax', + 9 => 'America/Halifax', 33 => 'America/Caracas', 65 => 'America/Santiago', 35 => 'America/Bogota', @@ -103,135 +251,11 @@ class TimeZoneUtil 39 => 'Pacific/Kwajalein', ]; - /** - * This method will try to find out the correct timezone for an iCalendar - * date-time value. - * - * You must pass the contents of the TZID parameter, as well as the full - * calendar. - * - * If the lookup fails, this method will return the default PHP timezone - * (as configured using date_default_timezone_set, or the date.timezone ini - * setting). - * - * Alternatively, if $failIfUncertain is set to true, it will throw an - * exception if we cannot accurately determine the timezone. - * - * @param string $tzid - * @param Sabre\VObject\Component $vcalendar - * - * @return \DateTimeZone - */ - public static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) - { - // First we will just see if the tzid is a support timezone identifier. - // - // The only exception is if the timezone starts with (. This is to - // handle cases where certain microsoft products generate timezone - // identifiers that for instance look like: - // - // (GMT+01.00) Sarajevo/Warsaw/Zagreb - // - // Since PHP 5.5.10, the first bit will be used as the timezone and - // this method will return just GMT+01:00. This is wrong, because it - // doesn't take DST into account. - if ('(' !== $tzid[0]) { - // PHP has a bug that logs PHP warnings even it shouldn't: - // https://bugs.php.net/bug.php?id=67881 - // - // That's why we're checking if we'll be able to successfull instantiate - // \DateTimeZone() before doing so. Otherwise we could simply instantiate - // and catch the exception. - $tzIdentifiers = \DateTimeZone::listIdentifiers(); - - try { - if ( - (in_array($tzid, $tzIdentifiers)) || - (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) || - (in_array($tzid, self::getIdentifiersBC())) - ) { - return new \DateTimeZone($tzid); - } - } catch (\Exception $e) { - } - } - - self::loadTzMaps(); - - // Next, we check if the tzid is somewhere in our tzid map. - if (isset(self::$map[$tzid])) { - return new \DateTimeZone(self::$map[$tzid]); - } - - // Some Microsoft products prefix the offset first, so let's strip that off - // and see if it is our tzid map. We don't want to check for this first just - // in case there are overrides in our tzid map. - if (preg_match('/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/', $tzid, $matches)) { - $tzidAlternate = $matches[3]; - if (isset(self::$map[$tzidAlternate])) { - return new \DateTimeZone(self::$map[$tzidAlternate]); - } - } - - // Maybe the author was hyper-lazy and just included an offset. We - // support it, but we aren't happy about it. - if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { - // Note that the path in the source will never be taken from PHP 5.5.10 - // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it - // already gets returned early in this function. Once we drop support - // for versions under PHP 5.5.10, this bit can be taken out of the - // source. - // @codeCoverageIgnoreStart - return new \DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0')); - // @codeCoverageIgnoreEnd - } - - if ($vcalendar) { - // If that didn't work, we will scan VTIMEZONE objects - foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { - if ((string) $vtimezone->TZID === $tzid) { - // Some clients add 'X-LIC-LOCATION' with the olson name. - if (isset($vtimezone->{'X-LIC-LOCATION'})) { - $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; - - // Libical generators may specify strings like - // "SystemV/EST5EDT". For those we must remove the - // SystemV part. - if ('SystemV/' === substr($lic, 0, 8)) { - $lic = substr($lic, 8); - } - - return self::getTimeZone($lic, null, $failIfUncertain); - } - // Microsoft may add a magic number, which we also have an - // answer for. - if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { - $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); - - // 2 can mean both Europe/Lisbon and Europe/Sarajevo. - if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { - return new \DateTimeZone('Europe/Sarajevo'); - } - - if (isset(self::$microsoftExchangeMap[$cdoId])) { - return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); - } - } - } - } - } - - if ($failIfUncertain) { - throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid); - } - - // If we got all the way here, we default to UTC. - return new \DateTimeZone(date_default_timezone_get()); - } - /** * This method will load in all the tz mapping information, if it's not yet * done. + * + * @deprecated */ public static function loadTzMaps() { @@ -243,7 +267,8 @@ public static function loadTzMaps() include __DIR__.'/timezonedata/windowszones.php', include __DIR__.'/timezonedata/lotuszones.php', include __DIR__.'/timezonedata/exchangezones.php', - include __DIR__.'/timezonedata/php-workaround.php' + include __DIR__.'/timezonedata/php-workaround.php', + include __DIR__.'/timezonedata/extrazones.php' ); } @@ -257,6 +282,8 @@ public static function loadTzMaps() * (See timezonedata/php-bc.php and timezonedata php-workaround.php) * * @return array + * + * @deprecated */ public static function getIdentifiersBC() { diff --git a/lib/TimezoneGuesser/FindFromMzVersionTimezone.php b/lib/TimezoneGuesser/FindFromMzVersionTimezone.php new file mode 100644 index 000000000..b6290288f --- /dev/null +++ b/lib/TimezoneGuesser/FindFromMzVersionTimezone.php @@ -0,0 +1,40 @@ + Eastern Standard Time + */ +class FindFromMzVersionTimezone implements TimezoneFinder +{ + public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone + { + if (strlen($tzid) < 1) { + return null; + } + + $trailingChar = (int) $tzid[strlen($tzid)-1]; + if ($trailingChar <= 9 && $trailingChar >= 1) { + $emptySpace = strrpos($tzid, ' '); + if ($emptySpace === false) { + return null; + } + + $tz = TimeZoneUtil::getTimeZone(substr($tzid, 0, $emptySpace)); + if ($tz->getName() === 'UTC') { + return null; + } + + return $tz; + } + + return null; + } +} diff --git a/lib/TimezoneGuesser/FindFromOffset.php b/lib/TimezoneGuesser/FindFromOffset.php new file mode 100644 index 000000000..990ac9692 --- /dev/null +++ b/lib/TimezoneGuesser/FindFromOffset.php @@ -0,0 +1,31 @@ + 'Africa/Lagos', + '+02:00' => 'Africa/Cairo', + '+03:00' => 'Europe/Moscow', + '+04:00' => 'Asia/Dubai', + '+05:00' => 'Asia/Karachi', + '+06:00' => 'Asia/Dhaka', + '+07:00' => 'Asia/Jakarta', + '+08:00' => 'Asia/Shanghai', + '+09:00' => 'Asia/Tokyo', + '+10:00' => 'Australia/Sydney', + '+11:00' => 'Pacific/Noumea', + '+12:00' => 'Pacific/Auckland', + '+13:00' => 'Pacific/Apia', + '-01:00' => 'Atlantic/Cape_Verde', + '-02:00' => 'Atlantic/South_Georgia', + '-03:00' => 'America/Sao_Paulo', + '-04:00' => 'America/Manaus', + '-05:00' => 'America/Lima', + '-06:00' => 'America/Guatemala', + '-07:00' => 'America/Hermosillo', + '-08:00' => 'America/Los_Angeles', + '-09:00' => 'Pacific/Gambier', + '-10:00' => 'America/Anchorage', + '-11:00' => 'Pacific/Niue', + ]; + + public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone + { + // only handle number timezone + if (strlen($tzid) > 6) { + return null; + } + + try { + $tzid = new DateTimeZone($tzid); + + return new DateTimeZone(self::$offsetTimezones[$tzid->getName()]) ?? null; + } catch (\Exception $e) { + return null; + } + } +} diff --git a/lib/TimezoneGuesser/FindFromOutlookCities.php b/lib/TimezoneGuesser/FindFromOutlookCities.php new file mode 100644 index 000000000..6ab672ecf --- /dev/null +++ b/lib/TimezoneGuesser/FindFromOutlookCities.php @@ -0,0 +1,44 @@ +getIdentifiersBC())) + ) { + return new DateTimeZone($tzid); + } + } catch (Exception $e) { + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + private function getIdentifiersBC() + { + return include __DIR__.'/../timezonedata/php-bc.php'; + } +} diff --git a/lib/TimezoneGuesser/FindFromTimezoneMap.php b/lib/TimezoneGuesser/FindFromTimezoneMap.php new file mode 100644 index 000000000..4e0a64e22 --- /dev/null +++ b/lib/TimezoneGuesser/FindFromTimezoneMap.php @@ -0,0 +1,85 @@ +hasTzInMap($tzid)) { + return new DateTimeZone($this->getTzFromMap($tzid)); + } + + // Some Microsoft products prefix the offset first, so let's strip that off + // and see if it is our tzid map. We don't want to check for this first just + // in case there are overrides in our tzid map. + foreach ($this->patterns as $pattern) { + if (!preg_match($pattern, $tzid, $matches)) { + continue; + } + $tzidAlternate = $matches[3]; + if ($this->hasTzInMap($tzidAlternate)) { + return new DateTimeZone($this->getTzFromMap($tzidAlternate)); + } + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + private function getTzMaps() + { + if ([] === $this->map) { + $map = array_merge( + include __DIR__.'/../timezonedata/windowszones.php', + include __DIR__.'/../timezonedata/lotuszones.php', + include __DIR__.'/../timezonedata/exchangezones.php', + include __DIR__.'/../timezonedata/php-workaround.php', + include __DIR__.'/../timezonedata/extrazones.php' + ); + $this->map = array_combine( + array_map(static fn (string $key) => str_replace(".", "", mb_strtolower($key, 'UTF-8')), array_keys($map)), + array_values($map), + ); + } + + return $this->map; + } + + private function getTzFromMap(string $tzid): string + { + return $this->getTzMaps()[mb_strtolower($tzid, 'UTF-8')]; + } + + private function hasTzInMap(string $tzid): bool + { + return isset($this->getTzMaps()[mb_strtolower($tzid, 'UTF-8')]); + } +} diff --git a/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php b/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php new file mode 100644 index 000000000..76a53fdb4 --- /dev/null +++ b/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php @@ -0,0 +1,97 @@ +TZID || $vtimezone->TZID->getValue() !== 'Customized Time Zone') { + return null; + } + + $timezones = DateTimeZone::listIdentifiers(); + $standard = $vtimezone->STANDARD; + $daylight = $vtimezone->DAYLIGHT; + if (!$standard) { + return null; + } + + $standardOffset = $standard->TZOFFSETTO; + if (!$standardOffset) { + return null; + } + $standardOffset = $standardOffset->getValue(); + + $standardRRule = $standard->RRULE ? $standard->RRULE->getValue() : 'FREQ=DAILY'; + // The guess will not be perfectly matched since we use the timezone data of the current year + // It might be wrong if the timezone data changed in the past + $year = (new DateTimeImmutable('now'))->format('Y'); + $start = new DateTimeImmutable($year . '-01-01'); + $standardIterator = new RRuleIterator($standardRRule, $start); + $standardIterator->next(); + + if ($daylight && !$daylight->TZOFFSETTO) { + $daylight = null; + } + $daylightOffset = $daylight ? $daylight->TZOFFSETTO->getValue() : ''; + $daylightRRule = $daylight ? ($daylight->RRULE ? $daylight->RRULE->getValue() : 'FREQ=DAILY') : ''; + $daylightIterator = $daylight ? new RRuleIterator($daylightRRule, $standardIterator->current()) : null; + $daylightIterator && $daylightIterator->next(); + + foreach ($timezones as $timezone) { + $tz = new DateTimeZone($timezone); + // check standard + $timestamp = $standardIterator->current()->getTimestamp(); + $transitions = $tz->getTransitions($timestamp, $timestamp + 1); + if (empty($transitions)) { + continue; + } + + $checkOffset = $transitions[0]['offset']; + + if ($checkOffset !== $this->parseOffsetToInteger($standardOffset)) { + continue; + } + + if (!$daylight) { + return TimeZoneUtil::getTimeZone($timezone, null, $failIfUncertain); + } + + // check daylight + $timestamp = $daylightIterator->current()->getTimestamp(); + $transitions = $tz->getTransitions($timestamp, $timestamp + 1); + if (empty($transitions)) { + continue; + } + + $checkOffset = $transitions[0]['offset']; + if ($checkOffset === $this->parseOffsetToInteger($daylightOffset)) { + return TimeZoneUtil::getTimeZone($timezone, null, $failIfUncertain); + } + } + + return null; + } + + private function parseOffsetToInteger(string $offset): int + { + $time = ((int) ($offset[1].$offset[2]) * 60) + (int) ($offset[3].$offset[4]); + + $time = $time * 60; + + if ($offset[0] === "-") { + $time = $time *-1; + } + + return $time; + } +} diff --git a/lib/TimezoneGuesser/GuessFromLicEntry.php b/lib/TimezoneGuesser/GuessFromLicEntry.php new file mode 100644 index 000000000..b57029919 --- /dev/null +++ b/lib/TimezoneGuesser/GuessFromLicEntry.php @@ -0,0 +1,37 @@ +{'X-LIC-LOCATION'})) { + return null; + } + + $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; + + if ($lic === 'Customized Time Zone') { + return null; + } + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if ('SystemV/' === substr($lic, 0, 8)) { + $lic = substr($lic, 8); + } + + return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain); + } +} diff --git a/lib/TimezoneGuesser/GuessFromMsTzId.php b/lib/TimezoneGuesser/GuessFromMsTzId.php new file mode 100644 index 000000000..21586d753 --- /dev/null +++ b/lib/TimezoneGuesser/GuessFromMsTzId.php @@ -0,0 +1,119 @@ + 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Lagos', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Maputo', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Riyadh', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Dubai', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Kolkata', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Yangon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Nuuk', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Pago_Pago', + 39 => 'Pacific/Kwajalein', + ]; + + public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone + { + // Microsoft may add a magic number, which we also have an + // answer for. + if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + return null; + } + $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { + return new DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + + return null; + } +} diff --git a/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php b/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php new file mode 100644 index 000000000..69fba4424 --- /dev/null +++ b/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php @@ -0,0 +1,21 @@ +getValueType(); } + if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) { + $valueType = null; + } $newProperty = $output->createProperty( $property->name, $property->getParts(), @@ -227,7 +226,7 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp // Lastly, we need to see if there's a need for a VALUE parameter. // - // We can do that by instantating a empty property with that name, and + // We can do that by instantiating a empty property with that name, and // seeing if the default valueType is identical to the current one. $tempProperty = $output->createProperty($newProperty->name); if ($tempProperty->getValueType() !== $newProperty->getValueType()) { @@ -242,8 +241,7 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp * * vCard 4.0 no longer supports BINARY properties. * - * @param Component\VCard $output - * @param Property\Uri $property the input property + * @param Property\Uri $property the input property * @param $parameters list of parameters that will eventually be added to * the new property * @@ -296,8 +294,7 @@ protected function convertBinaryToUri(Component\VCard $output, Property\Binary $ * be valid in vCard 3.0 as well, we should convert those to BINARY if * possible, to improve compatibility. * - * @param Component\VCard $output - * @param Property\Uri $property the input property + * @param Property\Uri $property the input property * * @return Property\Binary|null */ @@ -344,9 +341,6 @@ protected function convertUriToBinary(Component\VCard $output, Property\Uri $new /** * Adds parameters to a new property for vCard 4.0. - * - * @param Property $newProperty - * @param array $parameters */ protected function convertParameters40(Property $newProperty, array $parameters) { @@ -383,9 +377,6 @@ protected function convertParameters40(Property $newProperty, array $parameters) /** * Adds parameters to a new property for vCard 3.0. - * - * @param Property $newProperty - * @param array $parameters */ protected function convertParameters30(Property $newProperty, array $parameters) { diff --git a/lib/Version.php b/lib/Version.php index 257e66a79..858a3e95f 100644 --- a/lib/Version.php +++ b/lib/Version.php @@ -14,5 +14,5 @@ class Version /** * Full version number. */ - const VERSION = '4.2.0'; + const VERSION = '4.22.0'; } diff --git a/lib/Writer.php b/lib/Writer.php index c70a6ae4d..cbd22022e 100644 --- a/lib/Writer.php +++ b/lib/Writer.php @@ -19,8 +19,6 @@ class Writer /** * Serializes a vCard or iCalendar object. * - * @param Component $component - * * @return string */ public static function write(Component $component) @@ -31,8 +29,7 @@ public static function write(Component $component) /** * Serializes a jCal or jCard object. * - * @param Component $component - * @param int $options + * @param int $options * * @return string */ @@ -44,8 +41,6 @@ public static function writeJson(Component $component, $options = 0) /** * Serializes a xCal or xCard object. * - * @param Component $component - * * @return string */ public static function writeXml(Component $component) diff --git a/lib/timezonedata/exchangezones.php b/lib/timezonedata/exchangezones.php index 89bddc27c..2771ce922 100644 --- a/lib/timezonedata/exchangezones.php +++ b/lib/timezonedata/exchangezones.php @@ -21,20 +21,20 @@ 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', 'Prague, Central Europe' => 'Europe/Prague', - 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', - 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sofia', + 'West Central Africa' => 'Africa/Lagos', // This was a best guess 'Athens, Istanbul, Minsk' => 'Europe/Athens', 'Bucharest' => 'Europe/Bucharest', 'Cairo' => 'Africa/Cairo', - 'Harare, Pretoria' => 'Africa/Harare', + 'Harare, Pretoria' => 'Africa/Maputo', 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', 'Baghdad' => 'Asia/Baghdad', - 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Arab, Kuwait, Riyadh' => 'Asia/Riyadh', 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', 'East Africa, Nairobi' => 'Africa/Nairobi', 'Tehran' => 'Asia/Tehran', - 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Abu Dhabi, Muscat' => 'Asia/Dubai', // Best guess 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', 'Kabul' => 'Asia/Kabul', 'Ekaterinburg' => 'Asia/Yekaterinburg', @@ -71,7 +71,7 @@ 'Mid-Atlantic' => 'America/Noronha', 'Brasilia' => 'America/Sao_Paulo', // Best guess 'Buenos Aires' => 'America/Argentina/Buenos_Aires', - 'Greenland' => 'America/Godthab', + 'Greenland' => 'America/Nuuk', 'Newfoundland' => 'America/St_Johns', 'Atlantic Time (Canada)' => 'America/Halifax', 'Caracas, La Paz' => 'America/Caracas', @@ -89,6 +89,9 @@ 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess 'Alaska' => 'America/Anchorage', 'Hawaii' => 'Pacific/Honolulu', - 'Midway Island, Samoa' => 'Pacific/Midway', + 'Midway Island, Samoa' => 'Pacific/Pago_Pago', 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', + + // Localized timezones + 'Amsterdam, Berlin, Berne, Rome, Stockholm, Vienne' => 'Europe/Berlin', ]; diff --git a/lib/timezonedata/extrazones.php b/lib/timezonedata/extrazones.php new file mode 100644 index 000000000..d93ab4203 --- /dev/null +++ b/lib/timezonedata/extrazones.php @@ -0,0 +1,207 @@ + 'America/Rio_Branco', + 'Africa Central' => 'Africa/Maputo', + 'Africa Eastern' => 'Africa/Nairobi', + 'Africa FarWestern' => 'Africa/El_Aaiun', + 'Africa Southern' => 'Africa/Johannesburg', + 'Africa Western' => 'Africa/Lagos', + 'Aktyubinsk' => 'Asia/Aqtobe', + 'Alaska Hawaii' => 'America/Anchorage', + 'Almaty' => 'Asia/Almaty', + 'Amazon' => 'America/Manaus', + 'America Central' => 'America/Chicago', + 'America Eastern' => 'America/New_York', + 'America Mountain' => 'America/Denver', + 'America Pacific' => 'America/Los_Angeles', + 'Anadyr' => 'Asia/Anadyr', + 'Apia' => 'Pacific/Apia', + 'Aqtau' => 'Asia/Aqtau', + 'Aqtobe' => 'Asia/Aqtobe', + 'Argentina Western' => 'America/Argentina/San_Luis', + 'Armenia' => 'Asia/Yerevan', + 'Armenian Standard Time' => 'Asia/Yerevan', + 'Ashkhabad' => 'Asia/Ashgabat', + 'Australia Central' => 'Australia/Adelaide', + 'Australia CentralWestern' => 'Australia/Eucla', + 'Australia Eastern' => 'Australia/Sydney', + 'Australia Western' => 'Australia/Perth', + 'Azerbaijan' => 'Asia/Baku', + 'Baku' => 'Asia/Baku', + 'Bangladesh' => 'Asia/Dhaka', + 'Bering' => 'America/Adak', + 'Bhutan' => 'Asia/Thimphu', + 'Bolivia' => 'America/La_Paz', + 'Borneo' => 'Asia/Kuching', + 'British' => 'Europe/London', + 'Brunei' => 'Asia/Kuching', + 'Casey' => 'Antarctica/Casey', + 'Chamorro' => 'Pacific/Guam', + 'Chatham' => 'Pacific/Chatham', + 'Chile' => 'America/Santiago', + 'Choibalsan' => 'Asia/Choibalsan', + 'Christmas' => 'Asia/Bangkok', + 'Cocos' => 'Asia/Yangon', + 'Colombia' => 'America/Bogota', + 'Cook' => 'Pacific/Rarotonga', + 'Dacca' => 'Asia/Dhaka', + 'Davis' => 'Antarctica/Davis', + 'Dominican' => 'America/Santo_Domingo', + 'DumontDUrville' => 'Pacific/Port_Moresby', + 'Dushanbe' => 'Asia/Dushanbe', + 'Dutch Guiana' => 'America/Paramaribo', + 'East Timor' => 'Asia/Dili', + 'Easter' => 'Pacific/Easter', + 'Ecuador' => 'America/Guayaquil', + 'Europe Central' => 'Europe/Paris', + 'Europe Eastern' => 'Europe/Bucharest', + 'Europe Further Eastern' => 'Europe/Minsk', + 'Europe Western' => 'Atlantic/Canary', + 'Falkland' => 'Atlantic/Stanley', + 'Fiji Islands Standard Time' => 'Pacific/Fiji', + 'French Guiana' => 'America/Cayenne', + 'French Southern' => 'Indian/Maldives', + 'Frunze' => 'Asia/Bishkek', + 'Galapagos' => 'Pacific/Galapagos', + 'Gambier' => 'Pacific/Gambier', + 'Georgia' => 'Asia/Tbilisi', + 'Gilbert Islands' => 'Pacific/Tarawa', + 'GMT' => 'Europe/London', + 'Goose Bay' => 'America/Goose_Bay', + 'Greenland Central' => 'America/Scoresbysund', + 'Greenland Eastern' => 'America/Scoresbysund', + 'Greenland Western' => 'America/Nuuk', + 'Guam' => 'Pacific/Guam', + 'Gulf' => 'Asia/Dubai', + 'Guyana' => 'America/Guyana', + 'Hawaii Aleutian' => 'Pacific/Honolulu', + 'Hong Kong' => 'Asia/Hong_Kong', + 'Hovd' => 'Asia/Hovd', + 'Indian Ocean' => 'Indian/Chagos', + 'Indochina' => 'Asia/Bangkok', + 'Indonesia Central' => 'Asia/Makassar', + 'Indonesia Eastern' => 'Asia/Jayapura', + 'Indonesia Western' => 'Asia/Jakarta', + 'Irish' => 'Europe/Dublin', + 'Irkutsk' => 'Asia/Irkutsk', + 'Kamchatka' => 'Asia/Kamchatka', + 'Kamchatka Standard Time' => 'Asia/Kamchatka', + 'Karachi' => 'Asia/Karachi', + 'Kazakhstan Eastern' => 'Asia/Almaty', + 'Kazakhstan Western' => 'Asia/Aqtobe', + 'Kizilorda' => 'Asia/Qyzylorda', + 'Kosrae' => 'Pacific/Kosrae', + 'Kuybyshev' => 'Europe/Samara', + 'Kyrgystan' => 'Asia/Bishkek', + 'Lanka' => 'Asia/Colombo', + 'Liberia' => 'Africa/Monrovia', + 'Line Islands' => 'Pacific/Kiritimati', + 'Lord Howe' => 'Australia/Lord_Howe', + 'Macau' => 'Asia/Macau', + 'Macquarie' => 'Antarctica/Macquarie', + 'Magadan' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Malaya' => 'Asia/Singapore', + 'Malaysia' => 'Asia/Kuching', + 'Maldives' => 'Indian/Maldives', + 'Marquesas' => 'Pacific/Marquesas', + 'Marshall Islands' => 'Pacific/Tarawa', + 'Mawson' => 'Antarctica/Mawson', + 'Mexico Pacific' => 'America/Mazatlan', + 'Mexico Standard Time' => 'America/Mexico_City', + 'Mid-Atlantic Standard Time' => 'Atlantic/Cape_Verde', + 'Mongolia' => 'Asia/Ulaanbaatar', + 'Moscow' => 'Europe/Moscow', + 'Nauru' => 'Pacific/Nauru', + 'New Caledonia' => 'Pacific/Noumea', + 'Newfoundland And Labrador Standard Time' => 'America/St_Johns', + 'Niue' => 'Pacific/Niue', + 'Norfolk' => 'Pacific/Norfolk', + 'Noronha' => 'America/Noronha', + 'North Mariana' => 'Pacific/Guam', + 'Novosibirsk' => 'Asia/Novosibirsk', + 'Omsk' => 'Asia/Omsk', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Oral' => 'Asia/Oral', + 'Palau' => 'Pacific/Palau', + 'Papua New Guinea' => 'Pacific/Port_Moresby', + 'Paraguay' => 'America/Asuncion', + 'Peru' => 'America/Lima', + 'Philippines' => 'Asia/Manila', + 'Phoenix Islands' => 'Pacific/Kanton', + 'Pierre Miquelon' => 'America/Miquelon', + 'Pitcairn' => 'Pacific/Pitcairn', + 'Pyongyang' => 'Asia/Pyongyang', + 'Qyzylorda' => 'Asia/Qyzylorda', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Reunion' => 'Asia/Dubai', + 'Rothera' => 'Antarctica/Rothera', + 'Sakhalin' => 'Asia/Sakhalin', + 'Samara' => 'Europe/Samara', + 'Samarkand' => 'Asia/Samarkand', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Seychelles' => 'Asia/Dubai', + 'Shevchenko' => 'Asia/Aqtau', + 'Solomon' => 'Pacific/Guadalcanal', + 'South Georgia' => 'Atlantic/South_Georgia', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Suriname' => 'America/Paramaribo', + 'Sverdlovsk' => 'Asia/Yekaterinburg', + 'Syowa' => 'Asia/Riyadh', + 'Tahiti' => 'Pacific/Tahiti', + 'Tajikistan' => 'Asia/Dushanbe', + 'Tashkent' => 'Asia/Tashkent', + 'Tbilisi' => 'Asia/Tbilisi', + 'Tokelau' => 'Pacific/Fakaofo', + 'Transitional Islamic State Of Afghanistan Standard Time' => 'Asia/Kabul', + 'Turkmenistan' => 'Asia/Ashgabat', + 'Tuvalu' => 'Pacific/Tarawa', + 'Uralsk' => 'Asia/Oral', + 'Uruguay' => 'America/Montevideo', + 'Urumqi' => 'Asia/Urumqi', + 'Uzbekistan' => 'Asia/Tashkent', + 'Vanuatu' => 'Pacific/Efate', + 'Volgograd' => 'Europe/Volgograd', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'Vostok' => 'Asia/Urumqi', + 'Wake' => 'Pacific/Tarawa', + 'Wallis' => 'Pacific/Tarawa', + 'Yekaterinburg' => 'Asia/Yekaterinburg', + 'Yerevan' => 'Asia/Yerevan', + 'Yukon' => 'America/Yakutat', + // Overwrite + 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires', + 'Dateline' => 'Pacific/Auckland', + 'Dateline Standard Time' => 'Pacific/Niue', + 'India' => 'Asia/Kolkata', + 'India Standard Time' => 'Asia/Kolkata', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata', + 'Myanmar' => 'Asia/Yangon', + 'Myanmar Standard Time' => 'Asia/Yangon', + 'Nepal Standard Time' => 'Asia/Kathmandu', + 'Rangoon' => 'Asia/Yangon', + 'Greenwich' => 'Africa/Abidjan', + 'UTC-02' => 'America/Noronha', + 'UTC-08' => 'Pacific/Pitcairn', + 'UTC-09' => 'Pacific/Gambier', + 'UTC-11' => 'Pacific/Niue', + 'UTC+12' => 'Pacific/Auckland', + 'UTC-05:00' => 'America/Lima', + 'US Eastern Standard Time' => 'America/New_York', + 'tzone://Microsoft/Utc' => 'UTC', + 'America/Santa_Isabel' => 'America/Tijuana', + 'Asia/Chongqing' => 'Asia/Shanghai', + 'Asia/Harbin' => 'Asia/Shanghai', + 'Asia/Kashgar' => 'Asia/Urumqi', + 'Pacific/Johnston' => 'Pacific/Honolulu', + 'EDT' => 'America/Manaus', + 'America/Godthab' => 'America/Nuuk', + 'CDT' => 'America/Chicago', + 'PST' => 'America/Los_Angeles', +]; diff --git a/lib/timezonedata/lotuszones.php b/lib/timezonedata/lotuszones.php index 4b50808f9..3f6f7b339 100644 --- a/lib/timezonedata/lotuszones.php +++ b/lib/timezonedata/lotuszones.php @@ -34,19 +34,19 @@ 'Newfoundland' => 'America/St_Johns', 'Argentina' => 'America/Argentina/Buenos_Aires', 'E. South America' => 'America/Belem', - 'Greenland' => 'America/Godthab', + 'Greenland' => 'America/Nuuk', 'Montevideo' => 'America/Montevideo', 'SA Eastern' => 'America/Belem', // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. 'Azores' => 'Atlantic/Azores', 'Cape Verde' => 'Atlantic/Cape_Verde', - 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT. + 'Greenwich' => 'Africa/Abidjan', // No I'm serious.. Greenwich is not GMT. 'Morocco' => 'Africa/Casablanca', 'Central Europe' => 'Europe/Prague', - 'Central European' => 'Europe/Sarajevo', + 'Central European' => 'Europe/Belgrade', 'Romance' => 'Europe/Paris', 'W. Central Africa' => 'Africa/Lagos', // Best guess - 'W. Europe' => 'Europe/Amsterdam', + 'W. Europe' => 'Europe/Brussels', 'E. Europe' => 'Europe/Minsk', 'Egypt' => 'Africa/Cairo', 'FLE' => 'Europe/Helsinki', @@ -55,14 +55,14 @@ 'Jordan' => 'Asia/Amman', 'Middle East' => 'Asia/Beirut', 'Namibia' => 'Africa/Windhoek', - 'South Africa' => 'Africa/Harare', - 'Arab' => 'Asia/Kuwait', + 'South Africa' => 'Africa/Maputo', + 'Arab' => 'Asia/Riyadh', 'Arabic' => 'Asia/Baghdad', 'E. Africa' => 'Africa/Nairobi', 'Georgian' => 'Asia/Tbilisi', 'Russian' => 'Europe/Moscow', 'Iran' => 'Asia/Tehran', - 'Arabian' => 'Asia/Muscat', + 'Arabian' => 'Asia/Dubai', 'Armenian' => 'Asia/Yerevan', 'Azerbijan' => 'Asia/Baku', 'Caucasus' => 'Asia/Yerevan', diff --git a/lib/timezonedata/php-bc.php b/lib/timezonedata/php-bc.php index 83f38f507..d9f49008a 100644 --- a/lib/timezonedata/php-bc.php +++ b/lib/timezonedata/php-bc.php @@ -16,6 +16,21 @@ * @license http://sabre.io/license/ Modified BSD License */ return [ + // Moved to backward in 2021b + 'Pacific/Enderbury', + // Moved to backward in 2022b + 'Europe/Kiev', + // Moved to backward in 2022d + 'Europe/Uzhgorod', + 'Europe/Zaporozhye', + // Moved to backward in 2022f + 'America/Thunder_Bay', + 'America/Nipigon', + 'America/Rainy_River', + // Moved to backward in 2022g + 'America/Pangnirtung', + + // Original list 'Africa/Asmera', 'Africa/Timbuktu', 'America/Argentina/ComodRivadavia', @@ -147,7 +162,6 @@ 'US/Michigan', 'US/Mountain', 'US/Pacific', - 'US/Pacific-New', 'US/Samoa', 'WET', ]; diff --git a/lib/timezonedata/php-workaround.php b/lib/timezonedata/php-workaround.php index 13ff4b302..bd8fcdf1b 100644 --- a/lib/timezonedata/php-workaround.php +++ b/lib/timezonedata/php-workaround.php @@ -25,7 +25,7 @@ 'GMT0' => 'UTC', 'Greenwich' => 'UTC', 'Hongkong' => 'Asia/Hong_Kong', - 'Iceland' => 'Atlantic/Reykjavik', + 'Iceland' => 'Africa/Abidjan', 'Iran' => 'Asia/Tehran', 'Israel' => 'Asia/Jerusalem', 'Jamaica' => 'America/Jamaica', diff --git a/lib/timezonedata/windowszones.php b/lib/timezonedata/windowszones.php index af3904b12..42fdaf5a7 100644 --- a/lib/timezonedata/windowszones.php +++ b/lib/timezonedata/windowszones.php @@ -3,14 +3,14 @@ /** * Automatically generated timezone file. * - * Last update: 2016-08-24T17:35:38-04:00 - * Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml + * Last update: 2020-12-13T17:38:12+05:45 + * Source: https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml * - * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). + * @copyright Copyright (C) fruux GmbH (https://fruux.com/). * @license http://sabre.io/license/ Modified BSD License */ -return [ +return [ 'AUS Central Standard Time' => 'Australia/Darwin', 'AUS Eastern Standard Time' => 'Australia/Sydney', 'Afghanistan Standard Time' => 'Asia/Kabul', @@ -55,13 +55,13 @@ 'Eastern Standard Time (Mexico)' => 'America/Cancun', 'Egypt Standard Time' => 'Africa/Cairo', 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', - 'FLE Standard Time' => 'Europe/Kiev', + 'FLE Standard Time' => 'Europe/Kyiv', 'Fiji Standard Time' => 'Pacific/Fiji', 'GMT Standard Time' => 'Europe/London', 'GTB Standard Time' => 'Europe/Bucharest', 'Georgian Standard Time' => 'Asia/Tbilisi', - 'Greenland Standard Time' => 'America/Godthab', - 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Greenland Standard Time' => 'America/Nuuk', + 'Greenwich Standard Time' => 'Africa/Abidjan', 'Haiti Standard Time' => 'America/Port-au-Prince', 'Hawaiian Standard Time' => 'Pacific/Honolulu', 'India Standard Time' => 'Asia/Calcutta', @@ -74,6 +74,7 @@ 'Line Islands Standard Time' => 'Pacific/Kiritimati', 'Lord Howe Standard Time' => 'Australia/Lord_Howe', 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', 'Marquesas Standard Time' => 'Pacific/Marquesas', 'Mauritius Standard Time' => 'Indian/Mauritius', 'Middle East Standard Time' => 'Asia/Beirut', @@ -91,11 +92,13 @@ 'North Asia East Standard Time' => 'Asia/Irkutsk', 'North Asia Standard Time' => 'Asia/Krasnoyarsk', 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', 'Pacific SA Standard Time' => 'America/Santiago', 'Pacific Standard Time' => 'America/Los_Angeles', 'Pacific Standard Time (Mexico)' => 'America/Tijuana', 'Pakistan Standard Time' => 'Asia/Karachi', 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', 'Romance Standard Time' => 'Europe/Paris', 'Russia Time Zone 10' => 'Asia/Srednekolymsk', 'Russia Time Zone 11' => 'Asia/Kamchatka', @@ -108,9 +111,12 @@ 'Saint Pierre Standard Time' => 'America/Miquelon', 'Sakhalin Standard Time' => 'Asia/Sakhalin', 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', 'Singapore Standard Time' => 'Asia/Singapore', 'South Africa Standard Time' => 'Africa/Johannesburg', 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', 'Syria Standard Time' => 'Asia/Damascus', 'Taipei Standard Time' => 'Asia/Taipei', 'Tasmania Standard Time' => 'Australia/Hobart', @@ -125,6 +131,7 @@ 'US Mountain Standard Time' => 'America/Phoenix', 'UTC' => 'Etc/GMT', 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', 'UTC-02' => 'Etc/GMT+2', 'UTC-08' => 'Etc/GMT+8', 'UTC-09' => 'Etc/GMT+9', @@ -132,6 +139,7 @@ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', 'Venezuela Standard Time' => 'America/Caracas', 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', 'W. Australia Standard Time' => 'Australia/Perth', 'W. Central Africa Standard Time' => 'Africa/Lagos', 'W. Europe Standard Time' => 'Europe/Berlin', @@ -140,4 +148,92 @@ 'West Bank Standard Time' => 'Asia/Hebron', 'West Pacific Standard Time' => 'Pacific/Port_Moresby', 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', + 'coordinated universal time-11' => 'Pacific/Pago_Pago', + 'aleutian islands' => 'America/Adak', + 'marquesas islands' => 'Pacific/Marquesas', + 'coordinated universal time-09' => 'America/Anchorage', + 'baja california' => 'America/Tijuana', + 'coordinated universal time-08' => 'Pacific/Pitcairn', + 'chihuahua, la paz, mazatlan' => 'America/Chihuahua', + 'easter island' => 'Pacific/Easter', + 'guadalajara, mexico city, monterrey' => 'America/Mexico_City', + 'bogota, lima, quito, rio branco' => 'America/Bogota', + 'chetumal' => 'America/Cancun', + 'haiti' => 'America/Port-au-Prince', + 'havana' => 'America/Havana', + 'turks and caicos' => 'America/Grand_Turk', + 'asuncion' => 'America/Asuncion', + 'caracas' => 'America/Caracas', + 'cuiaba' => 'America/Cuiaba', + 'georgetown, la paz, manaus, san juan' => 'America/La_Paz', + 'araguaina' => 'America/Araguaina', + 'cayenne, fortaleza' => 'America/Cayenne', + 'city of buenos aires' => 'America/Argentina/Buenos_Aires', + 'punta arenas' => 'America/Punta_Arenas', + 'saint pierre and miquelon' => 'America/Miquelon', + 'salvador' => 'America/Bahia', + 'coordinated universal time-02' => 'America/Noronha', + 'mid-atlantic - old' => 'America/Noronha', + 'cabo verde is' => 'Atlantic/Cape_Verde', + 'coordinated universal time' => 'UTC', + 'dublin, edinburgh, lisbon, london' => 'Europe/London', + 'monrovia, reykjavik' => 'Africa/Abidjan', + 'belgrade, bratislava, budapest, ljubljana, prague' => 'Europe/Budapest', + 'casablanca' => 'Africa/Casablanca', + 'sao tome' => 'Africa/Sao_Tome', + 'sarajevo, skopje, warsaw, zagreb' => 'Europe/Warsaw', + 'amman' => 'Asia/Amman', + 'athens, bucharest' => 'Europe/Bucharest', + 'beirut' => 'Asia/Beirut', + 'chisinau' => 'Europe/Chisinau', + 'damascus' => 'Asia/Damascus', + 'gaza, hebron' => 'Asia/Hebron', + 'jerusalem' => 'Asia/Jerusalem', + 'kaliningrad' => 'Europe/Kaliningrad', + 'khartoum' => 'Africa/Khartoum', + 'tripoli' => 'Africa/Tripoli', + 'windhoek' => 'Africa/Windhoek', + 'istanbul' => 'Europe/Istanbul', + 'kuwait, riyadh' => 'Asia/Riyadh', + 'minsk' => 'Europe/Minsk', + 'moscow, st petersburg' => 'Europe/Moscow', + 'nairobi' => 'Africa/Nairobi', + 'astrakhan, ulyanovsk' => 'Europe/Astrakhan', + 'izhevsk, samara' => 'Europe/Samara', + 'port louis' => 'Indian/Mauritius', + 'saratov' => 'Europe/Saratov', + 'ashgabat, tashkent' => 'Asia/Tashkent', + 'islamabad, karachi' => 'Asia/Karachi', + 'chennai, kolkata, mumbai, new delhi' => 'Asia/Kolkata', + 'sri jayawardenepura' => 'Asia/Colombo', + 'kathmandu' => 'Asia/Kathmandu', + 'astana' => 'Asia/Almaty', + 'dhaka' => 'Asia/Dhaka', + 'yangon (rangoon)' => 'Asia/Yangon', + 'barnaul, gorno-altaysk' => 'Asia/Barnaul', + 'tomsk' => 'Asia/Tomsk', + 'beijing, chongqing, hong kong, urumqi' => 'Asia/Shanghai', + 'perth' => 'Australia/Perth', + 'ulaanbaatar' => 'Asia/Ulaanbaatar', + 'eucla' => 'Australia/Eucla', + 'chita' => 'Asia/Chita', + 'seoul' => 'Asia/Seoul', + 'adelaide' => 'Australia/Adelaide', + 'brisbane' => 'Australia/Brisbane', + 'canberra, melbourne, sydney' => 'Australia/Sydney', + 'hobart' => 'Australia/Hobart', + 'lord howe island' => 'Australia/Lord_Howe', + 'bougainville island' => 'Pacific/Bougainville', + 'chokurdakh' => 'Asia/Srednekolymsk', + 'norfolk island' => 'Pacific/Norfolk', + 'solomon is, new caledonia' => 'Pacific/Guadalcanal', + 'anadyr, petropavlovsk-kamchatsky' => 'Asia/Kamchatka', + 'coordinated universal time+12' => 'Pacific/Tarawa', + 'petropavlovsk-kamchatsky - old' => 'Asia/Anadyr', + 'chatham islands' => 'Pacific/Chatham', + 'coordinated universal time+13' => 'Pacific/Kanton', + "nuku'alofa" => 'Pacific/Tongatapu', + 'kiritimati island' => 'Pacific/Kiritimati', + 'helsinki, kyiv, riga, sofia, tallinn, vilnius' => 'Europe/Helsinki', ]; diff --git a/phpstan.neon b/phpstan.neon index 241663a72..c705178c9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,4 @@ parameters: level: 1 - bootstrap: %currentWorkingDirectory%/vendor/autoload.php + universalObjectCratesClasses: + - \Sabre\VObject\Component diff --git a/tests/VObject/BirthdayCalendarGeneratorTest.php b/tests/VObject/BirthdayCalendarGeneratorTest.php index 6e4f89a4c..d27362837 100644 --- a/tests/VObject/BirthdayCalendarGeneratorTest.php +++ b/tests/VObject/BirthdayCalendarGeneratorTest.php @@ -458,11 +458,9 @@ public function testVcardStringWithEmptyBirthdayProperty() ); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testParseException() { + $this->expectException(ParseException::class); $generator = new BirthdayCalendarGenerator(); $input = <<setObjects($input); } - /** - * @expectedException \InvalidArgumentException - */ public function testInvalidArgumentException() { + $this->expectException(\InvalidArgumentException::class); $generator = new BirthdayCalendarGenerator(); $input = <<setObjects($input); } - /** - * @expectedException \InvalidArgumentException - */ public function testInvalidArgumentExceptionForPartiallyInvalidArray() { + $this->expectException(\InvalidArgumentException::class); $generator = new BirthdayCalendarGenerator(); $input = []; diff --git a/tests/VObject/CliTest.php b/tests/VObject/CliTest.php index 11c969c9d..a4124b76b 100644 --- a/tests/VObject/CliTest.php +++ b/tests/VObject/CliTest.php @@ -11,8 +11,17 @@ */ class CliTest extends TestCase { - public function setUp() + /** @var CliMock */ + private $cli; + + private $sabreTempDir = __DIR__.'/../temp/'; + + public function setUp(): void { + if (!file_exists($this->sabreTempDir)) { + mkdir($this->sabreTempDir); + } + $this->cli = new CliMock(); $this->cli->stderr = fopen('php://memory', 'r+'); $this->cli->stdout = fopen('php://memory', 'r+'); @@ -266,7 +275,7 @@ public function testConvertMimeDir() public function testConvertDefaultFormats() { - $outputFile = SABRE_TEMPDIR.'bar.json'; + $outputFile = $this->sabreTempDir.'bar.json'; $this->assertEquals( 2, @@ -279,7 +288,7 @@ public function testConvertDefaultFormats() public function testConvertDefaultFormats2() { - $outputFile = SABRE_TEMPDIR.'bar.ics'; + $outputFile = $this->sabreTempDir.'bar.ics'; $this->assertEquals( 2, @@ -474,7 +483,15 @@ public function testRepair() ); rewind($this->cli->stdout); - $this->assertRegExp("/^BEGIN:VCARD\r\nVERSION:2.1\r\nUID:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\r\nEND:VCARD\r\n$/", stream_get_contents($this->cli->stdout)); + $regularExpression = "/^BEGIN:VCARD\r\nVERSION:2.1\r\nUID:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\r\nEND:VCARD\r\n$/"; + $data = stream_get_contents($this->cli->stdout); + // ToDo: when we do not need to run phpunit 7 or 8, remove this 'if' block and just use + // the new assertMatchesRegularExpression that exists since phpunit 9. + if (method_exists($this, 'assertMatchesRegularExpression')) { + $this->assertMatchesRegularExpression($regularExpression, $data); + } else { + $this->assertRegExp($regularExpression, $data); + } } public function testRepairNothing() diff --git a/tests/VObject/Component/AvailableTest.php b/tests/VObject/Component/AvailableTest.php index 55292424e..bf0a6716f 100644 --- a/tests/VObject/Component/AvailableTest.php +++ b/tests/VObject/Component/AvailableTest.php @@ -22,7 +22,7 @@ public function testAvailableComponent() END:VCALENDAR VCAL; $document = Reader::read($vcal); - $this->assertInstanceOf(__NAMESPACE__.'\Available', $document->AVAILABLE); + $this->assertInstanceOf(Available::class, $document->AVAILABLE); } public function testGetEffectiveStartEnd() diff --git a/tests/VObject/Component/VAlarmTest.php b/tests/VObject/Component/VAlarmTest.php index 1e7a55ad7..2823d16da 100644 --- a/tests/VObject/Component/VAlarmTest.php +++ b/tests/VObject/Component/VAlarmTest.php @@ -4,6 +4,7 @@ use DateTime; use PHPUnit\Framework\TestCase; +use Sabre\VObject\InvalidDataException; use Sabre\VObject\Reader; class VAlarmTest extends TestCase @@ -126,11 +127,9 @@ public function timeRangeTestData() return $tests; } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testInTimeRangeInvalidComponent() { + $this->expectException(InvalidDataException::class); $calendar = new VCalendar(); $valarm = $calendar->createComponent('VALARM'); $valarm->TRIGGER = '-P1D'; diff --git a/tests/VObject/Component/VAvailabilityTest.php b/tests/VObject/Component/VAvailabilityTest.php index b6b9a2b80..edd06b02b 100644 --- a/tests/VObject/Component/VAvailabilityTest.php +++ b/tests/VObject/Component/VAvailabilityTest.php @@ -24,7 +24,7 @@ public function testVAvailabilityComponent() VCAL; $document = Reader::read($vcal); - $this->assertInstanceOf(__NAMESPACE__.'\VAvailability', $document->VAVAILABILITY); + $this->assertInstanceOf(VAvailability::class, $document->VAVAILABILITY); } public function testGetEffectiveStartEnd() @@ -122,7 +122,7 @@ public function testIsInTimeRangeOutside() ); } - public function testRFCxxxSection3_1_availabilityprop_required() + public function testRFCxxxSection3Part1AvailabilitypropRequired() { // UID and DTSTAMP are present. $this->assertIsValid(Reader::read( @@ -177,7 +177,7 @@ public function testRFCxxxSection3_1_availabilityprop_required() )); } - public function testRFCxxxSection3_1_availabilityprop_optional_once() + public function testRFCxxxSection3Part1AvailabilitypropOptionalOnce() { $properties = [ 'BUSYTYPE:BUSY', @@ -205,7 +205,7 @@ public function testRFCxxxSection3_1_availabilityprop_optional_once() } } - public function testRFCxxxSection3_1_availabilityprop_dtend_duration() + public function testRFCxxxSection3Part1AvailabilitypropDtendDuration() { // Only DTEND. $this->assertIsValid(Reader::read($this->template([ @@ -236,10 +236,10 @@ public function testAvailableSubComponent() VCAL; $document = Reader::read($vcal); - $this->assertInstanceOf(__NAMESPACE__, $document->VAVAILABILITY->AVAILABLE); + $this->assertInstanceOf(Available::class, $document->VAVAILABILITY->AVAILABLE); } - public function testRFCxxxSection3_1_availableprop_required() + public function testRFCxxxSection3Part1AvailablepropRequired() { // UID, DTSTAMP and DTSTART are present. $this->assertIsValid(Reader::read( @@ -331,7 +331,7 @@ public function testRFCxxxSection3_1_availableprop_required() )); } - public function testRFCxxxSection3_1_available_dtend_duration() + public function testRFCxxxSection3Part1AvailableDtendDuration() { // Only DTEND. $this->assertIsValid(Reader::read($this->templateAvailable([ @@ -350,7 +350,7 @@ public function testRFCxxxSection3_1_available_dtend_duration() ]))); } - public function testRFCxxxSection3_1_available_optional_once() + public function testRFCxxxSection3Part1AvailableOptionalOnce() { $properties = [ 'CREATED:20111005T135125Z', @@ -373,7 +373,7 @@ public function testRFCxxxSection3_1_available_optional_once() } } - public function testRFCxxxSection3_2() + public function testRFCxxxSection3Part2() { $this->assertEquals( 'BUSY', diff --git a/tests/VObject/Component/VCalendarTest.php b/tests/VObject/Component/VCalendarTest.php index dbf2fef0a..d34e12d2b 100644 --- a/tests/VObject/Component/VCalendarTest.php +++ b/tests/VObject/Component/VCalendarTest.php @@ -5,6 +5,7 @@ use DateTimeZone; use PHPUnit\Framework\TestCase; use Sabre\VObject; +use Sabre\VObject\InvalidDataException; class VCalendarTest extends TestCase { @@ -330,11 +331,9 @@ public function expandData() return $tests; } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testBrokenEventExpand() { + $this->expectException(InvalidDataException::class); $input = 'BEGIN:VCALENDAR CALSCALE:GREGORIAN VERSION:2.0 @@ -351,6 +350,33 @@ public function testBrokenEventExpand() ); } + /** + * This test used to induce an infinite loop. + * The "medium" annotation means that phpunit will fail the + * test if it takes longer than a default of 10 seconds. + * + * @medium + */ + public function testEventExpandYearly() + { + $input = 'BEGIN:VCALENDAR +BEGIN:VEVENT +UID:1a093f1012086078fdd3d9df5ff4d7d0 +DTSTART;TZID=UTC:20210203T130000 +DTEND;TZID=UTC:20210203T140000 +RRULE:FREQ=YEARLY;COUNT=7;WKST=MO;BYDAY=MO;BYWEEKNO=13,15,50 +END:VEVENT +END:VCALENDAR +'; + $vcal = VObject\Reader::read($input); + $events = $vcal->expand( + new \DateTime('2021-01-01'), + new \DateTime('2023-01-01') + ); + + $this->assertCount(7, $events->VEVENT); + } + public function testGetDocumentType() { $vcard = new VCalendar(); diff --git a/tests/VObject/Component/VCardTest.php b/tests/VObject/Component/VCardTest.php index 1895ce6c5..d8e6110b6 100644 --- a/tests/VObject/Component/VCardTest.php +++ b/tests/VObject/Component/VCardTest.php @@ -135,8 +135,8 @@ public function testGetByType() $vcard = VObject\Reader::read($vcard); $this->assertEquals('1@example.org', $vcard->getByType('EMAIL', 'home')->getValue()); $this->assertEquals('2@example.org', $vcard->getByType('EMAIL', 'work')->getValue()); - $this->assertNull($vcard->getByType('EMAIL', 'non-existant')); - $this->assertNull($vcard->getByType('ADR', 'non-existant')); + $this->assertNull($vcard->getByType('EMAIL', 'non-existent')); + $this->assertNull($vcard->getByType('ADR', 'non-existent')); } public function testPreferredNoPref() @@ -204,7 +204,7 @@ public function testNoUIDCardDAV() VCF; $this->assertValidate( $vcard, - VCARD::PROFILE_CARDDAV, + VCard::PROFILE_CARDDAV, 3, 'vCards on CardDAV servers MUST have a UID property.' ); @@ -236,7 +236,7 @@ public function testNoUIDNoCardDAVRepair() VCF; $this->assertValidate( $vcard, - VCARD::REPAIR, + VCard::REPAIR, 1, 'Adding a UID to a vCard property is recommended.' ); @@ -253,7 +253,7 @@ public function testVCard21CardDAV() VCF; $this->assertValidate( $vcard, - VCARD::PROFILE_CARDDAV, + VCard::PROFILE_CARDDAV, 3, 'CardDAV servers are not allowed to accept vCard 2.1.' ); diff --git a/tests/VObject/Component/VTimeZoneTest.php b/tests/VObject/Component/VTimeZoneTest.php index ead22b81e..af9469d36 100644 --- a/tests/VObject/Component/VTimeZoneTest.php +++ b/tests/VObject/Component/VTimeZoneTest.php @@ -51,4 +51,25 @@ public function testGetTimeZone() $obj->VTIMEZONE->getTimeZone() ); } + + public function testGetEmptyTimeZone() + { + $input = <<assertEquals( + $tz, + $obj->VTIMEZONE->getTimeZone() + ); + } } diff --git a/tests/VObject/ComponentTest.php b/tests/VObject/ComponentTest.php index 8c0a0d7c6..cf3e196dc 100644 --- a/tests/VObject/ComponentTest.php +++ b/tests/VObject/ComponentTest.php @@ -21,10 +21,13 @@ public function testIterate() $count = 0; foreach ($comp->children() as $key => $subcomponent) { ++$count; - $this->assertInstanceOf('Sabre\\VObject\\Component', $subcomponent); + $this->assertInstanceOf(Component::class, $subcomponent); + + if (2 === $count) { + $this->assertEquals(1, $key); + } } $this->assertEquals(2, $count); - $this->assertEquals(1, $key); } public function testMagicGet() @@ -38,10 +41,10 @@ public function testMagicGet() $comp->add($sub); $event = $comp->vevent; - $this->assertInstanceOf('Sabre\\VObject\\Component', $event); + $this->assertInstanceOf(Component::class, $event); $this->assertEquals('VEVENT', $event->name); - $this->assertInternalType('null', $comp->vjournal); + $this->assertNull($comp->vjournal); } public function testMagicGetGroups() @@ -69,6 +72,23 @@ public function testMagicGetGroups() $this->assertEquals(null, $email3[0]->group); } + public function testAddGroupProperties() + { + $comp = new VCard([ + 'VERSION' => '3.0', + 'item2.X-ABLabel' => 'item2-Foo', + ]); + + $comp->{'ITEM1.X-ABLabel'} = 'ITEM1-Foo'; + + foreach (['item2', 'ITEM1'] as $group) { + $prop = $comp->{"$group.X-ABLabel"}; + $this->assertInstanceOf(Property::class, $prop); + $this->assertSame("$group-Foo", (string) $prop); + $this->assertSame($group, $prop->group); + } + } + public function testMagicIsset() { $comp = new VCalendar(); @@ -89,7 +109,7 @@ public function testMagicSetScalar() $comp = new VCalendar(); $comp->myProp = 'myValue'; - $this->assertInstanceOf('Sabre\\VObject\\Property', $comp->MYPROP); + $this->assertInstanceOf(Property::class, $comp->MYPROP); $this->assertEquals('myValue', (string) $comp->MYPROP); } @@ -100,7 +120,7 @@ public function testMagicSetScalarTwice() $comp->myProp = 'myValue'; $this->assertEquals(1, count($comp->children())); - $this->assertInstanceOf('Sabre\\VObject\\Property', $comp->MYPROP); + $this->assertInstanceOf(Property::class, $comp->MYPROP); $this->assertEquals('myValue', (string) $comp->MYPROP); } @@ -109,7 +129,7 @@ public function testMagicSetArray() $comp = new VCalendar(); $comp->ORG = ['Acme Inc', 'Section 9']; - $this->assertInstanceOf('Sabre\\VObject\\Property', $comp->ORG); + $this->assertInstanceOf(Property::class, $comp->ORG); $this->assertEquals(['Acme Inc', 'Section 9'], $comp->ORG->getParts()); } @@ -174,20 +194,16 @@ public function testArrayAccessExists() $this->assertTrue(isset($comp->vevent[1])); } - /** - * @expectedException \LogicException - */ public function testArrayAccessSet() { + $this->expectException(\LogicException::class); $comp = new VCalendar(); $comp['hey'] = 'hi there'; } - /** - * @expectedException \LogicException - */ public function testArrayAccessUnset() { + $this->expectException(\LogicException::class); $comp = new VCalendar(); unset($comp[0]); } @@ -217,7 +233,7 @@ public function testAddScalarParams() $bla = $comp->children()[0]; - $this->assertInstanceOf('Sabre\\VObject\\Property', $bla); + $this->assertInstanceOf(Property::class, $bla); $this->assertEquals('MYPROP', $bla->name); $this->assertEquals('value', (string) $bla); @@ -250,20 +266,16 @@ public function testAddComponentTwice() $this->assertEquals('VEVENT', $comp->VEVENT->name); } - /** - * @expectedException \InvalidArgumentException - */ public function testAddArgFail() { + $this->expectException(\InvalidArgumentException::class); $comp = new VCalendar(); $comp->add($comp->createComponent('VEVENT'), 'hello'); } - /** - * @expectedException \InvalidArgumentException - */ public function testAddArgFail2() { + $this->expectException(\InvalidArgumentException::class); $comp = new VCalendar(); $comp->add([]); } @@ -293,7 +305,7 @@ public function testChildren() $comp->add($comp->createComponent('VTODO')); $r = $comp->children(); - $this->assertInternalType('array', $r); + $this->assertIsArray($r); $this->assertEquals(2, count($r)); } @@ -305,7 +317,7 @@ public function testGetComponents() $comp->add($comp->createComponent('VTODO')); $r = $comp->getComponents(); - $this->assertInternalType('array', $r); + $this->assertIsArray($r); $this->assertEquals(1, count($r)); $this->assertEquals('VTODO', $r[0]->name); } @@ -414,11 +426,9 @@ public function testRemoveByObj() $this->assertTrue(isset($comp->prop1)); } - /** - * @expectedException \InvalidArgumentException - */ public function testRemoveNotFound() { + $this->expectException(\InvalidArgumentException::class); $comp = new VCalendar([], false); $prop = $comp->createProperty('A', 'B'); $comp->remove($prop); diff --git a/tests/VObject/DateTimeParserTest.php b/tests/VObject/DateTimeParserTest.php index 44fba80c5..ede81e321 100644 --- a/tests/VObject/DateTimeParserTest.php +++ b/tests/VObject/DateTimeParserTest.php @@ -31,11 +31,9 @@ public function testParseICalendarDurationDateInterval() $this->assertEquals($expected, DateTimeParser::parseDuration('-PT3M')); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testParseICalendarDurationFail() { + $this->expectException(InvalidDataException::class); DateTimeParser::parseDuration('P1X', true); } @@ -50,19 +48,19 @@ public function testParseICalendarDateTime() /** * @depends testParseICalendarDateTime - * @expectedException \Sabre\VObject\InvalidDataException */ public function testParseICalendarDateTimeBadFormat() { + $this->expectException(InvalidDataException::class); $dateTime = DateTimeParser::parseDateTime('20100316T141405 '); } /** * @depends testParseICalendarDateTime - * @expectedException \Sabre\VObject\InvalidDataException */ public function testParseICalendarDateTimeInvalidTime() { + $this->expectException(InvalidDataException::class); $dateTime = DateTimeParser::parseDateTime('20100316T251405'); } @@ -143,19 +141,19 @@ public function testParseICalendarDateTimeGreaterThan4000() /** * @depends testParseICalendarDate - * @expectedException \Sabre\VObject\InvalidDataException */ public function testParseICalendarDateBadFormat() { + $this->expectException(InvalidDataException::class); $dateTime = DateTimeParser::parseDate('20100316T141405'); } /** * @depends testParseICalendarDate - * @expectedException \Sabre\VObject\InvalidDataException */ public function testParseICalendarDateInvalidDate() { + $this->expectException(InvalidDataException::class); $dateTime = DateTimeParser::parseDate('20101331'); } @@ -170,19 +168,15 @@ public function testVCardDate($input, $output) ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testBadVCardDate() { + $this->expectException(InvalidDataException::class); DateTimeParser::parseVCardDateTime('1985---01'); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testBadVCardTime() { + $this->expectException(InvalidDataException::class); DateTimeParser::parseVCardTime('23:12:166'); } @@ -414,7 +408,7 @@ public function vcardDates() ]; } - public function testDateAndOrTime_DateWithYearMonthDay() + public function testDateAndOrTimeDateWithYearMonthDay() { $this->assertDateAndOrTimeEqualsTo( '20150128', @@ -426,7 +420,7 @@ public function testDateAndOrTime_DateWithYearMonthDay() ); } - public function testDateAndOrTime_DateWithYearMonth() + public function testDateAndOrTimeDateWithYearMonth() { $this->assertDateAndOrTimeEqualsTo( '2015-01', @@ -437,7 +431,7 @@ public function testDateAndOrTime_DateWithYearMonth() ); } - public function testDateAndOrTime_DateWithMonth() + public function testDateAndOrTimeDateWithMonth() { $this->assertDateAndOrTimeEqualsTo( '--01', @@ -447,7 +441,7 @@ public function testDateAndOrTime_DateWithMonth() ); } - public function testDateAndOrTime_DateWithMonthDay() + public function testDateAndOrTimeDateWithMonthDay() { $this->assertDateAndOrTimeEqualsTo( '--0128', @@ -458,7 +452,7 @@ public function testDateAndOrTime_DateWithMonthDay() ); } - public function testDateAndOrTime_DateWithDay() + public function testDateAndOrTimeDateWithDay() { $this->assertDateAndOrTimeEqualsTo( '---28', @@ -468,7 +462,7 @@ public function testDateAndOrTime_DateWithDay() ); } - public function testDateAndOrTime_TimeWithHour() + public function testDateAndOrTimeTimeWithHour() { $this->assertDateAndOrTimeEqualsTo( '13', @@ -478,7 +472,7 @@ public function testDateAndOrTime_TimeWithHour() ); } - public function testDateAndOrTime_TimeWithHourMinute() + public function testDateAndOrTimeTimeWithHourMinute() { $this->assertDateAndOrTimeEqualsTo( '1353', @@ -489,7 +483,7 @@ public function testDateAndOrTime_TimeWithHourMinute() ); } - public function testDateAndOrTime_TimeWithHourSecond() + public function testDateAndOrTimeTimeWithHourSecond() { $this->assertDateAndOrTimeEqualsTo( '135301', @@ -501,7 +495,7 @@ public function testDateAndOrTime_TimeWithHourSecond() ); } - public function testDateAndOrTime_TimeWithMinute() + public function testDateAndOrTimeTimeWithMinute() { $this->assertDateAndOrTimeEqualsTo( '-53', @@ -511,7 +505,7 @@ public function testDateAndOrTime_TimeWithMinute() ); } - public function testDateAndOrTime_TimeWithMinuteSecond() + public function testDateAndOrTimeTimeWithMinuteSecond() { $this->assertDateAndOrTimeEqualsTo( '-5301', @@ -522,7 +516,7 @@ public function testDateAndOrTime_TimeWithMinuteSecond() ); } - public function testDateAndOrTime_TimeWithSecond() + public function testDateAndOrTimeTimeWithSecond() { $this->assertTrue(true); @@ -532,7 +526,7 @@ public function testDateAndOrTime_TimeWithSecond() */ } - public function testDateAndOrTime_TimeWithSecondZ() + public function testDateAndOrTimeTimeWithSecondZ() { $this->assertDateAndOrTimeEqualsTo( '--01Z', @@ -543,7 +537,7 @@ public function testDateAndOrTime_TimeWithSecondZ() ); } - public function testDateAndOrTime_TimeWithSecondTZ() + public function testDateAndOrTimeTimeWithSecondTZ() { $this->assertDateAndOrTimeEqualsTo( '--01+1234', @@ -554,7 +548,7 @@ public function testDateAndOrTime_TimeWithSecondTZ() ); } - public function testDateAndOrTime_DateTimeWithYearMonthDayHour() + public function testDateAndOrTimeDateTimeWithYearMonthDayHour() { $this->assertDateAndOrTimeEqualsTo( '20150128T13', @@ -567,7 +561,7 @@ public function testDateAndOrTime_DateTimeWithYearMonthDayHour() ); } - public function testDateAndOrTime_DateTimeWithMonthDayHour() + public function testDateAndOrTimeDateTimeWithMonthDayHour() { $this->assertDateAndOrTimeEqualsTo( '--0128T13', @@ -579,7 +573,7 @@ public function testDateAndOrTime_DateTimeWithMonthDayHour() ); } - public function testDateAndOrTime_DateTimeWithDayHour() + public function testDateAndOrTimeDateTimeWithDayHour() { $this->assertDateAndOrTimeEqualsTo( '---28T13', @@ -590,7 +584,7 @@ public function testDateAndOrTime_DateTimeWithDayHour() ); } - public function testDateAndOrTime_DateTimeWithDayHourMinute() + public function testDateAndOrTimeDateTimeWithDayHourMinute() { $this->assertDateAndOrTimeEqualsTo( '---28T1353', @@ -602,7 +596,7 @@ public function testDateAndOrTime_DateTimeWithDayHourMinute() ); } - public function testDateAndOrTime_DateTimeWithDayHourMinuteSecond() + public function testDateAndOrTimeDateTimeWithDayHourMinuteSecond() { $this->assertDateAndOrTimeEqualsTo( '---28T135301', @@ -615,7 +609,7 @@ public function testDateAndOrTime_DateTimeWithDayHourMinuteSecond() ); } - public function testDateAndOrTime_DateTimeWithDayHourZ() + public function testDateAndOrTimeDateTimeWithDayHourZ() { $this->assertDateAndOrTimeEqualsTo( '---28T13Z', @@ -627,7 +621,7 @@ public function testDateAndOrTime_DateTimeWithDayHourZ() ); } - public function testDateAndOrTime_DateTimeWithDayHourTZ() + public function testDateAndOrTimeDateTimeWithDayHourTZ() { $this->assertDateAndOrTimeEqualsTo( '---28T13+1234', diff --git a/tests/VObject/DocumentTest.php b/tests/VObject/DocumentTest.php index 2665406f6..f2698f65f 100644 --- a/tests/VObject/DocumentTest.php +++ b/tests/VObject/DocumentTest.php @@ -24,11 +24,11 @@ public function testCreateComponent() $event = $vcal->createComponent('VEVENT'); - $this->assertInstanceOf('Sabre\VObject\Component\VEvent', $event); + $this->assertInstanceOf(Component\VEvent::class, $event); $vcal->add($event); $prop = $vcal->createProperty('X-PROP', '1234256', ['X-PARAM' => '3']); - $this->assertInstanceOf('Sabre\VObject\Property', $prop); + $this->assertInstanceOf(Property::class, $prop); $event->add($prop); @@ -46,16 +46,16 @@ public function testCreate() $vcal = new Component\VCalendar([], false); $event = $vcal->create('VEVENT'); - $this->assertInstanceOf('Sabre\VObject\Component\VEvent', $event); + $this->assertInstanceOf(Component\VEvent::class, $event); $prop = $vcal->create('CALSCALE'); - $this->assertInstanceOf('Sabre\VObject\Property\Text', $prop); + $this->assertInstanceOf(Property\Text::class, $prop); } public function testGetClassNameForPropertyValue() { $vcal = new Component\VCalendar([], false); - $this->assertEquals('Sabre\\VObject\\Property\\Text', $vcal->getClassNameForPropertyValue('TEXT')); + $this->assertEquals(Property\Text::class, $vcal->getClassNameForPropertyValue('TEXT')); $this->assertNull($vcal->getClassNameForPropertyValue('FOO')); } @@ -64,7 +64,7 @@ public function testDestroy() $vcal = new Component\VCalendar([], false); $event = $vcal->createComponent('VEVENT'); - $this->assertInstanceOf('Sabre\VObject\Component\VEvent', $event); + $this->assertInstanceOf(Component\VEvent::class, $event); $vcal->add($event); $prop = $vcal->createProperty('X-PROP', '1234256', ['X-PARAM' => '3']); diff --git a/tests/VObject/ElementListTest.php b/tests/VObject/ElementListTest.php index 1842ca963..f3bb8f2bb 100644 --- a/tests/VObject/ElementListTest.php +++ b/tests/VObject/ElementListTest.php @@ -22,9 +22,12 @@ public function testIterate() $count = 0; foreach ($elemList as $key => $subcomponent) { ++$count; - $this->assertInstanceOf('Sabre\\VObject\\Component', $subcomponent); + $this->assertInstanceOf(Component::class, $subcomponent); + + if (3 === $count) { + $this->assertEquals(2, $key); + } } $this->assertEquals(3, $count); - $this->assertEquals(2, $key); } } diff --git a/tests/VObject/EmptyParameterTest.php b/tests/VObject/EmptyParameterTest.php index 213e69ab8..52fe878e2 100644 --- a/tests/VObject/EmptyParameterTest.php +++ b/tests/VObject/EmptyParameterTest.php @@ -20,7 +20,7 @@ public function testRead() $vcard = Reader::read($input); - $this->assertInstanceOf('Sabre\\VObject\\Component\\VCard', $vcard); + $this->assertInstanceOf(Component\VCard::class, $vcard); $vcard = $vcard->convert(\Sabre\VObject\Document::VCARD30); $vcard = $vcard->serialize(); diff --git a/tests/VObject/EmptyValueIssueTest.php b/tests/VObject/EmptyValueIssueTest.php index 91a4d84f6..0798d9c4a 100644 --- a/tests/VObject/EmptyValueIssueTest.php +++ b/tests/VObject/EmptyValueIssueTest.php @@ -17,7 +17,7 @@ public function testDecodeValue() BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT -DESCRIPTION:This is a descpription\\nwith a linebreak and a \\; \\, and : +DESCRIPTION:This is a description\\nwith a linebreak and a \\; \\, and : END:VEVENT END:VCALENDAR ICS; @@ -25,6 +25,6 @@ public function testDecodeValue() $vobj = Reader::read($input); // Before this bug was fixed, getValue() would return nothing. - $this->assertEquals("This is a descpription\nwith a linebreak and a ; , and :", $vobj->VEVENT->DESCRIPTION->getValue()); + $this->assertEquals("This is a description\nwith a linebreak and a ; , and :", $vobj->VEVENT->DESCRIPTION->getValue()); } } diff --git a/tests/VObject/FreeBusyGeneratorTest.php b/tests/VObject/FreeBusyGeneratorTest.php index 323cf632b..4700a2800 100644 --- a/tests/VObject/FreeBusyGeneratorTest.php +++ b/tests/VObject/FreeBusyGeneratorTest.php @@ -22,15 +22,13 @@ public function testGeneratorBaseObject() $this->assertEquals('PUBLISH', $result->METHOD->getValue()); } - /** - * @expectedException \InvalidArgumentException - */ public function testInvalidArg() { + $this->expectException(\InvalidArgumentException::class); $gen = new FreeBusyGenerator( new \DateTime('2012-01-01'), new \DateTime('2012-12-31'), - new \StdClass() + new \stdClass() ); } diff --git a/tests/VObject/ICalendar/AttachParseTest.php b/tests/VObject/ICalendar/AttachParseTest.php index a32a2462e..e6e3d8685 100644 --- a/tests/VObject/ICalendar/AttachParseTest.php +++ b/tests/VObject/ICalendar/AttachParseTest.php @@ -3,6 +3,7 @@ namespace Sabre\VObject\ICalendar; use PHPUnit\Framework\TestCase; +use Sabre\VObject\Property\Uri; use Sabre\VObject\Reader; class AttachParseTest extends TestCase @@ -23,7 +24,7 @@ public function testParseAttach() $vcal = Reader::read($vcal); $prop = $vcal->VEVENT->ATTACH; - $this->assertInstanceOf('Sabre\\VObject\\Property\\URI', $prop); + $this->assertInstanceOf(Uri::class, $prop); $this->assertEquals('ftp://example.com/pub/reports/r-960812.ps', $prop->getValue()); } } diff --git a/tests/VObject/ITip/BrokerAttendeeReplyTest.php b/tests/VObject/ITip/BrokerAttendeeReplyTest.php index cd0c7bb66..284075adf 100644 --- a/tests/VObject/ITip/BrokerAttendeeReplyTest.php +++ b/tests/VObject/ITip/BrokerAttendeeReplyTest.php @@ -68,6 +68,124 @@ public function testAccepted() $this->parse($oldMessage, $newMessage, $expected); } + public function testAcceptedWithTz() + { + $oldMessage = << 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<parse($oldMessage, $newMessage, $expected); + } + public function testRecurringReply() { $oldMessage = <<parse(null, $message, $expected, 'mailto:strunk@example.org'); } - /** - * @expectedException \Sabre\VObject\ITip\ITipException - */ public function testBrokenEventUIDMisMatch() { + $this->expectException(ITipException::class); $message = <<parse(null, $message, [], 'mailto:strunk@example.org'); } - /** - * @expectedException \Sabre\VObject\ITip\ITipException - */ public function testBrokenEventOrganizerMisMatch() { + $this->expectException(ITipException::class); $message = <<parse(null, $message, [], 'mailto:strunk@example.org'); } - /** - * @expectedException \Sabre\VObject\ITip\ITipException - */ public function testMultipleUID() { + $this->expectException(ITipException::class); $message = <<parse(null, $message, [], 'mailto:strunk@example.org'); } - /** - * @expectedException \Sabre\VObject\ITip\SameOrganizerForAllComponentsException - */ public function testChangingOrganizers() { + $this->expectException(SameOrganizerForAllComponentsException::class); $message = <<process($itip, $old, $expected); } + public function testReplyWithTz() + { + $itip = <<process($itip, $old, $expected); + } + public function testReplyRequestStatus() { $itip = <<process($itip, $old, $expected); } - public function testReplyPartyCrashCreateExcepton() + public function testReplyPartyCrashCreateException() { // IN this test there's a recurring event that has an exception. The // exception is missing the attendee. diff --git a/tests/VObject/ITip/BrokerSignificantChangesTest.php b/tests/VObject/ITip/BrokerSignificantChangesTest.php index a225cb98c..a20d55025 100644 --- a/tests/VObject/ITip/BrokerSignificantChangesTest.php +++ b/tests/VObject/ITip/BrokerSignificantChangesTest.php @@ -105,4 +105,112 @@ public function testSignificantChangesRRuleOrderNoChange() $this->parse($old, $new, $expected, 'mailto:martin@fruux.com'); } + + /** + * Check significant changes detection (no change). + * Reordering of the attendees should not be a significant change (#540) + * https://github.com/sabre-io/vobject/issues/540. + */ + public function testSignificantChangesAttendeesOrderNoChange() + { + $old = << false]; + $expected[] = ['significantChange' => false]; + + $this->parse($old, $new, $expected, 'mailto:martin@fruux.com'); + } + + /** + * Check significant changes detection (no change). + * Reordering of vevent in a recurring event with exceptions should + * not be a significant change + * https://github.com/sabre-io/vobject/issues/542. + */ + public function testSignificantChangesVeventOrderNoChange() + { + $vevent1 = << false]]; + + $this->parse($old, $new, $expected, 'mailto:martin@fruux.com'); + } } diff --git a/tests/VObject/ITip/BrokerTester.php b/tests/VObject/ITip/BrokerTester.php index fcccd75d8..9e9956e03 100644 --- a/tests/VObject/ITip/BrokerTester.php +++ b/tests/VObject/ITip/BrokerTester.php @@ -45,8 +45,11 @@ public function process($input, $existingObject = null, $expected = false) $vcal = Reader::read($input); + $mainComponent = new \Sabre\VObject\Component\VEvent($vcal, 'VEVENT'); foreach ($vcal->getComponents() as $mainComponent) { - break; + if ('VEVENT' === $mainComponent->name) { + break; + } } $message = new Message(); diff --git a/tests/VObject/Issue36WorkAroundTest.php b/tests/VObject/Issue36WorkAroundTest.php index 332aace39..1afd3d184 100644 --- a/tests/VObject/Issue36WorkAroundTest.php +++ b/tests/VObject/Issue36WorkAroundTest.php @@ -34,6 +34,6 @@ public function testWorkaround() // If this does not throw an exception, it's all good. $it = new Recur\EventIterator($obj, '1833bd44-188b-405c-9f85-1a12105318aa'); - $this->assertInstanceOf('Sabre\\VObject\\Recur\\EventIterator', $it); + $this->assertInstanceOf(Recur\EventIterator::class, $it); } } diff --git a/tests/VObject/Issue64Test.php b/tests/VObject/Issue64Test.php index 9dc2bb984..2e623baa8 100644 --- a/tests/VObject/Issue64Test.php +++ b/tests/VObject/Issue64Test.php @@ -14,6 +14,6 @@ public function testRead() $converted = Reader::read($vcard); - $this->assertInstanceOf('Sabre\\VObject\\Component\\VCard', $converted); + $this->assertInstanceOf(Component\VCard::class, $converted); } } diff --git a/tests/VObject/Issue96Test.php b/tests/VObject/Issue96Test.php index 22d1fed2f..88803a3e0 100644 --- a/tests/VObject/Issue96Test.php +++ b/tests/VObject/Issue96Test.php @@ -18,7 +18,7 @@ public function testRead() VCF; $vcard = Reader::read($input, Reader::OPTION_FORGIVING); - $this->assertInstanceOf('Sabre\\VObject\\Component\\VCard', $vcard); + $this->assertInstanceOf(Component\VCard::class, $vcard); $this->assertEquals('http://www.example.org', $vcard->URL->getValue()); } } diff --git a/tests/VObject/IssueUndefinedIndexTest.php b/tests/VObject/IssueUndefinedIndexTest.php index 5a70b0885..7ed214a19 100644 --- a/tests/VObject/IssueUndefinedIndexTest.php +++ b/tests/VObject/IssueUndefinedIndexTest.php @@ -6,11 +6,9 @@ class IssueUndefinedIndexTest extends TestCase { - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testRead() { + $this->expectException(ParseException::class); $input = << 'WEEKLY', 'byday' => ['MO', 'TU'], ], ], [ - 'x-bool', new \StdClass(), 'boolean', true, + 'x-bool', new \stdClass(), 'boolean', true, ], [ - 'x-time', new \StdClass(), 'time', '08:00:00', + 'x-time', new \stdClass(), 'time', '08:00:00', ], [ - 'attach', new \StdClass(), 'binary', base64_encode('attachment'), + 'attach', new \stdClass(), 'binary', base64_encode('attachment'), ], [ 'request-status', - new \StdClass(), + new \stdClass(), 'text', ['2.0', 'Success'], ], [ 'request-status', - new \StdClass(), + new \stdClass(), 'text', ['3.7', 'Invalid Calendar User', 'ATTENDEE:mailto:jsmith@example.org'], ], [ 'dtend', - new \StdClass(), + new \stdClass(), 'date-time', '2015-01-08T13:30:00', ], diff --git a/tests/VObject/JCardTest.php b/tests/VObject/JCardTest.php index 2100a07a9..1864f666d 100644 --- a/tests/VObject/JCardTest.php +++ b/tests/VObject/JCardTest.php @@ -48,25 +48,25 @@ public function testToJCard() [ [ 'version', - new \StdClass(), + new \stdClass(), 'text', '4.0', ], [ 'prodid', - new \StdClass(), + new \stdClass(), 'text', '-//Sabre//Sabre VObject '.Version::VERSION.'//EN', ], [ 'uid', - new \StdClass(), + new \stdClass(), 'text', 'foo', ], [ 'bday', - new \StdClass(), + new \stdClass(), 'date-and-or-time', '1985-04-07', ], @@ -80,25 +80,25 @@ public function testToJCard() ], [ 'bday', - new \StdClass(), + new \stdClass(), 'date-time', '1979-12-25T02:00:00', ], [ 'rev', - new \StdClass(), + new \stdClass(), 'timestamp', '1995-10-31T22:27:10Z', ], [ 'lang', - new \StdClass(), + new \stdClass(), 'language-tag', 'nl', ], [ 'n', - new \StdClass(), + new \stdClass(), 'text', ['Last', 'First', 'Middle', '', ''], ], @@ -120,7 +120,7 @@ public function testToJCard() ], [ 'adr', - new \StdClass(), + new \stdClass(), 'text', [ '', @@ -134,55 +134,55 @@ public function testToJCard() ], [ 'x-truncated', - new \StdClass(), + new \stdClass(), 'date', '--12-25', ], [ 'x-time-local', - new \StdClass(), + new \stdClass(), 'time', '12:30:00', ], [ 'x-time-utc', - new \StdClass(), + new \stdClass(), 'time', '12:30:00Z', ], [ 'x-time-offset', - new \StdClass(), + new \stdClass(), 'time', '12:30:00-08:00', ], [ 'x-time-reduced', - new \StdClass(), + new \stdClass(), 'time', '23', ], [ 'x-time-truncated', - new \StdClass(), + new \stdClass(), 'time', '--30', ], [ 'x-karma-points', - new \StdClass(), + new \stdClass(), 'integer', 42, ], [ 'x-grade', - new \StdClass(), + new \stdClass(), 'float', 1.3, ], [ 'tz', - new \StdClass(), + new \stdClass(), 'utc-offset', '-05:00', ], diff --git a/tests/VObject/Parser/JsonTest.php b/tests/VObject/Parser/JsonTest.php index 587f55f70..e1c701489 100644 --- a/tests/VObject/Parser/JsonTest.php +++ b/tests/VObject/Parser/JsonTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Sabre\VObject; +use Sabre\VObject\ParseException; class JsonTest extends TestCase { @@ -14,25 +15,25 @@ public function testRoundTripJCard() [ [ 'version', - new \StdClass(), + new \stdClass(), 'text', '4.0', ], [ 'prodid', - new \StdClass(), + new \stdClass(), 'text', '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', ], [ 'uid', - new \StdClass(), + new \stdClass(), 'text', 'foo', ], [ 'bday', - new \StdClass(), + new \stdClass(), 'date-and-or-time', '1985-04-07', ], @@ -46,25 +47,25 @@ public function testRoundTripJCard() ], [ 'bday', - new \StdClass(), + new \stdClass(), 'date-time', '1979-12-25T02:00:00', ], [ 'rev', - new \StdClass(), + new \stdClass(), 'timestamp', '1995-10-31T22:27:10Z', ], [ 'lang', - new \StdClass(), + new \stdClass(), 'language-tag', 'nl', ], [ 'n', - new \StdClass(), + new \stdClass(), 'text', ['Last', 'First', 'Middle', '', ''], ], @@ -86,7 +87,7 @@ public function testRoundTripJCard() ], [ 'adr', - new \StdClass(), + new \stdClass(), 'text', [ '', @@ -101,55 +102,55 @@ public function testRoundTripJCard() [ 'x-truncated', - new \StdClass(), + new \stdClass(), 'date', '--12-25', ], [ 'x-time-local', - new \StdClass(), + new \stdClass(), 'time', '12:30:00', ], [ 'x-time-utc', - new \StdClass(), + new \stdClass(), 'time', '12:30:00Z', ], [ 'x-time-offset', - new \StdClass(), + new \stdClass(), 'time', '12:30:00-08:00', ], [ 'x-time-reduced', - new \StdClass(), + new \stdClass(), 'time', '23', ], [ 'x-time-truncated', - new \StdClass(), + new \stdClass(), 'time', '--30', ], [ 'x-karma-points', - new \StdClass(), + new \stdClass(), 'integer', 42, ], [ 'x-grade', - new \StdClass(), + new \stdClass(), 'float', 1.3, ], [ 'tz', - new \StdClass(), + new \stdClass(), 'utc-offset', '-05:00', ], @@ -203,19 +204,19 @@ public function testRoundTripJCal() [ [ 'version', - new \StdClass(), + new \stdClass(), 'text', '2.0', ], [ 'prodid', - new \StdClass(), + new \stdClass(), 'text', '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', ], [ 'calscale', - new \StdClass(), + new \stdClass(), 'text', 'GREGORIAN', ], @@ -224,25 +225,25 @@ public function testRoundTripJCal() ['vevent', [ [ - 'uid', new \StdClass(), 'text', 'foo', + 'uid', new \stdClass(), 'text', 'foo', ], [ - 'dtstart', new \StdClass(), 'date', '2013-05-26', + 'dtstart', new \stdClass(), 'date', '2013-05-26', ], [ - 'duration', new \StdClass(), 'duration', 'P1D', + 'duration', new \stdClass(), 'duration', 'P1D', ], [ - 'categories', new \StdClass(), 'text', 'home', 'testing', + 'categories', new \stdClass(), 'text', 'home', 'testing', ], [ - 'created', new \StdClass(), 'date-time', '2013-05-26T18:10:00Z', + 'created', new \stdClass(), 'date-time', '2013-05-26T18:10:00Z', ], [ - 'attach', new \StdClass(), 'binary', base64_encode('attachment'), + 'attach', new \stdClass(), 'binary', base64_encode('attachment'), ], [ - 'attendee', new \StdClass(), 'cal-address', 'mailto:armin@example.org', + 'attendee', new \stdClass(), 'cal-address', 'mailto:armin@example.org', ], [ 'attendee', @@ -254,41 +255,41 @@ public function testRoundTripJCal() 'mailto:dominik@example.org', ], [ - 'geo', new \StdClass(), 'float', [51.96668, 7.61876], + 'geo', new \stdClass(), 'float', [51.96668, 7.61876], ], [ - 'sequence', new \StdClass(), 'integer', 5, + 'sequence', new \stdClass(), 'integer', 5, ], [ - 'freebusy', new \StdClass(), 'period', ['2013-05-26T21:02:13', 'PT1H'], ['2013-06-26T12:00:00', '2013-06-26T13:00:00'], + 'freebusy', new \stdClass(), 'period', ['2013-05-26T21:02:13', 'PT1H'], ['2013-06-26T12:00:00', '2013-06-26T13:00:00'], ], [ - 'url', new \StdClass(), 'uri', 'http://example.org/', + 'url', new \stdClass(), 'uri', 'http://example.org/', ], [ - 'tzoffsetfrom', new \StdClass(), 'utc-offset', '+05:00', + 'tzoffsetfrom', new \stdClass(), 'utc-offset', '+05:00', ], [ - 'rrule', new \StdClass(), 'recur', [ + 'rrule', new \stdClass(), 'recur', [ 'freq' => 'WEEKLY', 'byday' => ['MO', 'TU'], ], ], [ - 'x-bool', new \StdClass(), 'boolean', true, + 'x-bool', new \stdClass(), 'boolean', true, ], [ - 'x-time', new \StdClass(), 'time', '08:00:00', + 'x-time', new \stdClass(), 'time', '08:00:00', ], [ 'request-status', - new \StdClass(), + new \stdClass(), 'text', ['2.0', 'Success'], ], [ 'request-status', - new \StdClass(), + new \stdClass(), 'text', ['3.7', 'Invalid Calendar User', 'ATTENDEE:mailto:jsmith@example.org'], ], @@ -297,7 +298,7 @@ public function testRoundTripJCal() ['valarm', [ [ - 'action', new \StdClass(), 'text', 'DISPLAY', + 'action', new \stdClass(), 'text', 'DISPLAY', ], ], [], @@ -358,7 +359,7 @@ public function testParseStreamArg() 'vcard', [ [ - 'FN', new \StdClass(), 'text', 'foo', + 'FN', new \stdClass(), 'text', 'foo', ], ], ]; @@ -371,17 +372,15 @@ public function testParseStreamArg() $this->assertEquals('foo', $result->FN->getValue()); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testParseInvalidData() { + $this->expectException(ParseException::class); $json = new Json(); $input = [ 'vlist', [ [ - 'FN', new \StdClass(), 'text', 'foo', + 'FN', new \stdClass(), 'text', 'foo', ], ], ]; diff --git a/tests/VObject/Parser/MimeDirTest.php b/tests/VObject/Parser/MimeDirTest.php index 671c23064..183c9ce4c 100644 --- a/tests/VObject/Parser/MimeDirTest.php +++ b/tests/VObject/Parser/MimeDirTest.php @@ -3,6 +3,7 @@ namespace Sabre\VObject\Parser; use PHPUnit\Framework\TestCase; +use Sabre\VObject\ParseException; /** * Note that most MimeDir related tests can actually be found in the ReaderTest @@ -10,13 +11,11 @@ */ class MimeDirTest extends TestCase { - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testParseError() { + $this->expectException(ParseException::class); $mimeDir = new MimeDir(); - $mimeDir->parse(fopen(__FILE__, 'a')); + $mimeDir->parse(fopen(__FILE__, 'a+')); } public function testDecodeLatin1() @@ -80,20 +79,16 @@ public function testDontDecodeLatin1() $this->assertEquals("umlaut u - \xFC", $vcard->FN->getValue()); } - /** - * @expectedException \InvalidArgumentException - */ public function testDecodeUnsupportedCharset() { + $this->expectException(\InvalidArgumentException::class); $mimeDir = new MimeDir(); $mimeDir->setCharset('foobar'); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testDecodeUnsupportedInlineCharset() { + $this->expectException(ParseException::class); $vcard = <<assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCARD', $result->name); $this->assertEquals(1, count($result->children())); $this->assertEquals('Aachen', $this->getPropertyValue($result->LABEL)); @@ -24,7 +25,7 @@ public function testReadQuotedPrintableNewlineSoft() $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aa=\r\n ch=\r\n en\r\nEND:VCARD"; $result = Reader::read($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCARD', $result->name); $this->assertEquals(1, count($result->children())); $this->assertEquals('Aachen', $this->getPropertyValue($result->LABEL)); @@ -35,7 +36,7 @@ public function testReadQuotedPrintableNewlineHard() $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aachen=0D=0A=\r\n Germany\r\nEND:VCARD"; $result = Reader::read($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCARD', $result->name); $this->assertEquals(1, count($result->children())); $this->assertEquals("Aachen\r\nGermany", $this->getPropertyValue($result->LABEL)); @@ -46,7 +47,7 @@ public function testReadQuotedPrintableCompatibilityMS() $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aachen=0D=0A=\r\nDeutschland:okay\r\nEND:VCARD"; $result = Reader::read($data, Reader::OPTION_FORGIVING); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCARD', $result->name); $this->assertEquals(1, count($result->children())); $this->assertEquals("Aachen\r\nDeutschland:okay", $this->getPropertyValue($result->LABEL)); diff --git a/tests/VObject/Parser/UnfoldingTest.php b/tests/VObject/Parser/UnfoldingTest.php new file mode 100644 index 000000000..8d751ae7a --- /dev/null +++ b/tests/VObject/Parser/UnfoldingTest.php @@ -0,0 +1,99 @@ +parse($vcard, Reader::OPTION_FIX_UNFOLDING); + + $this->assertNotNull($vcard->children()[0]->{'X-APPLE-STRUCTURED-LOCATION'}->getValue()); + } + + public function testNotFixUnfolding() + { + $this->expectException(ParseException::class); + + $vcard = <<parse($vcard); + } + + public function testNotFixUnknownProperty() + { + $vcard = <<parse($vcard); + + $this->assertNotNull($vcard->children()[0]->CONFERENCE->getValue()); + } +} diff --git a/tests/VObject/Parser/XmlTest.php b/tests/VObject/Parser/XmlTest.php index e520185ba..46ee30ce2 100644 --- a/tests/VObject/Parser/XmlTest.php +++ b/tests/VObject/Parser/XmlTest.php @@ -262,7 +262,7 @@ public function testRFC6321Example2() /** * iCalendar Stream. */ - public function testRFC6321Section3_2() + public function testRFC6321Section3Part2() { $this->assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( @@ -466,7 +466,7 @@ public function testRFC6321Section3_4_1_3() /** * Values, Binary. */ - public function testRFC6321Section3_6_1() + public function testRFC6321Section3Part6Part1() { $this->assertXMLEqualsToMimeDir( <<assertXMLEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( << with a positive and a non-negative numbers. - $this->testRFC6321Section3_4_1_2(); + $this->testRFC6321Section3Part4Part1Part2(); } /** * Values, Integer. */ - public function testRFC6321Section3_6_8() + public function testRFC6321Section3Part6Part8() { $this->assertXMLEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( @@ -1259,7 +1259,7 @@ public function testRFC6351Section5Group() /** * Extensibility. */ - public function testRFC6351Section5_1_NoNamespace() + public function testRFC6351Section5Part1NoNamespace() { $this->assertXMLEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<assertXMLReflexivelyEqualsToMimeDir( <<expectException(\InvalidArgumentException::class); $vcard = new VObject\Component\VCard(['VERSION' => '3.0']); $vcard->add('PHOTO', ['a', 'b']); } diff --git a/tests/VObject/Property/FloatTest.php b/tests/VObject/Property/FloatTest.php index c5237c47b..0ba02470e 100644 --- a/tests/VObject/Property/FloatTest.php +++ b/tests/VObject/Property/FloatTest.php @@ -14,7 +14,7 @@ public function testMimeDir() $result = $mimeDir->parse($input); - $this->assertInstanceOf('Sabre\VObject\Property\FloatValue', $result->{'X-FLOAT'}); + $this->assertInstanceOf(FloatValue::class, $result->{'X-FLOAT'}); $this->assertEquals([ 0.234, diff --git a/tests/VObject/Property/ICalendar/DateTimeTest.php b/tests/VObject/Property/ICalendar/DateTimeTest.php index 33525ff33..aa584dba5 100644 --- a/tests/VObject/Property/ICalendar/DateTimeTest.php +++ b/tests/VObject/Property/ICalendar/DateTimeTest.php @@ -2,14 +2,16 @@ namespace Sabre\VObject\Property\ICalendar; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\InvalidDataException; class DateTimeTest extends TestCase { protected $vcal; - public function setUp() + public function setUp(): void { $this->vcal = new VCalendar(); } @@ -256,11 +258,9 @@ public function testGetDateTimeDateLOCALTZ() $this->assertEquals('Europe/Amsterdam', $dt->getTimeZone()->getName()); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testGetDateTimeDateInvalid() { + $this->expectException(InvalidDataException::class); $elem = $this->vcal->createProperty('DTSTART', 'bla'); $dt = $elem->getDateTime(); } @@ -305,12 +305,10 @@ public function testGetDateTimeBadTimeZone() $this->vcal->add($event); $this->vcal->add($timezone); - $dt = $elem->getDateTime(); - - $this->assertInstanceOf('DateTimeImmutable', $dt); - $this->assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); - $this->assertEquals('Canada/Eastern', $dt->getTimeZone()->getName()); date_default_timezone_set($default); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('We were unable to determine the correct PHP timezone for tzid: Moon'); + $elem->getDateTime(); } public function testUpdateValueParameter() diff --git a/tests/VObject/Property/ICalendar/RecurTest.php b/tests/VObject/Property/ICalendar/RecurTest.php index 818840605..3902a6e13 100644 --- a/tests/VObject/Property/ICalendar/RecurTest.php +++ b/tests/VObject/Property/ICalendar/RecurTest.php @@ -16,7 +16,7 @@ public function testParts() $vcal = new VCalendar(); $recur = $vcal->add('RRULE', 'FREQ=Daily'); - $this->assertInstanceOf('Sabre\VObject\Property\ICalendar\Recur', $recur); + $this->assertInstanceOf(Recur::class, $recur); $this->assertEquals(['FREQ' => 'DAILY'], $recur->getParts()); $recur->setParts(['freq' => 'MONTHLY']); @@ -24,11 +24,9 @@ public function testParts() $this->assertEquals(['FREQ' => 'MONTHLY'], $recur->getParts()); } - /** - * @expectedException \InvalidArgumentException - */ public function testSetValueBadVal() { + $this->expectException(\InvalidArgumentException::class); $vcal = new VCalendar(); $recur = $vcal->add('RRULE', 'FREQ=Daily'); $recur->setValue(new \Exception()); diff --git a/tests/VObject/Property/UriTest.php b/tests/VObject/Property/UriTest.php index 4ab32a736..d3d0e0f9c 100644 --- a/tests/VObject/Property/UriTest.php +++ b/tests/VObject/Property/UriTest.php @@ -21,6 +21,6 @@ public function testAlwaysEncodeUriVCalendar() END:VCALENDAR ICS; $output = Reader::read($input)->serialize(); - $this->assertContains('URL;VALUE=URI:http://example.org/', $output); + $this->assertStringContainsString('URL;VALUE=URI:http://example.org/', $output); } } diff --git a/tests/VObject/Property/VCard/DateAndOrTimeTest.php b/tests/VObject/Property/VCard/DateAndOrTimeTest.php index a59411e01..f21b408a1 100644 --- a/tests/VObject/Property/VCard/DateAndOrTimeTest.php +++ b/tests/VObject/Property/VCard/DateAndOrTimeTest.php @@ -105,11 +105,9 @@ public function testSetPartsDateTimeImmutable() $this->assertEquals('20140402T183700Z', $prop->getValue()); } - /** - * @expectedException \InvalidArgumentException - */ public function testSetPartsTooMany() { + $this->expectException(\InvalidArgumentException::class); $vcard = new VObject\Component\VCard(); $prop = $vcard->createProperty('BDAY'); diff --git a/tests/VObject/Property/VCard/LanguageTagTest.php b/tests/VObject/Property/VCard/LanguageTagTest.php index ffb65f434..54106ffe8 100644 --- a/tests/VObject/Property/VCard/LanguageTagTest.php +++ b/tests/VObject/Property/VCard/LanguageTagTest.php @@ -14,7 +14,7 @@ public function testMimeDir() $result = $mimeDir->parse($input); - $this->assertInstanceOf('Sabre\VObject\Property\VCard\LanguageTag', $result->LANG); + $this->assertInstanceOf(LanguageTag::class, $result->LANG); $this->assertEquals('nl', $result->LANG->getValue()); @@ -31,7 +31,7 @@ public function testChangeAndSerialize() $result = $mimeDir->parse($input); - $this->assertInstanceOf('Sabre\VObject\Property\VCard\LanguageTag', $result->LANG); + $this->assertInstanceOf(LanguageTag::class, $result->LANG); // This replicates what the vcard converter does and triggered a bug in // the past. $result->LANG->setValue(['de']); diff --git a/tests/VObject/Property/VCard/PhoneNumberTest.php b/tests/VObject/Property/VCard/PhoneNumberTest.php new file mode 100644 index 000000000..668bc7e4c --- /dev/null +++ b/tests/VObject/Property/VCard/PhoneNumberTest.php @@ -0,0 +1,19 @@ +assertInstanceOf(PhoneNumber::class, $vCard->TEL); + $this->assertEquals('PHONE-NUMBER', $vCard->TEL->getValueType()); + $this->assertEquals($input, $vCard->serialize()); + } +} diff --git a/tests/VObject/PropertyTest.php b/tests/VObject/PropertyTest.php index 1c2dc0830..1f6e07022 100644 --- a/tests/VObject/PropertyTest.php +++ b/tests/VObject/PropertyTest.php @@ -62,7 +62,7 @@ public function testParameterGet() $property = $cal->createProperty('propname', 'propvalue'); $property['paramname'] = 'paramvalue'; - $this->assertInstanceOf('Sabre\\VObject\\Parameter', $property['paramname']); + $this->assertInstanceOf(Parameter::class, $property['paramname']); } public function testParameterNotExists() @@ -71,7 +71,7 @@ public function testParameterNotExists() $property = $cal->createProperty('propname', 'propvalue'); $property['paramname'] = 'paramvalue'; - $this->assertInternalType('null', $property['foo']); + $this->assertNull($property['foo']); } public function testParameterMultiple() @@ -81,7 +81,7 @@ public function testParameterMultiple() $property['paramname'] = 'paramvalue'; $property->add('paramname', 'paramvalue'); - $this->assertInstanceOf('Sabre\\VObject\\Parameter', $property['paramname']); + $this->assertInstanceOf(Parameter::class, $property['paramname']); $this->assertEquals(2, count($property['paramname']->getParts())); } @@ -92,7 +92,7 @@ public function testSetParameterAsString() $property['paramname'] = 'paramvalue'; $this->assertEquals(1, count($property->parameters())); - $this->assertInstanceOf('Sabre\\VObject\\Parameter', $property->parameters['PARAMNAME']); + $this->assertInstanceOf(Parameter::class, $property->parameters['PARAMNAME']); $this->assertEquals('PARAMNAME', $property->parameters['PARAMNAME']->name); $this->assertEquals('paramvalue', $property->parameters['PARAMNAME']->getValue()); } @@ -308,11 +308,10 @@ public function testGetValue() /** * ElementList should reject this. - * - * @expectedException \LogicException */ public function testArrayAccessSetInt() { + $this->expectException(\LogicException::class); $calendar = new VCalendar(); $property = $calendar->createProperty('X-PROP', null); @@ -322,11 +321,10 @@ public function testArrayAccessSetInt() /** * ElementList should reject this. - * - * @expectedException \LogicException */ public function testArrayAccessUnsetInt() { + $this->expectException(\LogicException::class); $calendar = new VCalendar(); $property = $calendar->createProperty('X-PROP', null); diff --git a/tests/VObject/ReaderTest.php b/tests/VObject/ReaderTest.php index 06310e80a..96ed56be0 100644 --- a/tests/VObject/ReaderTest.php +++ b/tests/VObject/ReaderTest.php @@ -12,7 +12,7 @@ public function testReadComponent() $result = Reader::read($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } @@ -27,7 +27,7 @@ public function testReadStream() $result = Reader::read($stream); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } @@ -38,7 +38,7 @@ public function testReadComponentUnixNewLine() $result = Reader::read($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } @@ -49,26 +49,22 @@ public function testReadComponentLineFold() $result = Reader::read($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testReadCorruptComponent() { + $this->expectException(ParseException::class); $data = "BEGIN:VCALENDAR\r\nEND:FOO"; $result = Reader::read($data); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testReadCorruptSubComponent() { + $this->expectException(ParseException::class); $data = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:FOO\r\nEND:VCALENDAR"; $result = Reader::read($data); @@ -80,7 +76,7 @@ public function testReadProperty() $result = Reader::read($data); $result = $result->SUMMARY; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('SUMMARY', $result->name); $this->assertEquals('propValue', $result->getValue()); } @@ -91,7 +87,7 @@ public function testReadPropertyWithNewLine() $result = Reader::read($data); $result = $result->SUMMARY; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('SUMMARY', $result->name); $this->assertEquals("Line1\nLine2\nLine3\\Not the 4th line!", $result->getValue()); } @@ -102,7 +98,7 @@ public function testReadMappedProperty() $result = Reader::read($data); $result = $result->DTSTART; - $this->assertInstanceOf('Sabre\\VObject\\Property\\ICalendar\\DateTime', $result); + $this->assertInstanceOf(Property\ICalendar\DateTime::class, $result); $this->assertEquals('DTSTART', $result->name); $this->assertEquals('20110529', $result->getValue()); } @@ -113,16 +109,14 @@ public function testReadMappedPropertyGrouped() $result = Reader::read($data); $result = $result->DTSTART; - $this->assertInstanceOf('Sabre\\VObject\\Property\\ICalendar\\DateTime', $result); + $this->assertInstanceOf(Property\ICalendar\DateTime::class, $result); $this->assertEquals('DTSTART', $result->name); $this->assertEquals('20110529', $result->getValue()); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testReadBrokenLine() { + $this->expectException(ParseException::class); $data = "BEGIN:VCALENDAR\r\nPROPNAME;propValue"; $result = Reader::read($data); } @@ -137,10 +131,10 @@ public function testReadPropertyInComponent() $result = Reader::read(implode("\r\n", $data)); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(1, count($result->children())); - $this->assertInstanceOf('Sabre\\VObject\\Property', $result->children()[0]); + $this->assertInstanceOf(Property::class, $result->children()[0]); $this->assertEquals('PROPNAME', $result->children()[0]->name); $this->assertEquals('propValue', $result->children()[0]->getValue()); } @@ -158,13 +152,13 @@ public function testReadNestedComponent() $result = Reader::read(implode("\r\n", $data)); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(1, count($result->children())); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result->children()[0]); + $this->assertInstanceOf(Component::class, $result->children()[0]); $this->assertEquals('VTIMEZONE', $result->children()[0]->name); $this->assertEquals(1, count($result->children()[0]->children())); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result->children()[0]->children()[0]); + $this->assertInstanceOf(Component::class, $result->children()[0]->children()[0]); $this->assertEquals('DAYLIGHT', $result->children()[0]->children()[0]->name); } @@ -175,7 +169,7 @@ public function testReadPropertyParameter() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -190,7 +184,7 @@ public function testReadPropertyRepeatingParameter() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -206,7 +200,7 @@ public function testReadPropertyRepeatingNamelessGuessedParameter() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -222,7 +216,7 @@ public function testReadPropertyNoName() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -238,7 +232,7 @@ public function testReadPropertyParameterExtraColon() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue:anotherrandomstring', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -253,7 +247,7 @@ public function testReadProperty2Parameters() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(2, count($result->parameters())); @@ -270,7 +264,7 @@ public function testReadPropertyParameterQuoted() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -285,7 +279,7 @@ public function testReadPropertyParameterNewLines() $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); @@ -300,7 +294,7 @@ public function testReadPropertyParameterQuotedColon() $result = Reader::read($data); $result = $result->PROPNAME; - $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertInstanceOf(Property::class, $result); $this->assertEquals('PROPNAME', $result->name); $this->assertEquals('propValue', $result->getValue()); $this->assertEquals(1, count($result->parameters())); @@ -342,7 +336,7 @@ public function testReadWithInvalidLine() $data = [ 'BEGIN:VCALENDAR', 'DESCRIPTION:propValue', - "Yes, we've actually seen a file with non-idented property values on multiple lines", + "Yes, we've actually seen a file with non-indented property values on multiple lines", 'END:VCALENDAR', ]; @@ -369,11 +363,10 @@ public function testReadWithInvalidLine() /** * Reported as Issue 32. - * - * @expectedException \Sabre\VObject\ParseException */ public function testReadIncompleteFile() { + $this->expectException(ParseException::class); $input = <<expectException(\InvalidArgumentException::class); Reader::read(false); } @@ -417,7 +408,7 @@ public function testReadBOM() $data = chr(0xef).chr(0xbb).chr(0xbf)."BEGIN:VCALENDAR\r\nEND:VCALENDAR"; $result = Reader::read($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } @@ -434,7 +425,7 @@ public function testReadXMLComponent() $result = Reader::readXML($data); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } @@ -455,8 +446,32 @@ public function testReadXMLStream() $result = Reader::readXML($stream); - $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertInstanceOf(Component::class, $result); $this->assertEquals('VCALENDAR', $result->name); $this->assertEquals(0, count($result->children())); } + + public function testReadDuplicateValue() + { + $input = <<assertSame($expected, $result->VEVENT->DTSTART->serialize()); + } } diff --git a/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php b/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php index 40f09364f..bd1eeb9b6 100644 --- a/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php +++ b/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php @@ -1,9 +1,10 @@ assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand(new DateTime('2013-09-28'), new DateTime('2014-09-11')); + $dates = []; foreach ($vcal->VEVENT as $event) { $dates[] = $event->DTSTART->getValue(); } diff --git a/tests/VObject/Recur/EventIterator/BySetPosHangTest.php b/tests/VObject/Recur/EventIterator/BySetPosHangTest.php index 82bd353e0..555f2ffd4 100644 --- a/tests/VObject/Recur/EventIterator/BySetPosHangTest.php +++ b/tests/VObject/Recur/EventIterator/BySetPosHangTest.php @@ -1,9 +1,10 @@ assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2016-01-01')); + $dates = []; foreach ($vcal->VEVENT as $event) { $dates[] = $event->DTSTART->getValue(); } diff --git a/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php b/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php index 635b0a8c5..605e10dde 100644 --- a/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php +++ b/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php @@ -5,6 +5,7 @@ use DateTime; use DateTimeZone; use PHPUnit\Framework\TestCase; +use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Reader; class ExpandFloatingTimesTest extends TestCase @@ -26,7 +27,7 @@ public function testExpand() ICS; $vcal = Reader::read($input); - $this->assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2015-01-31')); $output = <<assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand( new DateTime('2015-01-01'), diff --git a/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php b/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php index 698c5fe59..548820191 100644 --- a/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php +++ b/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use DateTimeZone; use PHPUnit\Framework\TestCase; +use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Reader; /** @@ -36,7 +37,7 @@ public function testExpand() ICS; $vcal = Reader::read($input); - $this->assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2015-12-01')); diff --git a/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php b/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php index afc509972..f9fcda442 100644 --- a/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php +++ b/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php @@ -4,6 +4,7 @@ use DateTime; use PHPUnit\Framework\TestCase; +use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Reader; /** @@ -34,7 +35,7 @@ public function testExpand() ICS; $vcal = Reader::read($input); - $this->assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand(new DateTime('2011-01-01'), new DateTime('2014-01-01')); diff --git a/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php b/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php index 2f73d8e37..5546c508d 100644 --- a/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php +++ b/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php @@ -6,11 +6,15 @@ use DateTimeZone; use PHPUnit\Framework\TestCase; use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\InvalidDataException; use Sabre\VObject\Recur; class InfiniteLoopProblemTest extends TestCase { - public function setUp() + /** @var VCalendar */ + private $vcal; + + public function setUp(): void { $this->vcal = new VCalendar(); } @@ -73,11 +77,10 @@ public function testYearlyByMonthLoop() * Something, somewhere produced an ics with an interval set to 0. Because * this means we increase the current day (or week, month) by 0, this also * results in an infinite loop. - * - * @expectedException \Sabre\VObject\InvalidDataException */ public function testZeroInterval() { + $this->expectException(InvalidDataException::class); $ev = $this->vcal->createComponent('VEVENT'); $ev->UID = 'uuid'; $ev->DTSTART = '20120824T145700Z'; diff --git a/tests/VObject/Recur/EventIterator/Issue26Test.php b/tests/VObject/Recur/EventIterator/Issue26Test.php index bb4df64df..3313c3ec6 100644 --- a/tests/VObject/Recur/EventIterator/Issue26Test.php +++ b/tests/VObject/Recur/EventIterator/Issue26Test.php @@ -3,16 +3,16 @@ namespace Sabre\VObject\Recur\EventIterator; use PHPUnit\Framework\TestCase; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\InvalidDataException; use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; class Issue26Test extends TestCase { - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testExpand() { + $this->expectException(InvalidDataException::class); $input = <<assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $it = new EventIterator($vcal, 'bae5d57a98'); } diff --git a/tests/VObject/Recur/EventIterator/Issue48Test.php b/tests/VObject/Recur/EventIterator/Issue48Test.php index fe4b06755..aef592590 100644 --- a/tests/VObject/Recur/EventIterator/Issue48Test.php +++ b/tests/VObject/Recur/EventIterator/Issue48Test.php @@ -1,10 +1,13 @@ assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); - $it = new Recur\EventIterator($vcal, 'foo'); + $it = new EventIterator($vcal, 'foo'); $result = iterator_to_array($it); diff --git a/tests/VObject/Recur/EventIterator/Issue50Test.php b/tests/VObject/Recur/EventIterator/Issue50Test.php index df9c15519..5c476e6f2 100644 --- a/tests/VObject/Recur/EventIterator/Issue50Test.php +++ b/tests/VObject/Recur/EventIterator/Issue50Test.php @@ -1,10 +1,13 @@ assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); - $it = new Recur\EventIterator($vcal, '1aef0b27-3d92-4581-829a-11999dd36724'); + $it = new EventIterator($vcal, '1aef0b27-3d92-4581-829a-11999dd36724'); $result = []; foreach ($it as $instance) { diff --git a/tests/VObject/Recur/EventIterator/MainTest.php b/tests/VObject/Recur/EventIterator/MainTest.php index 10782a53a..9350080e4 100644 --- a/tests/VObject/Recur/EventIterator/MainTest.php +++ b/tests/VObject/Recur/EventIterator/MainTest.php @@ -6,6 +6,7 @@ use DateTimeZone; use PHPUnit\Framework\TestCase; use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\InvalidDataException; use Sabre\VObject\Recur\EventIterator; class MainTest extends TestCase @@ -15,7 +16,7 @@ public function testValues() $vcal = new VCalendar(); $ev = $vcal->createComponent('VEVENT'); $ev->UID = 'bla'; - $ev->RRULE = 'FREQ=DAILY;BYHOUR=10;BYMINUTE=5;BYSECOND=16;BYWEEKNO=32;BYYEARDAY=100,200'; + $ev->RRULE = 'FREQ=DAILY;BYHOUR=10;BYMINUTE=5;BYSECOND=16'; $dtStart = $vcal->createProperty('DTSTART'); $dtStart->setDateTime(new DateTimeImmutable('2011-10-07')); @@ -29,11 +30,11 @@ public function testValues() } /** - * @expectedException \Sabre\VObject\InvalidDataException * @depends testValues */ public function testInvalidFreq() { + $this->expectException(InvalidDataException::class); $vcal = new VCalendar(); $ev = $vcal->createComponent('VEVENT'); $ev->RRULE = 'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z'; @@ -47,20 +48,16 @@ public function testInvalidFreq() $it = new EventIterator($vcal, (string) $ev->UID); } - /** - * @expectedException \InvalidArgumentException - */ public function testVCalendarNoUID() { + $this->expectException(\InvalidArgumentException::class); $vcal = new VCalendar(); $it = new EventIterator($vcal); } - /** - * @expectedException \InvalidArgumentException - */ public function testVCalendarInvalidUID() { + $this->expectException(\InvalidArgumentException::class); $vcal = new VCalendar(); $it = new EventIterator($vcal, 'foo'); } @@ -1175,7 +1172,7 @@ public function testComplexExclusions() /** * @depends testValues */ - public function testOverridenEvent() + public function testOverriddenEvent() { $vcal = new VCalendar(); @@ -1246,7 +1243,7 @@ public function testOverridenEvent() /** * @depends testValues */ - public function testOverridenEvent2() + public function testOverriddenEvent2() { $vcal = new VCalendar(); @@ -1294,7 +1291,7 @@ public function testOverridenEvent2() /** * @depends testValues */ - public function testOverridenEventNoValuesExpected() + public function testOverriddenEventNoValuesExpected() { $vcal = new VCalendar(); $ev1 = $vcal->createComponent('VEVENT'); @@ -1321,12 +1318,12 @@ public function testOverridenEventNoValuesExpected() $summaries = []; // The reported problem was specifically related to the VCALENDAR - // expansion. In this parcitular case, we had to forward to the 28th of + // expansion. In this particular case, we had to forward to the 28th of // january. $it->fastForward(new DateTimeImmutable('2012-01-28 23:00:00')); - // We stop the loop when it hits the 6th of februari. Normally this - // iterator would hit 24, 25 (overriden from 31) and 7 feb but because + // We stop the loop when it hits the 6th of February. Normally this + // iterator would hit 24, 25 (overridden from 31) and 7 feb but because // we 'filter' from the 28th till the 6th, we should get 0 results. while ($it->valid() && $it->getDTStart() < new DateTimeImmutable('2012-02-06 23:00:00')) { $dates[] = $it->getDTStart(); @@ -1386,10 +1383,10 @@ public function testRDATE() /** * @depends testValues - * @expectedException \InvalidArgumentException */ public function testNoMasterBadUID() { + $this->expectException(\InvalidArgumentException::class); $vcal = new VCalendar(); // ev2 overrides an event, and puts it on 2pm instead. $ev2 = $vcal->createComponent('VEVENT'); diff --git a/tests/VObject/Recur/EventIterator/MaxInstancesTest.php b/tests/VObject/Recur/EventIterator/MaxInstancesTest.php index d0571ee82..6314b3b5a 100644 --- a/tests/VObject/Recur/EventIterator/MaxInstancesTest.php +++ b/tests/VObject/Recur/EventIterator/MaxInstancesTest.php @@ -5,15 +5,14 @@ use DateTime; use PHPUnit\Framework\TestCase; use Sabre\VObject\Reader; +use Sabre\VObject\Recur\MaxInstancesExceededException; use Sabre\VObject\Settings; class MaxInstancesTest extends TestCase { - /** - * @expectedException \Sabre\VObject\Recur\MaxInstancesExceededException - */ public function testExceedMaxRecurrences() { + $this->expectException(MaxInstancesExceededException::class); $input = <<assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $vcal = $vcal->expand(new DateTime('2011-01-01'), new DateTime('2015-01-01')); diff --git a/tests/VObject/Recur/EventIterator/NoInstancesTest.php b/tests/VObject/Recur/EventIterator/NoInstancesTest.php index b3e5a11fa..d89afd197 100644 --- a/tests/VObject/Recur/EventIterator/NoInstancesTest.php +++ b/tests/VObject/Recur/EventIterator/NoInstancesTest.php @@ -1,17 +1,18 @@ expectException(NoInstancesException::class); $input = <<assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + $this->assertInstanceOf(VCalendar::class, $vcal); $it = new EventIterator($vcal, 'foo'); } diff --git a/tests/VObject/Recur/EventIterator/OverrideDurationTest.php b/tests/VObject/Recur/EventIterator/OverrideDurationTest.php new file mode 100644 index 000000000..f25ef13d9 --- /dev/null +++ b/tests/VObject/Recur/EventIterator/OverrideDurationTest.php @@ -0,0 +1,52 @@ +getComponents()); + + $this->assertEquals($eventIterator->current()->format('Y-m-d H:i:s'), '2021-05-17 09:00:00', 'recur event start time'); + $this->assertEquals($eventIterator->getDtEnd()->format('Y-m-d H:i:s'), '2021-05-17 10:00:00', 'recur event end time'); + + $eventIterator->next(); + $this->assertEquals($eventIterator->current()->format('Y-m-d H:i:s'), '2021-05-18 09:00:00', 'recur event start time'); + $this->assertEquals($eventIterator->getDtEnd()->format('Y-m-d H:i:s'), '2021-05-18 10:00:00', 'recur event end time'); + + $eventIterator->next(); + $this->assertEquals($eventIterator->current()->format('Y-m-d H:i:s'), '2021-05-19 09:00:00', 'overridden event start time'); + $this->assertEquals($eventIterator->getDtEnd()->format('Y-m-d H:i:s'), '2021-05-19 12:00:00', 'overridden event end time'); + + $eventIterator->next(); + $this->assertEquals($eventIterator->current()->format('Y-m-d H:i:s'), '2021-05-20 09:00:00', 'recur event start time'); + $this->assertEquals($eventIterator->getDtEnd()->format('Y-m-d H:i:s'), '2021-05-20 10:00:00', 'recur event end time'); + } +} diff --git a/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php b/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php index 8773c168c..150a13980 100644 --- a/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php +++ b/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php @@ -1,6 +1,6 @@ fastForwardBefore($ffDate); + $ru = getrusage(); + $endTime = $ru['ru_utime.tv_sec'] * 1000000 + $ru['ru_utime.tv_usec']; + $this->assertLessThan(self::FF_TIMEOUT, $endTime - $startTime); + } + + public function testFastForwardBeforeYearlyBasic() + { + $startDate = new DateTime('1970-10-23 00:00:00', new DateTimeZone('zulu')); + $ffDate = new DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 1); + $rrule = new RRuleIterator('FREQ=YEARLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $year = 60 * 60 * 24 * 365; + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(99998, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + // It's a leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + } + + public function testFastForwardBeforeYearlyByYearDay() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 5); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300', $startDate); + + $this->fastForward($rrule, $ffDate); + + // 1st day + $day = 60 * 60 * 24; + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(99999, 1, 1) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 300th day + $rrule->next(); + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 1st day + $expected += 66 * $day; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 300th day + $rrule->next(); + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 1st day (leap year, we have 366 days in this year) + $rrule->next(); + $expected += 67 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 5); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20', $startDate); + + $this->fastForward($rrule, $ffDate); + + $day = 60 * 60 * 24; + $week = 7 * $day; + + // 1st week + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(99999, 1, 4) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20st week + $rrule->next(); + $expected += $week * 19; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(10000, 1, 2)->setTime(8, 44, 13); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(10000, 1, 2) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // jump to 6th january 10002 + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(10002, 1, 6) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(18000, 1, 30); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(18000, 1, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // february + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // march + $rrule->next(); + $expected += 29 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // april + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // may + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // june + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // july + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // august + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMonthly31thDay() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(18000, 2, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(18000, 1, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // march + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(18000, 3, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // may + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(18000, 5, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // july + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(18000, 7, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // august + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(18000, 8, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // october + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(18000, 10, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // december + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(18000, 12, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMonthlyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(8000, 1, 6); + // every 2 months on the 1st Monday, 2nd Tuesday, 3rd Wednesday and 4th Thursday + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH', $startDate); + + $this->fastForward($rrule, $ffDate); + + // monday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 1, 3) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 1, 11) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 1, 19) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 1, 27) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // monday march + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 3, 6) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 3, 14) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday (this month starts on wednesday so that's just the next day) + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 3, 15) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected += 8 * 24 * 60 * 60; + $expected = (new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone))) + ->setDate(8000, 3, 23) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(4000, 1, 2); + $rrule = new RRuleIterator('FREQ=DAILY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(4000, 1, 1) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone($timezone)); + $ffDate->setDate(4000, 1, 4)->setTime(16, 30, 0); + // every 10 days at 16, 17 and 18 + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(4000, 1, 4) + ->setTime(16, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeHourlyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:12:34', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone($timezone)); + $ffDate->setDate(4000, 1, 2)->setTime(2, 0, 0); + $rrule = new RRuleIterator('FREQ=HOURLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(4000, 1, 2) + ->setTime(1, 12, 34) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeNotInFrequency() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone($timezone)); + $ffDate->setDate(2023, 3, 15)->setTime(1, 0, 0); + // every leap years + $rrule = new RRuleIterator('FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(2020, 2, 29) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // the next leap year + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(2024, 2, 29) + ->setTime(0, 0, 0) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMultipleTimesBasic() + { + $startDate = new DateTime('2020-01-02 00:00:00', new DateTimeZone('zulu')); + $ffDate = new DateTime('2020-01-18 00:00:00', new DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=WEEKLY', $startDate); + $expected = new DateTime('2020-01-16 00:00:00', new DateTimeZone('zulu')); + + $this->fastForward($rrule, $ffDate); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + + $this->fastForward($rrule, $ffDate); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + + $this->fastForward($rrule, $ffDate); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + } +} diff --git a/tests/VObject/Recur/FastForwardTest.php b/tests/VObject/Recur/FastForwardTest.php new file mode 100644 index 000000000..7a305b4ad --- /dev/null +++ b/tests/VObject/Recur/FastForwardTest.php @@ -0,0 +1,458 @@ +fastForward($ffDate); + $ru = getrusage(); + $endTime = $ru['ru_utime.tv_sec'] * 1000000 + $ru['ru_utime.tv_usec']; + $this->assertLessThan(self::FF_TIMEOUT, $endTime - $startTime); + } + + public function testFastForwardYearlyBasic() + { + $startDate = new DateTime('1970-10-23 00:00:00', new DateTimeZone('zulu')); + $ffDate = new DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 1); + $rrule = new RRuleIterator('FREQ=YEARLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $year = 60 * 60 * 24 * 365; + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(99999, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // It's a leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + } + + public function testFastForwardYearlyByYearDay() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(99998, 12, 31); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300', $startDate); + + $this->fastForward($rrule, $ffDate); + + $day = 60 * 60 * 24; + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(99999, 1, 1)// 20th day + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // 300th day + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // 1st day + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // 20th day + $expected += 66 * $day; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // 300th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // 1st day (leap year, we have 366 days in this year) + $expected += 67 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 1); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20', $startDate); + + $this->fastForward($rrule, $ffDate); + + $day = 60 * 60 * 24; + $week = 7 * $day; + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(99999, 1, 4)// 1st day + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $week * 19; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(9999, 1, 20)->setTime(0, 0, 13); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(10000, 1, 2) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // jump to 6th january 10002 + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(10002, 1, 6) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(18000, 1, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(18000, 1, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // february + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // march + $rrule->next(); + $expected += 29 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // april + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // may + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // june + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // july + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // august + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardMonthly31thDay() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(18000, 1, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 1, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // march + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 3, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // may + $rrule->next(); + $expected += (30 + 31) * 24 * 60 * 60; + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 5, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // july + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 7, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // august + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 8, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // october + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 10, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // december + $rrule->next(); + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(18000, 12, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardMonthlyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(8000, 1, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH', $startDate); + + $this->fastForward($rrule, $ffDate); + + // monday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 1, 3) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 1, 11) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 1, 19) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 1, 27) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // monday march + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 3, 6) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 3, 14) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday (this month starts on wednesday so that's just the next day) + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 3, 15) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(8000, 3, 23) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(4000, 1, 1); + $rrule = new RRuleIterator('FREQ=DAILY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(4000, 1, 1) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new DateTimeZone('zulu')); + $ffDate->setDate(4000, 1, 1); + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(4000, 1, 4) + ->setTime(16, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } +} diff --git a/tests/VObject/Recur/FastForwardToEndTest.php b/tests/VObject/Recur/FastForwardToEndTest.php new file mode 100644 index 000000000..992f2b9d8 --- /dev/null +++ b/tests/VObject/Recur/FastForwardToEndTest.php @@ -0,0 +1,355 @@ +fastForwardToEnd(); + $ru = getrusage(); + $endTime = $ru['ru_utime.tv_sec'] * 1000000 + $ru['ru_utime.tv_usec']; + $enfoceTiming && $this->assertLessThan(self::FF_TIMEOUT, $endTime - $startTime); + $this->assertTrue($ruleIterator->valid()); + $this->assertNotNull($ruleIterator->current()); + } + + public function testFastForwardToEndWithoutEndYearlyBasic() + { + $startDate = new DateTime('1970-10-23 00:00:00', new DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY', $startDate); + + $this->expectException(\LogicException::class); + $rrule->fastForwardToEnd(); + } + + public function testFastForwardToEndCountYearlyBasic() + { + $startDate = new DateTime('1970-10-23 00:00:00', new DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;COUNT=7777', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(9746, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyBasic() + { + $startDate = new DateTime('1970-10-23 00:00:00', new DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;UNTIL=97461212T000000', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(9746, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountYearlyByYearDay() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(5303, 1, 20) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyByYearDay() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300;UNTIL=53030808T000000', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(5303, 1, 20) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /* + * Issue CALENDAR-587 + public function testFastForwardToEndCountYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20;COUNT=100', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(2019, 12, 30) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20;UNTIL=20030808T000000', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(2019, 12, 30) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + */ + + public function testFastForwardToEndCountYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(4226, 1, 1) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30;UNTIL=42180125T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(4218, 1, 25) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=MONTHLY;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(2804, 1, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=MONTHLY;UNTIL=28040122T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone('zulu'))) + ->setDate(2803, 12, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /** + * @requires PHP < 8.1 + */ + public function testFastForwardToEndCountMonthly31thDay() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + $rrule = new RRuleIterator('FREQ=MONTHLY;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(3398, 10, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /** + * @requires PHP >= 8.1 + */ + public function testFastForwardToEndCountMonthly31thDayPHP81() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + $rrule = new RRuleIterator('FREQ=MONTHLY;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(3398, 7, 31); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilMonthly31thDay() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + $rrule = new RRuleIterator('FREQ=MONTHLY;UNTIL=33980909T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(3398, 8, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountMonthlyAdvanced() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new DateTimeZone('America/New_York')); + // every 2 months on the 1st Monday, 2nd Tuesday, 3rd Wednesday and 4th Thursday + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(2386, 9, 17) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilMonthlyAdvanced() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new DateTimeZone('America/New_York')); + // every 2 months on the 1st Monday, 2nd Tuesday, 3rd Wednesday and 4th Thursday + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH;UNTIL=23860914T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone('America/New_York'))) + ->setDate(2386, 9, 9) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=DAILY;COUNT=100000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(2244, 8, 6) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=DAILY;UNTIL=22440806T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(2244, 8, 6) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + // every 10 days at 16, 17 and 18 + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(2062, 1, 13) + ->setTime(18, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + // every 10 days at 16, 17 and 18 + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10;UNTIL=20620113T183456', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(2062, 1, 13) + ->setTime(18, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountHourlyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:12:34', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=HOURLY;COUNT=100000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(1982, 3, 21) + ->setTime(2, 12, 34) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilHourlyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:12:34', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=HOURLY;UNTIL=19820321T024032', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime('midnight', new DateTimeZone($timezone))) + ->setDate(1982, 3, 21) + ->setTime(2, 12, 34) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } +} diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index b6a8fdc53..6cb3feb5b 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use DateTimeZone; use PHPUnit\Framework\TestCase; +use Sabre\VObject\InvalidDataException; class RRuleIteratorTest extends TestCase { @@ -27,7 +28,8 @@ public function testHourly() '2011-10-08 15:00:00', '2011-10-08 18:00:00', '2011-10-08 21:00:00', - ] + ], + 'hourly', 12, 3, null ); } @@ -44,7 +46,8 @@ public function testDaily() '2011-10-19 00:00:00', '2011-10-22 00:00:00', '2011-10-25 00:00:00', - ] + ], + 'daily', null, 3, new DateTime('2011-10-25') ); } @@ -66,7 +69,8 @@ public function testDailyByDayByHour() '2011-10-22 07:00:00', '2011-10-23 06:00:00', '2011-10-23 07:00:00', - ] + ], + 'daily', null, 1, null ); } @@ -88,7 +92,8 @@ public function testDailyByHour() '2012-10-13 15:00:00', '2012-10-15 10:00:00', '2012-10-15 11:00:00', - ] + ], + 'daily', null, 2, null ); } @@ -110,7 +115,8 @@ public function testDailyByDay() '2011-11-18 12:00:00', '2011-11-22 12:00:00', '2011-11-30 12:00:00', - ] + ], + 'daily', null, 2, null ); } @@ -125,7 +131,8 @@ public function testDailyCount() '2014-08-03 18:03:00', '2014-08-04 18:03:00', '2014-08-05 18:03:00', - ] + ], + 'daily', 5, 1, null ); } @@ -142,6 +149,7 @@ public function testDailyByMonth() '2013-10-27 16:00:00', '2014-09-07 16:00:00', ], + 'daily', null, 1, null, '2013-09-28' ); } @@ -162,7 +170,8 @@ public function testWeekly() '2012-01-13 00:00:00', '2012-01-27 00:00:00', '2012-02-10 00:00:00', - ] + ], + 'weekly', 10, 2, null ); } @@ -176,7 +185,8 @@ public function testWeeklyByDay() '2014-08-04 00:00:00', '2014-08-11 00:00:00', '2014-08-18 00:00:00', - ] + ], + 'weekly', 4, 1, null ); } @@ -198,7 +208,8 @@ public function testWeeklyByDay2() '2011-11-18 00:00:00', '2011-11-29 00:00:00', '2011-11-30 00:00:00', - ] + ], + 'weekly', null, 2, null ); } @@ -223,7 +234,8 @@ public function testWeeklyByDayByHour() '2011-11-01 08:00:00', '2011-11-01 09:00:00', '2011-11-01 10:00:00', - ] + ], + 'weekly', null, 2, null ); } @@ -245,7 +257,8 @@ public function testWeeklyByDaySpecificHour() '2011-11-18 18:00:00', '2011-11-29 18:00:00', '2011-11-30 18:00:00', - ] + ], + 'weekly', null, 2, null ); } @@ -260,11 +273,12 @@ public function testMonthly() '2012-06-05 00:00:00', '2012-09-05 00:00:00', '2012-12-05 00:00:00', - ] + ], + 'monthly', 5, 3, null ); } - public function testMonlthyEndOfMonth() + public function testMonthlyEndOfMonth() { $this->parse( 'FREQ=MONTHLY;INTERVAL=2;COUNT=12', @@ -282,7 +296,8 @@ public function testMonlthyEndOfMonth() '2014-12-31 00:00:00', '2015-08-31 00:00:00', '2015-10-31 00:00:00', - ] + ], + 'monthly', 12, 2, null ); } @@ -301,10 +316,45 @@ public function testMonthlyByMonthDay() '2011-11-24 00:00:00', '2012-04-01 00:00:00', '2012-04-24 00:00:00', - ] + ], + 'monthly', 9, 5, null + ); + } + + public function testInvalidByMonthDay() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=MONTHLY;COUNT=6;BYMONTHDAY=1,5,10,42', + '2011-04-07 00:00:00', + [] ); } + /** @dataProvider invalidFreqByCombinationProviders */ + public function testInvalidFreqByCombination(string $rule) + { + $this->expectException(InvalidDataException::class); + $this->parse( + $rule, + '2011-01-01 00:00:00', + [] + ); + } + + public function invalidFreqByCombinationProviders(): iterable + { + return [ + ['FREQ=DAILY;BYWEEKNO=13,15,50'], + ['FREQ=WEEKLY;BYWEEKNO=13,15,50'], + ['FREQ=MONTHLY;BYWEEKNO=13,15,50'], + ['FREQ=DAILY;BYYEARDAY=1'], + ['FREQ=WEEKLY;BYYEARDAY=1'], + ['FREQ=MONTHLY;BYYEARDAY=1'], + ['FREQ=WEEKLY;BYMONTHDAY=1'], + ]; + } + public function testMonthlyByDay() { $this->parse( @@ -327,7 +377,37 @@ public function testMonthlyByDay() '2011-03-22 00:00:00', '2011-03-28 00:00:00', '2011-05-02 00:00:00', - ] + ], + 'monthly', 16, 2, null + ); + } + + public function testMonthlyByDayUntil() + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;BYDAY=WE;WKST=WE;UNTIL=20210317T000000Z', + '2021-02-10 00:00:00', + [ + '2021-02-10 00:00:00', + '2021-02-17 00:00:00', + '2021-02-24 00:00:00', + '2021-03-03 00:00:00', + '2021-03-10 00:00:00', + '2021-03-17 00:00:00', + ], + 'monthly', null, 1, new DateTime('2021-03-17') + ); + } + + public function testMonthlyByDayUntilWithImpossibleNextOccurrence() + { + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;BYDAY=2WE;BYMONTHDAY=2;WKST=WE;UNTIL=20210317T000000Z', + '2021-02-10 00:00:00', + [ + '2021-02-10 00:00:00', + ], + 'monthly', null, 1, new DateTime('2021-03-17') ); } @@ -347,7 +427,8 @@ public function testMonthlyByDayByMonthDay() '2016-02-01 00:00:00', '2016-08-01 00:00:00', '2017-05-01 00:00:00', - ] + ], + 'monthly', 10, 1, null ); } @@ -367,7 +448,8 @@ public function testMonthlyByDayBySetPos() '2011-04-29 00:00:00', '2011-05-02 00:00:00', '2011-05-31 00:00:00', - ] + ], + 'monthly', 10, 1, null ); } @@ -387,7 +469,8 @@ public function testYearly() '2032-01-01 00:00:00', '2035-01-01 00:00:00', '2038-01-01 00:00:00', - ] + ], + 'yearly', 10, 3, null ); } @@ -400,7 +483,8 @@ public function testYearlyLeapYear() '2012-02-29 00:00:00', '2016-02-29 00:00:00', '2020-02-29 00:00:00', - ] + ], + 'yearly', 3, 1, null ); } @@ -418,15 +502,14 @@ public function testYearlyByMonth() '2019-10-07 00:00:00', '2023-04-07 00:00:00', '2023-10-07 00:00:00', - ] + ], + 'yearly', 8, 4, null ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testYearlyByMonthInvalidValue1() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0', '2011-04-07 00:00:00', @@ -434,11 +517,9 @@ public function testYearlyByMonthInvalidValue1() ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testYearlyByMonthInvalidValue2() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=bla', '2011-04-07 00:00:00', @@ -446,11 +527,9 @@ public function testYearlyByMonthInvalidValue2() ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testYearlyByMonthManyInvalidValues() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0,bla', '2011-04-07 00:00:00', @@ -458,11 +537,9 @@ public function testYearlyByMonthManyInvalidValues() ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testYearlyByMonthEmptyValue() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=', '2011-04-07 00:00:00', @@ -484,7 +561,26 @@ public function testYearlyByMonthByDay() '2016-04-24 00:00:00', '2016-10-03 00:00:00', '2016-10-30 00:00:00', - ] + ], + 'yearly', 8, 5, null + ); + } + + public function testYearlyNewYearsEve() + { + $this->parse( + 'FREQ=YEARLY;COUNT=7;INTERVAL=2;BYYEARDAY=1', + '2011-01-01 03:07:00', + [ + '2011-01-01 03:07:00', + '2013-01-01 03:07:00', + '2015-01-01 03:07:00', + '2017-01-01 03:07:00', + '2019-01-01 03:07:00', + '2021-01-01 03:07:00', + '2023-01-01 03:07:00', + ], + 'yearly', 7, 2, null ); } @@ -492,16 +588,17 @@ public function testYearlyByYearDay() { $this->parse( 'FREQ=YEARLY;COUNT=7;INTERVAL=2;BYYEARDAY=190', - '2011-07-10 03:07:00', - [ - '2011-07-10 03:07:00', - '2013-07-10 03:07:00', - '2015-07-10 03:07:00', - '2017-07-10 03:07:00', - '2019-07-10 03:07:00', - '2021-07-10 03:07:00', - '2023-07-10 03:07:00', - ] + '2011-07-09 03:07:00', + [ + '2011-07-09 03:07:00', + '2013-07-09 03:07:00', + '2015-07-09 03:07:00', + '2017-07-09 03:07:00', + '2019-07-09 03:07:00', + '2021-07-09 03:07:00', + '2023-07-09 03:07:00', + ], + 'yearly', 7, 2, null ); } @@ -521,7 +618,7 @@ public function testYearlyByYearDayImmutable() $parser->next(); $item = $parser->current(); - $this->assertEquals($item->format('Y-m-d H:i:s'), '2013-07-10 03:07:00'); + $this->assertEquals($item->format('Y-m-d H:i:s'), '2013-07-09 03:07:00'); } public function testYearlyByYearDayMultiple() @@ -531,14 +628,15 @@ public function testYearlyByYearDayMultiple() '2011-07-10 14:53:11', [ '2011-07-10 14:53:11', - '2011-10-29 14:53:11', - '2014-07-10 14:53:11', - '2014-10-29 14:53:11', - '2017-07-10 14:53:11', - '2017-10-29 14:53:11', - '2020-07-09 14:53:11', - '2020-10-28 14:53:11', - ] + '2011-10-28 14:53:11', + '2014-07-09 14:53:11', + '2014-10-28 14:53:11', + '2017-07-09 14:53:11', + '2017-10-28 14:53:11', + '2020-07-08 14:53:11', + '2020-10-27 14:53:11', + ], + 'yearly', 8, 3, null ); } @@ -549,12 +647,13 @@ public function testYearlyByYearDayByDay() '2001-04-07 14:53:11', [ '2001-04-07 14:53:11', - '2006-04-08 14:53:11', - '2012-04-07 14:53:11', - '2017-04-08 14:53:11', - '2023-04-08 14:53:11', - '2034-04-08 14:53:11', - ] + '2007-04-07 14:53:11', + '2018-04-07 14:53:11', + '2024-04-06 14:53:11', + '2029-04-07 14:53:11', + '2035-04-07 14:53:11', + ], + 'yearly', 6, 1, null ); } @@ -572,15 +671,58 @@ public function testYearlyByYearDayNegative() '2003-12-27 14:53:11', '2004-09-26 14:53:11', '2004-12-27 14:53:11', - ] + ], + 'yearly', 8, 1, null + ); + } + + public function testFirstLastSundayEveryOtherYearAt1530and1730InJanuary() + { + $this->parse('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=1SU,-1SU;BYHOUR=15,17;BYMINUTE=30,35;BYSECOND=15,56', + '1999-12-01 12:34:56', + [ + '1999-12-01 12:34:56', + '2001-01-07 15:30:15', '2001-01-07 15:30:56', '2001-01-07 15:35:15', '2001-01-07 15:35:56', + '2001-01-07 17:30:15', '2001-01-07 17:30:56', '2001-01-07 17:35:15', '2001-01-07 17:35:56', + + '2001-01-28 15:30:15', '2001-01-28 15:30:56', '2001-01-28 15:35:15', '2001-01-28 15:35:56', + '2001-01-28 17:30:15', '2001-01-28 17:30:56', '2001-01-28 17:35:15', '2001-01-28 17:35:56', + + '2003-01-05 15:30:15', '2003-01-05 15:30:56', '2003-01-05 15:35:15', '2003-01-05 15:35:56', + '2003-01-05 17:30:15', '2003-01-05 17:30:56', '2003-01-05 17:35:15', '2003-01-05 17:35:56', + + '2003-01-26 15:30:15', '2003-01-26 15:30:56', '2003-01-26 15:35:15', '2003-01-26 15:35:56', + '2003-01-26 17:30:15', '2003-01-26 17:30:56', '2003-01-26 17:35:15', '2003-01-26 17:35:56', + ], + 'yearly', null, 2, null + ); + } + + public function testFirstFourthSundayEveryOtherMonthAt830and930() + { + $this->parse('FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU,4SU;BYHOUR=15,17;BYMINUTE=30,32;BYSECOND=11,12', + '2001-01-01 12:34:56', + [ + '2001-01-01 12:34:56', + '2001-01-07 15:30:11', '2001-01-07 15:30:12', '2001-01-07 15:32:11', '2001-01-07 15:32:12', + '2001-01-07 17:30:11', '2001-01-07 17:30:12', '2001-01-07 17:32:11', '2001-01-07 17:32:12', + + '2001-01-28 15:30:11', '2001-01-28 15:30:12', '2001-01-28 15:32:11', '2001-01-28 15:32:12', + '2001-01-28 17:30:11', '2001-01-28 17:30:12', '2001-01-28 17:32:11', '2001-01-28 17:32:12', + + '2001-03-04 15:30:11', '2001-03-04 15:30:12', '2001-03-04 15:32:11', '2001-03-04 15:32:12', + '2001-03-04 17:30:11', '2001-03-04 17:30:12', '2001-03-04 17:32:11', '2001-03-04 17:32:12', + + '2001-03-25 15:30:11', '2001-03-25 15:30:12', '2001-03-25 15:32:11', '2001-03-25 15:32:12', + '2001-03-25 17:30:11', '2001-03-25 17:30:12', '2001-03-25 17:32:11', '2001-03-25 17:32:12', + ], + 'monthly', null, 2, null ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testYearlyByYearDayInvalid390() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYYEARDAY=390', '2011-04-07 00:00:00', @@ -589,11 +731,9 @@ public function testYearlyByYearDayInvalid390() ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testYearlyByYearDayInvalid0() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYYEARDAY=0', '2011-04-07 00:00:00', @@ -602,6 +742,20 @@ public function testYearlyByYearDayInvalid0() ); } + public function testYearlyByDayByWeekNo() + { + $this->parse( + 'FREQ=YEARLY;COUNT=3;BYDAY=MO;BYWEEKNO=13,15,50', + '2021-01-01 00:00:00', + [ + '2021-01-01 00:00:00', + '2021-03-29 00:00:00', + '2021-04-12 00:00:00', + ], + 'yearly', 3, 1 + ); + } + public function testFastForward() { // The idea is that we're fast-forwarding too far in the future, so @@ -610,6 +764,7 @@ public function testFastForward() 'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU', '2011-04-04 00:00:00', [], + 'yearly', 8, 5, null, '2020-05-05 00:00:00' ); } @@ -631,7 +786,8 @@ public function testFifthTuesdayProblem() '2007-10-04 14:46:42', [ '2007-10-04 14:46:42', - ] + ], + 'monthly', null, 1, new DateTime('2007-10-30 03:59:59') ); } @@ -639,7 +795,7 @@ public function testFifthTuesdayProblem() * This bug came from a Fruux customer. This would result in a never-ending * request. */ - public function testFastFowardTooFar() + public function testFastForwardTooFar() { $this->parse( 'FREQ=WEEKLY;BYDAY=MO;UNTIL=20090704T205959Z;INTERVAL=1', @@ -656,7 +812,8 @@ public function testFastFowardTooFar() '2009-06-15 18:00:00', '2009-06-22 18:00:00', '2009-06-29 18:00:00', - ] + ], + 'weekly', null, 1, new DateTime('2009-07-04 20:59:59') ); } @@ -678,7 +835,8 @@ public function testValidByWeekNo() '2019-05-14 00:00:00', '2020-05-12 00:00:00', '2021-05-18 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -700,7 +858,8 @@ public function testNegativeValidByWeekNo() '2016-08-09 00:00:00', '2016-08-12 00:00:00', '2017-08-08 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -722,7 +881,8 @@ public function testTwoValidByWeekNo() '2016-05-17 09:00:00', '2016-05-20 09:00:00', '2017-05-16 09:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -744,7 +904,8 @@ public function testValidByWeekNoByDayDefault() '2020-05-11 00:00:00', '2021-05-17 00:00:00', '2022-05-16 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -766,15 +927,14 @@ public function testMultipleValidByWeekNo() '2013-05-14 00:00:00', '2013-05-17 00:00:00', '2013-12-10 00:00:00', - ] + ], + 'yearly', null, 1, null ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testInvalidByWeekNo() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;BYWEEKNO=54', '2011-05-16 00:00:00', @@ -794,6 +954,7 @@ public function testYearlyByMonthLoop() [ '2012-02-01 15:45:00', ], + 'yearly', null, 1, new DateTime('2012-02-03 22:59:59'), '2012-01-29 23:00:00' ); } @@ -802,24 +963,22 @@ public function testYearlyByMonthLoop() * Something, somewhere produced an ics with an interval set to 0. Because * this means we increase the current day (or week, month) by 0, this also * results in an infinite loop. - * - * @expectedException \Sabre\VObject\InvalidDataException */ public function testZeroInterval() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=YEARLY;INTERVAL=0', '2012-08-24 14:57:00', [], + 'yearly', null, 0, null, '2013-01-01 23:00:00' ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testInvalidFreq() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z', '2011-10-07', @@ -827,11 +986,19 @@ public function testInvalidFreq() ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ + public function testInvalidMissingFreq() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'COUNT=6;BYMONTHDAY=24;BYMONTH=1', + '2011-04-07 00:00:00', + [] + ); + } + public function testByDayBadOffset() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=0MO;WKST=SA', '2014-08-01 00:00:00', @@ -855,6 +1022,7 @@ public function testUntilBeginHasTimezone() '2013-11-11 18:30:00', '2013-11-18 18:30:00', ], + 'weekly', null, 1, new DateTime('2013-11-18 18:30:00-0500'), null, 'America/New_York' ); @@ -862,24 +1030,39 @@ public function testUntilBeginHasTimezone() public function testUntilBeforeDtStart() { + $dtstart = '2014-08-02 00:15:00'; $this->parse( 'FREQ=DAILY;UNTIL=20140101T000000Z', - '2014-08-02 00:15:00', + $dtstart, [ - '2014-08-02 00:15:00', - ] + $dtstart, + ], + 'daily', null, 1, new DateTime($dtstart) + ); + } + + public function testUntilAndCount() + { + $this->expectException(InvalidDataException::class); + $this->expectExceptionMessage('Can not have both UNTIL and COUNT property at the same time'); + + $this->parse( + 'FREQ=DAILY;COUNT=5;UNTIL=20201108T225959Z', + '2021-01-18 00:15:00', + [] ); } public function testIgnoredStuff() { $this->parse( - 'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;BYYEARDAY=1;BYWEEKNO=1;COUNT=2', + 'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;COUNT=2', '2014-08-02 00:15:00', [ '2014-08-02 00:15:00', '2014-08-03 00:15:00', - ] + ], + 'daily', 2, 1, null ); } @@ -893,10 +1076,18 @@ public function testMinusFifthThursday() '2015-01-08 00:15:00', '2015-02-05 00:15:00', '2015-03-05 00:15:00', - ] + ], + 'monthly', 4, 1, null ); } + /** + * This test can take some seconds to complete. + * The "large" annotation means phpunit will let it run for + * up to 60 seconds by default. + * + * @large + */ public function testNeverEnding() { $this->parse( @@ -905,17 +1096,16 @@ public function testNeverEnding() [ '2015-01-01 00:15:00', ], + 'monthly', null, 1, null, null, 'UTC', true ); } - /** - * @expectedException \Sabre\VObject\InvalidDataException - */ public function testUnsupportedPart() { + $this->expectException(InvalidDataException::class); $this->parse( 'FREQ=DAILY;BYWODAN=1', '2014-08-02 00:15:00', @@ -948,11 +1138,26 @@ public function testIteratorFunctions() ); } - public function parse($rule, $start, $expected, $fastForward = null, $tz = 'UTC', $runTillTheEnd = false) - { + public function parse( + $rule, + $start, + $expected, + $expectedFreq = null, + $expectedCount = null, + $expectedInterval = null, + $expectedUntil = null, + $fastForward = null, + $tz = 'UTC', + $runTillTheEnd = false + ) { $dt = new DateTime($start, new DateTimeZone($tz)); $parser = new RRuleIterator($rule, $dt); + $this->assertEquals($expectedFreq, $parser->getFrequency()); + $this->assertEquals($expectedCount, $parser->getCount()); + $this->assertEquals($expectedInterval, $parser->getInterval()); + $this->assertEquals($expectedUntil, $parser->getUntil()); + if ($fastForward) { $parser->fastForward(new DateTime($fastForward)); } diff --git a/tests/VObject/Splitter/ICalendarTest.php b/tests/VObject/Splitter/ICalendarTest.php index 5addf9f6d..2788b96d6 100644 --- a/tests/VObject/Splitter/ICalendarTest.php +++ b/tests/VObject/Splitter/ICalendarTest.php @@ -4,12 +4,13 @@ use PHPUnit\Framework\TestCase; use Sabre\VObject; +use Sabre\VObject\ParseException; class ICalendarTest extends TestCase { protected $version; - public function setUp() + public function setUp(): void { $this->version = VObject\Version::VERSION; } @@ -45,11 +46,9 @@ public function testICalendarImportValidEvent() $this->assertEquals([], VObject\Reader::read($return)->validate()); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testICalendarImportWrongType() { + $this->expectException(ParseException::class); $data = <<assertNull($object = $objects->getNext()); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testICalendarImportInvalidEvent() { + $this->expectException(ParseException::class); $data = <<createStream($data); diff --git a/tests/VObject/Splitter/VCardTest.php b/tests/VObject/Splitter/VCardTest.php index a7e4ea3d2..1402b5094 100644 --- a/tests/VObject/Splitter/VCardTest.php +++ b/tests/VObject/Splitter/VCardTest.php @@ -3,6 +3,7 @@ namespace Sabre\VObject\Splitter; use PHPUnit\Framework\TestCase; +use Sabre\VObject\ParseException; class VCardTest extends TestCase { @@ -33,11 +34,9 @@ public function testVCardImportValidVCard() $this->assertEquals(1, $count); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testVCardImportWrongType() { + $this->expectException(ParseException::class); $event[] = <<assertEquals(4, $count); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testVCardImportVCardNoComponent() { + $this->expectException(ParseException::class); $data = <<expectException(\Sabre\VObject\ParseException::class); + $this->expectException(ParseException::class); $this->expectExceptionMessage('Invalid MimeDir file. Unexpected component: "BEGIN:VCARD" in document type VCARD'); while ($object = $splitter->getNext()) { } @@ -161,11 +158,9 @@ public function testVCardImportEndOfData() $this->assertNull($objects->getNext()); } - /** - * @expectedException \Sabre\VObject\ParseException - */ public function testVCardImportCheckInvalidArgumentException() { + $this->expectException(ParseException::class); $data = <<getName(); + $actual = TimeZoneUtil::getTimeZone($slashTimezone)->getName(); + $this->assertEquals($expected, $actual); + } + public function testExchangeMap() { $vobj = <<assertEquals($ex->getName(), $tz->getName()); } - public function testWetherMicrosoftIsStillInsane() + public function testWhetherMicrosoftIsStillInsane() { $vobj = <<assertEquals($ex->getName(), $tz->getName()); } + public function testEmptyTimeZone() + { + $tz = TimeZoneUtil::getTimeZone(''); + $ex = new \DateTimeZone('UTC'); + $this->assertEquals($ex->getName(), $tz->getName()); + } + public function testWindowsTimeZone() { $tz = TimeZoneUtil::getTimeZone('Eastern Standard Time'); @@ -154,6 +177,29 @@ public function testWindowsTimeZone() $this->assertEquals($ex->getName(), $tz->getName()); } + public function testLowerCaseTimeZone() + { + $tz = TimeZoneUtil::getTimeZone('mountain time (us & canada)'); + $ex = new \DateTimeZone('America/Denver'); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function testDeprecatedTimeZone() + { + // Deprecated in 2022b + $tz = TimeZoneUtil::getTimeZone('Europe/Kiev'); + $ex = new \DateTimeZone('Europe/Kiev'); + $this->assertSame($ex->getName(), $tz->getName()); + } + + public function testDeprecatedUnsupportedTimeZone() + { + // Deprecated and unsupported + $tz = TimeZoneUtil::getTimeZone('America/Godthab'); + $ex = new \DateTimeZone('America/Godthab'); + $this->assertNotSame($ex->getName(), $tz->getName()); + } + /** * @dataProvider getPHPTimeZoneIdentifiers */ @@ -194,7 +240,7 @@ public function getPHPTimeZoneBCIdentifiers() function ($value) { return [$value]; }, - TimeZoneUtil::getIdentifiersBC() + include __DIR__.'/../../lib/timezonedata/php-bc.php' ); } @@ -210,11 +256,9 @@ public function testTimezoneOffset() $this->assertEquals($ex->getName(), $tz->getName()); } - /** - * @expectedException \InvalidArgumentException - */ public function testTimezoneFail() { + $this->expectException(\InvalidArgumentException::class); $tz = TimeZoneUtil::getTimeZone('FooBar', null, true); } @@ -356,4 +400,847 @@ public function testPrefixedOffsetExchangeIdentifier() $ex = new \DateTimeZone('America/New_York'); $this->assertEquals($ex->getName(), $tz->getName()); } + + public function testMicrosoftMap() + { + $tz = TimeZoneUtil::getTimeZone('tzone://Microsoft/Utc', null, true); + $ex = new \DateTimeZone('UTC'); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + /** + * @dataProvider unSupportTimezoneProvider + */ + public function testPHPUnSupportTimeZone(string $origin, string $expected) + { + $tz = TimeZoneUtil::getTimeZone($origin, null, true); + $ex = new \DateTimeZone($expected); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function unSupportTimezoneProvider(): iterable + { + yield 'America/Santa_Isabel' => [ + 'origin' => 'America/Santa_Isabel', + 'expected' => 'America/Tijuana', + ]; + + yield 'Asia/Chongqing' => [ + 'origin' => 'Asia/Chongqing', + 'expected' => 'Asia/Shanghai', + ]; + + yield 'Asia/Harbin' => [ + 'origin' => 'Asia/Harbin', + 'expected' => 'Asia/Shanghai', + ]; + + yield 'Asia/Kashgar' => [ + 'origin' => 'Asia/Kashgar', + 'expected' => 'Asia/Urumqi', + ]; + + yield 'Pacific/Johnston' => [ + 'origin' => 'Pacific/Johnston', + 'expected' => 'Pacific/Honolulu', + ]; + + yield 'EDT' => [ + 'origin' => 'EDT', + 'expected' => 'America/Manaus', + ]; + + yield 'CDT' => [ + 'origin' => 'CDT', + 'expected' => 'America/Chicago', + ]; + + yield 'PST' => [ + 'origin' => 'PST', + 'expected' => 'America/Los_Angeles', + ]; + + $microsoftTimezones = << $expected) { + yield $origin => [ + 'origin' => $origin, + 'expected' => $expected, + ]; + } + } + + /** + * @dataProvider offsetTimeZoneProvider + */ + public function testOffsetTimeZones(string $origin, string $expected) + { + $tz = TimeZoneUtil::getTimeZone($origin, null, true); + $ex = new \DateTimeZone($expected); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function offsetTimeZoneProvider(): iterable + { + yield 'UTC-05:00' => [ + 'origin' => 'UTC-05:00', + 'expected' => 'America/Lima', + ]; + + yield '-5' => [ + 'origin' => '-5', + 'expected' => 'America/Lima', + ]; + + yield '-05' => [ + 'origin' => '-05', + 'expected' => 'America/Lima', + ]; + + yield '-05:00' => [ + 'origin' => '-05:00', + 'expected' => 'America/Lima', + ]; + } + + /** + * @dataProvider letterCaseTimeZoneProvider + */ + public function testDifferentLetterCaseTimeZone(string $origin, string $expected) + { + $tz = TimeZoneUtil::getTimeZone($origin, null, true); + $ex = new \DateTimeZone($expected); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function letterCaseTimeZoneProvider(): iterable + { + yield 'case 1' => [ + 'origin' => 'Europe/paris', + 'expected' => 'Europe/Paris', + ]; + + yield 'case 2' => [ + 'origin' => 'europe/paris', + 'expected' => 'Europe/Paris', + ]; + + yield 'case 3' => [ + 'origin' => 'Europe/pAris', + 'expected' => 'Europe/Paris', + ]; + + yield 'case 4' => [ + 'origin' => 'Asia/taipei', + 'expected' => 'Asia/Taipei', + ]; + } + + /** + * @dataProvider outlookCitiesProvider + */ + public function testOutlookCities(string $origin, bool $failIfUncertain, string $expected) + { + $tz = TimeZoneUtil::getTimeZone($origin, null, $failIfUncertain); + $ex = new \DateTimeZone($expected); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function outlookCitiesProvider(): iterable + { + yield 'case 1' => [ + 'origin' => 'TZID:(UTC+01:00) Bruxelles\, København\, Madrid\, Paris', + 'failIfUncertain' => true, + 'expected' => 'Europe/Madrid', + ]; + + yield 'case 2' => [ + 'origin' => 'TZID:(UTC+01:00) Bruxelles, København, Madrid, Paris', + 'failIfUncertain' => true, + 'expected' => 'Europe/Madrid', + ]; + + yield 'case 3' => [ + 'origin' => 'TZID:(UTC+01:00)Bruxelles\, København\, Madrid\, Paris', + 'failIfUncertain' => true, + 'expected' => 'Europe/Madrid', + ]; + + yield 'case 4' => [ + 'origin' => 'Bruxelles\, København\, Madrid\, Paris', + 'failIfUncertain' => false, + 'expected' => 'UTC', + ]; + } + + /** + * @dataProvider versionTzProvider + */ + public function testVersionTz(string $origin, bool $failIfUncertain, string $expected) + { + $tz = TimeZoneUtil::getTimeZone($origin, null, $failIfUncertain); + $ex = new \DateTimeZone($expected); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function versionTzProvider(): iterable + { + yield 'case 1' => [ + 'origin' => 'Eastern Standard Time 1', + 'failIfUncertain' => true, + 'expected' => 'America/New_York', + ]; + + yield 'case 2' => [ + 'origin' => 'Eastern Standard Time 2', + 'failIfUncertain' => true, + 'expected' => 'America/New_York', + ]; + } + + public function testCustomizedTimeZone() + { + $ics = <<assertNotSame('Customized Time Zone', $tz->getName()); + $start = new \DateTimeImmutable('2022-04-25'); + $this->assertSame(10 * 60 * 60, $tz->getOffset($start)); + + $start = new \DateTimeImmutable('2022-11-10'); + $this->assertSame(11 * 60 * 60, $tz->getOffset($start)); + } + + public function testCustomizedTimeZoneWithoutDaylight() + { + $ics = $this->getCustomizedICS(); + $tz = TimeZoneUtil::getTimeZone('Customized Time Zone', Reader::read($ics)); + $this->assertSame('Asia/Brunei', $tz->getName()); + $start = new \DateTimeImmutable('2022-04-25'); + $this->assertSame(8 * 60 * 60, $tz->getOffset($start)); + } + + public function testCustomizedTimeZoneFlag() + { + $this->expectException(\InvalidArgumentException::class); + $ics = $this->getCustomizedICS(); + $vobject = Reader::read($ics); + $vobject->VEVENT->DTSTART->getDateTime(null, false); + } + + private function getCustomizedICS(): string + { + return <<expectException(\InvalidArgumentException::class); $input = <<convert(Document::VCARD40); } - /** - * @expectedException \InvalidArgumentException - */ public function testUnknownTargetVCardVersion() { + $this->expectException(\InvalidArgumentException::class); $input = <<assertInstanceOf('Sabre\\VObject\\Component\\VCard', $vcard); + $this->assertInstanceOf(Component\VCard::class, $vcard); $vcard = $vcard->convert(Document::VCARD40); $vcard = $vcard->serialize(); @@ -518,4 +514,34 @@ public function testNoLabel() $this->assertEquals($expected, str_replace("\r", '', $vcard)); } + + public function testPhoneNumberValueTypeGetsRemoved() + { + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 46e9014cb..2496aa4ff 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,13 +13,3 @@ break; } } - -$autoLoader->addPsr4('Sabre\\VObject\\', __DIR__.'/VObject'); - -if (!defined('SABRE_TEMPDIR')) { - define('SABRE_TEMPDIR', __DIR__.'/temp/'); -} - -if (!file_exists(SABRE_TEMPDIR)) { - mkdir(SABRE_TEMPDIR); -} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 46dad6a3d..c0588d460 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -6,18 +6,16 @@ convertWarningsToExceptions="true" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" - beStrictAboutTestSize="true" > - - VObject/ - + + + . + + ../lib/ - - ../lib/Sabre/VObject/includes.php -