diff --git a/.gitignore b/.gitignore index cae5d6494..ff0df8c23 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ aws/aws-sdk-php/.changes brick/math/random-tests.php brick/math/psalm*.xmls +cweagans/composer-configurable-plugin/docs +cweagans/composer-configurable-plugin/*.yml + deepdiver/zipstreamer/test deepdiver1975/tarstreamer/.drone.star diff --git a/.patches/sabre-vobject-iTipBroker-replies.patch b/.patches/sabre-vobject-iTipBroker-replies.patch index b335b84eb..e56b3423b 100644 --- a/.patches/sabre-vobject-iTipBroker-replies.patch +++ b/.patches/sabre-vobject-iTipBroker-replies.patch @@ -1,7 +1,7 @@ -From 49d53f187089ec9a67917e564a87c6dea89ea839 Mon Sep 17 00:00:00 2001 +From 4647f615ac01ee82298f6f79e4efda30fb2fc326 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Tue, 30 Dec 2025 13:48:29 -0500 -Subject: [PATCH] fix: send participation reply on fresh event +Subject: [PATCH 1/3] fix: send participation reply on fresh event Signed-off-by: SebastianKrupinski --- @@ -10,10 +10,10 @@ Signed-off-by: SebastianKrupinski 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php -index 76ee0c71..e621c6e2 100644 +index 6f0b1ed2f..fc3db753c 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php -@@ -246,16 +246,29 @@ public function parseEvent($calendar, $userHref, $oldCalendar = null): array +@@ -240,16 +240,29 @@ public function parseEvent($calendar, $userHref, $oldCalendar = null) $baseCalendar = $oldCalendar; } @@ -48,4 +48,4 @@ index 76ee0c71..e621c6e2 100644 + break; } } - \ No newline at end of file + diff --git a/.patches/sabre-vobject-rdate.patch b/.patches/sabre-vobject-rdate.patch index e18bf1bd4..0d59f7ec6 100644 --- a/.patches/sabre-vobject-rdate.patch +++ b/.patches/sabre-vobject-rdate.patch @@ -1,4 +1,4 @@ -From 099a37744a3aaf368bdd339f9c01e34553e8828e Mon Sep 17 00:00:00 2001 +From 35f5b552eac3f4747f0d8090146557dd16566e08 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Wed, 19 Nov 2025 19:19:32 -0500 Subject: [PATCH] fix: use RDATE in time range check and use all instances @@ -11,12 +11,12 @@ Signed-off-by: SebastianKrupinski 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/Component/VEvent.php b/lib/Component/VEvent.php -index e29e56322..6ca196d66 100644 +index 6ea93ed5e..a385a09e1 100644 --- a/lib/Component/VEvent.php +++ b/lib/Component/VEvent.php -@@ -41,7 +41,7 @@ class VEvent extends VObject\Component +@@ -29,7 +29,7 @@ class VEvent extends VObject\Component */ - public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool + public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { - if ($this->RRULE) { + if ($this->RRULE || $this->RDATE) { @@ -24,10 +24,10 @@ index e29e56322..6ca196d66 100644 $it = new EventIterator($this, null, $start->getTimezone()); } catch (NoInstancesException $e) { diff --git a/lib/Recur/EventIterator.php b/lib/Recur/EventIterator.php -index 2c93bcf4e..79a070ee4 100644 +index 55d6e4779..212c147d5 100644 --- a/lib/Recur/EventIterator.php +++ b/lib/Recur/EventIterator.php -@@ -164,8 +164,12 @@ public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZon +@@ -168,8 +168,12 @@ public function __construct($input, $uid = null, ?DateTimeZone $timeZone = null) } if (isset($this->masterEvent->RDATE)) { diff --git a/composer.json b/composer.json index c4bfbdb6a..25659c4bf 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "ext-mbstring": "*", "aws/aws-sdk-php": "^3.376", "bantu/ini-get-wrapper": "v1.0.1", - "cweagans/composer-patches": "^1.7", + "cweagans/composer-patches": "^2.0", "deepdiver/zipstreamer": "^3.0.1", "deepdiver1975/tarstreamer": "^2.1.0", "doctrine/dbal": "^3.10.4", @@ -87,9 +87,11 @@ "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices" }, "extra": { - "patches-file": "composer.patches.json", + "composer-patches": { + "patches-file": "composer.patches.json" + }, "aws/aws-sdk-php": [ "S3" ] } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 4f4f33e78..81d100ecb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "60ac218433a3dcf5a4a46a452bba3674", + "content-hash": "dc69a23da371a6354bf5d1a908ca66af", "packages": [ { "name": "aws/aws-crt-php", @@ -251,31 +251,100 @@ ], "time": "2023-11-29T23:19:16+00:00" }, + { + "name": "cweagans/composer-configurable-plugin", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-configurable-plugin.git", + "reference": "15433906511a108a1806710e988629fd24b89974" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-configurable-plugin/zipball/15433906511a108a1806710e988629fd24b89974", + "reference": "15433906511a108a1806710e988629fd24b89974", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "require-dev": { + "codeception/codeception": "~4.0", + "codeception/module-asserts": "^2.0", + "composer/composer": "~2.0", + "php-coveralls/php-coveralls": "~2.0", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpro/grumphp": "^1.8.0", + "sebastian/phpcpd": "^6.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a lightweight configuration system for Composer plugins.", + "support": { + "issues": "https://github.com/cweagans/composer-configurable-plugin/issues", + "source": "https://github.com/cweagans/composer-configurable-plugin/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/cweagans", + "type": "github" + } + ], + "time": "2023-02-12T04:58:58+00:00" + }, { "name": "cweagans/composer-patches", - "version": "1.7.3", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/cweagans/composer-patches.git", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + "reference": "bfa6018a5f864653d9ed899b902ea72f858a2cf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/bfa6018a5f864653d9ed899b902ea72f858a2cf7", + "reference": "bfa6018a5f864653d9ed899b902ea72f858a2cf7", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3.0" + "composer-plugin-api": "^2.0", + "cweagans/composer-configurable-plugin": "^2.0", + "ext-json": "*", + "php": ">=8.0.0" }, "require-dev": { - "composer/composer": "~1.0 || ~2.0", - "phpunit/phpunit": "~4.6" + "codeception/codeception": "~4.0", + "codeception/module-asserts": "^2.0", + "codeception/module-cli": "^2.0", + "codeception/module-filesystem": "^2.0", + "composer/composer": "~2.0", + "php-coveralls/php-coveralls": "~2.0", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpro/grumphp": "^1.8.0", + "sebastian/phpcpd": "^6.0", + "squizlabs/php_codesniffer": "^4.0" }, "type": "composer-plugin", "extra": { - "class": "cweagans\\Composer\\Patches" + "_": "The following two lines ensure that composer-patches is loaded as early as possible.", + "class": "cweagans\\Composer\\Plugin\\Patches", + "plugin-modifies-downloads": true, + "plugin-modifies-install-path": true }, "autoload": { "psr-4": { @@ -295,9 +364,15 @@ "description": "Provides a way to patch Composer packages.", "support": { "issues": "https://github.com/cweagans/composer-patches/issues", - "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + "source": "https://github.com/cweagans/composer-patches/tree/2.0.0" }, - "time": "2022-12-20T22:53:13+00:00" + "funding": [ + { + "url": "https://github.com/cweagans", + "type": "github" + } + ], + "time": "2025-10-30T23:44:22+00:00" }, { "name": "deepdiver/zipstreamer", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index e5b61b1f4..a6f4ed771 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -3974,9 +3974,47 @@ 'ZipStreamer\\Lib\\Count64_64' => $vendorDir . '/deepdiver/zipstreamer/src/Lib/Count64_64.php', 'ZipStreamer\\ZipStreamer' => $vendorDir . '/deepdiver/zipstreamer/src/ZipStreamer.php', 'bantu\\IniGetWrapper\\IniGetWrapper' => $vendorDir . '/bantu/ini-get-wrapper/src/IniGetWrapper.php', - 'cweagans\\Composer\\PatchEvent' => $vendorDir . '/cweagans/composer-patches/src/PatchEvent.php', - 'cweagans\\Composer\\PatchEvents' => $vendorDir . '/cweagans/composer-patches/src/PatchEvents.php', - 'cweagans\\Composer\\Patches' => $vendorDir . '/cweagans/composer-patches/src/Patches.php', + 'cweagans\\Composer\\Capability\\CommandProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/CommandProvider.php', + 'cweagans\\Composer\\Capability\\Downloader\\BaseDownloaderProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Downloader/BaseDownloaderProvider.php', + 'cweagans\\Composer\\Capability\\Downloader\\CoreDownloaderProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Downloader/CoreDownloaderProvider.php', + 'cweagans\\Composer\\Capability\\Downloader\\DownloaderProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Downloader/DownloaderProvider.php', + 'cweagans\\Composer\\Capability\\Patcher\\BasePatcherProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Patcher/BasePatcherProvider.php', + 'cweagans\\Composer\\Capability\\Patcher\\CorePatcherProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Patcher/CorePatcherProvider.php', + 'cweagans\\Composer\\Capability\\Patcher\\PatcherProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Patcher/PatcherProvider.php', + 'cweagans\\Composer\\Capability\\Resolver\\BaseResolverProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Resolver/BaseResolverProvider.php', + 'cweagans\\Composer\\Capability\\Resolver\\CoreResolverProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Resolver/CoreResolverProvider.php', + 'cweagans\\Composer\\Capability\\Resolver\\ResolverProvider' => $vendorDir . '/cweagans/composer-patches/src/Capability/Resolver/ResolverProvider.php', + 'cweagans\\Composer\\Command\\DoctorCommand' => $vendorDir . '/cweagans/composer-patches/src/Command/DoctorCommand.php', + 'cweagans\\Composer\\Command\\PatchesCommandBase' => $vendorDir . '/cweagans/composer-patches/src/Command/PatchesCommandBase.php', + 'cweagans\\Composer\\Command\\RelockCommand' => $vendorDir . '/cweagans/composer-patches/src/Command/RelockCommand.php', + 'cweagans\\Composer\\Command\\RepatchCommand' => $vendorDir . '/cweagans/composer-patches/src/Command/RepatchCommand.php', + 'cweagans\\Composer\\ConfigurablePlugin' => $vendorDir . '/cweagans/composer-configurable-plugin/src/ConfigurablePlugin.php', + 'cweagans\\Composer\\Downloader' => $vendorDir . '/cweagans/composer-patches/src/Downloader.php', + 'cweagans\\Composer\\Downloader\\ComposerDownloader' => $vendorDir . '/cweagans/composer-patches/src/Downloader/ComposerDownloader.php', + 'cweagans\\Composer\\Downloader\\DownloaderBase' => $vendorDir . '/cweagans/composer-patches/src/Downloader/DownloaderBase.php', + 'cweagans\\Composer\\Downloader\\DownloaderInterface' => $vendorDir . '/cweagans/composer-patches/src/Downloader/DownloaderInterface.php', + 'cweagans\\Composer\\Downloader\\Exception\\HashMismatchException' => $vendorDir . '/cweagans/composer-patches/src/Downloader/Exception/HashMismatchException.php', + 'cweagans\\Composer\\Event\\PatchEvent' => $vendorDir . '/cweagans/composer-patches/src/Event/PatchEvent.php', + 'cweagans\\Composer\\Event\\PatchEvents' => $vendorDir . '/cweagans/composer-patches/src/Event/PatchEvents.php', + 'cweagans\\Composer\\Event\\PluginEvent' => $vendorDir . '/cweagans/composer-patches/src/Event/PluginEvent.php', + 'cweagans\\Composer\\Event\\PluginEvents' => $vendorDir . '/cweagans/composer-patches/src/Event/PluginEvents.php', + 'cweagans\\Composer\\Locker' => $vendorDir . '/cweagans/composer-patches/src/Locker.php', + 'cweagans\\Composer\\Patch' => $vendorDir . '/cweagans/composer-patches/src/Patch.php', + 'cweagans\\Composer\\PatchCollection' => $vendorDir . '/cweagans/composer-patches/src/PatchCollection.php', + 'cweagans\\Composer\\Patcher' => $vendorDir . '/cweagans/composer-patches/src/Patcher.php', + 'cweagans\\Composer\\Patcher\\FreeformPatcher' => $vendorDir . '/cweagans/composer-patches/src/Patcher/FreeformPatcher.php', + 'cweagans\\Composer\\Patcher\\GitInitPatcher' => $vendorDir . '/cweagans/composer-patches/src/Patcher/GitInitPatcher.php', + 'cweagans\\Composer\\Patcher\\GitPatcher' => $vendorDir . '/cweagans/composer-patches/src/Patcher/GitPatcher.php', + 'cweagans\\Composer\\Patcher\\PatcherBase' => $vendorDir . '/cweagans/composer-patches/src/Patcher/PatcherBase.php', + 'cweagans\\Composer\\Patcher\\PatcherInterface' => $vendorDir . '/cweagans/composer-patches/src/Patcher/PatcherInterface.php', + 'cweagans\\Composer\\Plugin\\Patches' => $vendorDir . '/cweagans/composer-patches/src/Plugin/Patches.php', + 'cweagans\\Composer\\Resolver' => $vendorDir . '/cweagans/composer-patches/src/Resolver.php', + 'cweagans\\Composer\\Resolver\\Dependencies' => $vendorDir . '/cweagans/composer-patches/src/Resolver/Dependencies.php', + 'cweagans\\Composer\\Resolver\\PatchesFile' => $vendorDir . '/cweagans/composer-patches/src/Resolver/PatchesFile.php', + 'cweagans\\Composer\\Resolver\\ResolverBase' => $vendorDir . '/cweagans/composer-patches/src/Resolver/ResolverBase.php', + 'cweagans\\Composer\\Resolver\\ResolverInterface' => $vendorDir . '/cweagans/composer-patches/src/Resolver/ResolverInterface.php', + 'cweagans\\Composer\\Resolver\\RootComposer' => $vendorDir . '/cweagans/composer-patches/src/Resolver/RootComposer.php', + 'cweagans\\Composer\\Util' => $vendorDir . '/cweagans/composer-patches/src/Util.php', 'gapple\\StructuredFields\\Bytes' => $vendorDir . '/gapple/structured-fields/src/Bytes.php', 'gapple\\StructuredFields\\Date' => $vendorDir . '/gapple/structured-fields/src/Date.php', 'gapple\\StructuredFields\\Dictionary' => $vendorDir . '/gapple/structured-fields/src/Dictionary.php', diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php index fea701de0..f5dc31b93 100644 --- a/composer/autoload_psr4.php +++ b/composer/autoload_psr4.php @@ -13,7 +13,7 @@ 'kornrunner\\Blurhash\\' => array($vendorDir . '/kornrunner/blurhash/src'), 'gapple\\Tests\\StructuredFields\\' => array($vendorDir . '/gapple/structured-fields/tests'), 'gapple\\StructuredFields\\' => array($vendorDir . '/gapple/structured-fields/src'), - 'cweagans\\Composer\\' => array($vendorDir . '/cweagans/composer-patches/src'), + 'cweagans\\Composer\\' => array($vendorDir . '/cweagans/composer-patches/src', $vendorDir . '/cweagans/composer-configurable-plugin/src'), 'bantu\\IniGetWrapper\\' => array($vendorDir . '/bantu/ini-get-wrapper/src'), 'ZipStreamer\\' => array($vendorDir . '/deepdiver/zipstreamer/src'), 'Webauthn\\' => array($vendorDir . '/web-auth/webauthn-lib/src'), diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 91f87b82b..808567704 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -228,6 +228,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'cweagans\\Composer\\' => array ( 0 => __DIR__ . '/..' . '/cweagans/composer-patches/src', + 1 => __DIR__ . '/..' . '/cweagans/composer-configurable-plugin/src', ), 'bantu\\IniGetWrapper\\' => array ( @@ -4529,9 +4530,47 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'ZipStreamer\\Lib\\Count64_64' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/Lib/Count64_64.php', 'ZipStreamer\\ZipStreamer' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/ZipStreamer.php', 'bantu\\IniGetWrapper\\IniGetWrapper' => __DIR__ . '/..' . '/bantu/ini-get-wrapper/src/IniGetWrapper.php', - 'cweagans\\Composer\\PatchEvent' => __DIR__ . '/..' . '/cweagans/composer-patches/src/PatchEvent.php', - 'cweagans\\Composer\\PatchEvents' => __DIR__ . '/..' . '/cweagans/composer-patches/src/PatchEvents.php', - 'cweagans\\Composer\\Patches' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patches.php', + 'cweagans\\Composer\\Capability\\CommandProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/CommandProvider.php', + 'cweagans\\Composer\\Capability\\Downloader\\BaseDownloaderProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Downloader/BaseDownloaderProvider.php', + 'cweagans\\Composer\\Capability\\Downloader\\CoreDownloaderProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Downloader/CoreDownloaderProvider.php', + 'cweagans\\Composer\\Capability\\Downloader\\DownloaderProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Downloader/DownloaderProvider.php', + 'cweagans\\Composer\\Capability\\Patcher\\BasePatcherProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Patcher/BasePatcherProvider.php', + 'cweagans\\Composer\\Capability\\Patcher\\CorePatcherProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Patcher/CorePatcherProvider.php', + 'cweagans\\Composer\\Capability\\Patcher\\PatcherProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Patcher/PatcherProvider.php', + 'cweagans\\Composer\\Capability\\Resolver\\BaseResolverProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Resolver/BaseResolverProvider.php', + 'cweagans\\Composer\\Capability\\Resolver\\CoreResolverProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Resolver/CoreResolverProvider.php', + 'cweagans\\Composer\\Capability\\Resolver\\ResolverProvider' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Capability/Resolver/ResolverProvider.php', + 'cweagans\\Composer\\Command\\DoctorCommand' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Command/DoctorCommand.php', + 'cweagans\\Composer\\Command\\PatchesCommandBase' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Command/PatchesCommandBase.php', + 'cweagans\\Composer\\Command\\RelockCommand' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Command/RelockCommand.php', + 'cweagans\\Composer\\Command\\RepatchCommand' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Command/RepatchCommand.php', + 'cweagans\\Composer\\ConfigurablePlugin' => __DIR__ . '/..' . '/cweagans/composer-configurable-plugin/src/ConfigurablePlugin.php', + 'cweagans\\Composer\\Downloader' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Downloader.php', + 'cweagans\\Composer\\Downloader\\ComposerDownloader' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Downloader/ComposerDownloader.php', + 'cweagans\\Composer\\Downloader\\DownloaderBase' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Downloader/DownloaderBase.php', + 'cweagans\\Composer\\Downloader\\DownloaderInterface' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Downloader/DownloaderInterface.php', + 'cweagans\\Composer\\Downloader\\Exception\\HashMismatchException' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Downloader/Exception/HashMismatchException.php', + 'cweagans\\Composer\\Event\\PatchEvent' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Event/PatchEvent.php', + 'cweagans\\Composer\\Event\\PatchEvents' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Event/PatchEvents.php', + 'cweagans\\Composer\\Event\\PluginEvent' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Event/PluginEvent.php', + 'cweagans\\Composer\\Event\\PluginEvents' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Event/PluginEvents.php', + 'cweagans\\Composer\\Locker' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Locker.php', + 'cweagans\\Composer\\Patch' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patch.php', + 'cweagans\\Composer\\PatchCollection' => __DIR__ . '/..' . '/cweagans/composer-patches/src/PatchCollection.php', + 'cweagans\\Composer\\Patcher' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patcher.php', + 'cweagans\\Composer\\Patcher\\FreeformPatcher' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patcher/FreeformPatcher.php', + 'cweagans\\Composer\\Patcher\\GitInitPatcher' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patcher/GitInitPatcher.php', + 'cweagans\\Composer\\Patcher\\GitPatcher' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patcher/GitPatcher.php', + 'cweagans\\Composer\\Patcher\\PatcherBase' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patcher/PatcherBase.php', + 'cweagans\\Composer\\Patcher\\PatcherInterface' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patcher/PatcherInterface.php', + 'cweagans\\Composer\\Plugin\\Patches' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Plugin/Patches.php', + 'cweagans\\Composer\\Resolver' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Resolver.php', + 'cweagans\\Composer\\Resolver\\Dependencies' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Resolver/Dependencies.php', + 'cweagans\\Composer\\Resolver\\PatchesFile' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Resolver/PatchesFile.php', + 'cweagans\\Composer\\Resolver\\ResolverBase' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Resolver/ResolverBase.php', + 'cweagans\\Composer\\Resolver\\ResolverInterface' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Resolver/ResolverInterface.php', + 'cweagans\\Composer\\Resolver\\RootComposer' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Resolver/RootComposer.php', + 'cweagans\\Composer\\Util' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Util.php', 'gapple\\StructuredFields\\Bytes' => __DIR__ . '/..' . '/gapple/structured-fields/src/Bytes.php', 'gapple\\StructuredFields\\Date' => __DIR__ . '/..' . '/gapple/structured-fields/src/Date.php', 'gapple\\StructuredFields\\Dictionary' => __DIR__ . '/..' . '/gapple/structured-fields/src/Dictionary.php', diff --git a/composer/installed.json b/composer/installed.json index 362c1a10e..33bb97208 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -257,33 +257,105 @@ ], "install-path": "../brick/math" }, + { + "name": "cweagans/composer-configurable-plugin", + "version": "2.0.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-configurable-plugin.git", + "reference": "15433906511a108a1806710e988629fd24b89974" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-configurable-plugin/zipball/15433906511a108a1806710e988629fd24b89974", + "reference": "15433906511a108a1806710e988629fd24b89974", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "require-dev": { + "codeception/codeception": "~4.0", + "codeception/module-asserts": "^2.0", + "composer/composer": "~2.0", + "php-coveralls/php-coveralls": "~2.0", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpro/grumphp": "^1.8.0", + "sebastian/phpcpd": "^6.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "time": "2023-02-12T04:58:58+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a lightweight configuration system for Composer plugins.", + "support": { + "issues": "https://github.com/cweagans/composer-configurable-plugin/issues", + "source": "https://github.com/cweagans/composer-configurable-plugin/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/cweagans", + "type": "github" + } + ], + "install-path": "../cweagans/composer-configurable-plugin" + }, { "name": "cweagans/composer-patches", - "version": "1.7.3", - "version_normalized": "1.7.3.0", + "version": "2.0.0", + "version_normalized": "2.0.0.0", "source": { "type": "git", "url": "https://github.com/cweagans/composer-patches.git", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + "reference": "bfa6018a5f864653d9ed899b902ea72f858a2cf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/bfa6018a5f864653d9ed899b902ea72f858a2cf7", + "reference": "bfa6018a5f864653d9ed899b902ea72f858a2cf7", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3.0" + "composer-plugin-api": "^2.0", + "cweagans/composer-configurable-plugin": "^2.0", + "ext-json": "*", + "php": ">=8.0.0" }, "require-dev": { - "composer/composer": "~1.0 || ~2.0", - "phpunit/phpunit": "~4.6" + "codeception/codeception": "~4.0", + "codeception/module-asserts": "^2.0", + "codeception/module-cli": "^2.0", + "codeception/module-filesystem": "^2.0", + "composer/composer": "~2.0", + "php-coveralls/php-coveralls": "~2.0", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpro/grumphp": "^1.8.0", + "sebastian/phpcpd": "^6.0", + "squizlabs/php_codesniffer": "^4.0" }, - "time": "2022-12-20T22:53:13+00:00", + "time": "2025-10-30T23:44:22+00:00", "type": "composer-plugin", "extra": { - "class": "cweagans\\Composer\\Patches" + "_": "The following two lines ensure that composer-patches is loaded as early as possible.", + "class": "cweagans\\Composer\\Plugin\\Patches", + "plugin-modifies-downloads": true, + "plugin-modifies-install-path": true }, "installation-source": "dist", "autoload": { @@ -304,8 +376,14 @@ "description": "Provides a way to patch Composer packages.", "support": { "issues": "https://github.com/cweagans/composer-patches/issues", - "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + "source": "https://github.com/cweagans/composer-patches/tree/2.0.0" }, + "funding": [ + { + "url": "https://github.com/cweagans", + "type": "github" + } + ], "install-path": "../cweagans/composer-patches" }, { @@ -3917,11 +3995,6 @@ "bin/naturalselection" ], "type": "library", - "extra": { - "patches_applied": { - "Check for instanceof INode instead of Node": ".patches/check-for-instanceof-INode-instead-of-Node-1595.patch" - } - }, "installation-source": "dist", "autoload": { "psr-4": { @@ -4192,10 +4265,6 @@ "extra": { "branch-alias": { "dev-master": "4.0.x-dev" - }, - "patches_applied": { - "fix use RDATE in time range check and use all instances": ".patches/sabre-vobject-rdate.patch", - "fix send participation reply on fresh event": ".patches/sabre-vobject-iTipBroker-replies.patch" } }, "installation-source": "dist", @@ -4295,11 +4364,6 @@ }, "time": "2024-09-06T07:37:46+00:00", "type": "library", - "extra": { - "patches_applied": { - "Remove callable serialization": ".patches/sabre-xml-remove-callable-serialization.patch" - } - }, "installation-source": "dist", "autoload": { "files": [ @@ -6649,12 +6713,6 @@ "bin/mp3scan" ], "type": "library", - "extra": { - "patches_applied": { - "Break frame parsing on invalid frame": ".patches/mp3info-break-frame-parsing.patch", - "fix incorrect lookup for mpeg header": ".patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch" - } - }, "installation-source": "dist", "autoload": { "psr-4": { diff --git a/composer/installed.php b/composer/installed.php index 6d9d1f222..113ec59fb 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -46,10 +46,19 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'cweagans/composer-configurable-plugin' => array( + 'pretty_version' => '2.0.0', + 'version' => '2.0.0.0', + 'reference' => '15433906511a108a1806710e988629fd24b89974', + 'type' => 'library', + 'install_path' => __DIR__ . '/../cweagans/composer-configurable-plugin', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'cweagans/composer-patches' => array( - 'pretty_version' => '1.7.3', - 'version' => '1.7.3.0', - 'reference' => 'e190d4466fe2b103a55467dfa83fc2fecfcaf2db', + 'pretty_version' => '2.0.0', + 'version' => '2.0.0.0', + 'reference' => 'bfa6018a5f864653d9ed899b902ea72f858a2cf7', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../cweagans/composer-patches', 'aliases' => array(), diff --git a/cweagans/composer-patches/LICENSE.md b/cweagans/composer-configurable-plugin/LICENSE.md similarity index 97% rename from cweagans/composer-patches/LICENSE.md rename to cweagans/composer-configurable-plugin/LICENSE.md index d0dad3df7..2be383aa9 100644 --- a/cweagans/composer-patches/LICENSE.md +++ b/cweagans/composer-configurable-plugin/LICENSE.md @@ -1,4 +1,4 @@ -Copyright 2013 Cameron Eagans +Copyright 2023 Cameron Eagans Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/cweagans/composer-configurable-plugin/src/ConfigurablePlugin.php b/cweagans/composer-configurable-plugin/src/ConfigurablePlugin.php new file mode 100644 index 000000000..ea79dba46 --- /dev/null +++ b/cweagans/composer-configurable-plugin/src/ConfigurablePlugin.php @@ -0,0 +1,173 @@ +extra = $extra; + $this->pluginKey = $pluginKey; + } + + /** + * Retrieve a specified configuration value. + * + * @param string $key + * The configuration key to get a value for. + * @return string|int|bool|array + * The value of the config key. + */ + public function getConfig($key): mixed + { + // Bail out early if we don't have any information from the plugin or + // if the requested config key was not described to us. + if (is_null($this->extra) || is_null($this->pluginKey)) { + throw new \LogicException('You must call ConfigurablePlugin::configure() before attempting to retrieve a config value.'); + } + if (!array_key_exists($key, $this->configuration)) { + throw new \InvalidArgumentException('Config key ' . $key . ' was not declared in $configuration.'); + } + + // Start with the default value from configuration. + $value = $this->configuration[$key]['default']; + + // If a value is set in composer.json, override the default. + if (isset($this->extra[$this->pluginKey][$key])) { + $value = $this->extra[$this->pluginKey][$key]; + } + + // If a value is set in the environment, override anything that we've + // previously found. + if (getenv($this->getEnvvarName($key)) !== FALSE) { + $prevValue = $value; + $value = getenv($this->getEnvvarName($key)); + switch ($this->configuration[$key]['type']) { + case 'string': + // We don't need to do anything here. Envvars are all strings. + break; + case 'int': + $value = (int)$value; + break; + case 'bool': + $value = $this->castEnvvarToBool($value, $prevValue); + break; + case 'list': + $value = $this->castEnvvarToList($value, $prevValue); + break; + } + } + + return $value; + } + + /** + * Convert a config key name into an environment var name. + * + * @param $key + * The key to convert. + * @return string + * An envvar-ified version of $key + */ + public function getEnvvarName($key): string + { + $key = $this->pluginKey . '_' . $key; + $key = strtoupper($key); + $key = str_replace('-', '_', $key); + return $key; + } + + /** + * Get a boolean value from the environment. + * + * @param string $value + * The value retrieved from the environment. + * @param bool $default + * The default value to use if we can't figure out what the user wants. + * + * @return bool + */ + public function castEnvvarToBool($value, $default): bool + { + // Everything is strtolower()'d because that cuts the number of cases + // to look for in half. + $value = trim(strtolower($value)); + + // If it looks false-y, return FALSE. + if ($value == 'false' || $value == '0' || $value == 'no') { + return FALSE; + } + + // If it looks truth-y, return TRUE. + if ($value == 'true' || $value == '1' || $value == 'yes') { + return TRUE; + } + + // Otherwise, just return the default value that we were given. Ain't + // nobody got time to look for a million different ways of saying yes + // or no. + return $default; + } + + /** + * Get an array from the environment. + * + * @param string $value + * The value retrieved from the environment. + * @param array $default + * The default value to use if we can't figure out what the user wants. + * + * @return array + */ + public function castEnvvarToList($value, $default): array + { + // Trim any extra whitespace and then split the string on commas. + $value = explode(',', trim($value)); + + // Strip any empty values. + $value = array_filter($value); + + // If we didn't get anything from the supplied value, better to just use the default. + if (empty($value)) { + return $default; + } + + // Return the array. + return $value; + } +} diff --git a/cweagans/composer-patches/src/Capability/CommandProvider.php b/cweagans/composer-patches/src/Capability/CommandProvider.php new file mode 100644 index 000000000..0784839a3 --- /dev/null +++ b/cweagans/composer-patches/src/Capability/CommandProvider.php @@ -0,0 +1,22 @@ +composer = $args['composer']; + $this->io = $args['io']; + $this->plugin = $args['plugin']; + } + + /** + * @inheritDoc + */ + abstract public function getDownloaders(): array; +} diff --git a/cweagans/composer-patches/src/Capability/Downloader/CoreDownloaderProvider.php b/cweagans/composer-patches/src/Capability/Downloader/CoreDownloaderProvider.php new file mode 100644 index 000000000..76dd93889 --- /dev/null +++ b/cweagans/composer-patches/src/Capability/Downloader/CoreDownloaderProvider.php @@ -0,0 +1,18 @@ +composer, $this->io, $this->plugin), + ]; + } +} diff --git a/cweagans/composer-patches/src/Capability/Downloader/DownloaderProvider.php b/cweagans/composer-patches/src/Capability/Downloader/DownloaderProvider.php new file mode 100644 index 000000000..55bccff0a --- /dev/null +++ b/cweagans/composer-patches/src/Capability/Downloader/DownloaderProvider.php @@ -0,0 +1,22 @@ +composer = $args['composer']; + $this->io = $args['io']; + $this->plugin = $args['plugin']; + } + + /** + * @inheritDoc + */ + abstract public function getPatchers(): array; +} diff --git a/cweagans/composer-patches/src/Capability/Patcher/CorePatcherProvider.php b/cweagans/composer-patches/src/Capability/Patcher/CorePatcherProvider.php new file mode 100644 index 000000000..402a48423 --- /dev/null +++ b/cweagans/composer-patches/src/Capability/Patcher/CorePatcherProvider.php @@ -0,0 +1,22 @@ +composer, $this->io, $this->plugin), + new GitInitPatcher($this->composer, $this->io, $this->plugin), + new FreeformPatcher($this->composer, $this->io, $this->plugin) + ]; + } +} diff --git a/cweagans/composer-patches/src/Capability/Patcher/PatcherProvider.php b/cweagans/composer-patches/src/Capability/Patcher/PatcherProvider.php new file mode 100644 index 000000000..45e9176cf --- /dev/null +++ b/cweagans/composer-patches/src/Capability/Patcher/PatcherProvider.php @@ -0,0 +1,22 @@ +composer = $args['composer']; + $this->io = $args['io']; + $this->plugin = $args['plugin']; + } + + /** + * @inheritDoc + */ + abstract public function getResolvers(): array; +} diff --git a/cweagans/composer-patches/src/Capability/Resolver/CoreResolverProvider.php b/cweagans/composer-patches/src/Capability/Resolver/CoreResolverProvider.php new file mode 100644 index 000000000..3f9e58366 --- /dev/null +++ b/cweagans/composer-patches/src/Capability/Resolver/CoreResolverProvider.php @@ -0,0 +1,22 @@ +composer, $this->io, $this->plugin), + new PatchesFile($this->composer, $this->io, $this->plugin), + new Dependencies($this->composer, $this->io, $this->plugin), + ]; + } +} diff --git a/cweagans/composer-patches/src/Capability/Resolver/ResolverProvider.php b/cweagans/composer-patches/src/Capability/Resolver/ResolverProvider.php new file mode 100644 index 000000000..665a186bc --- /dev/null +++ b/cweagans/composer-patches/src/Capability/Resolver/ResolverProvider.php @@ -0,0 +1,23 @@ +setName('patches-doctor'); + $this->setDescription('Run a series of checks to ensure that Composer Patches has a usable environment.'); + $this->setAliases(['pd']); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $plugin = $this->getPatchesPluginInstance(); + if (is_null($plugin)) { + return 1; + } + $plugin->loadLockedPatches(); + + $composer = $this->requireComposer(); + $io = $this->getIO(); + $plugin_manager = $composer->getPluginManager(); + $capabilities = $plugin_manager->getPluginCapabilities( + PatcherProvider::class, + ['composer' => $composer, 'io' => $this->getIO()] + ); + + $suggestions = []; + + $io->write(""); + $io->write("System information"); + $io->write("================================================================================"); + $io->write( + str_pad("Composer version: ", 72) . "" . str_pad( + $this->getApplication()->getVersion(), + 8, + " ", + STR_PAD_LEFT + ) . "" + ); + + $system_issues = false; + + if (!str_starts_with($this->getApplication()->getVersion(), "2")) { + $system_issues = true; + } + + $io->write(str_pad("PHP version: ", 72) . "" . str_pad(PHP_VERSION, 8, " ", STR_PAD_LEFT) . ""); + if (PHP_VERSION_ID < 80000) { + $system_issues = true; + } + + if ($system_issues) { + $suggestions[] = [ + "message" => "Upgrade Composer and/or PHP to a more modern/supported version", + "link" => "https://docs.cweagans.net/composer-patches/troubleshooting/guide#upgrade-system-software" + ]; + } + + $io->write(""); + $io->write("Available patchers"); + $io->write("================================================================================"); + $has_usable_patcher = false; + foreach ($capabilities as $capability) { + /** @var PatcherProvider $capability */ + $newPatchers = $capability->getPatchers(); + foreach ($newPatchers as $i => $patcher) { + if (!$patcher instanceof PatcherInterface) { + throw new \UnexpectedValueException( + 'Plugin capability ' . get_class($capability) . ' returned an invalid value.' + ); + } + + $usable = $patcher->canUse(); + $has_usable_patcher = $has_usable_patcher || $usable; + $io->write( + str_pad(get_class($patcher) . " usable: ", 77) . + ($usable ? "yes" : " no") + ); + } + } + + $io->write( + str_pad( + "Has usable patchers: ", + 77 + ) . ($has_usable_patcher ? "yes" : " no") + ); + + if (!$has_usable_patcher) { + $suggestions[] = [ + "message" => "Install software dependencies for applying patches", + "link" => "https://docs.cweagans.net/composer-patches/troubleshooting/guide#install-patching-software" + ]; + } + + + $io->write(""); + $io->write("Common configuration issues"); + $io->write("================================================================================"); + $has_http_urls = false; + foreach ($plugin->getPatchCollection()->getPatchedPackages() as $package) { + foreach ($plugin->getPatchCollection()->getPatchesForPackage($package) as $patch) { + if (str_starts_with($patch->url, 'http://')) { + $has_http_urls = true; + break 2; + } + } + } + + $io->write( + str_pad("has plain http patch URLs:", 77) . ($has_http_urls ? "yes" : " no") + ); + if ($has_http_urls) { + $sh = $composer->getConfig()->get('secure-http'); + $io->write( + str_pad('secure-http disabled:', 77) . ($sh ? " no" : "yes") + ); + + if ($sh) { + $suggestions[] = [ + "message" => "Patches must either be downloaded securely or 'secure-http' must be disabled", + "link" => + "https://docs.cweagans.net/composer-patches/troubleshooting/guide#download-patches-securely" + ]; + } + } + + if (!empty($suggestions)) { + $io->write(""); + $io->write("Suggestions"); + $io->write("================================================================================"); + foreach ($suggestions as $suggestion) { + $io->write(" - " . $suggestion['message']); + $io->write(" More information: " . $suggestion['link']); + } + } + + + return 0; + } +} diff --git a/cweagans/composer-patches/src/Command/PatchesCommandBase.php b/cweagans/composer-patches/src/Command/PatchesCommandBase.php new file mode 100644 index 000000000..20442b617 --- /dev/null +++ b/cweagans/composer-patches/src/Command/PatchesCommandBase.php @@ -0,0 +1,27 @@ +requireComposer()->getPluginManager()->getPlugins() as $plugin) { + if ($plugin instanceof Patches) { + return $plugin; + } + } + + return null; + } +} diff --git a/cweagans/composer-patches/src/Command/RelockCommand.php b/cweagans/composer-patches/src/Command/RelockCommand.php new file mode 100644 index 000000000..52d9553e6 --- /dev/null +++ b/cweagans/composer-patches/src/Command/RelockCommand.php @@ -0,0 +1,35 @@ +setName('patches-relock'); + $filename = pathinfo(Patches::getPatchesLockFilePath(), \PATHINFO_BASENAME); + $this->setDescription("Find all patches defined in the project and re-write $filename."); + $this->setAliases(['prl']); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $plugin = $this->getPatchesPluginInstance(); + if (is_null($plugin)) { + return 1; + } + + if (file_exists($plugin->getLockFile()->getPath())) { + unlink($plugin->getLockFile()->getPath()); + } + $plugin->createNewPatchesLock(); + $output->write(' - patches.lock.json has been recreated successfully.', true); + return 0; + } +} diff --git a/cweagans/composer-patches/src/Command/RepatchCommand.php b/cweagans/composer-patches/src/Command/RepatchCommand.php new file mode 100644 index 000000000..db0399f0e --- /dev/null +++ b/cweagans/composer-patches/src/Command/RepatchCommand.php @@ -0,0 +1,64 @@ +setName('patches-repatch'); + $this->setDescription('Delete, re-download, and re-patch each dependency with any patches defined.'); + $this->setAliases(['prp']); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $plugin = $this->getPatchesPluginInstance(); + if (is_null($plugin)) { + return 1; + } + + $plugin->loadLockedPatches(); + $patchCollection = $plugin->getPatchCollection(); + if (is_null($patchCollection)) { + return 1; + } + + $localRepository = $this->requireComposer() + ->getRepositoryManager() + ->getLocalRepository(); + + $patched_packages = $patchCollection->getPatchedPackages(); + $packages = array_filter($localRepository->getPackages(), function ($val) use ($patched_packages) { + return in_array($val->getName(), $patched_packages); + }); + + // Remove patched packages so that we can re-install/re-patch. + $promises = []; + foreach ($packages as $package) { + $uninstallOperation = new UninstallOperation($package); + $promises[] = $this->requireComposer() + ->getInstallationManager() + ->uninstall($localRepository, $uninstallOperation); + } + // Wait for uninstalls to finish. + $promises = array_filter($promises); + if (!empty($promises)) { + $this->requireComposer()->getLoop()->wait($promises); + } + + $input = new ArrayInput(['command' => 'install']); + $application = $this->getApplication(); + $application->setAutoExit(false); + $application->run($input, $output); + + return 0; + } +} diff --git a/cweagans/composer-patches/src/Downloader.php b/cweagans/composer-patches/src/Downloader.php new file mode 100644 index 000000000..fd43803c1 --- /dev/null +++ b/cweagans/composer-patches/src/Downloader.php @@ -0,0 +1,125 @@ +composer = $composer; + $this->io = $io; + $this->disabledDownloaders = $disabledDownloaders; + + // If --no-cache is passed to Composer, we need a different location to + // download patches to. When --no-cache is passed, $composer_cache is + // set to /dev/null. + $composer_cache = $composer->getConfig()->get('cache-dir'); + if (!is_dir($composer_cache)) { + $composer_cache = sys_get_temp_dir(); + } + + // If the cache directory doesn't exist, create it. + $this->cacheDir = $composer_cache . '/patches'; + if (!is_dir($this->cacheDir)) { + mkdir($this->cacheDir); + } + } + + /** + * Download a patch using the available downloaders. + * + * A downloader will update Patch->localPath if it was able to download the patch. + * + * @param Patch $patch + * The patch to download. + */ + public function downloadPatch(Patch $patch) + { + if (isset($patch->sha256)) { + $cachedPatch = $this->cacheDir . '/' . $patch->sha256 . '.patch'; + if (file_exists($cachedPatch)) { + $this->io->write(" - Found cached patch at {$cachedPatch}", IOInterface::VERBOSE); + $patch->localPath = $cachedPatch; + return; + } + } + + foreach ($this->getDownloaders() as $downloader) { + $class = "\\" . get_class($downloader); + if (in_array($class, $this->disabledDownloaders, true)) { + $this->io->write( + ' - Skipping downloader ' . $class . '', + true, + IOInterface::VERBOSE + ); + continue; + } + + $downloader->download($patch); + + if (isset($patch->localPath)) { + $cachedPatch = $this->cacheDir . '/' . $patch->sha256 . '.patch'; + if (rename($patch->localPath, $cachedPatch)) { + $patch->localPath = $cachedPatch; + } + return; + } + } + } + + /** + * Gather a list of all patch downloaders from all enabled Composer plugins. + * + * @return DownloaderInterface[] + * A list of Downloaders that are available. + */ + protected function getDownloaders(): array + { + static $downloaders; + if (!is_null($downloaders)) { + return $downloaders; + } + + $downloaders = []; + $plugin_manager = $this->composer->getPluginManager(); + $capabilities = $plugin_manager->getPluginCapabilities( + DownloaderProvider::class, + ['composer' => $this->composer, 'io' => $this->io] + ); + foreach ($capabilities as $capability) { + /** @var DownloaderProvider $capability */ + $newDownloaders = $capability->getDownloaders(); + foreach ($newDownloaders as $downloader) { + if (!$downloader instanceof DownloaderInterface) { + throw new UnexpectedValueException( + 'Plugin capability ' . get_class($capability) . ' returned an invalid value.' + ); + } + } + $downloaders = array_merge($downloaders, $newDownloaders); + } + + + $event = new PluginEvent(PluginEvents::POST_DISCOVER_DOWNLOADERS, $downloaders, $this->composer, $this->io); + $this->composer->getEventDispatcher()->dispatch(PluginEvents::POST_DISCOVER_DOWNLOADERS, $event); + $downloaders = $event->getCapabilities(); + + return $downloaders; + } +} diff --git a/cweagans/composer-patches/src/Downloader/ComposerDownloader.php b/cweagans/composer-patches/src/Downloader/ComposerDownloader.php new file mode 100644 index 000000000..cac95268d --- /dev/null +++ b/cweagans/composer-patches/src/Downloader/ComposerDownloader.php @@ -0,0 +1,43 @@ +io, $this->composer->getConfig()); + } + + // Don't need to re-download a patch if it has already been downloaded. + if (isset($patch->localPath) && !empty($patch->localPath)) { + return; + } + + $patches_dir = sys_get_temp_dir() . '/composer-patches/'; + $filename = uniqid($patches_dir) . ".patch"; + if (!is_dir($patches_dir)) { + mkdir($patches_dir); + } + + $downloader->copy($patch->url, $filename); + $patch->localPath = $filename; + + $hash = hash_file('sha256', $filename); + + if (!isset($patch->sha256)) { + $patch->sha256 = $hash; + } elseif ($hash !== $patch->sha256) { + throw new HashMismatchException($patch->url, $hash, $patch->sha256); + } + } +} diff --git a/cweagans/composer-patches/src/Downloader/DownloaderBase.php b/cweagans/composer-patches/src/Downloader/DownloaderBase.php new file mode 100644 index 000000000..cbaf63e76 --- /dev/null +++ b/cweagans/composer-patches/src/Downloader/DownloaderBase.php @@ -0,0 +1,49 @@ +composer = $composer; + $this->io = $io; + $this->plugin = $plugin; + } + + /** + * @inheritDoc + * @throws HashMismatchException + */ + abstract public function download(Patch $patch): void; +} diff --git a/cweagans/composer-patches/src/Downloader/DownloaderInterface.php b/cweagans/composer-patches/src/Downloader/DownloaderInterface.php new file mode 100644 index 000000000..0947ad4cd --- /dev/null +++ b/cweagans/composer-patches/src/Downloader/DownloaderInterface.php @@ -0,0 +1,39 @@ +localPath to wherever the patch was downloaded to. + * + * @param Patch $patch + * The patch to apply. + */ + public function download(Patch $patch): void; +} diff --git a/cweagans/composer-patches/src/Downloader/Exception/HashMismatchException.php b/cweagans/composer-patches/src/Downloader/Exception/HashMismatchException.php new file mode 100644 index 000000000..00a65065d --- /dev/null +++ b/cweagans/composer-patches/src/Downloader/Exception/HashMismatchException.php @@ -0,0 +1,13 @@ +patch = $patch; + $this->composer = $composer; + $this->io = $io; + $this->error = $error; + } + + /** + * Returns the Patch object. + * + * @return Patch + */ + public function getPatch(): Patch + { + return $this->patch; + } + + /** + * Returns the Composer object. + * + * @return Composer + */ + public function getComposer(): Composer + { + return $this->composer; + } + + /** + * Returns the IOInterface. + * + * @return IOInterface + */ + public function getIO(): IOInterface + { + return $this->io; + } + + /** + * Returns the exception about to be thrown when a patch cannot be applied. + * + * @return Exception + */ + public function getError(): ?Exception + { + return $this->error; + } +} diff --git a/cweagans/composer-patches/src/Event/PatchEvents.php b/cweagans/composer-patches/src/Event/PatchEvents.php new file mode 100644 index 000000000..20b34e53b --- /dev/null +++ b/cweagans/composer-patches/src/Event/PatchEvents.php @@ -0,0 +1,66 @@ +capabilities = $capabilities; + $this->composer = $composer; + $this->io = $io; + } + + /** + * Get the list of capabilities that were discovered. + * + * @return array + */ + public function getCapabilities(): array + { + return $this->capabilities; + } + + /** + * Replace the list of capabilities that were discovered. + * + * You should take care to only include the correct type of capability classes here. e.g. If you're responding to + * the POST_DISCOVER_DOWNLOADERS event, you should only include implementations of DownloaderInterface. + * + * @param array $capabilities + * A complete list of capability objects. + */ + public function setCapabilities(array $capabilities): void + { + $this->capabilities = $capabilities; + } + + /** + * Returns the Composer object. + * + * @return Composer + */ + public function getComposer(): Composer + { + return $this->composer; + } + + /** + * Returns the IOInterface. + * + * @return IOInterface + */ + public function getIO(): IOInterface + { + return $this->io; + } +} diff --git a/cweagans/composer-patches/src/Event/PluginEvents.php b/cweagans/composer-patches/src/Event/PluginEvents.php new file mode 100644 index 000000000..296de7269 --- /dev/null +++ b/cweagans/composer-patches/src/Event/PluginEvents.php @@ -0,0 +1,38 @@ +lockFile = $lockFile; + } + + /** + * Returns the sha256 hash of the sorted content of the patches.lock.json file. + * + * @param PatchCollection $patchCollection + * The resolved patches for the project. + */ + public static function getCollectionHash(PatchCollection $patchCollection): string + { + $file = $patchCollection->jsonSerialize(); + ksort($file); + return hash('sha256', JsonFile::encode($file, 0)); + } + + public function isLocked(): bool + { + if (!$this->virtualFileWritten && !$this->lockFile->exists()) { + return false; + } + + try { + $data = $this->getLockData(); + return isset($data['patches']); + } catch (LogicException $e) { + return false; + } + } + + public function isFresh(PatchCollection $patchCollection): bool + { + try { + $lock = $this->getLockData(); + } catch (LogicException $e) { + return false; + } + + if (empty($lock['patches']) || empty($lock['_hash'])) { + return false; + } + + $expectedHash = self::getCollectionHash($patchCollection); + + return $expectedHash === $lock['_hash']; + } + + public function getLockData(): array + { + if (!is_null($this->lockDataCache)) { + return $this->lockDataCache; + } + + if (!$this->lockFile->exists()) { + throw new LogicException('No lockfile found. Unable to read locked patches.'); + } + + return $this->lockDataCache = $this->lockFile->read(); + } + + public function setLockData(PatchCollection $patchCollection, bool $write = true): bool + { + $lock = $patchCollection->jsonSerialize(); + $lock['_hash'] = self::getCollectionHash($patchCollection); + ksort($lock); + + try { + $isLocked = $this->isLocked(); + } catch (ParsingException $e) { + $isLocked = false; + } + if (!$isLocked || $lock !== $this->getLockData()) { + if ($write) { + $this->lockFile->write($lock); + $this->lockDataCache = null; + $this->virtualFileWritten = false; + } else { + $this->virtualFileWritten = true; + $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock)); + } + + return true; + } + + return false; + } +} diff --git a/cweagans/composer-patches/src/Patch.php b/cweagans/composer-patches/src/Patch.php new file mode 100644 index 000000000..7beb82f12 --- /dev/null +++ b/cweagans/composer-patches/src/Patch.php @@ -0,0 +1,101 @@ +{$property} = $json[$property]; + } + } + + return $patch; + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): mixed + { + return [ + 'package' => $this->package, + 'description' => $this->description, + 'url' => $this->url, + 'sha256' => $this->sha256 ?? null, + 'depth' => $this->depth ?? null, + 'extra' => $this->extra ?? [], + ]; + } +} diff --git a/cweagans/composer-patches/src/PatchCollection.php b/cweagans/composer-patches/src/PatchCollection.php new file mode 100644 index 000000000..c4c5fcef4 --- /dev/null +++ b/cweagans/composer-patches/src/PatchCollection.php @@ -0,0 +1,121 @@ +package, $this->patches)) { + foreach ($this->patches[$patch->package] as $existing_patch) { + $has_same_url = ($patch->url === $existing_patch->url); + $has_same_sha = ( + isset($patch->sha256) && + isset($existing_patch->sha256) && + ($patch->sha256 === $existing_patch->sha256) + ); + + if ($has_same_url || $has_same_sha) { + // If we already have the patch, don't try to add it again. + return; + } + } + } + + $this->patches[$patch->package][] = $patch; + } + + /** + * Retrieve a list of patches for a given package. + * + * @param string $package + * The package name to get patches for. + * + * @return array + * An array of Patch objects. + */ + public function getPatchesForPackage(string $package): array + { + if (isset($this->patches[$package])) { + return $this->patches[$package]; + } + + return []; + } + + /** + * Get a list of packages that have patches applied. + * + * @return array + */ + public function getPatchedPackages(): array + { + return array_keys($this->patches); + } + + /** + * Delete all patches for a package. + * + * @param string $package + * The package name to clear patches from. + */ + public function clearPatchesForPackage(string $package): void + { + if (isset($this->patches[$package])) { + unset($this->patches[$package]); + } + } + + /** + * Create a PatchCollection from a serialized representation. + * + * @param $json + * A JSON representation of a PatchCollection (or an array from JsonFile). + * + * @return PatchCollection + * A PatchCollection with all of the serialized patches included. + */ + public static function fromJson($json): static + { + if (!is_array($json)) { + $json = json_decode($json, true); + } + $collection = new static(); + + foreach ($json['patches'] as $package => $patches) { + foreach ($patches as $patch_json) { + $patch = Patch::fromJson($patch_json); + $collection->addPatch($patch); + } + } + + return $collection; + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): mixed + { + return [ + 'patches' => $this->patches, + ]; + } +} diff --git a/cweagans/composer-patches/src/PatchEvent.php b/cweagans/composer-patches/src/PatchEvent.php deleted file mode 100644 index 31d36f89f..000000000 --- a/cweagans/composer-patches/src/PatchEvent.php +++ /dev/null @@ -1,70 +0,0 @@ -package = $package; - $this->url = $url; - $this->description = $description; - } - - /** - * Returns the package that is patched. - * - * @return PackageInterface - */ - public function getPackage() { - return $this->package; - } - - /** - * Returns the url of the patch. - * - * @return string - */ - public function getUrl() { - return $this->url; - } - - /** - * Returns the description of the patch. - * - * @return string - */ - public function getDescription() { - return $this->description; - } - -} diff --git a/cweagans/composer-patches/src/PatchEvents.php b/cweagans/composer-patches/src/PatchEvents.php deleted file mode 100644 index ecee94768..000000000 --- a/cweagans/composer-patches/src/PatchEvents.php +++ /dev/null @@ -1,30 +0,0 @@ -composer = $composer; + $this->io = $io; + $this->disabledPatchers = $disabledPatchers; + } + + /** + * Apply a patch using the available Patchers. + * + * @param Patch $patch + * The patch to apply. + * + * @param string $path + * The path to where the package was installed by Composer. + * + * @return bool + * true if the patch was applied successfully. + */ + public function applyPatch(Patch $patch, string $path): bool + { + foreach ($this->getPatchers() as $patcher) { + $class = "\\" . get_class($patcher); + if (in_array($class, $this->disabledPatchers, true)) { + $this->io->write( + ' - Skipping patcher ' . $class . '', + true, + IOInterface::VERBOSE + ); + continue; + } + + $result = $patcher->apply($patch, $path); + + if ($result === true) { + return true; + } + } + + return false; + } + + /** + * Gather a list of all Patchers from all enabled Composer plugins. + * + * @return PatcherInterface[] + * A list of Patchers that are available. + */ + protected function getPatchers(): array + { + static $patchers; + + if (!is_null($patchers)) { + return $patchers; + } + + $patchers = []; + $plugin_manager = $this->composer->getPluginManager(); + $capabilities = $plugin_manager->getPluginCapabilities( + PatcherProvider::class, + ['composer' => $this->composer, 'io' => $this->io] + ); + foreach ($capabilities as $capability) { + /** @var PatcherProvider $capability */ + $newPatchers = $capability->getPatchers(); + foreach ($newPatchers as $i => $patcher) { + if (!$patcher instanceof PatcherInterface) { + throw new UnexpectedValueException( + 'Plugin capability ' . get_class($capability) . ' returned an invalid value.' + ); + } + + $usable = $patcher->canUse(); + $this->io->write( + get_class($patcher) . " usable: " . ($usable ? "yes" : "no"), + true, + IOInterface::DEBUG + ); + + if (!$usable) { + unset($newPatchers[$i]); + } + } + + $patchers = array_merge($patchers, $newPatchers); + } + + $event = new PluginEvent(PluginEvents::POST_DISCOVER_PATCHERS, $patchers, $this->composer, $this->io); + $this->composer->getEventDispatcher()->dispatch(PluginEvents::POST_DISCOVER_PATCHERS, $event); + $patchers = $event->getCapabilities(); + + if (count($patchers) === 0) { + throw new Exception('No patchers available.'); + } + + return $patchers; + } +} diff --git a/cweagans/composer-patches/src/Patcher/FreeformPatcher.php b/cweagans/composer-patches/src/Patcher/FreeformPatcher.php new file mode 100644 index 000000000..113f90434 --- /dev/null +++ b/cweagans/composer-patches/src/Patcher/FreeformPatcher.php @@ -0,0 +1,59 @@ +extra['freeform']['executable'] ?? ''; + $args = $patch->extra['freeform']['args'] ?? ''; + + // Optional. + $dryRunArgs = $patch->extra['freeform']['dry_run_args'] ?? ''; + + // If we don't have what we need, exit. + if (empty($patchTool) || empty($args)) { + $this->io->write( + 'Required configuration for FreeformPatcher not present. Skipping.', + true, + IOInterface::VERBOSE + ); + return false; + } + + // If we have dry-run args, do a dry-run. + if (!empty($dryRunArgs)) { + $status = $this->executeCommand( + '%s ' . $dryRunArgs, + $patchTool, + $patch->depth, + $path, + $patch->localPath, + ); + if (!$status) { + return false; + } + } + + // Apply the patch. + return $this->executeCommand( + '%s ' . $args, + $patchTool, + $patch->depth, + $path, + $patch->localPath, + ); + } + + public function canUse(): bool + { + // Hardcoded to true because apply() will bail out if the freeform args are not set on the patch (or globally). + // Users who opt-in to this patcher are responsible for providing a valid executable and arguments. + return true; + } +} diff --git a/cweagans/composer-patches/src/Patcher/GitInitPatcher.php b/cweagans/composer-patches/src/Patcher/GitInitPatcher.php new file mode 100644 index 000000000..e87a4f6c1 --- /dev/null +++ b/cweagans/composer-patches/src/Patcher/GitInitPatcher.php @@ -0,0 +1,40 @@ +io->write("Creating temporary git repo in $path to apply patch", true, IOInterface::VERBOSE); + + // Create a temporary git repository. + $status = $this->executeCommand( + '%s -C %s init', + $this->patchTool(), + $path + ); + + // If we couldn't create the Git repo, bail out. + if (!$status) { + return false; + } + + // Use the git patcher to apply the patch. + $status = parent::apply($patch, $path); + + // Clean up the git repo. + (new Filesystem($this->executor))->removeDirectory($path . '/.git'); + + return $status; + } +} diff --git a/cweagans/composer-patches/src/Patcher/GitPatcher.php b/cweagans/composer-patches/src/Patcher/GitPatcher.php new file mode 100644 index 000000000..7255ed6b5 --- /dev/null +++ b/cweagans/composer-patches/src/Patcher/GitPatcher.php @@ -0,0 +1,57 @@ +io->write("$path is not a git repo. Skipping Git patcher.", true, IOInterface::VERBOSE); + return false; + } + + // Dry run first. + $status = $this->executeCommand( + '%s -C %s apply --check --verbose -p%s %s', + $this->patchTool(), + $path, + $patch->depth, + $patch->localPath + ); + if (str_starts_with($this->executor->getErrorOutput(), 'Skipped')) { + // Git will indicate success but silently skip patches in some scenarios. + // + // @see https://github.com/cweagans/composer-patches/pull/165 + $status = false; + } + + // If the check failed, then don't proceed with actually applying the patch. + if (!$status) { + return false; + } + + // Otherwise, we can try to apply the patch. + return $this->executeCommand( + '%s -C %s apply -p%s --verbose %s', + $this->patchTool(), + $path, + $patch->depth, + $patch->localPath + ); + } + + public function canUse(): bool + { + $output = ""; + $result = $this->executor->execute($this->patchTool() . ' --version', $output); + return ($result === 0); + } +} diff --git a/cweagans/composer-patches/src/Patcher/PatcherBase.php b/cweagans/composer-patches/src/Patcher/PatcherBase.php new file mode 100644 index 000000000..a8e062038 --- /dev/null +++ b/cweagans/composer-patches/src/Patcher/PatcherBase.php @@ -0,0 +1,123 @@ +composer = $composer; + $this->io = $io; + $this->plugin = $plugin; + $this->executor = new ProcessExecutor($io); + } + + /** + * Return the tool to run when applying patches (when applicable). + * + * @return string + */ + protected function patchTool(): string + { + if (isset($this->toolPathOverride) && !empty($this->toolPathOverride)) { + return $this->toolPathOverride; + } + + return $this->tool; + } + + /** + * Executes a shell command with escaping. + * + * @param string $cmd + * @return bool + */ + protected function executeCommand($cmd) + { + // Shell-escape all arguments except the command. + $args = func_get_args(); + foreach ($args as $index => $arg) { + if ($index !== 0 && !is_int($arg)) { + $args[$index] = escapeshellarg($arg); + } + } + + // And replace the arguments. + $command = call_user_func_array('sprintf', $args); + $output = ''; + if ($this->io->isVerbose()) { + $this->io->write('' . $command . ''); + $io = $this->io; + $output = function ($type, $data) use ($io) { + if ($type === Process::ERR) { + $io->write('' . $data . ''); + } else { + $io->write('' . $data . ''); + } + }; + } + return ($this->executor->execute($command, $output) === 0); + } + + /** + * @inheritDoc + */ + abstract public function apply(Patch $patch, string $path): bool; + + /** + * @inheritDoc + */ + abstract public function canUse(): bool; +} diff --git a/cweagans/composer-patches/src/Patcher/PatcherInterface.php b/cweagans/composer-patches/src/Patcher/PatcherInterface.php new file mode 100644 index 000000000..8e9aeff02 --- /dev/null +++ b/cweagans/composer-patches/src/Patcher/PatcherInterface.php @@ -0,0 +1,43 @@ +composer = $composer; - $this->io = $io; - $this->eventDispatcher = $composer->getEventDispatcher(); - $this->executor = new ProcessExecutor($this->io); - $this->patches = array(); - $this->installedPatches = array(); - } - - /** - * Returns an array of event names this subscriber wants to listen to. - */ - public static function getSubscribedEvents() { - return array( - ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'), - ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'), - PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'), - PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'), - // The following is a higher weight for compatibility with - // https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with - // every Composer plugin which deploys downloaded packages to other locations. - // In such cases you want that those plugins deploy patched files so they have to run after - // the "composer-patches" plugin. - // @see: https://github.com/cweagans/composer-patches/pull/153 - PackageEvents::POST_PACKAGE_INSTALL => array('postInstall', 10), - PackageEvents::POST_PACKAGE_UPDATE => array('postInstall', 10), - ); - } - - /** - * Before running composer install, - * @param Event $event - */ - public function checkPatches(Event $event) { - if (!$this->isPatchingEnabled()) { - return; - } - - try { - $repositoryManager = $this->composer->getRepositoryManager(); - $localRepository = $repositoryManager->getLocalRepository(); - $installationManager = $this->composer->getInstallationManager(); - $packages = $localRepository->getPackages(); - - $extra = $this->composer->getPackage()->getExtra(); - $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); - - $tmp_patches = $this->grabPatches(); - foreach ($packages as $package) { - $extra = $package->getExtra(); - if (isset($extra['patches'])) { - if (isset($patches_ignore[$package->getName()])) { - foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { - if (isset($extra['patches'][$package_name])) { - $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches); - } - } - } - $this->installedPatches[$package->getName()] = $extra['patches']; - } - $patches = isset($extra['patches']) ? $extra['patches'] : array(); - $tmp_patches = $this->arrayMergeRecursiveDistinct($tmp_patches, $patches); - } - - if ($tmp_patches == FALSE) { - $this->io->write('No patches supplied.'); - return; - } - - // Remove packages for which the patch set has changed. - $promises = array(); - foreach ($packages as $package) { - if (!($package instanceof AliasPackage)) { - $package_name = $package->getName(); - $extra = $package->getExtra(); - $has_patches = isset($tmp_patches[$package_name]); - $has_applied_patches = isset($extra['patches_applied']) && count($extra['patches_applied']) > 0; - if (($has_patches && !$has_applied_patches) - || (!$has_patches && $has_applied_patches) - || ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) { - $uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.'); - $this->io->write('Removing package ' . $package_name . ' so that it can be re-installed and re-patched.'); - $promises[] = $installationManager->uninstall($localRepository, $uninstallOperation); - } - } - } - $promises = array_filter($promises); - if ($promises) { - $this->composer->getLoop()->wait($promises); - } - } - // If the Locker isn't available, then we don't need to do this. - // It's the first time packages have been installed. - catch (\LogicException $e) { - return; - } - } - - /** - * Gather patches from dependencies and store them for later use. - * - * @param PackageEvent $event - */ - public function gatherPatches(PackageEvent $event) { - // If we've already done this, then don't do it again. - if (isset($this->patches['_patchesGathered'])) { - $this->io->write('Patches already gathered. Skipping', TRUE, IOInterface::VERBOSE); - return; - } - // If patching has been disabled, bail out here. - elseif (!$this->isPatchingEnabled()) { - $this->io->write('Patching is disabled. Skipping.', TRUE, IOInterface::VERBOSE); - return; - } - - $this->patches = $this->grabPatches(); - if (empty($this->patches)) { - $this->io->write('No patches supplied.'); - } - - $extra = $this->composer->getPackage()->getExtra(); - $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); - - // Now add all the patches from dependencies that will be installed. - $operations = $event->getOperations(); - $this->io->write('Gathering patches for dependencies. This might take a minute.'); - foreach ($operations as $operation) { - if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) { - $package = $this->getPackageFromOperation($operation); - $extra = $package->getExtra(); - if (isset($extra['patches'])) { - if (isset($patches_ignore[$package->getName()])) { - foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { - if (isset($extra['patches'][$package_name])) { - $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches); - } - } - } - $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']); - } - // Unset installed patches for this package - if(isset($this->installedPatches[$package->getName()])) { - unset($this->installedPatches[$package->getName()]); - } - } - } - - // Merge installed patches from dependencies that did not receive an update. - foreach ($this->installedPatches as $patches) { - $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $patches); - } - - // If we're in verbose mode, list the projects we're going to patch. - if ($this->io->isVerbose()) { - foreach ($this->patches as $package => $patches) { - $number = count($patches); - $this->io->write('Found ' . $number . ' patches for ' . $package . '.'); - } - } - - // Make sure we don't gather patches again. Extra keys in $this->patches - // won't hurt anything, so we'll just stash it there. - $this->patches['_patchesGathered'] = TRUE; - } - - /** - * Get the patches from root composer or external file - * @return Patches - * @throws \Exception - */ - public function grabPatches() { - // First, try to get the patches from the root composer.json. - $extra = $this->composer->getPackage()->getExtra(); - if (isset($extra['patches'])) { - $this->io->write('Gathering patches for root package.'); - $patches = $extra['patches']; - return $patches; - } - // If it's not specified there, look for a patches-file definition. - elseif (isset($extra['patches-file'])) { - $this->io->write('Gathering patches from patch file.'); - $patches = file_get_contents($extra['patches-file']); - $patches = json_decode($patches, TRUE); - $error = json_last_error(); - if ($error != 0) { - switch ($error) { - case JSON_ERROR_DEPTH: - $msg = ' - Maximum stack depth exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $msg = ' - Underflow or the modes mismatch'; - break; - case JSON_ERROR_CTRL_CHAR: - $msg = ' - Unexpected control character found'; - break; - case JSON_ERROR_SYNTAX: - $msg = ' - Syntax error, malformed JSON'; - break; - case JSON_ERROR_UTF8: - $msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - default: - $msg = ' - Unknown error'; - break; - } - throw new \Exception('There was an error in the supplied patches file:' . $msg); - } - if (isset($patches['patches'])) { - $patches = $patches['patches']; - return $patches; - } - elseif(!$patches) { - throw new \Exception('There was an error in the supplied patch file'); - } - } - else { - return array(); - } - } - - /** - * @param PackageEvent $event - * @throws \Exception - */ - public function postInstall(PackageEvent $event) { - - // Check if we should exit in failure. - $extra = $this->composer->getPackage()->getExtra(); - $exitOnFailure = getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure']); - $skipReporting = getenv('COMPOSER_PATCHES_SKIP_REPORTING') || !empty($extra['composer-patches-skip-reporting']); - - // Get the package object for the current operation. - $operation = $event->getOperation(); - /** @var PackageInterface $package */ - $package = $this->getPackageFromOperation($operation); - $package_name = $package->getName(); - - if (!isset($this->patches[$package_name])) { - if ($this->io->isVerbose()) { - $this->io->write('No patches found for ' . $package_name . '.'); - } - return; - } - $this->io->write(' - Applying patches for ' . $package_name . ''); - - // Get the install path from the package object. - $manager = $event->getComposer()->getInstallationManager(); - $install_path = $manager->getInstaller($package->getType())->getInstallPath($package); - - // Set up a downloader. - $downloader = new RemoteFilesystem($this->io, $this->composer->getConfig()); - - // Track applied patches in the package info in installed.json - $localRepository = $this->composer->getRepositoryManager()->getLocalRepository(); - $localPackage = $localRepository->findPackage($package_name, $package->getVersion()); - $extra = $localPackage->getExtra(); - $extra['patches_applied'] = array(); - - foreach ($this->patches[$package_name] as $description => $url) { - $this->io->write(' ' . $url . ' (' . $description. ')'); - try { - $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description)); - $this->getAndApplyPatch($downloader, $install_path, $url, $package); - $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description)); - $extra['patches_applied'][$description] = $url; - } - catch (\Exception $e) { - $this->io->write(' Could not apply patch! Skipping. The error was: ' . $e->getMessage() . ''); - if ($exitOnFailure) { - throw new \Exception("Cannot apply patch $description ($url)!"); - } - } - } - $localPackage->setExtra($extra); - - $this->io->write(''); - - if (true !== $skipReporting) { - $this->writePatchReport($this->patches[$package_name], $install_path); - } - } - - /** - * Get a Package object from an OperationInterface object. - * - * @param OperationInterface $operation - * @return PackageInterface - * @throws \Exception - */ - protected function getPackageFromOperation(OperationInterface $operation) { - if ($operation instanceof InstallOperation) { - $package = $operation->getPackage(); - } - elseif ($operation instanceof UpdateOperation) { - $package = $operation->getTargetPackage(); - } - else { - throw new \Exception('Unknown operation: ' . get_class($operation)); - } - - return $package; - } - - /** - * Apply a patch on code in the specified directory. - * - * @param RemoteFilesystem $downloader - * @param $install_path - * @param $patch_url - * @param PackageInterface $package - * @throws \Exception - */ - protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url, PackageInterface $package) { - - // Local patch file. - if (file_exists($patch_url)) { - $filename = realpath($patch_url); - } - else { - // Generate random (but not cryptographically so) filename. - $filename = uniqid(sys_get_temp_dir().'/') . ".patch"; - - // Download file from remote filesystem to this location. - $hostname = parse_url($patch_url, PHP_URL_HOST); - - try { - $downloader->copy($hostname, $patch_url, $filename, false); - } catch (\Exception $e) { - // In case of an exception, retry once as the download might - // have failed due to intermittent network issues. - $downloader->copy($hostname, $patch_url, $filename, false); - } - } - - // The order here is intentional. p1 is most likely to apply with git apply. - // p0 is next likely. p2 is extremely unlikely, but for some special cases, - // it might be useful. p4 is useful for Magento 2 patches - $patch_levels = array('-p1', '-p0', '-p2', '-p4'); - - // Check for specified patch level for this package. - $extra = $this->composer->getPackage()->getExtra(); - if (!empty($extra['patchLevel'][$package->getName()])){ - $patch_levels = array($extra['patchLevel'][$package->getName()]); - } - // Attempt to apply with git apply. - $patched = $this->applyPatchWithGit($install_path, $patch_levels, $filename); - - // In some rare cases, git will fail to apply a patch, fallback to using - // the 'patch' command. - if (!$patched) { - foreach ($patch_levels as $patch_level) { - // --no-backup-if-mismatch here is a hack that fixes some - // differences between how patch works on windows and unix. - if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) { - break; - } - } - } - - // Clean up the temporary patch file. - if (isset($hostname)) { - unlink($filename); - } - // If the patch *still* isn't applied, then give up and throw an Exception. - // Otherwise, let the user know it worked. - if (!$patched) { - throw new \Exception("Cannot apply patch $patch_url"); - } - } - - /** - * Checks if the root package enables patching. - * - * @return bool - * Whether patching is enabled. Defaults to TRUE. - */ - protected function isPatchingEnabled() { - $extra = $this->composer->getPackage()->getExtra(); - - if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) { - // The root package has no patches of its own, so only allow patching if - // it has specifically opted in. - return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE; - } - else { - return TRUE; - } - } - - /** - * Writes a patch report to the target directory. - * - * @param array $patches - * @param string $directory - */ - protected function writePatchReport($patches, $directory) { - $output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n"; - $output .= "Patches applied to this directory:\n\n"; - foreach ($patches as $description => $url) { - $output .= $description . "\n"; - $output .= 'Source: ' . $url . "\n\n\n"; - } - file_put_contents($directory . "/PATCHES.txt", $output); - } - - /** - * Executes a shell command with escaping. - * - * @param string $cmd - * @return bool - */ - protected function executeCommand($cmd) { - // Shell-escape all arguments except the command. - $args = func_get_args(); - foreach ($args as $index => $arg) { - if ($index !== 0) { - $args[$index] = escapeshellarg($arg); - } - } - - // And replace the arguments. - $command = call_user_func_array('sprintf', $args); - $output = ''; - if ($this->io->isVerbose()) { - $this->io->write('' . $command . ''); - $io = $this->io; - $output = function ($type, $data) use ($io) { - if ($type == Process::ERR) { - $io->write('' . $data . ''); - } - else { - $io->write('' . $data . ''); - } - }; - } - return ($this->executor->execute($command, $output) == 0); - } - - /** - * Recursively merge arrays without changing data types of values. - * - * Does not change the data types of the values in the arrays. Matching keys' - * values in the second array overwrite those in the first array, as is the - * case with array_merge. - * - * @param array $array1 - * The first array. - * @param array $array2 - * The second array. - * @return array - * The merged array. - * - * @see http://php.net/manual/en/function.array-merge-recursive.php#92195 - */ - protected function arrayMergeRecursiveDistinct(array $array1, array $array2) { - $merged = $array1; - - foreach ($array2 as $key => &$value) { - if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { - $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value); - } - else { - $merged[$key] = $value; - } - } - - return $merged; - } - - /** - * Attempts to apply a patch with git apply. - * - * @param $install_path - * @param $patch_levels - * @param $filename - * - * @return bool - * TRUE if patch was applied, FALSE otherwise. - */ - protected function applyPatchWithGit($install_path, $patch_levels, $filename) { - // Do not use git apply unless the install path is itself a git repo - // @see https://stackoverflow.com/a/27283285 - if (!is_dir($install_path . '/.git')) { - return FALSE; - } - - $patched = FALSE; - foreach ($patch_levels as $patch_level) { - if ($this->io->isVerbose()) { - $comment = 'Testing ability to patch with git apply.'; - $comment .= ' This command may produce errors that can be safely ignored.'; - $this->io->write('' . $comment . ''); - } - $checked = $this->executeCommand('git -C %s apply --check -v %s %s', $install_path, $patch_level, $filename); - $output = $this->executor->getErrorOutput(); - if (substr($output, 0, 7) == 'Skipped') { - // Git will indicate success but silently skip patches in some scenarios. - // - // @see https://github.com/cweagans/composer-patches/pull/165 - $checked = FALSE; - } - if ($checked) { - // Apply the first successful style. - $patched = $this->executeCommand('git -C %s apply %s %s', $install_path, $patch_level, $filename); - break; - } - } - return $patched; - } - - /** - * Indicates if a package has been patched. - * - * @param \Composer\Package\PackageInterface $package - * The package to check. - * - * @return bool - * TRUE if the package has been patched. - */ - public static function isPackagePatched(PackageInterface $package) { - return array_key_exists('patches_applied', $package->getExtra()); - } - - /** - * {@inheritDoc} - */ - public function deactivate(Composer $composer, IOInterface $io) - { - } - - /** - * {@inheritDoc} - */ - public function uninstall(Composer $composer, IOInterface $io) - { - } - -} diff --git a/cweagans/composer-patches/src/Plugin/Patches.php b/cweagans/composer-patches/src/Plugin/Patches.php new file mode 100644 index 000000000..5b74ca19f --- /dev/null +++ b/cweagans/composer-patches/src/Plugin/Patches.php @@ -0,0 +1,413 @@ +composer = $composer; + $this->io = $io; + $this->executor = new ProcessExecutor($this->io); + $this->patches = array(); + $this->installedPatches = array(); + $this->lockFile = new JsonFile( + static::getPatchesLockFilePath(), + null, + $this->io + ); + $this->locker = new Locker($this->lockFile); + $this->configuration = [ + 'disable-resolvers' => [ + 'type' => 'list', + 'default' => [], + ], + 'disable-downloaders' => [ + 'type' => 'list', + 'default' => [], + ], + 'disable-patchers' => [ + 'type' => 'list', + 'default' => [], + ], + 'default-patch-depth' => [ + 'type' => 'int', + 'default' => 1, + ], + 'package-depths' => [ + 'type' => 'list', + 'default' => [], + ], + 'patches-file' => [ + 'type' => 'string', + 'default' => 'patches.json', + ], + "ignore-dependency-patches" => [ + 'type' => 'list', + 'default' => [], + ], + ]; + $this->configure($this->composer->getPackage()->getExtra(), 'composer-patches'); + } + + /** + * Returns an array of event names this subscriber wants to listen to. + * + * @calls resolvePatches + */ + public static function getSubscribedEvents(): array + { + return array( + PackageEvents::PRE_PACKAGE_INSTALL => ['loadLockedPatches'], + PackageEvents::PRE_PACKAGE_UPDATE => ['loadLockedPatches'], + // The POST_PACKAGE_* events are a higher weight for compatibility with + // https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with + // any Composer Plugin which deploys downloaded packages to other locations. In the cast that you want + // those plugins to deploy patched files, those plugins have to run *after* this plugin. + // @see: https://github.com/cweagans/composer-patches/pull/153 + PackageEvents::POST_PACKAGE_INSTALL => ['patchPackage', 10], + PackageEvents::POST_PACKAGE_UPDATE => ['patchPackage', 10], + ); + } + + /** + * Return a list of plugin capabilities. + * + * @return array + */ + public function getCapabilities(): array + { + return [ + ResolverProvider::class => CoreResolverProvider::class, + DownloaderProvider::class => CoreDownloaderProvider::class, + PatcherProvider::class => CorePatcherProvider::class, + CommandProviderCapability::class => CommandProvider::class, + ]; + } + + /** + * Discover patches using all available Resolvers. + */ + public function resolvePatches() + { + $resolver = new Resolver($this->composer, $this->io, $this->getConfig('disable-resolvers')); + return $resolver->loadFromResolvers(); + } + + /** + * Resolve and download patches so that all sha256 sums can be included in the lock file. + */ + public function createNewPatchesLock() + { + $this->patchCollection = $this->resolvePatches(); + $downloader = new Downloader($this->composer, $this->io, $this->getConfig('disable-downloaders')); + foreach ($this->patchCollection->getPatchedPackages() as $package) { + foreach ($this->patchCollection->getPatchesForPackage($package) as $patch) { + $this->download($patch); + $this->guessDepth($patch); + } + } + $this->locker->setLockData($this->patchCollection); + } + + /** + * Load previously discovered patches from the Composer lock file. + * + * @param PackageEvent $event + * The event provided by Composer. + */ + public function loadLockedPatches() + { + $locked = $this->locker->isLocked(); + if (!$locked) { + $filename = pathinfo($this->getLockFile()->getPath(), \PATHINFO_BASENAME); + $this->io->write("$filename does not exist. Creating a new $filename."); + $this->createNewPatchesLock(); + return; + } + + $this->patchCollection = PatchCollection::fromJson($this->locker->getLockData()); + } + + public function download(Patch $patch) + { + static $downloader; + if (is_null($downloader)) { + $downloader = new Downloader($this->composer, $this->io, $this->getConfig('disable-downloaders')); + } + + $this->composer->getEventDispatcher()->dispatch( + PatchEvents::PRE_PATCH_DOWNLOAD, + new PatchEvent(PatchEvents::PRE_PATCH_DOWNLOAD, $patch, $this->composer, $this->io) + ); + $downloader->downloadPatch($patch); + $this->composer->getEventDispatcher()->dispatch( + PatchEvents::POST_PATCH_DOWNLOAD, + new PatchEvent(PatchEvents::POST_PATCH_DOWNLOAD, $patch, $this->composer, $this->io) + ); + } + + public function guessDepth(Patch $patch) + { + $event = new PatchEvent(PatchEvents::PRE_PATCH_GUESS_DEPTH, $patch, $this->composer, $this->io); + $this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_GUESS_DEPTH, $event); + $patch = $event->getPatch(); + + $depth = $patch->depth ?? + $this->getConfig('package-depths')[$patch->package] ?? + Util::getDefaultPackagePatchDepth($patch->package) ?? + $this->getConfig('default-patch-depth'); + $patch->depth = $depth; + } + + public function apply(Patch $patch, string $install_path) + { + static $patcher; + if (is_null($patcher)) { + $patcher = new Patcher($this->composer, $this->io, $this->getConfig('disable-patchers')); + } + + $this->guessDepth($patch); + + $event = new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $patch, $this->composer, $this->io); + $this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_APPLY, $event); + $patch = $event->getPatch(); + + $this->io->write( + " - Applying patch {$patch->localPath} (depth: {$patch->depth})", + true, + IOInterface::DEBUG + ); + + $status = $patcher->applyPatch($patch, $install_path); + if ($status === false) { + $e = new Exception("No available patcher was able to apply patch {$patch->url} to {$patch->package}"); + + $this->composer->getEventDispatcher()->dispatch( + PatchEvents::POST_PATCH_APPLY_ERROR, + new PatchEvent(PatchEvents::POST_PATCH_APPLY_ERROR, $patch, $this->composer, $this->io, $e) + ); + + throw $e; + } + + $this->composer->getEventDispatcher()->dispatch( + PatchEvents::POST_PATCH_APPLY, + new PatchEvent(PatchEvents::POST_PATCH_APPLY, $patch, $this->composer, $this->io) + ); + } + + + /** + * Download and apply patches. + * + * @param PackageEvent $event + * The event that Composer provided to us. + */ + public function patchPackage(PackageEvent $event) + { + // Sometimes, patchPackage is called before a patch loading function (for instance, when composer-patches itself + // is installed -- the pre-install event can't be invoked before this plugin is installed, but the post-install + // event *can* be. Skipping composer-patches and composer-configurable-plugin ensures that this plugin and its + // dependency won't cause an error to be thrown when attempting to read from an uninitialized PatchCollection. + // This also means that neither composer-patches nor composer-configurable-plugin can have patches applied. + $package = $this->getPackageFromOperation($event->getOperation()); + if (in_array($package->getName(), ['cweagans/composer-patches', 'cweagans/composer-configurable-plugin'])) { + return; + } + + // If there aren't any patches, there's nothing to do. + if (empty($this->patchCollection->getPatchesForPackage($package->getName()))) { + $this->io->write( + "No patches found for {$package->getName()}", + true, + IOInterface::DEBUG, + ); + return; + } + + $install_path = $this->composer->getInstallationManager() + ->getInstaller($package->getType()) + ->getInstallPath($package); + + $this->io->write(" - Patching {$package->getName()}"); + + foreach ($this->patchCollection->getPatchesForPackage($package->getName()) as $patch) { + /** @var $patch Patch */ + + // Download patch. + $this->io->write( + " - Downloading and applying patch {$patch->url} ({$patch->description})", + true, + IOInterface::VERBOSE + ); + + $this->io->write(" - Downloading patch {$patch->url}", true, IOInterface::DEBUG); + + $this->download($patch); + $this->guessDepth($patch); + + // Apply patch. + $this->io->write( + " - Applying downloaded patch {$patch->localPath}", + true, + IOInterface::DEBUG + ); + + $this->apply($patch, $install_path); + } + + $this->io->write( + " - All patches for {$package->getName()} have been applied.", + true, + IOInterface::DEBUG + ); + } + + /** + * Get a Package object from an OperationInterface object. + * + * @param OperationInterface $operation + * @return PackageInterface + * @throws InvalidArgumentException + */ + protected function getPackageFromOperation(OperationInterface $operation): PackageInterface + { + if ($operation instanceof InstallOperation) { + $package = $operation->getPackage(); + } elseif ($operation instanceof UpdateOperation) { + $package = $operation->getTargetPackage(); + } else { + throw new InvalidArgumentException('Unknown operation: ' . get_class($operation)); + } + + return $package; + } + + public function getLocker(): Locker + { + return $this->locker; + } + + public function getLockFile(): JsonFile + { + return $this->lockFile; + } + + public function getPatchCollection(): ?PatchCollection + { + return $this->patchCollection; + } + + public function deactivate(Composer $composer, IOInterface $io) + { + } + + public function uninstall(Composer $composer, IOInterface $io) + { + } +} diff --git a/cweagans/composer-patches/src/Resolver.php b/cweagans/composer-patches/src/Resolver.php new file mode 100644 index 000000000..182430395 --- /dev/null +++ b/cweagans/composer-patches/src/Resolver.php @@ -0,0 +1,109 @@ +composer = $composer; + $this->io = $io; + $this->disabledResolvers = $disabledResolvers; + } + + /** + * Gather patches that need to be applied to the current set of packages. + * + * Note that this work is done unconditionally if this plugin is enabled, + * even if patching is disabled in any way. The point where patches are applied + * is where the work will be skipped. It's done this way to ensure that + * patching can be disabled temporarily in a way that doesn't affect the + * contents of composer.lock. + */ + public function loadFromResolvers(): PatchCollection + { + $patchCollection = new PatchCollection(); + + // Let each resolver discover patches and add them to the PatchCollection. + /** @var ResolverInterface $resolver */ + foreach ($this->getPatchResolvers() as $resolver) { + $class = "\\" . get_class($resolver); + + if (in_array($class, $this->disabledResolvers, true)) { + $this->io->write( + ' - Skipping resolver ' . $class . '', + true, + IOInterface::VERBOSE + ); + continue; + } + + $resolver->resolve($patchCollection); + } + + return $patchCollection; + } + + public function silenceOutput(): void + { + $this->inactive_io = $this->io; + $this->io = new NullIO(); + } + + public function unsilenceOutput(): void + { + $this->io = $this->inactive_io; + } + + /** + * Gather a list of all patch resolvers from all enabled Composer plugins. + * + * @return ResolverInterface[] + * A list of PatchResolvers to be run. + */ + protected function getPatchResolvers(): array + { + $resolvers = []; + $plugin_manager = $this->composer->getPluginManager(); + $capabilities = $plugin_manager->getPluginCapabilities( + ResolverProvider::class, + ['composer' => $this->composer, 'io' => $this->io] + ); + foreach ($capabilities as $capability) { + /** @var ResolverProvider $capability */ + $newResolvers = $capability->getResolvers(); + foreach ($newResolvers as $resolver) { + if (!$resolver instanceof ResolverInterface) { + throw new UnexpectedValueException( + 'Plugin capability ' . get_class($capability) . ' returned an invalid value.' + ); + } + } + $resolvers = array_merge($resolvers, $newResolvers); + } + + $event = new PluginEvent(PluginEvents::POST_DISCOVER_RESOLVERS, $resolvers, $this->composer, $this->io); + $this->composer->getEventDispatcher()->dispatch(PluginEvents::POST_DISCOVER_RESOLVERS, $event); + $resolvers = $event->getCapabilities(); + + return $resolvers; + } +} diff --git a/cweagans/composer-patches/src/Resolver/Dependencies.php b/cweagans/composer-patches/src/Resolver/Dependencies.php new file mode 100644 index 000000000..e7ee8350c --- /dev/null +++ b/cweagans/composer-patches/src/Resolver/Dependencies.php @@ -0,0 +1,54 @@ +composer->getLocker(); + if (!$locker->isLocked()) { + $this->io->write(' - Composer lock file does not exist.'); + $this->io->write(' - Patches defined in dependencies will not be resolved.'); + return; + } + + $this->io->write(' - Resolving patches from dependencies.'); + + $ignored_dependencies = $this->plugin->getConfig('ignore-dependency-patches'); + + $lockdata = $locker->getLockData(); + foreach ($lockdata['packages'] as $p) { + // If we're supposed to skip gathering patches from a dependency, do that. + if (in_array($p['name'], $ignored_dependencies)) { + continue; + } + + // Find patches in the composer.json for dependencies. + if (!isset($p['extra']) || !isset($p['extra']['patches'])) { + continue; + } + foreach ($this->findPatchesInJson($p['extra']['patches']) as $package => $patches) { + foreach ($patches as $patch) { + $patch->extra['provenance'] = "dependency:" . $package; + + /** @var Patch $patch */ + $collection->addPatch($patch); + } + } + + // TODO: Also find patches in a configured patches.json for the dependency. + } + } +} diff --git a/cweagans/composer-patches/src/Resolver/PatchesFile.php b/cweagans/composer-patches/src/Resolver/PatchesFile.php new file mode 100644 index 000000000..c78a04575 --- /dev/null +++ b/cweagans/composer-patches/src/Resolver/PatchesFile.php @@ -0,0 +1,73 @@ +plugin->getConfig('patches-file'); + $valid_patches_file = file_exists(realpath($patches_file_path)) && is_readable(realpath($patches_file_path)); + + // If we don't have a valid patches file, exit early. + if (!$valid_patches_file) { + return; + } + + $this->io->write(' - Resolving patches from patches file.'); + $patches_file = $this->readPatchesFile($patches_file_path); + + foreach ($this->findPatchesInJson($patches_file) as $package => $patches) { + foreach ($patches as $patch) { + /** @var Patch $patch */ + $patch->extra['provenance'] = "patches-file:" . $patches_file_path; + $collection->addPatch($patch); + } + } + } + + /** + * Read a patches file. + * + * @param $patches_file + * A URI to a file. Can be anything accepted by file_get_contents(). + * @return array + * A list of patches. + * @throws InvalidArgumentException + */ + protected function readPatchesFile($patches_file): array + { + if ($patches_file === '') { + return []; + } + + $patches = file_get_contents($patches_file); + $patches = json_decode($patches, true); + + // First, check for JSON syntax issues. + $json_error = json_last_error_msg(); + if ($json_error !== "No error") { + throw new InvalidArgumentException($json_error); + } + + // Next, make sure there is a patches key in the file. + if (!array_key_exists('patches', $patches)) { + throw new InvalidArgumentException('No patches found.'); + } + + // If nothing is wrong at this point, we can return the list of patches. + return $patches['patches']; + } +} diff --git a/cweagans/composer-patches/src/Resolver/ResolverBase.php b/cweagans/composer-patches/src/Resolver/ResolverBase.php new file mode 100644 index 000000000..a71ad3876 --- /dev/null +++ b/cweagans/composer-patches/src/Resolver/ResolverBase.php @@ -0,0 +1,111 @@ +composer = $composer; + $this->io = $io; + $this->plugin = $plugin; + } + + /** + * {@inheritdoc} + */ + abstract public function resolve(PatchCollection $collection): void; + + /** + * Handles the different patch definition formats and returns a list of Patches. + * + * @param array $patches + * An array of patch defs from composer.json or a patches file. + * + * @return array $patches + * An array of Patch objects grouped by package name. + */ + public function findPatchesInJson(array $patches): array + { + // Given an array of patch data (pulled directly from the root composer.json + // or a patches file), figure out what patch format each package is using and + // marshall everything into Patch objects. + foreach ($patches as $package => $patch_defs) { + if (isset($patch_defs[0]) && is_array($patch_defs[0])) { + $this->io->write( + " Using expanded definition format for package {$package}", + true, + IOInterface::VERBOSE + ); + + foreach ($patch_defs as $index => $def) { + $patch = new Patch(); + $patch->package = $package; + $patch->url = $def['url']; + $patch->description = $def['description']; + $patch->sha256 = $def['sha256'] ?? null; + $patch->depth = $def['depth'] ?? null; + $patch->extra = $def['extra'] ?? []; + + $patches[$package][$index] = $patch; + } + } else { + $this->io->write( + " Using compact definition format for package {$package}", + true, + IOInterface::VERBOSE + ); + + $temporary_patch_list = []; + + foreach ($patch_defs as $description => $url) { + $patch = new Patch(); + $patch->package = $package; + $patch->url = $url; + $patch->description = $description; + + $temporary_patch_list[] = $patch; + } + + $patches[$package] = $temporary_patch_list; + } + } + + return $patches; + } +} diff --git a/cweagans/composer-patches/src/Resolver/ResolverInterface.php b/cweagans/composer-patches/src/Resolver/ResolverInterface.php new file mode 100644 index 000000000..b7f5a748d --- /dev/null +++ b/cweagans/composer-patches/src/Resolver/ResolverInterface.php @@ -0,0 +1,37 @@ +composer->getPackage()->getExtra(); + if (empty($extra['patches'])) { + return; + } + + $this->io->write(' - Resolving patches from root package.'); + + foreach ($this->findPatchesInJson($extra['patches']) as $package => $patches) { + foreach ($patches as $patch) { + /** @var Patch $patch */ + $patch->extra['provenance'] = "root"; + $collection->addPatch($patch); + } + } + } +} diff --git a/cweagans/composer-patches/src/Util.php b/cweagans/composer-patches/src/Util.php new file mode 100644 index 000000000..e0e363922 --- /dev/null +++ b/cweagans/composer-patches/src/Util.php @@ -0,0 +1,25 @@ + 2, + ]; + + return $packages[$package] ?? null; + } +} diff --git a/patches.lock.json b/patches.lock.json new file mode 100644 index 000000000..6d7453f29 --- /dev/null +++ b/patches.lock.json @@ -0,0 +1,73 @@ +{ + "_hash": "b827683badf38ee383e2b0f62c03aedfa5f367a811b2ad426de87332e0910b2f", + "patches": { + "wapmorgan/mp3info": [ + { + "package": "wapmorgan/mp3info", + "description": "Break frame parsing on invalid frame", + "url": ".patches/mp3info-break-frame-parsing.patch", + "sha256": "d1093bc21282827bd49b98618cc8e44b1456d317d2713e27cc5f58a6af45a4bf", + "depth": 1, + "extra": { + "provenance": "patches-file:composer.patches.json" + } + }, + { + "package": "wapmorgan/mp3info", + "description": "fix incorrect lookup for mpeg header", + "url": ".patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch", + "sha256": "084e7b15afc1dd149a3a7bf3cc39a13918d2d6ec8aeb6e4ff7f4f2e47ce15eed", + "depth": 1, + "extra": { + "provenance": "patches-file:composer.patches.json" + } + } + ], + "sabre/dav": [ + { + "package": "sabre/dav", + "description": "Check for instanceof INode instead of Node", + "url": ".patches/check-for-instanceof-INode-instead-of-Node-1595.patch", + "sha256": "852573c258fccf8a89636870a752ab02a799d160e291ea04f358664ba956296d", + "depth": 1, + "extra": { + "provenance": "patches-file:composer.patches.json" + } + } + ], + "sabre/vobject": [ + { + "package": "sabre/vobject", + "description": "fix use RDATE in time range check and use all instances", + "url": ".patches/sabre-vobject-rdate.patch", + "sha256": "816d7b859858535c610ee7a6b0401d907da3f8b08dfc909149ad7cc72b5dc250", + "depth": 1, + "extra": { + "provenance": "patches-file:composer.patches.json" + } + }, + { + "package": "sabre/vobject", + "description": "fix send participation reply on fresh event", + "url": ".patches/sabre-vobject-iTipBroker-replies.patch", + "sha256": "fb764740364f485079058d9a653094c3f9f36e04d01f0f3f5b52bd7b66a9cb2b", + "depth": 1, + "extra": { + "provenance": "patches-file:composer.patches.json" + } + } + ], + "sabre/xml": [ + { + "package": "sabre/xml", + "description": "Remove callable serialization", + "url": ".patches/sabre-xml-remove-callable-serialization.patch", + "sha256": "96ff87a1eac6c0c8094f121b73cf1672aa21d991aa3cefb2a5435f2476f1026e", + "depth": 1, + "extra": { + "provenance": "patches-file:composer.patches.json" + } + } + ] + } +} diff --git a/sabre/dav/PATCHES.txt b/sabre/dav/PATCHES.txt deleted file mode 100644 index 61140f3cf..000000000 --- a/sabre/dav/PATCHES.txt +++ /dev/null @@ -1,7 +0,0 @@ -This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) -Patches applied to this directory: - -Check for instanceof INode instead of Node -Source: .patches/check-for-instanceof-INode-instead-of-Node-1595.patch - - diff --git a/sabre/vobject/PATCHES.txt b/sabre/vobject/PATCHES.txt deleted file mode 100644 index 358e87a0c..000000000 --- a/sabre/vobject/PATCHES.txt +++ /dev/null @@ -1,11 +0,0 @@ -This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) -Patches applied to this directory: - -fix use RDATE in time range check and use all instances -Source: .patches/sabre-vobject-rdate.patch - - -fix send participation reply on fresh event -Source: .patches/sabre-vobject-iTipBroker-replies.patch - - diff --git a/sabre/xml/PATCHES.txt b/sabre/xml/PATCHES.txt deleted file mode 100644 index 022f67c8e..000000000 --- a/sabre/xml/PATCHES.txt +++ /dev/null @@ -1,7 +0,0 @@ -This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) -Patches applied to this directory: - -Remove callable serialization -Source: .patches/sabre-xml-remove-callable-serialization.patch - - diff --git a/wapmorgan/mp3info/PATCHES.txt b/wapmorgan/mp3info/PATCHES.txt deleted file mode 100644 index 9d10c66da..000000000 --- a/wapmorgan/mp3info/PATCHES.txt +++ /dev/null @@ -1,11 +0,0 @@ -This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) -Patches applied to this directory: - -Break frame parsing on invalid frame -Source: .patches/mp3info-break-frame-parsing.patch - - -fix incorrect lookup for mpeg header -Source: .patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch - -