diff --git a/administrator/components/com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql b/administrator/components/com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql new file mode 100644 index 00000000000..e325600022f --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql @@ -0,0 +1,23 @@ +-- +-- Table structure for table `#__tuf_metadata` +-- + +CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( + `id` int NOT NULL AUTO_INCREMENT, + `update_site_id` int DEFAULT 0, + `root` text DEFAULT NULL, + `targets` text DEFAULT NULL, + `snapshot` text DEFAULT NULL, + `timestamp` text DEFAULT NULL, + `mirrors` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; + +-- -------------------------------------------------------- +INSERT INTO `#__tuf_metadata` (`update_site_id`, `root`) +VALUES ((SELECT ue.`update_site_id` FROM `#__update_sites_extensions` AS ue JOIN `#__extensions` AS e ON (e.`extension_id` = ue.`extension_id`) WHERE e.`type`='file' AND e.`element`='joomla'), '{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2025-03-02T11:22:17Z","keys":{"07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9"}},"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799"}},"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9"}},"31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2"}},"9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9"}}},"roles":{"root":{"keyids":["1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"snapshot":{"keyids":["07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"targets":{"keyids":["31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3"],"threshold":1},"timestamp":{"keyids":["9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e","sig":"2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501"},{"keyid":"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","sig":"8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005"}]}'); + +----------------------------------------------------------- +UPDATE `#__update_sites` + SET `type` = 'tuf', `location` = 'https://update.joomla.org/cms/' + WHERE `update_site_id` = (SELECT ue.`update_site_id` FROM `#__update_sites_extensions` AS ue JOIN `#__extensions` AS e ON (e.`extension_id` = ue.`extension_id`) WHERE e.`type`='file' AND e.`element`='joomla'); \ No newline at end of file diff --git a/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql b/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql new file mode 100644 index 00000000000..cda10e2c2e6 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql @@ -0,0 +1,23 @@ +-- +-- Table structure for table "#__tuf_metadata" +-- + +CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( +"id" serial NOT NULL, +"update_site_id" bigint DEFAULT 0 NOT NULL, +"root" text DEFAULT NULL, +"targets" text DEFAULT NULL, +"snapshot" text DEFAULT NULL, +"timestamp" text DEFAULT NULL, +"mirrors" text DEFAULT NULL, +PRIMARY KEY ("id") +); + +COMMENT ON TABLE "#__tuf_metadata" IS 'Secure TUF Updates'; + +INSERT INTO "#__tuf_metadata" ("update_site_id", "root") +VALUES (1, '{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2025-03-02T11:22:17Z","keys":{"07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9"}},"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799"}},"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9"}},"31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2"}},"9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9"}}},"roles":{"root":{"keyids":["1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"snapshot":{"keyids":["07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"targets":{"keyids":["31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3"],"threshold":1},"timestamp":{"keyids":["9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e","sig":"2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501"},{"keyid":"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","sig":"8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005"}]}'); + +UPDATE "#__update_sites" + SET "type" = 'tuf', "location" = 'https://update.joomla.org/cms/' + WHERE "update_site_id" = 1; \ No newline at end of file diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index fdb7932df51..409ee61fbe8 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -336,6 +336,23 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) continue; } + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('type') + ->from('#__update_sites') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $instance->update_site_id, ParameterType::INTEGER); + + $updateSiteType = (string) $db->setQuery($query)->loadResult(); + + // Tuf is currently only supported for Joomla core + if ($updateSiteType === 'tuf') { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error'); + + return; + } + $update->loadFromXml($instance->detailsurl, $minimumStability); // Find and use extra_query from update_site if available diff --git a/administrator/components/com_joomlaupdate/config.xml b/administrator/components/com_joomlaupdate/config.xml index 4aa5f5697c6..9259b7156e5 100644 --- a/administrator/components/com_joomlaupdate/config.xml +++ b/administrator/components/com_joomlaupdate/config.xml @@ -16,7 +16,6 @@ validate="options" > - diff --git a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php index 5e4f544bec9..175113ca528 100644 --- a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php +++ b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php @@ -62,6 +62,24 @@ public function download() $message = null; $messageType = null; + // The versions mismatch + if ($result['version'] !== $this->input->get('targetVersion')) { + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_VERSION_WRONG'); + $messageType = 'error'; + $url = 'index.php?option=com_joomlaupdate'; + + $this->app->setUserState('com_joomlaupdate.file', null); + $this->setRedirect($url, $message, $messageType); + + try { + Log::add($message, Log::ERROR, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return; + } + // The validation was not successful so stop. if ($result['check'] === false) { $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 977531e1347..df262f2e595 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -24,6 +24,7 @@ use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Table\Tuf as TufMetadata; use Joomla\CMS\Updater\Update; use Joomla\CMS\Updater\Updater; use Joomla\CMS\User\UserHelper; @@ -87,12 +88,7 @@ public function applyUpdateSite() // Determine the intended update URL. $params = ComponentHelper::getParams('com_joomlaupdate'); - switch ($params->get('updatesource', 'nochange')) { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; - break; - + switch ($params->get('updatesource', 'default')) { // "Testing" case 'testing': $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; @@ -111,17 +107,20 @@ public function applyUpdateSite() break; /** - * "Minor & Patch Release for Current version (recommended and default)". + * All "non-testing" releases of the official project hosted in the TUF repo. * The commented "case" below are for documenting where 'default' and legacy options falls * case 'default': + * case 'next': * case 'lts': * case 'sts': (It's shown as "Default" because that option does not exist any more) * case 'nochange': */ default: - $updateURL = 'https://update.joomla.org/core/list.xml'; + $updateURL = 'https://update.joomla.org/cms/'; } + $updateType = (pathinfo($updateURL, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf'; + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); $query = $db->getQuery(true) @@ -137,10 +136,11 @@ public function applyUpdateSite() $db->setQuery($query); $update_site = $db->loadObject(); - if ($update_site->location != $updateURL) { + if ($update_site->location !== $updateURL || $update_site->type !== $updateType) { // Modify the database record. $update_site->last_check_timestamp = 0; $update_site->location = $updateURL; + $update_site->type = $updateType; $db->updateObject('#__update_sites', $update_site, 'update_site_id'); // Remove cached updates. @@ -156,7 +156,7 @@ public function applyUpdateSite() /** * Makes sure that the Joomla! update cache is up-to-date. * - * @param boolean $force Force reload, ignoring the cache timeout. + * @param boolean $force Force reload, ignoring the cache timeout. * * @return void * @@ -176,7 +176,7 @@ public function refreshUpdates($force = false) $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - if (\in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { + if (\in_array($comJoomlaupdateParams->get('updatesource', 'default'), ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); } @@ -298,14 +298,34 @@ public function getUpdateInformation() $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + $channel = $comJoomlaupdateParams->get('updatesource', 'default'); - if (\in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { + if (\in_array($channel, ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); } - // Fetch the full update details from the update details URL. $update = new Update(); - $update->loadFromXml($updateObject->detailsurl, $minimumStability); + + $updateType = (pathinfo($updateObject->detailsurl, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf'; + + // Check if we have a local JSON string with update metadata + if (!empty($updateType === 'tuf')) { + // Use the correct identifier for the update channel + $updateChannel = Version::MAJOR_VERSION . '.x'; + + if ($channel === 'next') { + $updateChannel = (Version::MAJOR_VERSION + 1) . '.x'; + } + + $metadata = new TufMetadata($this->getDatabase()); + $metadata->load(['update_site_id' => $updateObject->update_site_id]); + + // Fetch update data from TUF repo + $update->loadFromTuf($metadata, $updateObject->detailsurl, $minimumStability, $updateChannel); + } else { + // We are using the legacy XML method + $update->loadFromXml($updateObject->location, $minimumStability, $channel); + } // Make sure we use the current information we got from the detailsurl $this->updateInformation['object'] = $update; @@ -370,12 +390,12 @@ public function download() $httpOptions = new Registry(); $httpOptions->set('follow_location', false); + $response = ['basename' => false, 'check' => null, 'version' => $updateInfo['latest']]; + try { $head = HttpFactory::getHttp($httpOptions)->head($packageURL); } catch (\RuntimeException $e) { // Passing false here -> download failed message - $response['basename'] = false; - return $response; } @@ -387,8 +407,6 @@ public function download() $head = HttpFactory::getHttp($httpOptions)->head($packageURL); } catch (\RuntimeException $e) { // Passing false here -> download failed message - $response['basename'] = false; - return $response; } } @@ -409,7 +427,6 @@ public function download() ) ->clean(Factory::getApplication()->get('tmp_path'), 'path'); $target = $tempdir . '/' . $basename; - $response = []; // Do we have a cached file? $exists = is_file($target); diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php index e3384029509..d644542c4e0 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php @@ -58,7 +58,10 @@ '; if ($this->getCurrentUser()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = '
' . Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND') . '
'; + $displayData['formAppend'] = ' +
' . Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND') . '
+ + '; endif; echo '
'; diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index cbfd4909505..e2deeffc76a 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -174,6 +174,7 @@ COM_JOOMLAUPDATE_VIEW_DEFAULT_UPLOAD_INTRO="You can use this feature to update J COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESEXTRACTED="Bytes extracted" COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESREAD="Bytes read" COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG="File Checksum Failed" +COM_JOOMLAUPDATE_VIEW_UPDATE_VERSION_WRONG="The version of the update package and the requested version do not match, try to refresh the update information." COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED="Download of update package failed." COM_JOOMLAUPDATE_VIEW_UPDATE_ITEMS="items" COM_JOOMLAUPDATE_VIEW_UPDATE_FILESEXTRACTED="Files extracted" diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index 3be38285342..4ca84a55879 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -660,6 +660,13 @@ JLIB_INSTALLER_SQL_BEGIN="Start of SQL updates." JLIB_INSTALLER_SQL_BEGIN_SCHEMA="The current database version (schema) is %s." JLIB_INSTALLER_SQL_END="End of SQL updates." JLIB_INSTALLER_SQL_END_NOT_COMPLETE="End of SQL updates - INCOMPLETE." +JLIB_INSTALLER_TUF_FREEZE_ATTACK="Update not possible because the offered update has expired." +JLIB_INSTALLER_TUF_DEBUG_MESSAGE="TUF Debug Message: %s" +JLIB_INSTALLER_TUF_INVALID_METADATA="The saved TUF update information is invalid." +JLIB_INSTALLER_TUF_NOT_AVAILABLE="TUF is not available for extensions yet." +JLIB_INSTALLER_TUF_DOWNLOAD_SIZE="The size of the update did not match the expected size." +JLIB_INSTALLER_TUF_ROLLBACK_ATTACK="Update not possible because the offered update version is older than the currently installed version." +JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD="Update not possible because the offered update does not have enough signatures." JLIB_INSTALLER_UNINSTALL="Uninstall" JLIB_INSTALLER_UPDATE="Update" JLIB_INSTALLER_UPDATE_LOG_QUERY="Ran query from file %1$s. Query text: %2$s." diff --git a/composer.json b/composer.json index b6c329e5cbe..0df62dd7bc9 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,11 @@ "type": "vcs", "url": "https://github.com/joomla-backports/json-api-php.git", "no-api": true + }, + { + "type": "vcs", + "url": "https://github.com/joomla-backports/php-tuf.git", + "no-api": true } ], "autoload": { @@ -100,7 +105,8 @@ "web-token/signature-pack": "^3.2.8", "phpseclib/bcmath_compat": "^2.0.1", "jfcherng/php-diff": "^6.15.3", - "voku/portable-utf8": "^6.0.13" + "voku/portable-utf8": "^6.0.13", + "php-tuf/php-tuf": "dev-main" }, "require-dev": { "phpunit/phpunit": "^9.6.11", @@ -121,6 +127,9 @@ "symfony/polyfill-php80": "*", "symfony/polyfill-php81": "*" }, + "extra": { + "composer-exit-on-patch-failure": true + }, "scripts": { "post-install-cmd": [ "php build/update_fido_cache.php" diff --git a/composer.lock b/composer.lock index 279f635fb34..b89b8778e40 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": "a11e9fcd1917529c4c799d5c8760ec09", + "content-hash": "2b4e1f920883bf2cb2197de50e4bc0dd", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -575,6 +575,201 @@ }, "time": "2023-02-18T17:41:46+00:00" }, + { + "name": "guzzlehttp/promises", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T12:31:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, { "name": "jakeasmith/http_build_url", "version": "1.0.1", @@ -2429,20 +2624,20 @@ }, { "name": "laminas/laminas-diactoros", - "version": "2.25.2", + "version": "2.26.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e" + "reference": "6584d44eb8e477e89d453313b858daac6183cddc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e", - "reference": "9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/6584d44eb8e477e89d453313b858daac6183cddc", + "reference": "6584d44eb8e477e89d453313b858daac6183cddc", "shasum": "" }, "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1" }, @@ -2522,7 +2717,7 @@ "type": "community_bridge" } ], - "time": "2023-04-17T15:44:17+00:00" + "time": "2023-10-29T16:17:44+00:00" }, { "name": "lcobucci/clock", @@ -2664,16 +2859,16 @@ }, { "name": "maximebf/debugbar", - "version": "v1.19.0", + "version": "v1.19.1", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "30f65f18f7ac086255a77a079f8e0dcdd35e828e" + "reference": "03dd40a1826f4d585ef93ef83afa2a9874a00523" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30f65f18f7ac086255a77a079f8e0dcdd35e828e", - "reference": "30f65f18f7ac086255a77a079f8e0dcdd35e828e", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/03dd40a1826f4d585ef93ef83afa2a9874a00523", + "reference": "03dd40a1826f4d585ef93ef83afa2a9874a00523", "shasum": "" }, "require": { @@ -2724,9 +2919,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.19.0" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.19.1" }, - "time": "2023-09-19T19:53:10+00:00" + "time": "2023-10-12T08:10:52+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -2881,18 +3076,93 @@ }, "time": "2023-04-30T00:54:53+00:00" }, + { + "name": "php-tuf/php-tuf", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/joomla-backports/php-tuf.git", + "reference": "1961502dff73cc2d2a3f19b930516a18bc0ccd22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-backports/php-tuf/zipball/1961502dff73cc2d2a3f19b930516a18bc0ccd22", + "reference": "1961502dff73cc2d2a3f19b930516a18bc0ccd22", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^2.4", + "paragonie/sodium_compat": "^1.13", + "php": "^8", + "symfony/polyfill-php81": "^1.27", + "symfony/validator": "^4.4 || ^5 || ^6" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5 || ^7.2", + "phpspec/prophecy": "^1.16", + "phpspec/prophecy-phpunit": "^2", + "phpunit/phpunit": "^9", + "slevomat/coding-standard": "^8.2", + "squizlabs/php_codesniffer": "^3.7", + "symfony/phpunit-bridge": "^5" + }, + "suggest": { + "ext-sodium": "Provides faster verification of updates" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Tuf\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tuf\\Tests\\": "tests/" + } + }, + "scripts": { + "coverage": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --coverage-text --color=always --testdox" + ], + "fixtures": [ + "pipenv install", + "pipenv run python generate_fixtures.py" + ], + "phpcs": [ + "phpcs" + ], + "phpcbf": [ + "phpcbf" + ], + "test": [ + "phpunit --testdox" + ], + "lint": [ + "find src -name '*.php' -exec php -l {} \\;" + ] + }, + "license": [ + "MIT" + ], + "description": "PHP implementation of The Update Framework (TUF)", + "time": "2024-01-26T15:28:14+00:00" + }, { "name": "phpmailer/phpmailer", - "version": "v6.8.1", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "e88da8d679acc3824ff231fdc553565b802ac016" + "reference": "039de174cd9c17a8389754d3b877a2ed22743e18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016", - "reference": "e88da8d679acc3824ff231fdc553565b802ac016", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18", + "reference": "039de174cd9c17a8389754d3b877a2ed22743e18", "shasum": "" }, "require": { @@ -2912,6 +3182,7 @@ "yoast/phpunit-polyfills": "^1.0.4" }, "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", "ext-openssl": "Needed for secure SMTP sending and DKIM signing", "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", @@ -2951,7 +3222,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1" }, "funding": [ { @@ -2959,7 +3230,7 @@ "type": "github" } ], - "time": "2023-08-29T08:26:30+00:00" + "time": "2023-11-25T22:23:28+00:00" }, { "name": "phpseclib/bcmath_compat", @@ -3542,6 +3813,50 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "spomky-labs/cbor-php", "version": "3.0.2", @@ -3737,16 +4052,16 @@ }, { "name": "symfony/console", - "version": "v6.3.4", + "version": "v6.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" + "reference": "a550a7c99daeedef3f9d23fb82e3531525ff11fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", - "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", + "url": "https://api.github.com/repos/symfony/console/zipball/a550a7c99daeedef3f9d23fb82e3531525ff11fd", + "reference": "a550a7c99daeedef3f9d23fb82e3531525ff11fd", "shasum": "" }, "require": { @@ -3754,7 +4069,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0" + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/dependency-injection": "<5.4", @@ -3768,12 +4083,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/lock": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -3807,7 +4126,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.4" + "source": "https://github.com/symfony/console/tree/v6.4.1" }, "funding": [ { @@ -3823,11 +4142,11 @@ "type": "tidelift" } ], - "time": "2023-08-16T10:10:12+00:00" + "time": "2023-11-30T10:54:28+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -3874,7 +4193,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -3894,30 +4213,31 @@ }, { "name": "symfony/error-handler", - "version": "v6.3.2", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a" + "reference": "c873490a1c97b3a0a4838afc36ff36c112d02788" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/85fd65ed295c4078367c784e8a5a6cee30348b7a", - "reference": "85fd65ed295c4078367c784e8a5a6cee30348b7a", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/c873490a1c97b3a0a4838afc36ff36c112d02788", + "reference": "c873490a1c97b3a0a4838afc36ff36c112d02788", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "conflict": { - "symfony/deprecation-contracts": "<2.5" + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" }, "require-dev": { "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0" + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -3948,7 +4268,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.3.2" + "source": "https://github.com/symfony/error-handler/tree/v6.4.0" }, "funding": [ { @@ -3964,35 +4284,35 @@ "type": "tidelift" } ], - "time": "2023-07-16T17:05:46+00:00" + "time": "2023-10-18T09:43:34+00:00" }, { "name": "symfony/ldap", - "version": "v6.3.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/ldap.git", - "reference": "5f1308de5d3a1ca9e5c6ef6bb4b736e5dd6f3508" + "reference": "c499319a1b8cafaa91cecf1d6b0321b8f40814b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ldap/zipball/5f1308de5d3a1ca9e5c6ef6bb4b736e5dd6f3508", - "reference": "5f1308de5d3a1ca9e5c6ef6bb4b736e5dd6f3508", + "url": "https://api.github.com/repos/symfony/ldap/zipball/c499319a1b8cafaa91cecf1d6b0321b8f40814b8", + "reference": "c499319a1b8cafaa91cecf1d6b0321b8f40814b8", "shasum": "" }, "require": { "ext-ldap": "*", "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/options-resolver": "^5.4|^6.0" + "symfony/options-resolver": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/options-resolver": "<5.4", "symfony/security-core": "<5.4" }, "require-dev": { - "symfony/security-core": "^5.4|^6.0", - "symfony/security-http": "^5.4|^6.0" + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/security-http": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4024,7 +4344,7 @@ "ldap" ], "support": { - "source": "https://github.com/symfony/ldap/tree/v6.3.0" + "source": "https://github.com/symfony/ldap/tree/v6.4.0" }, "funding": [ { @@ -4040,20 +4360,20 @@ "type": "tidelift" } ], - "time": "2023-04-28T15:57:00+00:00" + "time": "2023-11-08T11:28:30+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.3.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" + "reference": "22301f0e7fdeaacc14318928612dee79be99860e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", - "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e", "shasum": "" }, "require": { @@ -4091,7 +4411,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.3.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" }, "funding": [ { @@ -4107,7 +4427,7 @@ "type": "tidelift" } ], - "time": "2023-05-12T14:21:09+00:00" + "time": "2023-08-08T10:16:24+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4522,6 +4842,86 @@ ], "time": "2023-07-28T09:04:16+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-php80": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-16T06:22:46+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.28.0", @@ -4689,16 +5089,16 @@ }, { "name": "symfony/string", - "version": "v6.3.2", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "53d1a83225002635bca3482fcbf963001313fb68" + "reference": "b45fcf399ea9c3af543a92edf7172ba21174d809" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/53d1a83225002635bca3482fcbf963001313fb68", - "reference": "53d1a83225002635bca3482fcbf963001313fb68", + "url": "https://api.github.com/repos/symfony/string/zipball/b45fcf399ea9c3af543a92edf7172ba21174d809", + "reference": "b45fcf399ea9c3af543a92edf7172ba21174d809", "shasum": "" }, "require": { @@ -4712,11 +5112,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/intl": "^6.2", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0" + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4755,7 +5155,85 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.3.2" + "source": "https://github.com/symfony/string/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-28T20:41:49+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.0" }, "funding": [ { @@ -4771,20 +5249,20 @@ "type": "tidelift" } ], - "time": "2023-07-05T08:41:27+00:00" + "time": "2023-07-25T15:08:44+00:00" }, { "name": "symfony/uid", - "version": "v6.3.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384" + "reference": "8092dd1b1a41372110d06374f99ee62f7f0b9a92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/01b0f20b1351d997711c56f1638f7a8c3061e384", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384", + "url": "https://api.github.com/repos/symfony/uid/zipball/8092dd1b1a41372110d06374f99ee62f7f0b9a92", + "reference": "8092dd1b1a41372110d06374f99ee62f7f0b9a92", "shasum": "" }, "require": { @@ -4792,7 +5270,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^5.4|^6.0" + "symfony/console": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4829,7 +5307,103 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.3.0" + "source": "https://github.com/symfony/uid/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:18:17+00:00" + }, + { + "name": "symfony/validator", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "33e1f3bb76ef70e3170e12f878aefb9c69b0fc4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/33e1f3bb76ef70e3170e12f878aefb9c69b0fc4c", + "reference": "33e1f3bb76ef70e3170e12f878aefb9c69b0fc4c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.4.0" }, "funding": [ { @@ -4845,20 +5419,20 @@ "type": "tidelift" } ], - "time": "2023-04-08T07:25:02+00:00" + "time": "2023-11-29T07:47:42+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.3.4", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45" + "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2027be14f8ae8eae999ceadebcda5b4909b81d45", - "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c40f7d17e91d8b407582ed51a2bbf83c52c367f6", + "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6", "shasum": "" }, "require": { @@ -4871,10 +5445,11 @@ }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/uid": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", "twig/twig": "^2.13|^3.0.4" }, "bin": [ @@ -4913,7 +5488,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.3.4" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.0" }, "funding": [ { @@ -4929,20 +5504,20 @@ "type": "tidelift" } ], - "time": "2023-08-24T14:51:05+00:00" + "time": "2023-11-09T08:28:32+00:00" }, { "name": "symfony/web-link", - "version": "v6.3.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", - "reference": "0989ca617d0703cdca501a245f10e194ff22315b" + "reference": "c7e30b9b90c4a9b3c94cc5697c7b8046a6655a51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-link/zipball/0989ca617d0703cdca501a245f10e194ff22315b", - "reference": "0989ca617d0703cdca501a245f10e194ff22315b", + "url": "https://api.github.com/repos/symfony/web-link/zipball/c7e30b9b90c4a9b3c94cc5697c7b8046a6655a51", + "reference": "c7e30b9b90c4a9b3c94cc5697c7b8046a6655a51", "shasum": "" }, "require": { @@ -4956,7 +5531,7 @@ "psr/link-implementation": "1.0|2.0" }, "require-dev": { - "symfony/http-kernel": "^5.4|^6.0" + "symfony/http-kernel": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4996,7 +5571,7 @@ "push" ], "support": { - "source": "https://github.com/symfony/web-link/tree/v6.3.0" + "source": "https://github.com/symfony/web-link/tree/v6.4.0" }, "funding": [ { @@ -5012,20 +5587,20 @@ "type": "tidelift" } ], - "time": "2023-04-21T14:41:17+00:00" + "time": "2023-09-25T12:52:38+00:00" }, { "name": "symfony/yaml", - "version": "v6.3.3", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e23292e8c07c85b971b44c1c4b87af52133e2add" + "reference": "4f9237a1bb42455d609e6687d2613dde5b41a587" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e23292e8c07c85b971b44c1c4b87af52133e2add", - "reference": "e23292e8c07c85b971b44c1c4b87af52133e2add", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4f9237a1bb42455d609e6687d2613dde5b41a587", + "reference": "4f9237a1bb42455d609e6687d2613dde5b41a587", "shasum": "" }, "require": { @@ -5037,7 +5612,7 @@ "symfony/console": "<5.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0" + "symfony/console": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -5068,7 +5643,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.3.3" + "source": "https://github.com/symfony/yaml/tree/v6.4.0" }, "funding": [ { @@ -5084,7 +5659,7 @@ "type": "tidelift" } ], - "time": "2023-07-31T07:08:24+00:00" + "time": "2023-11-06T11:00:25+00:00" }, { "name": "tobscure/json-api", @@ -6765,16 +7340,16 @@ }, { "name": "doctrine/deprecations", - "version": "v1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", "shasum": "" }, "require": { @@ -6806,9 +7381,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" + "source": "https://github.com/doctrine/deprecations/tree/1.1.2" }, - "time": "2023-06-03T09:27:29+00:00" + "time": "2023-09-27T20:04:15+00:00" }, { "name": "doctrine/instantiator", @@ -7856,16 +8431,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.1", + "version": "1.24.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01" + "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", - "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6bd0c26f3786cd9b7c359675cb789e35a8e07496", + "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496", "shasum": "" }, "require": { @@ -7897,9 +8472,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.4" }, - "time": "2023-09-18T12:18:02+00:00" + "time": "2023-11-26T18:29:22+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8222,16 +8797,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", "shasum": "" }, "require": { @@ -8305,7 +8880,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" }, "funding": [ { @@ -8321,7 +8896,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T16:55:19+00:00" }, { "name": "psr/cache", @@ -9404,16 +9979,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.2", + "version": "3.8.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5805f7a4e4958dbb5e944ef1e6edae0a303765e7", + "reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7", "shasum": "" }, "require": { @@ -9423,7 +9998,7 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/phpcs", @@ -9442,35 +10017,58 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", "standards", "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2023-02-22T23:07:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2023-12-08T12:32:31+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.3.2", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e" + "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/adb01fe097a4ee930db9258a3cc906b5beb5cf2e", - "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d76d2632cfc2206eecb5ad2b26cd5934082941b6", + "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6", "shasum": "" }, "require": { @@ -9487,13 +10085,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/error-handler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0" + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -9521,7 +10119,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.2" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.0" }, "funding": [ { @@ -9537,11 +10135,11 @@ "type": "tidelift" } ], - "time": "2023-07-06T06:56:43+00:00" + "time": "2023-07-27T06:52:43+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", @@ -9597,7 +10195,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" }, "funding": [ { @@ -9617,16 +10215,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.3.1", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", - "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/952a8cb588c3bc6ce76f6023000fb932f16a6e59", + "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59", "shasum": "" }, "require": { @@ -9660,7 +10258,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.3.1" + "source": "https://github.com/symfony/filesystem/tree/v6.4.0" }, "funding": [ { @@ -9676,27 +10274,27 @@ "type": "tidelift" } ], - "time": "2023-06-01T08:30:39+00:00" + "time": "2023-07-26T17:27:13+00:00" }, { "name": "symfony/finder", - "version": "v6.3.3", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e" + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e", - "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", "autoload": { @@ -9724,7 +10322,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.3.3" + "source": "https://github.com/symfony/finder/tree/v6.4.0" }, "funding": [ { @@ -9740,20 +10338,20 @@ "type": "tidelift" } ], - "time": "2023-07-31T08:31:44+00:00" + "time": "2023-10-31T17:30:12+00:00" }, { "name": "symfony/process", - "version": "v6.3.4", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" + "reference": "191703b1566d97a5425dc969e4350d32b8ef17aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", - "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", + "url": "https://api.github.com/repos/symfony/process/zipball/191703b1566d97a5425dc969e4350d32b8ef17aa", + "reference": "191703b1566d97a5425dc969e4350d32b8ef17aa", "shasum": "" }, "require": { @@ -9785,7 +10383,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.3.4" + "source": "https://github.com/symfony/process/tree/v6.4.0" }, "funding": [ { @@ -9801,11 +10399,11 @@ "type": "tidelift" } ], - "time": "2023-08-07T10:39:22+00:00" + "time": "2023-11-17T21:06:49+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.3.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -9847,7 +10445,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.0" }, "funding": [ { @@ -9867,16 +10465,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -9905,7 +10503,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -9913,7 +10511,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "tysonandre/var_representation_polyfill", @@ -9981,7 +10579,8 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "tobscure/json-api": 20 + "tobscure/json-api": 20, + "php-tuf/php-tuf": 20 }, "prefer-stable": false, "prefer-lowest": false, @@ -9996,5 +10595,5 @@ "platform-overrides": { "php": "8.1.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index c34c49dd41e..d100c0c629b 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -870,6 +870,29 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( -- -------------------------------------------------------- +-- +-- Table structure for table `#__tuf_updates` +-- + +CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( + `id` int NOT NULL AUTO_INCREMENT, + `update_site_id` int DEFAULT 0, + `root` text DEFAULT NULL, + `targets` text DEFAULT NULL, + `snapshot` text DEFAULT NULL, + `timestamp` text DEFAULT NULL, + `mirrors` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; + +-- +-- Dumping data for table `#__tuf_metadata` +-- +INSERT INTO `#__tuf_metadata` (`update_site_id`, `root`) +VALUES (1, '{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2025-03-02T11:22:17Z","keys":{"07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9"}},"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799"}},"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9"}},"31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2"}},"9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9"}}},"roles":{"root":{"keyids":["1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"snapshot":{"keyids":["07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"targets":{"keyids":["31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3"],"threshold":1},"timestamp":{"keyids":["9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e","sig":"2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501"},{"keyid":"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","sig":"8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005"}]}'); + +-- -------------------------------------------------------- + -- -- Table structure for table `#__update_sites` -- @@ -892,7 +915,7 @@ CREATE TABLE IF NOT EXISTS `#__update_sites` ( -- INSERT INTO `#__update_sites` (`update_site_id`, `name`, `type`, `location`, `enabled`, `last_check_timestamp`) VALUES -(1, 'Joomla! Core', 'collection', 'https://update.joomla.org/core/list.xml', 1, 0), +(1, 'Joomla! Core', 'tuf', 'https://update.joomla.org/cms/', 1, 0), (2, 'Accredited Joomla! Translations', 'collection', 'https://update.joomla.org/language/translationlist_5.xml', 1, 0), (3, 'Joomla! Update Component', 'extension', 'https://update.joomla.org/core/extensions/com_joomlaupdate.xml', 1, 0); diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 435afc0ee21..5b06c905c4a 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -885,6 +885,30 @@ CREATE TABLE IF NOT EXISTS "#__updates" ( COMMENT ON TABLE "#__updates" IS 'Available Updates'; +-- +-- Table structure for table "#__tuf_metadata" +-- + +CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( +"id" serial NOT NULL, +"update_site_id" bigint DEFAULT 0 NOT NULL, +"root" text DEFAULT NULL, +"targets" text DEFAULT NULL, +"snapshot" text DEFAULT NULL, +"timestamp" text DEFAULT NULL, +"mirrors" text DEFAULT NULL, +PRIMARY KEY ("id") +); + +COMMENT ON TABLE "#__tuf_metadata" IS 'Secure TUF Updates'; + +-- +-- Dumping data for table "#__tuf_metadata" +-- + +INSERT INTO "#__tuf_metadata" ("update_site_id", "root") +VALUES (1, '{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2025-03-02T11:22:17Z","keys":{"07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9"}},"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799"}},"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9"}},"31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2"}},"9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9"}}},"roles":{"root":{"keyids":["1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"snapshot":{"keyids":["07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"targets":{"keyids":["31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3"],"threshold":1},"timestamp":{"keyids":["9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e","sig":"2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501"},{"keyid":"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","sig":"8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005"}]}'); + -- -- Table structure for table `#__update_sites` -- @@ -909,7 +933,7 @@ COMMENT ON TABLE "#__update_sites" IS 'Update Sites'; -- INSERT INTO "#__update_sites" ("update_site_id", "name", "type", "location", "enabled", "last_check_timestamp") VALUES -(1, 'Joomla! Core', 'collection', 'https://update.joomla.org/core/list.xml', 1, 0), +(1, 'Joomla! Core', 'tuf', 'https://update.joomla.org/cms/', 1, 0), (2, 'Accredited Joomla! Translations', 'collection', 'https://update.joomla.org/language/translationlist_5.xml', 1, 0), (3, 'Joomla! Update Component', 'extension', 'https://update.joomla.org/core/extensions/com_joomlaupdate.xml', 1, 0); diff --git a/libraries/src/Factory.php b/libraries/src/Factory.php index fba5557f9d0..91f7b1d0731 100644 --- a/libraries/src/Factory.php +++ b/libraries/src/Factory.php @@ -625,7 +625,8 @@ protected static function createContainer(): Container ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Toolbar()) ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAssetRegistry()) ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Router()) - ->registerServiceProvider(new \Joomla\CMS\Service\Provider\User()); + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\User()) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Http()); return $container; } diff --git a/libraries/src/Http/HttpFactory.php b/libraries/src/Http/HttpFactory.php index 7bc49a08596..0dd8192b37a 100644 --- a/libraries/src/Http/HttpFactory.php +++ b/libraries/src/Http/HttpFactory.php @@ -21,7 +21,7 @@ * * @since 3.0.0 */ -class HttpFactory +class HttpFactory implements HttpFactoryInterface { /** * Method to create a JHttp instance. diff --git a/libraries/src/Http/HttpFactoryInterface.php b/libraries/src/Http/HttpFactoryInterface.php new file mode 100644 index 00000000000..c1095f1d3a9 --- /dev/null +++ b/libraries/src/Http/HttpFactoryInterface.php @@ -0,0 +1,15 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Http; + +interface HttpFactoryInterface +{ + public static function getHttp($options = [], $adapters = null); +} diff --git a/libraries/src/Service/Provider/Http.php b/libraries/src/Service/Provider/Http.php new file mode 100644 index 00000000000..08c2f89f3be --- /dev/null +++ b/libraries/src/Service/Provider/Http.php @@ -0,0 +1,48 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Service\Provider; + +use Joomla\CMS\Http\HttpFactoryInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Http\HttpFactory; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Service provider for the application's PSR-3 logger dependency + * + * @since 4.0.0 + */ +class Http implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->alias('http', HttpFactoryInterface::class) + ->share( + HttpFactoryInterface::class, + function (Container $container) { + return new HttpFactory(); + }, + false + ); + } +} diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php new file mode 100644 index 00000000000..34fed7d0e2e --- /dev/null +++ b/libraries/src/TUF/DatabaseStorage.php @@ -0,0 +1,85 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF; + +use Joomla\CMS\Table\Table; +use Joomla\CMS\Table\TableInterface; +use Joomla\CMS\Table\Tuf; +use Tuf\Metadata\StorageBase; + +// phpcs:disable PSR1.Files.SideEffects +\defined('JPATH_PLATFORM') or die; +// phpcs:enable PSR1.Files.SideEffects; + +/** + * @since __DEPLOY_VERSION__ + */ +class DatabaseStorage extends StorageBase +{ + public const METADATA_COLUMNS = ['root', 'targets', 'snapshot', 'timestamp', 'mirrors']; + + /** + * The Tuf table object + * + * @var Table + */ + protected $table; + + protected $container = []; + + /** + * Initialize the DatabaseStorage class + * + * @param TableInterface $table The table object that represents the metadata row + */ + public function __construct(TableInterface $table) + { + $this->table = $table; + + foreach (self::METADATA_COLUMNS as $column) { + if ($this->table->$column === null) { + continue; + } + + $this->write($column, $this->table->$column); + } + } + + + public function read(string $name): ?string + { + return $this->container[$name] ?? null; + } + + public function write(string $name, string $data): void + { + $this->container[$name] = $data; + } + + public function delete(string $name): void + { + unset($this->container[$name]); + } + + public function persist(): bool + { + $data = []; + + foreach (self::METADATA_COLUMNS as $column) { + if (!\array_key_exists($column, $this->container)) { + continue; + } + + $data[$column] = $this->container[$column]; + } + + return $this->table->save($data); + } +} diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php new file mode 100644 index 00000000000..e7dd25fc488 --- /dev/null +++ b/libraries/src/TUF/HttpLoader.php @@ -0,0 +1,48 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF; + +use GuzzleHttp\Promise\Create; +use GuzzleHttp\Promise\PromiseInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Http\Http; +use Joomla\CMS\Http\HttpFactoryInterface; +use Tuf\Exception\RepoFileNotFound; +use Tuf\Loader\LoaderInterface; + +/** + * @since __DEPLOY_VERSION__ + */ +class HttpLoader implements LoaderInterface +{ + public function __construct(private readonly string $repositoryPath) + { + } + + public function load(string $locator, int $maxBytes): PromiseInterface + { + // Get client instance + $httpFactory = Factory::getContainer()->get(HttpFactoryInterface::class); + + /** @var Http $client */ + $client = $httpFactory->getHttp([], 'curl'); + $response = $client->get($this->repositoryPath . $locator); + + if ($response->code !== 200) { + throw new RepoFileNotFound(); + } + + // Rewind to start + $response->getBody()->rewind(); + + // Return response + return Create::promiseFor($response->getBody()); + } +} diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php new file mode 100644 index 00000000000..4faaa34cae8 --- /dev/null +++ b/libraries/src/TUF/TufFetcher.php @@ -0,0 +1,136 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Table\Tuf as MetadataTable; +use Joomla\Database\DatabaseDriver; +use Tuf\Client\Updater; +use Tuf\Exception\Attack\FreezeAttackException; +use Tuf\Exception\Attack\RollbackAttackException; +use Tuf\Exception\Attack\SignatureThresholdException; +use Tuf\Exception\DownloadSizeException; +use Tuf\Exception\MetadataException; +use Tuf\Loader\SizeCheckingLoader; + +// phpcs:disable PSR1.Files.SideEffects +\defined('JPATH_PLATFORM') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * @since __DEPLOY_VERSION__ + */ +class TufFetcher +{ + /** + * The table object holding the metadata + * + * @var MetadataTable + */ + private MetadataTable $metadataTable; + + /** + * The repository base url + * + * @var mixed + */ + private string $repositoryUrl; + + /** + * The database driver + * + * @var DatabaseDriver + */ + protected DatabaseDriver $db; + + /** + * Validating updates with TUF + * + * @param MetadataTable $metadataTable The table object holding the metadata + * @param string $repositoryUrl The base repo URL + */ + public function __construct(MetadataTable $metadataTable, string $repositoryUrl, DatabaseDriver $db = null) + { + $this->metadataTable = $metadataTable; + $this->repositoryUrl = $repositoryUrl; + $this->db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); + } + + + /** + * Checks for updates and writes it into the database if they are valid. Then it gets the targets.json content and + * returns it + * + * @return mixed Returns the targets.json if the validation is successful, otherwise null + */ + public function getValidUpdate() + { + $httpLoader = new HttpLoader($this->repositoryUrl); + $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); + + $storage = new DatabaseStorage($this->metadataTable); + + $updater = new Updater( + $sizeCheckingLoader, + $storage + ); + + $app = Factory::getApplication(); + + try { + try { + // Refresh the data if needed, it will be written inside the DB, then we fetch it afterwards and return it to + // the caller + $updater->refresh(); + + // Persist the data as it was correctly fetched and verified + $storage->persist(); + + return $storage->read('targets'); + } catch (\Exception $e) { + if (JDEBUG && $message = $e->getMessage()) { + $app->enqueueMessage(Text::sprintf('JLIB_INSTALLER_TUF_DEBUG_MESSAGE', $message), 'error'); + } + throw $e; + } + } catch (DownloadSizeException $e) { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); + } catch (MetadataException $e) { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_INVALID_METADATA'), 'error'); + } catch (FreezeAttackException $e) { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_FREEZE_ATTACK'), 'error'); + } catch (RollbackAttackException $e) { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_ROLLBACK_ATTACK'), 'error'); + } catch (SignatureThresholdException $e) { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD'), 'error'); + } + + $this->rollBackTufMetadata(); + } + + /** + * When the validation fails, for example when one file is written but the others don't, we roll back everything + * + * @return void + */ + private function rollBackTufMetadata() + { + $db = $this->db; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__tuf_metadata')) + ->set($db->quoteName('snapshot') . ' = NULL') + ->set($db->quoteName('targets') . ' = NULL') + ->set($db->quoteName('timestamp') . ' = NULL'); + + $db->setQuery($query)->execute(); + } +} diff --git a/libraries/src/Table/Tuf.php b/libraries/src/Table/Tuf.php new file mode 100644 index 00000000000..21b89fd2601 --- /dev/null +++ b/libraries/src/Table/Tuf.php @@ -0,0 +1,34 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Table; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * TUF map table + * + * @since __DEPLOY_VERSION__ + */ +class Tuf extends Table +{ + /** + * Constructor + * + * @param \Joomla\Database\DatabaseDriver $db A database connector object + * + * @since __DEPLOY_VERSION__ + */ + public function __construct($db) + { + parent::__construct('#__tuf_metadata', 'id', $db); + } +} diff --git a/libraries/src/Updater/Adapter/CollectionAdapter.php b/libraries/src/Updater/Adapter/CollectionAdapter.php index d52c65c6eaf..81458ddfa1d 100644 --- a/libraries/src/Updater/Adapter/CollectionAdapter.php +++ b/libraries/src/Updater/Adapter/CollectionAdapter.php @@ -135,7 +135,7 @@ public function _startElement($parser, $name, $attrs = []) } } - $client = ApplicationHelper::getClientInfo($attrs['CLIENT'], 1); + $client = ApplicationHelper::getClientInfo($attrs['CLIENT'], true); if (isset($client->id)) { $attrs['CLIENT_ID'] = $client->id; diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php new file mode 100644 index 00000000000..d8def598d20 --- /dev/null +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -0,0 +1,206 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater\Adapter; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +use Joomla\CMS\Application\ApplicationHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Table\Table; +use Joomla\CMS\Table\Tuf as MetadataTable; +use Joomla\CMS\TUF\TufFetcher; +use Joomla\CMS\Updater\ConstraintChecker; +use Joomla\CMS\Updater\UpdateAdapter; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Tuf\Exception\MetadataException; + +/** + * TUF Update Adapter Class + * + * @since __DEPLOY_VERSION__ + */ +class TufAdapter extends UpdateAdapter +{ + /** + * Finds an update. + * + * @param array $options Update options. + * + * @return array|boolean Array containing the array of update sites and array of updates. False on failure + * + * @since __DEPLOY_VERSION__ + */ + public function findUpdate($options) + { + $updates = []; + $targets = $this->getUpdateTargets($options); + + if ($targets) { + foreach ($targets as $target) { + $updateTable = Table::getInstance('update'); + $updateTable->set('update_site_id', $options['update_site_id']); + + $updateTable->bind($target); + + $updates[] = $updateTable; + } + } + + return ['update_sites' => [], 'updates' => $updates]; + } + + /** + * Finds targets. + * + * @param array $options Update options. + * + * @return array|boolean Array containing the array of update sites and array of updates. False on failure + * + * @since __DEPLOY_VERSION__ + */ + public function getUpdateTargets($options) + { + $versions = []; + + /** @var MetadataTable $metadataTable */ + $metadataTable = new MetadataTable(Factory::getDbo()); + $metadataTable->load(['update_site_id' => $options['update_site_id']]); + + $tufFetcher = new TufFetcher($metadataTable, $options['location']); + $metaData = $tufFetcher->getValidUpdate(); + + $metaData = json_decode($metaData, true); + + if (!isset($metaData["signed"]["targets"])) { + return false; + } + + foreach ($metaData["signed"]["targets"] as $filename => $target) { + $version = $this->processTufTarget($filename, $target); + + if (!$version) { + continue; + } + + $version['detailsurl'] = $options['location']; + + $versions[] = $version; + } + + // We only want the latest version we support + usort($versions, function ($a, $b) { + return version_compare($b['version'], $a['version']); + }); + + $checker = new ConstraintChecker(); + + foreach ($versions as $version) { + if ($checker->check($version)) { + return [$version]; + } + } + + return false; + } + + protected function processTufTarget(string $filename, array $target): bool|array + { + $resolver = new OptionsResolver(); + + try { + $this->configureUpdateOptions($resolver); + $customKeys = $resolver->getDefinedOptions(); + } catch (\Exception $e) { + return false; + } + + $values = []; + + if (!isset($target["hashes"])) { + throw new MetadataException("No trusted hashes are available for '$filename'"); + } + + foreach ($customKeys as $key) { + if (isset($target["custom"][$key])) { + $values[$key] = $target["custom"][$key]; + } + } + + if (isset($values['client']) && \is_string($values['client'])) { + $client = ApplicationHelper::getClientInfo($values['client'], true); + + if (\is_object($client)) { + $values['client'] = $client->id; + } + } + + if (isset($values['infourl']['url'])) { + $values['infourl'] = $values['infourl']['url']; + } + + try { + $values = $resolver->resolve($values); + } catch (\Exception $e) { + return false; + } + + return $values; + } + + /** + * Configures default values or pass arguments to params + * + * @param OptionsResolver $resolver The OptionsResolver for the params + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configureUpdateOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'name' => null, + 'description' => '', + 'element' => '', + 'type' => null, + 'client' => 0, + 'version' => "1", + 'data' => '', + 'detailsurl' => '', + 'infourl' => '', + 'downloads' => [], + 'targetplatform' => new \StdClass(), + 'php_minimum' => null, + 'channel' => null, + 'supported_databases' => new \StdClass(), + 'stability' => '', + ] + ) + ->setAllowedTypes('version', 'string') + ->setAllowedTypes('name', 'string') + ->setAllowedTypes('element', 'string') + ->setAllowedTypes('data', 'string') + ->setAllowedTypes('description', 'string') + ->setAllowedTypes('type', 'string') + ->setAllowedTypes('detailsurl', 'string') + ->setAllowedTypes('infourl', 'string') + ->setAllowedTypes('client', 'int') + ->setAllowedTypes('downloads', 'array') + ->setAllowedTypes('targetplatform', 'array') + ->setAllowedTypes('php_minimum', 'string') + ->setAllowedTypes('channel', 'string') + ->setAllowedTypes('supported_databases', 'array') + ->setAllowedTypes('stability', 'string') + ->setRequired(['version']); + } +} diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php new file mode 100644 index 00000000000..0717a5e03e2 --- /dev/null +++ b/libraries/src/Updater/ConstraintChecker.php @@ -0,0 +1,193 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Version; + +/** + * ConstrainChecker Class + * + * @since __DEPLOY_VERSION__ + */ +class ConstraintChecker +{ + /** + * Checks whether the passed constraints are matched + * + * @param array $candidate The provided constraints to be checked + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function check(array $candidate) + { + if (!isset($candidate['targetplatform'])) { + // targetplatform is required + return false; + } + + // Check targetplatform + if (!$this->checkTargetplatform($candidate['targetplatform'])) { + return false; + } + + // Check php_minimum, assume true when not set + if ( + isset($candidate['php_minimum']) + && !$this->checkPhpMinimum($candidate['php_minimum']) + ) { + return false; + } + + // Check supported databases, assume true when not set + if ( + isset($candidate['supported_databases']) + && !$this->checkSupportedDatabases($candidate['supported_databases']) + ) { + return false; + } + + // Check stability, assume true when not set + if ( + isset($candidate['stability']) + && !$this->checkStability($candidate['stability']) + ) { + return false; + } + + return true; + } + + /** + * Check the targetPlatform + * + * @param array $targetPlatform + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function checkTargetplatform(array $targetPlatform) + { + // Lower case and remove the exclamation mark + $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); + + // Check that the product matches and that the version matches (optionally a regexp) + if ( + $product === $targetPlatform["name"] + && preg_match('/^' . $targetPlatform["version"] . '/', JVERSION) + ) { + return true; + } + + return false; + } + + /** + * Check the minimum PHP version + * + * @param string $phpMinimum The minimum php version to check + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function checkPhpMinimum(string $phpMinimum) + { + // Check if PHP version supported via tag + return version_compare(PHP_VERSION, $phpMinimum, '>='); + } + + /** + * Check the supported databases and versions + * + * @param array $supportedDatabases array of supported databases and versions + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function checkSupportedDatabases(array $supportedDatabases) + { + $db = Factory::getDbo(); + $dbType = strtolower($db->getServerType()); + $dbVersion = $db->getVersion(); + + // MySQL and MariaDB use the same database driver but not the same version numbers + if ($dbType === 'mysql') { + // Check whether we have a MariaDB version string and extract the proper version from it + if (stripos($dbVersion, 'mariadb') !== false) { + // MariaDB: Strip off any leading '5.5.5-', if present + $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); + $dbType = 'mariadb'; + } + } + + // Do we have an entry for the database? + if (!empty($supportedDatabases["$dbType"])) { + $minimumVersion = $supportedDatabases["$dbType"]; + + return version_compare($dbVersion, $minimumVersion, '>='); + } + + return false; + } + + /** + * Check the stability + * + * @param string $stability Stability to check + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function checkStability(string $stability) + { + $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + + $stabilityInt = $this->stabilityToInteger($stability); + + if (($stabilityInt < $minimumStability)) { + return false; + } + + return true; + } + + /** + * Converts a tag to numeric stability representation. If the tag doesn't represent a known stability level (one of + * dev, alpha, beta, rc, stable) it is ignored. + * + * @param string $tag The tag string, e.g. dev, alpha, beta, rc, stable + * + * @return integer + * + * @since __DEPLOY_VERSION__ + */ + protected function stabilityToInteger($tag) + { + $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); + + if (\defined($constant)) { + return \constant($constant); + } + + return Updater::STABILITY_STABLE; + } +} diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 15c592ece43..775921c5972 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -16,6 +16,8 @@ use Joomla\CMS\Log\Log; use Joomla\CMS\Object\LegacyErrorHandlingTrait; use Joomla\CMS\Object\LegacyPropertyManagementTrait; +use Joomla\CMS\Table\Tuf as TufMetadata; +use Joomla\CMS\TUF\TufFetcher; use Joomla\CMS\Version; use Joomla\Registry\Registry; @@ -203,7 +205,7 @@ class Update protected $currentUpdate; /** - * Object containing the latest update data which meets the PHP and DB version requirements + * Object containing the latest update data which meets the requirements * * @var \stdClass * @since 3.0.0 @@ -233,6 +235,14 @@ class Update */ protected $minimum_stability = Updater::STABILITY_STABLE; + /** + * Current release channel + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $channel; + /** * Array with compatible versions used by the pre-update check * @@ -380,6 +390,19 @@ public function _endElement($parser, $name) $otherUpdateInfo->php->used = PHP_VERSION; } + $channelMatch = false; + + // Check if the release channel matches, assume true if tag isn't present + if (!$this->channel || !isset($this->currentUpdate->channel) || preg_match('/' . $this->channel . '/', $this->currentUpdate->channel->_data)) { + $channelMatch = true; + } + + if (!$channelMatch) { + $otherUpdateInfo->channel = new \stdClass(); + $otherUpdateInfo->channel->required = $this->currentUpdate->channel->_data; + $otherUpdateInfo->channel->used = $this->channel; + } + $dbMatch = false; // Check if DB & version is supported via tag, assume supported if tag isn't present @@ -423,7 +446,7 @@ public function _endElement($parser, $name) $stabilityMatch = false; } - if ($phpMatch && $stabilityMatch && $dbMatch) { + if ($phpMatch && $stabilityMatch && $dbMatch && $channelMatch) { if (!empty($this->currentUpdate->downloadurl) && !empty($this->currentUpdate->downloadurl->_data)) { $this->compatibleVersions[] = $this->currentUpdate->version->_data; } @@ -500,7 +523,92 @@ public function _characterData($parser, $data) } /** - * Loads an XML file from a URL. + * Loads update information from a TUF repo. + * + * + * @param TufMetadata $metadataTable The metadata table + * @param string $url The repo url + * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} + * @param string $channel The update channel + * + * @return boolean True on success + * + * @since __DEPLOY_VERSION__ + */ + public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) + { + $tufFetcher = new TufFetcher( + $metadataTable, + $url + ); + + $metaData = $tufFetcher->getValidUpdate(); + + $data = json_decode($metaData, true); + $constraintChecker = new ConstraintChecker(); + + foreach ($data['signed']['targets'] as $target) { + if (!$constraintChecker->check($target['custom'])) { + continue; + } + + if (!empty($target['custom']['downloads'])) { + $this->compatibleVersions[] = $target['custom']['version']; + } + + // Check if this target is newer than the current version + if (isset($this->latest) && version_compare($target['custom']['version'], $this->latest->version, '<')) { + continue; + } + + $this->latest = new \stdClass(); + + foreach ($target['custom'] as $key => $val) { + $this->latest->$key = $val; + } + + $this->downloadSources = []; + + if (!empty($this->latest->downloads)) { + foreach ($this->latest->downloads as $download) { + $source = new DownloadSource(); + + foreach ($download as $key => $sourceUrl) { + $key = strtolower($key); + $source->$key = $sourceUrl; + } + + $this->downloadSources[] = $source; + } + } + + $this->client = $this->latest->client; + + foreach ($target['hashes'] as $hashAlgorithm => $hashSum) { + $this->$hashAlgorithm = (object) ['_data' => $hashSum]; + } + } + + // If the latest item is set then we transfer it to where we want to + if (isset($this->latest)) { + foreach ($this->downloadSources as $source) { + $this->downloadurl = (object) [ + '_data' => $source->url, + 'type' => $source->type, + 'format' => $source->format, + ]; + + break; + } + + unset($this->latest); + } + + return true; + } + + /** + * Loads a XML file from a URL. * * @param string $url The URL. * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} @@ -509,7 +617,7 @@ public function _characterData($parser, $data) * * @since 1.7.0 */ - public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE) + public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) { $version = new Version(); $httpOption = new Registry(); @@ -530,6 +638,7 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE) } $this->minimum_stability = $minimumStability; + $this->channel = $channel; $this->xmlParser = xml_parser_create(''); xml_set_object($this->xmlParser, $this); diff --git a/libraries/src/Updater/Updater.php b/libraries/src/Updater/Updater.php index 30aafda50e4..bae83362917 100644 --- a/libraries/src/Updater/Updater.php +++ b/libraries/src/Updater/Updater.php @@ -260,7 +260,7 @@ private function getUpdateObjectsForSite($updateSite, $minimumStability = self:: // Get the update information from the remote update XML document /** @var UpdateAdapter $adapter */ - $adapter = $this->_adapters[ $updateSite['type']]; + $adapter = $this->_adapters[$updateSite['type']]; $update_result = $adapter->findUpdate($updateSite); // Version comparison operator. diff --git a/package-lock.json b/package-lock.json index 3b39a84b12b..d681f8c9588 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "joomla", - "version": "5.0.1", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "joomla", - "version": "5.0.1", + "version": "5.1.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/tests/System/fixtures/tuf/invalidMetadata.json b/tests/System/fixtures/tuf/invalidMetadata.json new file mode 100644 index 00000000000..f266c5a1409 --- /dev/null +++ b/tests/System/fixtures/tuf/invalidMetadata.json @@ -0,0 +1,117 @@ +{ + "root": { + "signed": { + "_type": "root", + "spec_version": "1.0", + "version": 3, + "expires": "2024-12-30T13:31:20Z", + "keys": { + "1e456d8b1aebbf1812f8181b8ffb30100864210ff203eec1a32faf72cc5921e8": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "cf4408fde3f3db32e1fd26dc4d3ae0eb00d0461aa22be34ccb8f3b863b69e56d" + } + }, + "6e6ea0f74918cff8deb1dfd5bfa5471c71a210106604081df0696cb6bc793bfc": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "c8e4c29b16f04419a54b72628de0e3e98f554a744d276dc1bb6a5410ac712c33" + } + }, + "788c596eb4b3d51f00a5bac53c904e6830d9a75d47fd37fab6bce13811268e5a": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "9fa0fc53c0466a73c6e585ea13e32b6c61bb807259f15f60ec458d944d6d69ea" + } + }, + "baf247cd493b5a0190304b26cf099fbaf6c6f4dd1c5a749b4265ecd8a7ae2ced": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "99f06efa082f1be2a9cacc8803e2cbe814a37e5f25b7289f08c7989d9616e6d4" + } + }, + "d363dcdfdbab98bc60e367e83c0b338e89c27bbdcd20517b1315d24b41c254dd": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "03c41b4aa5eb4e759d20b2e6e72084cdacd038dce3a9add8f8e450d7060f88ab" + } + }, + "df88d5d857df7ab7018a72ca5c033b3f419b8156cecf9f8a247be0b5d6d9d30e": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "e8d1faa248040a41a668fae3d2a5e9c56c2178afd9b2b8d09641bda8b4e8a7ee" + } + } + }, + "roles": { + "root": { + "keyids": [ + "df88d5d857df7ab7018a72ca5c033b3f419b8156cecf9f8a247be0b5d6d9d30e" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "baf247cd493b5a0190304b26cf099fbaf6c6f4dd1c5a749b4265ecd8a7ae2ced", + "ID" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "d363dcdfdbab98bc60e367e83c0b338e89c27bbdcd20517b1315d24b41c254dd", + "1e456d8b1aebbf1812f8181b8ffb30100864210ff203eec1a32faf72cc5921e8" + ], + "threshold": 2 + }, + "timestamp": { + "keyids": [ + "6e6ea0f74918cff8deb1dfd5bfa5471c71a210106604081df0696cb6bc793bfc", + "788c596eb4b3d51f00a5bac53c904e6830d9a75d47fd37fab6bce13811268e5a" + ], + "threshold": 1 + } + }, + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "df88d5d857df7ab7018a72ca5c033b3f419b8156cecf9f8a247be0b5d6d9d30e", + "sig": "3a5582627333a6cc9a2cc0acc471623f5456d28d3ca45494d4286fd671f0dd1ee1a711e249859000ea856d6bd2bd3576811035c88772b9fb62185189ca1d5605" + } + ] + }, + "snapshot": {}, + "timestamp": {}, + "targets": {} +} diff --git a/tests/System/fixtures/tuf/validMetadata.json b/tests/System/fixtures/tuf/validMetadata.json new file mode 100644 index 00000000000..c8b73173b5f --- /dev/null +++ b/tests/System/fixtures/tuf/validMetadata.json @@ -0,0 +1,109 @@ +{ + "root": { + "signed": { + "_type": "root", + "spec_version": "1.0", + "version": 2, + "expires": "2025-03-02T11:22:17Z", + "keys": { + "07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9" + } + }, + "1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799" + } + }, + "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9" + } + }, + "31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2" + } + }, + "9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9" + } + } + }, + "roles": { + "root": { + "keyids": [ + "1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669", + "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f", + "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc" + ], + "threshold": 1 + } + }, + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e", + "sig": "2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501" + }, + { + "keyid": "1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669", + "sig": "8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005" + } + ] + }, + "snapshot": {}, + "timestamp": {}, + "targets": {} +} diff --git a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js new file mode 100644 index 00000000000..bc81d06389e --- /dev/null +++ b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js @@ -0,0 +1,29 @@ +describe('Test the update retrieval logic', () => { + beforeEach(() => { + cy.doAdministratorLogin(); + }); + + afterEach(() => { + cy.db_setValidTufRoot(); + }); + + it('Can fetch available updates with valid metadata', () => { + cy.db_setValidTufRoot(); + + cy.visit('/administrator/index.php?option=com_joomlaupdate'); + + cy.get('#toolbar joomla-toolbar-button[task="update.purge"] button').click(); + + cy.get('#system-message-container').contains('Checked for updates.').should('exist'); + }); + + it('Receives error fetching available updates with invalid metadata', () => { + cy.db_setInvalidTufRoot(); + + cy.visit('/administrator/index.php?option=com_joomlaupdate'); + + cy.get('#confirmButton').click(); + + cy.get('#system-message-container').contains('Update not possible because the offered update does not have enough signatures.').should('exist'); + }); +}); diff --git a/tests/System/support/commands/db.js b/tests/System/support/commands/db.js index 8fd42d1d42c..bde2979a269 100644 --- a/tests/System/support/commands/db.js +++ b/tests/System/support/commands/db.js @@ -1,3 +1,6 @@ +import invalidTufMetadata from '../../fixtures/tuf/invalidMetadata.json'; +import validTufMetadata from '../../fixtures/tuf/validMetadata.json'; + /** * The global cached default categories */ @@ -599,3 +602,45 @@ Cypress.Commands.add('db_getUserId', () => { return id[0].id; }); }); + +/** + * Inserts an invalid tuf metadata set + * + * @returns integer + */ +Cypress.Commands.add('db_setInvalidTufRoot', () => { + cy.task('queryDB', 'DELETE FROM #__tuf_metadata WHERE id = 1'); + cy.task('queryDB', 'DELETE FROM #__updates WHERE update_site_id = 1'); + cy.task('queryDB', createInsertQuery( + 'tuf_metadata', + { + id: 1, + update_site_id: 1, + root: JSON.stringify(invalidTufMetadata.root), + targets: '', + snapshot: '', + timestamp: '', + }, + )); +}); + +/** + * Inserts an invalid tuf metadata set + * + * @returns integer + */ +Cypress.Commands.add('db_setValidTufRoot', () => { + cy.task('queryDB', 'DELETE FROM #__tuf_metadata WHERE id = 1'); + cy.task('queryDB', 'DELETE FROM #__updates WHERE update_site_id = 1'); + cy.task('queryDB', createInsertQuery( + 'tuf_metadata', + { + id: 1, + update_site_id: 1, + root: JSON.stringify(validTufMetadata.root), + targets: '', + snapshot: '', + timestamp: '', + }, + )); +}); diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php new file mode 100644 index 00000000000..7055902ebb1 --- /dev/null +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -0,0 +1,226 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\Updater; + +use Joomla\CMS\Factory; +use Joomla\CMS\Updater\ConstraintChecker; +use Joomla\CMS\Version; +use Joomla\Database\DatabaseDriver; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for Constraint Checker. + * + * @package Joomla.UnitTest + * @subpackage Updater + * @since __DEPLOY_VERSION__ + */ +class ConstraintCheckerTest extends UnitTestCase +{ + /** + * @var ConstraintChecker + * @since __DEPLOY_VERSION__ + */ + protected $checker; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function setUp(): void + { + $this->checker = new ConstraintChecker(); + } + + /** + * Overrides the parent tearDown method. + * + * @return void + * + * @see \PHPUnit\Framework\TestCase::tearDown() + * @since __DEPLOY_VERSION__ + */ + protected function tearDown(): void + { + unset($this->checker); + parent::tearDown(); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testCheckMethodReturnsFalseIfPlatformIsMissing() + { + $constraint = []; + $this->assertFalse($this->checker->check($constraint)); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testCheckMethodReturnsTrueIfPlatformIsOnlyConstraint() + { + $constraint = ['targetplatform' => (array) ["name" => "joomla", "version" => JVERSION]]; + $this->assertTrue($this->checker->check($constraint)); + } + + /** + * Tests the checkSupportedDatabases method + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @dataProvider supportedDatabasesDataProvider + */ + public function testCheckSupportedDatabases($currentDatabase, $supportedDatabases, $expectedResult) + { + $dbMock = $this->createMock(DatabaseDriver::class); + $dbMock->method('getServerType')->willReturn($currentDatabase['type']); + $dbMock->method('getVersion')->willReturn($currentDatabase['version']); + Factory::$database = $dbMock; + + $method = $this->getPublicMethod('checkSupportedDatabases'); + $result = $method->invoke($this->checker, $supportedDatabases); + + $this->assertSame($expectedResult, $result); + } + + /** + * Tests the checkPhpMinimum method + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @dataProvider targetplatformDataProvider + */ + public function testCheckPhpMinimumReturnFalseForFuturePhp() + { + $method = $this->getPublicMethod('checkPhpMinimum'); + + $this->assertFalse($method->invoke($this->checker, '99.9.9')); + } + + /** + * Tests the checkTargetplatform method + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @dataProvider targetplatformDataProvider + */ + public function testCheckTargetplatform($targetPlatform, $expectedResult) + { + $method = $this->getPublicMethod('checkTargetplatform'); + $result = $method->invoke($this->checker, $targetPlatform); + + $this->assertSame($expectedResult, $result); + } + + /** + * Data provider for testCheckSupportedDatabases method + * + * @since __DEPLOY_VERSION__ + * + * @return array[] + */ + protected function supportedDatabasesDataProvider() + { + return [ + [ + ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], + true, + ], + [ + ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], + true, + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], + true, + ], + [ + ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], + (array) ['mysql' => '5.8', 'mariadb' => '10.3'], + false, + ], + [ + ['type' => 'pgsql', 'version' => '14.3'], + (array) ['mysql' => '5.8', 'mariadb' => '10.3'], + false, + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (array) ['mysql' => '5.6', 'mariadb' => '10.4'], + false, + ], + [ + ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], + true, + ], + ]; + } + + /** + * Data provider for testCheckTargetplatform method + * + * @since __DEPLOY_VERSION__ + * + * @return array[] + */ + protected function targetplatformDataProvider() + { + return [ + [(array) ["name" => "foobar", "version" => "1.*"], false], + [(array) ["name" => "foobar", "version" => "4.*"], false], + [(array) ["name" => "joomla", "version" => "1.*"], false], + [(array) ["name" => "joomla", "version" => "3.1.2"], false], + [(array) ["name" => "joomla", "version" => "6.*"], false], + [(array) ["name" => "joomla", "version" => ""], true], + [(array) ["name" => "joomla", "version" => ".*"], true], + [(array) ["name" => "joomla", "version" => JVERSION], true], + [(array) ["name" => "joomla", "version" => "5.*"], true], + ]; + } + + /** + * Internal helper method to get access to protected methods + * + * @since __DEPLOY_VERSION__ + * + * @param $method + * + * @return \ReflectionMethod + * @throws \ReflectionException + */ + protected function getPublicMethod($method) + { + $reflectionClass = new \ReflectionClass($this->checker); + $method = $reflectionClass->getMethod($method); + $method->setAccessible(true); + + return $method; + } +} diff --git a/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php new file mode 100644 index 00000000000..3718394a23d --- /dev/null +++ b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php @@ -0,0 +1,167 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\Updater; + +use Joomla\CMS\Updater\Adapter\TufAdapter; +use Joomla\Tests\Unit\UnitTestCase; +use Joomla\Utilities\ArrayHelper; +use Tuf\Exception\MetadataException; + +/** + * Test class for Tuf Adapter. + * + * @package Joomla.UnitTest + * @subpackage Updater + * @since __DEPLOY_VERSION__ + */ +class TufAdapterTest extends UnitTestCase +{ + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testProcessTufTargetThrowsExceptionIfHashesAreMissing() + { + $this->expectException(MetadataException::class); + $this->expectExceptionMessage("No trusted hashes are available for 'nohash.json'"); + + $object = $this->getMockBuilder(TufAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $method = $this->getPublicMethod($object, 'processTufTarget'); + $method->invoke($object, 'nohash.json', []); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testProcesstuftargetAssignsCustomTargetKeys() + { + $object = $this->getMockBuilder(TufAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + $method = $this->getPublicMethod($object, 'processTufTarget'); + $result = $method->invoke($object, 'targets.json', $this->getMockTarget([ + 'custom' => [ + 'name' => 'Testupdate', + 'version' => '1.2.3', + ], + ])); + + $this->assertSame('Testupdate', $result['name']); + $this->assertSame('1.2.3', $result['version']); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testProcesstuftargetAssignsClientId() + { + $object = $this->getMockBuilder(TufAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + + $method = $this->getPublicMethod($object, 'processTufTarget'); + $result = $method->invoke($object, 'targets.json', $this->getMockTarget([ + 'client' => 'site', + ])); + + $this->assertSame(0, $result['client']); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testProcesstuftargetAssignsInfoUrl() + { + $object = $this->getMockBuilder(TufAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + + + $method = $this->getPublicMethod($object, 'processTufTarget'); + $result = $method->invoke($object, 'targets.json', $this->getMockTarget([ + 'custom' => [ + 'infourl' => [ + 'url' => 'https://example.org', + ], + ], + ])); + + $this->assertSame('https://example.org', $result['infourl']); + } + + /** + * Internal helper method to get access to protected methods + * + * @since __DEPLOY_VERSION__ + * + * @param $object + * @param $method + * + * @return \ReflectionMethod + * @throws \ReflectionException + */ + protected function getPublicMethod($object, $method) + { + $reflectionClass = new \ReflectionClass($object); + $method = $reflectionClass->getMethod($method); + $method->setAccessible(true); + + return $method; + } + + /** + * Target override data + * + * @param array $overrides + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + protected function getMockTarget(array $overrides) + { + return ArrayHelper::mergeRecursive( + [ + 'hashes' => [ + 'sha128' => '', + ], + 'custom' => [ + 'name' => 'Joomla', + 'type' => 'file', + 'version' => '1.2.3', + 'targetplatform' => [ + 'name' => 'joomla', + 'version' => '(5\.[0-4])|^(4\.4)', + ], + 'php_minimum' => '8.1.0', + 'channel' => '5.x', + 'stability' => 'stable', + 'supported_databases' => [ + 'mariadb' => '10.4', + ], + ], + ], + $overrides + ); + } +} diff --git a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php new file mode 100644 index 00000000000..0f736acd89d --- /dev/null +++ b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php @@ -0,0 +1,164 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Tuf; + +use Joomla\CMS\Table\Tuf; +use Joomla\CMS\TUF\DatabaseStorage; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for DatabaseStorage + * + * @package Joomla.UnitTest + * @subpackage Tuf + * @since __DEPLOY_VERSION__ + */ +class DatabaseStorageTest extends UnitTestCase +{ + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testContructorWritesColumnMetadataToInternalStorage() + { + $table = $this->getTableMock(['root' => 'rootfoo']); + $object = new DatabaseStorage($table); + + $this->assertEquals('rootfoo', $this->getInternalStorageValue($object)['root']); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testContructorIgnoresNonMetadataColumns() + { + $table = $this->getTableMock(['foobar' => 'aaa']); + $object = new DatabaseStorage($table); + + $this->assertArrayNotHasKey('foobar', $this->getInternalStorageValue($object)); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testReadReturnsStorageValueForExistingColumns() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foobar'])); + $this->assertEquals('foobar', $object->read('root')); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testReadReturnsNullForNonexistentColumns() + { + $object = new DatabaseStorage($this->getTableMock([])); + $this->assertNull($object->read('foobar')); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testWriteUpdatesGivenInternalStorageValue() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); + $object->write('root', 'bar'); + + $this->assertEquals('bar', $this->getInternalStorageValue($object)['root']); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testWriteCreatesNewInternalStorageValue() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); + $object->write('targets', 'bar'); + + $this->assertEquals('bar', $this->getInternalStorageValue($object)['targets']); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testDeleteRemovesRowFromInternalStorage() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); + $object->delete('root'); + + $this->assertArrayNotHasKey('root', $this->getInternalStorageValue($object)); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testPersistUpdatesTableObjectState() + { + $tableMock = $this->getTableMock(['root' => 'foo', 'targets' => 'Joomla', 'nonexistent' => 'value']); + + $tableMock + ->expects($this->once()) + ->method('save') + ->with(['root' => 'foo', 'targets' => 'Joomla']) + ->willReturn(true); + + $object = new DatabaseStorage($tableMock); + $this->assertTrue($object->persist()); + } + + /** + * @param array $mockData + * + * @since __DEPLOY_VERSION__ + * + * @return Tuf|(Tuf&\PHPUnit\Framework\MockObject\MockObject)|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getTableMock(array $mockData) + { + $table = $this->createMock(Tuf::class); + + // Write mock data to mock table + foreach (DatabaseStorage::METADATA_COLUMNS as $column) { + $table->$column = (!empty($mockData[$column])) ? $mockData[$column] : null; + } + + return $table; + } + + /** + * @param $class + * + * @since __DEPLOY_VERSION__ + * + * @return mixed + */ + protected function getInternalStorageValue($class) + { + $reflectionProperty = new \ReflectionProperty(DatabaseStorage::class, 'container'); + + return $reflectionProperty->getValue($class); + } +} diff --git a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php new file mode 100644 index 00000000000..21619dffb5a --- /dev/null +++ b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php @@ -0,0 +1,131 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Tuf; + +use Joomla\CMS\Factory; +use Joomla\CMS\Http\Http; +use Joomla\CMS\Http\HttpFactoryInterface; +use Joomla\CMS\TUF\HttpLoader; +use Joomla\Http\Response; +use Joomla\Tests\Unit\UnitTestCase; +use Laminas\Diactoros\Stream; +use Tuf\Exception\RepoFileNotFound; + +/** + * Test class for HttpLoader + * + * @package Joomla.UnitTest + * @subpackage Tuf + * @since __DEPLOY_VERSION__ + */ +class HttpLoaderTest extends UnitTestCase +{ + protected const REPOPATHMOCK = 'https://example.org/tuftest/'; + + protected HttpLoader $object; + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testLoaderQueriesCorrectUrl() + { + $responseBody = $this->createMock(Stream::class); + + Factory::getContainer()->set( + HttpFactoryInterface::class, + $this->getHttpFactoryMock(200, $responseBody, 'root.json') + ); + + $this->object->load('root.json', 2048); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testLoaderForwardsReturnedBodyFromHttpClient() + { + $responseBody = $this->createMock(Stream::class); + + Factory::getContainer()->set( + HttpFactoryInterface::class, + $this->getHttpFactoryMock(200, $responseBody, 'root.json') + ); + + $this->assertSame( + $responseBody, + $this->object->load('root.json', 2048)->wait() + ); + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testLoaderThrowsExceptionForNon200Response() + { + $this->expectException(RepoFileNotFound::class); + + $responseBody = $this->createMock(Stream::class); + + Factory::getContainer()->set( + HttpFactoryInterface::class, + $this->getHttpFactoryMock(400, $responseBody, 'root.json') + ); + + $this->object->load('root.json', 2048); + } + + /** + * @param int $responseCode + * @param Stream $responseBody + * @param string $expectedFile + * + * @since __DEPLOY_VERSION__ + * + * @return \PHPUnit\Framework\MockObject\MockObject|(\stdClass&\PHPUnit\Framework\MockObject\MockObject) + */ + protected function getHttpFactoryMock(int $responseCode, Stream $responseBody, string $expectedFile) + { + $responseMock = $this->createMock(Response::class); + $responseMock->method('__get')->with('code')->willReturn($responseCode); + $responseMock->method('getBody')->willReturn($responseBody); + + $httpClientMock = $this->createMock(Http::class); + $httpClientMock->expects($this->once()) + ->method('get') + ->with(self::REPOPATHMOCK . $expectedFile) + ->willReturn($responseMock); + + $httpFactoryMock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getHttp']) + ->getMock(); + $httpFactoryMock->method('getHttp')->willReturn($httpClientMock); + + return $httpFactoryMock; + } + + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function setUp(): void + { + $this->object = new HttpLoader(self::REPOPATHMOCK); + + parent::setUp(); + } +}