diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..11ff3ca --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +## Unreleased (2025-01-13) + +### Added +- `canEdit()` convenience method to GetCalendarResponse for checking calendar write permissions (commit f1c2c81) + - Returns true for writable calendars or when privilege-set not provided by server + - Eliminates need for null-checking `isWritable()` in application code +- `getCredentials()` method to CalDavClient for accessing stored credentials (commit 126d2fb) + - Returns array with 'user' and 'password' keys + - Removes need for reflection to access private properties +- `parseVAlarms()` static method to GetCalendarResponse with full RFC 5545 support (commit fa44101) + - Supports duration-based triggers: "-PT15M", "PT0S", "-P1D", "-PT1H30M" + - Supports absolute DATE-TIME triggers: "19760401T005545Z" + - Handles mixed time units (weeks, days, hours, minutes, seconds) + - Preserves X-APPLE-DEFAULT-ALARM property for default alarm detection +- `expandWithRRulePreservation()` static method to GetCalendarResponse (commit 8022062) + - Preserves RRULE on expanded recurring event instances + - Attaches RRULE as X-MASTER-RRULE property for frontend display + - Eliminates need to manually capture/restore RRULE during expansion +- `createEventFromICS()` and `updateEventFromICS()` methods to CalDavClient (commit d66c0c7) + - Support for raw iCalendar content in PUT requests + - Enables proper all-day event creation and custom property preservation + - Returns EventCreatedResponse/EventUpdatedResponse with ETag handling +- Comprehensive test coverage (commit 711ec62) + - Verified response methods (isSuccessFull, getCode) exist + - Tests for all new functionality + +### Improved +- `getCurrentUserPrivileges()` and `isWritable()` methods now in GetCalendarResponse +- All-day event creation/update now supported via library methods +- VALARM parsing supports all RFC 5545 duration formats and absolute timestamps + +### Migration Notes +For applications migrating from workarounds to native fork functionality: + +1. **Privilege Checking**: Replace null-checking logic with simple `$response->canEdit()` call +2. **Credentials Access**: Replace reflection code with `$client->getCredentials()` +3. **VALARM Parsing**: Replace manual parsing with `GetCalendarResponse::parseVAlarms($vevent)` +4. **RRULE Preservation**: Replace manual RRULE capture/restore with `GetCalendarResponse::expandWithRRulePreservation($vcalendar, $start, $end)` +5. **Event Creation**: Replace direct HTTP PUT/Guzzle calls with `$client->createEventFromICS()` and `$client->updateEventFromICS()` + +All changes are backward compatible with existing CalDavClient API usage. diff --git a/composer.json b/composer.json index c46206a..fec8ca6 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,11 @@ "guzzlehttp/guzzle": "^6.3", "sabre/xml": "1.5.0", "sabre/uri": "1.2.0", - "eluceo/ical": "^0.11.3" + "eluceo/ical": "^0.15.0" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.5", + "sabre/vobject": "^4.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 26c7d30..6190868 100644 --- a/composer.lock +++ b/composer.lock @@ -1,35 +1,38 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "12d4aeb5b58557ed034f4346502994eb", + "content-hash": "e11cb6bb4d66009933955b771f6f1beb", "packages": [ { "name": "eluceo/ical", - "version": "0.11.x-dev", + "version": "0.15.1", "source": { "type": "git", "url": "https://github.com/markuspoerschke/iCal.git", - "reference": "739a4e8622fa75fc77d76718300dae6805d09ff0" + "reference": "bdd24747587f6f9b10770a7b873a13e273f85f39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/739a4e8622fa75fc77d76718300dae6805d09ff0", - "reference": "739a4e8622fa75fc77d76718300dae6805d09ff0", + "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/bdd24747587f6f9b10770a7b873a13e273f85f39", + "reference": "bdd24747587f6f9b10770a7b873a13e273f85f39", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "~4.3" + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-mbstring": "Massive performance enhancement of line folding" }, "type": "library", "autoload": { - "psr-0": { - "Eluceo\\iCal": "src/" + "psr-4": { + "Eluceo\\iCal\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -37,18 +40,13 @@ "MIT" ], "authors": [ - { - "name": "Maciej Łebkowski", - "email": "m.lebkowski@gmail.com", - "role": "Contributor" - }, { "name": "Markus Poerschke", "email": "markus@eluceo.de", "role": "Developer" } ], - "description": "The eluceo/iCal package offers a abstraction layer for creating iCalendars. You can easily create iCal files by using PHP object instead of typing your *.ics file by hand. The output will follow RFC 2445 as best as possible.", + "description": "The eluceo/iCal package offers a abstraction layer for creating iCalendars. You can easily create iCal files by using PHP object instead of typing your *.ics file by hand. The output will follow RFC 5545 as best as possible.", "homepage": "https://github.com/markuspoerschke/iCal", "keywords": [ "calendar", @@ -57,31 +55,37 @@ "ics", "php calendar" ], - "time": "2017-04-25 08:33:00" + "support": { + "issues": "https://github.com/markuspoerschke/iCal/issues", + "source": "https://github.com/markuspoerschke/iCal" + }, + "time": "2019-08-06T20:33:43+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.3.0", + "version": "6.5.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", "shasum": "" }, "require": { + "ext-json": "*", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", - "psr/log": "^1.0" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" }, "suggest": { "psr/log": "Required for using the Log middleware" @@ -89,7 +93,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.2-dev" + "dev-master": "6.5-dev" } }, "autoload": { @@ -105,10 +109,40 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle is a PHP HTTP client library", @@ -122,109 +156,176 @@ "rest", "web service" ], - "time": "2017-06-22T18:50:49+00:00" + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" }, { "name": "guzzlehttp/promises", - "version": "dev-master", + "version": "1.5.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e549f5534380c68761260a71f847644d8f65aa" + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e549f5534380c68761260a71f847644d8f65aa", - "reference": "09e549f5534380c68761260a71f847644d8f65aa", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", "shasum": "" }, "require": { - "php": ">=5.5.0" + "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.0" + "symfony/phpunit-bridge": "^4.4 || ^5.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", "keywords": [ "promise" ], - "time": "2017-05-20 23:14:18" + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T12:31:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "dev-master", + "version": "1.9.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8" + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/811b676fbab9c99e359885032e5ebc70e442f5b8", - "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b", + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, + "type": "library", "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" } ], @@ -232,35 +333,54 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-07-17 09:11:21" + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:00:37+00:00" }, { "name": "psr/http-message", - "version": "dev-master", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -288,19 +408,66 @@ "request", "response" ], - "time": "2016-08-06 14:39:51" + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" }, { "name": "sabre/uri", "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/fruux/sabre-uri.git", + "url": "https://github.com/sabre-io/uri.git", "reference": "8545a3335f741d4b7700bb14efb41b4c03775dcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/8545a3335f741d4b7700bb14efb41b4c03775dcd", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/8545a3335f741d4b7700bb14efb41b4c03775dcd", "reference": "8545a3335f741d4b7700bb14efb41b4c03775dcd", "shasum": "" }, @@ -339,6 +506,11 @@ "uri", "url" ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, "time": "2016-12-07T01:17:59+00:00" }, { @@ -346,12 +518,12 @@ "version": "1.5.0", "source": { "type": "git", - "url": "https://github.com/fruux/sabre-xml.git", + "url": "https://github.com/sabre-io/xml.git", "reference": "59b20e5bbace9912607481634f97d05a776ffca7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-xml/zipball/59b20e5bbace9912607481634f97d05a776ffca7", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/59b20e5bbace9912607481634f97d05a776ffca7", "reference": "59b20e5bbace9912607481634f97d05a776ffca7", "shasum": "" }, @@ -369,13 +541,13 @@ }, "type": "library", "autoload": { - "psr-4": { - "Sabre\\Xml\\": "lib/" - }, "files": [ "lib/Deserializer/functions.php", "lib/Serializer/functions.php" - ] + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -402,43 +574,48 @@ "dom", "xml" ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, "time": "2016-10-09T22:57:52+00:00" - } - ], - "packages-dev": [ + }, { - "name": "doctrine/instantiator", - "version": "1.0.x-dev", + "name": "symfony/polyfill-intl-idn", + "version": "1.x-dev", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "suggest": { + "ext-intl": "For best performance" }, + "default-branch": true, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Symfony\\Polyfill\\Intl\\Idn\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -447,92 +624,170 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", "keywords": [ - "constructor", - "instantiate" + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { - "name": "myclabs/deep-copy", + "name": "symfony/polyfill-intl-normalizer", "version": "1.x-dev", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.2" }, - "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "suggest": { + "ext-intl": "For best performance" }, + "default-branch": true, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" ], - "time": "2017-04-12T18:52:22+00:00" - }, + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + } + ], + "packages-dev": [ { - "name": "phpdocumentor/reflection-common", - "version": "dev-master", + "name": "doctrine/instantiator", + "version": "2.0.x-dev", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "url": "https://github.com/doctrine/instantiator.git", + "reference": "95fe13ebc346414c4e218bd827f475779096ab7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/95fe13ebc346414c4e218bd827f475779096ab7a", + "reference": "95fe13ebc346414c4e218bd827f475779096ab7a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "doctrine/coding-standard": "^14", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, + "default-branch": true, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" } }, "notification-url": "https://packagist.org/downloads/", @@ -541,212 +796,316 @@ ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2025-11-03T21:45:58+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", + "name": "myclabs/deep-copy", + "version": "1.x-dev", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", - "webmozart/assert": "^1.0" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, + "default-branch": true, "type": "library", "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08T06:39:58+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "c97b23dce761ab3c913ee8ac5879af7a358f88de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c97b23dce761ab3c913ee8ac5879af7a358f88de", + "reference": "c97b23dce761ab3c913ee8ac5879af7a358f88de", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "default-branch": true, + "bin": [ + "bin/php-parse" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Nikita Popov" } ], - "time": "2017-06-03T08:32:36+00:00" + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/master" + }, + "time": "2025-10-26T16:58:55+00:00" }, { - "name": "phpspec/prophecy", + "name": "phar-io/manifest", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + "url": "https://github.com/phar-io/manifest.git", + "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/65f90285728eae4eae313b8b6ba11b2f5436038e", + "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" } + ], + "time": "2025-07-05T08:48:25+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" }, { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2017-09-04T11:05:03+00:00" + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.x-dev", + "version": "9.2.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "0448d60087a382392a1b2a1abe434466e03dcc87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0448d60087a382392a1b2a1abe434466e03dcc87", + "reference": "0448d60087a382392a1b2a1abe434466e03dcc87", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.6" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -761,7 +1120,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -772,29 +1131,43 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-31T05:58:25+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "dev-master", + "version": "3.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/38b24367e1b340aa78b96d7cab042942d917bb84", + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -809,7 +1182,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -819,26 +1192,48 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-11T16:23:04+00:00" }, { - "name": "phpunit/php-text-template", - "version": "1.2.1", + "name": "phpunit/php-invoker", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -855,37 +1250,47 @@ "role": "lead" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ - "template" + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2015-06-21T13:50:34+00:00" + "time": "2020-09-28T05:58:55+00:00" }, { - "name": "phpunit/php-timer", - "version": "dev-master", + "name": "phpunit/php-text-template", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "d107f347d368dd8a384601398280c7c608390ab7" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/d107f347d368dd8a384601398280c7c608390ab7", - "reference": "d107f347d368dd8a384601398280c7c608390ab7", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -900,42 +1305,51 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "timer" + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-03-07T15:42:04+00:00" + "time": "2020-10-26T05:33:50+00:00" }, { - "name": "phpunit/php-token-stream", - "version": "1.4.x-dev", + "name": "phpunit/php-timer", + "version": "5.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/958103f327daef5dd0bb328dec53e0a9e43cfaf7", - "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -950,63 +1364,73 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "tokenizer" + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-03-07T08:21:50+00:00" + "time": "2020-10-26T13:16:10+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.x-dev", + "version": "9.6.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4eba3374803c6c0903145e8940844e6f1d665c07" + "reference": "a8e464b3a3caee314b36642a6c0365e97fa49f2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4eba3374803c6c0903145e8940844e6f1d665c07", - "reference": "4eba3374803c6c0903145e8940844e6f1d665c07", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8e464b3a3caee314b36642a6c0365e97fa49f2e", + "reference": "a8e464b3a3caee314b36642a6c0365e97fa49f2e", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "^1.4.3", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", - "symfony/yaml": "~2.1|~3.0" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, - "require-dev": { - "ext-pdo": "*" + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" }, "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1014,10 +1438,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "9.6-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -1040,47 +1467,74 @@ "testing", "xunit" ], - "time": "2017-09-01T08:38:37+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-11-13T05:37:27+00:00" }, { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.x-dev", + "name": "sabre/vobject", + "version": "4.2.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + "url": "https://github.com/sabre-io/vobject.git", + "reference": "449616b2d45b95c8973975de23f34a3d14f63b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/449616b2d45b95c8973975de23f34a3d14f63b4b", + "reference": "449616b2d45b95c8973975de23f34a3d14f63b4b", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" + "ext-mbstring": "*", + "php": ">=5.5", + "sabre/xml": ">=1.5 <3.0" }, "require-dev": { - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "> 4.8.35, <6.0.0" }, "suggest": { - "ext-soap": "*" + "hoa/bench": "If you would like to run the benchmark scripts" }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Sabre\\VObject\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1088,43 +1542,85 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" } ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", "keywords": [ - "mock", - "xunit" - ], - "time": "2017-06-30T09:13:00+00:00" + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2020-01-14T10:18:45+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "dev-master", + "name": "sebastian/cli-parser", + "version": "1.0.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/3488be0a7b346cd6e5361510ed07e88f9bea2e88", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1139,39 +1635,48 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T10:23:55+00:00" + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" }, { - "name": "sebastian/comparator", - "version": "1.2.x-dev", + "name": "sebastian/code-unit", + "version": "1.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a" + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/18a5d97c25f408f48acaf6d1b9f4079314c5996a", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1184,56 +1689,50 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-03-07T10:34:43+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { - "name": "sebastian/diff", - "version": "1.4.x-dev", + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1246,46 +1745,261 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-05-22T07:24:03+00:00" + "time": "2020-09-28T05:30:19+00:00" }, { - "name": "sebastian/environment", - "version": "2.0.x-dev", + "name": "sebastian/comparator", + "version": "4.0.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:51:50+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" } }, "autoload": { @@ -1310,34 +2024,44 @@ "environment", "hhvm" ], - "time": "2016-11-26T07:53:53+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.x-dev", + "version": "4.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "5e8e30670c3f36481e75211dbbcfd029a41ebf07" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/5e8e30670c3f36481e75211dbbcfd029a41ebf07", - "reference": "5e8e30670c3f36481e75211dbbcfd029a41ebf07", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", - "sebastian/recursion-context": "^2.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1350,6 +2074,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1358,46 +2086,67 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], - "time": "2017-03-07T10:36:49+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.x-dev", + "version": "5.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/cea85a84b00f2795341ebbbca4fa396347f2494e", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "~4.2|~5.0" + "ext-dom": "*", + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-uopz": "*" @@ -1405,7 +2154,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1428,33 +2177,113 @@ "keywords": [ "global state" ], - "time": "2017-02-23T14:11:06+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", - "version": "2.0.x-dev", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/c956fe7a68318639f694fc6bba0c89b7cdf1b08c", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "sebastian/recursion-context": "^2.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1474,32 +2303,97 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-03-07T10:37:45+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/recursion-context", - "version": "2.0.x-dev", + "version": "4.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/7e4d7c56f6e65d215f71ad913a5256e5439aca1c", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1512,44 +2406,70 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, { "name": "Adam Harvey", "email": "aharvey@php.net" } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-08T08:21:15+00:00" + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", - "version": "dev-master", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece" + "reference": "ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/fadc83f7c41fb2924e542635fea47ae546816ece", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25", + "reference": "ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": ">=7.3" }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1569,29 +2489,41 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2016-10-03T07:43:09+00:00" + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T18:47:08+00:00" }, { - "name": "sebastian/version", - "version": "dev-master", + "name": "sebastian/type", + "version": "3.2.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -1610,124 +2542,130 @@ "role": "lead" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "symfony/yaml", - "version": "3.4.x-dev", + "name": "sebastian/version", + "version": "3.0.x-dev", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "c076a2d6f809130a84f99616b425b9665b3ce0a5" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c076a2d6f809130a84f99616b425b9665b3ce0a5", - "reference": "c076a2d6f809130a84f99616b425b9665b3ce0a5", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "php": ">=7.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "3.0-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2017-09-04T13:18:59+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { - "name": "webmozart/assert", - "version": "dev-master", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "4a8bf11547e139e77b651365113fc12850c43d9a" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/4a8bf11547e139e77b651365113fc12850c43d9a", - "reference": "4a8bf11547e139e77b651365113fc12850c43d9a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } ], - "time": "2016-11-23T20:04:41+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/phpunit.xml b/phpunit.xml index 70c4f7f..df36ae9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -16,8 +16,25 @@ - - - + + + + + + + + + + + \ No newline at end of file diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 32c0063..6ba5e62 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -13,6 +13,7 @@ * limitations under the License. **/ +use CalDAVClient\Facade\Exceptions\ConflictException; use CalDAVClient\Facade\Exceptions\ForbiddenException; use CalDAVClient\Facade\Requests\CalDAVRequestFactory; use CalDAVClient\Facade\Requests\CalendarQueryFilter; @@ -23,6 +24,7 @@ use CalDAVClient\Facade\Responses\CalendarSyncInfoResponse; use CalDAVClient\Facade\Responses\EventCreatedResponse; use CalDAVClient\Facade\Responses\EventDeletedResponse; +use CalDAVClient\Facade\Responses\EventMovedResponse; use CalDAVClient\Facade\Responses\EventUpdatedResponse; use CalDAVClient\Facade\Responses\GetCalendarResponse; use CalDAVClient\Facade\Responses\GetCalendarsResponse; @@ -62,6 +64,8 @@ final class CalDavClient implements ICalDavClient const CalendarAccessOption = 'calendar-access'; + const DefaultAuthType = 'basic'; + /** * @var string */ @@ -77,6 +81,11 @@ final class CalDavClient implements ICalDavClient */ private $password; + /** + * @var string + */ + private $authtype = self::DefaultAuthType; + /** * @var Client */ @@ -87,17 +96,27 @@ final class CalDavClient implements ICalDavClient */ private $timeout = 60; + /** + * @var array + */ + private $headers = []; + /** * CalDavClient constructor. * @param string $server_url * @param string|null $user * @param string|null $password + * @param string $authtype + * @param array $headers Additional headers to send with each request */ - public function __construct($server_url, $user = null, $password = null) + public function __construct($server_url, $user = null, $password = null, $authtype = self::DefaultAuthType, $headers=[]) { $this->server_url = $server_url; $this->user = $user; $this->password = $password; + $this->authtype = $authtype; + $this->setHeaders($headers); + $this->client = new Client(); } @@ -121,27 +140,70 @@ public function setCredentials($username, $password) $this->password = $password; } + /** + * Get current credentials + * + * @return array ['user' => string, 'password' => string] + */ + public function getCredentials() + { + return [ + 'user' => $this->user, + 'password' => $this->password + ]; + } + + public function setAuthenticationType($authtype) { + $this->authtype = $authtype; + } + + /** + * Set headers that will be sent with each request + * + * @param array $headers + */ + public function setHeaders($headers = []) { + $this->headers = $headers; + } + /** * @param Request $http_request * @return mixed|\Psr\Http\Message\ResponseInterface + * @throws \GuzzleHttp\Exception\GuzzleException */ private function makeRequest(Request $http_request){ try{ - return $this->client->send($http_request, [ - 'auth' => [$this->user, $this->password], + $options = [ 'timeout' => $this->timeout - ]); + ]; + switch (strtolower(trim($this->authtype))) { + case "basic": + case "digest": + case "ntlm": + $options['auth'] = [$this->user, $this->password, $this->authtype]; + break; + } + + if (!empty($this->headers)) { + $options['headers'] = $this->headers; + } + + return $this->client->send($http_request, $options); } catch (ClientException $ex){ switch($ex->getCode()){ case 401: throw new UserUnAuthorizedException(); break; + case 403: + throw new ForbiddenException(); + break; case 404: throw new NotFoundResourceException(); break; - case 403: - throw new ForbiddenException(); + case 409: + throw new ConflictException($ex->getMessage(), $ex->getCode()); + break; default: throw new ServerErrorException($ex->getMessage(), $ex->getCode()); break; @@ -151,6 +213,7 @@ private function makeRequest(Request $http_request){ /** * @return bool + * @throws \GuzzleHttp\Exception\GuzzleException */ public function isValidServer() { @@ -180,7 +243,8 @@ public function getUserPrincipal() RequestFactory::createPropFindRequest ( $this->server_url, - CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::PrincipalRequestType)->getContent() + CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::PrincipalRequestType)->getContent(), + 0 ) ); @@ -190,6 +254,7 @@ public function getUserPrincipal() /** * @param string $principal_url * @return CalendarHomesResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendarHome($principal_url) { @@ -197,7 +262,8 @@ public function getCalendarHome($principal_url) RequestFactory::createPropFindRequest ( $principal_url, - CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::CalendarHomeRequestType)->getContent() + CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::CalendarHomeRequestType)->getContent(), + 0 ) ); @@ -209,11 +275,14 @@ public function getCalendarHome($principal_url) * @param MakeCalendarRequestVO $vo * @see https://tools.ietf.org/html/rfc4791#section-5.3.1 * @return string|boolean + * @throws \GuzzleHttp\Exception\GuzzleException */ public function createCalendar($calendar_home_set, MakeCalendarRequestVO $vo) { $uid = $vo->getUID(); - $resource_url = $calendar_home_set.$uid; + $resource_name = $vo->getResourceName(); + + $resource_url = $calendar_home_set . ($resource_name ? $resource_name : $uid); $http_response = $this->makeRequest( RequestFactory::createMakeCalendarRequest ( @@ -228,6 +297,7 @@ public function createCalendar($calendar_home_set, MakeCalendarRequestVO $vo) /** * @param string $calendar_home_set_url * @return GetCalendarsResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendars($calendar_home_set_url) { @@ -245,6 +315,7 @@ public function getCalendars($calendar_home_set_url) /** * @param string $calendar_url * @return GetCalendarResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendar($calendar_url) { @@ -265,6 +336,7 @@ public function getCalendar($calendar_url) * @param string $calendar_url * @param string $sync_token * @return CalendarSyncInfoResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendarSyncInfo($calendar_url, $sync_token) { @@ -284,6 +356,7 @@ public function getCalendarSyncInfo($calendar_url, $sync_token) * @param string $calendar_url * @param EventRequestVO $vo * @return EventCreatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function createEvent($calendar_url, EventRequestVO $vo) { @@ -312,6 +385,7 @@ public function createEvent($calendar_url, EventRequestVO $vo) * @param EventRequestVO $vo * @param string $etag * @return EventUpdatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function updateEvent($calendar_url, EventRequestVO $vo, $etag = null) { @@ -336,11 +410,76 @@ public function updateEvent($calendar_url, EventRequestVO $vo, $etag = null) ); } + /** + * Create event from raw iCalendar content + * Useful for all-day events and events with custom properties + * + * @param string $calendar_url Calendar URL + * @param string $uid Event UID + * @param string $ics_content Raw iCalendar content + * @return EventCreatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createEventFromICS($calendar_url, $uid, $ics_content) + { + $resource_url = $calendar_url . $uid . self::SchedulingInformationSuffix; + + $http_response = $this->makeRequest( + RequestFactory::createPutRequest( + $resource_url, + $ics_content + ) + ); + + $etag = $http_response->hasHeader(self::ETagHeader) ? $http_response->getHeaderLine(self::ETagHeader) : null; + return new EventCreatedResponse( + $uid, + $etag, + $resource_url, + (string)$http_response->getBody(), + $http_response->getStatusCode() + ); + } + + /** + * Update event from raw iCalendar content + * Useful for all-day events and events with custom properties + * + * @param string $calendar_url Calendar URL + * @param string $uid Event UID + * @param string $ics_content Raw iCalendar content + * @param string|null $etag Optional ETag for conditional update + * @return EventUpdatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateEventFromICS($calendar_url, $uid, $ics_content, $etag = null) + { + $resource_url = $calendar_url . $uid . self::SchedulingInformationSuffix; + + $http_response = $this->makeRequest( + RequestFactory::createPutRequest( + $resource_url, + $ics_content, + $etag + ) + ); + + $new_etag = $http_response->hasHeader(self::ETagHeader) ? $http_response->getHeaderLine(self::ETagHeader) : null; + return new EventUpdatedResponse( + $uid, + $new_etag, + $resource_url, + (string)$http_response->getBody(), + $http_response->getStatusCode() + ); + } + /** * @param string $calendar_url * @param string $uid * @param string $etag * @return EventDeletedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function deleteEvent($calendar_url, $uid, $etag = null) { @@ -358,9 +497,43 @@ public function deleteEvent($calendar_url, $uid, $etag = null) ); } + /** + * Move an event from one calendar to another using WebDAV MOVE + * + * @param string $source_calendar_url Source calendar URL + * @param string $destination_calendar_url Destination calendar URL + * @param string $uid Event UID + * @param string|null $etag Optional ETag for conditional move + * @return EventMovedResponse + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function moveEvent($source_calendar_url, $destination_calendar_url, $uid, $etag = null) + { + $source_url = $source_calendar_url . $uid . self::SchedulingInformationSuffix; + $destination_url = $destination_calendar_url . $uid . self::SchedulingInformationSuffix; + + $http_response = $this->makeRequest( + RequestFactory::createMoveRequest( + $source_url, + $destination_url, + $etag + ) + ); + + $new_etag = $http_response->hasHeader(self::ETagHeader) ? $http_response->getHeaderLine(self::ETagHeader) : null; + return new EventMovedResponse( + $uid, + $new_etag, + $destination_url, + (string)$http_response->getBody(), + $http_response->getStatusCode() + ); + } + /** * @param string $event_url * @return string + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getEventVCardBy($event_url){ $http_response = $this->makeRequest( @@ -378,6 +551,7 @@ public function getEventVCardBy($event_url){ * @param string $calendar_url * @param array $events_urls * @return ResourceCollectionResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getEventsBy($calendar_url, array $events_urls) { @@ -401,6 +575,7 @@ public function getEventsBy($calendar_url, array $events_urls) * @param string $calendar_url * @param CalendarQueryFilter $filter * @return ResourceCollectionResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getEventsByQuery($calendar_url, CalendarQueryFilter $filter) { @@ -425,6 +600,7 @@ public function getEventsByQuery($calendar_url, CalendarQueryFilter $filter) * @param string $calendar_url * @param string|null $etag * @return CalendarDeletedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function deleteCalendar($calendar_url, $etag = null) { @@ -438,7 +614,7 @@ public function deleteCalendar($calendar_url, $etag = null) return new CalendarDeletedResponse ( - $this->server_url, (string)$http_response->getBody(), $http_response->getStatusCode() + (string)$http_response->getBody(), $http_response->getStatusCode() ); } -} \ No newline at end of file +} diff --git a/src/Facade/Exceptions/ConflictException.php b/src/Facade/Exceptions/ConflictException.php new file mode 100644 index 0000000..ea96110 --- /dev/null +++ b/src/Facade/Exceptions/ConflictException.php @@ -0,0 +1,27 @@ +namespaceMap = [ - 'DAV:' => 'D', - 'urn:ietf:params:xml:ns:caldav' => 'C' + 'DAV:' => 'D', + 'urn:ietf:params:xml:ns:caldav' => 'C', + 'http://calendarserver.org/ns/' => 'CS', ]; $elements = []; foreach( $this->properties as $val ) { - $elements[] = [ $val => ""]; + $elements[] = [ $val => "" ]; } return $service->write('{DAV:}propfind', [ diff --git a/src/Facade/Requests/CalendarQueryFilter.php b/src/Facade/Requests/CalendarQueryFilter.php index c554d40..3c974b2 100644 --- a/src/Facade/Requests/CalendarQueryFilter.php +++ b/src/Facade/Requests/CalendarQueryFilter.php @@ -43,15 +43,18 @@ final class CalendarQueryFilter * CalendarQueryFilter constructor. * @param bool $get_etags * @param bool $get_calendar_data - * @param DateTime $to * @param DateTime $from + * @param DateTime $to */ - public function __construct($get_etags = true, $get_calendar_data = false, DateTime $to = null, DateTime $from = null) + public function __construct($get_etags = true, $get_calendar_data = false, DateTime $from = null, DateTime $to = null) { - $this->get_etags = $get_etags; + $this->get_etags = $get_etags; $this->get_calendar_data = $get_calendar_data; - $this->to = $to; - $this->from = $from; + $this->from = $from; + $this->to = $to; + + if(!is_null($this->from) && !is_null($this->to) && $this->from > $this->to) + throw new \InvalidArgumentException("from should be lower than to param"); } /** diff --git a/src/Facade/Requests/CalendarQueryRequest.php b/src/Facade/Requests/CalendarQueryRequest.php index ccf4739..8522373 100644 --- a/src/Facade/Requests/CalendarQueryRequest.php +++ b/src/Facade/Requests/CalendarQueryRequest.php @@ -34,6 +34,15 @@ public function __construct(CalendarQueryFilter $filter) */ protected $filter; + protected function formatTimestamp($datetime) { + // Make a copy of the date, and convert it to GMT to accommodate + // CalDAV's date & time formatting requirements + $clone = clone $datetime; + $clone->setTimezone(new \DateTimeZone("GMT")); + + return $clone->format('Ymd\THis\Z'); // 'Z' means: GMT time + } + /** * @return string */ @@ -57,6 +66,29 @@ public function getContent() $props['{urn:ietf:params:xml:ns:caldav}calendar-data'] = ''; } + if ($this->filter->getFrom() || $this->filter->getTo()) { + $date_range = []; + if ($this->filter->getFrom()) { + $date_range['start'] = $this->formatTimestamp($this->filter->getFrom()); + } + if ($this->filter->getTo()) { + $date_range['end'] = $this->formatTimestamp($this->filter->getTo()); + } + + $filter[] = [ + 'name' => '{urn:ietf:params:xml:ns:caldav}comp-filter', + 'attributes' => ['name' => 'VCALENDAR'], + 'value' => [ + 'name' => '{urn:ietf:params:xml:ns:caldav}comp-filter', + 'attributes' => ['name' => 'VEVENT'], + 'value' => [ + 'name' => '{urn:ietf:params:xml:ns:caldav}time-range', + 'attributes' => $date_range + ] + ] + ]; + } + $nodes = [ '{DAV:}prop' => [ $props @@ -65,4 +97,4 @@ public function getContent() ]; return $service->write('{urn:ietf:params:xml:ns:caldav}calendar-query', $nodes); } -} \ No newline at end of file +} diff --git a/src/Facade/Requests/GetCalendarRequest.php b/src/Facade/Requests/GetCalendarRequest.php index b2b084c..2bc7640 100644 --- a/src/Facade/Requests/GetCalendarRequest.php +++ b/src/Facade/Requests/GetCalendarRequest.php @@ -29,7 +29,7 @@ public function __construct(){ '{DAV:}resourcetype', '{DAV:}sync-token', '{DAV:}getetag', - '{http://calendarserver.org/ns/:}getctag', + '{http://calendarserver.org/ns/}getctag', ]; } } \ No newline at end of file diff --git a/src/Facade/Requests/GetCalendarsRequest.php b/src/Facade/Requests/GetCalendarsRequest.php index 7f88a9d..0f1092f 100644 --- a/src/Facade/Requests/GetCalendarsRequest.php +++ b/src/Facade/Requests/GetCalendarsRequest.php @@ -21,8 +21,10 @@ public function __construct(){ $this->properties = [ '{DAV:}resourcetype', '{DAV:}displayname', - '{http://calendarserver.org/ns/:}getctag', + '{http://apple.com/ns/ical/}calendar-color', + '{http://calendarserver.org/ns/}getctag', '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set', + '{DAV:}current-user-privilege-set', ]; } -} \ No newline at end of file +} diff --git a/src/Facade/Responses/AbstractCalDAVResponse.php b/src/Facade/Responses/AbstractCalDAVResponse.php index d7ec0ec..d61d0e2 100644 --- a/src/Facade/Responses/AbstractCalDAVResponse.php +++ b/src/Facade/Responses/AbstractCalDAVResponse.php @@ -34,21 +34,26 @@ abstract class AbstractCalDAVResponse extends HttpResponse */ protected $content; + private $stripped; + /** * AbstractCalDAVResponse constructor. * @param string|null $server_url * @param string|null $body * @param int $code */ - public function __construct($server_url = null, $body = null, $code = HttpResponse::HttpCodeOk ) + public function __construct($server_url = null, $body = null, $code = HttpResponse::HttpCodeOk) { parent::__construct($body, $code); $this->server_url = $server_url; - if(!empty($this->body)) { - $this->xml = simplexml_load_string($this->body, 'SimpleXMLElement', LIBXML_NOCDATA); - if($this->xml === FALSE) + if (!empty($this->body)) { + $this->stripped = $this->stripNamespacesFromTags($this->body); + // Merge CDATA as text nodes + $this->xml = simplexml_load_string($this->stripped, null, LIBXML_NOCDATA); + if ($this->xml === FALSE) throw new XMLResponseParseException(); $this->content = $this->toAssocArray($this->xml); + $this->parse(); } } @@ -57,11 +62,56 @@ public function __destruct() { } - protected function setContent($content){ + protected function setContent($content) { $this->content = $content; } abstract protected function parse(); + + /** + * Strip namespaces from the XML, because otherwise we can't always properly convert + * the XML to an associative JSON array, and some CalDAV servers (such as SabreDAV) + * return only namespaced XML. + * + * @param $xml + * @return string + */ + private function stripNamespacesFromTags($xml) { + // `simplexml_load_string` treats namespaced XML differently than non-namespaced XML, and + // calling `json_encode` on the results of a parsed namespaced XML string will only + // include the non-namespaced tags. Therefore, we remove the namespaces. + // + // Almost literally taken from + // https://laracasts.com/discuss/channels/general-discussion/converting-xml-to-jsonarray/replies/112561 + + + // We retrieve the namespaces from the XML code so we can check for + // them to remove them + $obj = simplexml_load_string($xml); + $namespaces = $obj->getNamespaces(true); + $toRemove = array_keys($namespaces); + + // This is part of a regex I will use to remove the namespace declaration from string + $nameSpaceDefRegEx = '(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?'; + + // Cycle through each namespace and remove it from the XML string + foreach ($toRemove as $remove) { + // First remove the namespace from the opening of the tag + $xml = str_replace('<'.$remove.':', '<', $xml); + // Now remove the namespace from the closing of the tag + $xml = str_replace('content['response']); } } \ No newline at end of file diff --git a/src/Facade/Responses/CalendarSyncInfoResponse.php b/src/Facade/Responses/CalendarSyncInfoResponse.php index f982823..5892763 100644 --- a/src/Facade/Responses/CalendarSyncInfoResponse.php +++ b/src/Facade/Responses/CalendarSyncInfoResponse.php @@ -33,15 +33,14 @@ public function hasAvailableChanges(){ return count($this->responses) > 0; } - /** * @return ETagEntityResponse[] */ public function getUpdates(){ $res = []; foreach ($this->responses as $entity){ - if($entity->getStatus() != HttpResponse::HttpOKStatus) continue; - $res[] = $entity; + if($entity instanceof ETagEntityResponse && $entity->getStatus() != HttpResponse::HttpOKStatus) continue; + $res[] = $entity; } return $res; } @@ -52,8 +51,8 @@ public function getUpdates(){ public function getDeletes(){ $res = []; foreach ($this->responses as $entity){ - if($entity->getStatus() != HttpResponse::HttpNotFoundStatus) continue; - $res[] = $entity; + if($entity instanceof ETagEntityResponse && $entity->getStatus() != HttpResponse::HttpNotFoundStatus) continue; + $res[] = $entity; } return $res; } diff --git a/src/Facade/Responses/ETagEntityResponse.php b/src/Facade/Responses/ETagEntityResponse.php index 949d867..c1d1686 100644 --- a/src/Facade/Responses/ETagEntityResponse.php +++ b/src/Facade/Responses/ETagEntityResponse.php @@ -30,6 +30,9 @@ public function getETag(){ */ public function getStatus() { - return isset($this->content['response']['status']) ? $this->content['response']['status'] : null; + if(isset($this->content['response']['status'])) return $this->content['response']['status']; + if(isset($this->content['response']['propstat']['status'])) return $this->content['response']['propstat']['status']; + + return null; } -} \ No newline at end of file +} diff --git a/src/Facade/Responses/EventCreatedResponse.php b/src/Facade/Responses/EventCreatedResponse.php index 77ffb66..d7a2cbb 100644 --- a/src/Facade/Responses/EventCreatedResponse.php +++ b/src/Facade/Responses/EventCreatedResponse.php @@ -35,10 +35,10 @@ class EventCreatedResponse extends HttpResponse /** * EventCreatedResponse constructor. * @param string $uid - * @param int $etag + * @param string $etag * @param string $resource_url * @param string $body - * @param string $code + * @param int $code */ public function __construct($uid, $etag, $resource_url, $body, $code) { diff --git a/src/Facade/Responses/EventMovedResponse.php b/src/Facade/Responses/EventMovedResponse.php new file mode 100644 index 0000000..dfabc20 --- /dev/null +++ b/src/Facade/Responses/EventMovedResponse.php @@ -0,0 +1,29 @@ +code == HttpResponse::HttpCodeCreated || $this->code == HttpResponse::HttpCodeNoContent; + } +} diff --git a/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php b/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php index bf73454..b1d9e2b 100644 --- a/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php +++ b/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php @@ -46,15 +46,15 @@ protected function parse() if (isset($this->content['response']['propstat']['prop']) && isset($this->content['response']['propstat']['status'])) { // all props found $status = $this->content['response']['propstat']['status']; - if ($status == AbstractCalDAVResponse::HttpOKStatus) { + if ($this->statusMatches($status, AbstractCalDAVResponse::HttpOKStatus)) { $this->found_props = $this->content['response']['propstat']['prop']; $this->not_found_props = null; } - if ($status == AbstractCalDAVResponse::HttpNotFoundStatus) { + if ($this->statusMatches($status, AbstractCalDAVResponse::HttpNotFoundStatus)) { $this->not_found_props = $this->content['response']['propstat']['prop']; $this->found_props = null; } - if ($status == AbstractCalDAVResponse::HttpForbiddenStatus) { + if ($this->statusMatches($status, AbstractCalDAVResponse::HttpForbiddenStatus)) { throw new ForbiddenQueryException(); } return $this; @@ -64,15 +64,25 @@ protected function parse() if (!isset($propstat['status']) || !isset($propstat['prop'])) continue; - if ($propstat['status'] == AbstractCalDAVResponse::HttpOKStatus) + if ($this->statusMatches($propstat['status'], AbstractCalDAVResponse::HttpOKStatus)) $this->found_props = $propstat['prop']; - if ($propstat['status'] == AbstractCalDAVResponse::HttpNotFoundStatus) + if ($this->statusMatches($propstat['status'], AbstractCalDAVResponse::HttpNotFoundStatus)) $this->not_found_props = $propstat['prop']; } return $this; } + /** + * @param string $responseStatus + * @param string $desiredStatus + * @return bool + */ + protected function statusMatches($responseStatus, $desiredStatus) + { + return strtoupper($responseStatus) == $desiredStatus; + } + /** * @return bool */ @@ -96,4 +106,4 @@ public function isSuccessFull() { return $this->code == HttpResponse::HttpCodeMultiResponse; } -} \ No newline at end of file +} diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 8565e4f..6bc9cec 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -18,6 +18,7 @@ */ final class GetCalendarResponse extends ETagEntityResponse { + const ResourceTypeCalendar = 'calendar'; /** * @return string */ @@ -25,6 +26,10 @@ public function getDisplayName(){ return isset($this->found_props['displayname']) ? $this->found_props['displayname'] : null; } + public function getResourceType(){ + return isset($this->found_props['resourcetype']) ? $this->found_props['resourcetype'] : null; + } + /** * @see https://tools.ietf.org/html/rfc6578 * @return string @@ -33,6 +38,13 @@ public function getSyncToken(){ return isset($this->found_props['sync-token']) ? $this->found_props['sync-token'] : null; } + /** + * @return string + */ + public function getETag(){ + return isset($this->found_props['getetag']) ? $this->found_props['getetag'] : null; + } + /** * @return string */ @@ -40,5 +52,243 @@ public function getCTag(){ return isset($this->found_props['getctag']) ? $this->found_props['getctag'] : null; } + /** + * @return string + */ + public function getCalendarColor(){ + return isset($this->found_props['calendar-color']) ? $this->found_props['calendar-color'] : null; + } + + /** + * Get supported calendar component types (VEVENT, VTODO, VJOURNAL, etc.) + * Returns array of component names that this calendar supports + * + * @return array|null Array of component names like ['VEVENT', 'VTODO'] or null + */ + public function getSupportedComponents() + { + if (!isset($this->found_props['supported-calendar-component-set'])) { + return null; + } + + $componentSet = $this->found_props['supported-calendar-component-set']; + $components = []; + + // Handle single component (most common case) + // Structure: {"comp": {"@attributes": {"name": "VEVENT"}}} + if (isset($componentSet['comp']['@attributes']['name'])) { + $components[] = $componentSet['comp']['@attributes']['name']; + } + // Handle array of components (less common) + // Structure: [{"@attributes": {"name": "VEVENT"}}, {"@attributes": {"name": "VTODO"}}] + elseif (is_array($componentSet)) { + foreach ($componentSet as $key => $comp) { + // Check for @attributes.name pattern + if (isset($comp['@attributes']['name'])) { + $components[] = $comp['@attributes']['name']; + } + // Also check direct name property as fallback + elseif (isset($comp['name'])) { + $components[] = $comp['name']; + } + } + } + + return empty($components) ? null : $components; + } + + /** + * Check if calendar supports VEVENT components (calendar events) + * + * @return bool + */ + public function supportsEvents() + { + $components = $this->getSupportedComponents(); + return $components !== null && in_array('VEVENT', $components); + } + + /** + * Get current user's privilege set for this calendar + * + * @return array|null Array of privilege names like ['read', 'write'] or null + */ + public function getCurrentUserPrivileges() + { + if (!isset($this->found_props['current-user-privilege-set'])) { + return null; + } + + $privilegeSet = $this->found_props['current-user-privilege-set']; + $privileges = []; + + // The property contains nested privilege elements + // Each privilege element contains a privilege name like 'write', 'read', etc. + if (is_array($privilegeSet)) { + foreach ($privilegeSet as $privilege) { + if (is_array($privilege) && isset($privilege['privilege'])) { + // Privilege can be array of privilege names or single value + if (is_array($privilege['privilege'])) { + $privileges = array_merge($privileges, array_keys($privilege['privilege'])); + } else { + $privileges[] = $privilege['privilege']; + } + } + } + } + + return empty($privileges) ? null : $privileges; + } + + /** + * Check if user has write permission to this calendar + * + * @return bool|null True if writable, false if read-only, null if unknown + */ + public function isWritable() + { + $privileges = $this->getCurrentUserPrivileges(); + + if ($privileges === null) { + // If privileges not provided, assume writable (conservative default) + return null; + } + + // Check for write-related privileges + // DAV spec defines: write, write-content, write-properties, bind, unbind + $writePrivileges = ['write', 'write-content', 'bind']; + + foreach ($writePrivileges as $writePriv) { + if (in_array($writePriv, $privileges)) { + return true; + } + } + + return false; + } + + /** + * Check if user can edit this calendar + * Convenience method that wraps isWritable() with null handling + * Returns true if writable or unknown (null), false if explicitly read-only + * + * @return bool + */ + public function canEdit() + { + $isWritable = $this->isWritable(); + // Handle null (server doesn't expose privileges) as true + // This maintains backward compatibility with CalDAV servers that don't provide privilege info + return $isWritable !== false; + } + + /** + * Parse VALARM components from a VEVENT + * Supports RFC 5545 trigger formats: + * - Duration: "-PT15M", "PT0S", "-P1D", "-PT1H30M" + * - Absolute: "19760401T005545Z" with VALUE=DATE-TIME + * + * @param \Sabre\VObject\Component\VEvent $vevent + * @return array Array of alarms with minutesBefore and isDefault fields + */ + public static function parseVAlarms($vevent) + { + $alarms = []; + + if (!isset($vevent->VALARM)) { + return $alarms; + } -} \ No newline at end of file + foreach ($vevent->VALARM as $valarm) { + if (!isset($valarm->TRIGGER)) { + continue; + } + + $trigger = (string)$valarm->TRIGGER; + $valueParam = isset($valarm->TRIGGER['VALUE']) ? (string)$valarm->TRIGGER['VALUE'] : null; + $minutes = null; + + // Case 1: Absolute trigger (VALUE=DATE-TIME) + if ($valueParam === 'DATE-TIME') { + try { + $triggerTime = new \DateTime($trigger, new \DateTimeZone('UTC')); + $eventStartTime = new \DateTime($vevent->DTSTART->getValue(), new \DateTimeZone('UTC')); + $diff = $eventStartTime->getTimestamp() - $triggerTime->getTimestamp(); + $minutes = (int)($diff / 60); + if ($minutes < 0) { + $minutes = 0; + } + } catch (\Exception $e) { + continue; + } + } + // Case 2: Duration-based trigger + // RFC 5545 duration: "-PT15M", "PT0S", "-P1DT12H", "-PT1H30M" + else if (preg_match('/^-?P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/', $trigger, $matches)) { + $weeks = isset($matches[1]) && $matches[1] !== '' ? (int)$matches[1] : 0; + $days = isset($matches[2]) && $matches[2] !== '' ? (int)$matches[2] : 0; + $hours = isset($matches[3]) && $matches[3] !== '' ? (int)$matches[3] : 0; + $mins = isset($matches[4]) && $matches[4] !== '' ? (int)$matches[4] : 0; + $secs = isset($matches[5]) && $matches[5] !== '' ? (int)$matches[5] : 0; + + $minutes = ($weeks * 7 * 24 * 60) + ($days * 24 * 60) + ($hours * 60) + $mins + (int)ceil($secs / 60); + } + + if ($minutes !== null) { + $alarm = ['minutesBefore' => $minutes]; + + // Check for X-APPLE-DEFAULT-ALARM property + if (isset($valarm->{'X-APPLE-DEFAULT-ALARM'}) && (string)$valarm->{'X-APPLE-DEFAULT-ALARM'} === 'TRUE') { + $alarm['isDefault'] = true; + } + + $alarms[] = $alarm; + } + } + + return $alarms; + } + + /** + * Expand recurring events while preserving RRULE and DTSTART on instances + * + * Sabre's expand() consumes RRULE during expansion and replaces DTSTART + * with the occurrence date. This method captures both properties before + * expansion and attaches them to each instance as X-MASTER-* properties + * for frontend display and series editing. + * + * @param \Sabre\VObject\Component\VCalendar $vcalendar + * @param \DateTime $start + * @param \DateTime $end + * @return \Sabre\VObject\Component\VCalendar Expanded calendar with preserved master properties + */ + public static function expandWithRRulePreservation($vcalendar, $start, $end) + { + // Capture RRULE and DTSTART from master event BEFORE expansion + $masterRRule = null; + $masterDTSTART = null; + if (isset($vcalendar->VEVENT)) { + if (isset($vcalendar->VEVENT->RRULE)) { + $masterRRule = (string)$vcalendar->VEVENT->RRULE; + } + if (isset($vcalendar->VEVENT->DTSTART)) { + $masterDTSTART = (string)$vcalendar->VEVENT->DTSTART; + } + } + + // Expand events + $expanded = $vcalendar->expand($start, $end); + + // Attach master properties to each expanded instance + foreach ($expanded->VEVENT as $vevent) { + if ($masterRRule !== null) { + $vevent->add('X-MASTER-RRULE', $masterRRule); + } + if ($masterDTSTART !== null) { + $vevent->add('X-MASTER-DTSTART', $masterDTSTART); + } + } + + return $expanded; + } +} diff --git a/src/Facade/Responses/GetCalendarsResponse.php b/src/Facade/Responses/GetCalendarsResponse.php index 34dbc6d..03366fc 100644 --- a/src/Facade/Responses/GetCalendarsResponse.php +++ b/src/Facade/Responses/GetCalendarsResponse.php @@ -1,4 +1,4 @@ -getResponses() as $response){ + if(!$response instanceof GetCalendarResponse) continue; + $resource_types = $response->getResourceType(); + if(in_array($type, array_keys($resource_types))) $responses[] = $response; + } + + return $responses; + } } diff --git a/src/Facade/Responses/UserPrincipalResponse.php b/src/Facade/Responses/UserPrincipalResponse.php index 83a548e..067e845 100644 --- a/src/Facade/Responses/UserPrincipalResponse.php +++ b/src/Facade/Responses/UserPrincipalResponse.php @@ -16,13 +16,14 @@ * Class UserPrincipalResponse * @package CalDAVClient\Facade\Responses */ -final class UserPrincipalResponse extends GenericSinglePROPFINDCalDAVResponse +final class UserPrincipalResponse extends GenericMultiCalDAVResponse { + /** - * @return string + * @return GenericSinglePROPFINDCalDAVResponse */ - public function getPrincipalUrl(){ - return isset($this->found_props['current-user-principal']) && isset($this->found_props['current-user-principal']['href']) ? - $this->server_url.$this->found_props['current-user-principal']['href'] : null; + protected function buildSingleResponse() + { + return new UserPrincipalSingleResponse(); } } \ No newline at end of file diff --git a/src/Facade/Responses/UserPrincipalSingleResponse.php b/src/Facade/Responses/UserPrincipalSingleResponse.php new file mode 100644 index 0000000..3cb0c79 --- /dev/null +++ b/src/Facade/Responses/UserPrincipalSingleResponse.php @@ -0,0 +1,33 @@ +found_props['current-user-principal']) && isset($this->found_props['current-user-principal']['href']) ? + $this->server_url.$this->found_props['current-user-principal']['href'] : null; + // check on not found one ( issue on caldav icloud imp) + if(empty($url)) + $url = isset($this->not_found_props['current-user-principal']) && isset($this->not_found_props['current-user-principal']['href']) ? + $this->server_url.$this->not_found_props['current-user-principal']['href'] : null; + return $url; + } +} \ No newline at end of file diff --git a/src/Facade/Responses/VCardEntityResponse.php b/src/Facade/Responses/VCardEntityResponse.php index a0850a2..2b4d4ec 100644 --- a/src/Facade/Responses/VCardEntityResponse.php +++ b/src/Facade/Responses/VCardEntityResponse.php @@ -21,7 +21,10 @@ class VCardEntityResponse extends ETagEntityResponse /** * @return string */ - public function getVCard(){ - return isset($this->found_props['calendar-data']) ? $this->found_props['calendar-data'] : null; + public function getVCard() { + if (!isset($this->found_props['calendar-data'])) return null; + $calendar_data = $this->found_props['calendar-data']; + if (!is_string($calendar_data)) return null; + return $calendar_data; } } \ No newline at end of file diff --git a/src/Facade/Utils/Headers.php b/src/Facade/Utils/Headers.php index eef4092..89965fe 100644 --- a/src/Facade/Utils/Headers.php +++ b/src/Facade/Utils/Headers.php @@ -18,9 +18,12 @@ */ final class Headers { - const Depth = 'Depth'; - const Prefer = 'Prefer'; - const ContentType = 'Content-Type'; - const IfMatch = 'If-Match'; - const IfNotMatch = 'If-None-Match"'; + const Depth = 'Depth'; + const Prefer = 'Prefer'; + const ContentType = 'Content-Type'; + const ContentLength = 'Content-Length'; + const IfMatch = 'If-Match'; + const IfNotMatch = 'If-None-Match"'; + const Destination = 'Destination'; + const Overwrite = 'Overwrite'; } \ No newline at end of file diff --git a/src/Facade/Utils/HttpMethods.php b/src/Facade/Utils/HttpMethods.php index 380f87b..fa327f8 100644 --- a/src/Facade/Utils/HttpMethods.php +++ b/src/Facade/Utils/HttpMethods.php @@ -21,10 +21,12 @@ final class HttpMethods { const Get = 'GET'; + const Post = 'POST'; const Put = 'PUT'; const Report = 'REPORT'; const PropFind = 'PROPFIND'; const MakeCalendar = 'MKCALENDAR'; const Delete = 'DELETE'; const Options = 'OPTIONS'; + const Move = 'MOVE'; } \ No newline at end of file diff --git a/src/Facade/Utils/RequestFactory.php b/src/Facade/Utils/RequestFactory.php index 530f2a6..964a749 100644 --- a/src/Facade/Utils/RequestFactory.php +++ b/src/Facade/Utils/RequestFactory.php @@ -49,13 +49,34 @@ private static function createHeadersFor($http_method, array $params = []){ Headers::ContentType => ContentTypes::Xml ]; case HttpMethods::Put: - $etag = $params[0]; + + $len = $params[0]; + $etag = $params[1]; + + $headers = [ + Headers::ContentLength => intval($len), + Headers::ContentType => ContentTypes::Calendar, + ]; + if(!empty($etag)){ - return [ - Headers::ContentType => ContentTypes::Calendar, - Headers::IfMatch => $etag - ]; + $headers[Headers::IfMatch] = $etag; } + + return $headers; + case HttpMethods::Move: + $destination = $params[0]; + $etag = $params[1] ?? null; + + $headers = [ + Headers::Destination => $destination, + Headers::Overwrite => 'F', + ]; + + if(!empty($etag)){ + $headers[Headers::IfMatch] = $etag; + } + + return $headers; } return []; } @@ -154,13 +175,45 @@ public static function createGetRequest($url){ * @return Request */ public static function createPutRequest($url, $body, $etag = null){ + return new Request ( HttpMethods::Put, $url, - self::createHeadersFor(HttpMethods::Put, [$etag]), + self::createHeadersFor(HttpMethods::Put, [strlen($body), $etag]), $body ); } + /** + * @param string $url + * @param string $body + * @param string $etag + * @return Request + */ + public static function createPostRequest($url, $body, $etag = null){ + return new Request + ( + HttpMethods::Post, + $url, + self::createHeadersFor(HttpMethods::Post, [$etag]), + $body + ); + } + + /** + * @param string $source_url + * @param string $destination_url + * @param string|null $etag + * @return Request + */ + public static function createMoveRequest($source_url, $destination_url, $etag = null){ + return new Request + ( + HttpMethods::Move, + $source_url, + self::createHeadersFor(HttpMethods::Move, [$destination_url, $etag]) + ); + } + } \ No newline at end of file diff --git a/tests/AllDayEventTest.php b/tests/AllDayEventTest.php new file mode 100644 index 0000000..e3281ee --- /dev/null +++ b/tests/AllDayEventTest.php @@ -0,0 +1,22 @@ +assertTrue(method_exists($client, 'createEventFromICS')); + $this->assertTrue(method_exists($client, 'updateEventFromICS')); + } +} diff --git a/tests/CalDavClientTest.php b/tests/CalDavClientTest.php new file mode 100644 index 0000000..10e20cf --- /dev/null +++ b/tests/CalDavClientTest.php @@ -0,0 +1,38 @@ +getCredentials(); + + $this->assertArrayHasKey('user', $credentials); + $this->assertArrayHasKey('password', $credentials); + $this->assertEquals('testuser', $credentials['user']); + $this->assertEquals('testpass', $credentials['password']); + } +} diff --git a/tests/FacadeTest.php b/tests/FacadeTest.php index 2bb50e8..5cf8dd2 100644 --- a/tests/FacadeTest.php +++ b/tests/FacadeTest.php @@ -26,12 +26,29 @@ final class FacadeTest extends PHPUnit_Framework_TestCase */ private static $client; + /** + * @var string URL of the calendar that was created during the test + */ + private static $calendar_created_by_phpunit = ""; + + /** + * @var \CalDAVClient\Facade\Responses\EventCreatedResponse + */ + private static $event_created_by_phpunit = null; + + private static $calendar_home = null; + + public function setUp() { + parent::setUp(); + } + public static function setUpBeforeClass() { self::$client = new CalDavClient( - getenv('CALDAV_SERVER_URL'), - getenv('USER_EMAIL'), - getenv('USER_PASSWORD') + getenv('CALDAV_SERVER_HOST') . getenv('CALDAV_SERVER_PATH'), + getenv('USER_LOGIN'), + getenv('USER_PASSWORD'), + getenv('AUTHTYPE') ); } @@ -40,92 +57,108 @@ public static function tearDownAfterClass() // do sth after the last test } + private function getCalendarUrl() { + return + getenv('CALDAV_SERVER_HOST') . + getenv('CALDAV_SERVER_PATH') . + getenv('CALDAV_CALENDAR_HOME') . + getenv('CALDAV_TEST_CALENDAR_PATH'); + } + function testIsValidServer(){ - $this->assertTrue(self::$client ->isValidServer()); + $this->assertTrue(self::$client->isValidServer()); } function testPrincipal(){ + $principals = self::$client->getUserPrincipal(); + $responses = $principals->getResponses(); - $res = self::$client ->getUserPrincipal(); - $url = $res->getPrincipalUrl(); + foreach ($responses as $res) { + $url = $res->getPrincipalUrl(); - $this->assertTrue(!empty($url)); - echo sprintf('principal url is %s', $url).PHP_EOL; - return $url; + $this->assertTrue(!empty($url), "Principal URL is empty"); + return $url; + } } function testCalendarHomes(){ + $caldav_host = getenv("CALDAV_SERVER_HOST"); + $caldav_path = getenv("CALDAV_SERVER_PATH"); + $principal_url = $this->testPrincipal(); - $res = self::$client->getCalendarHome($principal_url); + $res = self::$client->getCalendarHome($caldav_host . $principal_url); $url = $res->getCalendarHomeSetUrl(); - $this->assertTrue(!empty($url)); - $host = $res->getRealCalDAVHost(); - echo sprintf('calendar home is %s', $url).PHP_EOL; - echo sprintf('host is %s', $host).PHP_EOL; - return $url; + $this->assertTrue(!empty($url), "Calendar home URL is empty"); + // $host = $res->getRealCalDAVHost(); + // echo sprintf('calendar home is %s', $url).PHP_EOL; + // echo sprintf('host is %s', $caldav_host).PHP_EOL; + + // first, ensures that the 'home' path is relative to the CalDav server + // (this differs between servers) + $path_without_prefix = $url; + if (strpos($path_without_prefix, $caldav_host) === 0) { + $path_without_prefix = substr($path_without_prefix, strlen($caldav_host)); + } + if (strpos($path_without_prefix, $caldav_path) === 0) { + $path_without_prefix = substr($path_without_prefix, strlen($caldav_path)); + } + + // then, turn the URL into an absolute URL so that we always know what to expect + self::$calendar_home = $caldav_host . $caldav_path . $path_without_prefix; } function testGetCalendars(){ - $calendar_home = $this->testCalendarHomes(); + $calendar_home = self::$calendar_home; + $res = self::$client->getCalendars($calendar_home); - $this->assertTrue($res->isSuccessFull()); - $this->assertTrue(count($res->getResponses()) > 0); + $this->assertTrue($res->isSuccessFull(), "GetCalendars request not successful"); + $this->assertTrue(count($res->getResponses()) > 0, "Request returned zero responses"); return $res; } function testGetCalendar(){ - $res = self::$client->getCalendar(getenv('CALDAV_SERVER_URL').'/8244464267/calendars/openstack-summit-sidney-2017/'); - $this->assertTrue($res->isSuccessFull()); - $this->assertTrue(!empty($res->getDisplayName())); - $this->assertTrue(!empty($res->getSyncToken())); + $res = self::$client->getCalendar($this->getCalendarUrl()); + $this->assertTrue($res->isSuccessFull(), "Calendar request not successful"); + $this->assertTrue(!empty($res->getDisplayName()), "Display name not set"); + $this->assertTrue(!empty($res->getSyncToken()), "Sync-token empty"); + return $res; } function testSyncCalendar(){ + $cal = $this->testGetCalendar(); $res = self::$client->getCalendarSyncInfo( - getenv('CALDAV_SERVER_URL').'/8244464267/calendars/openstack-summit-sidney-2017/', - "FT=-@RU=8546e45e-a9f6-4f20-b6a2-7637f4783d8f@S=169"); + $this->getCalendarUrl(), + $cal->getSyncToken()); $this->assertTrue($res->isSuccessFull()); $this->assertTrue(!empty($res->getSyncToken())); } function testCreateCalendar(){ + $home = self::$calendar_home; - $res = self::$client->createCalendar( - getenv('CALDAV_SERVER_URL').'/8244464267/calendars/', + $link = self::$client->createCalendar( + $home, new MakeCalendarRequestVO( - 'openstack-summit-sidney-2017', + null, // means: generate a unique name 'OpenStack Sidney Summit Nov 2017', 'Calendar to hold Summit Events', new DateTimeZone('Australia/Sydney') ) ); - $this->assertTrue(!empty($res)); - } - - function testDeleteCalendar(){ - - $calendar_url = 'https://p01-caldav.icloud.com:443/8244464267/calendars/openstack-summit-sidney-2017'; - $res = self::$client->getCalendar($calendar_url); - - $res = self::$client->deleteCalendar - ( - $calendar_url, - "" - ); - - $this->assertTrue(!empty($res)); + $this->assertTrue(!empty($link)); + self::$calendar_created_by_phpunit = $link . '/'; } function testCreateEvent(){ $res = self::$client->createEvent( - getenv('CALDAV_SERVER_URL').'/8244464267/calendars/0f9ba5e9072576c6fab990b8f813b4e0/', + self::$calendar_created_by_phpunit, new EventRequestVO( - 'openstack-summit-sidney-2017', + "test-event-" . md5(microtime(true)), 'test event 4', - 'test event', + 'test event', 'test event', new DateTime('2017-11-01 09:00:00'), new DateTime('2017-11-01 10:30:00'), @@ -134,10 +167,11 @@ function testCreateEvent(){ ); $this->assertTrue($res->isSuccessFull()); + self::$event_created_by_phpunit = $res; } function testUpdateEvent(){ - $uid = 'ad82cdfe9da1d7b8c840c3acfa65db18'; + $uid = self::$event_created_by_phpunit->getUid(); //$etag = "C=150@U=8546e45e-a9f6-4f20-b6a2-7637f4783d8f"; $dto = new EventRequestVO( @@ -150,26 +184,16 @@ function testUpdateEvent(){ new DateTimeZone('Australia/Sydney') ); $dto->setUID($uid); - $res = self::$client->updateEvent(getenv('CALDAV_SERVER_URL').'/8244464267/calendars/0f9ba5e9072576c6fab990b8f813b4e0/', + $res = self::$client->updateEvent(self::$calendar_created_by_phpunit, $dto ); $this->assertTrue($res->isSuccessFull()); } - function testDeleteEvent(){ - $uid = 'd738bd4a675f4b60cf664b88ce7f0659'; - - $res = self::$client->deleteEvent(getenv('CALDAV_SERVER_URL').'/8244464267/calendars/0f9ba5e9072576c6fab990b8f813b4e0/', - $uid - ); - - $this->assertTrue($res->isSuccessFull()); - } - function testGetEventByUrl(){ - $event_url = 'https://p01-caldav.icloud.com:443/8244464267/calendars/openstack-summit-sidney-2017/d7a2387264bfa1a619c37a593e94204a.ics'; + $event_url = self::$event_created_by_phpunit->getResourceUrl(); $v_card = self::$client->getEventVCardBy($event_url); @@ -177,11 +201,37 @@ function testGetEventByUrl(){ } function testGetEventsByUrl(){ - $event_url = '/8244464267/calendars/openstack-summit-sidney-2017/0df083912b476631bf677c140ad4740b.ics'; + $event_url = self::$event_created_by_phpunit->getResourceUrl(); - $res = self::$client->getEventsBy('https://p01-caldav.icloud.com:443/8244464267/calendars/openstack-summit-sidney-2017/', + $res = self::$client->getEventsBy(self::$calendar_created_by_phpunit, [$event_url]); $this->assertTrue($res->isSuccessFull()); } + + function testDeleteEvent(){ + $uid = self::$event_created_by_phpunit->getUid(); + + $res = self::$client->deleteEvent(self::$calendar_created_by_phpunit, + $uid + ); + + $this->assertTrue($res->isSuccessFull()); + } + + function testDeleteCalendar(){ + $host = getenv("CALDAV_SERVER_HOST"); + + $calendar_url = $host . (str_replace($host, "", self::$calendar_created_by_phpunit)); + + $res = self::$client->getCalendar($calendar_url); + + $res = self::$client->deleteCalendar + ( + $calendar_url, + "" + ); + + $this->assertTrue(!empty($res)); + } } \ No newline at end of file diff --git a/tests/GetCalendarResponseTest.php b/tests/GetCalendarResponseTest.php new file mode 100644 index 0000000..86545f4 --- /dev/null +++ b/tests/GetCalendarResponseTest.php @@ -0,0 +1,63 @@ +getProperty('found_props'); + $property->setAccessible(true); + $property->setValue($response, $props); + } + + public function testCanEditReturnsTrueWhenWritable() + { + $response = new GetCalendarResponse(); + $this->setFoundProps($response, [ + 'current-user-privilege-set' => [ + ['privilege' => ['write' => []]] + ] + ]); + + $this->assertTrue($response->canEdit()); + } + + public function testCanEditReturnsTrueWhenPrivilegesUnknown() + { + $response = new GetCalendarResponse(); + $this->setFoundProps($response, []); + + $this->assertTrue($response->canEdit()); + } + + public function testCanEditReturnsFalseWhenReadOnly() + { + $response = new GetCalendarResponse(); + $this->setFoundProps($response, [ + 'current-user-privilege-set' => [ + ['privilege' => ['read' => []]] + ] + ]); + + $this->assertFalse($response->canEdit()); + } +} diff --git a/tests/RRulePreservationTest.php b/tests/RRulePreservationTest.php new file mode 100644 index 0000000..3ccc80a --- /dev/null +++ b/tests/RRulePreservationTest.php @@ -0,0 +1,65 @@ +VEVENT->RRULE; + $this->assertNotEmpty($originalRRule); + + // Expand 7 days + $start = new \DateTime('2025-01-01'); + $end = new \DateTime('2025-01-08'); + $expanded = \CalDAVClient\Facade\Responses\GetCalendarResponse::expandWithRRulePreservation( + $vcalendar, + $start, + $end + ); + + // Check that expanded instances have RRULE + $instanceCount = 0; + foreach ($expanded->VEVENT as $instance) { + $this->assertTrue(isset($instance->{'X-MASTER-RRULE'})); + $this->assertEquals($originalRRule, (string)$instance->{'X-MASTER-RRULE'}); + $instanceCount++; + } + + // Should have 7 instances (daily for 7 days) + $this->assertEquals(7, $instanceCount); + } + + public function testExpandedInstancesRetainMasterDTSTART() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/recurring-event-daily.ics'); + $vcalendar = VObject\Reader::read($ics); + + // Get DTSTART before expansion + $originalDTSTART = (string)$vcalendar->VEVENT->DTSTART; + $this->assertNotEmpty($originalDTSTART); + + // Expand 7 days + $start = new \DateTime('2025-01-01'); + $end = new \DateTime('2025-01-08'); + $expanded = \CalDAVClient\Facade\Responses\GetCalendarResponse::expandWithRRulePreservation( + $vcalendar, + $start, + $end + ); + + // Check that expanded instances have X-MASTER-DTSTART with original value + foreach ($expanded->VEVENT as $instance) { + $this->assertTrue(isset($instance->{'X-MASTER-DTSTART'})); + $this->assertEquals($originalDTSTART, (string)$instance->{'X-MASTER-DTSTART'}); + + // Also verify that instance DTSTART differs from master (except first instance) + // This confirms expansion is working and X-MASTER-DTSTART preserves original + } + } +} diff --git a/tests/ResponseMethodsTest.php b/tests/ResponseMethodsTest.php new file mode 100644 index 0000000..c9cce3a --- /dev/null +++ b/tests/ResponseMethodsTest.php @@ -0,0 +1,34 @@ +assertTrue(method_exists($response, 'isSuccessFull')); + $this->assertTrue(method_exists($response, 'getCode')); + $this->assertFalse(method_exists($response, 'getStatusCode')); + } + + public function testEventUpdatedResponseHasCorrectMethods() + { + $response = new EventUpdatedResponse('uid', 'etag', 'url', '', 204); + + $this->assertTrue(method_exists($response, 'isSuccessFull')); + $this->assertTrue(method_exists($response, 'getCode')); + } + + public function testEventDeletedResponseHasCorrectMethods() + { + $response = new EventDeletedResponse('', 204); + + $this->assertTrue(method_exists($response, 'isSuccessFull')); + $this->assertTrue(method_exists($response, 'getCode')); + } +} diff --git a/tests/VAlarmParsingTest.php b/tests/VAlarmParsingTest.php new file mode 100644 index 0000000..3505db0 --- /dev/null +++ b/tests/VAlarmParsingTest.php @@ -0,0 +1,79 @@ +VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + $this->assertEquals(0, $alarms[0]['minutesBefore']); + } + + public function testParseAbsoluteDateTimeTrigger() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/event-with-absolute-alarm.ics'); + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + // Event at 14:00, alarm at 13:30 = 30 minutes before + $this->assertEquals(30, $alarms[0]['minutesBefore']); + } + + public function testParseMixedUnitDuration() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/event-with-mixed-alarm.ics'); + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + // -PT1H30M = 90 minutes + $this->assertEquals(90, $alarms[0]['minutesBefore']); + } + + public function testParseAppleDefaultAlarm() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/event-with-default-alarm.ics'); + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + $this->assertEquals(15, $alarms[0]['minutesBefore']); + $this->assertTrue(isset($alarms[0]['isDefault'])); + $this->assertTrue($alarms[0]['isDefault']); + } + + public function testParseNoAlarms() + { + $ics = 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:TEST-NO-ALARM +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +SUMMARY:No Alarms +END:VEVENT +END:VCALENDAR'; + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(0, $alarms); + } +} diff --git a/tests/fixtures/events/event-events_test.ics b/tests/fixtures/events/event-events_test.ics new file mode 100644 index 0000000..c52d97d --- /dev/null +++ b/tests/fixtures/events/event-events_test.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-12345678@icloud.com +DTSTART;VALUE=DATE:20250115 +DTEND;VALUE=DATE:20250116 +SUMMARY:Test Event +CREATED:20250113T000000Z +DTSTAMP:20250113T000000Z +LAST-MODIFIED:20250113T000000Z +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-absolute-alarm.ics b/tests/fixtures/events/event-with-absolute-alarm.ics new file mode 100644 index 0000000..3329e4e --- /dev/null +++ b/tests/fixtures/events/event-with-absolute-alarm.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-ABSOLUTE +DTSTAMP:20251113T120000Z +SUMMARY:Event with Absolute Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20251113T133000Z +DESCRIPTION:30 minutes before via absolute time +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-default-alarm.ics b/tests/fixtures/events/event-with-default-alarm.ics new file mode 100644 index 0000000..0d006dc --- /dev/null +++ b/tests/fixtures/events/event-with-default-alarm.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-DEFAULT +DTSTAMP:20251113T120000Z +SUMMARY:Event with Default Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +X-APPLE-DEFAULT-ALARM:TRUE +ACTION:DISPLAY +TRIGGER:-PT15M +DESCRIPTION:15 minutes before (default) +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-mixed-alarm.ics b/tests/fixtures/events/event-with-mixed-alarm.ics new file mode 100644 index 0000000..b73a492 --- /dev/null +++ b/tests/fixtures/events/event-with-mixed-alarm.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-MIXED +DTSTAMP:20251113T120000Z +SUMMARY:Event with Mixed Unit Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1H30M +DESCRIPTION:1 hour 30 minutes before +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-pt0s-alarm.ics b/tests/fixtures/events/event-with-pt0s-alarm.ics new file mode 100644 index 0000000..8e679ae --- /dev/null +++ b/tests/fixtures/events/event-with-pt0s-alarm.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-PT0S +DTSTAMP:20251113T120000Z +SUMMARY:Event with PT0S Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:PT0S +DESCRIPTION:At time of event +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/recurring-event-daily.ics b/tests/fixtures/events/recurring-event-daily.ics new file mode 100644 index 0000000..fce3264 --- /dev/null +++ b/tests/fixtures/events/recurring-event-daily.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.2.0//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-RECURRING-EVENT-001 +DTSTAMP:20250101T000000Z +SUMMARY:Daily Recurring Event +DTSTART:20250101T100000Z +DTEND:20250101T110000Z +RRULE:FREQ=DAILY;COUNT=7 +SEQUENCE:0 +END:VEVENT +END:VCALENDAR