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'] = '
';
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();
+ }
+}