From f76534064ec549fcb3501e432ee1f779501a7982 Mon Sep 17 00:00:00 2001 From: Franciska Perisa Date: Sun, 20 Mar 2022 14:25:05 +0100 Subject: [PATCH 001/126] Set up tuf_updates mysql queries --- .../sql/updates/mysql/4.2.0-2022-03-20.sql | 19 ++++++++++++++++ installation/sql/mysql/base.sql | 22 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql new file mode 100644 index 00000000000..c84ad612f21 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -0,0 +1,19 @@ +-- +-- Table structure for table `#__tuf_updates` +-- + +CREATE TABLE IF NOT EXISTS `#__tuf_updates` ( + `id` int NOT NULL AUTO_INCREMENT, + `extension_id` int DEFAULT 0, + `version` varchar(32) DEFAULT '', + `timestamp_json` text NOT NULL, + `root_json` text NOT NULL, + `target_json` text NOT NULL, + `snapshot_json` text NOT NULL, + `mirrors_json` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; + +-- -------------------------------------------------------- +INSERT INTO `tupsb_tuf_updates` (`extension_id`, `root_json`) +SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `tupsb_extensions` WHERE `type`='file' AND `element`='joomla'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index dd7ab9e5a77..59dccdefd4e 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -836,7 +836,27 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( `changelogurl` text, `extra_query` varchar(1000) DEFAULT '', PRIMARY KEY (`update_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Available Updates'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Available Updates';-- -------------------------------------------------------- + +-- +-- Table structure for table `#__tuf_updates` +-- + +CREATE TABLE IF NOT EXISTS `#__tuf_updates` ( + `id` int NOT NULL AUTO_INCREMENT, + `extension_id` int DEFAULT 0, + `timestamp_json` text DEFAULT '', + `root_json` text NOT NULL, + `target_json` text DEFAULT '', + `snapshot_json` text DEFAULT '', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; + +-- +-- Dumping data for table `#__tuf_updates` +-- +INSERT INTO `#__tuf_updates` (`extension_id`, `root_json`) +SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; -- -------------------------------------------------------- From fd82ae8d8ccf093ac3e098fd6f78b9b8aec92dc3 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sun, 20 Mar 2022 14:48:29 +0100 Subject: [PATCH 002/126] Fix table structure for TUF metadatas --- .../sql/updates/mysql/4.2.0-2022-03-20.sql | 7 +++---- installation/sql/mysql/base.sql | 17 ++++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index c84ad612f21..842a756818c 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -1,11 +1,10 @@ -- --- Table structure for table `#__tuf_updates` +-- Table structure for table `#__tuf_metadata` -- -CREATE TABLE IF NOT EXISTS `#__tuf_updates` ( +CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `version` varchar(32) DEFAULT '', `timestamp_json` text NOT NULL, `root_json` text NOT NULL, `target_json` text NOT NULL, @@ -15,5 +14,5 @@ CREATE TABLE IF NOT EXISTS `#__tuf_updates` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; -- -------------------------------------------------------- -INSERT INTO `tupsb_tuf_updates` (`extension_id`, `root_json`) +INSERT INTO `#__tuf_metadata` (`extension_id`, `root_json`) SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `tupsb_extensions` WHERE `type`='file' AND `element`='joomla'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 59dccdefd4e..8cedde33cb6 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -836,26 +836,29 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( `changelogurl` text, `extra_query` varchar(1000) DEFAULT '', PRIMARY KEY (`update_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Available Updates';-- -------------------------------------------------------- +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Available Updates'; + +-- -------------------------------------------------------- -- -- Table structure for table `#__tuf_updates` -- -CREATE TABLE IF NOT EXISTS `#__tuf_updates` ( +CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `timestamp_json` text DEFAULT '', + `timestamp_json` text NOT NULL, `root_json` text NOT NULL, - `target_json` text DEFAULT '', - `snapshot_json` text DEFAULT '', + `target_json` text NOT NULL, + `snapshot_json` text NOT NULL, + `mirrors_json` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; -- --- Dumping data for table `#__tuf_updates` +-- Dumping data for table `#__tuf_metadata` -- -INSERT INTO `#__tuf_updates` (`extension_id`, `root_json`) +INSERT INTO `#__tuf_metadata` (`extension_id`, `root_json`) SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; -- -------------------------------------------------------- From f43a95ed11805c081b2e77cd438ba4b29a43073a Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sun, 20 Mar 2022 14:53:00 +0100 Subject: [PATCH 003/126] Change ordering of timestamp --- .../components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql | 2 +- installation/sql/mysql/base.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index 842a756818c..ffee0bd2029 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -5,10 +5,10 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `timestamp_json` text NOT NULL, `root_json` text NOT NULL, `target_json` text NOT NULL, `snapshot_json` text NOT NULL, + `timestamp_json` text NOT NULL, `mirrors_json` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 8cedde33cb6..d8e93662916 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -847,10 +847,10 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `timestamp_json` text NOT NULL, `root_json` text NOT NULL, `target_json` text NOT NULL, `snapshot_json` text NOT NULL, + `timestamp_json` text NOT NULL, `mirrors_json` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; From 68ec0b1b8deb9670338fd5cea714324147628a88 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sun, 20 Mar 2022 14:59:04 +0100 Subject: [PATCH 004/126] Add allow null --- .../com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql | 10 +++++----- installation/sql/mysql/base.sql | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index ffee0bd2029..735f26797b3 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -5,11 +5,11 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `root_json` text NOT NULL, - `target_json` text NOT NULL, - `snapshot_json` text NOT NULL, - `timestamp_json` text NOT NULL, - `mirrors_json` text NOT NULL, + `root_json` text NULL, + `target_json` text NULL, + `snapshot_json` text NULL, + `timestamp_json` text NULL, + `mirrors_json` text NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index d8e93662916..408089985f1 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -847,11 +847,11 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `root_json` text NOT NULL, - `target_json` text NOT NULL, - `snapshot_json` text NOT NULL, - `timestamp_json` text NOT NULL, - `mirrors_json` text NOT NULL, + `root_json` text NULL, + `target_json` text NULL, + `snapshot_json` text NULL, + `timestamp_json` text NULL, + `mirrors_json` text NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; From 37d23412b77b0d77e0eaab773fcd907529788db2 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sun, 20 Mar 2022 15:00:55 +0100 Subject: [PATCH 005/126] Fix null values --- .../com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql | 10 +++++----- installation/sql/mysql/base.sql | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index 735f26797b3..007d7a69130 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -5,11 +5,11 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `root_json` text NULL, - `target_json` text NULL, - `snapshot_json` text NULL, - `timestamp_json` text NULL, - `mirrors_json` text NULL, + `root_json` text DEFAULT NULL, + `target_json` text DEFAULT NULL, + `snapshot_json` text DEFAULT NULL, + `timestamp_json` text DEFAULT NULL, + `mirrors_json` text DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 408089985f1..af4a39a2fa5 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -847,11 +847,11 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `root_json` text NULL, - `target_json` text NULL, - `snapshot_json` text NULL, - `timestamp_json` text NULL, - `mirrors_json` text NULL, + `root_json` text DEFAULT NULL, + `target_json` text DEFAULT NULL, + `snapshot_json` text DEFAULT NULL, + `timestamp_json` text DEFAULT NULL, + `mirrors_json` text DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; From 351bb65e4c2d782ff1880d96d5470942dd1f4c70 Mon Sep 17 00:00:00 2001 From: Franciska Perisa Date: Sun, 20 Mar 2022 16:05:40 +0100 Subject: [PATCH 006/126] Add TUF databaseStorage --- libraries/src/TUF/DatabaseStorage.php | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 libraries/src/TUF/DatabaseStorage.php diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php new file mode 100644 index 00000000000..e34be138c21 --- /dev/null +++ b/libraries/src/TUF/DatabaseStorage.php @@ -0,0 +1,57 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF; + +use Joomla\CMS\Table\Table; + +\defined('JPATH_PLATFORM') or die; + +/** + * @since VERSION + */ +class DatabaseStorage implements \ArrayAccess +{ + public function __construct(Table $table) + { + // $this->table = new \Joomla\CMS\Table\Extension($this->getDbo()); + // $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return file_exists($this->pathWithBasePath($offset)); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return file_get_contents($this->pathWithBasePath($offset)); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + file_put_contents($this->pathWithBasePath($offset), $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + @unlink($this->pathWithBasePath($offset)); + } +} From e69ec811e2b094e93f748cd95b2a51139910e3a7 Mon Sep 17 00:00:00 2001 From: Franciska Perisa Date: Sun, 20 Mar 2022 16:18:27 +0100 Subject: [PATCH 007/126] Create tuf table --- libraries/src/TUF/DatabaseStorage.php | 10 ++++++--- libraries/src/Table/Tuf.php | 31 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 libraries/src/Table/Tuf.php diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index e34be138c21..400523a5888 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -17,10 +17,11 @@ */ class DatabaseStorage implements \ArrayAccess { - public function __construct(Table $table) + private Table $table; + + public function __construct() { - // $this->table = new \Joomla\CMS\Table\Extension($this->getDbo()); - // $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); + $this->table = new \Joomla\CMS\Table\Tuf(); } /** @@ -36,6 +37,7 @@ public function offsetExists($offset) */ public function offsetGet($offset) { + // return $this->table->load(''); return file_get_contents($this->pathWithBasePath($offset)); } @@ -44,6 +46,7 @@ public function offsetGet($offset) */ public function offsetSet($offset, $value) { + // return $this->table->store(''); file_put_contents($this->pathWithBasePath($offset), $value); } @@ -52,6 +55,7 @@ public function offsetSet($offset, $value) */ public function offsetUnset($offset) { + // return $this->table->delete(''); @unlink($this->pathWithBasePath($offset)); } } diff --git a/libraries/src/Table/Tuf.php b/libraries/src/Table/Tuf.php new file mode 100644 index 00000000000..0d4b7f9d178 --- /dev/null +++ b/libraries/src/Table/Tuf.php @@ -0,0 +1,31 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Table; + +\defined('JPATH_PLATFORM') or die; + +/** + * 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); + } +} From 7571eada96e2e6e263aabe2a52e1e1c65bac8144 Mon Sep 17 00:00:00 2001 From: Franciska Perisa Date: Sun, 20 Mar 2022 16:49:57 +0100 Subject: [PATCH 008/126] Add TUF exceptions --- .../TUF/Exception/InvalidRoleException.php | 20 +++++++++++++++++++ .../TUF/Exception/RoleNotFoundException.php | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 libraries/src/TUF/Exception/InvalidRoleException.php create mode 100644 libraries/src/TUF/Exception/RoleNotFoundException.php diff --git a/libraries/src/TUF/Exception/InvalidRoleException.php b/libraries/src/TUF/Exception/InvalidRoleException.php new file mode 100644 index 00000000000..b892f37992c --- /dev/null +++ b/libraries/src/TUF/Exception/InvalidRoleException.php @@ -0,0 +1,20 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF\Exception; + +\defined('JPATH_PLATFORM') or die; + +/** + * Exception class defining an Invalid Role error + * + * @since __DEPLOY_VERSION__ + */ +class InvalidRoleException extends \RuntimeException +{ +} diff --git a/libraries/src/TUF/Exception/RoleNotFoundException.php b/libraries/src/TUF/Exception/RoleNotFoundException.php new file mode 100644 index 00000000000..2960147defa --- /dev/null +++ b/libraries/src/TUF/Exception/RoleNotFoundException.php @@ -0,0 +1,20 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF\Exception; + +\defined('JPATH_PLATFORM') or die; + +/** + * Exception class defining that the Role could not be found + * + * @since __DEPLOY_VERSION__ + */ +class RoleNotFoundException extends \RuntimeException +{ +} From 702d21201ae70c5e7f17754e123f4ffa98d7a345 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sun, 20 Mar 2022 16:50:30 +0100 Subject: [PATCH 009/126] Implement all ArrayAccess methods --- libraries/src/TUF/DatabaseStorage.php | 111 ++++++++++++++++++-------- 1 file changed, 79 insertions(+), 32 deletions(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 400523a5888..6a6fa028997 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -2,60 +2,107 @@ /** * Joomla! Content Management System * - * @copyright (C) 2019 Open Source Matters, Inc. + * @copyright (C) 2022 Open Source Matters, Inc. * @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\Tuf; +use Joomla\Database\DatabaseDriver; \defined('JPATH_PLATFORM') or die; /** - * @since VERSION + * @since __DEPLOY_VERSION__ */ class DatabaseStorage implements \ArrayAccess { - private Table $table; + /** + * The Tuf table object + * + * @var Table + */ + protected Table $table; - public function __construct() + /** + * Initialize the DatabaseStorage class + * + * @param DatabaseDriver $db + * @param integer $extensionId + */ + public function __construct(DatabaseDriver $db, int $extensionId) { - $this->table = new \Joomla\CMS\Table\Tuf(); + $this->table = new Tuf($db); + + $this->table->load($extensionId); } - /** - * {@inheritdoc} - */ - public function offsetExists($offset) - { - return file_exists($this->pathWithBasePath($offset)); - } + /** + * {@inheritdoc} + */ + public function offsetExists(mixed $offset): bool + { + $column = $this->getCleanColumn($offset); - /** - * {@inheritdoc} - */ - public function offsetGet($offset) - { - // return $this->table->load(''); - return file_get_contents($this->pathWithBasePath($offset)); - } + return substr($offset, -5) === '.json' && $this->table->hasField($column) && strlen($this->table->$column); + } - /** - * {@inheritdoc} - */ - public function offsetSet($offset, $value) - { - // return $this->table->store(''); - file_put_contents($this->pathWithBasePath($offset), $value); - } + /** + * {@inheritdoc} + */ + public function offsetGet($offset): mixed + { + if (!$this->offsetExists($offset)) + { + throw new \Exception('Table column does not exists'); + } + + $column = $this->getCleanColumn($offset); + + return $this->table->$column; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value): void + { + if (!$this->offsetExists($offset)) + { + throw new \Exception('Table column does not exists'); + } + + $this->table->$offset = $value; + + $this->table->store(); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset): void + { + if (!$this->offsetExists($offset)) + { + throw new \Exception('Table column does not exists'); + } + + $this->table->$offset = ''; + + $this->table->store(); + } /** - * {@inheritdoc} + * Convert file names to table columns + * + * @param string $name + * + * @return string */ - public function offsetUnset($offset) + protected function getCleanColumn($name): string { - // return $this->table->delete(''); - @unlink($this->pathWithBasePath($offset)); + return str_replace('.', '_', $name); } } From e83a573a46870e19ea19f3369cf9c7120dd1e844 Mon Sep 17 00:00:00 2001 From: Franciska Perisa Date: Sun, 20 Mar 2022 17:28:33 +0100 Subject: [PATCH 010/126] Set TUF exception --- libraries/src/TUF/DatabaseStorage.php | 7 ++++--- .../TUF/Exception/InvalidRoleException.php | 20 ------------------- .../TUF/Exception/RoleNotFoundException.php | 2 +- 3 files changed, 5 insertions(+), 24 deletions(-) delete mode 100644 libraries/src/TUF/Exception/InvalidRoleException.php diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 6a6fa028997..98119e5dd14 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -10,6 +10,7 @@ use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Tuf; +use Joomla\CMS\TUF\Exception\RoleNotFoundException; use Joomla\Database\DatabaseDriver; \defined('JPATH_PLATFORM') or die; @@ -56,7 +57,7 @@ public function offsetGet($offset): mixed { if (!$this->offsetExists($offset)) { - throw new \Exception('Table column does not exists'); + throw new RoleNotFoundException; } $column = $this->getCleanColumn($offset); @@ -71,7 +72,7 @@ public function offsetSet($offset, $value): void { if (!$this->offsetExists($offset)) { - throw new \Exception('Table column does not exists'); + throw new RoleNotFoundException; } $this->table->$offset = $value; @@ -86,7 +87,7 @@ public function offsetUnset($offset): void { if (!$this->offsetExists($offset)) { - throw new \Exception('Table column does not exists'); + throw new RoleNotFoundException; } $this->table->$offset = ''; diff --git a/libraries/src/TUF/Exception/InvalidRoleException.php b/libraries/src/TUF/Exception/InvalidRoleException.php deleted file mode 100644 index b892f37992c..00000000000 --- a/libraries/src/TUF/Exception/InvalidRoleException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\CMS\TUF\Exception; - -\defined('JPATH_PLATFORM') or die; - -/** - * Exception class defining an Invalid Role error - * - * @since __DEPLOY_VERSION__ - */ -class InvalidRoleException extends \RuntimeException -{ -} diff --git a/libraries/src/TUF/Exception/RoleNotFoundException.php b/libraries/src/TUF/Exception/RoleNotFoundException.php index 2960147defa..fa90a250158 100644 --- a/libraries/src/TUF/Exception/RoleNotFoundException.php +++ b/libraries/src/TUF/Exception/RoleNotFoundException.php @@ -15,6 +15,6 @@ * * @since __DEPLOY_VERSION__ */ -class RoleNotFoundException extends \RuntimeException +class RoleNotFoundException extends \Exception { } From 798bc72fc27af5982de12ba1668b6c343b4c9f60 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Wed, 23 Mar 2022 19:30:09 +0100 Subject: [PATCH 011/126] implement basic version of the tuf validator --- composer.json | 3 +- installation/sql/mysql/base.sql | 23 + libraries/src/TUF/DatabaseStorage.php | 109 ++++ .../TUF/Exception/RoleNotFoundException.php | 20 + libraries/src/TUF/TufValidation.php | 154 +++++ .../DurableStorageAccessValidator.php | 97 +++ .../src/Client/DurableStorage/FileStorage.php | 82 +++ .../src/TUF/src/Client/GuzzleFileFetcher.php | 172 +++++ .../src/Client/RepoFileFetcherInterface.php | 56 ++ .../src/TUF/src/Client/ResponseStream.php | 166 +++++ .../src/TUF/src/Client/SignatureVerifier.php | 153 +++++ libraries/src/TUF/src/Client/Updater.php | 600 ++++++++++++++++++ .../src/TUF/src/Constraints/Collection.php | 17 + .../src/Constraints/CollectionValidator.php | 30 + libraries/src/TUF/src/DelegatedRole.php | 96 +++ .../src/Exception/Attack/AttackException.php | 15 + .../Attack/DenialOfServiceAttackException.php | 10 + .../Attack/FreezeAttackException.php | 10 + .../Exception/Attack/InvalidHashException.php | 63 ++ .../Attack/RollbackAttackException.php | 10 + .../Attack/SignatureThresholdException.php | 10 + .../src/Exception/DownloadSizeException.php | 11 + .../src/TUF/src/Exception/FormatException.php | 30 + .../TUF/src/Exception/InvalidKeyException.php | 10 + .../TUF/src/Exception/MetadataException.php | 12 + .../TUF/src/Exception/NotFoundException.php | 32 + .../TUF/src/Exception/RepoFileNotFound.php | 11 + .../TUF/src/Exception/RoleExistsException.php | 10 + .../src/TUF/src/Exception/TufException.php | 10 + libraries/src/TUF/src/Helper/Clock.php | 22 + libraries/src/TUF/src/JsonNormalizer.php | 118 ++++ libraries/src/TUF/src/Key.php | 131 ++++ libraries/src/TUF/src/KeyDB.php | 125 ++++ .../src/TUF/src/Metadata/ConstraintsTrait.php | 173 +++++ libraries/src/TUF/src/Metadata/Factory.php | 71 +++ .../TUF/src/Metadata/FileInfoMetadataBase.php | 27 + .../src/TUF/src/Metadata/MetadataBase.php | 255 ++++++++ .../src/TUF/src/Metadata/RootMetadata.php | 100 +++ .../src/TUF/src/Metadata/SnapshotMetadata.php | 72 +++ .../src/TUF/src/Metadata/TargetsMetadata.php | 209 ++++++ .../TUF/src/Metadata/TimestampMetadata.php | 39 ++ .../Metadata/Verifier/FileInfoVerifier.php | 48 ++ .../src/Metadata/Verifier/RootVerifier.php | 63 ++ .../Metadata/Verifier/SnapshotVerifier.php | 79 +++ .../src/Metadata/Verifier/TargetsVerifier.php | 51 ++ .../Metadata/Verifier/TimestampVerifier.php | 40 ++ .../Verifier/TrustedAuthorityTrait.php | 78 +++ .../Metadata/Verifier/UniversalVerifier.php | 92 +++ .../src/Metadata/Verifier/VerifierBase.php | 140 ++++ libraries/src/TUF/src/Role.php | 122 ++++ libraries/src/TUF/src/RoleDB.php | 116 ++++ libraries/src/Table/Tuf.php | 31 + 52 files changed, 4223 insertions(+), 1 deletion(-) create mode 100644 libraries/src/TUF/DatabaseStorage.php create mode 100644 libraries/src/TUF/Exception/RoleNotFoundException.php create mode 100644 libraries/src/TUF/TufValidation.php create mode 100644 libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php create mode 100644 libraries/src/TUF/src/Client/DurableStorage/FileStorage.php create mode 100644 libraries/src/TUF/src/Client/GuzzleFileFetcher.php create mode 100644 libraries/src/TUF/src/Client/RepoFileFetcherInterface.php create mode 100644 libraries/src/TUF/src/Client/ResponseStream.php create mode 100644 libraries/src/TUF/src/Client/SignatureVerifier.php create mode 100644 libraries/src/TUF/src/Client/Updater.php create mode 100644 libraries/src/TUF/src/Constraints/Collection.php create mode 100644 libraries/src/TUF/src/Constraints/CollectionValidator.php create mode 100644 libraries/src/TUF/src/DelegatedRole.php create mode 100644 libraries/src/TUF/src/Exception/Attack/AttackException.php create mode 100644 libraries/src/TUF/src/Exception/Attack/DenialOfServiceAttackException.php create mode 100644 libraries/src/TUF/src/Exception/Attack/FreezeAttackException.php create mode 100644 libraries/src/TUF/src/Exception/Attack/InvalidHashException.php create mode 100644 libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php create mode 100644 libraries/src/TUF/src/Exception/Attack/SignatureThresholdException.php create mode 100644 libraries/src/TUF/src/Exception/DownloadSizeException.php create mode 100644 libraries/src/TUF/src/Exception/FormatException.php create mode 100644 libraries/src/TUF/src/Exception/InvalidKeyException.php create mode 100644 libraries/src/TUF/src/Exception/MetadataException.php create mode 100644 libraries/src/TUF/src/Exception/NotFoundException.php create mode 100644 libraries/src/TUF/src/Exception/RepoFileNotFound.php create mode 100644 libraries/src/TUF/src/Exception/RoleExistsException.php create mode 100644 libraries/src/TUF/src/Exception/TufException.php create mode 100644 libraries/src/TUF/src/Helper/Clock.php create mode 100644 libraries/src/TUF/src/JsonNormalizer.php create mode 100644 libraries/src/TUF/src/Key.php create mode 100644 libraries/src/TUF/src/KeyDB.php create mode 100644 libraries/src/TUF/src/Metadata/ConstraintsTrait.php create mode 100644 libraries/src/TUF/src/Metadata/Factory.php create mode 100644 libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php create mode 100644 libraries/src/TUF/src/Metadata/MetadataBase.php create mode 100644 libraries/src/TUF/src/Metadata/RootMetadata.php create mode 100644 libraries/src/TUF/src/Metadata/SnapshotMetadata.php create mode 100644 libraries/src/TUF/src/Metadata/TargetsMetadata.php create mode 100644 libraries/src/TUF/src/Metadata/TimestampMetadata.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php create mode 100644 libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php create mode 100644 libraries/src/TUF/src/Role.php create mode 100644 libraries/src/TUF/src/RoleDB.php create mode 100644 libraries/src/Table/Tuf.php diff --git a/composer.json b/composer.json index dc3a984f438..da23fceb15a 100644 --- a/composer.json +++ b/composer.json @@ -93,7 +93,8 @@ "ext-gd": "*", "web-auth/webauthn-lib": "2.1.*", "composer/ca-bundle": "^1.2", - "dragonmantank/cron-expression": "^3.1" + "dragonmantank/cron-expression": "^3.1", + "symfony/validator": "^5.4" }, "require-dev": { "phpunit/phpunit": "^8.5", diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index dd7ab9e5a77..e5a85182ec3 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -840,6 +840,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, + `extension_id` int DEFAULT 0, + `root_json` text DEFAULT NULL, + `targets_json` text DEFAULT NULL, + `snapshot_json` text DEFAULT NULL, + `timestamp_json` text DEFAULT NULL, + `mirrors_json` 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` (`extension_id`, `root_json`) +SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; + +-- -------------------------------------------------------- + -- -- Table structure for table `#__update_sites` -- diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php new file mode 100644 index 00000000000..253cec551ae --- /dev/null +++ b/libraries/src/TUF/DatabaseStorage.php @@ -0,0 +1,109 @@ + + * @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\Tuf; +use Joomla\CMS\TUF\Exception\RoleNotFoundException; +use Joomla\Database\DatabaseDriver; + +\defined('JPATH_PLATFORM') or die; + +/** + * @since __DEPLOY_VERSION__ + */ +class DatabaseStorage implements \ArrayAccess +{ + /** + * The Tuf table object + * + * @var Table + */ + protected Table $table; + + /** + * Initialize the DatabaseStorage class + * + * @param DatabaseDriver $db + * @param integer $extensionId + */ + public function __construct(DatabaseDriver $db, int $extensionId) + { + $this->table = new Tuf($db); + + $this->table->load($extensionId); + } + + /** + * {@inheritdoc} + */ + public function offsetExists(mixed $offset): bool + { + $column = $this->getCleanColumn($offset); + + return substr($offset, -5) === '_json' && $this->table->hasField($column) && strlen($this->table->$column); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset): mixed + { + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } + + $column = $this->getCleanColumn($offset); + + return $this->table->$column; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value): void + { + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } + + $this->table->$offset = $value; + + $this->table->store(); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset): void + { + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } + + $this->table->$offset = ''; + + $this->table->store(); + } + + /** + * Convert file names to table columns + * + * @param string $name + * + * @return string + */ + protected function getCleanColumn($name): string + { + return str_replace('.', '_', $name); + } +} diff --git a/libraries/src/TUF/Exception/RoleNotFoundException.php b/libraries/src/TUF/Exception/RoleNotFoundException.php new file mode 100644 index 00000000000..fa90a250158 --- /dev/null +++ b/libraries/src/TUF/Exception/RoleNotFoundException.php @@ -0,0 +1,20 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF\Exception; + +\defined('JPATH_PLATFORM') or die; + +/** + * Exception class defining that the Role could not be found + * + * @since __DEPLOY_VERSION__ + */ +class RoleNotFoundException extends \Exception +{ +} diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php new file mode 100644 index 00000000000..73c9a595bbb --- /dev/null +++ b/libraries/src/TUF/TufValidation.php @@ -0,0 +1,154 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF; + +use JLoader; +use Joomla\CMS\Factory; +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Tuf\Client\GuzzleFileFetcher; +use Tuf\Client\Updater; +use Tuf\Exception\Attack\FreezeAttackException; +use Tuf\Exception\Attack\RollbackAttackException; +use Tuf\Exception\Attack\SignatureThresholdException; +use Tuf\Exception\MetadataException; +use Tuf\JsonNormalizer; + +JLoader::registerNamespace('Tuf', JPATH_ROOT . '/libraries/src/TUF/src'); + +\defined('JPATH_PLATFORM') or die; + +/** + * @since __DEPLOY_VERSION__ + */ +class TufValidation +{ + /** + * The id of the extension to be updated + * + * @var integer + */ + private int $extensionId; + + /** + * The params of the validator + * + * @var mixed + */ + private mixed $params; + + /** + * Validating updates with TUF + * + * @param integer $extensionId The ID of the extension to be checked + * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for + * the update + */ + public function __construct(int $extensionId, mixed $params) + { + $this->extensionId = $extensionId; + + $resolver = new OptionsResolver; + + try + { + $this->configureTufOptions($resolver); + } + catch (\Exception) + { + } + + try + { + $params = $resolver->resolve($params); + } + catch (\Exception $e) + { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) + { + throw $e; + } + } + + $this->params = $params; + } + + /** + * Configures default values or pass arguments to params + * + * @param OptionsResolver $resolver The OptionsResolver for the params + * @return void + */ + protected function configureTufOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'url_prefix' => 'https://raw.githubusercontent.com', + 'metadata_path' => '/joomla/updates/test/repository/', + 'targets_path' => '/targets/', + 'mirrors' => [], + ] + ) + ->setAllowedTypes('url_prefix', 'string') + ->setAllowedTypes('metadata_path', 'string') + ->setAllowedTypes('targets_path', 'string') + ->setAllowedTypes('mirrors', 'array'); + } + + /** + * 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(): mixed + { + $db = Factory::getContainer()->get(DatabaseDriver::class); + + // $db = Factory::getDbo(); + + $fileFetcher = GuzzleFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); + $updater = new Updater( + $fileFetcher, + $this->params['mirrors'], + new DatabaseStorage($db, $this->extensionId) + ); + + 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(); + $query = $db->getQuery(true) + ->select('targets_json') + ->from($db->quoteName('#__tuf_metadata', 'map')) + ->where($db->quoteName('map.id') . ' = :id') + ->bind(':id', $this->extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + $resultArray = (array) $db->loadObject(); + + return JsonNormalizer::decode($resultArray['targets_json']); + } + catch (FreezeAttackException | MetadataException | SignatureThresholdException | RollbackAttackException $e) + { + // When the validation fails, for example when one file is written but the others don't, we roll back everything + // and cancel the update + $query = $db->getQuery(true) + ->delete('#__tuf_metadata') + ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); + $db->setQuery($query); + + return null; + } + } +} diff --git a/libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php b/libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php new file mode 100644 index 00000000000..50da999d9cc --- /dev/null +++ b/libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php @@ -0,0 +1,97 @@ +backend = $backend; + } + + /** + * Verifies that a given offset is valid. + * + * This is meant as a security measure to reduce the likelihood of + * undesired storage behavior. For example, a filesystem storage can't be + * tricked into executing in a different directory. + * + * @param mixed $offset + * The ArrayAccess offset. + * + * @return void + * + * @throws \OutOfBoundsException + * Thrown if the offset is not a string, or if it is not a valid + * filename (characters other than alphanumeric characters, periods, + * underscores, or hyphens). + */ + protected function throwIfInvalidOffset($offset): void + { + //if (! is_string($offset) || !preg_match("|^[\w._-]+$|", $offset)) { + if (! is_string($offset)) { + throw new \OutOfBoundsException("Array offset '$offset' is not a valid durable storage key."); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + $this->throwIfInvalidOffset($offset); + return $this->backend->offsetExists($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + $this->throwIfInvalidOffset($offset); + return $this->backend->offsetGet($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + $this->throwIfInvalidOffset($offset); + // @todo Consider enforcing an application-configurable maximum length + // here. https://github.com/php-tuf/php-tuf/issues/27 + if (! is_string($value)) { + $format = "Cannot store %s at offset $offset: only strings are allowed in durable storage."; + throw new \UnexpectedValueException(sprintf($format, gettype($value))); + } + $this->backend->offsetSet($offset, $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + $this->throwIfInvalidOffset($offset); + $this->backend->offsetUnset($offset); + } +} diff --git a/libraries/src/TUF/src/Client/DurableStorage/FileStorage.php b/libraries/src/TUF/src/Client/DurableStorage/FileStorage.php new file mode 100644 index 00000000000..271a5c97094 --- /dev/null +++ b/libraries/src/TUF/src/Client/DurableStorage/FileStorage.php @@ -0,0 +1,82 @@ +basePath = $basePath; + } + + /** + * Returns a full path for an item in the storage. + * + * @param mixed $offset + * The ArrayAccess offset for the item. + * + * @return string + * The full path for the item in the storage. + */ + protected function pathWithBasePath($offset): string + { + return $this->basePath . DIRECTORY_SEPARATOR . $offset; + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return file_exists($this->pathWithBasePath($offset)); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return file_get_contents($this->pathWithBasePath($offset)); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + file_put_contents($this->pathWithBasePath($offset), $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + @unlink($this->pathWithBasePath($offset)); + } +} diff --git a/libraries/src/TUF/src/Client/GuzzleFileFetcher.php b/libraries/src/TUF/src/Client/GuzzleFileFetcher.php new file mode 100644 index 00000000000..163bfb8914b --- /dev/null +++ b/libraries/src/TUF/src/Client/GuzzleFileFetcher.php @@ -0,0 +1,172 @@ +client = $client; + $this->metadataPrefix = $metadataPrefix; + $this->targetsPrefix = $targetsPrefix; + } + + /** + * Creates an instance of this class with a specific base URI. + * + * @param string $baseUri + * The base URI from which to fetch files. + * @param string $metadataPrefix + * (optional) The path prefix for metadata. Defaults to '/metadata/'. + * @param string $targetsPrefix + * (optional) The path prefix for targets. Defaults to '/targets/'. + * + * @return static + * A new instance of this class. + */ + public static function createFromUri(string $baseUri, string $metadataPrefix = '/metadata/', string $targetsPrefix = '/targets/'): self + { + $client = new Client(['base_uri' => $baseUri]); + return new static($client, $metadataPrefix, $targetsPrefix); + } + + /** + * {@inheritDoc} + */ + public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface + { + return $this->fetchFile($this->metadataPrefix . $fileName, $maxBytes); + } + + /** + * {@inheritDoc} + * + * @param array $options + * (optional) Additional request options to pass to the Guzzle client. + * See \GuzzleHttp\RequestOptions. + * @param string $url + * (optional) An arbitrary URL from which the target should be downloaded. + * If passed, takes precedence over $fileName. + */ + public function fetchTarget(string $fileName, int $maxBytes, array $options = [], string $url = null): PromiseInterface + { + $location = $url ?: $this->targetsPrefix . $fileName; + return $this->fetchFile($location, $maxBytes, $options); + } + + /** + * Fetches a file from a URL. + * + * @param string $url + * The URL of the file to fetch. + * @param integer $maxBytes + * The maximum number of bytes to download. + * @param array $options + * (optional) Additional request options to pass to the Guzzle client. + * See \GuzzleHttp\RequestOptions. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * A promise representing the eventual result of the operation. + */ + protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface + { + // Create a progress callback to abort the download if it exceeds + // $maxBytes. This will only work with cURL, so we also verify the + // download size when request is finished. + $progress = function (int $expectedBytes, int $downloadedBytes) use ($url, $maxBytes) { + if ($expectedBytes > $maxBytes || $downloadedBytes > $maxBytes) { + throw new DownloadSizeException("$url exceeded $maxBytes bytes"); + } + }; + $options += [RequestOptions::PROGRESS => $progress]; + + return $this->client->requestAsync('GET', $url, $options) + ->then( + function (ResponseInterface $response) { + return new ResponseStream($response); + }, + $this->onRejected($url) + ); + } + + /** + * Creates a callback function for when the promise is rejected. + * + * @param string $fileName + * The file name being fetched from the remote repo. + * + * @return \Closure + * The callback function. + */ + private function onRejected(string $fileName): \Closure + { + return function (\Throwable $e) use ($fileName) { + if ($e instanceof ClientException) { + if ($e->getCode() === 404) { + throw new RepoFileNotFound("$fileName not found", 0, $e); + } else { + // Re-throwing the original exception will blow away the + // backtrace, so wrap the exception in a more generic one to aid + // in debugging. + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } + throw $e; + }; + } + + /** + * {@inheritDoc} + */ + public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string + { + try { + return $this->fetchMetadata($fileName, $maxBytes)->wait(); + } catch (RepoFileNotFound $exception) { + return null; + } + } +} diff --git a/libraries/src/TUF/src/Client/RepoFileFetcherInterface.php b/libraries/src/TUF/src/Client/RepoFileFetcherInterface.php new file mode 100644 index 00000000000..e776fb51876 --- /dev/null +++ b/libraries/src/TUF/src/Client/RepoFileFetcherInterface.php @@ -0,0 +1,56 @@ +response = $response; + } + + /** + * Returns the response that produced this stream. + * + * @return \Psr\Http\Message\ResponseInterface + * The response. + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->getResponse()->getBody()->__toString(); + } + + /** + * {@inheritDoc} + */ + public function close() + { + $this->getResponse()->getBody()->close(); + } + + /** + * {@inheritDoc} + */ + public function detach() + { + return $this->getResponse()->getBody()->detach(); + } + + /** + * {@inheritDoc} + */ + public function getSize() + { + return $this->getResponse()->getBody()->getSize(); + } + + /** + * {@inheritDoc} + */ + public function tell() + { + return $this->getResponse()->getBody()->tell(); + } + + /** + * {@inheritDoc} + */ + public function eof() + { + return $this->getResponse()->getBody()->eof(); + } + + /** + * {@inheritDoc} + */ + public function isSeekable() + { + return $this->getResponse()->getBody()->isSeekable(); + } + + /** + * {@inheritDoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + return $this->getResponse()->getBody()->seek($offset, $whence); + } + + /** + * {@inheritDoc} + */ + public function rewind() + { + return $this->getResponse()->getBody()->rewind(); + } + + /** + * {@inheritDoc} + */ + public function isWritable() + { + return $this->getResponse()->getBody()->isWritable(); + } + + /** + * {@inheritDoc} + */ + public function write($string) + { + return $this->getResponse()->getBody()->write($string); + } + + /** + * {@inheritDoc} + */ + public function isReadable() + { + return $this->getResponse()->getBody()->isReadable(); + } + + /** + * {@inheritDoc} + */ + public function read($length) + { + return $this->getResponse()->getBody()->read($length); + } + + /** + * {@inheritDoc} + */ + public function getContents() + { + return $this->getResponse()->getBody()->getContents(); + } + + /** + * {@inheritDoc} + */ + public function getMetadata($key = null): string + { + return $this->getResponse()->getBody()->getMetadata($key); + } +} diff --git a/libraries/src/TUF/src/Client/SignatureVerifier.php b/libraries/src/TUF/src/Client/SignatureVerifier.php new file mode 100644 index 00000000000..3395498207d --- /dev/null +++ b/libraries/src/TUF/src/Client/SignatureVerifier.php @@ -0,0 +1,153 @@ +roleDb = $roleDb; + $this->keyDb = $keyDb; + } + + /** + * Creates a SignatureVerifier object from a RootMetadata object. + * + * @param RootMetadata $rootMetadata + * @param bool $allowUntrustedAccess + * + * @return static + */ + public static function createFromRootMetadata(RootMetadata $rootMetadata, bool $allowUntrustedAccess = false): self + { + return new static( + RoleDB::createFromRootMetadata($rootMetadata, $allowUntrustedAccess), + KeyDB::createFromRootMetadata($rootMetadata, $allowUntrustedAccess) + ); + } + + /** + * Checks signatures on a verifiable structure. + * + * @param \Tuf\Metadata\MetadataBase $metadata + * The metadata to check signatures on. + * + * @return void + * + * @throws \Tuf\Exception\Attack\SignatureThresholdException + * Thrown if the signature threshold has not be reached. + */ + public function checkSignatures(MetadataBase $metadata): void + { + $signatures = $metadata->getSignatures(); + + $role = $this->roleDb->getRole($metadata->getRole()); + $needVerified = $role->getThreshold(); + $verifiedKeySignatures = []; + + $canonicalBytes = JsonNormalizer::asNormalizedJson($metadata->getSigned()); + + foreach ($signatures as $signature) + { + // Don't allow the same key to be counted twice. + if ($role->isKeyIdAcceptable($signature['keyid']) && $this->verifySingleSignature($canonicalBytes, $signature)) + { + $verifiedKeySignatures[$signature['keyid']] = true; + } + + // @todo Determine if we should check all signatures and warn for + // bad signatures even if this method returns TRUE because the + // threshold has been met. + // https://github.com/php-tuf/php-tuf/issues/172 + if (count($verifiedKeySignatures) >= $needVerified) + { + break; + } + } + + if (count($verifiedKeySignatures) < $needVerified) + { + throw new SignatureThresholdException("Signature threshold not met on " . $metadata->getRole()); + } + } + + /** + * Verifies a single signature. + * + * @param string $bytes + * The canonical JSON string of the 'signed' section of the given file. + * @param \ArrayAccess $signatureMeta + * The ArrayAccess object of metadata for the signature. Each signature + * metadata contains two elements: + * - keyid: The identifier of the key signing the role data. + * - sig: The hex-encoded signature of the canonical form of the + * metadata for the role. + * + * @return boolean + * TRUE if the signature is valid for $bytes. + */ + private function verifySingleSignature(string $bytes, \ArrayAccess $signatureMeta): bool + { + // Get the pubkey from the key database. + $pubkey = $this->keyDb->getKey($signatureMeta['keyid'])->getPublic(); + + // Encode the pubkey and signature, and check that the signature is + // valid for the given data and pubkey. + $pubkeyBytes = hex2bin($pubkey); + $sigBytes = hex2bin($signatureMeta['sig']); + + // @todo Check that the key type in $signatureMeta is ed25519; return + // false if not. + // https://github.com/php-tuf/php-tuf/issues/168 + return \sodium_crypto_sign_verify_detached($sigBytes, $bytes, $pubkeyBytes); + } + + /** + * Adds a role to the signature verifier. + * + * @param \Tuf\Role $role + */ + public function addRole(Role $role): void + { + if (!$this->roleDb->roleExists($role->getName())) + { + $this->roleDb->addRole($role); + } + } + + /** + * Adds a key to the signature verifier. + * + * @param string $keyId + * @param \Tuf\Key $key + */ + public function addKey(string $keyId, Key $key): void + { + $this->keyDb->addKey($keyId, $key); + } +} diff --git a/libraries/src/TUF/src/Client/Updater.php b/libraries/src/TUF/src/Client/Updater.php new file mode 100644 index 00000000000..d993141105f --- /dev/null +++ b/libraries/src/TUF/src/Client/Updater.php @@ -0,0 +1,600 @@ +repoFileFetcher = $repoFileFetcher; + $this->mirrors = $mirrors; + $this->durableStorage = new DurableStorageAccessValidator($durableStorage); + $this->clock = new Clock(); + $this->metadataFactory = new MetadataFactory($this->durableStorage); + } + + /** + * Gets the type for the file name. + * + * @param string $fileName + * The file name. + * + * @return string + * The type. + */ + private static function getFileNameType(string $fileName): string + { + $parts = explode('.', $fileName); + array_pop($parts); + return array_pop($parts); + } + + /** + * @todo Add docs. See python comments: + * https://github.com/theupdateframework/tuf/blob/1cf085a360aaad739e1cc62fa19a2ece270bb693/tuf/client/updater.py#L999 + * https://github.com/php-tuf/php-tuf/issues/162 + * @todo The Python implementation has an optional flag to "unsafely update + * root if necessary". Do we need it? + * https://github.com/php-tuf/php-tuf/issues/21 + * + * @param bool $force + * (optional) If false, return early if this updater has already been + * refreshed. Defaults to false. + * + * @return boolean + * TRUE if the data was successfully refreshed. + * + * @see https://github.com/php-tuf/php-tuf/issues/21 + * + * @throws \Tuf\Exception\MetadataException + * Throw if an upated root metadata file is not valid. + * @throws \Tuf\Exception\Attack\FreezeAttackException + * Throw if a freeze attack is detected. + * @throws \Tuf\Exception\Attack\RollbackAttackException + * Throw if a rollback attack is detected. + * @throws \Tuf\Exception\Attack\SignatureThresholdException + * Thrown if the signature threshold has not be reached. + */ + public function refresh(bool $force = false): bool + { + if ($force) { + $this->isRefreshed = false; + $this->metadataExpiration = null; + } + if ($this->isRefreshed) { + return true; + } + + // § 5.1 + $this->metadataExpiration = $this->getUpdateStartTime(); + + // § 5.2 + /** @var \Tuf\Metadata\RootMetadata $rootData */ + $rootData = $this->metadataFactory->load('root'); + + $this->signatureVerifier = SignatureVerifier::createFromRootMetadata($rootData); + $this->universalVerifier = new UniversalVerifier($this->metadataFactory, $this->signatureVerifier, $this->metadataExpiration); + + // § 5.3 + $this->updateRoot($rootData); + + // § 5.4 + $newTimestampData = $this->updateTimestamp(); + + $snapshotInfo = $newTimestampData->getFileMetaInfo('snapshot.json'); + $snapShotVersion = $snapshotInfo['version']; + + // § 5.5 + if ($rootData->supportsConsistentSnapshots()) { + // § 5.5.1 + $newSnapshotContents = $this->fetchFile("$snapShotVersion.snapshot.json"); + + $newSnapshotData = SnapshotMetadata::createFromJson($newSnapshotContents); + + $this->universalVerifier->verify(SnapshotMetadata::TYPE, $newSnapshotData); + + // § 5.5.7 + // TODO: here change .json to _json + $this->durableStorage['snapshot_json'] = $newSnapshotContents; + } else { + // @todo Add support for not using consistent snapshots in + // https://github.com/php-tuf/php-tuf/issues/97 + throw new \UnexpectedValueException("Currently only repos using consistent snapshots are supported."); + } + + // § 5.6 + if ($rootData->supportsConsistentSnapshots()) { + $this->fetchAndVerifyTargetsMetadata('targets'); + } else { + // @todo Add support for not using consistent snapshots in + // https://github.com/php-tuf/php-tuf/issues/97 + throw new \UnexpectedValueException("Currently only repos using consistent snapshots are supported."); + } + $this->isRefreshed = true; + return true; + } + + /** + * Updates the timestamp role, per section 5.3 of the TUF spec. + */ + private function updateTimestamp(): TimestampMetadata + { + // § 5.4.1 + $newTimestampContents = $this->fetchFile('timestamp.json'); + $newTimestampData = TimestampMetadata::createFromJson($newTimestampContents); + + $this->universalVerifier->verify(TimestampMetadata::TYPE, $newTimestampData); + + // § 5.4.5: Persist timestamp metadata + // TODO: here change .json to _json + $this->durableStorage['timestamp_json'] = $newTimestampContents; + + return $newTimestampData; + } + + + + /** + * Updates the root metadata if needed. + * + * @param \Tuf\Metadata\RootMetadata $rootData + * The current root metadata. + * + * @return void + *@throws \Tuf\Exception\Attack\FreezeAttackException + * Throw if a freeze attack is detected. + * @throws \Tuf\Exception\Attack\RollbackAttackException + * Throw if a rollback attack is detected. + * @throws \Tuf\Exception\Attack\SignatureThresholdException + * Thrown if an updated root file is not signed with the need signatures. + * + * @throws \Tuf\Exception\MetadataException + * Throw if an upated root metadata file is not valid. + */ + private function updateRoot(RootMetadata &$rootData): void + { + // § 5.3.1 needs no action, since we currently require consistent + // snapshots. + $rootsDownloaded = 0; + $originalRootData = $rootData; + // § 5.3.2 and 5.3.3 + $nextVersion = $rootData->getVersion() + 1; + while ($nextRootContents = $this->repoFileFetcher->fetchMetadataIfExists("$nextVersion.root.json", static::MAXIMUM_DOWNLOAD_BYTES)) { + $rootsDownloaded++; + if ($rootsDownloaded > static::MAX_ROOT_DOWNLOADS) { + throw new DenialOfServiceAttackException("The maximum number root files have already been downloaded: " . static::MAX_ROOT_DOWNLOADS); + } + $nextRoot = RootMetadata::createFromJson($nextRootContents); + $this->universalVerifier->verify(RootMetadata::TYPE, $nextRoot); + + // § 5.3.6 Needs no action. The expiration of the new (intermediate) + // root metadata file does not matter yet, because we will check for + // it in § 5.3.10. + // § 5.3.7 + $rootData = $nextRoot; + + // § 5.3.8 + // TODO: here change .json to _json + $this->durableStorage['root_json'] = $nextRootContents; + // § 5.3.9: repeat from § 5.3.2. + $nextVersion = $rootData->getVersion() + 1; + } + // § 5.3.10 + RootVerifier::checkFreezeAttack($rootData, $this->metadataExpiration); + + // § 5.3.11: Delete the trusted timestamp and snapshot files if either + // file has rooted keys. + if ($rootsDownloaded && + (static::hasRotatedKeys($originalRootData, $rootData, 'timestamp') + || static::hasRotatedKeys($originalRootData, $rootData, 'snapshot'))) { + unset($this->durableStorage['timestamp_json'], $this->durableStorage['snapshot_json']); + } + // § 5.3.12 needs no action because we currently require consistent + // snapshots. + } + + /** + * Determines if the new root metadata has rotated keys for a role. + * + * @param \Tuf\Metadata\RootMetadata $previousRootData + * The previous root metadata. + * @param \Tuf\Metadata\RootMetadata $newRootData + * The new root metadta. + * @param string $role + * The role to check for rotated keys. + * + * @return boolean + * True if the keys for the role have been rotated, otherwise false. + */ + private static function hasRotatedKeys(RootMetadata $previousRootData, RootMetadata $newRootData, string $role): bool + { + $previousRole = $previousRootData->getRoles()[$role] ?? null; + $newRole = $newRootData->getRoles()[$role] ?? null; + if ($previousRole && $newRole) { + return !$previousRole->keysMatch($newRole); + } + return false; + } + + /** + * Synchronously fetches a file from the remote repo. + * + * @param string $fileName + * The name of the file to fetch. + * @param integer $maxBytes + * (optional) The maximum number of bytes to download. + * + * @return string + * The contents of the fetched file. + */ + private function fetchFile(string $fileName, int $maxBytes = self::MAXIMUM_DOWNLOAD_BYTES): string + { + return $this->repoFileFetcher->fetchMetadata($fileName, $maxBytes) + ->then(function (StreamInterface $data) use ($fileName, $maxBytes) { + $this->checkLength($data, $maxBytes, $fileName); + return $data; + }) + ->wait(); + } + + /** + * Verifies the length of a data stream. + * + * @param \Psr\Http\Message\StreamInterface $data + * The data stream to check. + * @param int $maxBytes + * The maximum acceptable length of the stream, in bytes. + * @param string $fileName + * The filename associated with the stream. + * + * @throws \Tuf\Exception\DownloadSizeException + * If the stream's length exceeds $maxBytes in size. + */ + protected function checkLength(StreamInterface $data, int $maxBytes, string $fileName): void + { + $error = new DownloadSizeException("$fileName exceeded $maxBytes bytes"); + $size = $data->getSize(); + + if (isset($size)) { + if ($size > $maxBytes) { + throw $error; + } + } else { + // @todo Handle non-seekable streams. + // https://github.com/php-tuf/php-tuf/issues/169 + $data->rewind(); + $data->read($maxBytes); + + // If we reached the end of the stream, we didn't exceed the + // maximum number of bytes. + if ($data->eof() === false) { + throw $error; + } + $data->rewind(); + } + } + + /** + * Verifies a stream of data against a known TUF target. + * + * @param string $target + * The path of the target file. Needs to be known to the most recent + * targets metadata downloaded in ::refresh(). + * @param \Psr\Http\Message\StreamInterface $data + * A stream pointing to the downloaded target data. + * + * @throws \Tuf\Exception\MetadataException + * If the target has no trusted hash(es). + * @throws \Tuf\Exception\Attack\InvalidHashException + * If the data stream does not match the known hash(es) for the target. + */ + protected function verify(string $target, StreamInterface $data): void + { + $this->refresh(); + + $targetsMetadata = $this->getMetadataForTarget($target); + if ($targetsMetadata === null) { + throw new NotFoundException($target, 'Target'); + } + $maxBytes = $targetsMetadata->getLength($target) ?? static::MAXIMUM_DOWNLOAD_BYTES; + $this->checkLength($data, $maxBytes, $target); + + $hashes = $targetsMetadata->getHashes($target); + if (count($hashes) === 0) { + // § 5.7.2 + throw new MetadataException("No trusted hashes are available for '$target'"); + } + foreach ($hashes as $algo => $hash) { + // If the stream has a URI that refers to a file, use + // hash_file() to verify it. Otherwise, read the entire stream + // as a string and use hash() to verify it. + $uri = $data->getMetadata('uri'); + if ($uri && file_exists($uri)) { + $streamHash = hash_file($algo, $uri); + } else { + $streamHash = hash($algo, $data->getContents()); + $data->rewind(); + } + + if ($hash !== $streamHash) { + throw new InvalidHashException($data, "Invalid $algo hash for $target"); + } + } + } + + /** + * Downloads a target file, verifies it, and returns its contents. + * + * @param string $target + * The path of the target file. Needs to be known to the most recent + * targets metadata downloaded in ::refresh(). + * @param mixed ...$extra + * Additional arguments to pass to the file fetcher. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * A promise representing the eventual verified result of the download + * operation. + */ + public function download(string $target, ...$extra): PromiseInterface + { + $this->refresh(); + + $targetsMetadata = $this->getMetadataForTarget($target); + if ($targetsMetadata === null) { + return new RejectedPromise(new NotFoundException($target, 'Target')); + } + + // If the target isn't known, immediately return a rejected promise. + try { + $length = $targetsMetadata->getLength($target) ?? static::MAXIMUM_DOWNLOAD_BYTES; + } catch (NotFoundException $e) { + return new RejectedPromise($e); + } + + return $this->repoFileFetcher->fetchTarget($target, $length, ...$extra) + ->then(function (StreamInterface $stream) use ($target) { + $this->verify($target, $stream); + return $stream; + }); + } + + /** + * Gets a target metadata object that contains the specified target, if any. + * + * @param string $target + * The path of the target file. + * + * @return \Tuf\Metadata\TargetsMetadata|null + * The targets metadata with information about the desired target, or null if no relevant metadata is found. + */ + protected function getMetadataForTarget(string $target): ?TargetsMetadata + { + // Search the top level targets metadata. + /** @var \Tuf\Metadata\TargetsMetadata $targetsMetadata */ + $targetsMetadata = $this->metadataFactory->load('targets'); + if ($targetsMetadata->hasTarget($target)) { + return $targetsMetadata; + } + // Recursively search any delegated roles. + return $this->searchDelegatedRolesForTarget($targetsMetadata, $target, ['targets']); + } + + /** + * Fetches and verifies a targets metadata file. + * + * The metadata file will be stored as '$role_json'. + * + * @param string $role + * The role name. This may be 'targets' or a delegated role. + */ + private function fetchAndVerifyTargetsMetadata(string $role): void + { + $newSnapshotData = $this->metadataFactory->load('snapshot'); + $targetsVersion = $newSnapshotData->getFileMetaInfo($role. ".json")['version']; + // § 5.6.1 + $newTargetsContent = $this->fetchFile("$targetsVersion.$role.json"); + $newTargetsData = TargetsMetadata::createFromJson($newTargetsContent, $role); + $this->universalVerifier->verify(TargetsMetadata::TYPE, $newTargetsData); + // § 5.5.6 + // TODO: here change .json to _json + $this->durableStorage[$role . "_json"] = $newTargetsContent; + } + + /** + * Returns the time that the update began. + * + * @return \DateTimeImmutable + * The time that the update began. + */ + private function getUpdateStartTime(): \DateTimeImmutable + { + return (new \DateTimeImmutable())->setTimestamp($this->clock->getCurrentTime()); + } + + /** + * Searches delegated roles for metadata concerning a specific target. + * + * @param \Tuf\Metadata\TargetsMetadata|null $targetsMetadata + * The targets metadata to search. + * @param string $target + * The path of the target file. + * @param string[] $searchedRoles + * The roles that have already been searched. This is for internal use only and should not be passed by calling code. + * @param bool $terminated + * (optional) For internal recursive calls only. This will be set to true if a terminating delegation is found in + * the search. + * + * + * @return \Tuf\Metadata\TargetsMetadata|null + * The target metadata that contains the metadata for the target or null if the target is not found. + */ + private function searchDelegatedRolesForTarget(TargetsMetadata $targetsMetadata, string $target, array $searchedRoles, bool &$terminated = false): ?TargetsMetadata + { + foreach ($targetsMetadata->getDelegatedKeys() as $keyId => $delegatedKey) { + $this->signatureVerifier->addKey($keyId, $delegatedKey); + } + foreach ($targetsMetadata->getDelegatedRoles() as $delegatedRole) { + $delegatedRoleName = $delegatedRole->getName(); + if (in_array($delegatedRoleName, $searchedRoles, true)) { + // § 5.6.7.1 + // If this role has been visited before, skip it (to avoid cycles in the delegation graph). + continue; + } + // § 5.6.7.1 + if (count($searchedRoles) > static::MAXIMUM_TARGET_ROLES) { + return null; + } + + $this->signatureVerifier->addRole($delegatedRole); + // Targets must match the paths of all roles in the delegation chain, so if the path does not match, + // do not evaluate this role or any roles it delegates to. + if ($delegatedRole->matchesPath($target)) { + $this->fetchAndVerifyTargetsMetadata($delegatedRoleName); + /** @var \Tuf\Metadata\TargetsMetadata $delegatedTargetsMetadata */ + $delegatedTargetsMetadata = $this->metadataFactory->load($delegatedRoleName); + if ($delegatedTargetsMetadata->hasTarget($target)) { + return $delegatedTargetsMetadata; + } + $searchedRoles[] = $delegatedRoleName; + // § 5.6.7.2.1 + // Recursively search the list of delegations in order of appearance. + $delegatedRolesMetadataSearchResult = $this->searchDelegatedRolesForTarget($delegatedTargetsMetadata, $target, $searchedRoles, $terminated); + if ($terminated || $delegatedRolesMetadataSearchResult) { + return $delegatedRolesMetadataSearchResult; + } + + // If $delegatedRole is terminating then we do not search any of the next delegated roles after it + // in the delegations from $targetsMetadata. + if ($delegatedRole->isTerminating()) { + $terminated = true; + // § 5.6.7.2.2 + // If the role is terminating then abort searching for a target. + return null; + } + } + } + return null; + } +} diff --git a/libraries/src/TUF/src/Constraints/Collection.php b/libraries/src/TUF/src/Constraints/Collection.php new file mode 100644 index 00000000000..77cae65fe82 --- /dev/null +++ b/libraries/src/TUF/src/Constraints/Collection.php @@ -0,0 +1,17 @@ +unsupportedFields as $unsupportedField) { + $existsInArray = \is_array($value) && \array_key_exists($unsupportedField, $value); + $existsInArrayAccess = $value instanceof \ArrayAccess && $value->offsetExists($unsupportedField); + if ($existsInArray || $existsInArrayAccess) { + $this->context->buildViolation('This field is not supported.') + ->atPath("[$unsupportedField]") + ->setInvalidValue(null) + ->setCode(Collection::MISSING_FIELD_ERROR) + ->addViolation(); + } + } + parent::validate($value, $constraint); + } +} diff --git a/libraries/src/TUF/src/DelegatedRole.php b/libraries/src/TUF/src/DelegatedRole.php new file mode 100644 index 00000000000..6c3744c8e86 --- /dev/null +++ b/libraries/src/TUF/src/DelegatedRole.php @@ -0,0 +1,96 @@ +terminating; + } + + /** + * DelegatedRole constructor. + * + * @param string $name + * @param int $threshold + * @param array $keyIds + * @param array $paths + * @param bool $terminating + */ + private function __construct(string $name, int $threshold, array $keyIds, array $paths, bool $terminating) + { + parent::__construct($name, $threshold, $keyIds); + $this->paths = $paths; + $this->terminating = $terminating; + } + + public static function createFromMetadata(\ArrayObject $roleInfo, string $name = null): Role + { + $roleConstraints = static::getRoleConstraints(); + $roleConstraints->fields += [ + 'name' => new Required( + [ + new Type('string'), + new NotBlank(), + ] + ), + 'terminating' => new Required(new Type('boolean')), + 'paths' => new Required(new Type('array')), + ]; + static::validate($roleInfo, $roleConstraints); + return new static( + $roleInfo['name'], + $roleInfo['threshold'], + $roleInfo['keyids'], + $roleInfo['paths'], + $roleInfo['terminating'] + ); + } + + /** + * Determines whether a target matches a path for this role. + * + * @param string $target + * The path of the target file. + * + * @return bool + * True if there is path match or no path criteria is set for the role, or + * false otherwise. + */ + public function matchesPath(string $target): bool + { + if ($this->paths) { + foreach ($this->paths as $path) { + if (fnmatch($path, $target)) { + return true; + } + } + return false; + } + // If no paths are set then any target is a match. + return true; + } +} diff --git a/libraries/src/TUF/src/Exception/Attack/AttackException.php b/libraries/src/TUF/src/Exception/Attack/AttackException.php new file mode 100644 index 00000000000..d267778ee60 --- /dev/null +++ b/libraries/src/TUF/src/Exception/Attack/AttackException.php @@ -0,0 +1,15 @@ +stream = $stream; + } + + /** + * Returns the untrusted stream object pointing to the downloaded target. + * + * WARNING: The contents of the stream failed TUF validation. Any code + * interacting it should treat it as unsafe and proceed with great caution. + * + * @return \Psr\Http\Message\StreamInterface + * The stream object. + */ + public function getStream(): StreamInterface + { + return $this->stream; + } +} diff --git a/libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php b/libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php new file mode 100644 index 00000000000..26a7a021731 --- /dev/null +++ b/libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php @@ -0,0 +1,10 @@ +ksort(); + } elseif (is_object($structure)) { + throw new \RuntimeException('\Tuf\JsonNormalizer::rKeySort() is not intended to sort objects except \ArrayObject. Found: ' . get_class($structure)); + } + + foreach ($structure as $key => $value) { + if (is_array($value) || $value instanceof \ArrayObject) { + self::rKeySort($structure[$key]); + } + } + } + + /** + * Replaces all instance of \stdClass in the data structure with \ArrayObject. + * + * Symfony Validator library's built-in constraints cannot validate + * \stdClass objects. This method should only be used with the return value + * of json_decode therefore should not contain any objects except instances + * of \stdClass. + * + * @param array|\stdClass $data + * The data to convert. The data structure should contain no objects + * except \stdClass instances. + * + * @return iterable + * The data with all stdClass instances replaced with ArrayObject. + * + * @throws \RuntimeException + * Thrown if the an object other than \stdClass is found. + */ + private static function replaceStdClassWithArrayObject($data): iterable + { + if ($data instanceof \stdClass) { + $data = new \ArrayObject($data); + } elseif (!is_array($data)) { + throw new \RuntimeException('Cannot convert type: ' . get_class($data)); + } + foreach ($data as $key => $datum) { + if (is_array($datum) || is_object($datum)) { + $data[$key] = static::replaceStdClassWithArrayObject($datum); + } + } + return $data; + } +} diff --git a/libraries/src/TUF/src/Key.php b/libraries/src/TUF/src/Key.php new file mode 100644 index 00000000000..46bf17a1e74 --- /dev/null +++ b/libraries/src/TUF/src/Key.php @@ -0,0 +1,131 @@ +type = $type; + $this->scheme = $scheme; + $this->public = $public; + } + + /** + * Creates a key object from TUF metadata. + * + * @param \ArrayObject $keyInfo + * The key information from TUF metadata including. + * - keytype: The public key signature system, e.g. 'ed25519'. + * - scheme: The corresponding signature scheme, e.g. 'ed25519'. + * - keyval: An associative array containing the public key value. + + * + * @return static + * + * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats + */ + public static function createFromMetadata(\ArrayObject $keyInfo): self + { + self::validate($keyInfo, static::getKeyConstraints()); + return new static( + $keyInfo['keytype'], + $keyInfo['scheme'], + $keyInfo['keyval']['public'] + ); + } + + /** + * Gets the public key value. + * + * @return string + * The public key value. + */ + public function getPublic(): string + { + return $this->public; + } + + /** + * Gets the key type. + * + * @return string + * The key type. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Computes the key ID. + * + * Per specification section 4.2, the KEYID is a hexdigest of the SHA-256 + * hash of the canonical form of the key. + * + * @return string + * The key ID in hex format for the key metadata hashed using sha256. + * + * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats + * + * @todo https://github.com/php-tuf/php-tuf/issues/56 + */ + public function getComputedKeyId(): string + { + // @see https://github.com/secure-systems-lab/securesystemslib/blob/master/securesystemslib/keys.py + // The keyid_hash_algorithms array value is based on the TUF settings, + // it's not expected to be part of the key metadata. The fact that it is + // currently included is a quirk of the TUF python code that may be + // fixed in future versions. Calculate using the normal TUF settings + // since this is how it's calculated in the securesystemslib code and + // any value for keyid_hash_algorithms in the key data in root.json is + // ignored. + $keyCanonicalStruct = [ + 'keytype' => $this->getType(), + 'scheme' => $this->scheme, + 'keyid_hash_algorithms' => ['sha256', 'sha512'], + 'keyval' => ['public' => $this->getPublic()], + ]; + $keyCanonicalForm = JsonNormalizer::asNormalizedJson($keyCanonicalStruct); + + return hash('sha256', $keyCanonicalForm, false); + } +} diff --git a/libraries/src/TUF/src/KeyDB.php b/libraries/src/TUF/src/KeyDB.php new file mode 100644 index 00000000000..43ce954b482 --- /dev/null +++ b/libraries/src/TUF/src/KeyDB.php @@ -0,0 +1,125 @@ +getKeys($allowUntrustedAccess) as $keyId => $key) { + $db->addKey($keyId, $key); + } + + return $db; + } + + /** + * Gets the supported encryption key types. + * + * @return string[] + * An array of supported encryption key type names (e.g. 'ed25519'). + */ + public static function getSupportedKeyTypes(): array + { + return ['ed25519']; + } + + /** + * Constructs a new KeyDB. + */ + public function __construct() + { + $this->keys = []; + } + + /** + * Adds key metadata to the key database while avoiding duplicates. + * + * @param string $keyId + * The key ID given as the object key in root.json or another keys list. + * @param \Tuf\Key + * The key. + * + * @return void + * + * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats + */ + public function addKey(string $keyId, Key $key): void + { + if (! in_array($key->getType(), self::getSupportedKeyTypes(), true)) { + // @todo Convert this to a log line as per Python. + // https://github.com/php-tuf/php-tuf/issues/160 + throw new InvalidKeyException("Root metadata file contains an unsupported key type: \"${keyMeta['keytype']}\""); + } + // Per TUF specification 4.3, Clients MUST calculate each KEYID to + // verify this is correct for the associated key. + if ($keyId !== $key->getComputedKeyId()) { + throw new InvalidKeyException('The calculated KEYID does not match the value provided.'); + } + $this->keys[$keyId] = $key; + } + + /** + * Returns the key metadata for a given key ID. + * + * @param string $keyId + * The key ID. + * + * @return \Tuf\Key + * The key. + * + * @throws \Tuf\Exception\NotFoundException + * Thrown if the key ID is not found in the keydb database. + * + * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats + */ + public function getKey(string $keyId): Key + { + if (empty($this->keys[$keyId])) { + throw new NotFoundException($keyId, 'key'); + } + return $this->keys[$keyId]; + } +} diff --git a/libraries/src/TUF/src/Metadata/ConstraintsTrait.php b/libraries/src/TUF/src/Metadata/ConstraintsTrait.php new file mode 100644 index 00000000000..a79fa6260df --- /dev/null +++ b/libraries/src/TUF/src/Metadata/ConstraintsTrait.php @@ -0,0 +1,173 @@ +validate($data, $constraints); + if (count($violations)) { + $exceptionMessages = []; + foreach ($violations as $violation) { + $exceptionMessages[] = (string) $violation; + } + throw new MetadataException(implode(", \n", $exceptionMessages)); + } + } + + /** + * Gets the common hash constraints. + * + * @return \Symfony\Component\Validator\Constraint[][] + * The hash constraints. + */ + protected static function getHashesConstraints(): array + { + return [ + 'hashes' => [ + new Count(['min' => 1]), + new Type('\ArrayObject'), + // The keys for 'hashes is not know but they all must be strings. + new All([ + new Type(['type' => 'string']), + new NotBlank(), + ]), + ], + ]; + } + + /** + * Gets the common version constraints. + * + * @return \Symfony\Component\Validator\Constraint[][] + * The version constraints. + */ + protected static function getVersionConstraints(): array + { + return [ + 'version' => [ + new Type(['type' => 'integer']), + new GreaterThanOrEqual(1), + ], + ]; + } + + /** + * Gets the common threshold constraints. + * + * @return \Symfony\Component\Validator\Constraint[][] + * The threshold constraints. + */ + protected static function getThresholdConstraints(): array + { + return [ + 'threshold' => [ + new Type(['type' => 'integer']), + new GreaterThanOrEqual(1), + ], + ]; + } + /** + * Gets the common keyids constraints. + * + * @return \Symfony\Component\Validator\Constraint[][] + * The keysids constraints. + */ + protected static function getKeyidsConstraints(): array + { + return [ + 'keyids' => [ + new Count(['min' => 1]), + new Type(['type' => 'array']), + // The keys for 'hashes is not know but they all must be strings. + new All([ + new Type(['type' => 'string']), + new NotBlank(), + ]), + ], + ]; + } + + /** + * Gets the common key Collection constraints. + * + * @return Collection + * The 'key' Collection constraint. + */ + protected static function getKeyConstraints(): Collection + { + return new Collection([ + // This field is not part of the TUF specification and is being + // removed from the Python TUF reference implementation in + // https://github.com/theupdateframework/tuf/issues/848. + // If it is provided though we only support the default value which + // is passed on from a setting in the Python `securesystemslib` + // library. + 'keyid_hash_algorithms' => new Optional([ + new EqualTo(['value' => ["sha256", "sha512"]]), + ]), + 'keytype' => [ + new Type(['type' => 'string']), + new NotBlank(), + ], + 'keyval' => [ + new Type('\ArrayObject'), + new Collection([ + 'public' => [ + new Type(['type' => 'string']), + new NotBlank(), + ], + ]), + ], + 'scheme' => [ + new Type(['type' => 'string']), + new NotBlank(), + ], + ]); + } + + /** + * Gets the role constraints. + * + * @return \Symfony\Component\Validator\Constraints\Collection + * The role constraints collection. + */ + protected static function getRoleConstraints(): Collection + { + return new Collection( + static::getKeyidsConstraints() + + static::getThresholdConstraints() + ); + } +} diff --git a/libraries/src/TUF/src/Metadata/Factory.php b/libraries/src/TUF/src/Metadata/Factory.php new file mode 100644 index 00000000000..601716a3352 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Factory.php @@ -0,0 +1,71 @@ +storage = $storage; + } + + /** + * Loads a value object for trusted metadata. + * + * @param string $role + * The role to be loaded. + * + * @return \Tuf\Metadata\MetadataBase|null + * The trusted metadata for the role, or NULL if none was found. + * @throws \Tuf\Exception\MetadataException + */ + public function load(string $role): ?MetadataBase + { + // TODO: this is changed from $role . ".json" to $role . "_json" + $fileName = $role . "_json"; + + if (isset($this->storage[$fileName])) + { + $json = $this->storage[$fileName]; + + switch ($role) + { + case RootMetadata::TYPE: + $currentMetadata = RootMetadata::createFromJson($json); + break; + case SnapshotMetadata::TYPE: + $currentMetadata = SnapshotMetadata::createFromJson($json); + break; + case TimestampMetadata::TYPE: + $currentMetadata = TimestampMetadata::createFromJson($json); + break; + default: + $currentMetadata = TargetsMetadata::createFromJson($json, $role); + } + + $currentMetadata->trust(); + + return $currentMetadata; + } + else + { + return null; + } + } +} diff --git a/libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php b/libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php new file mode 100644 index 00000000000..cd7e6410407 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php @@ -0,0 +1,27 @@ +ensureIsTrusted($allowUntrustedAccess); + $signed = $this->getSigned(); + return $signed['meta'][$key] ?? null; + } +} diff --git a/libraries/src/TUF/src/Metadata/MetadataBase.php b/libraries/src/TUF/src/Metadata/MetadataBase.php new file mode 100644 index 00000000000..76ce37ff6cc --- /dev/null +++ b/libraries/src/TUF/src/Metadata/MetadataBase.php @@ -0,0 +1,255 @@ +metadata = $metadata; + $this->sourceJson = $sourceJson; + } + + /** + * Gets the original JSON source. + * + * @return string + * The JSON source. + */ + public function getSource():string + { + return $this->sourceJson; + } + + /** + * Create an instance and also validate the decoded JSON. + * + * @param string $json + * A JSON string representing TUF metadata. + * + * @return static + * The new instance. + * + * @throws \Tuf\Exception\MetadataException + * Thrown if validation fails. + */ + public static function createFromJson(string $json): self + { + $data = JsonNormalizer::decode($json); + static::validate($data, new Collection(static::getConstraints())); + + return new static($data, $json); + } + + /** + * Gets the constraints for top-level metadata. + * + * @return \Symfony\Component\Validator\Constraint[] + * Array of constraints. + */ + protected static function getConstraints(): array + { + return [ + 'signatures' => new Required( + [ + new Type('array'), + new Count(['min' => 1]), + new All( + [ + new Collection( + [ + 'keyid' => [ + new NotBlank, + new Type(['type' => 'string']), + ], + 'sig' => [ + new NotBlank, + new Type(['type' => 'string']), + ], + ] + ), + ] + ), + ] + ), + 'signed' => new Required( + [ + new Collection(static::getSignedCollectionOptions()), + ] + ), + ]; + } + + /** + * Gets options for the 'signed' metadata property. + * + * @return array + * An options array as expected by + * \Symfony\Component\Validator\Constraints\Collection::__construct(). + */ + protected static function getSignedCollectionOptions(): array + { + return [ + 'fields' => [ + '_type' => [ + new EqualTo(['value' => static::TYPE]), + new Type(['type' => 'string']), + ], + 'expires' => new DateTime(['value' => \DateTimeInterface::ISO8601]), + // We only expect to work with major version 1. + 'spec_version' => [ + new NotBlank, + new Type(['type' => 'string']), + //new Regex(['pattern' => '/^1\.[0-9]+\.[0-9]+$/']), + ], + ] + static::getVersionConstraints(), + 'allowExtraFields' => true, + ]; + } + + /** + * Get signed. + * + * @return \ArrayObject + * The "signed" section of the data. + */ + public function getSigned(): \ArrayObject + { + return (new DeepCopy)->copy($this->metadata['signed']); + } + + /** + * Get version. + * + * @return integer + * The version. + */ + public function getVersion(): int + { + return $this->getSigned()['version']; + } + + /** + * Get the expires date string. + * + * @return string + * The date string. + */ + public function getExpires(): string + { + return $this->getSigned()['expires']; + } + + /** + * Get signatures. + * + * @return array + * The "signatures" section of the data. + */ + public function getSignatures(): array + { + return (new DeepCopy)->copy($this->metadata['signatures']); + } + + /** + * Get the metadata type. + * + * @return string + * The type. + */ + public function getType(): string + { + return $this->getSigned()['_type']; + } + + /** + * Gets the role for the metadata. + * + * @return string + * The type. + */ + public function getRole(): string + { + // For most metadata types the 'type' and the 'role' are the same. + // Metadata types that need to specify a different role should override + // this method. + return $this->getType(); + } + + /** + * Sets the metadata as trusted. + * + * @return void + */ + public function trust(): void + { + $this->isTrusted = true; + } + + /** + * Ensures that the metadata is trusted or the caller explicitly expects untrusted metadata. + * + * @param boolean $allowUntrustedAccess + * Whether this method should access even if the metadata is not trusted. + * + * @return void + */ + public function ensureIsTrusted(bool $allowUntrustedAccess = false): void + { + if (!$allowUntrustedAccess && !$this->isTrusted) + { + throw new \RuntimeException("Cannot use untrusted '{$this->getRole()}'. metadata."); + } + } +} diff --git a/libraries/src/TUF/src/Metadata/RootMetadata.php b/libraries/src/TUF/src/Metadata/RootMetadata.php new file mode 100644 index 00000000000..9558f23060f --- /dev/null +++ b/libraries/src/TUF/src/Metadata/RootMetadata.php @@ -0,0 +1,100 @@ + 1]), + new All([ + static::getKeyConstraints(), + ]), + ]); + $roleConstraints = static::getRoleConstraints(); + $options['fields']['roles'] = new Collection([ + 'targets' => new Required($roleConstraints), + 'timestamp' => new Required($roleConstraints), + 'snapshot' => new Required($roleConstraints), + 'root' => new Required($roleConstraints), + 'mirror' => new Optional($roleConstraints), + ]); + $options['fields']['consistent_snapshot'] = new Required([ + new Type('boolean'), + new EqualTo(true), + ]); + return $options; + } + + /** + * Gets the roles from the metadata. + * + * @param boolean $allowUntrustedAccess + * Whether this method should access even if the metadata is not trusted. + * + * @return \Tuf\Role[] + * The roles. + */ + public function getRoles(bool $allowUntrustedAccess = false): array + { + $this->ensureIsTrusted($allowUntrustedAccess); + $roles = []; + foreach ($this->getSigned()['roles'] as $roleName => $roleInfo) { + $roles[$roleName] = Role::createFromMetadata($roleInfo, $roleName); + } + return $roles; + } + + /** + * Gets the keys for the root metadata. + * + * @param boolean $allowUntrustedAccess + * Whether this method should access even if the metadata is not trusted. + * + * @return \Tuf\Key[] + * The keys for the metadata. + */ + public function getKeys(bool $allowUntrustedAccess = false): array + { + $this->ensureIsTrusted($allowUntrustedAccess); + $keys = []; + foreach ($this->getSigned()['keys'] as $keyId => $keyInfo) { + $keys[$keyId] = Key::createFromMetadata($keyInfo); + } + return $keys; + } + + /** + * Determines whether consistent snapshots are supported. + * + * @return boolean + * Whether consistent snapshots are supported. + */ + public function supportsConsistentSnapshots(): bool + { + $this->ensureIsTrusted(); + return $this->getSigned()['consistent_snapshot']; + } +} diff --git a/libraries/src/TUF/src/Metadata/SnapshotMetadata.php b/libraries/src/TUF/src/Metadata/SnapshotMetadata.php new file mode 100644 index 00000000000..7978a3afb30 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/SnapshotMetadata.php @@ -0,0 +1,72 @@ + 1]), + new All( + [ + new Collection( + [ + 'fields' => static::getSnapshotMetaConstraints(), + 'allowExtraFields' => true, + ] + ), + ] + ), + ] + ); + + return $options; + } + + /** + * Returns the fields required or optional for a snapshot meta file + * + * @return array + */ + private static function getSnapshotMetaConstraints() + { + return [ + 'version' => [ + new Type(['type' => 'integer']), + new GreaterThanOrEqual(1), + ], + new Optional( + [ + new Collection( + [ + 'length' => [ + new Type(['type' => 'integer']), + new GreaterThanOrEqual(1), + ], + ] + static::getHashesConstraints() + ), + ] + ), + ]; + } +} diff --git a/libraries/src/TUF/src/Metadata/TargetsMetadata.php b/libraries/src/TUF/src/Metadata/TargetsMetadata.php new file mode 100644 index 00000000000..dec63edf377 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/TargetsMetadata.php @@ -0,0 +1,209 @@ +role = $roleName; + return $newMetadata; + } + + /** + * {@inheritdoc} + */ + protected static function getSignedCollectionOptions(): array + { + $options = parent::getSignedCollectionOptions(); + $options['fields']['delegations'] = new Optional([ + new Collection([ + 'keys' => new Required([ + new Type('\ArrayObject'), + new All([ + static::getKeyConstraints(), + ]), + ]), + 'roles' => new All([ + new Type('\ArrayObject'), + new TufCollection([ + 'fields' => [ + 'name' => [ + new NotBlank(), + new Type(['type' => 'string']), + ], + 'paths' => [ + new Type(['type' => 'array']), + new All([ + new Type(['type' => 'string']), + new NotBlank(), + ]), + ], + 'terminating' => [ + new Type(['type' => 'boolean']), + ], + ] + static::getKeyidsConstraints() + static::getThresholdConstraints(), + // @todo Support 'path_hash_prefixes' in + // https://github.com/php-tuf/php-tuf/issues/191 + 'unsupportedFields' => ['path_hash_prefixes'], + ]), + ]), + ]), + ]); + $options['fields']['targets'] = new Required([ + new All([ + new Collection([ + 'length' => [ + new Type(['type' => 'integer']), + new GreaterThanOrEqual(1), + ], + 'custom' => new Optional([ + new Type('\ArrayObject'), + ]), + ] + static::getHashesConstraints()), + ]), + + ]); + return $options; + } + + /** + * Returns the length, in bytes, of a specific target. + * + * @param string $target + * The target path. + * + * @return integer + * The length (size) of the target, in bytes. + */ + public function getLength(string $target): int + { + return $this->getInfo($target)['length']; + } + + /** + * {@inheritdoc} + */ + public function getRole(): string + { + return $this->role ?? $this->getType(); + } + + /** + * Returns the known hashes for a specific target. + * + * @param string $target + * The target path. + * + * @return \ArrayObject + * The known hashes for the object. The keys are the hash algorithm (e.g. + * 'sha256') and the values are the hash digest. + */ + public function getHashes(string $target): \ArrayObject + { + return $this->getInfo($target)['hashes']; + } + + /** + * Determines if a target is specified in the current metadata. + * + * @param string $target + * The target path. + * + * @return bool + * True if the target is specified, or false otherwise. + */ + public function hasTarget(string $target): bool + { + try { + $this->getInfo($target); + return true; + } catch (NotFoundException $exception) { + return false; + } + } + + /** + * Gets info about a specific target. + * + * @param string $target + * The target path. + * + * @return \ArrayObject + * The target's info. + * + * @throws \Tuf\Exception\NotFoundException + * Thrown if the target is not mentioned in this metadata. + */ + protected function getInfo(string $target): \ArrayObject + { + $signed = $this->getSigned(); + if (isset($signed['targets'][$target])) { + return $signed['targets'][$target]; + } + throw new NotFoundException($target, 'Target'); + } + + /** + * Gets the delegated keys if any. + * + * @return \Tuf\Key[] + * The delegated keys. + */ + public function getDelegatedKeys(): array + { + $keys = []; + foreach ($this->getSigned()['delegations']['keys'] as $keyId => $keyInfo) { + $keys[$keyId] = Key::createFromMetadata($keyInfo); + } + return $keys; + } + + /** + * Gets the delegated roles if any. + * + * @return \Tuf\DelegatedRole[] + * The delegated roles. + */ + public function getDelegatedRoles(): array + { + $roles = []; + foreach ($this->getSigned()['delegations']['roles'] as $roleInfo) { + $role = DelegatedRole::createFromMetadata($roleInfo); + $roles[$role->getName()] = $role; + } + return $roles; + } +} diff --git a/libraries/src/TUF/src/Metadata/TimestampMetadata.php b/libraries/src/TUF/src/Metadata/TimestampMetadata.php new file mode 100644 index 00000000000..7325d78c9db --- /dev/null +++ b/libraries/src/TUF/src/Metadata/TimestampMetadata.php @@ -0,0 +1,39 @@ + 1]), + new All([ + new Collection([ + 'length' => [ + new Type(['type' => 'integer']), + new GreaterThanOrEqual(1), + ], + ] + static::getHashesConstraints() + static::getVersionConstraints()), + ]), + ]); + return $options; + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php new file mode 100644 index 00000000000..db0f3e36ab3 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php @@ -0,0 +1,48 @@ +trustedMetadata->getSigned()['meta']; + $type = $this->trustedMetadata->getType(); + foreach ($localMetaFileInfos as $fileName => $localFileInfo) { + /** @var \Tuf\Metadata\SnapshotMetadata|\Tuf\Metadata\TimestampMetadata $untrustedMetadata */ + if ($remoteFileInfo = $untrustedMetadata->getFileMetaInfo($fileName, true)) { + if ($remoteFileInfo['version'] < $localFileInfo['version']) { + $message = "Remote $type metadata file '$fileName' version \"${$remoteFileInfo['version']}\" " . + "is less than previously seen version \"${$localFileInfo['version']}\""; + throw new RollbackAttackException($message); + } + } + } + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php new file mode 100644 index 00000000000..b4201bb1f25 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php @@ -0,0 +1,63 @@ +signatureVerifier->checkSignatures($untrustedMetadata); + $this->signatureVerifier = SignatureVerifier::createFromRootMetadata($untrustedMetadata, true); + $this->signatureVerifier->checkSignatures($untrustedMetadata); + // § 5.3.5 + $this->checkRollbackAttack($untrustedMetadata); + } + + /** + * {@inheritDoc} + */ + protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void + { + $expectedUntrustedVersion = $this->trustedMetadata->getVersion() + 1; + $untrustedVersion = $untrustedMetadata->getVersion(); + if ($expectedUntrustedVersion && ($untrustedMetadata->getVersion() !== $expectedUntrustedVersion)) { + throw new RollbackAttackException("Remote 'root' metadata version \"$$untrustedVersion\" " . + "does not the expected version \"$$expectedUntrustedVersion\""); + } + parent::checkRollbackAttack($untrustedMetadata); + } + + /** + * {@inheritdoc} + * + * Overridden to make public. + * + * After attempting to update the root metadata, the new or non-updated metadata must be checked + * for a freeze attack. We cannot check for a freeze attack in ::verify() because when the client + * is many root files behind, only the last version to be downloaded needs to be checked for a + * freeze attack. + */ + public static function checkFreezeAttack(MetadataBase $metadata, \DateTimeImmutable $expiration): void + { + parent::checkFreezeAttack($metadata, $expiration); + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php new file mode 100644 index 00000000000..7f0f7a023e5 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php @@ -0,0 +1,79 @@ +setTrustedAuthority($timestampMetadata); + } + + /** + * {@inheritdoc} + */ + public function verify(MetadataBase $untrustedMetadata): void + { + // § 5.5.2 + $this->checkAgainstHashesFromTrustedAuthority($untrustedMetadata); + + // § 5.5.3 + $this->signatureVerifier->checkSignatures($untrustedMetadata); + + // § 5.5.4 + $this->checkAgainstVersionFromTrustedAuthority($untrustedMetadata); + + // If the timestamp or snapshot keys were rotating then the snapshot file + // will not exist. + if ($this->trustedMetadata) { + // § 5.5.5 + $this->checkRollbackAttack($untrustedMetadata); + } + + // § 5.5.6 + static::checkFreezeAttack($untrustedMetadata, $this->metadataExpiration); + } + + /** + * {@inheritdoc} + */ + protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void + { + // TUF-SPEC-v1.0.16 Section 5.4.4 + /** @var TimestampMetadata $untrustedMetadata */ + $this->checkFileInfoVersions($untrustedMetadata); + $localMetaFileInfos = $this->trustedMetadata->getSigned()['meta']; + foreach ($localMetaFileInfos as $fileName => $localFileInfo) { + /** @var \Tuf\Metadata\SnapshotMetadata|\Tuf\Metadata\TimestampMetadata $untrustedMetadata */ + if (!$untrustedMetadata->getFileMetaInfo($fileName, true)) { + // § 5.5.5 + // Any targets metadata filename that was listed in the trusted snapshot metadata file, if any, MUST + // continue to be listed in the new snapshot metadata file. + throw new RollbackAttackException("Remote snapshot metadata file references '$fileName' but this is not present in the remote file"); + } + } + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php new file mode 100644 index 00000000000..bcdfa021dc0 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php @@ -0,0 +1,51 @@ +setTrustedAuthority($snapshotMetadata); + } + + /** + * {@inheritdoc} + */ + public function verify(MetadataBase $untrustedMetadata): void + { + // § 5.6.2 + $this->checkAgainstHashesFromTrustedAuthority($untrustedMetadata); + + // § 5.6.3 + $this->signatureVerifier->checkSignatures($untrustedMetadata); + + // § 5.6.4 + $this->checkAgainstVersionFromTrustedAuthority($untrustedMetadata); + + // § 5.6.5 + static::checkFreezeAttack($untrustedMetadata, $this->metadataExpiration); + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php new file mode 100644 index 00000000000..8d28459bbd8 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php @@ -0,0 +1,40 @@ +signatureVerifier->checkSignatures($untrustedMetadata); + // If the timestamp or snapshot keys were rotating then the timestamp file + // will not exist. + if ($this->trustedMetadata) { + // § 5.4.3 + $this->checkRollbackAttack($untrustedMetadata); + } + // § 5.4.4 + static::checkFreezeAttack($untrustedMetadata, $this->metadataExpiration); + } + + /** + * {@inheritdoc} + */ + protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void + { + // § 5.3.2.1 + parent::checkRollbackAttack($untrustedMetadata); + // § 5.3.2.2 + /** @var \Tuf\Metadata\SnapshotMetadata $untrustedMetadata */ + $this->checkFileInfoVersions($untrustedMetadata); + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php b/libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php new file mode 100644 index 00000000000..6456244f063 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php @@ -0,0 +1,78 @@ +ensureIsTrusted(); + $this->authority = $authority; + } + + /** + * Verifies the hashes of untrusted metadata against hashes in the trusted metadata. + * + * @param \Tuf\Metadata\MetadataBase $untrustedMetadata + * The untrusted metadata. + * + * @throws \Tuf\Exception\MetadataException + * Thrown if the new metadata object cannot be verified. + * + * @return void + */ + protected function checkAgainstHashesFromTrustedAuthority(MetadataBase $untrustedMetadata): void + { + $role = $untrustedMetadata->getRole(); + $fileInfo = $this->authority->getFileMetaInfo($role . '.json'); + if (isset($fileInfo['hashes'])) { + foreach ($fileInfo['hashes'] as $algo => $hash) { + if ($hash !== hash($algo, $untrustedMetadata->getSource())) { + /** @var \Tuf\Metadata\MetadataBase $authorityMetadata */ + throw new MetadataException("The '{$role}' contents does not match hash '$algo' specified in the '{$this->authority->getType()}' metadata."); + } + } + } + } + + /** + * Verifies the version of untrusted metadata against the version in trusted metadata. + * + * @param \Tuf\Metadata\MetadataBase $untrustedMetadata + * The untrusted metadata. + * + * @throws \Tuf\Exception\MetadataException + * Thrown if the new metadata object cannot be verified. + * + * @return void + */ + protected function checkAgainstVersionFromTrustedAuthority(MetadataBase $untrustedMetadata): void + { + $role = $untrustedMetadata->getRole(); + $fileInfo = $this->authority->getFileMetaInfo($role . '.json'); + $expectedVersion = $fileInfo['version']; + if ($expectedVersion !== $untrustedMetadata->getVersion()) { + throw new MetadataException("Expected {$role} version {$expectedVersion} does not match actual version {$untrustedMetadata->getVersion()}."); + } + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php new file mode 100644 index 00000000000..085530c937a --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php @@ -0,0 +1,92 @@ +metadataFactory = $metadataFactory; + $this->signatureVerifier = $signatureVerifier; + $this->metadataExpiration = $metadataExpiration; + } + + /** + * Verifies an untrusted metadata object for a role. + * + * @param string $role + * The metadata role (e.g. 'root', 'targets', etc.) + * @param \Tuf\Metadata\MetadataBase $untrustedMetadata + * The untrusted metadata object. + * + * @throws \Tuf\Exception\Attack\FreezeAttackException + * @throws \Tuf\Exception\Attack\RollbackAttackException + * @throws \Tuf\Exception\Attack\InvalidHashException + * @throws \Tuf\Exception\Attack\SignatureThresholdException + */ + public function verify(string $role, MetadataBase $untrustedMetadata): void + { + $trustedMetadata = $this->metadataFactory->load($role); + switch ($role) { + case RootMetadata::TYPE: + $verifier = new RootVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata); + break; + case SnapshotMetadata::TYPE: + /** @var \Tuf\Metadata\TimestampMetadata $timestampMetadata */ + $timestampMetadata = $this->metadataFactory->load(TimestampMetadata::TYPE); + $verifier = new SnapshotVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata, $timestampMetadata); + break; + case TimestampMetadata::TYPE: + $verifier = new TimestampVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata); + break; + default: + /** @var \Tuf\Metadata\SnapshotMetadata $snapshotMetadata */ + $snapshotMetadata = $this->metadataFactory->load(SnapshotMetadata::TYPE); + $verifier = new TargetsVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata, $snapshotMetadata); + } + $verifier->verify($untrustedMetadata); + // If the verifier didn't throw an exception, we can trust this metadata. + $untrustedMetadata->trust(); + } +} diff --git a/libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php b/libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php new file mode 100644 index 00000000000..6c571e2eb21 --- /dev/null +++ b/libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php @@ -0,0 +1,140 @@ +signatureVerifier = $signatureVerifier; + $this->metadataExpiration = $metadataExpiration; + if ($trustedMetadata) { + $trustedMetadata->ensureIsTrusted(); + } + $this->trustedMetadata = $trustedMetadata; + } + + /** + * Verify metadata according to the specification. + * + * @param \Tuf\Metadata\MetadataBase $untrustedMetadata + * The untrusted metadata to verify. + * + * @throws \Tuf\Exception\Attack\FreezeAttackException + * @throws \Tuf\Exception\Attack\RollbackAttackException + * @throws \Tuf\Exception\Attack\InvalidHashException + * @throws \Tuf\Exception\Attack\SignatureThresholdException + */ + abstract public function verify(MetadataBase $untrustedMetadata): void; + + /** + * Checks for a rollback attack. + * + * Verifies that an incoming remote version of a metadata file is greater + * than or equal to the last known version. + * + * @param \Tuf\Metadata\MetadataBase $untrustedMetadata + * The untrusted metadata. + * + * @return void + * + * @throws \Tuf\Exception\Attack\RollbackAttackException + * Thrown if a potential rollback attack is detected. + */ + protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void + { + $type = $this->trustedMetadata->getType(); + $remoteVersion = $untrustedMetadata->getVersion(); + $localVersion = $this->trustedMetadata->getVersion(); + if ($remoteVersion < $localVersion) { + $message = "Remote $type metadata version \"$$remoteVersion\" " . + "is less than previously seen $type version \"$$localVersion\""; + throw new RollbackAttackException($message); + } + } + + /** + * Checks for a freeze attack. + * + * Verifies that metadata has not expired, and assumes a potential freeze + * attack if it has. + * + * @param \Tuf\Metadata\MetadataBase $metadata + * The metadata to check. + * @param \DateTimeImmutable $expiration + * The metadata expiration. + * + * @return void + * + * @throws \Tuf\Exception\Attack\FreezeAttackException Thrown if a potential freeze attack is detected. + */ + protected static function checkFreezeAttack(MetadataBase $metadata, \DateTimeImmutable $expiration): void + { + $metadataExpiration = static::metadataTimestampToDatetime($metadata->getExpires()); + if ($metadataExpiration < $expiration) { + $format = "Remote %s metadata expired on %s"; + throw new FreezeAttackException(sprintf($format, $metadata->getRole(), $metadataExpiration->format('c'))); + } + } + + /** + * Converts a metadata timestamp string into an immutable DateTime object. + * + * @param string $timestamp + * The timestamp string in the metadata. + * + * @return \DateTimeImmutable + * An immutable DateTime object for the given timestamp. + * + * @throws FormatException + * Thrown if the timestamp string format is not valid. + */ + protected static function metadataTimestampToDateTime(string $timestamp): \DateTimeImmutable + { + $dateTime = \DateTimeImmutable::createFromFormat("Y-m-d\TH:i:sT", $timestamp); + if ($dateTime === false) { + throw new FormatException($timestamp, "Could not be interpreted as a DateTime"); + } + return $dateTime; + } +} diff --git a/libraries/src/TUF/src/Role.php b/libraries/src/TUF/src/Role.php new file mode 100644 index 00000000000..91d2e4f5297 --- /dev/null +++ b/libraries/src/TUF/src/Role.php @@ -0,0 +1,122 @@ +name = $name; + $this->threshold = $threshold; + $this->keyIds = $keyIds; + } + + /** + * Creates a role object from TUF metadata. + * + * @param \ArrayObject $roleInfo + * The role information from TUF metadata. + * @param string $name + * The name of the role. + * + * @return static + * + * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats + */ + public static function createFromMetadata(\ArrayObject $roleInfo, string $name): Role + { + self::validate($roleInfo, static::getRoleConstraints()); + return new static( + $name, + $roleInfo['threshold'], + $roleInfo['keyids'] + ); + } + + /** + * Checks if this role's key IDs match another role's. + * + * @param \Tuf\Role $other + * + * @return bool + */ + public function keysMatch(Role $other): bool + { + return $this->keyIds === $other->keyIds; + } + + /** + * Gets the role name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Gets the threshold required. + * + * @return int + * The threshold number of signatures required for the role. + */ + public function getThreshold(): int + { + return $this->threshold; + } + + + /** + * Checks whether the given key is authorized for the role. + * + * @param string $keyId + * The key ID to check. + * + * @return boolean + * TRUE if the key is authorized for the given role, or FALSE + * otherwise. + */ + public function isKeyIdAcceptable(string $keyId): bool + { + return in_array($keyId, $this->keyIds, true); + } +} diff --git a/libraries/src/TUF/src/RoleDB.php b/libraries/src/TUF/src/RoleDB.php new file mode 100644 index 00000000000..9f06b059773 --- /dev/null +++ b/libraries/src/TUF/src/RoleDB.php @@ -0,0 +1,116 @@ +getRoles($allowUntrustedAccess) as $roleName => $roleInfo) { + $db->addRole($roleInfo); + } + + return $db; + } + + /** + * Constructs a new RoleDB object. + */ + public function __construct() + { + $this->roles = []; + } + + /** + * Adds role metadata to the database. + * + * @param string $roleName + * The role name. + * @param \Tuf\Role $role + * The role to add. + * + * @return void + * + * @throws \Exception Thrown if the role already exists. + */ + public function addRole(Role $role): void + { + if ($this->roleExists($role->getName())) { + throw new RoleExistsException('Role already exists: ' . $role->getName()); + } + + $this->roles[$role->getName()] = $role; + } + + /** + * Verifies whether a given role name is stored in the role database. + * + * @param string $roleName + * The role name. + * + * @return boolean + * True if the role is found in the role database; false otherwise. + */ + public function roleExists(string $roleName): bool + { + return !empty($this->roles[$roleName]); + } + + /** + * Gets the role information. + * + * @param string $roleName + * The role name. + * + * @return \Tuf\Role + * The role. + * + * @throws \Tuf\Exception\NotFoundException + * Thrown if the role does not exist. + * + * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats + */ + public function getRole(string $roleName): Role + { + if (! $this->roleExists($roleName)) { + throw new NotFoundException($roleName, 'role'); + } + + /** @var \Tuf\Role $role */ + $role = $this->roles[$roleName]; + return $role; + } +} diff --git a/libraries/src/Table/Tuf.php b/libraries/src/Table/Tuf.php new file mode 100644 index 00000000000..0d4b7f9d178 --- /dev/null +++ b/libraries/src/Table/Tuf.php @@ -0,0 +1,31 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Table; + +\defined('JPATH_PLATFORM') or die; + +/** + * 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); + } +} From 0a3f0b4f3cace860535ac7063f41566b07fe32f9 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Wed, 23 Mar 2022 23:22:36 +0100 Subject: [PATCH 012/126] remove php-tuf library --- libraries/src/TUF/TufValidation.php | 3 - .../DurableStorageAccessValidator.php | 97 --- .../src/Client/DurableStorage/FileStorage.php | 82 --- .../src/TUF/src/Client/GuzzleFileFetcher.php | 172 ----- .../src/Client/RepoFileFetcherInterface.php | 56 -- .../src/TUF/src/Client/ResponseStream.php | 166 ----- .../src/TUF/src/Client/SignatureVerifier.php | 153 ----- libraries/src/TUF/src/Client/Updater.php | 600 ------------------ .../src/TUF/src/Constraints/Collection.php | 17 - .../src/Constraints/CollectionValidator.php | 30 - libraries/src/TUF/src/DelegatedRole.php | 96 --- .../src/Exception/Attack/AttackException.php | 15 - .../Attack/DenialOfServiceAttackException.php | 10 - .../Attack/FreezeAttackException.php | 10 - .../Exception/Attack/InvalidHashException.php | 63 -- .../Attack/RollbackAttackException.php | 10 - .../Attack/SignatureThresholdException.php | 10 - .../src/Exception/DownloadSizeException.php | 11 - .../src/TUF/src/Exception/FormatException.php | 30 - .../TUF/src/Exception/InvalidKeyException.php | 10 - .../TUF/src/Exception/MetadataException.php | 12 - .../TUF/src/Exception/NotFoundException.php | 32 - .../TUF/src/Exception/RepoFileNotFound.php | 11 - .../TUF/src/Exception/RoleExistsException.php | 10 - .../src/TUF/src/Exception/TufException.php | 10 - libraries/src/TUF/src/Helper/Clock.php | 22 - libraries/src/TUF/src/JsonNormalizer.php | 118 ---- libraries/src/TUF/src/Key.php | 131 ---- libraries/src/TUF/src/KeyDB.php | 125 ---- .../src/TUF/src/Metadata/ConstraintsTrait.php | 173 ----- libraries/src/TUF/src/Metadata/Factory.php | 71 --- .../TUF/src/Metadata/FileInfoMetadataBase.php | 27 - .../src/TUF/src/Metadata/MetadataBase.php | 255 -------- .../src/TUF/src/Metadata/RootMetadata.php | 100 --- .../src/TUF/src/Metadata/SnapshotMetadata.php | 72 --- .../src/TUF/src/Metadata/TargetsMetadata.php | 209 ------ .../TUF/src/Metadata/TimestampMetadata.php | 39 -- .../Metadata/Verifier/FileInfoVerifier.php | 48 -- .../src/Metadata/Verifier/RootVerifier.php | 63 -- .../Metadata/Verifier/SnapshotVerifier.php | 79 --- .../src/Metadata/Verifier/TargetsVerifier.php | 51 -- .../Metadata/Verifier/TimestampVerifier.php | 40 -- .../Verifier/TrustedAuthorityTrait.php | 78 --- .../Metadata/Verifier/UniversalVerifier.php | 92 --- .../src/Metadata/Verifier/VerifierBase.php | 140 ---- libraries/src/TUF/src/Role.php | 122 ---- libraries/src/TUF/src/RoleDB.php | 116 ---- 47 files changed, 3887 deletions(-) delete mode 100644 libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php delete mode 100644 libraries/src/TUF/src/Client/DurableStorage/FileStorage.php delete mode 100644 libraries/src/TUF/src/Client/GuzzleFileFetcher.php delete mode 100644 libraries/src/TUF/src/Client/RepoFileFetcherInterface.php delete mode 100644 libraries/src/TUF/src/Client/ResponseStream.php delete mode 100644 libraries/src/TUF/src/Client/SignatureVerifier.php delete mode 100644 libraries/src/TUF/src/Client/Updater.php delete mode 100644 libraries/src/TUF/src/Constraints/Collection.php delete mode 100644 libraries/src/TUF/src/Constraints/CollectionValidator.php delete mode 100644 libraries/src/TUF/src/DelegatedRole.php delete mode 100644 libraries/src/TUF/src/Exception/Attack/AttackException.php delete mode 100644 libraries/src/TUF/src/Exception/Attack/DenialOfServiceAttackException.php delete mode 100644 libraries/src/TUF/src/Exception/Attack/FreezeAttackException.php delete mode 100644 libraries/src/TUF/src/Exception/Attack/InvalidHashException.php delete mode 100644 libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php delete mode 100644 libraries/src/TUF/src/Exception/Attack/SignatureThresholdException.php delete mode 100644 libraries/src/TUF/src/Exception/DownloadSizeException.php delete mode 100644 libraries/src/TUF/src/Exception/FormatException.php delete mode 100644 libraries/src/TUF/src/Exception/InvalidKeyException.php delete mode 100644 libraries/src/TUF/src/Exception/MetadataException.php delete mode 100644 libraries/src/TUF/src/Exception/NotFoundException.php delete mode 100644 libraries/src/TUF/src/Exception/RepoFileNotFound.php delete mode 100644 libraries/src/TUF/src/Exception/RoleExistsException.php delete mode 100644 libraries/src/TUF/src/Exception/TufException.php delete mode 100644 libraries/src/TUF/src/Helper/Clock.php delete mode 100644 libraries/src/TUF/src/JsonNormalizer.php delete mode 100644 libraries/src/TUF/src/Key.php delete mode 100644 libraries/src/TUF/src/KeyDB.php delete mode 100644 libraries/src/TUF/src/Metadata/ConstraintsTrait.php delete mode 100644 libraries/src/TUF/src/Metadata/Factory.php delete mode 100644 libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php delete mode 100644 libraries/src/TUF/src/Metadata/MetadataBase.php delete mode 100644 libraries/src/TUF/src/Metadata/RootMetadata.php delete mode 100644 libraries/src/TUF/src/Metadata/SnapshotMetadata.php delete mode 100644 libraries/src/TUF/src/Metadata/TargetsMetadata.php delete mode 100644 libraries/src/TUF/src/Metadata/TimestampMetadata.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php delete mode 100644 libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php delete mode 100644 libraries/src/TUF/src/Role.php delete mode 100644 libraries/src/TUF/src/RoleDB.php diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 73c9a595bbb..521d11561f8 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -8,7 +8,6 @@ namespace Joomla\CMS\TUF; -use JLoader; use Joomla\CMS\Factory; use Joomla\Database\DatabaseDriver; use Joomla\Database\ParameterType; @@ -23,8 +22,6 @@ use Tuf\Exception\MetadataException; use Tuf\JsonNormalizer; -JLoader::registerNamespace('Tuf', JPATH_ROOT . '/libraries/src/TUF/src'); - \defined('JPATH_PLATFORM') or die; /** diff --git a/libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php b/libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php deleted file mode 100644 index 50da999d9cc..00000000000 --- a/libraries/src/TUF/src/Client/DurableStorage/DurableStorageAccessValidator.php +++ /dev/null @@ -1,97 +0,0 @@ -backend = $backend; - } - - /** - * Verifies that a given offset is valid. - * - * This is meant as a security measure to reduce the likelihood of - * undesired storage behavior. For example, a filesystem storage can't be - * tricked into executing in a different directory. - * - * @param mixed $offset - * The ArrayAccess offset. - * - * @return void - * - * @throws \OutOfBoundsException - * Thrown if the offset is not a string, or if it is not a valid - * filename (characters other than alphanumeric characters, periods, - * underscores, or hyphens). - */ - protected function throwIfInvalidOffset($offset): void - { - //if (! is_string($offset) || !preg_match("|^[\w._-]+$|", $offset)) { - if (! is_string($offset)) { - throw new \OutOfBoundsException("Array offset '$offset' is not a valid durable storage key."); - } - } - - /** - * {@inheritdoc} - */ - public function offsetExists($offset) - { - $this->throwIfInvalidOffset($offset); - return $this->backend->offsetExists($offset); - } - - /** - * {@inheritdoc} - */ - public function offsetGet($offset) - { - $this->throwIfInvalidOffset($offset); - return $this->backend->offsetGet($offset); - } - - /** - * {@inheritdoc} - */ - public function offsetSet($offset, $value) - { - $this->throwIfInvalidOffset($offset); - // @todo Consider enforcing an application-configurable maximum length - // here. https://github.com/php-tuf/php-tuf/issues/27 - if (! is_string($value)) { - $format = "Cannot store %s at offset $offset: only strings are allowed in durable storage."; - throw new \UnexpectedValueException(sprintf($format, gettype($value))); - } - $this->backend->offsetSet($offset, $value); - } - - /** - * {@inheritdoc} - */ - public function offsetUnset($offset) - { - $this->throwIfInvalidOffset($offset); - $this->backend->offsetUnset($offset); - } -} diff --git a/libraries/src/TUF/src/Client/DurableStorage/FileStorage.php b/libraries/src/TUF/src/Client/DurableStorage/FileStorage.php deleted file mode 100644 index 271a5c97094..00000000000 --- a/libraries/src/TUF/src/Client/DurableStorage/FileStorage.php +++ /dev/null @@ -1,82 +0,0 @@ -basePath = $basePath; - } - - /** - * Returns a full path for an item in the storage. - * - * @param mixed $offset - * The ArrayAccess offset for the item. - * - * @return string - * The full path for the item in the storage. - */ - protected function pathWithBasePath($offset): string - { - return $this->basePath . DIRECTORY_SEPARATOR . $offset; - } - - /** - * {@inheritdoc} - */ - public function offsetExists($offset) - { - return file_exists($this->pathWithBasePath($offset)); - } - - /** - * {@inheritdoc} - */ - public function offsetGet($offset) - { - return file_get_contents($this->pathWithBasePath($offset)); - } - - /** - * {@inheritdoc} - */ - public function offsetSet($offset, $value) - { - file_put_contents($this->pathWithBasePath($offset), $value); - } - - /** - * {@inheritdoc} - */ - public function offsetUnset($offset) - { - @unlink($this->pathWithBasePath($offset)); - } -} diff --git a/libraries/src/TUF/src/Client/GuzzleFileFetcher.php b/libraries/src/TUF/src/Client/GuzzleFileFetcher.php deleted file mode 100644 index 163bfb8914b..00000000000 --- a/libraries/src/TUF/src/Client/GuzzleFileFetcher.php +++ /dev/null @@ -1,172 +0,0 @@ -client = $client; - $this->metadataPrefix = $metadataPrefix; - $this->targetsPrefix = $targetsPrefix; - } - - /** - * Creates an instance of this class with a specific base URI. - * - * @param string $baseUri - * The base URI from which to fetch files. - * @param string $metadataPrefix - * (optional) The path prefix for metadata. Defaults to '/metadata/'. - * @param string $targetsPrefix - * (optional) The path prefix for targets. Defaults to '/targets/'. - * - * @return static - * A new instance of this class. - */ - public static function createFromUri(string $baseUri, string $metadataPrefix = '/metadata/', string $targetsPrefix = '/targets/'): self - { - $client = new Client(['base_uri' => $baseUri]); - return new static($client, $metadataPrefix, $targetsPrefix); - } - - /** - * {@inheritDoc} - */ - public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface - { - return $this->fetchFile($this->metadataPrefix . $fileName, $maxBytes); - } - - /** - * {@inheritDoc} - * - * @param array $options - * (optional) Additional request options to pass to the Guzzle client. - * See \GuzzleHttp\RequestOptions. - * @param string $url - * (optional) An arbitrary URL from which the target should be downloaded. - * If passed, takes precedence over $fileName. - */ - public function fetchTarget(string $fileName, int $maxBytes, array $options = [], string $url = null): PromiseInterface - { - $location = $url ?: $this->targetsPrefix . $fileName; - return $this->fetchFile($location, $maxBytes, $options); - } - - /** - * Fetches a file from a URL. - * - * @param string $url - * The URL of the file to fetch. - * @param integer $maxBytes - * The maximum number of bytes to download. - * @param array $options - * (optional) Additional request options to pass to the Guzzle client. - * See \GuzzleHttp\RequestOptions. - * - * @return \GuzzleHttp\Promise\PromiseInterface - * A promise representing the eventual result of the operation. - */ - protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface - { - // Create a progress callback to abort the download if it exceeds - // $maxBytes. This will only work with cURL, so we also verify the - // download size when request is finished. - $progress = function (int $expectedBytes, int $downloadedBytes) use ($url, $maxBytes) { - if ($expectedBytes > $maxBytes || $downloadedBytes > $maxBytes) { - throw new DownloadSizeException("$url exceeded $maxBytes bytes"); - } - }; - $options += [RequestOptions::PROGRESS => $progress]; - - return $this->client->requestAsync('GET', $url, $options) - ->then( - function (ResponseInterface $response) { - return new ResponseStream($response); - }, - $this->onRejected($url) - ); - } - - /** - * Creates a callback function for when the promise is rejected. - * - * @param string $fileName - * The file name being fetched from the remote repo. - * - * @return \Closure - * The callback function. - */ - private function onRejected(string $fileName): \Closure - { - return function (\Throwable $e) use ($fileName) { - if ($e instanceof ClientException) { - if ($e->getCode() === 404) { - throw new RepoFileNotFound("$fileName not found", 0, $e); - } else { - // Re-throwing the original exception will blow away the - // backtrace, so wrap the exception in a more generic one to aid - // in debugging. - throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); - } - } - throw $e; - }; - } - - /** - * {@inheritDoc} - */ - public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string - { - try { - return $this->fetchMetadata($fileName, $maxBytes)->wait(); - } catch (RepoFileNotFound $exception) { - return null; - } - } -} diff --git a/libraries/src/TUF/src/Client/RepoFileFetcherInterface.php b/libraries/src/TUF/src/Client/RepoFileFetcherInterface.php deleted file mode 100644 index e776fb51876..00000000000 --- a/libraries/src/TUF/src/Client/RepoFileFetcherInterface.php +++ /dev/null @@ -1,56 +0,0 @@ -response = $response; - } - - /** - * Returns the response that produced this stream. - * - * @return \Psr\Http\Message\ResponseInterface - * The response. - */ - public function getResponse(): ResponseInterface - { - return $this->response; - } - - /** - * {@inheritDoc} - */ - public function __toString() - { - return $this->getResponse()->getBody()->__toString(); - } - - /** - * {@inheritDoc} - */ - public function close() - { - $this->getResponse()->getBody()->close(); - } - - /** - * {@inheritDoc} - */ - public function detach() - { - return $this->getResponse()->getBody()->detach(); - } - - /** - * {@inheritDoc} - */ - public function getSize() - { - return $this->getResponse()->getBody()->getSize(); - } - - /** - * {@inheritDoc} - */ - public function tell() - { - return $this->getResponse()->getBody()->tell(); - } - - /** - * {@inheritDoc} - */ - public function eof() - { - return $this->getResponse()->getBody()->eof(); - } - - /** - * {@inheritDoc} - */ - public function isSeekable() - { - return $this->getResponse()->getBody()->isSeekable(); - } - - /** - * {@inheritDoc} - */ - public function seek($offset, $whence = SEEK_SET) - { - return $this->getResponse()->getBody()->seek($offset, $whence); - } - - /** - * {@inheritDoc} - */ - public function rewind() - { - return $this->getResponse()->getBody()->rewind(); - } - - /** - * {@inheritDoc} - */ - public function isWritable() - { - return $this->getResponse()->getBody()->isWritable(); - } - - /** - * {@inheritDoc} - */ - public function write($string) - { - return $this->getResponse()->getBody()->write($string); - } - - /** - * {@inheritDoc} - */ - public function isReadable() - { - return $this->getResponse()->getBody()->isReadable(); - } - - /** - * {@inheritDoc} - */ - public function read($length) - { - return $this->getResponse()->getBody()->read($length); - } - - /** - * {@inheritDoc} - */ - public function getContents() - { - return $this->getResponse()->getBody()->getContents(); - } - - /** - * {@inheritDoc} - */ - public function getMetadata($key = null): string - { - return $this->getResponse()->getBody()->getMetadata($key); - } -} diff --git a/libraries/src/TUF/src/Client/SignatureVerifier.php b/libraries/src/TUF/src/Client/SignatureVerifier.php deleted file mode 100644 index 3395498207d..00000000000 --- a/libraries/src/TUF/src/Client/SignatureVerifier.php +++ /dev/null @@ -1,153 +0,0 @@ -roleDb = $roleDb; - $this->keyDb = $keyDb; - } - - /** - * Creates a SignatureVerifier object from a RootMetadata object. - * - * @param RootMetadata $rootMetadata - * @param bool $allowUntrustedAccess - * - * @return static - */ - public static function createFromRootMetadata(RootMetadata $rootMetadata, bool $allowUntrustedAccess = false): self - { - return new static( - RoleDB::createFromRootMetadata($rootMetadata, $allowUntrustedAccess), - KeyDB::createFromRootMetadata($rootMetadata, $allowUntrustedAccess) - ); - } - - /** - * Checks signatures on a verifiable structure. - * - * @param \Tuf\Metadata\MetadataBase $metadata - * The metadata to check signatures on. - * - * @return void - * - * @throws \Tuf\Exception\Attack\SignatureThresholdException - * Thrown if the signature threshold has not be reached. - */ - public function checkSignatures(MetadataBase $metadata): void - { - $signatures = $metadata->getSignatures(); - - $role = $this->roleDb->getRole($metadata->getRole()); - $needVerified = $role->getThreshold(); - $verifiedKeySignatures = []; - - $canonicalBytes = JsonNormalizer::asNormalizedJson($metadata->getSigned()); - - foreach ($signatures as $signature) - { - // Don't allow the same key to be counted twice. - if ($role->isKeyIdAcceptable($signature['keyid']) && $this->verifySingleSignature($canonicalBytes, $signature)) - { - $verifiedKeySignatures[$signature['keyid']] = true; - } - - // @todo Determine if we should check all signatures and warn for - // bad signatures even if this method returns TRUE because the - // threshold has been met. - // https://github.com/php-tuf/php-tuf/issues/172 - if (count($verifiedKeySignatures) >= $needVerified) - { - break; - } - } - - if (count($verifiedKeySignatures) < $needVerified) - { - throw new SignatureThresholdException("Signature threshold not met on " . $metadata->getRole()); - } - } - - /** - * Verifies a single signature. - * - * @param string $bytes - * The canonical JSON string of the 'signed' section of the given file. - * @param \ArrayAccess $signatureMeta - * The ArrayAccess object of metadata for the signature. Each signature - * metadata contains two elements: - * - keyid: The identifier of the key signing the role data. - * - sig: The hex-encoded signature of the canonical form of the - * metadata for the role. - * - * @return boolean - * TRUE if the signature is valid for $bytes. - */ - private function verifySingleSignature(string $bytes, \ArrayAccess $signatureMeta): bool - { - // Get the pubkey from the key database. - $pubkey = $this->keyDb->getKey($signatureMeta['keyid'])->getPublic(); - - // Encode the pubkey and signature, and check that the signature is - // valid for the given data and pubkey. - $pubkeyBytes = hex2bin($pubkey); - $sigBytes = hex2bin($signatureMeta['sig']); - - // @todo Check that the key type in $signatureMeta is ed25519; return - // false if not. - // https://github.com/php-tuf/php-tuf/issues/168 - return \sodium_crypto_sign_verify_detached($sigBytes, $bytes, $pubkeyBytes); - } - - /** - * Adds a role to the signature verifier. - * - * @param \Tuf\Role $role - */ - public function addRole(Role $role): void - { - if (!$this->roleDb->roleExists($role->getName())) - { - $this->roleDb->addRole($role); - } - } - - /** - * Adds a key to the signature verifier. - * - * @param string $keyId - * @param \Tuf\Key $key - */ - public function addKey(string $keyId, Key $key): void - { - $this->keyDb->addKey($keyId, $key); - } -} diff --git a/libraries/src/TUF/src/Client/Updater.php b/libraries/src/TUF/src/Client/Updater.php deleted file mode 100644 index d993141105f..00000000000 --- a/libraries/src/TUF/src/Client/Updater.php +++ /dev/null @@ -1,600 +0,0 @@ -repoFileFetcher = $repoFileFetcher; - $this->mirrors = $mirrors; - $this->durableStorage = new DurableStorageAccessValidator($durableStorage); - $this->clock = new Clock(); - $this->metadataFactory = new MetadataFactory($this->durableStorage); - } - - /** - * Gets the type for the file name. - * - * @param string $fileName - * The file name. - * - * @return string - * The type. - */ - private static function getFileNameType(string $fileName): string - { - $parts = explode('.', $fileName); - array_pop($parts); - return array_pop($parts); - } - - /** - * @todo Add docs. See python comments: - * https://github.com/theupdateframework/tuf/blob/1cf085a360aaad739e1cc62fa19a2ece270bb693/tuf/client/updater.py#L999 - * https://github.com/php-tuf/php-tuf/issues/162 - * @todo The Python implementation has an optional flag to "unsafely update - * root if necessary". Do we need it? - * https://github.com/php-tuf/php-tuf/issues/21 - * - * @param bool $force - * (optional) If false, return early if this updater has already been - * refreshed. Defaults to false. - * - * @return boolean - * TRUE if the data was successfully refreshed. - * - * @see https://github.com/php-tuf/php-tuf/issues/21 - * - * @throws \Tuf\Exception\MetadataException - * Throw if an upated root metadata file is not valid. - * @throws \Tuf\Exception\Attack\FreezeAttackException - * Throw if a freeze attack is detected. - * @throws \Tuf\Exception\Attack\RollbackAttackException - * Throw if a rollback attack is detected. - * @throws \Tuf\Exception\Attack\SignatureThresholdException - * Thrown if the signature threshold has not be reached. - */ - public function refresh(bool $force = false): bool - { - if ($force) { - $this->isRefreshed = false; - $this->metadataExpiration = null; - } - if ($this->isRefreshed) { - return true; - } - - // § 5.1 - $this->metadataExpiration = $this->getUpdateStartTime(); - - // § 5.2 - /** @var \Tuf\Metadata\RootMetadata $rootData */ - $rootData = $this->metadataFactory->load('root'); - - $this->signatureVerifier = SignatureVerifier::createFromRootMetadata($rootData); - $this->universalVerifier = new UniversalVerifier($this->metadataFactory, $this->signatureVerifier, $this->metadataExpiration); - - // § 5.3 - $this->updateRoot($rootData); - - // § 5.4 - $newTimestampData = $this->updateTimestamp(); - - $snapshotInfo = $newTimestampData->getFileMetaInfo('snapshot.json'); - $snapShotVersion = $snapshotInfo['version']; - - // § 5.5 - if ($rootData->supportsConsistentSnapshots()) { - // § 5.5.1 - $newSnapshotContents = $this->fetchFile("$snapShotVersion.snapshot.json"); - - $newSnapshotData = SnapshotMetadata::createFromJson($newSnapshotContents); - - $this->universalVerifier->verify(SnapshotMetadata::TYPE, $newSnapshotData); - - // § 5.5.7 - // TODO: here change .json to _json - $this->durableStorage['snapshot_json'] = $newSnapshotContents; - } else { - // @todo Add support for not using consistent snapshots in - // https://github.com/php-tuf/php-tuf/issues/97 - throw new \UnexpectedValueException("Currently only repos using consistent snapshots are supported."); - } - - // § 5.6 - if ($rootData->supportsConsistentSnapshots()) { - $this->fetchAndVerifyTargetsMetadata('targets'); - } else { - // @todo Add support for not using consistent snapshots in - // https://github.com/php-tuf/php-tuf/issues/97 - throw new \UnexpectedValueException("Currently only repos using consistent snapshots are supported."); - } - $this->isRefreshed = true; - return true; - } - - /** - * Updates the timestamp role, per section 5.3 of the TUF spec. - */ - private function updateTimestamp(): TimestampMetadata - { - // § 5.4.1 - $newTimestampContents = $this->fetchFile('timestamp.json'); - $newTimestampData = TimestampMetadata::createFromJson($newTimestampContents); - - $this->universalVerifier->verify(TimestampMetadata::TYPE, $newTimestampData); - - // § 5.4.5: Persist timestamp metadata - // TODO: here change .json to _json - $this->durableStorage['timestamp_json'] = $newTimestampContents; - - return $newTimestampData; - } - - - - /** - * Updates the root metadata if needed. - * - * @param \Tuf\Metadata\RootMetadata $rootData - * The current root metadata. - * - * @return void - *@throws \Tuf\Exception\Attack\FreezeAttackException - * Throw if a freeze attack is detected. - * @throws \Tuf\Exception\Attack\RollbackAttackException - * Throw if a rollback attack is detected. - * @throws \Tuf\Exception\Attack\SignatureThresholdException - * Thrown if an updated root file is not signed with the need signatures. - * - * @throws \Tuf\Exception\MetadataException - * Throw if an upated root metadata file is not valid. - */ - private function updateRoot(RootMetadata &$rootData): void - { - // § 5.3.1 needs no action, since we currently require consistent - // snapshots. - $rootsDownloaded = 0; - $originalRootData = $rootData; - // § 5.3.2 and 5.3.3 - $nextVersion = $rootData->getVersion() + 1; - while ($nextRootContents = $this->repoFileFetcher->fetchMetadataIfExists("$nextVersion.root.json", static::MAXIMUM_DOWNLOAD_BYTES)) { - $rootsDownloaded++; - if ($rootsDownloaded > static::MAX_ROOT_DOWNLOADS) { - throw new DenialOfServiceAttackException("The maximum number root files have already been downloaded: " . static::MAX_ROOT_DOWNLOADS); - } - $nextRoot = RootMetadata::createFromJson($nextRootContents); - $this->universalVerifier->verify(RootMetadata::TYPE, $nextRoot); - - // § 5.3.6 Needs no action. The expiration of the new (intermediate) - // root metadata file does not matter yet, because we will check for - // it in § 5.3.10. - // § 5.3.7 - $rootData = $nextRoot; - - // § 5.3.8 - // TODO: here change .json to _json - $this->durableStorage['root_json'] = $nextRootContents; - // § 5.3.9: repeat from § 5.3.2. - $nextVersion = $rootData->getVersion() + 1; - } - // § 5.3.10 - RootVerifier::checkFreezeAttack($rootData, $this->metadataExpiration); - - // § 5.3.11: Delete the trusted timestamp and snapshot files if either - // file has rooted keys. - if ($rootsDownloaded && - (static::hasRotatedKeys($originalRootData, $rootData, 'timestamp') - || static::hasRotatedKeys($originalRootData, $rootData, 'snapshot'))) { - unset($this->durableStorage['timestamp_json'], $this->durableStorage['snapshot_json']); - } - // § 5.3.12 needs no action because we currently require consistent - // snapshots. - } - - /** - * Determines if the new root metadata has rotated keys for a role. - * - * @param \Tuf\Metadata\RootMetadata $previousRootData - * The previous root metadata. - * @param \Tuf\Metadata\RootMetadata $newRootData - * The new root metadta. - * @param string $role - * The role to check for rotated keys. - * - * @return boolean - * True if the keys for the role have been rotated, otherwise false. - */ - private static function hasRotatedKeys(RootMetadata $previousRootData, RootMetadata $newRootData, string $role): bool - { - $previousRole = $previousRootData->getRoles()[$role] ?? null; - $newRole = $newRootData->getRoles()[$role] ?? null; - if ($previousRole && $newRole) { - return !$previousRole->keysMatch($newRole); - } - return false; - } - - /** - * Synchronously fetches a file from the remote repo. - * - * @param string $fileName - * The name of the file to fetch. - * @param integer $maxBytes - * (optional) The maximum number of bytes to download. - * - * @return string - * The contents of the fetched file. - */ - private function fetchFile(string $fileName, int $maxBytes = self::MAXIMUM_DOWNLOAD_BYTES): string - { - return $this->repoFileFetcher->fetchMetadata($fileName, $maxBytes) - ->then(function (StreamInterface $data) use ($fileName, $maxBytes) { - $this->checkLength($data, $maxBytes, $fileName); - return $data; - }) - ->wait(); - } - - /** - * Verifies the length of a data stream. - * - * @param \Psr\Http\Message\StreamInterface $data - * The data stream to check. - * @param int $maxBytes - * The maximum acceptable length of the stream, in bytes. - * @param string $fileName - * The filename associated with the stream. - * - * @throws \Tuf\Exception\DownloadSizeException - * If the stream's length exceeds $maxBytes in size. - */ - protected function checkLength(StreamInterface $data, int $maxBytes, string $fileName): void - { - $error = new DownloadSizeException("$fileName exceeded $maxBytes bytes"); - $size = $data->getSize(); - - if (isset($size)) { - if ($size > $maxBytes) { - throw $error; - } - } else { - // @todo Handle non-seekable streams. - // https://github.com/php-tuf/php-tuf/issues/169 - $data->rewind(); - $data->read($maxBytes); - - // If we reached the end of the stream, we didn't exceed the - // maximum number of bytes. - if ($data->eof() === false) { - throw $error; - } - $data->rewind(); - } - } - - /** - * Verifies a stream of data against a known TUF target. - * - * @param string $target - * The path of the target file. Needs to be known to the most recent - * targets metadata downloaded in ::refresh(). - * @param \Psr\Http\Message\StreamInterface $data - * A stream pointing to the downloaded target data. - * - * @throws \Tuf\Exception\MetadataException - * If the target has no trusted hash(es). - * @throws \Tuf\Exception\Attack\InvalidHashException - * If the data stream does not match the known hash(es) for the target. - */ - protected function verify(string $target, StreamInterface $data): void - { - $this->refresh(); - - $targetsMetadata = $this->getMetadataForTarget($target); - if ($targetsMetadata === null) { - throw new NotFoundException($target, 'Target'); - } - $maxBytes = $targetsMetadata->getLength($target) ?? static::MAXIMUM_DOWNLOAD_BYTES; - $this->checkLength($data, $maxBytes, $target); - - $hashes = $targetsMetadata->getHashes($target); - if (count($hashes) === 0) { - // § 5.7.2 - throw new MetadataException("No trusted hashes are available for '$target'"); - } - foreach ($hashes as $algo => $hash) { - // If the stream has a URI that refers to a file, use - // hash_file() to verify it. Otherwise, read the entire stream - // as a string and use hash() to verify it. - $uri = $data->getMetadata('uri'); - if ($uri && file_exists($uri)) { - $streamHash = hash_file($algo, $uri); - } else { - $streamHash = hash($algo, $data->getContents()); - $data->rewind(); - } - - if ($hash !== $streamHash) { - throw new InvalidHashException($data, "Invalid $algo hash for $target"); - } - } - } - - /** - * Downloads a target file, verifies it, and returns its contents. - * - * @param string $target - * The path of the target file. Needs to be known to the most recent - * targets metadata downloaded in ::refresh(). - * @param mixed ...$extra - * Additional arguments to pass to the file fetcher. - * - * @return \GuzzleHttp\Promise\PromiseInterface - * A promise representing the eventual verified result of the download - * operation. - */ - public function download(string $target, ...$extra): PromiseInterface - { - $this->refresh(); - - $targetsMetadata = $this->getMetadataForTarget($target); - if ($targetsMetadata === null) { - return new RejectedPromise(new NotFoundException($target, 'Target')); - } - - // If the target isn't known, immediately return a rejected promise. - try { - $length = $targetsMetadata->getLength($target) ?? static::MAXIMUM_DOWNLOAD_BYTES; - } catch (NotFoundException $e) { - return new RejectedPromise($e); - } - - return $this->repoFileFetcher->fetchTarget($target, $length, ...$extra) - ->then(function (StreamInterface $stream) use ($target) { - $this->verify($target, $stream); - return $stream; - }); - } - - /** - * Gets a target metadata object that contains the specified target, if any. - * - * @param string $target - * The path of the target file. - * - * @return \Tuf\Metadata\TargetsMetadata|null - * The targets metadata with information about the desired target, or null if no relevant metadata is found. - */ - protected function getMetadataForTarget(string $target): ?TargetsMetadata - { - // Search the top level targets metadata. - /** @var \Tuf\Metadata\TargetsMetadata $targetsMetadata */ - $targetsMetadata = $this->metadataFactory->load('targets'); - if ($targetsMetadata->hasTarget($target)) { - return $targetsMetadata; - } - // Recursively search any delegated roles. - return $this->searchDelegatedRolesForTarget($targetsMetadata, $target, ['targets']); - } - - /** - * Fetches and verifies a targets metadata file. - * - * The metadata file will be stored as '$role_json'. - * - * @param string $role - * The role name. This may be 'targets' or a delegated role. - */ - private function fetchAndVerifyTargetsMetadata(string $role): void - { - $newSnapshotData = $this->metadataFactory->load('snapshot'); - $targetsVersion = $newSnapshotData->getFileMetaInfo($role. ".json")['version']; - // § 5.6.1 - $newTargetsContent = $this->fetchFile("$targetsVersion.$role.json"); - $newTargetsData = TargetsMetadata::createFromJson($newTargetsContent, $role); - $this->universalVerifier->verify(TargetsMetadata::TYPE, $newTargetsData); - // § 5.5.6 - // TODO: here change .json to _json - $this->durableStorage[$role . "_json"] = $newTargetsContent; - } - - /** - * Returns the time that the update began. - * - * @return \DateTimeImmutable - * The time that the update began. - */ - private function getUpdateStartTime(): \DateTimeImmutable - { - return (new \DateTimeImmutable())->setTimestamp($this->clock->getCurrentTime()); - } - - /** - * Searches delegated roles for metadata concerning a specific target. - * - * @param \Tuf\Metadata\TargetsMetadata|null $targetsMetadata - * The targets metadata to search. - * @param string $target - * The path of the target file. - * @param string[] $searchedRoles - * The roles that have already been searched. This is for internal use only and should not be passed by calling code. - * @param bool $terminated - * (optional) For internal recursive calls only. This will be set to true if a terminating delegation is found in - * the search. - * - * - * @return \Tuf\Metadata\TargetsMetadata|null - * The target metadata that contains the metadata for the target or null if the target is not found. - */ - private function searchDelegatedRolesForTarget(TargetsMetadata $targetsMetadata, string $target, array $searchedRoles, bool &$terminated = false): ?TargetsMetadata - { - foreach ($targetsMetadata->getDelegatedKeys() as $keyId => $delegatedKey) { - $this->signatureVerifier->addKey($keyId, $delegatedKey); - } - foreach ($targetsMetadata->getDelegatedRoles() as $delegatedRole) { - $delegatedRoleName = $delegatedRole->getName(); - if (in_array($delegatedRoleName, $searchedRoles, true)) { - // § 5.6.7.1 - // If this role has been visited before, skip it (to avoid cycles in the delegation graph). - continue; - } - // § 5.6.7.1 - if (count($searchedRoles) > static::MAXIMUM_TARGET_ROLES) { - return null; - } - - $this->signatureVerifier->addRole($delegatedRole); - // Targets must match the paths of all roles in the delegation chain, so if the path does not match, - // do not evaluate this role or any roles it delegates to. - if ($delegatedRole->matchesPath($target)) { - $this->fetchAndVerifyTargetsMetadata($delegatedRoleName); - /** @var \Tuf\Metadata\TargetsMetadata $delegatedTargetsMetadata */ - $delegatedTargetsMetadata = $this->metadataFactory->load($delegatedRoleName); - if ($delegatedTargetsMetadata->hasTarget($target)) { - return $delegatedTargetsMetadata; - } - $searchedRoles[] = $delegatedRoleName; - // § 5.6.7.2.1 - // Recursively search the list of delegations in order of appearance. - $delegatedRolesMetadataSearchResult = $this->searchDelegatedRolesForTarget($delegatedTargetsMetadata, $target, $searchedRoles, $terminated); - if ($terminated || $delegatedRolesMetadataSearchResult) { - return $delegatedRolesMetadataSearchResult; - } - - // If $delegatedRole is terminating then we do not search any of the next delegated roles after it - // in the delegations from $targetsMetadata. - if ($delegatedRole->isTerminating()) { - $terminated = true; - // § 5.6.7.2.2 - // If the role is terminating then abort searching for a target. - return null; - } - } - } - return null; - } -} diff --git a/libraries/src/TUF/src/Constraints/Collection.php b/libraries/src/TUF/src/Constraints/Collection.php deleted file mode 100644 index 77cae65fe82..00000000000 --- a/libraries/src/TUF/src/Constraints/Collection.php +++ /dev/null @@ -1,17 +0,0 @@ -unsupportedFields as $unsupportedField) { - $existsInArray = \is_array($value) && \array_key_exists($unsupportedField, $value); - $existsInArrayAccess = $value instanceof \ArrayAccess && $value->offsetExists($unsupportedField); - if ($existsInArray || $existsInArrayAccess) { - $this->context->buildViolation('This field is not supported.') - ->atPath("[$unsupportedField]") - ->setInvalidValue(null) - ->setCode(Collection::MISSING_FIELD_ERROR) - ->addViolation(); - } - } - parent::validate($value, $constraint); - } -} diff --git a/libraries/src/TUF/src/DelegatedRole.php b/libraries/src/TUF/src/DelegatedRole.php deleted file mode 100644 index 6c3744c8e86..00000000000 --- a/libraries/src/TUF/src/DelegatedRole.php +++ /dev/null @@ -1,96 +0,0 @@ -terminating; - } - - /** - * DelegatedRole constructor. - * - * @param string $name - * @param int $threshold - * @param array $keyIds - * @param array $paths - * @param bool $terminating - */ - private function __construct(string $name, int $threshold, array $keyIds, array $paths, bool $terminating) - { - parent::__construct($name, $threshold, $keyIds); - $this->paths = $paths; - $this->terminating = $terminating; - } - - public static function createFromMetadata(\ArrayObject $roleInfo, string $name = null): Role - { - $roleConstraints = static::getRoleConstraints(); - $roleConstraints->fields += [ - 'name' => new Required( - [ - new Type('string'), - new NotBlank(), - ] - ), - 'terminating' => new Required(new Type('boolean')), - 'paths' => new Required(new Type('array')), - ]; - static::validate($roleInfo, $roleConstraints); - return new static( - $roleInfo['name'], - $roleInfo['threshold'], - $roleInfo['keyids'], - $roleInfo['paths'], - $roleInfo['terminating'] - ); - } - - /** - * Determines whether a target matches a path for this role. - * - * @param string $target - * The path of the target file. - * - * @return bool - * True if there is path match or no path criteria is set for the role, or - * false otherwise. - */ - public function matchesPath(string $target): bool - { - if ($this->paths) { - foreach ($this->paths as $path) { - if (fnmatch($path, $target)) { - return true; - } - } - return false; - } - // If no paths are set then any target is a match. - return true; - } -} diff --git a/libraries/src/TUF/src/Exception/Attack/AttackException.php b/libraries/src/TUF/src/Exception/Attack/AttackException.php deleted file mode 100644 index d267778ee60..00000000000 --- a/libraries/src/TUF/src/Exception/Attack/AttackException.php +++ /dev/null @@ -1,15 +0,0 @@ -stream = $stream; - } - - /** - * Returns the untrusted stream object pointing to the downloaded target. - * - * WARNING: The contents of the stream failed TUF validation. Any code - * interacting it should treat it as unsafe and proceed with great caution. - * - * @return \Psr\Http\Message\StreamInterface - * The stream object. - */ - public function getStream(): StreamInterface - { - return $this->stream; - } -} diff --git a/libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php b/libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php deleted file mode 100644 index 26a7a021731..00000000000 --- a/libraries/src/TUF/src/Exception/Attack/RollbackAttackException.php +++ /dev/null @@ -1,10 +0,0 @@ -ksort(); - } elseif (is_object($structure)) { - throw new \RuntimeException('\Tuf\JsonNormalizer::rKeySort() is not intended to sort objects except \ArrayObject. Found: ' . get_class($structure)); - } - - foreach ($structure as $key => $value) { - if (is_array($value) || $value instanceof \ArrayObject) { - self::rKeySort($structure[$key]); - } - } - } - - /** - * Replaces all instance of \stdClass in the data structure with \ArrayObject. - * - * Symfony Validator library's built-in constraints cannot validate - * \stdClass objects. This method should only be used with the return value - * of json_decode therefore should not contain any objects except instances - * of \stdClass. - * - * @param array|\stdClass $data - * The data to convert. The data structure should contain no objects - * except \stdClass instances. - * - * @return iterable - * The data with all stdClass instances replaced with ArrayObject. - * - * @throws \RuntimeException - * Thrown if the an object other than \stdClass is found. - */ - private static function replaceStdClassWithArrayObject($data): iterable - { - if ($data instanceof \stdClass) { - $data = new \ArrayObject($data); - } elseif (!is_array($data)) { - throw new \RuntimeException('Cannot convert type: ' . get_class($data)); - } - foreach ($data as $key => $datum) { - if (is_array($datum) || is_object($datum)) { - $data[$key] = static::replaceStdClassWithArrayObject($datum); - } - } - return $data; - } -} diff --git a/libraries/src/TUF/src/Key.php b/libraries/src/TUF/src/Key.php deleted file mode 100644 index 46bf17a1e74..00000000000 --- a/libraries/src/TUF/src/Key.php +++ /dev/null @@ -1,131 +0,0 @@ -type = $type; - $this->scheme = $scheme; - $this->public = $public; - } - - /** - * Creates a key object from TUF metadata. - * - * @param \ArrayObject $keyInfo - * The key information from TUF metadata including. - * - keytype: The public key signature system, e.g. 'ed25519'. - * - scheme: The corresponding signature scheme, e.g. 'ed25519'. - * - keyval: An associative array containing the public key value. - - * - * @return static - * - * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats - */ - public static function createFromMetadata(\ArrayObject $keyInfo): self - { - self::validate($keyInfo, static::getKeyConstraints()); - return new static( - $keyInfo['keytype'], - $keyInfo['scheme'], - $keyInfo['keyval']['public'] - ); - } - - /** - * Gets the public key value. - * - * @return string - * The public key value. - */ - public function getPublic(): string - { - return $this->public; - } - - /** - * Gets the key type. - * - * @return string - * The key type. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Computes the key ID. - * - * Per specification section 4.2, the KEYID is a hexdigest of the SHA-256 - * hash of the canonical form of the key. - * - * @return string - * The key ID in hex format for the key metadata hashed using sha256. - * - * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats - * - * @todo https://github.com/php-tuf/php-tuf/issues/56 - */ - public function getComputedKeyId(): string - { - // @see https://github.com/secure-systems-lab/securesystemslib/blob/master/securesystemslib/keys.py - // The keyid_hash_algorithms array value is based on the TUF settings, - // it's not expected to be part of the key metadata. The fact that it is - // currently included is a quirk of the TUF python code that may be - // fixed in future versions. Calculate using the normal TUF settings - // since this is how it's calculated in the securesystemslib code and - // any value for keyid_hash_algorithms in the key data in root.json is - // ignored. - $keyCanonicalStruct = [ - 'keytype' => $this->getType(), - 'scheme' => $this->scheme, - 'keyid_hash_algorithms' => ['sha256', 'sha512'], - 'keyval' => ['public' => $this->getPublic()], - ]; - $keyCanonicalForm = JsonNormalizer::asNormalizedJson($keyCanonicalStruct); - - return hash('sha256', $keyCanonicalForm, false); - } -} diff --git a/libraries/src/TUF/src/KeyDB.php b/libraries/src/TUF/src/KeyDB.php deleted file mode 100644 index 43ce954b482..00000000000 --- a/libraries/src/TUF/src/KeyDB.php +++ /dev/null @@ -1,125 +0,0 @@ -getKeys($allowUntrustedAccess) as $keyId => $key) { - $db->addKey($keyId, $key); - } - - return $db; - } - - /** - * Gets the supported encryption key types. - * - * @return string[] - * An array of supported encryption key type names (e.g. 'ed25519'). - */ - public static function getSupportedKeyTypes(): array - { - return ['ed25519']; - } - - /** - * Constructs a new KeyDB. - */ - public function __construct() - { - $this->keys = []; - } - - /** - * Adds key metadata to the key database while avoiding duplicates. - * - * @param string $keyId - * The key ID given as the object key in root.json or another keys list. - * @param \Tuf\Key - * The key. - * - * @return void - * - * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats - */ - public function addKey(string $keyId, Key $key): void - { - if (! in_array($key->getType(), self::getSupportedKeyTypes(), true)) { - // @todo Convert this to a log line as per Python. - // https://github.com/php-tuf/php-tuf/issues/160 - throw new InvalidKeyException("Root metadata file contains an unsupported key type: \"${keyMeta['keytype']}\""); - } - // Per TUF specification 4.3, Clients MUST calculate each KEYID to - // verify this is correct for the associated key. - if ($keyId !== $key->getComputedKeyId()) { - throw new InvalidKeyException('The calculated KEYID does not match the value provided.'); - } - $this->keys[$keyId] = $key; - } - - /** - * Returns the key metadata for a given key ID. - * - * @param string $keyId - * The key ID. - * - * @return \Tuf\Key - * The key. - * - * @throws \Tuf\Exception\NotFoundException - * Thrown if the key ID is not found in the keydb database. - * - * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats - */ - public function getKey(string $keyId): Key - { - if (empty($this->keys[$keyId])) { - throw new NotFoundException($keyId, 'key'); - } - return $this->keys[$keyId]; - } -} diff --git a/libraries/src/TUF/src/Metadata/ConstraintsTrait.php b/libraries/src/TUF/src/Metadata/ConstraintsTrait.php deleted file mode 100644 index a79fa6260df..00000000000 --- a/libraries/src/TUF/src/Metadata/ConstraintsTrait.php +++ /dev/null @@ -1,173 +0,0 @@ -validate($data, $constraints); - if (count($violations)) { - $exceptionMessages = []; - foreach ($violations as $violation) { - $exceptionMessages[] = (string) $violation; - } - throw new MetadataException(implode(", \n", $exceptionMessages)); - } - } - - /** - * Gets the common hash constraints. - * - * @return \Symfony\Component\Validator\Constraint[][] - * The hash constraints. - */ - protected static function getHashesConstraints(): array - { - return [ - 'hashes' => [ - new Count(['min' => 1]), - new Type('\ArrayObject'), - // The keys for 'hashes is not know but they all must be strings. - new All([ - new Type(['type' => 'string']), - new NotBlank(), - ]), - ], - ]; - } - - /** - * Gets the common version constraints. - * - * @return \Symfony\Component\Validator\Constraint[][] - * The version constraints. - */ - protected static function getVersionConstraints(): array - { - return [ - 'version' => [ - new Type(['type' => 'integer']), - new GreaterThanOrEqual(1), - ], - ]; - } - - /** - * Gets the common threshold constraints. - * - * @return \Symfony\Component\Validator\Constraint[][] - * The threshold constraints. - */ - protected static function getThresholdConstraints(): array - { - return [ - 'threshold' => [ - new Type(['type' => 'integer']), - new GreaterThanOrEqual(1), - ], - ]; - } - /** - * Gets the common keyids constraints. - * - * @return \Symfony\Component\Validator\Constraint[][] - * The keysids constraints. - */ - protected static function getKeyidsConstraints(): array - { - return [ - 'keyids' => [ - new Count(['min' => 1]), - new Type(['type' => 'array']), - // The keys for 'hashes is not know but they all must be strings. - new All([ - new Type(['type' => 'string']), - new NotBlank(), - ]), - ], - ]; - } - - /** - * Gets the common key Collection constraints. - * - * @return Collection - * The 'key' Collection constraint. - */ - protected static function getKeyConstraints(): Collection - { - return new Collection([ - // This field is not part of the TUF specification and is being - // removed from the Python TUF reference implementation in - // https://github.com/theupdateframework/tuf/issues/848. - // If it is provided though we only support the default value which - // is passed on from a setting in the Python `securesystemslib` - // library. - 'keyid_hash_algorithms' => new Optional([ - new EqualTo(['value' => ["sha256", "sha512"]]), - ]), - 'keytype' => [ - new Type(['type' => 'string']), - new NotBlank(), - ], - 'keyval' => [ - new Type('\ArrayObject'), - new Collection([ - 'public' => [ - new Type(['type' => 'string']), - new NotBlank(), - ], - ]), - ], - 'scheme' => [ - new Type(['type' => 'string']), - new NotBlank(), - ], - ]); - } - - /** - * Gets the role constraints. - * - * @return \Symfony\Component\Validator\Constraints\Collection - * The role constraints collection. - */ - protected static function getRoleConstraints(): Collection - { - return new Collection( - static::getKeyidsConstraints() + - static::getThresholdConstraints() - ); - } -} diff --git a/libraries/src/TUF/src/Metadata/Factory.php b/libraries/src/TUF/src/Metadata/Factory.php deleted file mode 100644 index 601716a3352..00000000000 --- a/libraries/src/TUF/src/Metadata/Factory.php +++ /dev/null @@ -1,71 +0,0 @@ -storage = $storage; - } - - /** - * Loads a value object for trusted metadata. - * - * @param string $role - * The role to be loaded. - * - * @return \Tuf\Metadata\MetadataBase|null - * The trusted metadata for the role, or NULL if none was found. - * @throws \Tuf\Exception\MetadataException - */ - public function load(string $role): ?MetadataBase - { - // TODO: this is changed from $role . ".json" to $role . "_json" - $fileName = $role . "_json"; - - if (isset($this->storage[$fileName])) - { - $json = $this->storage[$fileName]; - - switch ($role) - { - case RootMetadata::TYPE: - $currentMetadata = RootMetadata::createFromJson($json); - break; - case SnapshotMetadata::TYPE: - $currentMetadata = SnapshotMetadata::createFromJson($json); - break; - case TimestampMetadata::TYPE: - $currentMetadata = TimestampMetadata::createFromJson($json); - break; - default: - $currentMetadata = TargetsMetadata::createFromJson($json, $role); - } - - $currentMetadata->trust(); - - return $currentMetadata; - } - else - { - return null; - } - } -} diff --git a/libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php b/libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php deleted file mode 100644 index cd7e6410407..00000000000 --- a/libraries/src/TUF/src/Metadata/FileInfoMetadataBase.php +++ /dev/null @@ -1,27 +0,0 @@ -ensureIsTrusted($allowUntrustedAccess); - $signed = $this->getSigned(); - return $signed['meta'][$key] ?? null; - } -} diff --git a/libraries/src/TUF/src/Metadata/MetadataBase.php b/libraries/src/TUF/src/Metadata/MetadataBase.php deleted file mode 100644 index 76ce37ff6cc..00000000000 --- a/libraries/src/TUF/src/Metadata/MetadataBase.php +++ /dev/null @@ -1,255 +0,0 @@ -metadata = $metadata; - $this->sourceJson = $sourceJson; - } - - /** - * Gets the original JSON source. - * - * @return string - * The JSON source. - */ - public function getSource():string - { - return $this->sourceJson; - } - - /** - * Create an instance and also validate the decoded JSON. - * - * @param string $json - * A JSON string representing TUF metadata. - * - * @return static - * The new instance. - * - * @throws \Tuf\Exception\MetadataException - * Thrown if validation fails. - */ - public static function createFromJson(string $json): self - { - $data = JsonNormalizer::decode($json); - static::validate($data, new Collection(static::getConstraints())); - - return new static($data, $json); - } - - /** - * Gets the constraints for top-level metadata. - * - * @return \Symfony\Component\Validator\Constraint[] - * Array of constraints. - */ - protected static function getConstraints(): array - { - return [ - 'signatures' => new Required( - [ - new Type('array'), - new Count(['min' => 1]), - new All( - [ - new Collection( - [ - 'keyid' => [ - new NotBlank, - new Type(['type' => 'string']), - ], - 'sig' => [ - new NotBlank, - new Type(['type' => 'string']), - ], - ] - ), - ] - ), - ] - ), - 'signed' => new Required( - [ - new Collection(static::getSignedCollectionOptions()), - ] - ), - ]; - } - - /** - * Gets options for the 'signed' metadata property. - * - * @return array - * An options array as expected by - * \Symfony\Component\Validator\Constraints\Collection::__construct(). - */ - protected static function getSignedCollectionOptions(): array - { - return [ - 'fields' => [ - '_type' => [ - new EqualTo(['value' => static::TYPE]), - new Type(['type' => 'string']), - ], - 'expires' => new DateTime(['value' => \DateTimeInterface::ISO8601]), - // We only expect to work with major version 1. - 'spec_version' => [ - new NotBlank, - new Type(['type' => 'string']), - //new Regex(['pattern' => '/^1\.[0-9]+\.[0-9]+$/']), - ], - ] + static::getVersionConstraints(), - 'allowExtraFields' => true, - ]; - } - - /** - * Get signed. - * - * @return \ArrayObject - * The "signed" section of the data. - */ - public function getSigned(): \ArrayObject - { - return (new DeepCopy)->copy($this->metadata['signed']); - } - - /** - * Get version. - * - * @return integer - * The version. - */ - public function getVersion(): int - { - return $this->getSigned()['version']; - } - - /** - * Get the expires date string. - * - * @return string - * The date string. - */ - public function getExpires(): string - { - return $this->getSigned()['expires']; - } - - /** - * Get signatures. - * - * @return array - * The "signatures" section of the data. - */ - public function getSignatures(): array - { - return (new DeepCopy)->copy($this->metadata['signatures']); - } - - /** - * Get the metadata type. - * - * @return string - * The type. - */ - public function getType(): string - { - return $this->getSigned()['_type']; - } - - /** - * Gets the role for the metadata. - * - * @return string - * The type. - */ - public function getRole(): string - { - // For most metadata types the 'type' and the 'role' are the same. - // Metadata types that need to specify a different role should override - // this method. - return $this->getType(); - } - - /** - * Sets the metadata as trusted. - * - * @return void - */ - public function trust(): void - { - $this->isTrusted = true; - } - - /** - * Ensures that the metadata is trusted or the caller explicitly expects untrusted metadata. - * - * @param boolean $allowUntrustedAccess - * Whether this method should access even if the metadata is not trusted. - * - * @return void - */ - public function ensureIsTrusted(bool $allowUntrustedAccess = false): void - { - if (!$allowUntrustedAccess && !$this->isTrusted) - { - throw new \RuntimeException("Cannot use untrusted '{$this->getRole()}'. metadata."); - } - } -} diff --git a/libraries/src/TUF/src/Metadata/RootMetadata.php b/libraries/src/TUF/src/Metadata/RootMetadata.php deleted file mode 100644 index 9558f23060f..00000000000 --- a/libraries/src/TUF/src/Metadata/RootMetadata.php +++ /dev/null @@ -1,100 +0,0 @@ - 1]), - new All([ - static::getKeyConstraints(), - ]), - ]); - $roleConstraints = static::getRoleConstraints(); - $options['fields']['roles'] = new Collection([ - 'targets' => new Required($roleConstraints), - 'timestamp' => new Required($roleConstraints), - 'snapshot' => new Required($roleConstraints), - 'root' => new Required($roleConstraints), - 'mirror' => new Optional($roleConstraints), - ]); - $options['fields']['consistent_snapshot'] = new Required([ - new Type('boolean'), - new EqualTo(true), - ]); - return $options; - } - - /** - * Gets the roles from the metadata. - * - * @param boolean $allowUntrustedAccess - * Whether this method should access even if the metadata is not trusted. - * - * @return \Tuf\Role[] - * The roles. - */ - public function getRoles(bool $allowUntrustedAccess = false): array - { - $this->ensureIsTrusted($allowUntrustedAccess); - $roles = []; - foreach ($this->getSigned()['roles'] as $roleName => $roleInfo) { - $roles[$roleName] = Role::createFromMetadata($roleInfo, $roleName); - } - return $roles; - } - - /** - * Gets the keys for the root metadata. - * - * @param boolean $allowUntrustedAccess - * Whether this method should access even if the metadata is not trusted. - * - * @return \Tuf\Key[] - * The keys for the metadata. - */ - public function getKeys(bool $allowUntrustedAccess = false): array - { - $this->ensureIsTrusted($allowUntrustedAccess); - $keys = []; - foreach ($this->getSigned()['keys'] as $keyId => $keyInfo) { - $keys[$keyId] = Key::createFromMetadata($keyInfo); - } - return $keys; - } - - /** - * Determines whether consistent snapshots are supported. - * - * @return boolean - * Whether consistent snapshots are supported. - */ - public function supportsConsistentSnapshots(): bool - { - $this->ensureIsTrusted(); - return $this->getSigned()['consistent_snapshot']; - } -} diff --git a/libraries/src/TUF/src/Metadata/SnapshotMetadata.php b/libraries/src/TUF/src/Metadata/SnapshotMetadata.php deleted file mode 100644 index 7978a3afb30..00000000000 --- a/libraries/src/TUF/src/Metadata/SnapshotMetadata.php +++ /dev/null @@ -1,72 +0,0 @@ - 1]), - new All( - [ - new Collection( - [ - 'fields' => static::getSnapshotMetaConstraints(), - 'allowExtraFields' => true, - ] - ), - ] - ), - ] - ); - - return $options; - } - - /** - * Returns the fields required or optional for a snapshot meta file - * - * @return array - */ - private static function getSnapshotMetaConstraints() - { - return [ - 'version' => [ - new Type(['type' => 'integer']), - new GreaterThanOrEqual(1), - ], - new Optional( - [ - new Collection( - [ - 'length' => [ - new Type(['type' => 'integer']), - new GreaterThanOrEqual(1), - ], - ] + static::getHashesConstraints() - ), - ] - ), - ]; - } -} diff --git a/libraries/src/TUF/src/Metadata/TargetsMetadata.php b/libraries/src/TUF/src/Metadata/TargetsMetadata.php deleted file mode 100644 index dec63edf377..00000000000 --- a/libraries/src/TUF/src/Metadata/TargetsMetadata.php +++ /dev/null @@ -1,209 +0,0 @@ -role = $roleName; - return $newMetadata; - } - - /** - * {@inheritdoc} - */ - protected static function getSignedCollectionOptions(): array - { - $options = parent::getSignedCollectionOptions(); - $options['fields']['delegations'] = new Optional([ - new Collection([ - 'keys' => new Required([ - new Type('\ArrayObject'), - new All([ - static::getKeyConstraints(), - ]), - ]), - 'roles' => new All([ - new Type('\ArrayObject'), - new TufCollection([ - 'fields' => [ - 'name' => [ - new NotBlank(), - new Type(['type' => 'string']), - ], - 'paths' => [ - new Type(['type' => 'array']), - new All([ - new Type(['type' => 'string']), - new NotBlank(), - ]), - ], - 'terminating' => [ - new Type(['type' => 'boolean']), - ], - ] + static::getKeyidsConstraints() + static::getThresholdConstraints(), - // @todo Support 'path_hash_prefixes' in - // https://github.com/php-tuf/php-tuf/issues/191 - 'unsupportedFields' => ['path_hash_prefixes'], - ]), - ]), - ]), - ]); - $options['fields']['targets'] = new Required([ - new All([ - new Collection([ - 'length' => [ - new Type(['type' => 'integer']), - new GreaterThanOrEqual(1), - ], - 'custom' => new Optional([ - new Type('\ArrayObject'), - ]), - ] + static::getHashesConstraints()), - ]), - - ]); - return $options; - } - - /** - * Returns the length, in bytes, of a specific target. - * - * @param string $target - * The target path. - * - * @return integer - * The length (size) of the target, in bytes. - */ - public function getLength(string $target): int - { - return $this->getInfo($target)['length']; - } - - /** - * {@inheritdoc} - */ - public function getRole(): string - { - return $this->role ?? $this->getType(); - } - - /** - * Returns the known hashes for a specific target. - * - * @param string $target - * The target path. - * - * @return \ArrayObject - * The known hashes for the object. The keys are the hash algorithm (e.g. - * 'sha256') and the values are the hash digest. - */ - public function getHashes(string $target): \ArrayObject - { - return $this->getInfo($target)['hashes']; - } - - /** - * Determines if a target is specified in the current metadata. - * - * @param string $target - * The target path. - * - * @return bool - * True if the target is specified, or false otherwise. - */ - public function hasTarget(string $target): bool - { - try { - $this->getInfo($target); - return true; - } catch (NotFoundException $exception) { - return false; - } - } - - /** - * Gets info about a specific target. - * - * @param string $target - * The target path. - * - * @return \ArrayObject - * The target's info. - * - * @throws \Tuf\Exception\NotFoundException - * Thrown if the target is not mentioned in this metadata. - */ - protected function getInfo(string $target): \ArrayObject - { - $signed = $this->getSigned(); - if (isset($signed['targets'][$target])) { - return $signed['targets'][$target]; - } - throw new NotFoundException($target, 'Target'); - } - - /** - * Gets the delegated keys if any. - * - * @return \Tuf\Key[] - * The delegated keys. - */ - public function getDelegatedKeys(): array - { - $keys = []; - foreach ($this->getSigned()['delegations']['keys'] as $keyId => $keyInfo) { - $keys[$keyId] = Key::createFromMetadata($keyInfo); - } - return $keys; - } - - /** - * Gets the delegated roles if any. - * - * @return \Tuf\DelegatedRole[] - * The delegated roles. - */ - public function getDelegatedRoles(): array - { - $roles = []; - foreach ($this->getSigned()['delegations']['roles'] as $roleInfo) { - $role = DelegatedRole::createFromMetadata($roleInfo); - $roles[$role->getName()] = $role; - } - return $roles; - } -} diff --git a/libraries/src/TUF/src/Metadata/TimestampMetadata.php b/libraries/src/TUF/src/Metadata/TimestampMetadata.php deleted file mode 100644 index 7325d78c9db..00000000000 --- a/libraries/src/TUF/src/Metadata/TimestampMetadata.php +++ /dev/null @@ -1,39 +0,0 @@ - 1]), - new All([ - new Collection([ - 'length' => [ - new Type(['type' => 'integer']), - new GreaterThanOrEqual(1), - ], - ] + static::getHashesConstraints() + static::getVersionConstraints()), - ]), - ]); - return $options; - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php deleted file mode 100644 index db0f3e36ab3..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/FileInfoVerifier.php +++ /dev/null @@ -1,48 +0,0 @@ -trustedMetadata->getSigned()['meta']; - $type = $this->trustedMetadata->getType(); - foreach ($localMetaFileInfos as $fileName => $localFileInfo) { - /** @var \Tuf\Metadata\SnapshotMetadata|\Tuf\Metadata\TimestampMetadata $untrustedMetadata */ - if ($remoteFileInfo = $untrustedMetadata->getFileMetaInfo($fileName, true)) { - if ($remoteFileInfo['version'] < $localFileInfo['version']) { - $message = "Remote $type metadata file '$fileName' version \"${$remoteFileInfo['version']}\" " . - "is less than previously seen version \"${$localFileInfo['version']}\""; - throw new RollbackAttackException($message); - } - } - } - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php deleted file mode 100644 index b4201bb1f25..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/RootVerifier.php +++ /dev/null @@ -1,63 +0,0 @@ -signatureVerifier->checkSignatures($untrustedMetadata); - $this->signatureVerifier = SignatureVerifier::createFromRootMetadata($untrustedMetadata, true); - $this->signatureVerifier->checkSignatures($untrustedMetadata); - // § 5.3.5 - $this->checkRollbackAttack($untrustedMetadata); - } - - /** - * {@inheritDoc} - */ - protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void - { - $expectedUntrustedVersion = $this->trustedMetadata->getVersion() + 1; - $untrustedVersion = $untrustedMetadata->getVersion(); - if ($expectedUntrustedVersion && ($untrustedMetadata->getVersion() !== $expectedUntrustedVersion)) { - throw new RollbackAttackException("Remote 'root' metadata version \"$$untrustedVersion\" " . - "does not the expected version \"$$expectedUntrustedVersion\""); - } - parent::checkRollbackAttack($untrustedMetadata); - } - - /** - * {@inheritdoc} - * - * Overridden to make public. - * - * After attempting to update the root metadata, the new or non-updated metadata must be checked - * for a freeze attack. We cannot check for a freeze attack in ::verify() because when the client - * is many root files behind, only the last version to be downloaded needs to be checked for a - * freeze attack. - */ - public static function checkFreezeAttack(MetadataBase $metadata, \DateTimeImmutable $expiration): void - { - parent::checkFreezeAttack($metadata, $expiration); - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php deleted file mode 100644 index 7f0f7a023e5..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/SnapshotVerifier.php +++ /dev/null @@ -1,79 +0,0 @@ -setTrustedAuthority($timestampMetadata); - } - - /** - * {@inheritdoc} - */ - public function verify(MetadataBase $untrustedMetadata): void - { - // § 5.5.2 - $this->checkAgainstHashesFromTrustedAuthority($untrustedMetadata); - - // § 5.5.3 - $this->signatureVerifier->checkSignatures($untrustedMetadata); - - // § 5.5.4 - $this->checkAgainstVersionFromTrustedAuthority($untrustedMetadata); - - // If the timestamp or snapshot keys were rotating then the snapshot file - // will not exist. - if ($this->trustedMetadata) { - // § 5.5.5 - $this->checkRollbackAttack($untrustedMetadata); - } - - // § 5.5.6 - static::checkFreezeAttack($untrustedMetadata, $this->metadataExpiration); - } - - /** - * {@inheritdoc} - */ - protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void - { - // TUF-SPEC-v1.0.16 Section 5.4.4 - /** @var TimestampMetadata $untrustedMetadata */ - $this->checkFileInfoVersions($untrustedMetadata); - $localMetaFileInfos = $this->trustedMetadata->getSigned()['meta']; - foreach ($localMetaFileInfos as $fileName => $localFileInfo) { - /** @var \Tuf\Metadata\SnapshotMetadata|\Tuf\Metadata\TimestampMetadata $untrustedMetadata */ - if (!$untrustedMetadata->getFileMetaInfo($fileName, true)) { - // § 5.5.5 - // Any targets metadata filename that was listed in the trusted snapshot metadata file, if any, MUST - // continue to be listed in the new snapshot metadata file. - throw new RollbackAttackException("Remote snapshot metadata file references '$fileName' but this is not present in the remote file"); - } - } - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php deleted file mode 100644 index bcdfa021dc0..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/TargetsVerifier.php +++ /dev/null @@ -1,51 +0,0 @@ -setTrustedAuthority($snapshotMetadata); - } - - /** - * {@inheritdoc} - */ - public function verify(MetadataBase $untrustedMetadata): void - { - // § 5.6.2 - $this->checkAgainstHashesFromTrustedAuthority($untrustedMetadata); - - // § 5.6.3 - $this->signatureVerifier->checkSignatures($untrustedMetadata); - - // § 5.6.4 - $this->checkAgainstVersionFromTrustedAuthority($untrustedMetadata); - - // § 5.6.5 - static::checkFreezeAttack($untrustedMetadata, $this->metadataExpiration); - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php deleted file mode 100644 index 8d28459bbd8..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/TimestampVerifier.php +++ /dev/null @@ -1,40 +0,0 @@ -signatureVerifier->checkSignatures($untrustedMetadata); - // If the timestamp or snapshot keys were rotating then the timestamp file - // will not exist. - if ($this->trustedMetadata) { - // § 5.4.3 - $this->checkRollbackAttack($untrustedMetadata); - } - // § 5.4.4 - static::checkFreezeAttack($untrustedMetadata, $this->metadataExpiration); - } - - /** - * {@inheritdoc} - */ - protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void - { - // § 5.3.2.1 - parent::checkRollbackAttack($untrustedMetadata); - // § 5.3.2.2 - /** @var \Tuf\Metadata\SnapshotMetadata $untrustedMetadata */ - $this->checkFileInfoVersions($untrustedMetadata); - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php b/libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php deleted file mode 100644 index 6456244f063..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/TrustedAuthorityTrait.php +++ /dev/null @@ -1,78 +0,0 @@ -ensureIsTrusted(); - $this->authority = $authority; - } - - /** - * Verifies the hashes of untrusted metadata against hashes in the trusted metadata. - * - * @param \Tuf\Metadata\MetadataBase $untrustedMetadata - * The untrusted metadata. - * - * @throws \Tuf\Exception\MetadataException - * Thrown if the new metadata object cannot be verified. - * - * @return void - */ - protected function checkAgainstHashesFromTrustedAuthority(MetadataBase $untrustedMetadata): void - { - $role = $untrustedMetadata->getRole(); - $fileInfo = $this->authority->getFileMetaInfo($role . '.json'); - if (isset($fileInfo['hashes'])) { - foreach ($fileInfo['hashes'] as $algo => $hash) { - if ($hash !== hash($algo, $untrustedMetadata->getSource())) { - /** @var \Tuf\Metadata\MetadataBase $authorityMetadata */ - throw new MetadataException("The '{$role}' contents does not match hash '$algo' specified in the '{$this->authority->getType()}' metadata."); - } - } - } - } - - /** - * Verifies the version of untrusted metadata against the version in trusted metadata. - * - * @param \Tuf\Metadata\MetadataBase $untrustedMetadata - * The untrusted metadata. - * - * @throws \Tuf\Exception\MetadataException - * Thrown if the new metadata object cannot be verified. - * - * @return void - */ - protected function checkAgainstVersionFromTrustedAuthority(MetadataBase $untrustedMetadata): void - { - $role = $untrustedMetadata->getRole(); - $fileInfo = $this->authority->getFileMetaInfo($role . '.json'); - $expectedVersion = $fileInfo['version']; - if ($expectedVersion !== $untrustedMetadata->getVersion()) { - throw new MetadataException("Expected {$role} version {$expectedVersion} does not match actual version {$untrustedMetadata->getVersion()}."); - } - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php b/libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php deleted file mode 100644 index 085530c937a..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/UniversalVerifier.php +++ /dev/null @@ -1,92 +0,0 @@ -metadataFactory = $metadataFactory; - $this->signatureVerifier = $signatureVerifier; - $this->metadataExpiration = $metadataExpiration; - } - - /** - * Verifies an untrusted metadata object for a role. - * - * @param string $role - * The metadata role (e.g. 'root', 'targets', etc.) - * @param \Tuf\Metadata\MetadataBase $untrustedMetadata - * The untrusted metadata object. - * - * @throws \Tuf\Exception\Attack\FreezeAttackException - * @throws \Tuf\Exception\Attack\RollbackAttackException - * @throws \Tuf\Exception\Attack\InvalidHashException - * @throws \Tuf\Exception\Attack\SignatureThresholdException - */ - public function verify(string $role, MetadataBase $untrustedMetadata): void - { - $trustedMetadata = $this->metadataFactory->load($role); - switch ($role) { - case RootMetadata::TYPE: - $verifier = new RootVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata); - break; - case SnapshotMetadata::TYPE: - /** @var \Tuf\Metadata\TimestampMetadata $timestampMetadata */ - $timestampMetadata = $this->metadataFactory->load(TimestampMetadata::TYPE); - $verifier = new SnapshotVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata, $timestampMetadata); - break; - case TimestampMetadata::TYPE: - $verifier = new TimestampVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata); - break; - default: - /** @var \Tuf\Metadata\SnapshotMetadata $snapshotMetadata */ - $snapshotMetadata = $this->metadataFactory->load(SnapshotMetadata::TYPE); - $verifier = new TargetsVerifier($this->signatureVerifier, $this->metadataExpiration, $trustedMetadata, $snapshotMetadata); - } - $verifier->verify($untrustedMetadata); - // If the verifier didn't throw an exception, we can trust this metadata. - $untrustedMetadata->trust(); - } -} diff --git a/libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php b/libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php deleted file mode 100644 index 6c571e2eb21..00000000000 --- a/libraries/src/TUF/src/Metadata/Verifier/VerifierBase.php +++ /dev/null @@ -1,140 +0,0 @@ -signatureVerifier = $signatureVerifier; - $this->metadataExpiration = $metadataExpiration; - if ($trustedMetadata) { - $trustedMetadata->ensureIsTrusted(); - } - $this->trustedMetadata = $trustedMetadata; - } - - /** - * Verify metadata according to the specification. - * - * @param \Tuf\Metadata\MetadataBase $untrustedMetadata - * The untrusted metadata to verify. - * - * @throws \Tuf\Exception\Attack\FreezeAttackException - * @throws \Tuf\Exception\Attack\RollbackAttackException - * @throws \Tuf\Exception\Attack\InvalidHashException - * @throws \Tuf\Exception\Attack\SignatureThresholdException - */ - abstract public function verify(MetadataBase $untrustedMetadata): void; - - /** - * Checks for a rollback attack. - * - * Verifies that an incoming remote version of a metadata file is greater - * than or equal to the last known version. - * - * @param \Tuf\Metadata\MetadataBase $untrustedMetadata - * The untrusted metadata. - * - * @return void - * - * @throws \Tuf\Exception\Attack\RollbackAttackException - * Thrown if a potential rollback attack is detected. - */ - protected function checkRollbackAttack(MetadataBase $untrustedMetadata): void - { - $type = $this->trustedMetadata->getType(); - $remoteVersion = $untrustedMetadata->getVersion(); - $localVersion = $this->trustedMetadata->getVersion(); - if ($remoteVersion < $localVersion) { - $message = "Remote $type metadata version \"$$remoteVersion\" " . - "is less than previously seen $type version \"$$localVersion\""; - throw new RollbackAttackException($message); - } - } - - /** - * Checks for a freeze attack. - * - * Verifies that metadata has not expired, and assumes a potential freeze - * attack if it has. - * - * @param \Tuf\Metadata\MetadataBase $metadata - * The metadata to check. - * @param \DateTimeImmutable $expiration - * The metadata expiration. - * - * @return void - * - * @throws \Tuf\Exception\Attack\FreezeAttackException Thrown if a potential freeze attack is detected. - */ - protected static function checkFreezeAttack(MetadataBase $metadata, \DateTimeImmutable $expiration): void - { - $metadataExpiration = static::metadataTimestampToDatetime($metadata->getExpires()); - if ($metadataExpiration < $expiration) { - $format = "Remote %s metadata expired on %s"; - throw new FreezeAttackException(sprintf($format, $metadata->getRole(), $metadataExpiration->format('c'))); - } - } - - /** - * Converts a metadata timestamp string into an immutable DateTime object. - * - * @param string $timestamp - * The timestamp string in the metadata. - * - * @return \DateTimeImmutable - * An immutable DateTime object for the given timestamp. - * - * @throws FormatException - * Thrown if the timestamp string format is not valid. - */ - protected static function metadataTimestampToDateTime(string $timestamp): \DateTimeImmutable - { - $dateTime = \DateTimeImmutable::createFromFormat("Y-m-d\TH:i:sT", $timestamp); - if ($dateTime === false) { - throw new FormatException($timestamp, "Could not be interpreted as a DateTime"); - } - return $dateTime; - } -} diff --git a/libraries/src/TUF/src/Role.php b/libraries/src/TUF/src/Role.php deleted file mode 100644 index 91d2e4f5297..00000000000 --- a/libraries/src/TUF/src/Role.php +++ /dev/null @@ -1,122 +0,0 @@ -name = $name; - $this->threshold = $threshold; - $this->keyIds = $keyIds; - } - - /** - * Creates a role object from TUF metadata. - * - * @param \ArrayObject $roleInfo - * The role information from TUF metadata. - * @param string $name - * The name of the role. - * - * @return static - * - * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats - */ - public static function createFromMetadata(\ArrayObject $roleInfo, string $name): Role - { - self::validate($roleInfo, static::getRoleConstraints()); - return new static( - $name, - $roleInfo['threshold'], - $roleInfo['keyids'] - ); - } - - /** - * Checks if this role's key IDs match another role's. - * - * @param \Tuf\Role $other - * - * @return bool - */ - public function keysMatch(Role $other): bool - { - return $this->keyIds === $other->keyIds; - } - - /** - * Gets the role name. - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Gets the threshold required. - * - * @return int - * The threshold number of signatures required for the role. - */ - public function getThreshold(): int - { - return $this->threshold; - } - - - /** - * Checks whether the given key is authorized for the role. - * - * @param string $keyId - * The key ID to check. - * - * @return boolean - * TRUE if the key is authorized for the given role, or FALSE - * otherwise. - */ - public function isKeyIdAcceptable(string $keyId): bool - { - return in_array($keyId, $this->keyIds, true); - } -} diff --git a/libraries/src/TUF/src/RoleDB.php b/libraries/src/TUF/src/RoleDB.php deleted file mode 100644 index 9f06b059773..00000000000 --- a/libraries/src/TUF/src/RoleDB.php +++ /dev/null @@ -1,116 +0,0 @@ -getRoles($allowUntrustedAccess) as $roleName => $roleInfo) { - $db->addRole($roleInfo); - } - - return $db; - } - - /** - * Constructs a new RoleDB object. - */ - public function __construct() - { - $this->roles = []; - } - - /** - * Adds role metadata to the database. - * - * @param string $roleName - * The role name. - * @param \Tuf\Role $role - * The role to add. - * - * @return void - * - * @throws \Exception Thrown if the role already exists. - */ - public function addRole(Role $role): void - { - if ($this->roleExists($role->getName())) { - throw new RoleExistsException('Role already exists: ' . $role->getName()); - } - - $this->roles[$role->getName()] = $role; - } - - /** - * Verifies whether a given role name is stored in the role database. - * - * @param string $roleName - * The role name. - * - * @return boolean - * True if the role is found in the role database; false otherwise. - */ - public function roleExists(string $roleName): bool - { - return !empty($this->roles[$roleName]); - } - - /** - * Gets the role information. - * - * @param string $roleName - * The role name. - * - * @return \Tuf\Role - * The role. - * - * @throws \Tuf\Exception\NotFoundException - * Thrown if the role does not exist. - * - * @see https://theupdateframework.github.io/specification/v1.0.18#document-formats - */ - public function getRole(string $roleName): Role - { - if (! $this->roleExists($roleName)) { - throw new NotFoundException($roleName, 'role'); - } - - /** @var \Tuf\Role $role */ - $role = $this->roles[$roleName]; - return $role; - } -} From b06ed4963ba664acccda1d23d43d617051c3a916 Mon Sep 17 00:00:00 2001 From: Franciska Perisa <9084265+fancyFranci@users.noreply.github.com> Date: Fri, 10 Jun 2022 16:31:55 +0200 Subject: [PATCH 013/126] What zero-24 says Co-authored-by: Tobias Zulauf --- .../components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index 007d7a69130..ff16bbaef2d 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -15,4 +15,4 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- -------------------------------------------------------- INSERT INTO `#__tuf_metadata` (`extension_id`, `root_json`) -SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `tupsb_extensions` WHERE `type`='file' AND `element`='joomla'; +SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; From f81e0158b03bc9162d289c16fce7f9dc974821d8 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Fri, 10 Jun 2022 17:49:26 +0200 Subject: [PATCH 014/126] Update drone hash --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 4a705b79001..cbda382bbb9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -478,6 +478,6 @@ trigger: --- kind: signature -hmac: 234ae9e7e2fbfa114ba754c68056dec518c76a93de2f5b098f569e355b50cc1b +hmac: d3733928f0d060a756fa26acf7ae591853a7a50cad53cffa243df7ae1bab66a8 ... From 7934f524d5271203daed31d7069ecfea4252b567 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Fri, 10 Jun 2022 17:54:53 +0200 Subject: [PATCH 015/126] Add symfony/validator into composer.lock --- composer.lock | 351 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 271 insertions(+), 80 deletions(-) diff --git a/composer.lock b/composer.lock index 71d70731a75..9dfb239cb98 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": "e02971ec0a050805d6d4f8e175c3876e", + "content-hash": "1868030bc5c5a79c3bd59fc9e6aa8cf4", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -4368,6 +4368,85 @@ ], "time": "2022-03-04T08:16:47+00:00" }, + { + "name": "symfony/polyfill-php81", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "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.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.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": "2021-09-13T13:58:11+00:00" + }, { "name": "symfony/service-contracts", "version": "v2.5.1", @@ -4537,6 +4616,197 @@ ], "time": "2022-01-02T09:53:40+00:00" }, + { + "name": "symfony/translation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "1211df0afa701e45a04253110e959d4af4ef0f07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/1211df0afa701e45a04253110e959d4af4ef0f07", + "reference": "1211df0afa701e45a04253110e959d4af4ef0f07", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "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/v2.5.1" + }, + "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": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/validator", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "bdc6d04ba95c73ccbf906b4ad9b8775c738d83ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/bdc6d04ba95c73ccbf906b4ad9b8775c738d83ad", + "reference": "bdc6d04ba95c73ccbf906b4ad9b8775c738d83ad", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22", + "symfony/translation-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/cache": "<1.11", + "doctrine/lexer": "<1.1", + "phpunit/phpunit": "<5.4.3", + "symfony/dependency-injection": "<4.4", + "symfony/expression-language": "<5.1", + "symfony/http-kernel": "<4.4", + "symfony/intl": "<4.4", + "symfony/property-info": "<5.3", + "symfony/translation": "<4.4", + "symfony/yaml": "<4.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13", + "doctrine/cache": "^1.11|^2.0", + "egulias/email-validator": "^2.1.10|^3", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^5.1|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/property-info": "^5.3|^6.0", + "symfony/translation": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "egulias/email-validator": "Strict (RFC compliant) email validation", + "psr/cache-implementation": "For using the mapping cache.", + "symfony/config": "", + "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints", + "symfony/http-foundation": "", + "symfony/intl": "", + "symfony/property-access": "For accessing properties within comparison constraints", + "symfony/property-info": "To automatically add NotNull and Type constraints", + "symfony/translation": "For translating validation errors.", + "symfony/yaml": "" + }, + "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/v5.4.8" + }, + "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": "2022-04-15T08:07:45+00:00" + }, { "name": "symfony/var-dumper", "version": "v5.4.6", @@ -10394,85 +10664,6 @@ ], "time": "2022-01-26T16:34:36+00:00" }, - { - "name": "symfony/polyfill-php81", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "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.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.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": "2021-09-13T13:58:11+00:00" - }, { "name": "symfony/process", "version": "v5.4.7", From 7fd25c83c47a7a29fd72eda2a735488f4a0c4c82 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Fri, 10 Jun 2022 17:57:55 +0200 Subject: [PATCH 016/126] Codestyle fixes --- libraries/src/TUF/DatabaseStorage.php | 62 +++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 253cec551ae..2b8acb5d6ff 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -37,7 +37,7 @@ public function __construct(DatabaseDriver $db, int $extensionId) { $this->table = new Tuf($db); - $this->table->load($extensionId); + $this->table->load($extensionId); } /** @@ -45,9 +45,9 @@ public function __construct(DatabaseDriver $db, int $extensionId) */ public function offsetExists(mixed $offset): bool { - $column = $this->getCleanColumn($offset); + $column = $this->getCleanColumn($offset); - return substr($offset, -5) === '_json' && $this->table->hasField($column) && strlen($this->table->$column); + return substr($offset, -5) === '_json' && $this->table->hasField($column) && strlen($this->table->$column); } /** @@ -55,14 +55,14 @@ public function offsetExists(mixed $offset): bool */ public function offsetGet($offset): mixed { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } - $column = $this->getCleanColumn($offset); + $column = $this->getCleanColumn($offset); - return $this->table->$column; + return $this->table->$column; } /** @@ -70,14 +70,14 @@ public function offsetGet($offset): mixed */ public function offsetSet($offset, $value): void { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } $this->table->$offset = $value; - $this->table->store(); + $this->table->store(); } /** @@ -85,25 +85,25 @@ public function offsetSet($offset, $value): void */ public function offsetUnset($offset): void { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } - $this->table->$offset = ''; + $this->table->$offset = ''; - $this->table->store(); + $this->table->store(); } - /** - * Convert file names to table columns - * - * @param string $name - * - * @return string - */ - protected function getCleanColumn($name): string - { - return str_replace('.', '_', $name); - } + /** + * Convert file names to table columns + * + * @param string $name + * + * @return string + */ + protected function getCleanColumn($name): string + { + return str_replace('.', '_', $name); + } } From dd22ede62a2630ed2f786f85c294f6f5c4055f1a Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Fri, 10 Jun 2022 18:22:26 +0200 Subject: [PATCH 017/126] Fix code style --- libraries/src/TUF/DatabaseStorage.php | 31 +++++++++++++++++++++------ libraries/src/TUF/TufValidation.php | 7 +++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 2b8acb5d6ff..54bbfc075fd 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -30,8 +30,8 @@ class DatabaseStorage implements \ArrayAccess /** * Initialize the DatabaseStorage class * - * @param DatabaseDriver $db - * @param integer $extensionId + * @param DatabaseDriver $db A database connector object + * @param integer $extensionId The extension ID where the storage should be implemented for */ public function __construct(DatabaseDriver $db, int $extensionId) { @@ -41,7 +41,11 @@ public function __construct(DatabaseDriver $db, int $extensionId) } /** - * {@inheritdoc} + * Check if an offset/table column exists + * + * @param mixed $offset The offset/database column to check for + * + * @return boolean */ public function offsetExists(mixed $offset): bool { @@ -51,7 +55,11 @@ public function offsetExists(mixed $offset): bool } /** - * {@inheritdoc} + * Get the value of a table column + * + * @param mixed $offset The column name to get the value for + * + * @return mixed */ public function offsetGet($offset): mixed { @@ -66,7 +74,12 @@ public function offsetGet($offset): mixed } /** - * {@inheritdoc} + * Set a value in a column + * + * @param [type] $offset The table column to set the value + * @param [type] $value The value to set + * + * @return void */ public function offsetSet($offset, $value): void { @@ -81,7 +94,11 @@ public function offsetSet($offset, $value): void } /** - * {@inheritdoc} + * Reset the value to a + * + * @param mixed $offset The table column to reset the value to null + * + * @return void */ public function offsetUnset($offset): void { @@ -98,7 +115,7 @@ public function offsetUnset($offset): void /** * Convert file names to table columns * - * @param string $name + * @param string $name The original file name * * @return string */ diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 521d11561f8..b8c644d0629 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -46,9 +46,8 @@ class TufValidation /** * Validating updates with TUF * - * @param integer $extensionId The ID of the extension to be checked - * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for - * the update + * @param integer $extensionId The ID of the extension to be checked + * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for the update */ public function __construct(int $extensionId, mixed $params) { @@ -60,7 +59,7 @@ public function __construct(int $extensionId, mixed $params) { $this->configureTufOptions($resolver); } - catch (\Exception) + catch (\Exception $e) { } From 93fcbfb2cc620043a6babef4084a7695a6c3d02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sat, 11 Jun 2022 14:39:55 +0200 Subject: [PATCH 018/126] wip tuf --- composer.json | 9 +- libraries/src/TUF/DatabaseStorage.php | 111 ++++--- libraries/src/TUF/TufValidation.php | 15 +- libraries/src/Updater/Adapter/TufAdapter.php | 309 +++++++++++++++++++ 4 files changed, 394 insertions(+), 50 deletions(-) create mode 100644 libraries/src/Updater/Adapter/TufAdapter.php diff --git a/composer.json b/composer.json index 423c707d04e..7f84da2fbd6 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-projects/php-tuf.git", + "no-api": true } ], "autoload": { @@ -94,8 +99,8 @@ "web-auth/webauthn-lib": "2.1.*", "composer/ca-bundle": "^1.2", "dragonmantank/cron-expression": "^3.1", - "symfony/validator": "^5.4", - "enshrined/svg-sanitize": "^0.15.4" + "enshrined/svg-sanitize": "^0.15.4", + "php-tuf/php-tuf": "dev-joomla-cfhack2022" }, "require-dev": { "phpunit/phpunit": "^8.5", diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 253cec551ae..d203b5fb5fa 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -30,80 +30,115 @@ class DatabaseStorage implements \ArrayAccess /** * Initialize the DatabaseStorage class * - * @param DatabaseDriver $db - * @param integer $extensionId + * @param DatabaseDriver $db A database connector object + * @param integer $extensionId The extension ID where the storage should be implemented for */ public function __construct(DatabaseDriver $db, int $extensionId) { $this->table = new Tuf($db); - $this->table->load($extensionId); + $this->table->load(['extension_id' => $extensionId]); } /** - * {@inheritdoc} + * Check if an offset/table column exists and is not null + * + * @param mixed $offset The offset/database column to check for + * + * @return boolean */ public function offsetExists(mixed $offset): bool { - $column = $this->getCleanColumn($offset); + $column = $this->getCleanColumn($offset); + + return substr($column, -5) === '_json' && $this->table->hasField($column) && !is_null($this->table->$column); + } + + /** + * Check if an offset/table column exists + * + * @param mixed $offset The offset/database column to check for + * + * @return boolean + */ + public function tableColumnExists(mixed $offset): bool + { + $column = $this->getCleanColumn($offset); - return substr($offset, -5) === '_json' && $this->table->hasField($column) && strlen($this->table->$column); + return substr($column, -5) === '_json' && $this->table->hasField($column); } /** - * {@inheritdoc} + * Get the value of a table column + * + * @param mixed $offset The column name to get the value for + * + * @return mixed */ public function offsetGet($offset): mixed { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } - $column = $this->getCleanColumn($offset); + $column = $this->getCleanColumn($offset); - return $this->table->$column; + return $this->table->$column; } /** - * {@inheritdoc} + * Set a value in a column + * + * @param [type] $offset The table column to set the value + * @param [type] $value The value to set + * + * @return void */ public function offsetSet($offset, $value): void { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } + if (!$this->tableColumnExists($offset)) + { + throw new RoleNotFoundException; + } + + $column = $this->getCleanColumn($offset); - $this->table->$offset = $value; + $this->table->$column = $value; - $this->table->store(); + $this->table->store(); } /** - * {@inheritdoc} + * Reset the value to a + * + * @param mixed $offset The table column to reset the value to null + * + * @return void */ public function offsetUnset($offset): void { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } + if (!$this->offsetExists($offset)) + { + throw new RoleNotFoundException; + } + + $column = $this->getCleanColumn($offset); - $this->table->$offset = ''; + $this->table->$column = null; - $this->table->store(); + $this->table->store(true); } - /** - * Convert file names to table columns - * - * @param string $name - * - * @return string - */ - protected function getCleanColumn($name): string - { - return str_replace('.', '_', $name); - } + /** + * Convert file names to table columns + * + * @param string $name The original file name + * + * @return string + */ + protected function getCleanColumn($name): string + { + return str_replace('.', '_', $name); + } } diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 521d11561f8..70239082975 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -114,10 +114,13 @@ public function getValidUpdate(): mixed // $db = Factory::getDbo(); $fileFetcher = GuzzleFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); + + $storage = new DatabaseStorage($db, $this->extensionId); + $updater = new Updater( $fileFetcher, $this->params['mirrors'], - new DatabaseStorage($db, $this->extensionId) + $storage ); try @@ -125,16 +128,8 @@ public function getValidUpdate(): mixed // 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(); - $query = $db->getQuery(true) - ->select('targets_json') - ->from($db->quoteName('#__tuf_metadata', 'map')) - ->where($db->quoteName('map.id') . ' = :id') - ->bind(':id', $this->extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - $resultArray = (array) $db->loadObject(); - return JsonNormalizer::decode($resultArray['targets_json']); + return $storage['targets.json']; } catch (FreezeAttackException | MetadataException | SignatureThresholdException | RollbackAttackException $e) { diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php new file mode 100644 index 00000000000..1b5ac30f32e --- /dev/null +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -0,0 +1,309 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater\Adapter; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Application\ApplicationHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Table\Table; +use Joomla\CMS\Updater\UpdateAdapter; +use Joomla\CMS\Updater\Updater; +use Joomla\CMS\Version; +use Joomla\CMS\TUF\TufValidation; +use Joomla\Database\ParameterType; + +/** + * Extension class for updater + * + * @since 1.7.0 + */ +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 1.7.0 + */ + public function findUpdate($options) + { + // Get extension_id for TufValidation + $db = $this->parent->getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__update_sites_extensions')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); + $db->setQuery($query); + + try + { + $extension_id = $db->loadResult(); + } + catch (\RuntimeException $e) + { + // Do nothing + } + + $params = [ + 'url_prefix' => 'https://raw.githubusercontent.com', + 'metadata_path' => '/joomla/updates/test/repository/', + 'targets_path' => '/targets/', + 'mirrors' => [] + ]; + + $TufValidation = new TufValidation($extension_id, $params); + $metaData = $TufValidation->getValidUpdate(); + + if ($metaData === false) + { + return false; + } + + $metaData = json_decode($metaData); + + $b = $metaData['signed']['targets']; + + if (isset($metaData->signed->targets)) + { + $targets = $metaData->signed->targets; + foreach ($targets as $filename => $target) + { + + } + $c = $metaData->signed->targets + } + + + + //print_r($metaData->version); + var_dump($metaData);die(); + + $table = Table::getInstance('Update'); + + // Evaluate Data + + + $this->currentUpdate->update_site_id = $this->updateSiteId; + $this->currentUpdate->detailsurl = $this->_url; + $this->currentUpdate->folder = ''; + $this->currentUpdate->client_id = 1; + $this->currentUpdate->infourl = ''; + /** + if (\in_array($name, $this->updatecols)) + { + $name = strtolower($name); + $this->currentUpdate->$name = ''; + } + + if ($name === 'TARGETPLATFORM') + { + $this->currentUpdate->targetplatform = $attrs; + } + + if ($name === 'PHP_MINIMUM') + { + $this->currentUpdate->php_minimum = ''; + } + + if ($name === 'SUPPORTED_DATABASES') + { + $this->currentUpdate->supported_databases = $attrs; + } + **/ + // Lower case and remove the exclamation mark + $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); + print_r($product); + + // Check that the product matches and that the version matches (optionally a regexp) + if ($product == $this->currentUpdate->targetplatform['NAME'] + && preg_match('/^' . $this->currentUpdate->targetplatform['VERSION'] . '/', JVERSION)) + { + // Check if PHP version supported via tag, assume true if tag isn't present + if (!isset($this->currentUpdate->php_minimum) || version_compare(PHP_VERSION, $this->currentUpdate->php_minimum, '>=')) + { + $phpMatch = true; + } + else + { + // Notify the user of the potential update + $msg = Text::sprintf( + 'JLIB_INSTALLER_AVAILABLE_UPDATE_PHP_VERSION', + $this->currentUpdate->name, + $this->currentUpdate->version, + $this->currentUpdate->php_minimum, + PHP_VERSION + ); + + Factory::getApplication()->enqueueMessage($msg, 'warning'); + + $phpMatch = false; + } + + $dbMatch = false; + + // Check if DB & version is supported via tag, assume supported if tag isn't present + if (isset($this->currentUpdate->supported_databases)) + { + $db = Factory::getDbo(); + $dbType = strtoupper($db->getServerType()); + $dbVersion = $db->getVersion(); + $supportedDbs = $this->currentUpdate->supported_databases; + + // 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 (\array_key_exists($dbType, $supportedDbs)) + { + $minimumVersion = $supportedDbs[$dbType]; + $dbMatch = version_compare($dbVersion, $minimumVersion, '>='); + + if (!$dbMatch) + { + // Notify the user of the potential update + $dbMsg = Text::sprintf( + 'JLIB_INSTALLER_AVAILABLE_UPDATE_DB_MINIMUM', + $this->currentUpdate->name, + $this->currentUpdate->version, + Text::_($db->name), + $dbVersion, + $minimumVersion + ); + + Factory::getApplication()->enqueueMessage($dbMsg, 'warning'); + } + } + else + { + // Notify the user of the potential update + $dbMsg = Text::sprintf( + 'JLIB_INSTALLER_AVAILABLE_UPDATE_DB_TYPE', + $this->currentUpdate->name, + $this->currentUpdate->version, + Text::_($db->name) + ); + + Factory::getApplication()->enqueueMessage($dbMsg, 'warning'); + } + } + else + { + // Set to true if the tag is not set + $dbMatch = true; + } + + // Check minimum stability + $stabilityMatch = true; + + if (isset($this->currentUpdate->stability) && ($this->currentUpdate->stability < $this->minimum_stability)) + { + $stabilityMatch = false; + } + + // Some properties aren't valid fields in the update table so unset them to prevent J! from trying to store them + unset($this->currentUpdate->targetplatform); + + if (isset($this->currentUpdate->php_minimum)) + { + unset($this->currentUpdate->php_minimum); + } + + if (isset($this->currentUpdate->supported_databases)) + { + unset($this->currentUpdate->supported_databases); + } + + if (isset($this->currentUpdate->stability)) + { + unset($this->currentUpdate->stability); + } + + // If the PHP version and minimum stability checks pass, consider this version as a possible update + if ($phpMatch && $stabilityMatch && $dbMatch) + { + if (isset($this->latest)) + { + // We already have a possible update. Check the version. + if (version_compare($this->currentUpdate->version, $this->latest->version, '>') == 1) + { + $this->latest = $this->currentUpdate; + } + } + else + { + // We don't have any possible updates yet, assume this is an available update. + $this->latest = $this->currentUpdate; + } + } + } + + if (\array_key_exists('minimum_stability', $options)) + { + $this->minimum_stability = $options['minimum_stability']; + } +//$this->update_sites[] = array('type' => 'collection', 'location' => $attrs['REF'], 'update_site_id' => $this->updateSiteId); + if (isset($this->latest)) + { + if (isset($this->latest->client) && \strlen($this->latest->client)) + { + $this->latest->client_id = ApplicationHelper::getClientInfo($this->latest->client, true)->id; + + unset($this->latest->client); + } + + $updates = array($this->latest); + } + else + { + $updates = array(); + } + + return array('update_sites' => array(), 'updates' => $updates); + } + + /** + * 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 3.4 + */ + protected function stabilityTagToInteger($tag) + { + $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); + + if (\defined($constant)) + { + return \constant($constant); + } + + return Updater::STABILITY_STABLE; + } +} From effde12d8b0f2fd4339f0f1a9f9e4dcda99eac17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sat, 11 Jun 2022 15:01:18 +0200 Subject: [PATCH 019/126] Change GuzzleFilteFetcher to HttpFileFetcher --- libraries/src/TUF/TufValidation.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 70239082975..0fdee05862a 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -15,6 +15,7 @@ use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\OptionsResolver; use Tuf\Client\GuzzleFileFetcher; +use Tuf\Client\HttpFileFetcher; use Tuf\Client\Updater; use Tuf\Exception\Attack\FreezeAttackException; use Tuf\Exception\Attack\RollbackAttackException; @@ -113,7 +114,7 @@ public function getValidUpdate(): mixed // $db = Factory::getDbo(); - $fileFetcher = GuzzleFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); + $fileFetcher = HttpFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); $storage = new DatabaseStorage($db, $this->extensionId); From 3b057a895622fa0b35899be269baf56c963684da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sat, 11 Jun 2022 15:03:26 +0200 Subject: [PATCH 020/126] add missing semicolon --- libraries/src/Updater/Adapter/TufAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 1b5ac30f32e..bb6d111d8bd 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -85,7 +85,7 @@ public function findUpdate($options) { } - $c = $metaData->signed->targets + $c = $metaData->signed->targets; } From 1da619b26b3eb2d039ce4af54b99e86068ae1355 Mon Sep 17 00:00:00 2001 From: zero-24 Date: Sat, 11 Jun 2022 17:36:33 +0200 Subject: [PATCH 021/126] initial wip --- libraries/src/Updater/ConstrainChecker.php | 195 +++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 libraries/src/Updater/ConstrainChecker.php diff --git a/libraries/src/Updater/ConstrainChecker.php b/libraries/src/Updater/ConstrainChecker.php new file mode 100644 index 00000000000..9fd0af7f130 --- /dev/null +++ b/libraries/src/Updater/ConstrainChecker.php @@ -0,0 +1,195 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Updater\Updater; +use Joomla\CMS\Version; + +/** + * ConstrainChecker Class + * + * @since __DEPLOY_VERSION__ + */ +class ConstrainChecker +{ + /** + * Checks whether the passed constraints are matched + * + * @param array $constraints + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function check($constraints) + { + if (!isset($constraints['targetplatform'])) + { + // targetplatform is required + return false; + } + + // check targetplatform -> true/false + if (!$this->checkTargetplatform($constraints['targetplatform'])) + { + return false; + } + + // check php_minimum + if (isset($constraints['phpMinimum']) && !$this->checkPhpMinimum($constraints['phpMinimum'])) + { + return false; + } + + // check supported databases + if (isset($constraints['supportedDatabases']) && !$this->checkSupportedDatabases($constraints['supportedDatabases'])) + { + return false; + } + + // check stability + if (isset($constraints['stability']) && !$this->checkStability($constraints['stability'])) + { + return false; + } + + return true; + + } + + /** + * Check the targetPlatform + * + * @param object $targetPlatform + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function checkTargetplatform($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; + } + + /** + * Character Parser Function + * + * @param string $phpMinimum The minimum php version + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkPhpMinimum($phpMinimum) + { + // Check if PHP version supported via tag, assume true if tag isn't present + if (version_compare(PHP_VERSION, $phpMinimum, '>=')) + { + return true; + } + } + + /** + * Character Parser Function + * + * @param object $supportedDatabases stdClass of supporte databases and versions + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkSupportedDatabases($supportedDatabases) + { + $db = Factory::getDbo(); + $dbType = strtoupper($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 (\property_exists($dbType, $supportedDatabases)) + { + $minimumVersion = $supportedDatabases[$dbType]; + + return version_compare($dbVersion, $minimumVersion, '>='); + } + + return false; + } + + /** + * Character Parser Function + * + * @param string $stability Stability to check + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkStability($stability) + { + $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + + $stability = $this->stabilityTagToInteger($stability); + + if (($stability < $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 stabilityTagToInteger($tag) + { + $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); + + if (\defined($constant)) + { + return \constant($constant); + } + + return Updater::STABILITY_STABLE; + } +} From f6ac9c520f47e1edbe9744c2e7216966dd42d9af Mon Sep 17 00:00:00 2001 From: zero-24 Date: Sat, 11 Jun 2022 17:44:27 +0200 Subject: [PATCH 022/126] doc block updates --- libraries/src/Updater/ConstrainChecker.php | 25 +++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/libraries/src/Updater/ConstrainChecker.php b/libraries/src/Updater/ConstrainChecker.php index 9fd0af7f130..9b895eab6d5 100644 --- a/libraries/src/Updater/ConstrainChecker.php +++ b/libraries/src/Updater/ConstrainChecker.php @@ -26,7 +26,7 @@ class ConstrainChecker /** * Checks whether the passed constraints are matched * - * @param array $constraints + * @param array $constraints The provided constraints to be checked * * @return bool * @@ -40,32 +40,31 @@ public function check($constraints) return false; } - // check targetplatform -> true/false + // Check targetplatform if (!$this->checkTargetplatform($constraints['targetplatform'])) { return false; } - // check php_minimum + // Check php_minimum if (isset($constraints['phpMinimum']) && !$this->checkPhpMinimum($constraints['phpMinimum'])) { return false; } - // check supported databases + // Check supported databases if (isset($constraints['supportedDatabases']) && !$this->checkSupportedDatabases($constraints['supportedDatabases'])) { return false; } - // check stability + // Check stability if (isset($constraints['stability']) && !$this->checkStability($constraints['stability'])) { return false; } return true; - } /** @@ -73,7 +72,7 @@ public function check($constraints) * * @param object $targetPlatform * - * @return void + * @return bool * * @since __DEPLOY_VERSION__ */ @@ -93,9 +92,9 @@ protected function checkTargetplatform($targetPlatform) } /** - * Character Parser Function + * Check the minimum PHP version * - * @param string $phpMinimum The minimum php version + * @param string $phpMinimum The minimum php version to check * * @return bool * @@ -111,9 +110,9 @@ protected function checkPhpMinimum($phpMinimum) } /** - * Character Parser Function + * Check the supported databases and versions * - * @param object $supportedDatabases stdClass of supporte databases and versions + * @param object $supportedDatabases stdClass of supported databases and versions * * @return bool * @@ -149,9 +148,9 @@ protected function checkSupportedDatabases($supportedDatabases) } /** - * Character Parser Function + * Check the stability * - * @param string $stability Stability to check + * @param string $stability Stability tag to check * * @return bool * From 5585d3276e8809d32cd10c0545d02c1259b1e9f8 Mon Sep 17 00:00:00 2001 From: zero-24 Date: Sat, 11 Jun 2022 18:08:51 +0200 Subject: [PATCH 023/126] wip php minimum & stability --- libraries/src/Updater/ConstrainChecker.php | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/libraries/src/Updater/ConstrainChecker.php b/libraries/src/Updater/ConstrainChecker.php index 9b895eab6d5..37f122e961a 100644 --- a/libraries/src/Updater/ConstrainChecker.php +++ b/libraries/src/Updater/ConstrainChecker.php @@ -59,7 +59,7 @@ public function check($constraints) } // Check stability - if (isset($constraints['stability']) && !$this->checkStability($constraints['stability'])) + if (isset($constraints['stability']) && !$this->checkStability($constraints['stability']['tags'])) { return false; } @@ -103,10 +103,7 @@ protected function checkTargetplatform($targetPlatform) protected function checkPhpMinimum($phpMinimum) { // Check if PHP version supported via tag, assume true if tag isn't present - if (version_compare(PHP_VERSION, $phpMinimum, '>=')) - { - return true; - } + return version_compare(PHP_VERSION, $phpMinimum, '>='); } /** @@ -150,24 +147,27 @@ protected function checkSupportedDatabases($supportedDatabases) /** * Check the stability * - * @param string $stability Stability tag to check + * @param array $stabilityTags Stability tags to check * * @return bool * * @since __DEPLOY_VERSION__ */ - protected function checkStability($stability) + protected function checkStability($stabilityTags) { $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); - $stability = $this->stabilityTagToInteger($stability); - - if (($stability < $minimumStability)) + foreach ($stabilityTags as $tag) { - return false; - } + $stability = $this->stabilityTagToInteger($stability); - return true; + if (($stability < $minimumStability)) + { + return false; + } + + return true; + } } /** From f35a4a9161f52f739722c5dbe2f6f1e58cf38224 Mon Sep 17 00:00:00 2001 From: zero-24 Date: Sat, 11 Jun 2022 18:21:29 +0200 Subject: [PATCH 024/126] wip stability validation --- libraries/src/Updater/ConstrainChecker.php | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libraries/src/Updater/ConstrainChecker.php b/libraries/src/Updater/ConstrainChecker.php index 37f122e961a..b319f7d0b87 100644 --- a/libraries/src/Updater/ConstrainChecker.php +++ b/libraries/src/Updater/ConstrainChecker.php @@ -32,7 +32,7 @@ class ConstrainChecker * * @since __DEPLOY_VERSION__ */ - public function check($constraints) + public function check(array $constraints) { if (!isset($constraints['targetplatform'])) { @@ -76,7 +76,7 @@ public function check($constraints) * * @since __DEPLOY_VERSION__ */ - protected function checkTargetplatform($targetPlatform) + protected function checkTargetplatform(stdClass $targetPlatform) { // Lower case and remove the exclamation mark $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); @@ -100,7 +100,7 @@ protected function checkTargetplatform($targetPlatform) * * @since __DEPLOY_VERSION__ */ - protected function checkPhpMinimum($phpMinimum) + protected function checkPhpMinimum(string $phpMinimum) { // Check if PHP version supported via tag, assume true if tag isn't present return version_compare(PHP_VERSION, $phpMinimum, '>='); @@ -115,7 +115,7 @@ protected function checkPhpMinimum($phpMinimum) * * @since __DEPLOY_VERSION__ */ - protected function checkSupportedDatabases($supportedDatabases) + protected function checkSupportedDatabases(stdClass $supportedDatabases) { $db = Factory::getDbo(); $dbType = strtoupper($db->getServerType()); @@ -153,21 +153,23 @@ protected function checkSupportedDatabases($supportedDatabases) * * @since __DEPLOY_VERSION__ */ - protected function checkStability($stabilityTags) + protected function checkStability(array $stabilityTags) { $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + $stabilityMatch = false; + foreach ($stabilityTags as $tag) { - $stability = $this->stabilityTagToInteger($stability); + $stability = $this->stabilityTagToInteger($tag); - if (($stability < $minimumStability)) + if (($stability >= $minimumStability)) { - return false; + $stabilityMatch = true; } - - return true; } + + return $stabilityMatch; } /** From 59eb40ce799e5c677066310afc95a73a0ab63d44 Mon Sep 17 00:00:00 2001 From: zero-24 Date: Sat, 11 Jun 2022 18:25:03 +0200 Subject: [PATCH 025/126] property_exists and variable names --- libraries/src/Updater/ConstrainChecker.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/src/Updater/ConstrainChecker.php b/libraries/src/Updater/ConstrainChecker.php index b319f7d0b87..f94574a5e83 100644 --- a/libraries/src/Updater/ConstrainChecker.php +++ b/libraries/src/Updater/ConstrainChecker.php @@ -47,19 +47,19 @@ public function check(array $constraints) } // Check php_minimum - if (isset($constraints['phpMinimum']) && !$this->checkPhpMinimum($constraints['phpMinimum'])) + if (isset($constraints['php_minimum']) && !$this->checkPhpMinimum($constraints['php_minimum'])) { return false; } // Check supported databases - if (isset($constraints['supportedDatabases']) && !$this->checkSupportedDatabases($constraints['supportedDatabases'])) + if (isset($constraints['supported_databases']) && !$this->checkSupportedDatabases($constraints['supported_databases'])) { return false; } // Check stability - if (isset($constraints['stability']) && !$this->checkStability($constraints['stability']['tags'])) + if (isset($constraints['tags']) && !$this->checkStability($constraints['tags'])) { return false; } @@ -134,7 +134,7 @@ protected function checkSupportedDatabases(stdClass $supportedDatabases) } // Do we have an entry for the database? - if (\property_exists($dbType, $supportedDatabases)) + if (\property_exists($supportedDatabases, $dbType)) { $minimumVersion = $supportedDatabases[$dbType]; From ee8d1e9cc26ac59b9d8a0f6af425a2ea730d8b95 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 11 Jun 2022 18:52:37 +0200 Subject: [PATCH 026/126] added tests and tweaks --- ...trainChecker.php => ConstraintChecker.php} | 10 +- .../Cms/Updater/ConstraintCheckerTest.php | 213 ++++++++++++++++++ 2 files changed, 218 insertions(+), 5 deletions(-) rename libraries/src/Updater/{ConstrainChecker.php => ConstraintChecker.php} (94%) create mode 100644 tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php diff --git a/libraries/src/Updater/ConstrainChecker.php b/libraries/src/Updater/ConstraintChecker.php similarity index 94% rename from libraries/src/Updater/ConstrainChecker.php rename to libraries/src/Updater/ConstraintChecker.php index f94574a5e83..70db5ffaf25 100644 --- a/libraries/src/Updater/ConstrainChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -21,7 +21,7 @@ * * @since __DEPLOY_VERSION__ */ -class ConstrainChecker +class ConstraintChecker { /** * Checks whether the passed constraints are matched @@ -76,7 +76,7 @@ public function check(array $constraints) * * @since __DEPLOY_VERSION__ */ - protected function checkTargetplatform(stdClass $targetPlatform) + protected function checkTargetplatform(\stdClass $targetPlatform) { // Lower case and remove the exclamation mark $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); @@ -115,10 +115,10 @@ protected function checkPhpMinimum(string $phpMinimum) * * @since __DEPLOY_VERSION__ */ - protected function checkSupportedDatabases(stdClass $supportedDatabases) + protected function checkSupportedDatabases(\stdClass $supportedDatabases) { $db = Factory::getDbo(); - $dbType = strtoupper($db->getServerType()); + $dbType = strtolower($db->getServerType()); $dbVersion = $db->getVersion(); // MySQL and MariaDB use the same database driver but not the same version numbers @@ -136,7 +136,7 @@ protected function checkSupportedDatabases(stdClass $supportedDatabases) // Do we have an entry for the database? if (\property_exists($supportedDatabases, $dbType)) { - $minimumVersion = $supportedDatabases[$dbType]; + $minimumVersion = $supportedDatabases->$dbType; return version_compare($dbVersion, $minimumVersion, '>='); } diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php new file mode 100644 index 00000000000..0a2439f3d61 --- /dev/null +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -0,0 +1,213 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms; + +use Joomla\CMS\Factory; +use Joomla\CMS\Updater\ConstraintChecker; +use Joomla\Database\DatabaseDriver; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for Version. + * + * @package Joomla.UnitTest + * @subpackage Version + * @since __DEPLOY_VERSION__ + */ +class ConstraintCheckerTest extends UnitTestCase +{ + /** + * @var ConstraintChecker + * @since 3.0 + */ + 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(); + } + + public function testCheckMethodReturnsFalseIfPlatformIsMissing() + { + $constraint = []; + $this->assertFalse($this->checker->check($constraint)); + } + + public function testCheckMethodReturnsTrueIfPlatformIsOnlyConstraint() + { + $constraint = ['targetplatform' => (object) ["name" => "joomla", "version" => "4.*"]]; + $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'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], + (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + false + ], + [ + ['type' => 'pgsql', 'version' => '14.3'], + (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + false + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.4'], + false + ], + [ + ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + ]; + } + + /** + * Data provider for testCheckTargetplatform method + * + * @since __DEPLOY_VERSION__ + * + * @return array[] + */ + protected function targetplatformDataProvider() + { + return [ + [(object) ["name" => "foobar", "version" => "1.*"], false], + [(object) ["name" => "foobar", "version" => "4.*"], false], + [(object) ["name" => "joomla", "version" => "1.*"], false], + [(object) ["name" => "joomla", "version" => "3.1.2"], false], + [(object) ["name" => "joomla", "version" => ""], true], + [(object) ["name" => "joomla", "version" => ".*"], true], + [(object) ["name" => "joomla", "version" => JVERSION], true], + [(object) ["name" => "joomla", "version" => "4.*"], 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; + } +} From 99654511d8434fa5ee50791c6b8f8ed6e592d3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sat, 11 Jun 2022 19:00:13 +0200 Subject: [PATCH 027/126] wip --- .../com_installer/src/Model/UpdateModel.php | 2 + libraries/src/TUF/TufValidation.php | 4 +- libraries/src/Updater/Adapter/TufAdapter.php | 313 ++++++------------ 3 files changed, 108 insertions(+), 211 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 78d0560fefb..df36a454743 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -361,6 +361,8 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) continue; } + // TODO Load Type based on #__updates_site.type + // if type tuf loadFromTuf $update->loadFromXml($instance->detailsurl, $minimumStability); // Find and use extra_query from update_site if available diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 0fdee05862a..978ce99c949 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -42,7 +42,7 @@ class TufValidation * * @var mixed */ - private mixed $params; + private $params; /** * Validating updates with TUF @@ -61,7 +61,7 @@ public function __construct(int $extensionId, mixed $params) { $this->configureTufOptions($resolver); } - catch (\Exception) + catch (\Exception $e) { } diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index bb6d111d8bd..b57629f721c 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -3,7 +3,7 @@ * Joomla! Content Management System * * @copyright (C) 2008 Open Source Matters, Inc. - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Updater\Adapter; @@ -20,6 +20,7 @@ use Joomla\CMS\Version; use Joomla\CMS\TUF\TufValidation; use Joomla\Database\ParameterType; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * Extension class for updater @@ -28,6 +29,13 @@ */ class TufAdapter extends UpdateAdapter { + private $clientId = [ + 'site' => 0, + 'administrator' => 1, + 'installation' => 2, + 'api' => 3, + 'cli' => 4 + ]; /** * Finds an update. @@ -40,6 +48,36 @@ class TufAdapter extends UpdateAdapter */ public function findUpdate($options) { + $updates = []; + $targets = $this->getUpdateTargets($options); + + foreach ($targets as $target) + { + $updateTable = Table::getInstance('update'); + $updateTable->set('update_site_id', $options['update_site_id']); + + $updateTable->bind($target); + + $updates[] = $updateTable; + } + + return array('update_sites' => array(), 'updates' => $updates); + } + + public function getUpdateTargets($options) + { + $versions = array(); + $resolver = new OptionsResolver; + + try + { + $this->configureUpdateOptions($resolver); + $keys = $resolver->getDefinedOptions(); + } + catch (\Exception $e) + { + } + // Get extension_id for TufValidation $db = $this->parent->getDbo(); @@ -60,250 +98,107 @@ public function findUpdate($options) } $params = [ - 'url_prefix' => 'https://raw.githubusercontent.com', + 'url_prefix' => 'https://raw.githubusercontent.com', 'metadata_path' => '/joomla/updates/test/repository/', - 'targets_path' => '/targets/', - 'mirrors' => [] + 'targets_path' => '/targets/', + 'mirrors' => [] ]; $TufValidation = new TufValidation($extension_id, $params); - $metaData = $TufValidation->getValidUpdate(); - - if ($metaData === false) - { - return false; - } + $metaData = $TufValidation->getValidUpdate(); $metaData = json_decode($metaData); - $b = $metaData['signed']['targets']; - if (isset($metaData->signed->targets)) { - $targets = $metaData->signed->targets; - foreach ($targets as $filename => $target) - { - - } - $c = $metaData->signed->targets; - } - - - - //print_r($metaData->version); - var_dump($metaData);die(); - - $table = Table::getInstance('Update'); - - // Evaluate Data - - - $this->currentUpdate->update_site_id = $this->updateSiteId; - $this->currentUpdate->detailsurl = $this->_url; - $this->currentUpdate->folder = ''; - $this->currentUpdate->client_id = 1; - $this->currentUpdate->infourl = ''; - /** - if (\in_array($name, $this->updatecols)) - { - $name = strtolower($name); - $this->currentUpdate->$name = ''; - } - - if ($name === 'TARGETPLATFORM') - { - $this->currentUpdate->targetplatform = $attrs; - } - - if ($name === 'PHP_MINIMUM') - { - $this->currentUpdate->php_minimum = ''; - } - - if ($name === 'SUPPORTED_DATABASES') - { - $this->currentUpdate->supported_databases = $attrs; - } - **/ - // Lower case and remove the exclamation mark - $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); - print_r($product); - - // Check that the product matches and that the version matches (optionally a regexp) - if ($product == $this->currentUpdate->targetplatform['NAME'] - && preg_match('/^' . $this->currentUpdate->targetplatform['VERSION'] . '/', JVERSION)) - { - // Check if PHP version supported via tag, assume true if tag isn't present - if (!isset($this->currentUpdate->php_minimum) || version_compare(PHP_VERSION, $this->currentUpdate->php_minimum, '>=')) - { - $phpMatch = true; - } - else - { - // Notify the user of the potential update - $msg = Text::sprintf( - 'JLIB_INSTALLER_AVAILABLE_UPDATE_PHP_VERSION', - $this->currentUpdate->name, - $this->currentUpdate->version, - $this->currentUpdate->php_minimum, - PHP_VERSION - ); - - Factory::getApplication()->enqueueMessage($msg, 'warning'); - - $phpMatch = false; - } - - $dbMatch = false; - - // Check if DB & version is supported via tag, assume supported if tag isn't present - if (isset($this->currentUpdate->supported_databases)) + foreach ($metaData->signed->targets as $filename => $target) { - $db = Factory::getDbo(); - $dbType = strtoupper($db->getServerType()); - $dbVersion = $db->getVersion(); - $supportedDbs = $this->currentUpdate->supported_databases; + $values = []; - // MySQL and MariaDB use the same database driver but not the same version numbers - if ($dbType === 'mysql') + foreach ($keys as $key) { - // Check whether we have a MariaDB version string and extract the proper version from it - if (stripos($dbVersion, 'mariadb') !== false) + if (isset($target->custom->$key)) { - // MariaDB: Strip off any leading '5.5.5-', if present - $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); - $dbType = 'mariadb'; + $values[$key] = $target->custom->$key; } } - // Do we have an entry for the database? - if (\array_key_exists($dbType, $supportedDbs)) - { - $minimumVersion = $supportedDbs[$dbType]; - $dbMatch = version_compare($dbVersion, $minimumVersion, '>='); - - if (!$dbMatch) - { - // Notify the user of the potential update - $dbMsg = Text::sprintf( - 'JLIB_INSTALLER_AVAILABLE_UPDATE_DB_MINIMUM', - $this->currentUpdate->name, - $this->currentUpdate->version, - Text::_($db->name), - $dbVersion, - $minimumVersion - ); - Factory::getApplication()->enqueueMessage($dbMsg, 'warning'); - } - } - else + if (isset($values['client']) && is_string($values['client']) + && key_exists(strtolower($values['client']), $this->clientId)) { - // Notify the user of the potential update - $dbMsg = Text::sprintf( - 'JLIB_INSTALLER_AVAILABLE_UPDATE_DB_TYPE', - $this->currentUpdate->name, - $this->currentUpdate->version, - Text::_($db->name) - ); - - Factory::getApplication()->enqueueMessage($dbMsg, 'warning'); + $values['client'] = $this->clientId[strtolower($values['client'])]; } - } - else - { - // Set to true if the tag is not set - $dbMatch = true; - } - - // Check minimum stability - $stabilityMatch = true; - - if (isset($this->currentUpdate->stability) && ($this->currentUpdate->stability < $this->minimum_stability)) - { - $stabilityMatch = false; - } - - // Some properties aren't valid fields in the update table so unset them to prevent J! from trying to store them - unset($this->currentUpdate->targetplatform); - - if (isset($this->currentUpdate->php_minimum)) - { - unset($this->currentUpdate->php_minimum); - } - - if (isset($this->currentUpdate->supported_databases)) - { - unset($this->currentUpdate->supported_databases); - } - if (isset($this->currentUpdate->stability)) - { - unset($this->currentUpdate->stability); - } + if (isset($values['infourl']) && isset($values['infourl']->url)) + { + $values['infourl'] = $values['infourl']->url; + } - // If the PHP version and minimum stability checks pass, consider this version as a possible update - if ($phpMatch && $stabilityMatch && $dbMatch) - { - if (isset($this->latest)) + try { - // We already have a possible update. Check the version. - if (version_compare($this->currentUpdate->version, $this->latest->version, '>') == 1) - { - $this->latest = $this->currentUpdate; - } + $values = $resolver->resolve($values); } - else + catch (\Exception $e) { - // We don't have any possible updates yet, assume this is an available update. - $this->latest = $this->currentUpdate; + continue; } - } - } - - if (\array_key_exists('minimum_stability', $options)) - { - $this->minimum_stability = $options['minimum_stability']; - } -//$this->update_sites[] = array('type' => 'collection', 'location' => $attrs['REF'], 'update_site_id' => $this->updateSiteId); - if (isset($this->latest)) - { - if (isset($this->latest->client) && \strlen($this->latest->client)) - { - $this->latest->client_id = ApplicationHelper::getClientInfo($this->latest->client, true)->id; - unset($this->latest->client); + $versions[$values['version']] = $values; } - $updates = array($this->latest); - } - else - { - $updates = array(); + usort($versions, function ($a, $b) { + return version_compare($a['version'], $b['version']); + }); + + // TODO ConstraintsCheck } - return array('update_sites' => array(), 'updates' => $updates); + return $versions; } /** - * 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 + * Configures default values or pass arguments to params * - * @return integer + * @param OptionsResolver $resolver The OptionsResolver for the params * - * @since 3.4 + * @return void */ - protected function stabilityTagToInteger($tag) + protected function configureUpdateOptions(OptionsResolver $resolver) { - $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); - - if (\defined($constant)) - { - return \constant($constant); - } - - return Updater::STABILITY_STABLE; + $resolver->setDefaults( + [ + 'version' => "1", + 'name' => null, + 'client' => 1, + 'description' => '', + 'element' => '', + 'detailsurl' => '', + 'data' => '', + 'infourl' => '', + 'type' => null, + 'tags' => new \StdClass, + 'targetplatform' => new \StdClass, + 'supported_databases' => new \StdClass, + 'downloads' => [], + 'php_minimum' => null + ] + ) + ->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('php_minimum', 'string') + ->setAllowedTypes('downloads', 'array') + ->setAllowedTypes('tags', 'object') + ->setAllowedTypes('targetplatform', 'object') + ->setAllowedTypes('supported_databases', 'object') + ->setAllowedTypes('targetplatform', 'object') + ->setRequired(['version']); } } From 6086b2d9bd80ab4536c85e16fa1ba799e84e93a7 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Sun, 12 Jun 2022 11:22:59 +0200 Subject: [PATCH 028/126] throw exception in installer update model when update site is tuf --- .../com_installer/src/Model/UpdateModel.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 78d0560fefb..ffb0e9db9f4 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -23,10 +23,12 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Updater\Update; use Joomla\CMS\Updater\Updater; +use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseQuery; use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\ParameterType; use Joomla\Utilities\ArrayHelper; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; /** * Installer Update Model @@ -361,7 +363,23 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) continue; } - $update->loadFromXml($instance->detailsurl, $minimumStability); + $db = Factory::getContainer()->get(DatabaseDriver::class); + $query = $db->getQuery(true) + ->select('type') + ->from('#__update_sites') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $instance->update_site_id, ParameterType::INTEGER); + $db->setQuery($query); + $updateSiteType = (string) $db->loadObject(); + + if ($updateSiteType == 'tuf') + { + throw new NoSuchOptionException("TUF updates are not yet supported"); + } + else + { + $update->loadFromXml($instance->detailsurl, $minimumStability); + } // Find and use extra_query from update_site if available $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); From d1f622b562f6e02073b2c380260c15fe0c97a7c4 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Sun, 12 Jun 2022 11:59:31 +0200 Subject: [PATCH 029/126] Enqueue message instead of throwing an exception --- .../components/com_installer/src/Model/UpdateModel.php | 5 ++++- administrator/language/en-GB/lib_joomla.ini | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index ffb0e9db9f4..deebdf75a1b 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -363,6 +363,7 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) continue; } + $app = Factory::getApplication(); $db = Factory::getContainer()->get(DatabaseDriver::class); $query = $db->getQuery(true) ->select('type') @@ -374,7 +375,9 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) if ($updateSiteType == 'tuf') { - throw new NoSuchOptionException("TUF updates are not yet supported"); + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error'); + + return; } else { diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index 178145886bc..450de241eae 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -659,6 +659,7 @@ JLIB_INSTALLER_UNINSTALL="Uninstall" JLIB_INSTALLER_UPDATE="Update" JLIB_INSTALLER_UPDATE_LOG_QUERY="Ran query from file %1$s. Query text: %2$s." JLIB_INSTALLER_WARNING_UNABLE_TO_INSTALL_CONTENT_LANGUAGE="Unable to create a content language for %s language: %s" +JLIB_INSTALLER_TUF_NOT_AVAILABLE="TUF is not available for extensions yet." JLIB_JS_AJAX_ERROR_CONNECTION_ABORT="A connection abort has occurred while fetching the JSON data." JLIB_JS_AJAX_ERROR_NO_CONTENT="No content was returned." From 88a99f77822b969e563ae5e4c2d0dcba4925ad6c Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 12:16:34 +0200 Subject: [PATCH 030/126] move jhttp file fetcher into CMS library, update TUF client fork branch --- composer.json | 8 +- composer.lock | 953 ++++++++++-------- libraries/src/TUF/HttpFileFetcher.php | 162 +++ .../Libraries/Cms/TUF/HttpFileFetcherTest.php | 227 +++++ 4 files changed, 902 insertions(+), 448 deletions(-) create mode 100644 libraries/src/TUF/HttpFileFetcher.php create mode 100644 tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php diff --git a/composer.json b/composer.json index 423c707d04e..a7bab3394d6 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-projects/php-tuf.git", + "no-api": true } ], "autoload": { @@ -94,8 +99,9 @@ "web-auth/webauthn-lib": "2.1.*", "composer/ca-bundle": "^1.2", "dragonmantank/cron-expression": "^3.1", + "enshrined/svg-sanitize": "^0.15.4", "symfony/validator": "^5.4", - "enshrined/svg-sanitize": "^0.15.4" + "php-tuf/php-tuf": "dev-joomla-tuf-combined" }, "require-dev": { "phpunit/phpunit": "^8.5", diff --git a/composer.lock b/composer.lock index 9dfb239cb98..74f29f8f49a 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": "1868030bc5c5a79c3bd59fc9e6aa8cf4", + "content-hash": "c149d12f58bfe592455c492336ba5c73", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -717,6 +717,200 @@ }, "time": "2020-03-31T17:50:54+00:00" }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "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.1" + }, + "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": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/337e3ad8e5716c15f9657bd214d16cc5e69df268", + "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "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/1.8.5" + }, + "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": "2022-03-20T21:51:18+00:00" + }, { "name": "jakeasmith/http_build_url", "version": "1.0.1", @@ -2490,6 +2684,65 @@ }, "time": "2022-03-31T14:55:54+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, { "name": "nyholm/psr7", "version": "1.5.0", @@ -2707,6 +2960,69 @@ }, "time": "2015-12-19T14:08:53+00:00" }, + { + "name": "php-tuf/php-tuf", + "version": "dev-joomla-tuf-combined", + "source": { + "type": "git", + "url": "https://github.com/joomla-projects/php-tuf.git", + "reference": "2d71b4039686196adab273c59d1fd4ea4468e511" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-projects/php-tuf/zipball/2d71b4039686196adab273c59d1fd4ea4468e511", + "reference": "2d71b4039686196adab273c59d1fd4ea4468e511", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.7", + "myclabs/deep-copy": "^1.10.2", + "paragonie/sodium_compat": "^1.13", + "php": ">=7.2.5", + "symfony/validator": "^4.4 || ^5" + }, + "require-dev": { + "php-tuf/phpcodesniffer-standard": "dev-main", + "phpunit/phpunit": "^8.5.8|^9", + "symfony/phpunit-bridge": "^5" + }, + "suggest": { + "ext-libsodium": "Provides faster verification of updates", + "guzzlehttp/guzzle": "Required package if GuzzleFileFetcher shall be used" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuf\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tuf\\Tests\\": "tests/" + } + }, + "scripts": { + "phpcs": [ + "phpcs -s --standard=PhpTuf ./src ./tests" + ], + "phpcbf": [ + "phpcbf --standard=PhpTuf ./src ./tests" + ], + "test": [ + "phpunit ./tests" + ], + "lint": [ + "find src -name '*.php' -exec php -l {} \\;" + ] + }, + "license": [ + "MIT" + ], + "description": "PHP implementation of The Update Framework (TUF)", + "time": "2022-06-12T09:53:53+00:00" + }, { "name": "phpmailer/phpmailer", "version": "v6.6.0", @@ -3062,16 +3378,65 @@ "require": { "php": ">=5.3.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+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": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } + "files": [ + "src/getallheaders.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3079,21 +3444,16 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], + "description": "A polyfill for getallheaders.", "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { "name": "ramsey/uuid", @@ -3719,16 +4079,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -3743,7 +4103,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3781,7 +4141,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" }, "funding": [ { @@ -3797,7 +4157,7 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-iconv", @@ -4049,16 +4409,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { @@ -4073,7 +4433,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4112,7 +4472,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -4128,7 +4488,7 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php72", @@ -4208,16 +4568,16 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -4226,7 +4586,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4267,7 +4627,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" }, "funding": [ { @@ -4283,20 +4643,20 @@ "type": "tidelift" } ], - "time": "2021-06-05T21:20:04+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", - "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -4305,7 +4665,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4350,7 +4710,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" }, "funding": [ { @@ -4366,20 +4726,20 @@ "type": "tidelift" } ], - "time": "2022-03-04T08:16:47+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", - "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", "shasum": "" }, "require": { @@ -4388,7 +4748,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4429,7 +4789,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" }, "funding": [ { @@ -4445,7 +4805,7 @@ "type": "tidelift" } ], - "time": "2021-09-13T13:58:11+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/service-contracts", @@ -6863,236 +7223,51 @@ "shasum": "" }, "require": { - "composer/semver": "^3.2", - "composer/xdebug-handler": "^2.0", - "doctrine/annotations": "^1.12", - "ext-json": "*", - "ext-tokenizer": "*", - "php": "^7.2.5 || ^8.0", - "php-cs-fixer/diff": "^2.0", - "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", - "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", - "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", - "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", - "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.23", - "symfony/process": "^4.4.20 || ^5.0 || ^6.0", - "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" - }, - "require-dev": { - "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^1.5", - "mikey179/vfsstream": "^1.6.8", - "php-coveralls/php-coveralls": "^2.5.2", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^8.5.21 || ^9.5", - "phpunitgoodpractices/polyfill": "^1.5", - "phpunitgoodpractices/traits": "^1.9.1", - "symfony/phpunit-bridge": "^5.2.4 || ^6.0", - "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" - }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters." - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - } - ], - "description": "A tool to automatically fix PHP code style", - "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.4.0" - }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2021-12-11T16:25:08+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "7.4.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ac1ec1cd9b5624694c3a40be801d94137afb12b4", - "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.8.3 || ^2.1", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.5 || ^9.3.5", - "psr/log": "^1.1 || ^2.0 || ^3.0" - }, - "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.4-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "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": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.4.2" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", - "type": "tidelift" - } - ], - "time": "2022-03-20T14:16:28+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "shasum": "" - }, - "require": { - "php": ">=5.5" + "composer/semver": "^3.2", + "composer/xdebug-handler": "^2.0", + "doctrine/annotations": "^1.12", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2.5 || ^8.0", + "php-cs-fixer/diff": "^2.0", + "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", + "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", + "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", + "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", + "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php80": "^1.23", + "symfony/polyfill-php81": "^1.23", + "symfony/process": "^4.4.20 || ^5.0 || ^6.0", + "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^1.5", + "mikey179/vfsstream": "^1.6.8", + "php-coveralls/php-coveralls": "^2.5.2", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5", + "phpunitgoodpractices/polyfill": "^1.5", + "phpunitgoodpractices/traits": "^1.9.1", + "symfony/phpunit-bridge": "^5.2.4 || ^6.0", + "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { - "GuzzleHttp\\Promise\\": "src/" + "PhpCsFixer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7101,91 +7276,76 @@ ], "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": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" } ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], + "description": "A tool to automatically fix PHP code style", "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.4.0" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", + "url": "https://github.com/keradus", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", - "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2021-12-11T16:25:08+00:00" }, { - "name": "guzzlehttp/psr7", - "version": "2.2.1", + "name": "guzzlehttp/guzzle", + "version": "7.4.2", "source": { "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2" + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2", - "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ac1ec1cd9b5624694c3a40be801d94137afb12b4", + "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4", "shasum": "" }, "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.8.3 || ^2.1", "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "ralouphie/getallheaders": "^3.0" + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" }, "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" + "psr/http-client-implementation": "1.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4.1", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.8 || ^9.3.10" + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "7.4-dev" } }, "autoload": { + "files": [ + "src/functions_include.php" + ], "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" + "GuzzleHttp\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7203,6 +7363,11 @@ "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, { "name": "George Mponos", "email": "gmponos@gmail.com", @@ -7222,27 +7387,23 @@ "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", + "description": "Guzzle is a PHP HTTP client library", "keywords": [ + "client", + "curl", + "framework", "http", - "message", + "http client", + "psr-18", "psr-7", - "request", - "response", - "stream", - "uri", - "url" + "rest", + "web service" ], "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.2.1" + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.4.2" }, "funding": [ { @@ -7254,11 +7415,11 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", "type": "tidelift" } ], - "time": "2022-03-20T21:55:58+00:00" + "time": "2022-03-20T14:16:28+00:00" }, { "name": "hoa/consistency", @@ -8259,65 +8420,6 @@ }, "time": "2022-04-13T08:02:27+00:00" }, - { - "name": "myclabs/deep-copy", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2022-03-03T13:19:32+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.3", @@ -9266,50 +9368,6 @@ }, "time": "2019-01-08T18:20:26+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": "sebastian/code-unit-reverse-lookup", "version": "1.0.2", @@ -10844,6 +10902,7 @@ "stability-flags": { "maximebf/debugbar": 20, "tobscure/json-api": 20, + "php-tuf/php-tuf": 20, "joomla/cms-coding-standards": 20, "joomla/coding-standards": 20 }, @@ -10859,5 +10918,5 @@ "platform-overrides": { "php": "7.2.5" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php new file mode 100644 index 00000000000..481a4f6ac38 --- /dev/null +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -0,0 +1,162 @@ +client = $client; + $this->metadataPrefix = $metadataPrefix; + $this->targetsPrefix = $targetsPrefix; + } + + /** + * Creates an instance of this class with a specific base URI. + * + * @param string $baseUri + * The base URI from which to fetch files. + * @param string $metadataPrefix + * (optional) The path prefix for metadata. Defaults to '/metadata/'. + * @param string $targetsPrefix + * (optional) The path prefix for targets. Defaults to '/targets/'. + * + * @return static + * A new instance of this class. + */ + public static function createFromUri( + string $baseUri, + string $metadataPrefix = '/metadata/', + string $targetsPrefix = '/targets/' + ): self { + $httpFactory = new HttpFactory(); + $client = $httpFactory->getHttp([], 'curl'); + + return new static($client, $metadataPrefix, $targetsPrefix); + } + + /** + * {@inheritDoc} + */ + public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface + { + return $this->fetchFile($this->metadataPrefix . $fileName, $maxBytes); + } + + /** + * {@inheritDoc} + * + * @param array $options + * (optional) Additional request options to pass to the Guzzle client. + * See \GuzzleHttp\RequestOptions. + * @param string $url + * (optional) An arbitrary URL from which the target should be downloaded. + * If passed, takes precedence over $fileName. + */ + public function fetchTarget( + string $fileName, + int $maxBytes, + array $options = [], + string $url = null + ): PromiseInterface { + $location = $url ?: $this->targetsPrefix . $fileName; + return $this->fetchFile($location, $maxBytes, $options); + } + + /** + * Fetches a file from a URL. + * + * @param string $url + * The URL of the file to fetch. + * @param integer $maxBytes + * The maximum number of bytes to download. + * @param array $options + * (optional) Additional request options to pass to the Guzzle client. + * See \GuzzleHttp\RequestOptions. + * + * @return \Psr\Http\Message\StreamInterface + * A promise representing the eventual result of the operation. + */ + protected function fetchFile(string $url, int $maxBytes, array $headers = []): PromiseInterface + { + // Create a progress callback to abort the download if it exceeds + // $maxBytes. This will only work with cURL, so we also verify the + // download size when request is finished. + $progress = function (int $expectedBytes, int $downloadedBytes) use ($url, $maxBytes) { + if ($expectedBytes > $maxBytes || $downloadedBytes > $maxBytes) { + throw new DownloadSizeException("$url exceeded $maxBytes bytes"); + } + }; + + /** @var Response $response */ + $response = $this->client->get($url, $headers); + $response->getBody()->rewind(); + + if ($response->getStatusCode() === 404) { + throw new RepoFileNotFound(); + } + + if ($response->getStatusCode() !== 200) { + throw new \RuntimeException( + "Invalid TUF repo response: " . $response->getBody()->getContents(), + $response->getStatusCode() + ); + } + + return new FulfilledPromise($response->getBody()->getContents()); + } + + /** + * {@inheritDoc} + */ + public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string + { + try { + return $this->fetchMetadata($fileName, $maxBytes)->wait(); + } catch (RepoFileNotFound $exception) { + return null; + } + } +} diff --git a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php new file mode 100644 index 00000000000..b653d78275e --- /dev/null +++ b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php @@ -0,0 +1,227 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\TUF; + +use Joomla\Http\Http; +use Psr\Http\Message\StreamInterface; +use Joomla\CMS\TUF\HttpFileFetcher; +use Tuf\Exception\RepoFileNotFound; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * @coversDefaultClass \Joomla\CMS\TUF\HttpFileFetcher + */ +class HttpFileFetcherTest extends UnitTestCase +{ + /** + * The content of the mocked response(s). + * + * This is deliberately not readable by json_decode(), in order to prove + * that the fetcher does not try to parse or process the response content + * in any way. + * + * @var string + */ + private $testContent = 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.'; + + + /** + * Returns an instance of the file fetcher under test. + * + * @return HttpFileFetcher + * An instance of the file fetcher under test. + */ + private function getFetcher($clientMock): HttpFileFetcher + { + return new HttpFileFetcher($clientMock, '/metadata/', '/targets/'); + } + + /** + * Data provider for testfetchFileError(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerFetchFileError(): array + { + return [ + [404, RepoFileNotFound::class, 0], + [403, 'RuntimeException'] + ]; + } + + /** + * Data provider for testFetchFileIfExistsError(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerFileIfExistsError(): array + { + return [ + [403, 'RuntimeException'] + ]; + } + + /** + * Tests various error conditions when fetching a file with fetchFile(). + * + * @param integer $statusCode + * The response status code. + * @param string $exceptionClass + * The expected exception class that will be thrown. + * @param integer|null $exceptionCode + * (optional) The expected exception code. Defaults to the status code. + * @param integer|null $maxBytes + * (optional) The maximum number of bytes to read from the response. + * Defaults to the length of $this->testContent. + * + * @return void + * + * @dataProvider providerFetchFileError + * + * @covers ::fetchFile + */ + public function testFetchFileError( + int $statusCode, + string $exceptionClass, + ?int $exceptionCode = null, + ?int $maxBytes = null + ): void { + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn($statusCode); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->expectException($exceptionClass); + $this->expectExceptionCode($exceptionCode ?? $statusCode); + $this->getFetcher($clientMock) + ->fetchMetadata('test.json', $maxBytes ?? strlen($this->testContent)) + ->wait(); + } + + /** + * Tests various error conditions when fetching a file with fetchFileIfExists(). + * + * @param integer $statusCode + * The response status code. + * @param string $exceptionClass + * The expected exception class that will be thrown. + * @param integer|null $exceptionCode + * (optional) The expected exception code. Defaults to the status code. + * @param integer|null $maxBytes + * (optional) The maximum number of bytes to read from the response. + * Defaults to the length of $this->testContent. + * + * @return void + * + * @dataProvider providerFileIfExistsError + * + * @covers ::providerFileIfExists + */ + public function testFetchFileIfExistsError( + int $statusCode, + string $exceptionClass, + ?int $exceptionCode = null, + ?int $maxBytes = null + ): void { + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn($statusCode); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->expectException($exceptionClass); + $this->expectExceptionCode($exceptionCode ?? $statusCode); + $this->getFetcher($clientMock) + ->fetchMetadataIfExists('test.json', $maxBytes ?? strlen($this->testContent)); + } + + /** + * Tests fetching a file without any errors. + * + * @return void + */ + public function testFetchMetadataReturnsCorrectResponseOnSuccessfulFetch(): void + { + $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); + $clientBodyMock->method('getContents')->willReturn($this->testContent); + + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn(200); + $clientResponseMock->method('getBody')->willReturn($clientBodyMock); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->assertSame( + $this->testContent, + $this->getFetcher($clientMock)->fetchMetadata('test.json', 256)->wait() + ); + } + + /** + * Tests fetching a file without any errors. + * + * @return void + */ + public function testFetchMetadataIfExistsReturnsCorrectResponseOnSuccessfulFetch(): void + { + $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); + $clientBodyMock->method('getContents')->willReturn($this->testContent); + + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn(200); + $clientResponseMock->method('getBody')->willReturn($clientBodyMock); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->assertSame( + $this->testContent, + $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) + ); + } + + /** + * Tests fetching a file without any errors. + * + * @return void + */ + public function testFetchMetadataIfExistsReturnsCorrectResponseOnNotFoundFetch(): void + { + $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); + $clientBodyMock->method('getContents')->willReturn($this->testContent); + + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn(404); + $clientResponseMock->method('getBody')->willReturn($clientBodyMock); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->assertNull( + $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) + ); + } + + /** + * Tests creating a file fetcher with a repo base URI. + * + * @return void + * + * @covers ::createFromUri + */ + public function testCreateFromUri(): void + { + $this->assertInstanceOf(HttpFileFetcher::class, HttpFileFetcher::createFromUri('https://example.com')); + } +} From 2442045b11c5db4620237ccb02a66fdcb914a9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sun, 12 Jun 2022 12:30:02 +0200 Subject: [PATCH 031/126] change stabilityTags to stability --- libraries/src/Updater/ConstraintChecker.php | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index 70db5ffaf25..92664fae510 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -59,7 +59,7 @@ public function check(array $constraints) } // Check stability - if (isset($constraints['tags']) && !$this->checkStability($constraints['tags'])) + if (isset($constraints['stability']) && !$this->checkStability($constraints['stability'])) { return false; } @@ -147,29 +147,24 @@ protected function checkSupportedDatabases(\stdClass $supportedDatabases) /** * Check the stability * - * @param array $stabilityTags Stability tags to check + * @param string $stability Stability to check * * @return bool * * @since __DEPLOY_VERSION__ */ - protected function checkStability(array $stabilityTags) + protected function checkStability(string $stability) { $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); - $stabilityMatch = false; + $stabilityInt = $this->stabilityToInteger($stability); - foreach ($stabilityTags as $tag) + if (($stabilityInt < $minimumStability)) { - $stability = $this->stabilityTagToInteger($tag); - - if (($stability >= $minimumStability)) - { - $stabilityMatch = true; - } + return false; } - return $stabilityMatch; + return true; } /** @@ -182,7 +177,7 @@ protected function checkStability(array $stabilityTags) * * @since __DEPLOY_VERSION__ */ - protected function stabilityTagToInteger($tag) + protected function stabilityToInteger($tag) { $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); From 0bed04ff08fa3da4278f0e6be4a279767cb9661c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sun, 12 Jun 2022 12:30:20 +0200 Subject: [PATCH 032/126] wip tuf --- libraries/src/TUF/TufValidation.php | 2 - libraries/src/Updater/Adapter/TufAdapter.php | 53 +++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 978ce99c949..89dfdc5d00f 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -112,8 +112,6 @@ public function getValidUpdate(): mixed { $db = Factory::getContainer()->get(DatabaseDriver::class); - // $db = Factory::getDbo(); - $fileFetcher = HttpFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); $storage = new DatabaseStorage($db, $this->extensionId); diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index b57629f721c..8ddd45d0459 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -2,7 +2,7 @@ /** * Joomla! Content Management System * - * @copyright (C) 2008 Open Source Matters, Inc. + * @copyright (C) 2022 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ @@ -16,6 +16,7 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\Table\Table; use Joomla\CMS\Updater\UpdateAdapter; +use Joomla\CMS\Updater\ConstraintChecker; use Joomla\CMS\Updater\Updater; use Joomla\CMS\Version; use Joomla\CMS\TUF\TufValidation; @@ -23,9 +24,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Extension class for updater + * TUF Update Adapter Class * - * @since 1.7.0 + * @since __DEPLOY_VERSION__ */ class TufAdapter extends UpdateAdapter { @@ -44,7 +45,7 @@ class TufAdapter extends UpdateAdapter * * @return array|boolean Array containing the array of update sites and array of updates. False on failure * - * @since 1.7.0 + * @since __DEPLOY_VERSION__ */ public function findUpdate($options) { @@ -64,6 +65,15 @@ public function findUpdate($options) return array('update_sites' => array(), '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 = array(); @@ -148,13 +158,21 @@ public function getUpdateTargets($options) } usort($versions, function ($a, $b) { - return version_compare($a['version'], $b['version']); + return version_compare($b['version'], $a['version']); }); - // TODO ConstraintsCheck + $checker = new ConstraintChecker; + + foreach ($versions as $version) + { + if ($checker->check((array) $version)) + { + return array($version); + } + } } - return $versions; + return false; } /** @@ -168,20 +186,20 @@ protected function configureUpdateOptions(OptionsResolver $resolver) { $resolver->setDefaults( [ - 'version' => "1", 'name' => null, - 'client' => 1, - 'description' => '', + 'description' => '', 'element' => '', + 'type' => null, + 'client' => 1, + 'version' => "1", + 'data' => '', 'detailsurl' => '', - 'data' => '', 'infourl' => '', - 'type' => null, - 'tags' => new \StdClass, + 'downloads' => [], 'targetplatform' => new \StdClass, + 'php_minimum' => null, 'supported_databases' => new \StdClass, - 'downloads' => [], - 'php_minimum' => null + 'stability' => null ] ) ->setAllowedTypes('version', 'string') @@ -193,12 +211,11 @@ protected function configureUpdateOptions(OptionsResolver $resolver) ->setAllowedTypes('detailsurl', 'string') ->setAllowedTypes('infourl', 'string') ->setAllowedTypes('client', 'int') - ->setAllowedTypes('php_minimum', 'string') ->setAllowedTypes('downloads', 'array') - ->setAllowedTypes('tags', 'object') ->setAllowedTypes('targetplatform', 'object') + ->setAllowedTypes('php_minimum', 'string') ->setAllowedTypes('supported_databases', 'object') - ->setAllowedTypes('targetplatform', 'object') + ->setAllowedTypes('stability', 'string') ->setRequired(['version']); } } From 393f7ed278cee694a581765a925ed6e3c387db79 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 12:54:47 +0200 Subject: [PATCH 033/126] cs fixes, tweaks --- libraries/src/TUF/HttpFileFetcher.php | 89 +++++++++++-------- .../Libraries/Cms/TUF/HttpFileFetcherTest.php | 7 +- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php index 481a4f6ac38..baf3f8401bc 100644 --- a/libraries/src/TUF/HttpFileFetcher.php +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -1,4 +1,10 @@ + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ namespace Joomla\CMS\TUF; @@ -12,7 +18,7 @@ use Tuf\Exception\RepoFileNotFound; /** - * Defines a file fetcher that uses joomla/http to read a file over HTTPS. + * @since __DEPLOY_VERSION__ */ class HttpFileFetcher implements RepoFileFetcherInterface { @@ -23,6 +29,13 @@ class HttpFileFetcher implements RepoFileFetcherInterface */ private $client; + /** + * The base URI for requests + * + * @var string|null + */ + private $baseUri; + /** * The path prefix for metadata. * @@ -39,32 +52,27 @@ class HttpFileFetcher implements RepoFileFetcherInterface /** * JHttpFileFetcher constructor. - * @param \Joomla\Http\Http $client - * The HTTP client. - * @param string $metadataPrefix - * The path prefix for metadata. - * @param string $targetsPrefix - * The path prefix for targets. + * + * @param \Joomla\Http\Http $client The HTTP client. + * @param string $metadataPrefix The path prefix for metadata. + * @param string $targetsPrefix The path prefix for targets. */ - public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix) + public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, $baseUri) { $this->client = $client; $this->metadataPrefix = $metadataPrefix; $this->targetsPrefix = $targetsPrefix; + $this->baseUri = $baseUri; } /** * Creates an instance of this class with a specific base URI. * - * @param string $baseUri - * The base URI from which to fetch files. - * @param string $metadataPrefix - * (optional) The path prefix for metadata. Defaults to '/metadata/'. - * @param string $targetsPrefix - * (optional) The path prefix for targets. Defaults to '/targets/'. + * @param string $baseUri The base URI from which to fetch files. + * @param string $metadataPrefix (optional) The path prefix for metadata. Defaults to '/metadata/'. + * @param string $targetsPrefix (optional) The path prefix for targets. Defaults to '/targets/'. * - * @return static - * A new instance of this class. + * @return static A new instance of this class. */ public static function createFromUri( string $baseUri, @@ -74,11 +82,16 @@ public static function createFromUri( $httpFactory = new HttpFactory(); $client = $httpFactory->getHttp([], 'curl'); - return new static($client, $metadataPrefix, $targetsPrefix); + return new static($client, $metadataPrefix, $targetsPrefix, $baseUri); } /** - * {@inheritDoc} + * Fetches a metadata file from the remote repo. + * + * @param string $fileName The name of the metadata file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * + * @return \GuzzleHttp\Promise\PromiseInterface A promise wrapping a StreamInterface instanfe */ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface { @@ -86,14 +99,12 @@ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface } /** - * {@inheritDoc} + * Fetches a target file from the remote repo. + * + * @param array $options (optional) Additional request options to pass to the http client + * @param string $url An arbitrary URL from which the target should be downloaded. * - * @param array $options - * (optional) Additional request options to pass to the Guzzle client. - * See \GuzzleHttp\RequestOptions. - * @param string $url - * (optional) An arbitrary URL from which the target should be downloaded. - * If passed, takes precedence over $fileName. + * @return PromiseInterface */ public function fetchTarget( string $fileName, @@ -108,18 +119,13 @@ public function fetchTarget( /** * Fetches a file from a URL. * - * @param string $url - * The URL of the file to fetch. - * @param integer $maxBytes - * The maximum number of bytes to download. - * @param array $options - * (optional) Additional request options to pass to the Guzzle client. - * See \GuzzleHttp\RequestOptions. + * @param string $url The URL of the file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * @param array $options Additional request options to pass to the http client * - * @return \Psr\Http\Message\StreamInterface - * A promise representing the eventual result of the operation. + * @return PromiseInterface A promise representing the eventual result of the operation. */ - protected function fetchFile(string $url, int $maxBytes, array $headers = []): PromiseInterface + protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface { // Create a progress callback to abort the download if it exceeds // $maxBytes. This will only work with cURL, so we also verify the @@ -130,8 +136,10 @@ protected function fetchFile(string $url, int $maxBytes, array $headers = []): P } }; + $headers = (!empty($options['headers'])) ? $options['headers'] : []; + /** @var Response $response */ - $response = $this->client->get($url, $headers); + $response = $this->client->get($this->baseUri . $url, $headers); $response->getBody()->rewind(); if ($response->getStatusCode() === 404) { @@ -145,11 +153,16 @@ protected function fetchFile(string $url, int $maxBytes, array $headers = []): P ); } - return new FulfilledPromise($response->getBody()->getContents()); + return new FulfilledPromise($response->getBody()); } /** - * {@inheritDoc} + * Gets a file if it exists in the remote repo. + * + * @param string $fileName The file name to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * + * @return string|null The contents of the file or null if it does not exist. */ public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string { diff --git a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php index b653d78275e..79eb26b65ef 100644 --- a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php +++ b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php @@ -40,7 +40,7 @@ class HttpFileFetcherTest extends UnitTestCase */ private function getFetcher($clientMock): HttpFileFetcher { - return new HttpFileFetcher($clientMock, '/metadata/', '/targets/'); + return new HttpFileFetcher($clientMock, '/metadata/', '/targets/', ""); } /** @@ -164,7 +164,7 @@ public function testFetchMetadataReturnsCorrectResponseOnSuccessfulFetch(): void $this->assertSame( $this->testContent, - $this->getFetcher($clientMock)->fetchMetadata('test.json', 256)->wait() + $this->getFetcher($clientMock)->fetchMetadata('test.json', 256)->wait()->getContents() ); } @@ -176,7 +176,8 @@ public function testFetchMetadataReturnsCorrectResponseOnSuccessfulFetch(): void public function testFetchMetadataIfExistsReturnsCorrectResponseOnSuccessfulFetch(): void { $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('getContents')->willReturn($this->testContent); + $clientBodyMock->method('rewind')->willReturnSelf(); + $clientBodyMock->method('__toString')->willReturn($this->testContent); $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); $clientResponseMock->method('getStatusCode')->willReturn(200); From 3c20851e7508f5344566d9fbe84c81ec9fdb3a8f Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 12:59:57 +0200 Subject: [PATCH 034/126] Apply suggestions from code review Co-authored-by: Tobias Zulauf --- libraries/src/TUF/HttpFileFetcher.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php index baf3f8401bc..0ec4b99fcd4 100644 --- a/libraries/src/TUF/HttpFileFetcher.php +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -26,6 +26,8 @@ class HttpFileFetcher implements RepoFileFetcherInterface * The HTTP client. * * @var \Joomla\Http\Http + * + * @since __DEPLOY_VERSION__ */ private $client; @@ -40,6 +42,8 @@ class HttpFileFetcher implements RepoFileFetcherInterface * The path prefix for metadata. * * @var string|null + * + * @since __DEPLOY_VERSION__ */ private $metadataPrefix; @@ -47,6 +51,8 @@ class HttpFileFetcher implements RepoFileFetcherInterface * The path prefix for targets. * * @var string|null + * + * @since __DEPLOY_VERSION__ */ private $targetsPrefix; @@ -56,6 +62,8 @@ class HttpFileFetcher implements RepoFileFetcherInterface * @param \Joomla\Http\Http $client The HTTP client. * @param string $metadataPrefix The path prefix for metadata. * @param string $targetsPrefix The path prefix for targets. + * + * @since __DEPLOY_VERSION__ */ public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, $baseUri) { @@ -73,6 +81,8 @@ public function __construct(Http $client, string $metadataPrefix, string $target * @param string $targetsPrefix (optional) The path prefix for targets. Defaults to '/targets/'. * * @return static A new instance of this class. + * + * @since __DEPLOY_VERSION__ */ public static function createFromUri( string $baseUri, @@ -105,6 +115,8 @@ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface * @param string $url An arbitrary URL from which the target should be downloaded. * * @return PromiseInterface + * + * @since __DEPLOY_VERSION__ */ public function fetchTarget( string $fileName, @@ -124,6 +136,8 @@ public function fetchTarget( * @param array $options Additional request options to pass to the http client * * @return PromiseInterface A promise representing the eventual result of the operation. + * + * @since __DEPLOY_VERSION__ */ protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface { From 9805efa8b85183a4db656a57d4f00ee53b06b509 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:00:22 +0200 Subject: [PATCH 035/126] cs fixes --- libraries/src/TUF/HttpFileFetcher.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php index 0ec4b99fcd4..1db794e03e1 100644 --- a/libraries/src/TUF/HttpFileFetcher.php +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -35,6 +35,8 @@ class HttpFileFetcher implements RepoFileFetcherInterface * The base URI for requests * * @var string|null + * + * @since __DEPLOY_VERSION__ */ private $baseUri; @@ -145,7 +147,8 @@ protected function fetchFile(string $url, int $maxBytes, array $options = []): P // $maxBytes. This will only work with cURL, so we also verify the // download size when request is finished. $progress = function (int $expectedBytes, int $downloadedBytes) use ($url, $maxBytes) { - if ($expectedBytes > $maxBytes || $downloadedBytes > $maxBytes) { + if ($expectedBytes > $maxBytes || $downloadedBytes > $maxBytes) + { throw new DownloadSizeException("$url exceeded $maxBytes bytes"); } }; @@ -156,11 +159,13 @@ protected function fetchFile(string $url, int $maxBytes, array $options = []): P $response = $this->client->get($this->baseUri . $url, $headers); $response->getBody()->rewind(); - if ($response->getStatusCode() === 404) { + if ($response->getStatusCode() === 404) + { throw new RepoFileNotFound(); } - if ($response->getStatusCode() !== 200) { + if ($response->getStatusCode() !== 200) + { throw new \RuntimeException( "Invalid TUF repo response: " . $response->getBody()->getContents(), $response->getStatusCode() @@ -180,9 +185,12 @@ protected function fetchFile(string $url, int $maxBytes, array $options = []): P */ public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string { - try { + try + { return $this->fetchMetadata($fileName, $maxBytes)->wait(); - } catch (RepoFileNotFound $exception) { + } + catch (RepoFileNotFound $exception) + { return null; } } From a4e1e21b054b197b48a8aed57843d0b42a994445 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:04:09 +0200 Subject: [PATCH 036/126] Update composer.json Co-authored-by: Tobias Zulauf --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7bab3394d6..9c8e5fac3b3 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ { "type": "vcs", "url": "https://github.com/joomla-projects/php-tuf.git", - "no-api": true + "no-api": true } ], "autoload": { From 311051b8390bb4e0ebab9d21c1a61aa3f84b97f8 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:05:21 +0200 Subject: [PATCH 037/126] Update composer.json Co-authored-by: Tobias Zulauf --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9c8e5fac3b3..5d7188fd957 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ { "type": "vcs", "url": "https://github.com/joomla-backports/json-api-php.git", - "no-api": true + "no-api": true }, { "type": "vcs", From c594935bdf4f3ec22fb51856fe5d8e23076c4d17 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:06:51 +0200 Subject: [PATCH 038/126] Update tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php Co-authored-by: Tobias Zulauf --- tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php index 79eb26b65ef..5ae27351da9 100644 --- a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php +++ b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php @@ -3,7 +3,7 @@ * @package Joomla.UnitTest * @subpackage Version * - * @copyright (C) 2019 Open Source Matters, Inc. + * @copyright (C) 2022 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ From b749a32979ea099d5ae818cb43ed7a5cbce836ec Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:06:56 +0200 Subject: [PATCH 039/126] Update tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php Co-authored-by: Tobias Zulauf --- tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php index 5ae27351da9..4f80d9b3842 100644 --- a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php +++ b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php @@ -1,7 +1,7 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt From 3e65cc21964e728ef1ee97e8d6b0eabcdecf0b5d Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:08:35 +0200 Subject: [PATCH 040/126] Apply suggestions from code review Co-authored-by: Tobias Zulauf --- tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php index 4f80d9b3842..88e464b9e14 100644 --- a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php +++ b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php @@ -1,10 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Tests\Unit\Libraries\Cms\TUF; @@ -31,7 +31,6 @@ class HttpFileFetcherTest extends UnitTestCase */ private $testContent = 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.'; - /** * Returns an instance of the file fetcher under test. * @@ -223,6 +222,9 @@ public function testFetchMetadataIfExistsReturnsCorrectResponseOnNotFoundFetch() */ public function testCreateFromUri(): void { - $this->assertInstanceOf(HttpFileFetcher::class, HttpFileFetcher::createFromUri('https://example.com')); + $this->assertInstanceOf( + HttpFileFetcher::class, + HttpFileFetcher::createFromUri('https://example.com') + ); } } From b0c009757332188e6182fe007677141fcae25e5a Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:13:12 +0200 Subject: [PATCH 041/126] cs fixes --- libraries/src/TUF/HttpFileFetcher.php | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php index 1db794e03e1..f128aacfb7f 100644 --- a/libraries/src/TUF/HttpFileFetcher.php +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -27,7 +27,7 @@ class HttpFileFetcher implements RepoFileFetcherInterface * * @var \Joomla\Http\Http * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ private $client; @@ -45,7 +45,7 @@ class HttpFileFetcher implements RepoFileFetcherInterface * * @var string|null * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ private $metadataPrefix; @@ -54,7 +54,7 @@ class HttpFileFetcher implements RepoFileFetcherInterface * * @var string|null * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ private $targetsPrefix; @@ -65,7 +65,7 @@ class HttpFileFetcher implements RepoFileFetcherInterface * @param string $metadataPrefix The path prefix for metadata. * @param string $targetsPrefix The path prefix for targets. * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, $baseUri) { @@ -84,7 +84,7 @@ public function __construct(Http $client, string $metadataPrefix, string $target * * @return static A new instance of this class. * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ public static function createFromUri( string $baseUri, @@ -118,7 +118,7 @@ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface * * @return PromiseInterface * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ public function fetchTarget( string $fileName, @@ -139,20 +139,10 @@ public function fetchTarget( * * @return PromiseInterface A promise representing the eventual result of the operation. * - * @since __DEPLOY_VERSION__ + * @since __DEPLOY_VERSION__ */ protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface { - // Create a progress callback to abort the download if it exceeds - // $maxBytes. This will only work with cURL, so we also verify the - // download size when request is finished. - $progress = function (int $expectedBytes, int $downloadedBytes) use ($url, $maxBytes) { - if ($expectedBytes > $maxBytes || $downloadedBytes > $maxBytes) - { - throw new DownloadSizeException("$url exceeded $maxBytes bytes"); - } - }; - $headers = (!empty($options['headers'])) ? $options['headers'] : []; /** @var Response $response */ From 48f0b34d7de4c2d7248d4e10e53f8bfafd66e719 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:21:54 +0200 Subject: [PATCH 042/126] cs fixes --- libraries/src/TUF/HttpFileFetcher.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php index f128aacfb7f..1a428f9f1a5 100644 --- a/libraries/src/TUF/HttpFileFetcher.php +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -64,10 +64,11 @@ class HttpFileFetcher implements RepoFileFetcherInterface * @param \Joomla\Http\Http $client The HTTP client. * @param string $metadataPrefix The path prefix for metadata. * @param string $targetsPrefix The path prefix for targets. + * @param string $baseUri Repo base uri for requests * * @since __DEPLOY_VERSION__ */ - public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, $baseUri) + public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, string $baseUri) { $this->client = $client; $this->metadataPrefix = $metadataPrefix; @@ -91,7 +92,7 @@ public static function createFromUri( string $metadataPrefix = '/metadata/', string $targetsPrefix = '/targets/' ): self { - $httpFactory = new HttpFactory(); + $httpFactory = new HttpFactory; $client = $httpFactory->getHttp([], 'curl'); return new static($client, $metadataPrefix, $targetsPrefix, $baseUri); @@ -100,8 +101,8 @@ public static function createFromUri( /** * Fetches a metadata file from the remote repo. * - * @param string $fileName The name of the metadata file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. + * @param string $fileName The name of the metadata file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. * * @return \GuzzleHttp\Promise\PromiseInterface A promise wrapping a StreamInterface instanfe */ @@ -113,8 +114,10 @@ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface /** * Fetches a target file from the remote repo. * - * @param array $options (optional) Additional request options to pass to the http client - * @param string $url An arbitrary URL from which the target should be downloaded. + * @param string $fileName The name of the target to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * @param array $options (optional) Additional request options to pass to the http client + * @param string $url An arbitrary URL from which the target should be downloaded. * * @return PromiseInterface * @@ -127,6 +130,7 @@ public function fetchTarget( string $url = null ): PromiseInterface { $location = $url ?: $this->targetsPrefix . $fileName; + return $this->fetchFile($location, $maxBytes, $options); } @@ -151,7 +155,7 @@ protected function fetchFile(string $url, int $maxBytes, array $options = []): P if ($response->getStatusCode() === 404) { - throw new RepoFileNotFound(); + throw new RepoFileNotFound; } if ($response->getStatusCode() !== 200) From cd1eb5f3ce0036eff89c6a697044b01e10255373 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 12 Jun 2022 13:38:06 +0200 Subject: [PATCH 043/126] fix baseuri usage --- libraries/src/TUF/HttpFileFetcher.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php index 1a428f9f1a5..f325b787744 100644 --- a/libraries/src/TUF/HttpFileFetcher.php +++ b/libraries/src/TUF/HttpFileFetcher.php @@ -108,7 +108,7 @@ public static function createFromUri( */ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface { - return $this->fetchFile($this->metadataPrefix . $fileName, $maxBytes); + return $this->fetchFile($this->baseUri . $this->metadataPrefix . $fileName, $maxBytes); } /** @@ -129,7 +129,7 @@ public function fetchTarget( array $options = [], string $url = null ): PromiseInterface { - $location = $url ?: $this->targetsPrefix . $fileName; + $location = $url ?: $this->baseUri . $this->targetsPrefix . $fileName; return $this->fetchFile($location, $maxBytes, $options); } @@ -150,7 +150,7 @@ protected function fetchFile(string $url, int $maxBytes, array $options = []): P $headers = (!empty($options['headers'])) ? $options['headers'] : []; /** @var Response $response */ - $response = $this->client->get($this->baseUri . $url, $headers); + $response = $this->client->get($url, $headers); $response->getBody()->rewind(); if ($response->getStatusCode() === 404) From 271c5446472f2f006f41f38eccb8bb783c87e132 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Sun, 12 Jun 2022 13:50:03 +0200 Subject: [PATCH 044/126] remove unused import --- administrator/components/com_installer/src/Model/UpdateModel.php | 1 - 1 file changed, 1 deletion(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index deebdf75a1b..405b1255bbc 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -28,7 +28,6 @@ use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\ParameterType; use Joomla\Utilities\ArrayHelper; -use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; /** * Installer Update Model From b28fc9319667129db76d04e24a74cb62f76b8521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sun, 12 Jun 2022 14:15:45 +0200 Subject: [PATCH 045/126] implement HttpFileFetcher --- composer.lock | 8 ++++---- libraries/src/TUF/TufValidation.php | 2 +- libraries/src/Updater/Adapter/TufAdapter.php | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index f5518f1f0ab..8f0517ac845 100644 --- a/composer.lock +++ b/composer.lock @@ -2966,12 +2966,12 @@ "source": { "type": "git", "url": "https://github.com/joomla-projects/php-tuf.git", - "reference": "2d71b4039686196adab273c59d1fd4ea4468e511" + "reference": "9f4725a4a95ee1c2833140144c3b681778aa0bde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-projects/php-tuf/zipball/2d71b4039686196adab273c59d1fd4ea4468e511", - "reference": "2d71b4039686196adab273c59d1fd4ea4468e511", + "url": "https://api.github.com/repos/joomla-projects/php-tuf/zipball/9f4725a4a95ee1c2833140144c3b681778aa0bde", + "reference": "9f4725a4a95ee1c2833140144c3b681778aa0bde", "shasum": "" }, "require": { @@ -3021,7 +3021,7 @@ "MIT" ], "description": "PHP implementation of The Update Framework (TUF)", - "time": "2022-06-12T09:53:53+00:00" + "time": "2022-06-12T12:05:25+00:00" }, { "name": "phpmailer/phpmailer", diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 42ff8494980..4c2203df78b 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -15,7 +15,7 @@ use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\OptionsResolver; use Tuf\Client\GuzzleFileFetcher; -use Tuf\Client\HttpFileFetcher; +use Joomla\CMS\TUF\HttpFileFetcher; use Tuf\Client\Updater; use Tuf\Exception\Attack\FreezeAttackException; use Tuf\Exception\Attack\RollbackAttackException; diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 8ddd45d0459..9500d1d9060 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -133,7 +133,6 @@ public function getUpdateTargets($options) } } - if (isset($values['client']) && is_string($values['client']) && key_exists(strtolower($values['client']), $this->clientId)) { @@ -199,7 +198,7 @@ protected function configureUpdateOptions(OptionsResolver $resolver) 'targetplatform' => new \StdClass, 'php_minimum' => null, 'supported_databases' => new \StdClass, - 'stability' => null + 'stability' => '' ] ) ->setAllowedTypes('version', 'string') From ff330d3a68bc44dae4603b20806bda60a42e52e0 Mon Sep 17 00:00:00 2001 From: Tobias Zulauf Date: Sun, 12 Jun 2022 14:19:43 +0200 Subject: [PATCH 046/126] Update TufAdapter.php --- libraries/src/Updater/Adapter/TufAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 9500d1d9060..5e5ebb4942a 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -3,7 +3,7 @@ * Joomla! Content Management System * * @copyright (C) 2022 Open Source Matters, Inc. - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Updater\Adapter; From a4d8b4782468ca667ba77c641045f3e0023af27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sun, 12 Jun 2022 14:24:47 +0200 Subject: [PATCH 047/126] remove typehint for paramater to support php 7.2.5 --- libraries/src/TUF/DatabaseStorage.php | 10 +++++----- libraries/src/TUF/TufValidation.php | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index d60cef604b5..926dc8be9f7 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -25,7 +25,7 @@ class DatabaseStorage implements \ArrayAccess * * @var Table */ - protected Table $table; + protected $table; /** * Initialize the DatabaseStorage class @@ -47,7 +47,7 @@ public function __construct(DatabaseDriver $db, int $extensionId) * * @return boolean */ - public function offsetExists(mixed $offset): bool + public function offsetExists($offset): bool { $column = $this->getCleanColumn($offset); @@ -61,7 +61,7 @@ public function offsetExists(mixed $offset): bool * * @return boolean */ - public function tableColumnExists(mixed $offset): bool + public function tableColumnExists($offset): bool { $column = $this->getCleanColumn($offset); @@ -75,7 +75,7 @@ public function tableColumnExists(mixed $offset): bool * * @return mixed */ - public function offsetGet($offset): mixed + public function offsetGet($offset) { if (!$this->offsetExists($offset)) { @@ -95,7 +95,7 @@ public function offsetGet($offset): mixed * * @return void */ - public function offsetSet($offset, $value): void + public function offsetSet($offset, $value) { if (!$this->tableColumnExists($offset)) { diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 4c2203df78b..4214690f6d4 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -35,7 +35,7 @@ class TufValidation * * @var integer */ - private int $extensionId; + private $extensionId; /** * The params of the validator @@ -50,7 +50,7 @@ class TufValidation * @param integer $extensionId The ID of the extension to be checked * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for the update */ - public function __construct(int $extensionId, mixed $params) + public function __construct(int $extensionId, $params) { $this->extensionId = $extensionId; @@ -107,7 +107,7 @@ protected function configureTufOptions(OptionsResolver $resolver) * * @return mixed Returns the targets.json if the validation is successful, otherwise null */ - public function getValidUpdate(): mixed + public function getValidUpdate() { $db = Factory::getContainer()->get(DatabaseDriver::class); From 258a2901f019dc375aafc617993900c3d4124d50 Mon Sep 17 00:00:00 2001 From: Tobias Zulauf Date: Sun, 12 Jun 2022 14:26:29 +0200 Subject: [PATCH 048/126] Update ConstraintCheckerTest.php --- .../Libraries/Cms/Updater/ConstraintCheckerTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php index 0a2439f3d61..85b32172e86 100644 --- a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -1,10 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Tests\Unit\Libraries\Cms; @@ -18,14 +18,14 @@ * Test class for Version. * * @package Joomla.UnitTest - * @subpackage Version + * @subpackage Updater * @since __DEPLOY_VERSION__ */ class ConstraintCheckerTest extends UnitTestCase { /** * @var ConstraintChecker - * @since 3.0 + * @since __DEPLOY_VERSION__ */ protected $checker; From cf6c1a38dd6e1160635e005ae04788e0a8022276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sun, 12 Jun 2022 14:27:26 +0200 Subject: [PATCH 049/126] remove whitespace --- libraries/src/TUF/DatabaseStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 926dc8be9f7..27a11d6cdeb 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -25,7 +25,7 @@ class DatabaseStorage implements \ArrayAccess * * @var Table */ - protected $table; + protected $table; /** * Initialize the DatabaseStorage class From c5c8f422b8de178166ebd4dc51ada0ef0a2f56ce Mon Sep 17 00:00:00 2001 From: Tobias Zulauf Date: Sun, 12 Jun 2022 14:28:34 +0200 Subject: [PATCH 050/126] Update ConstraintChecker.php --- libraries/src/Updater/ConstraintChecker.php | 54 ++++++++++----------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index 92664fae510..8a2a1f438c2 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -28,7 +28,7 @@ class ConstraintChecker * * @param array $constraints The provided constraints to be checked * - * @return bool + * @return boolean * * @since __DEPLOY_VERSION__ */ @@ -46,19 +46,19 @@ public function check(array $constraints) return false; } - // Check php_minimum + // Check php_minimumm, assume true when not set if (isset($constraints['php_minimum']) && !$this->checkPhpMinimum($constraints['php_minimum'])) { return false; } - // Check supported databases + // Check supported databases, assume true when not set if (isset($constraints['supported_databases']) && !$this->checkSupportedDatabases($constraints['supported_databases'])) { return false; } - // Check stability + // Check stability, assume true when not set if (isset($constraints['stability']) && !$this->checkStability($constraints['stability'])) { return false; @@ -72,7 +72,7 @@ public function check(array $constraints) * * @param object $targetPlatform * - * @return bool + * @return boolean * * @since __DEPLOY_VERSION__ */ @@ -96,7 +96,7 @@ protected function checkTargetplatform(\stdClass $targetPlatform) * * @param string $phpMinimum The minimum php version to check * - * @return bool + * @return boolean * * @since __DEPLOY_VERSION__ */ @@ -111,37 +111,37 @@ protected function checkPhpMinimum(string $phpMinimum) * * @param object $supportedDatabases stdClass of supported databases and versions * - * @return bool + * @return boolean * * @since __DEPLOY_VERSION__ */ protected function checkSupportedDatabases(\stdClass $supportedDatabases) { - $db = Factory::getDbo(); - $dbType = strtolower($db->getServerType()); - $dbVersion = $db->getVersion(); + $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') + // 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) { - // 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'; - } + // 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 (\property_exists($supportedDatabases, $dbType)) - { - $minimumVersion = $supportedDatabases->$dbType; + // Do we have an entry for the database? + if (\property_exists($supportedDatabases, $dbType)) + { + $minimumVersion = $supportedDatabases->$dbType; - return version_compare($dbVersion, $minimumVersion, '>='); - } + return version_compare($dbVersion, $minimumVersion, '>='); + } - return false; + return false; } /** @@ -149,7 +149,7 @@ protected function checkSupportedDatabases(\stdClass $supportedDatabases) * * @param string $stability Stability to check * - * @return bool + * @return boolean * * @since __DEPLOY_VERSION__ */ From 5f175afc04d3634842db6d28f2eb72e88bbcc390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20N=C3=BCbel?= Date: Sun, 12 Jun 2022 14:31:56 +0200 Subject: [PATCH 051/126] Apply suggestions from code review Co-authored-by: Tobias Zulauf --- .../com_installer/src/Model/UpdateModel.php | 2 +- libraries/src/Updater/Adapter/TufAdapter.php | 7 +++++++ libraries/src/Updater/ConstraintChecker.php | 11 +++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index df36a454743..da5b2d76149 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -362,7 +362,7 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) } // TODO Load Type based on #__updates_site.type - // if type tuf loadFromTuf + // If type tuf loadFromTuf $update->loadFromXml($instance->detailsurl, $minimumStability); // Find and use extra_query from update_site if available diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 5e5ebb4942a..bcc72a653fe 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -30,6 +30,11 @@ */ class TufAdapter extends UpdateAdapter { + /** + * The client ID mapping array + * + * @var array + */ private $clientId = [ 'site' => 0, 'administrator' => 1, @@ -180,6 +185,8 @@ public function getUpdateTargets($options) * @param OptionsResolver $resolver The OptionsResolver for the params * * @return void + * + * @since __DEPLOY_VERSION__ */ protected function configureUpdateOptions(OptionsResolver $resolver) { diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index 8a2a1f438c2..d49fbbeb551 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -47,19 +47,22 @@ public function check(array $constraints) } // Check php_minimumm, assume true when not set - if (isset($constraints['php_minimum']) && !$this->checkPhpMinimum($constraints['php_minimum'])) + if (isset($constraints['php_minimum']) + && !$this->checkPhpMinimum($constraints['php_minimum'])) { return false; } // Check supported databases, assume true when not set - if (isset($constraints['supported_databases']) && !$this->checkSupportedDatabases($constraints['supported_databases'])) + if (isset($constraints['supported_databases']) + && !$this->checkSupportedDatabases($constraints['supported_databases'])) { return false; } // Check stability, assume true when not set - if (isset($constraints['stability']) && !$this->checkStability($constraints['stability'])) + if (isset($constraints['stability']) + && !$this->checkStability($constraints['stability'])) { return false; } @@ -102,7 +105,7 @@ protected function checkTargetplatform(\stdClass $targetPlatform) */ protected function checkPhpMinimum(string $phpMinimum) { - // Check if PHP version supported via tag, assume true if tag isn't present + // Check if PHP version supported via tag return version_compare(PHP_VERSION, $phpMinimum, '>='); } From 52b59d34c6706656c8a9cdafc294bd5a3e432939 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Sat, 17 Sep 2022 17:34:33 +0200 Subject: [PATCH 052/126] TufValidation add quoteName on delete query (#13) --- libraries/src/TUF/TufValidation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 4214690f6d4..1dfc1d0d9ff 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -134,7 +134,7 @@ public function getValidUpdate() // When the validation fails, for example when one file is written but the others don't, we roll back everything // and cancel the update $query = $db->getQuery(true) - ->delete('#__tuf_metadata') + ->delete($db->quoteName('#__tuf_metadata')) ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); $db->setQuery($query); From d6bb842afdcb7a2d738d103d63f965705a3d2726 Mon Sep 17 00:00:00 2001 From: Franciska Perisa <9084265+fancyFranci@users.noreply.github.com> Date: Sat, 17 Sep 2022 19:31:39 +0200 Subject: [PATCH 053/126] Convert to PSR12 code style (#15) --- .../com_installer/src/Model/UpdateModel.php | 1227 ++++++++--------- libraries/src/TUF/DatabaseStorage.php | 240 ++-- .../TUF/Exception/RoleNotFoundException.php | 1 + libraries/src/TUF/HttpFileFetcher.php | 331 +++-- libraries/src/TUF/TufValidation.php | 218 ++- libraries/src/Table/Tuf.php | 23 +- libraries/src/Updater/Adapter/TufAdapter.php | 381 +++-- libraries/src/Updater/ConstraintChecker.php | 334 +++-- .../Libraries/Cms/TUF/HttpFileFetcherTest.php | 415 +++--- .../Cms/Updater/ConstraintCheckerTest.php | 375 ++--- 10 files changed, 1731 insertions(+), 1814 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 405b1255bbc..1916668733d 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $app = Factory::getApplication(); - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - - // Grab updates ignoring new installs - $query = $db->getQuery(true) - ->select('u.*') - ->select($db->quoteName('e.manifest_cache')) - ->from($db->quoteName('#__updates', 'u')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') - ) - ->where($db->quoteName('u.extension_id') . ' != 0'); - - // Process select filters. - $clientId = $this->getState('filter.client_id'); - $type = $this->getState('filter.type'); - $folder = $this->getState('filter.folder'); - $extensionId = $this->getState('filter.extension_id'); - - if ($type) - { - $query->where($db->quoteName('u.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('u.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('u.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - if ($extensionId) - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('u.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - else - { - $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $query->where($db->quoteName('u.extension_id') . ' != 0') - ->where($db->quoteName('u.extension_id') . ' != :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'eid:') !== false) - { - $sid = (int) substr($search, 4); - $query->where($db->quoteName('u.extension_id') . ' = :sid') - ->bind(':sid', $sid, ParameterType::INTEGER); - } - else - { - if (stripos($search, 'uid:') !== false) - { - $suid = (int) substr($search, 4); - $query->where($db->quoteName('u.update_site_id') . ' = :suid') - ->bind(':suid', $suid, ParameterType::INTEGER); - } - elseif (stripos($search, 'id:') !== false) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('u.update_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - } - - return $query; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - * - * @since 3.5 - */ - protected function translate(&$items) - { - foreach ($items as &$item) - { - $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $manifest = json_decode($item->manifest_cache); - $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); - $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); - } - - return $items; - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - * - * @since 3.5 - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $db = $this->getDatabase(); - $listOrder = $this->getState('list.ordering', 'u.name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Process ordering. - if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) - { - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - $total = count($result); - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Get the count of disabled update sites - * - * @return integer - * - * @since 3.4 - */ - public function getDisabledUpdateSites() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__update_sites')) - ->where($db->quoteName('enabled') . ' = 0'); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Finds updates for an extension. - * - * @param int $eid Extension identifier to look for - * @param int $cacheTimeout Cache timeout - * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) - * - * @return boolean Result - * - * @since 1.6 - */ - public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) - { - Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); - - return true; - } - - /** - * Removes all of the updates from the table. - * - * @return boolean result of operation - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDatabase(); - - try - { - $db->truncateTable('#__updates'); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); - - return false; - } - - // Reset the last update check timestamp - $query = $db->getQuery(true) - ->update($db->quoteName('#__update_sites')) - ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); - $db->setQuery($query); - $db->execute(); - - // Clear the administrator cache - $this->cleanCache('_system'); - - $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); - - return true; - } - - /** - * Update function. - * - * Sets the "result" state with the result of the operation. - * - * @param int[] $uids List of updates to apply - * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} - * - * @return void - * - * @since 1.6 - */ - public function update($uids, $minimumStability = Updater::STABILITY_STABLE) - { - $result = true; - - foreach ($uids as $uid) - { - $update = new Update; - $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); - - if (!$instance->load($uid)) - { - // Update no longer available, maybe already updated by a package. - continue; - } - - $app = Factory::getApplication(); - $db = Factory::getContainer()->get(DatabaseDriver::class); - $query = $db->getQuery(true) - ->select('type') - ->from('#__update_sites') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $instance->update_site_id, ParameterType::INTEGER); - $db->setQuery($query); - $updateSiteType = (string) $db->loadObject(); - - if ($updateSiteType == 'tuf') - { - $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error'); - - return; - } - else - { - $update->loadFromXml($instance->detailsurl, $minimumStability); - } - - // Find and use extra_query from update_site if available - $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); - $updateSiteInstance->load($instance->update_site_id); - - if ($updateSiteInstance->extra_query) - { - $update->set('extra_query', $updateSiteInstance->extra_query); - } - - $this->preparePreUpdate($update, $instance); - - // Install sets state and enqueues messages - $res = $this->install($update); - - if ($res) - { - $instance->delete($uid); - } - - $result = $res & $result; - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - // Set the final state - $this->setState('result', $result); - } - - /** - * Handles the actual update installation. - * - * @param Update $update An update definition - * - * @return boolean Result of install - * - * @since 1.6 - */ - private function install($update) - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - if (!isset($update->get('downloadurl')->_data)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); - - return false; - } - - $url = trim($update->downloadurl->_data); - $sources = $update->get('downloadSources', array()); - - if ($extra_query = $update->get('extra_query')) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror = 0; - - while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $url = trim($name->url); - - if ($extra_query) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror++; - } - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); - - return false; - } - - $config = $app->getConfig(); - $tmp_dest = $config->get('tmp_path'); - - // Unpack the downloaded package file - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); - - if (empty($package)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); - - return false; - } - - // Get an installer instance - $installer = Installer::getInstance(); - $update->set('type', $package['type']); - - // Check the package - $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); - - if ($check === InstallerHelper::HASH_NOT_VALIDATED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); - - return false; - } - - if ($check === InstallerHelper::HASH_NOT_PROVIDED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); - } - - // Install the package - if (!$installer->update($package['dir'])) - { - // There was an error updating the package - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_ERROR', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'error' - ); - $result = false; - } - else - { - // Package updated successfully - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_SUCCESS', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'success' - ); - $result = true; - } - - // Quick change - $this->type = $package['type']; - - // @todo: Reconfigure this code when you have more battery life left - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Cleanup the install files - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 2.5.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/models/forms'); - Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); - $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); - - // Check for an error. - if ($form == false) - { - $this->setError($form->getMessage()); - - return false; - } - - // Check the session for previously entered form data. - $data = $this->loadFormData(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5.2 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState($this->context, array()); - - return $data; - } - - /** - * Method to add parameters to the update - * - * @param Update $update An update definition - * @param \Joomla\CMS\Table\Update $table The update instance from the database - * - * @return void - * - * @since 3.7.0 - */ - protected function preparePreUpdate($update, $table) - { - switch ($table->type) - { - // Components could have a helper which adds additional data - case 'component': - $ename = str_replace('com_', '', $table->element); - $fname = $ename . '.php'; - $cname = ucfirst($ename) . 'Helper'; - - $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // Modules could have a helper which adds additional data - case 'module': - $cname = str_replace('_', '', $table->element) . 'Helper'; - $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" - // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin - case 'plugin': - $cname = str_replace('plg_', '', $table->element); - PluginHelper::importPlugin($table->folder, $cname); - break; - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->_db->quoteName('extension_id') . ' != 0'); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', 'u.name', + 'client_id', 'u.client_id', 'client_translated', + 'type', 'u.type', 'type_translated', + 'folder', 'u.folder', 'folder_translated', + 'extension_id', 'u.extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'u.name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $app = Factory::getApplication(); + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Grab updates ignoring new installs + $query = $db->getQuery(true) + ->select('u.*') + ->select($db->quoteName('e.manifest_cache')) + ->from($db->quoteName('#__updates', 'u')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') + ) + ->where($db->quoteName('u.extension_id') . ' != 0'); + + // Process select filters. + $clientId = $this->getState('filter.client_id'); + $type = $this->getState('filter.type'); + $folder = $this->getState('filter.folder'); + $extensionId = $this->getState('filter.extension_id'); + + if ($type) { + $query->where($db->quoteName('u.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('u.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('u.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + if ($extensionId) { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('u.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } else { + $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $query->where($db->quoteName('u.extension_id') . ' != 0') + ->where($db->quoteName('u.extension_id') . ' != :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'eid:') !== false) { + $sid = (int) substr($search, 4); + $query->where($db->quoteName('u.extension_id') . ' = :sid') + ->bind(':sid', $sid, ParameterType::INTEGER); + } else { + if (stripos($search, 'uid:') !== false) { + $suid = (int) substr($search, 4); + $query->where($db->quoteName('u.update_site_id') . ' = :suid') + ->bind(':suid', $suid, ParameterType::INTEGER); + } elseif (stripos($search, 'id:') !== false) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('u.update_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + } + + return $query; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + * + * @since 3.5 + */ + protected function translate(&$items) + { + foreach ($items as &$item) { + $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $manifest = json_decode($item->manifest_cache); + $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); + $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); + } + + return $items; + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + * + * @since 3.5 + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $db = $this->getDatabase(); + $listOrder = $this->getState('list.ordering', 'u.name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Process ordering. + if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + $total = count($result); + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Get the count of disabled update sites + * + * @return integer + * + * @since 3.4 + */ + public function getDisabledUpdateSites() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('enabled') . ' = 0'); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Finds updates for an extension. + * + * @param int $eid Extension identifier to look for + * @param int $cacheTimeout Cache timeout + * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) + * + * @return boolean Result + * + * @since 1.6 + */ + public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) + { + Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); + + return true; + } + + /** + * Removes all of the updates from the table. + * + * @return boolean result of operation + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__updates'); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); + + return false; + } + + // Reset the last update check timestamp + $query = $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); + $db->setQuery($query); + $db->execute(); + + // Clear the administrator cache + $this->cleanCache('_system'); + + $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); + + return true; + } + + /** + * Update function. + * + * Sets the "result" state with the result of the operation. + * + * @param int[] $uids List of updates to apply + * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} + * + * @return void + * + * @since 1.6 + */ + public function update($uids, $minimumStability = Updater::STABILITY_STABLE) + { + $result = true; + + foreach ($uids as $uid) { + $update = new Update; + $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); + + if (!$instance->load($uid)) { + // Update no longer available, maybe already updated by a package. + continue; + } + + $app = Factory::getApplication(); + $db = Factory::getContainer()->get(DatabaseDriver::class); + $query = $db->getQuery(true) + ->select('type') + ->from('#__update_sites') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $instance->update_site_id, ParameterType::INTEGER); + $db->setQuery($query); + $updateSiteType = (string) $db->loadObject(); + + if ($updateSiteType == 'tuf') { + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error'); + + return; + } else { + $update->loadFromXml($instance->detailsurl, $minimumStability); + } + + // Find and use extra_query from update_site if available + $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); + $updateSiteInstance->load($instance->update_site_id); + + if ($updateSiteInstance->extra_query) { + $update->set('extra_query', $updateSiteInstance->extra_query); + } + + $this->preparePreUpdate($update, $instance); + + // Install sets state and enqueues messages + $res = $this->install($update); + + if ($res) { + $instance->delete($uid); + } + + $result = $res & $result; + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + // Set the final state + $this->setState('result', $result); + } + + /** + * Handles the actual update installation. + * + * @param Update $update An update definition + * + * @return boolean Result of install + * + * @since 1.6 + */ + private function install($update) + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + if (!isset($update->get('downloadurl')->_data)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); + + return false; + } + + $url = trim($update->downloadurl->_data); + $sources = $update->get('downloadSources', array()); + + if ($extra_query = $update->get('extra_query')) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror = 0; + + while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $url = trim($name->url); + + if ($extra_query) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror++; + } + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + + return false; + } + + $config = $app->getConfig(); + $tmp_dest = $config->get('tmp_path'); + + // Unpack the downloaded package file + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); + + if (empty($package)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); + + return false; + } + + // Get an installer instance + $installer = Installer::getInstance(); + $update->set('type', $package['type']); + + // Check the package + $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); + + if ($check === InstallerHelper::HASH_NOT_VALIDATED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); + + return false; + } + + if ($check === InstallerHelper::HASH_NOT_PROVIDED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); + } + + // Install the package + if (!$installer->update($package['dir'])) { + // There was an error updating the package + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_ERROR', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'error' + ); + $result = false; + } else { + // Package updated successfully + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_SUCCESS', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'success' + ); + $result = true; + } + + // Quick change + $this->type = $package['type']; + + // @todo: Reconfigure this code when you have more battery life left + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Cleanup the install files + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 2.5.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/models/forms'); + Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); + $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); + + // Check for an error. + if ($form == false) { + $this->setError($form->getMessage()); + + return false; + } + + // Check the session for previously entered form data. + $data = $this->loadFormData(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5.2 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState($this->context, array()); + + return $data; + } + + /** + * Method to add parameters to the update + * + * @param Update $update An update definition + * @param \Joomla\CMS\Table\Update $table The update instance from the database + * + * @return void + * + * @since 3.7.0 + */ + protected function preparePreUpdate($update, $table) + { + switch ($table->type) { + // Components could have a helper which adds additional data + case 'component': + $ename = str_replace('com_', '', $table->element); + $fname = $ename . '.php'; + $cname = ucfirst($ename) . 'Helper'; + + $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // Modules could have a helper which adds additional data + case 'module': + $cname = str_replace('_', '', $table->element) . 'Helper'; + $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" + // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin + case 'plugin': + $cname = str_replace('plg_', '', $table->element); + PluginHelper::importPlugin($table->folder, $cname); + break; + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->_db->quoteName('extension_id') . ' != 0'); + + return $query; + } } diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 27a11d6cdeb..8024a34176b 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -1,4 +1,5 @@ table = new Tuf($db); - - $this->table->load(['extension_id' => $extensionId]); - } - - /** - * Check if an offset/table column exists - * - * @param mixed $offset The offset/database column to check for - * - * @return boolean - */ - public function offsetExists($offset): bool - { - $column = $this->getCleanColumn($offset); - - return substr($column, -5) === '_json' && $this->table->hasField($column) && !is_null($this->table->$column); - } - - /** - * Check if an offset/table column exists - * - * @param mixed $offset The offset/database column to check for - * - * @return boolean - */ - public function tableColumnExists($offset): bool - { - $column = $this->getCleanColumn($offset); - - return substr($column, -5) === '_json' && $this->table->hasField($column); - } - - /** - * Get the value of a table column - * - * @param mixed $offset The column name to get the value for - * - * @return mixed - */ - public function offsetGet($offset) - { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } - - $column = $this->getCleanColumn($offset); - - return $this->table->$column; - } - - /** - * Set a value in a column - * - * @param [type] $offset The table column to set the value - * @param [type] $value The value to set - * - * @return void - */ - public function offsetSet($offset, $value) - { - if (!$this->tableColumnExists($offset)) - { - throw new RoleNotFoundException; - } - - $column = $this->getCleanColumn($offset); - - $this->table->$column = $value; - - $this->table->store(); - } - - /** - * Reset the value to a - * - * @param mixed $offset The table column to reset the value to null - * - * @return void - */ - public function offsetUnset($offset): void - { - if (!$this->offsetExists($offset)) - { - throw new RoleNotFoundException; - } - - $column = $this->getCleanColumn($offset); - - $this->table->$column = null; - - $this->table->store(true); - } - - /** - * Convert file names to table columns - * - * @param string $name The original file name - * - * @return string - */ - protected function getCleanColumn($name): string - { - return str_replace('.', '_', $name); - } + /** + * The Tuf table object + * + * @var Table + */ + protected $table; + + /** + * Initialize the DatabaseStorage class + * + * @param DatabaseDriver $db A database connector object + * @param integer $extensionId The extension ID where the storage should be implemented for + */ + public function __construct(DatabaseDriver $db, int $extensionId) + { + $this->table = new Tuf($db); + + $this->table->load(['extension_id' => $extensionId]); + } + + /** + * Check if an offset/table column exists + * + * @param mixed $offset The offset/database column to check for + * + * @return boolean + */ + public function offsetExists($offset): bool + { + $column = $this->getCleanColumn($offset); + + return substr($column, -5) === '_json' && $this->table->hasField($column) && !is_null($this->table->$column); + } + + /** + * Check if an offset/table column exists + * + * @param mixed $offset The offset/database column to check for + * + * @return boolean + */ + public function tableColumnExists($offset): bool + { + $column = $this->getCleanColumn($offset); + + return substr($column, -5) === '_json' && $this->table->hasField($column); + } + + /** + * Get the value of a table column + * + * @param mixed $offset The column name to get the value for + * + * @return mixed + */ + public function offsetGet($offset) + { + if (!$this->offsetExists($offset)) { + throw new RoleNotFoundException(); + } + + $column = $this->getCleanColumn($offset); + + return $this->table->$column; + } + + /** + * Set a value in a column + * + * @param [type] $offset The table column to set the value + * @param [type] $value The value to set + * + * @return void + */ + public function offsetSet($offset, $value) + { + if (!$this->tableColumnExists($offset)) { + throw new RoleNotFoundException(); + } + + $column = $this->getCleanColumn($offset); + + $this->table->$column = $value; + + $this->table->store(); + } + + /** + * Reset the value to a + * + * @param mixed $offset The table column to reset the value to null + * + * @return void + */ + public function offsetUnset($offset): void + { + if (!$this->offsetExists($offset)) { + throw new RoleNotFoundException(); + } + + $column = $this->getCleanColumn($offset); + + $this->table->$column = null; + + $this->table->store(true); + } + + /** + * Convert file names to table columns + * + * @param string $name The original file name + * + * @return string + */ + protected function getCleanColumn($name): string + { + return str_replace('.', '_', $name); + } } diff --git a/libraries/src/TUF/Exception/RoleNotFoundException.php b/libraries/src/TUF/Exception/RoleNotFoundException.php index fa90a250158..1a279349d61 100644 --- a/libraries/src/TUF/Exception/RoleNotFoundException.php +++ b/libraries/src/TUF/Exception/RoleNotFoundException.php @@ -1,4 +1,5 @@ client = $client; - $this->metadataPrefix = $metadataPrefix; - $this->targetsPrefix = $targetsPrefix; - $this->baseUri = $baseUri; - } - - /** - * Creates an instance of this class with a specific base URI. - * - * @param string $baseUri The base URI from which to fetch files. - * @param string $metadataPrefix (optional) The path prefix for metadata. Defaults to '/metadata/'. - * @param string $targetsPrefix (optional) The path prefix for targets. Defaults to '/targets/'. - * - * @return static A new instance of this class. - * - * @since __DEPLOY_VERSION__ - */ - public static function createFromUri( - string $baseUri, - string $metadataPrefix = '/metadata/', - string $targetsPrefix = '/targets/' - ): self { - $httpFactory = new HttpFactory; - $client = $httpFactory->getHttp([], 'curl'); - - return new static($client, $metadataPrefix, $targetsPrefix, $baseUri); - } - - /** - * Fetches a metadata file from the remote repo. - * - * @param string $fileName The name of the metadata file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * - * @return \GuzzleHttp\Promise\PromiseInterface A promise wrapping a StreamInterface instanfe - */ - public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface - { - return $this->fetchFile($this->baseUri . $this->metadataPrefix . $fileName, $maxBytes); - } - - /** - * Fetches a target file from the remote repo. - * - * @param string $fileName The name of the target to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * @param array $options (optional) Additional request options to pass to the http client - * @param string $url An arbitrary URL from which the target should be downloaded. - * - * @return PromiseInterface - * - * @since __DEPLOY_VERSION__ - */ - public function fetchTarget( - string $fileName, - int $maxBytes, - array $options = [], - string $url = null - ): PromiseInterface { - $location = $url ?: $this->baseUri . $this->targetsPrefix . $fileName; - - return $this->fetchFile($location, $maxBytes, $options); - } - - /** - * Fetches a file from a URL. - * - * @param string $url The URL of the file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * @param array $options Additional request options to pass to the http client - * - * @return PromiseInterface A promise representing the eventual result of the operation. - * - * @since __DEPLOY_VERSION__ - */ - protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface - { - $headers = (!empty($options['headers'])) ? $options['headers'] : []; - - /** @var Response $response */ - $response = $this->client->get($url, $headers); - $response->getBody()->rewind(); - - if ($response->getStatusCode() === 404) - { - throw new RepoFileNotFound; - } - - if ($response->getStatusCode() !== 200) - { - throw new \RuntimeException( - "Invalid TUF repo response: " . $response->getBody()->getContents(), - $response->getStatusCode() - ); - } - - return new FulfilledPromise($response->getBody()); - } - - /** - * Gets a file if it exists in the remote repo. - * - * @param string $fileName The file name to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * - * @return string|null The contents of the file or null if it does not exist. - */ - public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string - { - try - { - return $this->fetchMetadata($fileName, $maxBytes)->wait(); - } - catch (RepoFileNotFound $exception) - { - return null; - } - } + /** + * The HTTP client. + * + * @var \Joomla\Http\Http + * + * @since __DEPLOY_VERSION__ + */ + private $client; + + /** + * The base URI for requests + * + * @var string|null + * + * @since __DEPLOY_VERSION__ + */ + private $baseUri; + + /** + * The path prefix for metadata. + * + * @var string|null + * + * @since __DEPLOY_VERSION__ + */ + private $metadataPrefix; + + /** + * The path prefix for targets. + * + * @var string|null + * + * @since __DEPLOY_VERSION__ + */ + private $targetsPrefix; + + /** + * JHttpFileFetcher constructor. + * + * @param \Joomla\Http\Http $client The HTTP client. + * @param string $metadataPrefix The path prefix for metadata. + * @param string $targetsPrefix The path prefix for targets. + * @param string $baseUri Repo base uri for requests + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, string $baseUri) + { + $this->client = $client; + $this->metadataPrefix = $metadataPrefix; + $this->targetsPrefix = $targetsPrefix; + $this->baseUri = $baseUri; + } + + /** + * Creates an instance of this class with a specific base URI. + * + * @param string $baseUri The base URI from which to fetch files. + * @param string $metadataPrefix (optional) The path prefix for metadata. Defaults to '/metadata/'. + * @param string $targetsPrefix (optional) The path prefix for targets. Defaults to '/targets/'. + * + * @return static A new instance of this class. + * + * @since __DEPLOY_VERSION__ + */ + public static function createFromUri( + string $baseUri, + string $metadataPrefix = '/metadata/', + string $targetsPrefix = '/targets/' + ): self { + $httpFactory = new HttpFactory(); + $client = $httpFactory->getHttp([], 'curl'); + + return new static($client, $metadataPrefix, $targetsPrefix, $baseUri); + } + + /** + * Fetches a metadata file from the remote repo. + * + * @param string $fileName The name of the metadata file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * + * @return \GuzzleHttp\Promise\PromiseInterface A promise wrapping a StreamInterface instanfe + */ + public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface + { + return $this->fetchFile($this->baseUri . $this->metadataPrefix . $fileName, $maxBytes); + } + + /** + * Fetches a target file from the remote repo. + * + * @param string $fileName The name of the target to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * @param array $options (optional) Additional request options to pass to the http client + * @param string $url An arbitrary URL from which the target should be downloaded. + * + * @return PromiseInterface + * + * @since __DEPLOY_VERSION__ + */ + public function fetchTarget( + string $fileName, + int $maxBytes, + array $options = [], + string $url = null + ): PromiseInterface { + $location = $url ?: $this->baseUri . $this->targetsPrefix . $fileName; + + return $this->fetchFile($location, $maxBytes, $options); + } + + /** + * Fetches a file from a URL. + * + * @param string $url The URL of the file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * @param array $options Additional request options to pass to the http client + * + * @return PromiseInterface A promise representing the eventual result of the operation. + * + * @since __DEPLOY_VERSION__ + */ + protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface + { + $headers = (!empty($options['headers'])) ? $options['headers'] : []; + + /** @var Response $response */ + $response = $this->client->get($url, $headers); + $response->getBody()->rewind(); + + if ($response->getStatusCode() === 404) { + throw new RepoFileNotFound(); + } + + if ($response->getStatusCode() !== 200) { + throw new \RuntimeException( + "Invalid TUF repo response: " . $response->getBody()->getContents(), + $response->getStatusCode() + ); + } + + return new FulfilledPromise($response->getBody()); + } + + /** + * Gets a file if it exists in the remote repo. + * + * @param string $fileName The file name to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * + * @return string|null The contents of the file or null if it does not exist. + */ + public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string + { + try { + return $this->fetchMetadata($fileName, $maxBytes)->wait(); + } catch (RepoFileNotFound $exception) { + return null; + } + } } diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 1dfc1d0d9ff..fb53e0ca597 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -1,4 +1,5 @@ extensionId = $extensionId; - - $resolver = new OptionsResolver; - - try - { - $this->configureTufOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $params = $resolver->resolve($params); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - $this->params = $params; - } - - /** - * Configures default values or pass arguments to params - * - * @param OptionsResolver $resolver The OptionsResolver for the params - * @return void - */ - protected function configureTufOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'url_prefix' => 'https://raw.githubusercontent.com', - 'metadata_path' => '/joomla/updates/test/repository/', - 'targets_path' => '/targets/', - 'mirrors' => [], - ] - ) - ->setAllowedTypes('url_prefix', 'string') - ->setAllowedTypes('metadata_path', 'string') - ->setAllowedTypes('targets_path', 'string') - ->setAllowedTypes('mirrors', 'array'); - } - - /** - * 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() - { - $db = Factory::getContainer()->get(DatabaseDriver::class); - - $fileFetcher = HttpFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); - - $storage = new DatabaseStorage($db, $this->extensionId); - - $updater = new Updater( - $fileFetcher, - $this->params['mirrors'], - $storage - ); - - 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(); - - return $storage['targets.json']; - } - catch (FreezeAttackException | MetadataException | SignatureThresholdException | RollbackAttackException $e) - { - // When the validation fails, for example when one file is written but the others don't, we roll back everything - // and cancel the update - $query = $db->getQuery(true) - ->delete($db->quoteName('#__tuf_metadata')) - ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); - $db->setQuery($query); - - return null; - } - } + /** + * The id of the extension to be updated + * + * @var integer + */ + private $extensionId; + + /** + * The params of the validator + * + * @var mixed + */ + private $params; + + /** + * Validating updates with TUF + * + * @param integer $extensionId The ID of the extension to be checked + * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for the update + */ + public function __construct(int $extensionId, $params) + { + $this->extensionId = $extensionId; + + $resolver = new OptionsResolver; + + try { + $this->configureTufOptions($resolver); + } catch (\Exception $e) { + } + + try { + $params = $resolver->resolve($params); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + $this->params = $params; + } + + /** + * Configures default values or pass arguments to params + * + * @param OptionsResolver $resolver The OptionsResolver for the params + * @return void + */ + protected function configureTufOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'url_prefix' => 'https://raw.githubusercontent.com', + 'metadata_path' => '/joomla/updates/test/repository/', + 'targets_path' => '/targets/', + 'mirrors' => [], + ] + ) + ->setAllowedTypes('url_prefix', 'string') + ->setAllowedTypes('metadata_path', 'string') + ->setAllowedTypes('targets_path', 'string') + ->setAllowedTypes('mirrors', 'array'); + } + + /** + * 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() + { + $db = Factory::getContainer()->get(DatabaseDriver::class); + + $fileFetcher = HttpFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); + + $storage = new DatabaseStorage($db, $this->extensionId); + + $updater = new Updater( + $fileFetcher, + $this->params['mirrors'], + $storage + ); + + 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(); + + return $storage['targets.json']; + } catch (FreezeAttackException | MetadataException | SignatureThresholdException | RollbackAttackException $e) { + // When the validation fails, for example when one file is written but the others don't, we roll back everything + // and cancel the update + $query = $db->getQuery(true) + ->delete($db->quoteName('#__tuf_metadata')) + ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); + $db->setQuery($query); + + return null; + } + } } diff --git a/libraries/src/Table/Tuf.php b/libraries/src/Table/Tuf.php index 0d4b7f9d178..6c604164e9e 100644 --- a/libraries/src/Table/Tuf.php +++ b/libraries/src/Table/Tuf.php @@ -1,4 +1,5 @@ 0, - 'administrator' => 1, - 'installation' => 2, - 'api' => 3, - 'cli' => 4 - ]; - - /** - * 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); - - foreach ($targets as $target) - { - $updateTable = Table::getInstance('update'); - $updateTable->set('update_site_id', $options['update_site_id']); - - $updateTable->bind($target); - - $updates[] = $updateTable; - } - - return array('update_sites' => array(), '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 = array(); - $resolver = new OptionsResolver; - - try - { - $this->configureUpdateOptions($resolver); - $keys = $resolver->getDefinedOptions(); - } - catch (\Exception $e) - { - } - - // Get extension_id for TufValidation - $db = $this->parent->getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__update_sites_extensions')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extension_id = $db->loadResult(); - } - catch (\RuntimeException $e) - { - // Do nothing - } - - $params = [ - 'url_prefix' => 'https://raw.githubusercontent.com', - 'metadata_path' => '/joomla/updates/test/repository/', - 'targets_path' => '/targets/', - 'mirrors' => [] - ]; - - $TufValidation = new TufValidation($extension_id, $params); - $metaData = $TufValidation->getValidUpdate(); - - $metaData = json_decode($metaData); - - if (isset($metaData->signed->targets)) - { - foreach ($metaData->signed->targets as $filename => $target) - { - $values = []; - - foreach ($keys as $key) - { - if (isset($target->custom->$key)) - { - $values[$key] = $target->custom->$key; - } - } - - if (isset($values['client']) && is_string($values['client']) - && key_exists(strtolower($values['client']), $this->clientId)) - { - $values['client'] = $this->clientId[strtolower($values['client'])]; - } - - if (isset($values['infourl']) && isset($values['infourl']->url)) - { - $values['infourl'] = $values['infourl']->url; - } - - try - { - $values = $resolver->resolve($values); - } - catch (\Exception $e) - { - continue; - } - - $versions[$values['version']] = $values; - } - - usort($versions, function ($a, $b) { - return version_compare($b['version'], $a['version']); - }); - - $checker = new ConstraintChecker; - - foreach ($versions as $version) - { - if ($checker->check((array) $version)) - { - return array($version); - } - } - } - - return false; - } - - /** - * 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' => 1, - 'version' => "1", - 'data' => '', - 'detailsurl' => '', - 'infourl' => '', - 'downloads' => [], - 'targetplatform' => new \StdClass, - 'php_minimum' => 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', 'object') - ->setAllowedTypes('php_minimum', 'string') - ->setAllowedTypes('supported_databases', 'object') - ->setAllowedTypes('stability', 'string') - ->setRequired(['version']); - } + /** + * The client ID mapping array + * + * @var array + */ + private $clientId = [ + 'site' => 0, + 'administrator' => 1, + 'installation' => 2, + 'api' => 3, + 'cli' => 4 + ]; + + /** + * 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); + + foreach ($targets as $target) { + $updateTable = Table::getInstance('update'); + $updateTable->set('update_site_id', $options['update_site_id']); + + $updateTable->bind($target); + + $updates[] = $updateTable; + } + + return array('update_sites' => array(), '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 = array(); + $resolver = new OptionsResolver(); + + try { + $this->configureUpdateOptions($resolver); + $keys = $resolver->getDefinedOptions(); + } catch (\Exception $e) { + } + + // Get extension_id for TufValidation + $db = $this->parent->getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__update_sites_extensions')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extension_id = $db->loadResult(); + } catch (\RuntimeException $e) { + // Do nothing + } + + $params = [ + 'url_prefix' => 'https://raw.githubusercontent.com', + 'metadata_path' => '/joomla/updates/test/repository/', + 'targets_path' => '/targets/', + 'mirrors' => [] + ]; + + $TufValidation = new TufValidation($extension_id, $params); + $metaData = $TufValidation->getValidUpdate(); + + $metaData = json_decode($metaData); + + if (isset($metaData->signed->targets)) { + foreach ($metaData->signed->targets as $filename => $target) { + $values = []; + + foreach ($keys as $key) { + if (isset($target->custom->$key)) { + $values[$key] = $target->custom->$key; + } + } + + if ( + isset($values['client']) && is_string($values['client']) + && key_exists(strtolower($values['client']), $this->clientId) + ) { + $values['client'] = $this->clientId[strtolower($values['client'])]; + } + + if (isset($values['infourl']) && isset($values['infourl']->url)) { + $values['infourl'] = $values['infourl']->url; + } + + try { + $values = $resolver->resolve($values); + } catch (\Exception $e) { + continue; + } + + $versions[$values['version']] = $values; + } + + usort($versions, function ($a, $b) { + return version_compare($b['version'], $a['version']); + }); + + $checker = new ConstraintChecker(); + + foreach ($versions as $version) { + if ($checker->check((array) $version)) { + return array($version); + } + } + } + + return false; + } + + /** + * 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' => 1, + 'version' => "1", + 'data' => '', + 'detailsurl' => '', + 'infourl' => '', + 'downloads' => [], + 'targetplatform' => new \StdClass(), + 'php_minimum' => 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', 'object') + ->setAllowedTypes('php_minimum', 'string') + ->setAllowedTypes('supported_databases', 'object') + ->setAllowedTypes('stability', 'string') + ->setRequired(['version']); + } } diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index d49fbbeb551..0c62279a44b 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -1,4 +1,5 @@ checkTargetplatform($constraints['targetplatform'])) - { - return false; - } - - // Check php_minimumm, assume true when not set - if (isset($constraints['php_minimum']) - && !$this->checkPhpMinimum($constraints['php_minimum'])) - { - return false; - } - - // Check supported databases, assume true when not set - if (isset($constraints['supported_databases']) - && !$this->checkSupportedDatabases($constraints['supported_databases'])) - { - return false; - } - - // Check stability, assume true when not set - if (isset($constraints['stability']) - && !$this->checkStability($constraints['stability'])) - { - return false; - } - - return true; - } - - /** - * Check the targetPlatform - * - * @param object $targetPlatform - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function checkTargetplatform(\stdClass $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 object $supportedDatabases stdClass of supported databases and versions - * - * @return boolean - * - * @since __DEPLOY_VERSION__ - */ - protected function checkSupportedDatabases(\stdClass $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 (\property_exists($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; - } + /** + * Checks whether the passed constraints are matched + * + * @param array $constraints The provided constraints to be checked + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function check(array $constraints) + { + if (!isset($constraints['targetplatform'])) { + // targetplatform is required + return false; + } + + // Check targetplatform + if (!$this->checkTargetplatform($constraints['targetplatform'])) { + return false; + } + + // Check php_minimumm, assume true when not set + if ( + isset($constraints['php_minimum']) + && !$this->checkPhpMinimum($constraints['php_minimum']) + ) { + return false; + } + + // Check supported databases, assume true when not set + if ( + isset($constraints['supported_databases']) + && !$this->checkSupportedDatabases($constraints['supported_databases']) + ) { + return false; + } + + // Check stability, assume true when not set + if ( + isset($constraints['stability']) + && !$this->checkStability($constraints['stability']) + ) { + return false; + } + + return true; + } + + /** + * Check the targetPlatform + * + * @param object $targetPlatform + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function checkTargetplatform(\stdClass $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 object $supportedDatabases stdClass of supported databases and versions + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + protected function checkSupportedDatabases(\stdClass $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 (\property_exists($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/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php index 88e464b9e14..933f63a03ac 100644 --- a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php +++ b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php @@ -1,4 +1,5 @@ testContent. - * - * @return void - * - * @dataProvider providerFetchFileError - * - * @covers ::fetchFile - */ - public function testFetchFileError( - int $statusCode, - string $exceptionClass, - ?int $exceptionCode = null, - ?int $maxBytes = null - ): void { - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn($statusCode); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->expectException($exceptionClass); - $this->expectExceptionCode($exceptionCode ?? $statusCode); - $this->getFetcher($clientMock) - ->fetchMetadata('test.json', $maxBytes ?? strlen($this->testContent)) - ->wait(); - } - - /** - * Tests various error conditions when fetching a file with fetchFileIfExists(). - * - * @param integer $statusCode - * The response status code. - * @param string $exceptionClass - * The expected exception class that will be thrown. - * @param integer|null $exceptionCode - * (optional) The expected exception code. Defaults to the status code. - * @param integer|null $maxBytes - * (optional) The maximum number of bytes to read from the response. - * Defaults to the length of $this->testContent. - * - * @return void - * - * @dataProvider providerFileIfExistsError - * - * @covers ::providerFileIfExists - */ - public function testFetchFileIfExistsError( - int $statusCode, - string $exceptionClass, - ?int $exceptionCode = null, - ?int $maxBytes = null - ): void { - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn($statusCode); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->expectException($exceptionClass); - $this->expectExceptionCode($exceptionCode ?? $statusCode); - $this->getFetcher($clientMock) - ->fetchMetadataIfExists('test.json', $maxBytes ?? strlen($this->testContent)); - } - - /** - * Tests fetching a file without any errors. - * - * @return void - */ - public function testFetchMetadataReturnsCorrectResponseOnSuccessfulFetch(): void - { - $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('getContents')->willReturn($this->testContent); - - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn(200); - $clientResponseMock->method('getBody')->willReturn($clientBodyMock); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->assertSame( - $this->testContent, - $this->getFetcher($clientMock)->fetchMetadata('test.json', 256)->wait()->getContents() - ); - } - - /** - * Tests fetching a file without any errors. - * - * @return void - */ - public function testFetchMetadataIfExistsReturnsCorrectResponseOnSuccessfulFetch(): void - { - $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('rewind')->willReturnSelf(); - $clientBodyMock->method('__toString')->willReturn($this->testContent); - - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn(200); - $clientResponseMock->method('getBody')->willReturn($clientBodyMock); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->assertSame( - $this->testContent, - $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) - ); - } - - /** - * Tests fetching a file without any errors. - * - * @return void - */ - public function testFetchMetadataIfExistsReturnsCorrectResponseOnNotFoundFetch(): void - { - $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('getContents')->willReturn($this->testContent); - - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn(404); - $clientResponseMock->method('getBody')->willReturn($clientBodyMock); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->assertNull( - $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) - ); - } - - /** - * Tests creating a file fetcher with a repo base URI. - * - * @return void - * - * @covers ::createFromUri - */ - public function testCreateFromUri(): void - { - $this->assertInstanceOf( - HttpFileFetcher::class, - HttpFileFetcher::createFromUri('https://example.com') - ); - } + /** + * The content of the mocked response(s). + * + * This is deliberately not readable by json_decode(), in order to prove + * that the fetcher does not try to parse or process the response content + * in any way. + * + * @var string + */ + private $testContent = 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.'; + + /** + * Returns an instance of the file fetcher under test. + * + * @return HttpFileFetcher + * An instance of the file fetcher under test. + */ + private function getFetcher($clientMock): HttpFileFetcher + { + return new HttpFileFetcher($clientMock, '/metadata/', '/targets/', ""); + } + + /** + * Data provider for testfetchFileError(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerFetchFileError(): array + { + return [ + [404, RepoFileNotFound::class, 0], + [403, 'RuntimeException'] + ]; + } + + /** + * Data provider for testFetchFileIfExistsError(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerFileIfExistsError(): array + { + return [ + [403, 'RuntimeException'] + ]; + } + + /** + * Tests various error conditions when fetching a file with fetchFile(). + * + * @param integer $statusCode + * The response status code. + * @param string $exceptionClass + * The expected exception class that will be thrown. + * @param integer|null $exceptionCode + * (optional) The expected exception code. Defaults to the status code. + * @param integer|null $maxBytes + * (optional) The maximum number of bytes to read from the response. + * Defaults to the length of $this->testContent. + * + * @return void + * + * @dataProvider providerFetchFileError + * + * @covers ::fetchFile + */ + public function testFetchFileError( + int $statusCode, + string $exceptionClass, + ?int $exceptionCode = null, + ?int $maxBytes = null + ): void { + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn($statusCode); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->expectException($exceptionClass); + $this->expectExceptionCode($exceptionCode ?? $statusCode); + $this->getFetcher($clientMock) + ->fetchMetadata('test.json', $maxBytes ?? strlen($this->testContent)) + ->wait(); + } + + /** + * Tests various error conditions when fetching a file with fetchFileIfExists(). + * + * @param integer $statusCode + * The response status code. + * @param string $exceptionClass + * The expected exception class that will be thrown. + * @param integer|null $exceptionCode + * (optional) The expected exception code. Defaults to the status code. + * @param integer|null $maxBytes + * (optional) The maximum number of bytes to read from the response. + * Defaults to the length of $this->testContent. + * + * @return void + * + * @dataProvider providerFileIfExistsError + * + * @covers ::providerFileIfExists + */ + public function testFetchFileIfExistsError( + int $statusCode, + string $exceptionClass, + ?int $exceptionCode = null, + ?int $maxBytes = null + ): void { + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn($statusCode); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->expectException($exceptionClass); + $this->expectExceptionCode($exceptionCode ?? $statusCode); + $this->getFetcher($clientMock) + ->fetchMetadataIfExists('test.json', $maxBytes ?? strlen($this->testContent)); + } + + /** + * Tests fetching a file without any errors. + * + * @return void + */ + public function testFetchMetadataReturnsCorrectResponseOnSuccessfulFetch(): void + { + $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); + $clientBodyMock->method('getContents')->willReturn($this->testContent); + + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn(200); + $clientResponseMock->method('getBody')->willReturn($clientBodyMock); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->assertSame( + $this->testContent, + $this->getFetcher($clientMock)->fetchMetadata('test.json', 256)->wait()->getContents() + ); + } + + /** + * Tests fetching a file without any errors. + * + * @return void + */ + public function testFetchMetadataIfExistsReturnsCorrectResponseOnSuccessfulFetch(): void + { + $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); + $clientBodyMock->method('rewind')->willReturnSelf(); + $clientBodyMock->method('__toString')->willReturn($this->testContent); + + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn(200); + $clientResponseMock->method('getBody')->willReturn($clientBodyMock); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->assertSame( + $this->testContent, + $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) + ); + } + + /** + * Tests fetching a file without any errors. + * + * @return void + */ + public function testFetchMetadataIfExistsReturnsCorrectResponseOnNotFoundFetch(): void + { + $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); + $clientBodyMock->method('getContents')->willReturn($this->testContent); + + $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); + $clientResponseMock->method('getStatusCode')->willReturn(404); + $clientResponseMock->method('getBody')->willReturn($clientBodyMock); + + $clientMock = $this->getMockBuilder(Http::class)->getMock(); + $clientMock->method('get')->willReturn($clientResponseMock); + + $this->assertNull( + $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) + ); + } + + /** + * Tests creating a file fetcher with a repo base URI. + * + * @return void + * + * @covers ::createFromUri + */ + public function testCreateFromUri(): void + { + $this->assertInstanceOf( + HttpFileFetcher::class, + HttpFileFetcher::createFromUri('https://example.com') + ); + } } diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php index 85b32172e86..2316a54c7f5 100644 --- a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -1,4 +1,5 @@ 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(); - } - - public function testCheckMethodReturnsFalseIfPlatformIsMissing() - { - $constraint = []; - $this->assertFalse($this->checker->check($constraint)); - } - - public function testCheckMethodReturnsTrueIfPlatformIsOnlyConstraint() - { - $constraint = ['targetplatform' => (object) ["name" => "joomla", "version" => "4.*"]]; - $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'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], - true - ], - [ - ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], - true - ], - [ - ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], - true - ], - [ - ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], - (object) ['mysql' => '5.8', 'mariadb' => '10.3'], - false - ], - [ - ['type' => 'pgsql', 'version' => '14.3'], - (object) ['mysql' => '5.8', 'mariadb' => '10.3'], - false - ], - [ - ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], - (object) ['mysql' => '5.6', 'mariadb' => '10.4'], - false - ], - [ - ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], - true - ], - ]; - } - - /** - * Data provider for testCheckTargetplatform method - * - * @since __DEPLOY_VERSION__ - * - * @return array[] - */ - protected function targetplatformDataProvider() - { - return [ - [(object) ["name" => "foobar", "version" => "1.*"], false], - [(object) ["name" => "foobar", "version" => "4.*"], false], - [(object) ["name" => "joomla", "version" => "1.*"], false], - [(object) ["name" => "joomla", "version" => "3.1.2"], false], - [(object) ["name" => "joomla", "version" => ""], true], - [(object) ["name" => "joomla", "version" => ".*"], true], - [(object) ["name" => "joomla", "version" => JVERSION], true], - [(object) ["name" => "joomla", "version" => "4.*"], 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; - } + /** + * @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(); + } + + public function testCheckMethodReturnsFalseIfPlatformIsMissing() + { + $constraint = []; + $this->assertFalse($this->checker->check($constraint)); + } + + public function testCheckMethodReturnsTrueIfPlatformIsOnlyConstraint() + { + $constraint = ['targetplatform' => (object) ["name" => "joomla", "version" => "4.*"]]; + $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'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], + (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + false + ], + [ + ['type' => 'pgsql', 'version' => '14.3'], + (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + false + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.4'], + false + ], + [ + ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + ]; + } + + /** + * Data provider for testCheckTargetplatform method + * + * @since __DEPLOY_VERSION__ + * + * @return array[] + */ + protected function targetplatformDataProvider() + { + return [ + [(object) ["name" => "foobar", "version" => "1.*"], false], + [(object) ["name" => "foobar", "version" => "4.*"], false], + [(object) ["name" => "joomla", "version" => "1.*"], false], + [(object) ["name" => "joomla", "version" => "3.1.2"], false], + [(object) ["name" => "joomla", "version" => ""], true], + [(object) ["name" => "joomla", "version" => ".*"], true], + [(object) ["name" => "joomla", "version" => JVERSION], true], + [(object) ["name" => "joomla", "version" => "4.*"], 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; + } } From 16108c110a122b68ce8d8dface9339949d1d2b94 Mon Sep 17 00:00:00 2001 From: Franciska Perisa <9084265+fancyFranci@users.noreply.github.com> Date: Sat, 17 Sep 2022 19:36:34 +0200 Subject: [PATCH 054/126] Add error messages (#14) --- administrator/language/en-GB/lib_joomla.ini | 6 +++- libraries/src/TUF/TufValidation.php | 38 +++++++++++++++----- libraries/src/Updater/Adapter/TufAdapter.php | 12 ++++--- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index 450de241eae..201135c0c11 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -655,11 +655,15 @@ 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 is expired." +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_ROLLBACK_ATTACK="Update not possible because the offered update version is older than the current installed version." +JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD="Update not possible because the offered update has not 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." JLIB_INSTALLER_WARNING_UNABLE_TO_INSTALL_CONTENT_LANGUAGE="Unable to create a content language for %s language: %s" -JLIB_INSTALLER_TUF_NOT_AVAILABLE="TUF is not available for extensions yet." JLIB_JS_AJAX_ERROR_CONNECTION_ABORT="A connection abort has occurred while fetching the JSON data." JLIB_JS_AJAX_ERROR_NO_CONTENT="No content was returned." diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index fb53e0ca597..27a190d63e4 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -10,6 +10,7 @@ namespace Joomla\CMS\TUF; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\TUF\HttpFileFetcher; use Joomla\Database\DatabaseDriver; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; @@ -118,15 +119,36 @@ public function getValidUpdate() $updater->refresh(); return $storage['targets.json']; - } catch (FreezeAttackException | MetadataException | SignatureThresholdException | RollbackAttackException $e) { - // When the validation fails, for example when one file is written but the others don't, we roll back everything - // and cancel the update - $query = $db->getQuery(true) - ->delete($db->quoteName('#__tuf_metadata')) - ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); - $db->setQuery($query); - + } catch (MetadataException $e) { + $this->rollBackTufMetadata(); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_INVALID_METADATA'), 'error'); + return null; + } catch (FreezeAttackException $e) { + $this->rollBackTufMetadata(); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_FREEZE_ATTACK'), 'error'); + return null; + } catch (RollbackAttackException $e) { + $this->rollBackTufMetadata(); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_ROLLBACK_ATTACK'), 'error'); + return null; + } catch (SignatureThresholdException $e) { + $this->rollBackTufMetadata(); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD'), 'error'); return null; } } + + /** + * 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 = Factory::getContainer()->get(DatabaseDriver::class); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__tuf_metadata')) + ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); + $db->setQuery($query); + } } diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index a46c5df8151..446210acc12 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -52,13 +52,15 @@ public function findUpdate($options) $updates = []; $targets = $this->getUpdateTargets($options); - foreach ($targets as $target) { - $updateTable = Table::getInstance('update'); - $updateTable->set('update_site_id', $options['update_site_id']); + if ($targets) { + foreach ($targets as $target) { + $updateTable = Table::getInstance('update'); + $updateTable->set('update_site_id', $options['update_site_id']); - $updateTable->bind($target); + $updateTable->bind($target); - $updates[] = $updateTable; + $updates[] = $updateTable; + } } return array('update_sites' => array(), 'updates' => $updates); From e1cfeb4def6ef4dfc155e87dad1baf72515780f7 Mon Sep 17 00:00:00 2001 From: Magnus Singer Date: Sun, 18 Sep 2022 15:20:45 +0200 Subject: [PATCH 055/126] Continue integration of TUF into Joomla (#16) --- .../src/Model/UpdateModel.php | 317 ++++++++---------- libraries/src/TUF/DatabaseStorage.php | 15 +- libraries/src/TUF/HttpFileFetcher.php | 47 +-- libraries/src/TUF/TufValidation.php | 13 +- libraries/src/Table/Update.php | 11 +- .../src/Updater/Adapter/CollectionAdapter.php | 14 +- libraries/src/Updater/Adapter/TufAdapter.php | 90 +++-- libraries/src/Updater/ConstraintChecker.php | 18 +- .../src/Updater/Update/AbstractUpdate.php | 193 +++++++++++ libraries/src/Updater/Update/DataUpdate.php | 96 ++++++ .../src/Updater/Update/UpdateInterface.php | 16 + .../{Update.php => Update/XmlUpdate.php} | 208 ++---------- libraries/src/Updater/UpdateAdapter.php | 22 +- libraries/src/Updater/Updater.php | 64 ++-- 14 files changed, 648 insertions(+), 476 deletions(-) create mode 100644 libraries/src/Updater/Update/AbstractUpdate.php create mode 100644 libraries/src/Updater/Update/DataUpdate.php create mode 100644 libraries/src/Updater/Update/UpdateInterface.php rename libraries/src/Updater/{Update.php => Update/XmlUpdate.php} (71%) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index a0899f9dc20..f376b7b5b96 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -1,5 +1,4 @@ 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; - - // "Testing" - case 'testing': - $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; - break; - - // "Custom" - // @todo: check if the customurl is valid and not just "not empty". - case 'custom': - if (trim($params->get('customurl', '')) != '') { - $updateURL = trim($params->get('customurl', '')); - } else { - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); - - return; - } - break; + $updateURL = 'https://raw.githubusercontent.com/joomla/updates/test8/repository/'; + if ($params->get('updatesource', 'nochange') == 'custom') { + $paramsURL = $params->get('customurl', ''); + if (trim($paramsURL) != '') { + $updateURL = trim($paramsURL); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * 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'; + return; + } } $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; @@ -132,7 +109,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 * @@ -144,12 +121,12 @@ public function refreshUpdates($force = false) $cache_timeout = 0; } else { $update_params = ComponentHelper::getParams('com_installer'); - $cache_timeout = (int) $update_params->get('cachetimeout', 6); + $cache_timeout = (int)$update_params->get('cachetimeout', 6); $cache_timeout = 3600 * $cache_timeout; } - $updater = Updater::getInstance(); - $minimumStability = Updater::STABILITY_STABLE; + $updater = Updater::getInstance(); + $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { @@ -240,15 +217,14 @@ public function getUpdateInformation() // Initialise the return array. $this->updateInformation = array( 'installed' => \JVERSION, - 'latest' => null, - 'object' => null, + 'latest' => null, + 'object' => null, 'hasUpdate' => false, - 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly ); // Fetch the update information from the database. $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__updates')) @@ -272,7 +248,15 @@ public function getUpdateInformation() return $this->updateInformation; } - $minimumStability = Updater::STABILITY_STABLE; + $this->updateInformation['latest'] = $updateObject->version; + $this->updateInformation['current'] = JVERSION; + + // Check whether this is an update or not. + if (version_compare($updateObject->version, JVERSION, '>')) { + $this->updateInformation['hasUpdate'] = true; + } + + $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { @@ -280,17 +264,15 @@ public function getUpdateInformation() } // Fetch the full update details from the update details URL. - $update = new Update(); - $update->loadFromXml($updateObject->detailsurl, $minimumStability); + if (empty($updateObject->data)) { + $update = new XmlUpdate(); + $update->loadFromXml($updateObject->detailsurl, $minimumStability); + } else { + $update = new DataUpdate(); + $update->loadFromData($updateObject, $minimumStability); + } - // Make sure we use the current information we got from the detailsurl $this->updateInformation['object'] = $update; - $this->updateInformation['latest'] = $updateObject->version; - - // Check whether this is an update or not. - if (version_compare($this->updateInformation['latest'], JVERSION, '>')) { - $this->updateInformation['hasUpdate'] = true; - } return $this->updateInformation; } @@ -304,10 +286,10 @@ public function getUpdateInformation() */ public function purge() { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); // Modify the database record - $update_site = new \stdClass(); + $update_site = new \stdClass; $update_site->last_check_timestamp = 0; $update_site->enabled = 1; $update_site->update_site_id = 1; @@ -340,10 +322,10 @@ public function download() { $updateInfo = $this->getUpdateInformation(); $packageURL = trim($updateInfo['object']->downloadurl->_data); - $sources = $updateInfo['object']->get('downloadSources', array()); + $sources = $updateInfo['object']->get('downloadSources', array()); // We have to manually follow the redirects here so we set the option to false. - $httpOptions = new Registry(); + $httpOptions = new Registry; $httpOptions->set('follow_location', false); try { @@ -357,7 +339,7 @@ public function download() // Follow the Location headers until the actual download URL is known while (isset($head->headers['location'])) { - $packageURL = (string) $head->headers['location'][0]; + $packageURL = (string)$head->headers['location'][0]; try { $head = HttpFactory::getHttp($httpOptions)->head($packageURL); @@ -377,14 +359,14 @@ public function download() } // Find the path to the temp directory and the local package. - $tempdir = (string) InputFilter::getInstance( + $tempdir = (string)InputFilter::getInstance( [], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES ) ->clean(Factory::getApplication()->get('tmp_path'), 'path'); - $target = $tempdir . '/' . $basename; + $target = $tempdir . '/' . $basename; $response = []; // Do we have a cached file? @@ -395,7 +377,7 @@ public function download() $mirror = 0; while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { - $name = $sources[$mirror]; + $name = $sources[$mirror]; $packageURL = trim($name->url); $mirror++; } @@ -409,7 +391,7 @@ public function download() $mirror = 0; while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { - $name = $sources[$mirror]; + $name = $sources[$mirror]; $packageURL = trim($name->url); $mirror++; } @@ -429,8 +411,8 @@ public function download() /** * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest * - * @param string $packagefile Location of the package to be installed - * @param Update $updateObject The Update Object + * @param string $packagefile Location of the package to be installed + * @param Update $updateObject The Update Object * * @return boolean False in case the validation did not work; true in any other case. * @@ -446,7 +428,7 @@ private function isChecksumValid($packagefile, $updateObject) foreach ($hashes as $hash) { if ($updateObject->get($hash, false)) { $hashPackage = hash_file($hash, $packagefile); - $hashRemote = $updateObject->$hash->_data; + $hashRemote = $updateObject->$hash->_data; if ($hashPackage !== $hashRemote) { // Return false in case the hash did not match @@ -462,8 +444,8 @@ private function isChecksumValid($packagefile, $updateObject) /** * Downloads a package file to a specific directory * - * @param string $url The URL to download from - * @param string $target The directory to store the file + * @param string $url The URL to download from + * @param string $target The directory to store the file * * @return boolean True on success * @@ -500,7 +482,7 @@ protected function downloadPackage($url, $target) /** * Backwards compatibility. Use createUpdateFile() instead. * - * @param null $basename The basename of the file to create + * @param null $basename The basename of the file to create * * @return boolean * @since 2.5.1 @@ -519,7 +501,7 @@ public function createRestorationFile($basename = null): bool * thereby determining how many and which overrides need to be checked and possibly updated * after Joomla installed an update. * - * @param string $basename Optional base path to the file. + * @param string $basename Optional base path to the file. * * @return boolean True if successful; false otherwise. * @@ -548,9 +530,9 @@ public function createUpdateFile($basename = null): bool } // Get the package name. - $config = $app->getConfig(); + $config = $app->getConfig(); $tempdir = $config->get('tmp_path'); - $file = $tempdir . '/' . $basename; + $file = $tempdir . '/' . $basename; $filesize = @filesize($file); $app->setUserState('com_joomlaupdate.password', $password); @@ -636,8 +618,7 @@ public function finaliseUpgrade() $installer->setUpgrade(true); $installer->setOverwrite(true); - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); - $installer->extension = new \Joomla\CMS\Table\Extension($db); + $installer->extension = new \Joomla\CMS\Table\Extension($this->getDatabase()); $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); $installer->setAdapter($installer->extension->type); @@ -649,7 +630,7 @@ public function finaliseUpgrade() // Run the script file. \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); - $manifestClass = new \JoomlaInstallerScript(); + $manifestClass = new \JoomlaInstallerScript; ob_start(); ob_implicit_flush(false); @@ -672,7 +653,7 @@ public function finaliseUpgrade() ob_end_clean(); // Get a database connector object. - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); /* * Check to see if a file extension by the same name is already installed. @@ -699,7 +680,7 @@ public function finaliseUpgrade() } $id = $db->loadResult(); - $row = new \Joomla\CMS\Table\Extension($db); + $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); if ($id) { // Load the entry and update the manifest_cache. @@ -784,7 +765,7 @@ public function finaliseUpgrade() ob_end_clean(); // Clobber any possible pending updates. - $update = new \Joomla\CMS\Table\Update($db); + $update = new \Joomla\CMS\Table\Update($this->getDatabase()); $uid = $update->find( array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') ); @@ -889,7 +870,7 @@ public function upload() $userfile = $input->files->get('install_package', null, 'raw'); // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) { + if (!(bool)ini_get('file_uploads')) { throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); } @@ -927,7 +908,7 @@ public function upload() // Build the appropriate paths. $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); - $tmp_src = $userfile['tmp_name']; + $tmp_src = $userfile['tmp_name']; // Move uploaded file. $result = File::upload($tmp_src, $tmp_dest, false, true); @@ -942,7 +923,7 @@ public function upload() /** * Checks the super admin credentials are valid for the currently logged in users * - * @param array $credentials The credentials to authenticate the user with + * @param array $credentials The credentials to authenticate the user with * * @return boolean * @@ -952,7 +933,7 @@ public function captiveLogin($credentials) { // Make sure the username matches $username = $credentials['username'] ?? null; - $user = Factory::getUser(); + $user = Factory::getUser(); if (strtolower($user->username) != strtolower($username)) { return false; @@ -965,7 +946,7 @@ public function captiveLogin($credentials) // Get the global Authentication object. $authenticate = Authentication::getInstance(); - $response = $authenticate->authenticate($credentials); + $response = $authenticate->authenticate($credentials); if ($response->status !== Authentication::STATUS_SUCCESS) { return false; @@ -1015,10 +996,10 @@ public function removePackageFiles() /** * Gets PHP options. - * @todo: Outsource, build common code base for pre install and pre update check - * * @return array Array of PHP config options * + * @todo: Outsource, build common code base for pre install and pre update check + * * @since 3.10.0 */ public function getPhpOptions() @@ -1030,54 +1011,54 @@ public function getPhpOptions() * A Joomla! Update which is not supported by current PHP * version is not shown. So this check is actually unnecessary. */ - $option = new \stdClass(); - $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); - $option->state = $this->isPhpVersionSupported(); + $option = new \stdClass; + $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); + $option->state = $this->isPhpVersionSupported(); $option->notice = null; - $options[] = $option; + $options[] = $option; // Check for zlib support. - $option = new \stdClass(); - $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); - $option->state = extension_loaded('zlib'); + $option = new \stdClass; + $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); + $option->state = extension_loaded('zlib'); $option->notice = null; - $options[] = $option; + $options[] = $option; // Check for XML support. - $option = new \stdClass(); - $option->label = Text::_('INSTL_XML_SUPPORT'); - $option->state = extension_loaded('xml'); + $option = new \stdClass; + $option->label = Text::_('INSTL_XML_SUPPORT'); + $option->state = extension_loaded('xml'); $option->notice = null; - $options[] = $option; + $options[] = $option; // Check for mbstring options. if (extension_loaded('mbstring')) { // Check for default MB language. - $option = new \stdClass(); - $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); - $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; + $option = new \stdClass; + $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); + $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); $options[] = $option; // Check for MB function overload. - $option = new \stdClass(); - $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); - $option->state = ini_get('mbstring.func_overload') == 0; + $option = new \stdClass; + $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); + $option->state = ini_get('mbstring.func_overload') == 0; $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); $options[] = $option; } // Check for a missing native parse_ini_file implementation. - $option = new \stdClass(); - $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); - $option->state = $this->getIniParserAvailability(); + $option = new \stdClass; + $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); + $option->state = $this->getIniParserAvailability(); $option->notice = null; $options[] = $option; // Check for missing native json_encode / json_decode support. - $option = new \stdClass(); - $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); - $option->state = function_exists('json_encode') && function_exists('json_decode'); + $option = new \stdClass; + $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); + $option->state = function_exists('json_encode') && function_exists('json_decode'); $option->notice = null; $options[] = $option; $updateInformation = $this->getUpdateInformation(); @@ -1085,18 +1066,18 @@ public function getPhpOptions() // Check if configured database is compatible with the next major version of Joomla $nextMajorVersion = Version::MAJOR_VERSION + 1; - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { - $option = new \stdClass(); - $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); - $option->state = $this->isDatabaseTypeSupported(); + if (version_compare($updateInformation['latest'], (string)$nextMajorVersion, '>=')) { + $option = new \stdClass; + $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); + $option->state = $this->isDatabaseTypeSupported(); $option->notice = null; - $options[] = $option; + $options[] = $option; } // Check if database structure is up to date - $option = new \stdClass(); - $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); - $option->state = $this->getDatabaseSchemaCheck(); + $option = new \stdClass; + $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); + $option->state = $this->getDatabaseSchemaCheck(); $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); $options[] = $option; @@ -1105,10 +1086,10 @@ public function getPhpOptions() /** * Gets PHP Settings. - * @todo: Outsource, build common code base for pre install and pre update check - * * @return array * + * @todo: Outsource, build common code base for pre install and pre update check + * * @since 3.10.0 */ public function getPhpSettings() @@ -1116,56 +1097,56 @@ public function getPhpSettings() $settings = array(); // Check for display errors. - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); - $setting->state = (bool) ini_get('display_errors'); + $setting->state = (bool)ini_get('display_errors'); $setting->recommended = false; $settings[] = $setting; // Check for file uploads. - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::_('INSTL_FILE_UPLOADS'); - $setting->state = (bool) ini_get('file_uploads'); + $setting->state = (bool)ini_get('file_uploads'); $setting->recommended = true; $settings[] = $setting; // Check for output buffering. - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); - $setting->state = (int) ini_get('output_buffering') !== 0; + $setting->state = (int)ini_get('output_buffering') !== 0; $setting->recommended = false; $settings[] = $setting; // Check for session auto-start. - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::_('INSTL_SESSION_AUTO_START'); - $setting->state = (bool) ini_get('session.auto_start'); + $setting->state = (bool)ini_get('session.auto_start'); $setting->recommended = false; $settings[] = $setting; // Check for native ZIP support. - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); $setting->state = function_exists('zip_open') && function_exists('zip_read'); $setting->recommended = true; $settings[] = $setting; // Check for GD support - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); $setting->state = extension_loaded('gd'); $setting->recommended = true; $settings[] = $setting; // Check for iconv support - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); $setting->state = function_exists('iconv'); $setting->recommended = true; $settings[] = $setting; // Check for intl support - $setting = new \stdClass(); + $setting = new \stdClass; $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); $setting->state = function_exists('transliterator_transliterate'); $setting->recommended = true; @@ -1197,10 +1178,10 @@ private function getConfiguredDatabaseType() public function isDatabaseTypeSupported() { $updateInformation = $this->getUpdateInformation(); - $nextMajorVersion = Version::MAJOR_VERSION + 1; + $nextMajorVersion = Version::MAJOR_VERSION + 1; // Check if configured database is compatible with Joomla 4 - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + if (version_compare($updateInformation['latest'], (string)$nextMajorVersion, '>=')) { $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); $currentDatabaseType = $this->getConfiguredDatabaseType(); @@ -1242,10 +1223,10 @@ private function getTargetMinimumPHPVersion() /** * Checks the availability of the parse_ini_file and parse_ini_string functions. - * @todo: Outsource, build common code base for pre install and pre update check - * * @return boolean True if the method exists. * + * @todo: Outsource, build common code base for pre install and pre update check + * * @since 3.10.0 */ public function getIniParserAvailability() @@ -1330,7 +1311,7 @@ private function getDatabaseSchemaCheck(): bool */ public function getNonCoreExtensions() { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1372,15 +1353,15 @@ public function getNonCoreExtensions() /** * Gets an array containing all installed and enabled plugins, that are not core plugins. * - * @param array $folderFilter Limit the list of plugins to a specific set of folder values + * @param array $folderFilter Limit the list of plugins to a specific set of folder values * * @return array name,version,updateserver * * @since 3.10.0 */ - public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','multifactorauth']) + public function getNonCorePlugins($folderFilter = ['system', 'user', 'authentication', 'actionlog', 'multifactorauth']) { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1399,8 +1380,7 @@ public function getNonCorePlugins($folderFilter = ['system','user','authenticati )->where( $db->qn('ex.enabled') . ' = 1' )->whereNotIn( - $db->quoteName('ex.extension_id'), - ExtensionHelper::getCoreExtensionIds() + $db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds() ); if (count($folderFilter) > 0) { @@ -1432,8 +1412,8 @@ public function getNonCorePlugins($folderFilter = ['system','user','authenticati /** * Called by controller's fetchExtensionCompatibility, which is called via AJAX. * - * @param string $extensionID The ID of the checked extension - * @param string $joomlaTargetVersion Target version of Joomla + * @param string $extensionID The ID of the checked extension + * @param string $joomlaTargetVersion Target version of Joomla * * @return object * @@ -1444,7 +1424,7 @@ public function fetchCompatibility($extensionID, $joomlaTargetVersion) $updateSites = $this->getUpdateSitesInfo($extensionID); if (empty($updateSites)) { - return (object) array('state' => 2); + return (object)array('state' => 2); } foreach ($updateSites as $updateSite) { @@ -1455,24 +1435,24 @@ public function fetchCompatibility($extensionID, $joomlaTargetVersion) $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + return (object)array('state' => 1, 'compatibleVersions' => $compatibleVersions); } } else { $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + return (object)array('state' => 1, 'compatibleVersions' => $compatibleVersions); } } // In any other case we mark this extension as not compatible - return (object) array('state' => 0); + return (object)array('state' => 0); } /** * Returns records with update sites and extension information for a given extension ID. * - * @param int $extensionID The extension ID + * @param int $extensionID The extension ID * * @return array * @@ -1480,8 +1460,8 @@ public function fetchCompatibility($extensionID, $joomlaTargetVersion) */ private function getUpdateSitesInfo($extensionID) { - $id = (int) $extensionID; - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $id = (int)$extensionID; + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1519,8 +1499,8 @@ private function getUpdateSitesInfo($extensionID) /** * Method to get details URLs from a collection update site for given extension and Joomla target version. * - * @param array $updateSiteInfo The update site and extension information record to process - * @param string $joomlaTargetVersion The Joomla! version to test against, + * @param array $updateSiteInfo The update site and extension information record to process + * @param string $joomlaTargetVersion The Joomla! version to test against, * * @return array An array of URLs. * @@ -1530,7 +1510,7 @@ private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) { $return = array(); - $http = new Http(); + $http = new Http; try { $response = $http->get($updateSiteInfo['location']); @@ -1545,24 +1525,22 @@ private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) $updateSiteXML = simplexml_load_string($response->body); foreach ($updateSiteXML->extension as $extension) { - $attribs = new \stdClass(); + $attribs = new \stdClass; - $attribs->element = ''; - $attribs->type = ''; - $attribs->folder = ''; + $attribs->element = ''; + $attribs->type = ''; + $attribs->folder = ''; $attribs->targetplatformversion = ''; foreach ($extension->attributes() as $key => $value) { - $attribs->$key = (string) $value; + $attribs->$key = (string)$value; } - if ( - $attribs->element === $updateSiteInfo['ext_element'] + if ($attribs->element === $updateSiteInfo['ext_element'] && $attribs->type === $updateSiteInfo['ext_type'] && $attribs->folder === $updateSiteInfo['ext_folder'] - && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion) - ) { - $return[] = (string) $extension['detailsurl']; + && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion)) { + $return[] = (string)$extension['detailsurl']; } } @@ -1572,8 +1550,8 @@ private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) /** * Method to check non core extensions for compatibility. * - * @param string $updateFileUrl The items update XML url. - * @param string $joomlaTargetVersion The Joomla! version to test against + * @param string $updateFileUrl The items update XML url. + * @param string $joomlaTargetVersion The Joomla! version to test against * * @return array An array of strings with compatible version numbers * @@ -1583,7 +1561,7 @@ private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) { $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); - $update = new Update(); + $update = new Update; $update->set('jversion.full', $joomlaTargetVersion); $update->loadFromXml($updateFileUrl, $minimumStability); @@ -1605,7 +1583,7 @@ private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) /** * Translates an extension name * - * @param object &$item The extension of which the name needs to be translated + * @param object &$item The extension of which the name needs to be translated * * @return void * @@ -1656,7 +1634,7 @@ protected function translateExtensionName(&$item) /** * Checks whether a given template is active * - * @param string $template The template name to be checked + * @param string $template The template name to be checked * * @return boolean * @@ -1664,7 +1642,7 @@ protected function translateExtensionName(&$item) */ public function isTemplateActive($template) { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1701,8 +1679,7 @@ function ($value) { )->from( $db->qn('#__menu') )->whereIn( - $db->qn('template_style_id'), - $ids + $db->qn('template_style_id'), $ids ); $menu = $db->setQuery($query)->loadResult() > 0; diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 8024a34176b..67c9cad2168 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -1,5 +1,4 @@ getHttp([], 'curl'); @@ -101,8 +101,8 @@ public static function createFromUri( /** * Fetches a metadata file from the remote repo. * - * @param string $fileName The name of the metadata file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. + * @param string $fileName The name of the metadata file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. * * @return \GuzzleHttp\Promise\PromiseInterface A promise wrapping a StreamInterface instanfe */ @@ -114,10 +114,10 @@ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface /** * Fetches a target file from the remote repo. * - * @param string $fileName The name of the target to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * @param array $options (optional) Additional request options to pass to the http client - * @param string $url An arbitrary URL from which the target should be downloaded. + * @param string $fileName The name of the target to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * @param array $options (optional) Additional request options to pass to the http client + * @param string $url An arbitrary URL from which the target should be downloaded. * * @return PromiseInterface * @@ -125,10 +125,11 @@ public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface */ public function fetchTarget( string $fileName, - int $maxBytes, - array $options = [], + int $maxBytes, + array $options = [], string $url = null - ): PromiseInterface { + ): PromiseInterface + { $location = $url ?: $this->baseUri . $this->targetsPrefix . $fileName; return $this->fetchFile($location, $maxBytes, $options); @@ -137,9 +138,9 @@ public function fetchTarget( /** * Fetches a file from a URL. * - * @param string $url The URL of the file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * @param array $options Additional request options to pass to the http client + * @param string $url The URL of the file to fetch. + * @param integer $maxBytes The maximum number of bytes to download. + * @param array $options Additional request options to pass to the http client * * @return PromiseInterface A promise representing the eventual result of the operation. * @@ -170,8 +171,8 @@ protected function fetchFile(string $url, int $maxBytes, array $options = []): P /** * Gets a file if it exists in the remote repo. * - * @param string $fileName The file name to fetch. - * @param integer $maxBytes The maximum number of bytes to download. + * @param string $fileName The file name to fetch. + * @param integer $maxBytes The maximum number of bytes to download. * * @return string|null The contents of the file or null if it does not exist. */ diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 27a190d63e4..c5d76550f08 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -1,5 +1,4 @@ setDefaults( [ - 'url_prefix' => 'https://raw.githubusercontent.com', - 'metadata_path' => '/joomla/updates/test/repository/', - 'targets_path' => '/targets/', + 'url_prefix' => '', + 'metadata_path' => '', + 'targets_path' => '', 'mirrors' => [], ] ) diff --git a/libraries/src/Table/Update.php b/libraries/src/Table/Update.php index 22a2994d21c..91774f038b1 100644 --- a/libraries/src/Table/Update.php +++ b/libraries/src/Table/Update.php @@ -1,5 +1,4 @@ stack[] = $name; - $tag = $this->_getStackLocation(); + $tag = $this->_getStackLocation(); // Reset the data if (isset($this->$tag)) { @@ -183,8 +183,8 @@ public function _startElement($parser, $name, $attrs = array()) * Closing an XML element * Note: This is a protected function though has to be exposed externally as a callback * - * @param object $parser Parser object - * @param string $name Name of the element closing + * @param object $parser Parser object + * @param string $name Name of the element closing * * @return void * @@ -205,7 +205,7 @@ protected function _endElement($parser, $name) /** * Finds an update * - * @param array $options Options to use: update_site_id: the unique ID of the update site to look at + * @param array $options Options to use: update_site_id: the unique ID of the update site to look at * * @return array|boolean Update_sites and updates discovered. False on failure * diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 446210acc12..0bcc1a62057 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -31,17 +31,17 @@ class TufAdapter extends UpdateAdapter * @var array */ private $clientId = [ - 'site' => 0, + 'site' => 0, 'administrator' => 1, - 'installation' => 2, - 'api' => 3, - 'cli' => 4 + 'installation' => 2, + 'api' => 3, + 'cli' => 4 ]; /** * Finds an update. * - * @param array $options Update options. + * @param array $options Update options. * * @return array|boolean Array containing the array of update sites and array of updates. False on failure * @@ -69,7 +69,7 @@ public function findUpdate($options) /** * Finds targets. * - * @param array $options Update options. + * @param array $options Update options. * * @return array|boolean Array containing the array of update sites and array of updates. False on failure * @@ -102,15 +102,42 @@ public function getUpdateTargets($options) // Do nothing } - $params = [ - 'url_prefix' => 'https://raw.githubusercontent.com', - 'metadata_path' => '/joomla/updates/test/repository/', - 'targets_path' => '/targets/', - 'mirrors' => [] - ]; + // Get params for TufValidation + $query = $db->getQuery(true) + ->select($db->quoteName('location')) + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); + $db->setQuery($query); + + try { + $params_string = $db->loadResult(); + $params_mirrors = explode('|', $params_string); + $params = [ + 'url_prefix' => '', + 'metadata_path' => '', + 'targets_path' => '', + 'mirrors' => [] + ]; + for ($i = 0; $i < count($params_mirrors); $i++) { + if ($i == 0) { + $params['url_prefix'] = $params_mirrors[$i]; + } else { + $mirror = [ + 'url_prefix' => $params_mirrors[$i], + 'metadata_path' => '', + 'targets_path' => '', + 'confined_target_dirs' => [] + ]; + array_push($params['mirrors'], $mirror); + } + } + } catch (\RuntimeException $e) { + // Do nothing + } $TufValidation = new TufValidation($extension_id, $params); - $metaData = $TufValidation->getValidUpdate(); + $metaData = $TufValidation->getValidUpdate(); $metaData = json_decode($metaData); @@ -141,6 +168,13 @@ public function getUpdateTargets($options) continue; } + $values['data'] = [ + 'downloads' => $values['downloads'], + 'targetplatform' => $values['targetplatform'], + 'supported_databases' => $values['supported_databases'], + 'hashes' => $target->hashes + ]; + $versions[$values['version']] = $values; } @@ -151,7 +185,7 @@ public function getUpdateTargets($options) $checker = new ConstraintChecker(); foreach ($versions as $version) { - if ($checker->check((array) $version)) { + if ($checker->check((array)$version)) { return array($version); } } @@ -163,7 +197,7 @@ public function getUpdateTargets($options) /** * Configures default values or pass arguments to params * - * @param OptionsResolver $resolver The OptionsResolver for the params + * @param OptionsResolver $resolver The OptionsResolver for the params * * @return void * @@ -173,20 +207,20 @@ protected function configureUpdateOptions(OptionsResolver $resolver) { $resolver->setDefaults( [ - 'name' => null, - 'description' => '', - 'element' => '', - 'type' => null, - 'client' => 1, - 'version' => "1", - 'data' => '', - 'detailsurl' => '', - 'infourl' => '', - 'downloads' => [], - 'targetplatform' => new \StdClass(), - 'php_minimum' => null, + 'name' => null, + 'description' => '', + 'element' => '', + 'type' => null, + 'client' => 0, + 'version' => "1", + 'data' => '', + 'detailsurl' => '', + 'infourl' => '', + 'downloads' => [], + 'targetplatform' => new \StdClass(), + 'php_minimum' => null, 'supported_databases' => new \StdClass(), - 'stability' => '' + 'stability' => '' ] ) ->setAllowedTypes('version', 'string') diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index 0c62279a44b..fbf95eb8b5a 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -27,7 +27,7 @@ class ConstraintChecker /** * Checks whether the passed constraints are matched * - * @param array $constraints The provided constraints to be checked + * @param array $constraints The provided constraints to be checked * * @return boolean * @@ -75,7 +75,7 @@ public function check(array $constraints) /** * Check the targetPlatform * - * @param object $targetPlatform + * @param object $targetPlatform * * @return boolean * @@ -100,7 +100,7 @@ protected function checkTargetplatform(\stdClass $targetPlatform) /** * Check the minimum PHP version * - * @param string $phpMinimum The minimum php version to check + * @param string $phpMinimum The minimum php version to check * * @return boolean * @@ -115,7 +115,7 @@ protected function checkPhpMinimum(string $phpMinimum) /** * Check the supported databases and versions * - * @param object $supportedDatabases stdClass of supported databases and versions + * @param object $supportedDatabases stdClass of supported databases and versions * * @return boolean * @@ -123,8 +123,8 @@ protected function checkPhpMinimum(string $phpMinimum) */ protected function checkSupportedDatabases(\stdClass $supportedDatabases) { - $db = Factory::getDbo(); - $dbType = strtolower($db->getServerType()); + $db = Factory::getDbo(); + $dbType = strtolower($db->getServerType()); $dbVersion = $db->getVersion(); // MySQL and MariaDB use the same database driver but not the same version numbers @@ -133,7 +133,7 @@ protected function checkSupportedDatabases(\stdClass $supportedDatabases) if (stripos($dbVersion, 'mariadb') !== false) { // MariaDB: Strip off any leading '5.5.5-', if present $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); - $dbType = 'mariadb'; + $dbType = 'mariadb'; } } @@ -150,7 +150,7 @@ protected function checkSupportedDatabases(\stdClass $supportedDatabases) /** * Check the stability * - * @param string $stability Stability to check + * @param string $stability Stability to check * * @return boolean * @@ -173,7 +173,7 @@ protected function checkStability(string $stability) * 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 + * @param string $tag The tag string, e.g. dev, alpha, beta, rc, stable * * @return integer * diff --git a/libraries/src/Updater/Update/AbstractUpdate.php b/libraries/src/Updater/Update/AbstractUpdate.php new file mode 100644 index 00000000000..0b93c1d5d19 --- /dev/null +++ b/libraries/src/Updater/Update/AbstractUpdate.php @@ -0,0 +1,193 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater\Update; + +use Joomla\CMS\Object\CMSObject; +use Joomla\CMS\Updater\DownloadSource; +use Joomla\CMS\Updater\Updater; + +\defined('JPATH_PLATFORM') or die; + +abstract class AbstractUpdate extends CMSObject implements UpdateInterface +{ + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + public $name; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $description; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $element; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $type; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $version; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $infourl; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $client; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $group; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $downloads; + + /** + * Update manifest `` elements + * + * @var DownloadSource[] + * @since 3.8.3 + */ + protected $downloadSources = array(); + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $tags; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $maintainer; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $maintainerurl; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $category; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $relationships; + + /** + * Update manifest `` element + * + * @var string + * @since 1.7.0 + */ + protected $targetplatform; + + /** + * Extra query for download URLs + * + * @var string + * @since 3.2.0 + */ + protected $extra_query; + + /** + * Object containing the current update data + * + * @var \stdClass + * @since 3.0.0 + */ + protected $currentUpdate; + + /** + * Object containing the latest update data + * + * @var \stdClass + * @since 3.0.0 + */ + protected $latest; + + /** + * The minimum stability required for updates to be taken into account. The possible values are: + * 0 dev Development snapshots, nightly builds, pre-release versions and so on + * 1 alpha Alpha versions (work in progress, things are likely to be broken) + * 2 beta Beta versions (major functionality in place, show-stopper bugs are likely to be present) + * 3 rc Release Candidate versions (almost stable, minor bugs might be present) + * 4 stable Stable versions (production quality code) + * + * @var integer + * @since 14.1 + * + * @see Updater + */ + protected $minimum_stability = Updater::STABILITY_STABLE; + + /** + * Array with compatible versions used by the pre-update check + * + * @var array + * @since 3.10.2 + */ + protected $compatibleVersions = array(); +} diff --git a/libraries/src/Updater/Update/DataUpdate.php b/libraries/src/Updater/Update/DataUpdate.php new file mode 100644 index 00000000000..64dea6792bb --- /dev/null +++ b/libraries/src/Updater/Update/DataUpdate.php @@ -0,0 +1,96 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater\Update; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Http\HttpFactory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Log\Log; +use Joomla\CMS\Updater\DownloadSource; +use Joomla\CMS\Updater\Updater; +use Joomla\CMS\Version; +use Joomla\Registry\Registry; + +/** + * Update class. It is used by Updater::update() to install an update. Use Updater::findUpdates() to find updates for + * an extension. + * + * @since 1.7.0 + */ +class DataUpdate extends AbstractUpdate +{ + /** + * Object for holding for data + * + * @var resource + * @since 3.0.0 + */ + protected $data; + + /** + * Loads an XML file from a URL. + * + * @param mixed $updateObject The object of the update containing all information. + * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public function loadFromData($updateObject, $minimumStability = Updater::STABILITY_STABLE) + { + foreach ($updateObject as $key => $data) { + $this->$key = $updateObject->$key; + } + + $dataJson = json_decode($updateObject->data); + $this->targetplatform = $dataJson->targetplatform; + + foreach ($dataJson->downloads as $download) { + $source = new DownloadSource; + foreach ($download as $key => $data) { + $key = strtolower($key); + $source->$key = $data; + } + $this->downloadSources[] = $source; + } + + $this->currentUpdate = new \stdClass(); + $this->downloadurl = new \stdClass(); + $this->downloadurl->_data = $this->downloadSources[0]->url; + $this->downloadurl->format = $this->downloadSources[0]->format; + $this->downloadurl->type = $this->downloadSources[0]->type; + + 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 3.4 + */ + protected function stabilityTagToInteger($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/UpdateInterface.php b/libraries/src/Updater/Update/UpdateInterface.php new file mode 100644 index 00000000000..38d654dbae8 --- /dev/null +++ b/libraries/src/Updater/Update/UpdateInterface.php @@ -0,0 +1,16 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater\Update; + +\defined('JPATH_PLATFORM') or die; + +interface UpdateInterface +{ + +} diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update/XmlUpdate.php similarity index 71% rename from libraries/src/Updater/Update.php rename to libraries/src/Updater/Update/XmlUpdate.php index c2999b1bb1e..e4e1ea6eacb 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update/XmlUpdate.php @@ -1,20 +1,20 @@ + * @copyright (C) 2022 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\CMS\Updater; +namespace Joomla\CMS\Updater\Update; use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; -use Joomla\CMS\Object\CMSObject; +use Joomla\CMS\Updater\DownloadSource; +use Joomla\CMS\Updater\Updater; use Joomla\CMS\Version; use Joomla\Registry\Registry; @@ -28,144 +28,8 @@ * * @since 1.7.0 */ -class Update extends CMSObject +class XmlUpdate extends AbstractUpdate { - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $name; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $description; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $element; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $type; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $version; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $infourl; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $client; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $group; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $downloads; - - /** - * Update manifest `` elements - * - * @var DownloadSource[] - * @since 3.8.3 - */ - protected $downloadSources = array(); - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $tags; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $maintainer; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $maintainerurl; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $category; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $relationships; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $targetplatform; - - /** - * Extra query for download URLs - * - * @var string - * @since 3.2.0 - */ - protected $extra_query; - /** * Resource handle for the XML Parser * @@ -256,9 +120,9 @@ protected function _getLastTag() /** * XML Start Element callback * - * @param object $parser Parser object - * @param string $name Name of the tag found - * @param array $attrs Attributes of the tag + * @param object $parser Parser object + * @param string $name Name of the tag found + * @param array $attrs Attributes of the tag * * @return void * @@ -268,7 +132,7 @@ protected function _getLastTag() public function _startElement($parser, $name, $attrs = array()) { $this->stack[] = $name; - $tag = $this->_getStackLocation(); + $tag = $this->_getStackLocation(); // Reset the data if (isset($this->$tag)) { @@ -278,12 +142,12 @@ public function _startElement($parser, $name, $attrs = array()) switch ($name) { // This is a new update; create a current update case 'UPDATE': - $this->currentUpdate = new \stdClass(); + $this->currentUpdate = new \stdClass; break; // Handle the array of download sources case 'DOWNLOADSOURCE': - $source = new DownloadSource(); + $source = new DownloadSource; foreach ($attrs as $key => $data) { $key = strtolower($key); @@ -303,7 +167,7 @@ public function _startElement($parser, $name, $attrs = array()) $name = strtolower($name); if (!isset($this->currentUpdate->$name)) { - $this->currentUpdate->$name = new \stdClass(); + $this->currentUpdate->$name = new \stdClass; } $this->currentUpdate->$name->_data = ''; @@ -319,8 +183,8 @@ public function _startElement($parser, $name, $attrs = array()) /** * Callback for closing the element * - * @param object $parser Parser object - * @param string $name Name of element that was closed + * @param object $parser Parser object + * @param string $name Name of element that was closed * * @return void * @@ -337,11 +201,9 @@ public function _endElement($parser, $name) $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); // Check that the product matches and that the version matches (optionally a regexp) - if ( - isset($this->currentUpdate->targetplatform->name) + if (isset($this->currentUpdate->targetplatform->name) && $product == $this->currentUpdate->targetplatform->name - && preg_match('/^' . $this->currentUpdate->targetplatform->version . '/', $this->get('jversion.full', JVERSION)) - ) { + && preg_match('/^' . $this->currentUpdate->targetplatform->version . '/', $this->get('jversion.full', JVERSION))) { $phpMatch = false; // Check if PHP version supported via tag, assume true if tag isn't present @@ -353,9 +215,9 @@ public function _endElement($parser, $name) // Check if DB & version is supported via tag, assume supported if tag isn't present if (isset($this->currentUpdate->supported_databases)) { - $db = Factory::getDbo(); - $dbType = strtolower($db->getServerType()); - $dbVersion = $db->getVersion(); + $db = Factory::getDbo(); + $dbType = strtolower($db->getServerType()); + $dbVersion = $db->getVersion(); $supportedDbs = $this->currentUpdate->supported_databases; // MySQL and MariaDB use the same database driver but not the same version numbers @@ -364,14 +226,14 @@ public function _endElement($parser, $name) if (stripos($dbVersion, 'mariadb') !== false) { // MariaDB: Strip off any leading '5.5.5-', if present $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); - $dbType = 'mariadb'; + $dbType = 'mariadb'; } } // Do we have an entry for the database? if (isset($supportedDbs->$dbType)) { $minimumVersion = $supportedDbs->$dbType; - $dbMatch = version_compare($dbVersion, $minimumVersion, '>='); + $dbMatch = version_compare($dbVersion, $minimumVersion, '>='); } } else { // Set to true if the tag is not set @@ -390,10 +252,8 @@ public function _endElement($parser, $name) $this->compatibleVersions[] = $this->currentUpdate->version->_data; } - if ( - !isset($this->latest) - || version_compare($this->currentUpdate->version->_data, $this->latest->version->_data, '>') - ) { + if (!isset($this->latest) + || version_compare($this->currentUpdate->version->_data, $this->latest->version->_data, '>')) { $this->latest = $this->currentUpdate; } } @@ -419,8 +279,8 @@ public function _endElement($parser, $name) /** * Character Parser Function * - * @param object $parser Parser object. - * @param object $data The data. + * @param object $parser Parser object. + * @param object $data The data. * * @return void * @@ -435,7 +295,7 @@ public function _characterData($parser, $data) $tag = strtolower($tag); if ($tag === 'tag') { - $this->currentUpdate->stability = $this->stabilityTagToInteger((string) $data); + $this->currentUpdate->stability = $this->stabilityTagToInteger((string)$data); return; } @@ -456,8 +316,8 @@ public function _characterData($parser, $data) /** * Loads an XML file from a URL. * - * @param string $url The URL. - * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} + * @param string $url The URL. + * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} * * @return boolean True on success * @@ -465,8 +325,8 @@ public function _characterData($parser, $data) */ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE) { - $version = new Version(); - $httpOption = new Registry(); + $version = new Version; + $httpOption = new Registry; $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); try { @@ -493,12 +353,10 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE) if (!xml_parse($this->xmlParser, $response->body)) { Log::add( sprintf( - 'XML error: %s at line %d', - xml_error_string(xml_get_error_code($this->xmlParser)), + 'XML error: %s at line %d', xml_error_string(xml_get_error_code($this->xmlParser)), xml_get_current_line_number($this->xmlParser) ), - Log::WARNING, - 'updater' + Log::WARNING, 'updater' ); return false; @@ -513,7 +371,7 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE) * 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 + * @param string $tag The tag string, e.g. dev, alpha, beta, rc, stable * * @return integer * diff --git a/libraries/src/Updater/UpdateAdapter.php b/libraries/src/Updater/UpdateAdapter.php index 310e0ba88b6..a5db109b95e 100644 --- a/libraries/src/Updater/UpdateAdapter.php +++ b/libraries/src/Updater/UpdateAdapter.php @@ -124,7 +124,7 @@ protected function _getLastTag() /** * Finds an update * - * @param array $options Options to use: update_site_id: the unique ID of the update site to look at + * @param array $options Options to use: update_site_id: the unique ID of the update site to look at * * @return array Update_sites and updates discovered * @@ -137,15 +137,15 @@ abstract public function findUpdate($options); * from their URL and enabled afterwards. If the URL fetch fails with a PHP fatal error (e.g. timeout) the faulty * update site will remain disabled the next time we attempt to load the update information. * - * @param int $updateSiteId The numeric ID of the update site to enable/disable - * @param bool $enabled Enable the site when true, disable it when false + * @param int $updateSiteId The numeric ID of the update site to enable/disable + * @param bool $enabled Enable the site when true, disable it when false * * @return void */ protected function toggleUpdateSite($updateSiteId, $enabled = true) { - $updateSiteId = (int) $updateSiteId; - $enabled = (bool) $enabled ? 1 : 0; + $updateSiteId = (int)$updateSiteId; + $enabled = (bool)$enabled ? 1 : 0; if (empty($updateSiteId)) { return; @@ -170,13 +170,13 @@ protected function toggleUpdateSite($updateSiteId, $enabled = true) /** * Get the name of an update site. This is used in logging. * - * @param int $updateSiteId The numeric ID of the update site + * @param int $updateSiteId The numeric ID of the update site * * @return string The name of the update site or an empty string if it's not found */ protected function getUpdateSiteName($updateSiteId) { - $updateSiteId = (int) $updateSiteId; + $updateSiteId = (int)$updateSiteId; if (empty($updateSiteId)) { return ''; @@ -204,7 +204,7 @@ protected function getUpdateSiteName($updateSiteId) /** * Try to get the raw HTTP response from the update site, hopefully containing the update XML. * - * @param array $options The update options, see findUpdate() in children classes + * @param array $options The update options, see findUpdate() in children classes * * @return \Joomla\CMS\Http\Response|bool False if we can't connect to the site, HTTP Response object otherwise * @@ -220,7 +220,7 @@ protected function getUpdateSiteResponse($options = array()) $options['update_site_name'] = $this->getUpdateSiteName($this->updateSiteId); } - $this->updateSiteName = $options['update_site_name']; + $this->updateSiteName = $options['update_site_name']; $this->appendExtension = false; if (\array_key_exists('append_extension', $options)) { @@ -241,7 +241,7 @@ protected function getUpdateSiteResponse($options = array()) $startTime = microtime(true); - $version = new Version(); + $version = new Version(); $httpOption = new Registry(); $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); @@ -257,7 +257,7 @@ protected function getUpdateSiteResponse($options = array()) $this->toggleUpdateSite($this->updateSiteId, true); // Log the time it took to load this update site's information - $endTime = microtime(true); + $endTime = microtime(true); $timeToLoad = sprintf('%0.2f', $endTime - $startTime); Log::add( "Loading information from update site #{$this->updateSiteId} with name " . diff --git a/libraries/src/Updater/Updater.php b/libraries/src/Updater/Updater.php index 613dbcfc36d..680e96aebcb 100644 --- a/libraries/src/Updater/Updater.php +++ b/libraries/src/Updater/Updater.php @@ -76,9 +76,9 @@ class Updater extends Adapter /** * Constructor * - * @param string $basepath Base Path of the adapters - * @param string $classprefix Class prefix of adapters - * @param string $adapterfolder Name of folder to append to base path + * @param string $basepath Base Path of the adapters + * @param string $classprefix Class prefix of adapters + * @param string $adapterfolder Name of folder to append to base path * * @since 3.1 */ @@ -107,13 +107,13 @@ public static function getInstance() /** * Finds the update for an extension. Any discovered updates are stored in the #__updates table. * - * @param int|array $eid Extension Identifier or list of Extension Identifiers; if zero use all + * @param int|array $eid Extension Identifier or list of Extension Identifiers; if zero use all * sites - * @param integer $cacheTimeout How many seconds to cache update information; if zero, force reload the + * @param integer $cacheTimeout How many seconds to cache update information; if zero, force reload the * update information - * @param integer $minimumStability Minimum stability for the updates; 0=dev, 1=alpha, 2=beta, 3=rc, + * @param integer $minimumStability Minimum stability for the updates; 0=dev, 1=alpha, 2=beta, 3=rc, * 4=stable - * @param boolean $includeCurrent Should I include the current version in the results? + * @param boolean $includeCurrent Should I include the current version in the results? * * @return boolean True if there are updates * @@ -129,8 +129,8 @@ public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = sel return $retval; } - $now = time(); - $earliestTime = $now - $cacheTimeout; + $now = time(); + $earliestTime = $now - $cacheTimeout; $sitesWithUpdates = array(); if ($cacheTimeout > 0) { @@ -185,7 +185,7 @@ public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = sel * Returns the update site records for an extension with ID $eid. If $eid is zero all enabled update sites records * will be returned. * - * @param int $eid The extension ID to fetch. + * @param int $eid The extension ID to fetch. * * @return array * @@ -193,7 +193,7 @@ public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = sel */ private function getUpdateSites($eid = 0) { - $db = $this->getDbo(); + $db = $this->getDbo(); $query = $db->getQuery(true); $query->select( @@ -217,7 +217,7 @@ private function getUpdateSites($eid = 0) if (\is_array($eid)) { $query->whereIn($db->quoteName('b.extension_id'), $eid); - } elseif ($eid = (int) $eid) { + } elseif ($eid = (int)$eid) { $query->where($db->quoteName('b.extension_id') . ' = :eid') ->bind(':eid', $eid, ParameterType::INTEGER); } @@ -237,9 +237,9 @@ private function getUpdateSites($eid = 0) /** * Loads the contents of an update site record $updateSite and returns the update objects * - * @param array $updateSite The update site record to process - * @param int $minimumStability Minimum stability for the returned update records - * @param bool $includeCurrent Should I also include the current version? + * @param array $updateSite The update site record to process + * @param int $minimumStability Minimum stability for the returned update records + * @param bool $includeCurrent Should I also include the current version? * * @return array The update records. Empty array if no updates are found. * @@ -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. @@ -270,11 +270,11 @@ private function getUpdateObjectsForSite($updateSite, $minimumStability = self:: // If we have additional update sites in the remote (collection) update XML document, parse them if (\array_key_exists('update_sites', $update_result) && \count($update_result['update_sites'])) { $thisUrl = trim($updateSite['location']); - $thisId = (int) $updateSite['update_site_id']; + $thisId = (int)$updateSite['update_site_id']; foreach ($update_result['update_sites'] as $extraUpdateSite) { $extraUrl = trim($extraUpdateSite['location']); - $extraId = (int) $extraUpdateSite['update_site_id']; + $extraId = (int)$extraUpdateSite['update_site_id']; // Do not try to fetch the same update site twice if (($thisId == $extraId) || ($thisUrl == $extraUrl)) { @@ -303,20 +303,20 @@ private function getUpdateObjectsForSite($updateSite, $minimumStability = self:: $uid = $update ->find( array( - 'element' => $current_update->get('element'), - 'type' => $current_update->get('type'), + 'element' => $current_update->get('element'), + 'type' => $current_update->get('type'), 'client_id' => $current_update->get('client_id'), - 'folder' => $current_update->get('folder'), + 'folder' => $current_update->get('folder'), ) ); $eid = $extension ->find( array( - 'element' => $current_update->get('element'), - 'type' => $current_update->get('type'), + 'element' => $current_update->get('element'), + 'type' => $current_update->get('type'), 'client_id' => $current_update->get('client_id'), - 'folder' => $current_update->get('folder'), + 'folder' => $current_update->get('folder'), ) ); @@ -339,7 +339,7 @@ private function getUpdateObjectsForSite($updateSite, $minimumStability = self:: $update->load($uid); // We already have an update in the database lets check whether it has an extension_id - if ((int) $update->extension_id === 0 && $eid) { + if ((int)$update->extension_id === 0 && $eid) { // The current update does not have an extension_id but we found one. Let's use it. $current_update->extension_id = $eid; } @@ -359,7 +359,7 @@ private function getUpdateObjectsForSite($updateSite, $minimumStability = self:: /** * Returns the IDs of the update sites with cached updates * - * @param int $timestamp Optional. If set, only update sites checked before $timestamp will be taken into + * @param int $timestamp Optional. If set, only update sites checked before $timestamp will be taken into * account. * * @return array The IDs of the update sites with cached updates @@ -368,8 +368,8 @@ private function getUpdateObjectsForSite($updateSite, $minimumStability = self:: */ private function getSitesWithUpdates($timestamp = 0) { - $db = Factory::getDbo(); - $timestamp = (int) $timestamp; + $db = Factory::getDbo(); + $timestamp = (int)$timestamp; $query = $db->getQuery(true) ->select('DISTINCT ' . $db->quoteName('update_site_id')) @@ -403,7 +403,7 @@ private function getSitesWithUpdates($timestamp = 0) /** * Update the last check timestamp of an update site * - * @param int $updateSiteId The update site ID to mark as just checked + * @param int $updateSiteId The update site ID to mark as just checked * * @return void * @@ -411,9 +411,9 @@ private function getSitesWithUpdates($timestamp = 0) */ private function updateLastCheckTimestamp($updateSiteId) { - $timestamp = time(); - $db = Factory::getDbo(); - $updateSiteId = (int) $updateSiteId; + $timestamp = time(); + $db = Factory::getDbo(); + $updateSiteId = (int)$updateSiteId; $query = $db->getQuery(true) ->update($db->quoteName('#__update_sites')) From 86b583a7663d84452fad5e38d8a9faf11d306b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Fri, 30 Jun 2023 19:43:33 +0200 Subject: [PATCH 056/126] add file exists check in downloadPackage --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 07290a0f7b0..2714e78e638 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -461,8 +461,10 @@ protected function downloadPackage($url, $target) // Informational log only } - // Make sure the target does not exist. - File::delete($target); + // Make sure the target does not exist. + if (file_exists($target)) { + File::delete($target); + } // Download the package try { From 26d4cb8e0f9796d8366efcf57a5ef0140895ded1 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 30 Jun 2023 20:07:27 +0200 Subject: [PATCH 057/126] reset to main tuf repo --- composer.json | 4 +- composer.lock | 488 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 489 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index e57cb3ac615..0c7a9dfb754 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, { "type": "vcs", - "url": "https://github.com/joomla-projects/php-tuf.git", + "url": "https://github.com/php-tuf/php-tuf.git", "no-api": true } ], @@ -106,7 +106,7 @@ "phpseclib/bcmath_compat": "^2.0.1", "jfcherng/php-diff": "^6.15.3", "voku/portable-utf8": "^6.0.13", - "php-tuf/php-tuf": "dev-joomla-tuf-combined" + "php-tuf/php-tuf": "dev-main" }, "require-dev": { "phpunit/phpunit": "^9.6.9", diff --git a/composer.lock b/composer.lock index 2de46586bbe..f263533de7b 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": "400059c24d1a490e27493f7f6c7d8ae8", + "content-hash": "2459aec40ce83141f551ca7b58e612b5", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -651,6 +651,122 @@ }, "time": "2023-02-18T17:41:46+00:00" }, + { + "name": "guzzlehttp/psr7", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "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.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "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.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:11:26+00:00" + }, { "name": "jakeasmith/http_build_url", "version": "1.0.1", @@ -2957,6 +3073,80 @@ }, "time": "2023-04-30T00:54:53+00:00" }, + { + "name": "php-tuf/php-tuf", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/php-tuf/php-tuf.git", + "reference": "d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", + "reference": "d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", + "shasum": "" + }, + "require": { + "ext-json": "*", + "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": "2023-05-22T17:55:39+00:00" + }, { "name": "phpmailer/phpmailer", "version": "v6.8.0", @@ -3568,6 +3758,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", @@ -4595,6 +4829,83 @@ ], "time": "2022-11-03T14:55:06+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "508c652ba3ccf69f8c97f251534f229791b52a57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/508c652ba3ccf69f8c97f251534f229791b52a57", + "reference": "508c652ba3ccf69f8c97f251534f229791b52a57", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-php80": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + } + }, + "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.27.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": "2022-11-03T14:55:06+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.27.0", @@ -4846,6 +5157,84 @@ ], "time": "2023-03-21T21:06:29+00:00" }, + { + "name": "symfony/translation-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "02c24deb352fb0d79db5486c0c79905a85e37e86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/02c24deb352fb0d79db5486c0c79905a85e37e86", + "reference": "02c24deb352fb0d79db5486c0c79905a85e37e86", + "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.3.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-05-30T17:17:10+00:00" + }, { "name": "symfony/uid", "version": "v6.3.0", @@ -4920,6 +5309,102 @@ ], "time": "2023-04-08T07:25:02+00:00" }, + { + "name": "symfony/validator", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "1b71f43c62ee867ab08195ba6039a1bc3e6654dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/1b71f43c62ee867ab08195ba6039a1bc3e6654dc", + "reference": "1b71f43c62ee867ab08195ba6039a1bc3e6654dc", + "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", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.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.3.1" + }, + "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-06-21T12:08:28+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.3.0", @@ -10118,6 +10603,7 @@ "joomla/uri": 20, "joomla/utilities": 20, "tobscure/json-api": 20, + "php-tuf/php-tuf": 20, "joomla/mediawiki": 20, "joomla/test": 20 }, From 63deea89bec5b29eda571155e298739b2d3240cb Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 30 Jun 2023 20:44:45 +0200 Subject: [PATCH 058/126] WIp commit of compatible interfaces --- libraries/src/TUF/DatabaseStorage.php | 103 ++-------- .../TUF/Exception/RoleNotFoundException.php | 21 -- libraries/src/TUF/HttpFileFetcher.php | 187 ------------------ libraries/src/TUF/HttpLoader.php | 31 +++ libraries/src/TUF/TufValidation.php | 7 +- 5 files changed, 49 insertions(+), 300 deletions(-) delete mode 100644 libraries/src/TUF/Exception/RoleNotFoundException.php delete mode 100644 libraries/src/TUF/HttpFileFetcher.php create mode 100644 libraries/src/TUF/HttpLoader.php diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 67c9cad2168..f2d4fcd55f0 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -10,15 +10,15 @@ use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Tuf; -use Joomla\CMS\TUF\Exception\RoleNotFoundException; use Joomla\Database\DatabaseDriver; +use Tuf\Metadata\StorageBase; \defined('JPATH_PLATFORM') or die; /** * @since __DEPLOY_VERSION__ */ -class DatabaseStorage implements \ArrayAccess +class DatabaseStorage extends StorageBase { /** * The Tuf table object @@ -38,104 +38,29 @@ public function __construct(DatabaseDriver $db, int $extensionId) $this->table = new Tuf($db); $this->table->load(['extension_id' => $extensionId]); - } - - /** - * Check if an offset/table column exists - * - * @param mixed $offset The offset/database column to check for - * - * @return boolean - */ - public function offsetExists($offset): bool - { - $column = $this->getCleanColumn($offset); - - return substr($column, -5) === '_json' && $this->table->hasField($column) && !is_null($this->table->$column); - } - - /** - * Check if an offset/table column exists - * - * @param mixed $offset The offset/database column to check for - * - * @return boolean - */ - public function tableColumnExists($offset): bool - { - $column = $this->getCleanColumn($offset); - return substr($column, -5) === '_json' && $this->table->hasField($column); - } + foreach (["root_json", "targets_json", "snapshot_json", "timestamp_json", "mirrors_json"] as $column) { + if ($this->table->$column === null) { + continue; + } - /** - * Get the value of a table column - * - * @param mixed $offset The column name to get the value for - * - * @return mixed - */ - public function offsetGet($offset) - { - if (!$this->offsetExists($offset)) { - throw new RoleNotFoundException(); + $this->write(explode("_", $column, 2)[0], $this->table->$column); } - - $column = $this->getCleanColumn($offset); - - return $this->table->$column; } - /** - * Set a value in a column - * - * @param [type] $offset The table column to set the value - * @param [type] $value The value to set - * - * @return void - */ - public function offsetSet($offset, $value) - { - if (!$this->tableColumnExists($offset)) { - throw new RoleNotFoundException(); - } - - $column = $this->getCleanColumn($offset); - - $this->table->$column = $value; - $this->table->store(); + public function read(string $name): ?string + { + return $this->container[$name] ?? null; } - /** - * Reset the value to a - * - * @param mixed $offset The table column to reset the value to null - * - * @return void - */ - public function offsetUnset($offset): void + public function write(string $name, string $data): void { - if (!$this->offsetExists($offset)) { - throw new RoleNotFoundException(); - } - - $column = $this->getCleanColumn($offset); - - $this->table->$column = null; - - $this->table->store(true); + $this->container[$name] = $data; } - /** - * Convert file names to table columns - * - * @param string $name The original file name - * - * @return string - */ - protected function getCleanColumn($name): string + public function delete(string $name): void { - return str_replace('.', '_', $name); + unset($this->container[$name]); } } diff --git a/libraries/src/TUF/Exception/RoleNotFoundException.php b/libraries/src/TUF/Exception/RoleNotFoundException.php deleted file mode 100644 index 1a279349d61..00000000000 --- a/libraries/src/TUF/Exception/RoleNotFoundException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\CMS\TUF\Exception; - -\defined('JPATH_PLATFORM') or die; - -/** - * Exception class defining that the Role could not be found - * - * @since __DEPLOY_VERSION__ - */ -class RoleNotFoundException extends \Exception -{ -} diff --git a/libraries/src/TUF/HttpFileFetcher.php b/libraries/src/TUF/HttpFileFetcher.php deleted file mode 100644 index fd7efafc9ba..00000000000 --- a/libraries/src/TUF/HttpFileFetcher.php +++ /dev/null @@ -1,187 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\CMS\TUF; - -use GuzzleHttp\Promise\FulfilledPromise; -use GuzzleHttp\Promise\PromiseInterface; -use Joomla\Http\Http; -use Joomla\Http\HttpFactory; -use Joomla\Http\Response; -use Tuf\Client\RepoFileFetcherInterface; -use Tuf\Exception\RepoFileNotFound; - -/** - * @since __DEPLOY_VERSION__ - */ -class HttpFileFetcher implements RepoFileFetcherInterface -{ - /** - * The HTTP client. - * - * @var \Joomla\Http\Http - * - * @since __DEPLOY_VERSION__ - */ - private $client; - - /** - * The base URI for requests - * - * @var string|null - * - * @since __DEPLOY_VERSION__ - */ - private $baseUri; - - /** - * The path prefix for metadata. - * - * @var string|null - * - * @since __DEPLOY_VERSION__ - */ - private $metadataPrefix; - - /** - * The path prefix for targets. - * - * @var string|null - * - * @since __DEPLOY_VERSION__ - */ - private $targetsPrefix; - - /** - * JHttpFileFetcher constructor. - * - * @param \Joomla\Http\Http $client The HTTP client. - * @param string $metadataPrefix The path prefix for metadata. - * @param string $targetsPrefix The path prefix for targets. - * @param string $baseUri Repo base uri for requests - * - * @since __DEPLOY_VERSION__ - */ - public function __construct(Http $client, string $metadataPrefix, string $targetsPrefix, string $baseUri) - { - $this->client = $client; - $this->metadataPrefix = $metadataPrefix; - $this->targetsPrefix = $targetsPrefix; - $this->baseUri = $baseUri; - } - - /** - * Creates an instance of this class with a specific base URI. - * - * @param string $baseUri The base URI from which to fetch files. - * @param string $metadataPrefix (optional) The path prefix for metadata. Defaults to '/metadata/'. - * @param string $targetsPrefix (optional) The path prefix for targets. Defaults to '/targets/'. - * - * @return static A new instance of this class. - * - * @since __DEPLOY_VERSION__ - */ - public static function createFromUri( - string $baseUri, - string $metadataPrefix = '/metadata/', - string $targetsPrefix = '/targets/' - ): self - { - $httpFactory = new HttpFactory(); - $client = $httpFactory->getHttp([], 'curl'); - - return new static($client, $metadataPrefix, $targetsPrefix, $baseUri); - } - - /** - * Fetches a metadata file from the remote repo. - * - * @param string $fileName The name of the metadata file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * - * @return \GuzzleHttp\Promise\PromiseInterface A promise wrapping a StreamInterface instanfe - */ - public function fetchMetadata(string $fileName, int $maxBytes): PromiseInterface - { - return $this->fetchFile($this->baseUri . $this->metadataPrefix . $fileName, $maxBytes); - } - - /** - * Fetches a target file from the remote repo. - * - * @param string $fileName The name of the target to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * @param array $options (optional) Additional request options to pass to the http client - * @param string $url An arbitrary URL from which the target should be downloaded. - * - * @return PromiseInterface - * - * @since __DEPLOY_VERSION__ - */ - public function fetchTarget( - string $fileName, - int $maxBytes, - array $options = [], - string $url = null - ): PromiseInterface - { - $location = $url ?: $this->baseUri . $this->targetsPrefix . $fileName; - - return $this->fetchFile($location, $maxBytes, $options); - } - - /** - * Fetches a file from a URL. - * - * @param string $url The URL of the file to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * @param array $options Additional request options to pass to the http client - * - * @return PromiseInterface A promise representing the eventual result of the operation. - * - * @since __DEPLOY_VERSION__ - */ - protected function fetchFile(string $url, int $maxBytes, array $options = []): PromiseInterface - { - $headers = (!empty($options['headers'])) ? $options['headers'] : []; - - /** @var Response $response */ - $response = $this->client->get($url, $headers); - $response->getBody()->rewind(); - - if ($response->getStatusCode() === 404) { - throw new RepoFileNotFound(); - } - - if ($response->getStatusCode() !== 200) { - throw new \RuntimeException( - "Invalid TUF repo response: " . $response->getBody()->getContents(), - $response->getStatusCode() - ); - } - - return new FulfilledPromise($response->getBody()); - } - - /** - * Gets a file if it exists in the remote repo. - * - * @param string $fileName The file name to fetch. - * @param integer $maxBytes The maximum number of bytes to download. - * - * @return string|null The contents of the file or null if it does not exist. - */ - public function fetchMetadataIfExists(string $fileName, int $maxBytes): ?string - { - try { - return $this->fetchMetadata($fileName, $maxBytes)->wait(); - } catch (RepoFileNotFound $exception) { - return null; - } - } -} diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php new file mode 100644 index 00000000000..8299edb9112 --- /dev/null +++ b/libraries/src/TUF/HttpLoader.php @@ -0,0 +1,31 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\TUF; + +use Joomla\Http\HttpFactory; +use Joomla\Http\Http; +use Psr\Http\Message\StreamInterface; +use Tuf\Loader\LoaderInterface; + +class HttpLoader implements LoaderInterface +{ + public function load(string $locator, int $maxBytes): StreamInterface + { + $httpFactory = new HttpFactory(); + + /** @var Http $client */ + $client = $httpFactory->getHttp([], 'curl'); + $response = $client->get($locator); + + // Rewind to start + $response->getBody()->rewind(); + + return $response->getBody(); + } +} diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index c5d76550f08..9c5bec9b24f 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -20,6 +20,7 @@ use Tuf\Exception\Attack\RollbackAttackException; use Tuf\Exception\Attack\SignatureThresholdException; use Tuf\Exception\MetadataException; +use Tuf\Loader\SizeCheckingLoader; \defined('JPATH_PLATFORM') or die; @@ -102,13 +103,13 @@ public function getValidUpdate() { $db = Factory::getContainer()->get(DatabaseDriver::class); - $fileFetcher = HttpFileFetcher::createFromUri($this->params['url_prefix'], $this->params['metadata_path'], $this->params['targets_path']); + $httpLoader = new HttpLoader(); + $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); $storage = new DatabaseStorage($db, $this->extensionId); $updater = new Updater( - $fileFetcher, - $this->params['mirrors'], + $sizeCheckingLoader, $storage ); From a6baf3f7f6bd4760841af565e1d7fbe59b06c158 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 30 Jun 2023 21:49:27 +0200 Subject: [PATCH 059/126] hotfix data fetching --- libraries/src/TUF/HttpLoader.php | 11 ++++++++++- libraries/src/TUF/TufValidation.php | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php index 8299edb9112..aefd5b61cd3 100644 --- a/libraries/src/TUF/HttpLoader.php +++ b/libraries/src/TUF/HttpLoader.php @@ -11,17 +11,26 @@ use Joomla\Http\HttpFactory; use Joomla\Http\Http; use Psr\Http\Message\StreamInterface; +use Tuf\Exception\RepoFileNotFound; use Tuf\Loader\LoaderInterface; class HttpLoader implements LoaderInterface { + public function __construct(public string $basePath) + { + } + public function load(string $locator, int $maxBytes): StreamInterface { $httpFactory = new HttpFactory(); /** @var Http $client */ $client = $httpFactory->getHttp([], 'curl'); - $response = $client->get($locator); + $response = $client->get($this->basePath . $locator); + + if ($response->code === 404) { + throw new RepoFileNotFound(); + } // Rewind to start $response->getBody()->rewind(); diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 9c5bec9b24f..0dc9db1c749 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -103,7 +103,7 @@ public function getValidUpdate() { $db = Factory::getContainer()->get(DatabaseDriver::class); - $httpLoader = new HttpLoader(); + $httpLoader = new HttpLoader("https://raw.githubusercontent.com/joomla/updates/test8/repository/"); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); $storage = new DatabaseStorage($db, $this->extensionId); From 17085454087ad9a86fde526331997b1f15167b7d Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 1 Jul 2023 10:21:15 +0200 Subject: [PATCH 060/126] add additional catch case --- administrator/language/en-GB/lib_joomla.ini | 1 + libraries/src/TUF/HttpLoader.php | 8 ++++---- libraries/src/TUF/TufValidation.php | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index 0ad258e66ae..a4112acedae 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -660,6 +660,7 @@ JLIB_INSTALLER_SQL_END_NOT_COMPLETE="End of SQL updates - INCOMPLETE." JLIB_INSTALLER_TUF_FREEZE_ATTACK="Update not possible because the offered update is expired." 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 message did not match the expected size" JLIB_INSTALLER_TUF_ROLLBACK_ATTACK="Update not possible because the offered update version is older than the current installed version." JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD="Update not possible because the offered update has not enough signatures." JLIB_INSTALLER_UNINSTALL="Uninstall" diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php index aefd5b61cd3..613fc7e62a7 100644 --- a/libraries/src/TUF/HttpLoader.php +++ b/libraries/src/TUF/HttpLoader.php @@ -9,14 +9,13 @@ namespace Joomla\CMS\TUF; use Joomla\Http\HttpFactory; -use Joomla\Http\Http; use Psr\Http\Message\StreamInterface; use Tuf\Exception\RepoFileNotFound; use Tuf\Loader\LoaderInterface; class HttpLoader implements LoaderInterface { - public function __construct(public string $basePath) + public function __construct(private readonly string $repositoryPath) { } @@ -24,9 +23,9 @@ public function load(string $locator, int $maxBytes): StreamInterface { $httpFactory = new HttpFactory(); - /** @var Http $client */ + // Get client instance $client = $httpFactory->getHttp([], 'curl'); - $response = $client->get($this->basePath . $locator); + $response = $client->get($this->repositoryPath . $locator); if ($response->code === 404) { throw new RepoFileNotFound(); @@ -35,6 +34,7 @@ public function load(string $locator, int $maxBytes): StreamInterface // Rewind to start $response->getBody()->rewind(); + // Return reponse return $response->getBody(); } } diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 0dc9db1c749..12f79d4debd 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -10,7 +10,6 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; -use Joomla\CMS\TUF\HttpFileFetcher; use Joomla\Database\DatabaseDriver; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; @@ -19,6 +18,7 @@ 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; @@ -119,6 +119,10 @@ public function getValidUpdate() $updater->refresh(); return $storage['targets.json']; + } catch (DownloadSizeException $e) { + $this->rollBackTufMetadata(); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); + return null; } catch (MetadataException $e) { $this->rollBackTufMetadata(); Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_INVALID_METADATA'), 'error'); From a3f674983ce8a820b2c4a2420d9fb10c0721f2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sat, 1 Jul 2023 10:46:07 +0200 Subject: [PATCH 061/126] Add TUF Debug Message --- administrator/language/en-GB/lib_joomla.ini | 1 + libraries/src/TUF/TufValidation.php | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index a4112acedae..f7acbe53d7e 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -658,6 +658,7 @@ 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 is 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 message did not match the expected size" diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 12f79d4debd..b0205f1af8f 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -11,6 +11,8 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\Database\DatabaseDriver; +use PHPUnit\Exception; +use PHPUnit\TextUI\RuntimeException; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -114,11 +116,19 @@ public function getValidUpdate() ); 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(); - - return $storage['targets.json']; + 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(); + + return $storage['targets.json']; + } catch (\Exception $e) { + if (JDEBUG && $message = $e->getMessage()) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_INSTALLER_TUF_DEBUG_MESSAGE', $message), 'error'); + } + throw $e; + } } catch (DownloadSizeException $e) { $this->rollBackTufMetadata(); Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); From f009732ce2245d5b3ac727c77333430189bf187a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Nu=CC=88bel?= Date: Sat, 1 Jul 2023 10:48:10 +0200 Subject: [PATCH 062/126] Remove PHPUnit methods --- libraries/src/TUF/TufValidation.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index b0205f1af8f..e7b13b96623 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -11,8 +11,6 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\Database\DatabaseDriver; -use PHPUnit\Exception; -use PHPUnit\TextUI\RuntimeException; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\OptionsResolver; From 695d8cab1837f1590be7024cf457e06ca4336996 Mon Sep 17 00:00:00 2001 From: Timo Feuerstein Date: Sat, 1 Jul 2023 12:08:11 +0200 Subject: [PATCH 063/126] Provide patch to drop patch level for php-tuf via composer-patches --- .../5.0.0-2023-07-01_php-tuf_meta.patch | 15 ++++++ composer.json | 14 +++++- composer.lock | 50 ++++++++++++++++++- 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch diff --git a/build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch b/build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch new file mode 100644 index 00000000000..df9726f2c92 --- /dev/null +++ b/build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch @@ -0,0 +1,15 @@ +# php-tuf/php-tuf - patch 20230701 +# TUF signed metadata should use semantic versioning 1.xx.xx while used TUF GO client drops patch level and uses +# 1.xx - therefor make patch level optional +# +--- a/src/Metadata/MetadataBase.php ++++ b/src/Metadata/MetadataBase.php (date 1688199141423) +@@ -179,7 +179,7 @@ + 'spec_version' => [ + new NotBlank(), + new Type('string'), +- new Regex('/^1\.[0-9]+\.[0-9]+$/'), ++ new Regex('/^1\.[0-9]+(\.[0-9]+)?$/'), + ], + ] + static::getVersionConstraints(), + 'allowExtraFields' => true, diff --git a/composer.json b/composer.json index 0c7a9dfb754..7531861a157 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "vendor-dir": "libraries/vendor", "github-protocols": ["https"], "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "cweagans/composer-patches": true } }, "support": { @@ -106,7 +107,8 @@ "phpseclib/bcmath_compat": "^2.0.1", "jfcherng/php-diff": "^6.15.3", "voku/portable-utf8": "^6.0.13", - "php-tuf/php-tuf": "dev-main" + "php-tuf/php-tuf": "dev-main", + "cweagans/composer-patches": "^1.7" }, "require-dev": { "phpunit/phpunit": "^9.6.9", @@ -127,6 +129,14 @@ "symfony/polyfill-php80": "*", "symfony/polyfill-php81": "*" }, + "extra": { + "composer-exit-on-patch-failure": true, + "patches": { + "php-tuf/php-tuf": { + "Patch level optional on TUF signed metadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch" + } + } + }, "scripts": { "post-install-cmd": [ "php build/update_fido_cache.php" diff --git a/composer.lock b/composer.lock index f263533de7b..6cc54c81450 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": "2459aec40ce83141f551ca7b58e612b5", + "content-hash": "775df2d98d02d59134eeabf1440bf522", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -193,6 +193,54 @@ ], "time": "2023-06-06T12:02:59+00:00" }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "time": "2022-12-20T22:53:13+00:00" + }, { "name": "defuse/php-encryption", "version": "v2.4.0", From 4656fb3439eed132036df23321bbffc38e0e7805 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 1 Jul 2023 15:43:28 +0200 Subject: [PATCH 064/126] adjust database storage handling --- .../sql/updates/mysql/4.2.0-2022-03-20.sql | 12 +++---- installation/sql/mysql/base.sql | 12 +++---- libraries/src/TUF/DatabaseStorage.php | 17 ++++++++-- libraries/src/TUF/TufValidation.php | 31 ++++++++++--------- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index ff16bbaef2d..ab6a60c4ba6 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -5,14 +5,14 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `root_json` text DEFAULT NULL, - `target_json` text DEFAULT NULL, - `snapshot_json` text DEFAULT NULL, - `timestamp_json` text DEFAULT NULL, - `mirrors_json` text DEFAULT NULL, + `root` text DEFAULT NULL, + `target` 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` (`extension_id`, `root_json`) +INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index f401289638d..4feb83ea555 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -862,18 +862,18 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, `extension_id` int DEFAULT 0, - `root_json` text DEFAULT NULL, - `targets_json` text DEFAULT NULL, - `snapshot_json` text DEFAULT NULL, - `timestamp_json` text DEFAULT NULL, - `mirrors_json` text DEFAULT NULL, + `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` (`extension_id`, `root_json`) +INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; -- -------------------------------------------------------- diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index f2d4fcd55f0..508f22b43f5 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -20,6 +20,8 @@ */ class DatabaseStorage extends StorageBase { + const METADATA_COLUMNS = ["root", "targets", "snapshot", "timestamp", "mirrors"]; + /** * The Tuf table object * @@ -39,12 +41,12 @@ public function __construct(DatabaseDriver $db, int $extensionId) $this->table->load(['extension_id' => $extensionId]); - foreach (["root_json", "targets_json", "snapshot_json", "timestamp_json", "mirrors_json"] as $column) { + foreach (self::METADATA_COLUMNS as $column) { if ($this->table->$column === null) { continue; } - $this->write(explode("_", $column, 2)[0], $this->table->$column); + $this->write($column, $this->table->$column); } } @@ -63,4 +65,15 @@ public function delete(string $name): void { unset($this->container[$name]); } + + public function persist(): bool + { + $data = []; + + foreach (self::METADATA_COLUMNS as $column) { + $data[$column] = $this->container[$column]; + } + + return $this->table->save($data); + } } diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index e7b13b96623..5967090fe59 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -103,7 +103,7 @@ public function getValidUpdate() { $db = Factory::getContainer()->get(DatabaseDriver::class); - $httpLoader = new HttpLoader("https://raw.githubusercontent.com/joomla/updates/test8/repository/"); + $httpLoader = new HttpLoader("https://raw.githubusercontent.com/joomla/updates/beta-target/repository/"); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); $storage = new DatabaseStorage($db, $this->extensionId); @@ -114,19 +114,22 @@ public function getValidUpdate() ); 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(); - - return $storage['targets.json']; - } catch (\Exception $e) { - if (JDEBUG && $message = $e->getMessage()) { - Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_INSTALLER_TUF_DEBUG_MESSAGE', $message), 'error'); - } - throw $e; - } + 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()) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_INSTALLER_TUF_DEBUG_MESSAGE', $message), 'error'); + } + throw $e; + } } catch (DownloadSizeException $e) { $this->rollBackTufMetadata(); Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); From 076ae0dcdf3d52f854d70700feee26d86fdf55b3 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 1 Jul 2023 15:57:21 +0200 Subject: [PATCH 065/126] fix notices --- libraries/src/TUF/DatabaseStorage.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 508f22b43f5..225c31a8f19 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -29,6 +29,8 @@ class DatabaseStorage extends StorageBase */ protected $table; + protected $container = array(); + /** * Initialize the DatabaseStorage class * @@ -71,6 +73,10 @@ public function persist(): bool $data = []; foreach (self::METADATA_COLUMNS as $column) { + if (!array_key_exists($column, $this->container)) { + continue; + } + $data[$column] = $this->container[$column]; } From 7ecf7113841883ed7f20e03da4491749fd4b5fba Mon Sep 17 00:00:00 2001 From: Timo Feuerstein Date: Sat, 1 Jul 2023 16:09:21 +0200 Subject: [PATCH 066/126] Patch for proper sorting of canonical json in php-tuf --- ...23-07-01_php-tuf_sort-canonical-json.patch | 68 +++++++++++++++++++ composer.json | 3 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch diff --git a/build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch b/build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch new file mode 100644 index 00000000000..7eb5458e065 --- /dev/null +++ b/build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch @@ -0,0 +1,68 @@ +# php-tuf/php-tuf - patch 20230701 +# Canonical json needs to be sorted in proper way +# +--- a/src/CanonicalJsonTrait.php ++++ b/src/CanonicalJsonTrait.php (date 1688219259891) +@@ -55,8 +55,14 @@ + */ + private static function sortKeys(array &$data): void + { ++ // Apply recursively on potential subarrays ++ foreach ($data as $key => $value) { ++ if (is_array($value)) { ++ static::sortKeys($data[$key]); ++ } ++ } + // If $data is numerically indexed, the keys are already sorted, by +- // definition. ++ // definition, no key sorting on this level necessary + if (array_is_list($data)) { + return; + } +@@ -65,10 +71,5 @@ + throw new \RuntimeException("Failure sorting keys. Canonicalization is not possible."); + } + +- foreach ($data as $key => $value) { +- if (is_array($value)) { +- static::sortKeys($data[$key]); +- } +- } + } + } + +--- a/tests/Unit/CanonicalJsonTraitTest.php ++++ b/tests/Unit/CanonicalJsonTraitTest.php (date 1688219312679) +@@ -55,6 +55,32 @@ + $this->assertSame([2, 3], array_keys($data['c'])); + } + ++ public function testSortForListArrays(): void ++ { ++ // Indexed arrays should not be sorted in alphabetical order, at any ++ // level. ++ $data = [ ++ // Use an associative nested array ++ 0 => [ ++ 'b' => 'Hey', ++ 'a' => 'Ho', ++ ], ++ // This should be sorted too, because PHP doesn't consider it a list. ++ 1 => [ ++ 3 => 'Hey', ++ 2 => 'Ho', ++ ], ++ ]; ++ ++ static::sortKeys($data); ++ ++ // The associative keys should be in canonical order now, and the ++ // nested, indexed array should be unchanged. ++ $this->assertSame([0,1], array_keys($data)); ++ $this->assertSame(['a', 'b'], array_keys($data['0'])); ++ $this->assertSame([2,3], array_keys($data[1])); ++ } ++ + /** + * @covers ::encodeJson + */ diff --git a/composer.json b/composer.json index 7531861a157..086d6d82dcc 100644 --- a/composer.json +++ b/composer.json @@ -133,7 +133,8 @@ "composer-exit-on-patch-failure": true, "patches": { "php-tuf/php-tuf": { - "Patch level optional on TUF signed metadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch" + "Patch level optional on TUF signed metadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch", + "Sort canonical json": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch" } } }, From 9c16fad3136a185b1233d015a886bf494475afe9 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 1 Jul 2023 17:07:50 +0200 Subject: [PATCH 067/126] write hashsum information from tuf into update object --- libraries/src/Updater/Update/DataUpdate.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/src/Updater/Update/DataUpdate.php b/libraries/src/Updater/Update/DataUpdate.php index 64dea6792bb..ae09b4c59ce 100644 --- a/libraries/src/Updater/Update/DataUpdate.php +++ b/libraries/src/Updater/Update/DataUpdate.php @@ -64,6 +64,10 @@ public function loadFromData($updateObject, $minimumStability = Updater::STABILI $this->downloadSources[] = $source; } + foreach ($dataJson->hashes as $hashAlgorithm => $hashSum) { + $this->$hashAlgorithm = $hashSum; + } + $this->currentUpdate = new \stdClass(); $this->downloadurl = new \stdClass(); $this->downloadurl->_data = $this->downloadSources[0]->url; From 765e0231c8a90dd785f176a1fa6b95fcc24b9071 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 1 Jul 2023 17:10:11 +0200 Subject: [PATCH 068/126] fix data format --- libraries/src/Updater/Update/DataUpdate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Updater/Update/DataUpdate.php b/libraries/src/Updater/Update/DataUpdate.php index ae09b4c59ce..788225eb5f2 100644 --- a/libraries/src/Updater/Update/DataUpdate.php +++ b/libraries/src/Updater/Update/DataUpdate.php @@ -65,7 +65,7 @@ public function loadFromData($updateObject, $minimumStability = Updater::STABILI } foreach ($dataJson->hashes as $hashAlgorithm => $hashSum) { - $this->$hashAlgorithm = $hashSum; + $this->$hashAlgorithm = (object) ["_data" => $hashSum]; } $this->currentUpdate = new \stdClass(); From 35ceeb7ab8bce77f7b309ec7f3199b367688db61 Mon Sep 17 00:00:00 2001 From: Timo Feuerstein Date: Sat, 1 Jul 2023 17:23:18 +0200 Subject: [PATCH 069/126] Fix invalid canonical JSON of TargetsMetadata in php-tuf --- ...p-tuf_canonical-json-targetsMetadata.patch | 92 +++++++++++++++++++ composer.json | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch diff --git a/build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch b/build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch new file mode 100644 index 00000000000..254c0508ebe --- /dev/null +++ b/build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch @@ -0,0 +1,92 @@ +# +# +--- a/src/Metadata/MetadataBase.php ++++ b/src/Metadata/MetadataBase.php (date 1688224003871) +@@ -51,19 +51,6 @@ + { + } + +- /** +- * Returns a normalized array version of this object for JSON encoding. +- * +- * @see ::toCanonicalJson() +- * +- * @return array +- * A normalized array representation of this object. +- */ +- protected function toNormalizedArray(): array +- { +- return $this->getSigned(); +- } +- + /** + * Returns a canonical JSON representation of this metadata object. + * +@@ -72,7 +59,7 @@ + */ + public function toCanonicalJson(): string + { +- return static::encodeJson($this->toNormalizedArray()); ++ return static::encodeJson($this->getSigned()); + } + + /** + + +--- a/src/Metadata/TargetsMetadata.php ++++ b/src/Metadata/TargetsMetadata.php (date 1688224236309) +@@ -67,26 +67,32 @@ + + /** + * {@inheritDoc} ++ * Returns a canonical JSON representation of this metadata object. ++ * ++ * @return string ++ * The canonical JSON representation of this object. + */ +- protected function toNormalizedArray(): array ++ public function toCanonicalJson(): string + { +- $normalized = parent::toNormalizedArray(); ++ $metadata = $this->getSigned(); + +- foreach ($normalized['targets'] as $path => $target) { ++ // Apply sorting ++ self::sortKeys($metadata); ++ ++ foreach ($metadata['targets'] as $path => $target) { + // Custom target info should always encode to an object, even if + // it's empty. + if (array_key_exists('custom', $target)) { +- $normalized['targets'][$path]['custom'] = (object) $target['custom']; ++ $metadata['targets'][$path]['custom'] = (object) $target['custom']; + } + } + + // Ensure that these will encode as objects even if they're empty. +- $normalized['targets'] = (object) $normalized['targets']; +- if (array_key_exists('delegations', $normalized)) { +- $normalized['delegations']['keys'] = (object) $normalized['delegations']['keys']; ++ $metadata['targets'] = (object) $metadata['targets']; ++ if (array_key_exists('delegations', $metadata)) { ++ $metadata['delegations']['keys'] = (object)$metadata['delegations']['keys']; + } +- +- return $normalized; ++ return static::encodeJson($metadata); + } + + /** + + +--- a/src/CanonicalJsonTrait.php ++++ b/src/CanonicalJsonTrait.php (date 1688223879893) +@@ -53,7 +53,7 @@ + * @throws \RuntimeException + * Thrown if sorting the array fails. + */ +- private static function sortKeys(array &$data): void ++ protected static function sortKeys(array &$data): void + { + // If $data is numerically indexed, the keys are already sorted, by + // definition. diff --git a/composer.json b/composer.json index 086d6d82dcc..7df4996dcb5 100644 --- a/composer.json +++ b/composer.json @@ -134,7 +134,8 @@ "patches": { "php-tuf/php-tuf": { "Patch level optional on TUF signed metadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch", - "Sort canonical json": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch" + "Sort canonical json": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch", + "Fix invalid canonical JSON of TargetsMetadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch" } } }, From 9794793bf80ae1d42307896ca6d863e1dc35e5a8 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 2 Jul 2023 10:08:39 +0200 Subject: [PATCH 070/126] update repo url --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 1 - installation/sql/mysql/base.sql | 4 ++-- installation/sql/postgresql/base.sql | 2 +- libraries/src/TUF/TufValidation.php | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 2714e78e638..9b7f9f130fe 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -65,7 +65,6 @@ public function applyUpdateSite() // Determine the intended update URL. $params = ComponentHelper::getParams('com_joomlaupdate'); - $updateURL = 'https://raw.githubusercontent.com/joomla/updates/test8/repository/'; if ($params->get('updatesource', 'nochange') == 'custom') { $paramsURL = $params->get('customurl', ''); if (trim($paramsURL) != '') { diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 4feb83ea555..b6dc7ff57f9 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -874,7 +874,7 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- Dumping data for table `#__tuf_metadata` -- INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) -SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; +SELECT `extension_id`, '{"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"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; -- -------------------------------------------------------- @@ -900,7 +900,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://raw.githubusercontent.com/joomla/updates/beta-target/repository/', 1, 0), (2, 'Accredited Joomla! Translations', 'collection', 'https://update.joomla.org/language/translationlist_4.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 4938db28084..c37ec4371f8 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -894,7 +894,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://raw.githubusercontent.com/joomla/updates/beta-target/repository/', 1, 0), (2, 'Accredited Joomla! Translations', 'collection', 'https://update.joomla.org/language/translationlist_4.xml', 1, 0), (3, 'Joomla! Update Component', 'extension', 'https://update.joomla.org/core/extensions/com_joomlaupdate.xml', 1, 0); diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufValidation.php index 5967090fe59..1fa403181ed 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufValidation.php @@ -103,7 +103,7 @@ public function getValidUpdate() { $db = Factory::getContainer()->get(DatabaseDriver::class); - $httpLoader = new HttpLoader("https://raw.githubusercontent.com/joomla/updates/beta-target/repository/"); + $httpLoader = new HttpLoader($this->params["url_prefix"]); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); $storage = new DatabaseStorage($db, $this->extensionId); From 2905f4c33093a37200bee9dad5fc6aabfa7ff64a Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 2 Jul 2023 10:32:18 +0200 Subject: [PATCH 071/126] cleanup update model --- .../src/Model/UpdateModel.php | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 9b7f9f130fe..150cf6b77b7 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -65,19 +65,41 @@ public function applyUpdateSite() // Determine the intended update URL. $params = ComponentHelper::getParams('com_joomlaupdate'); - if ($params->get('updatesource', 'nochange') == 'custom') { - $paramsURL = $params->get('customurl', ''); - if (trim($paramsURL) != '') { - $updateURL = trim($paramsURL); + switch ($params->get('updatesource', 'nochange')) { + // "Testing" + case 'testing': + $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; + break; + + // "Custom" + // @todo: check if the customurl is valid and not just "not empty". + case 'custom': + if (trim($params->get('customurl', '')) != '') { + $updateURL = trim($params->get('customurl', '')); } else { Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); return; } + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * 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://raw.githubusercontent.com/joomla/updates/beta-target/repository/'; } + $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(); + $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('us') . '.*') ->from($db->quoteName('#__update_sites_extensions', 'map')) @@ -91,10 +113,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. @@ -155,7 +178,7 @@ public function refreshUpdates($force = false) */ public function getCheckForSelfUpdate() { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) @@ -226,7 +249,7 @@ public function getUpdateInformation() // Fetch the update information from the database. $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__updates')) @@ -287,7 +310,7 @@ public function getUpdateInformation() */ public function purge() { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); // Modify the database record $update_site = new \stdClass(); @@ -461,7 +484,7 @@ protected function downloadPackage($url, $target) } // Make sure the target does not exist. - if (file_exists($target)) { + if (is_file($target)) { File::delete($target); } @@ -477,7 +500,11 @@ protected function downloadPackage($url, $target) } // Write the file to disk - File::write($target, $result->body); + $result = File::write($target, $result->body); + + if (!$result) { + return false; + } return basename($target); } @@ -558,24 +585,14 @@ public function createUpdateFile($basename = null): bool $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; if (is_file($configpath)) { - if (!File::delete($configpath)) { - File::invalidateFileCache($configpath); - @unlink($configpath); - } + File::delete($configpath); } // Write new file. First try with File. $result = File::write($configpath, $data); - // In case File used FTP but direct access could help. + // In case File failed but direct access could help. if (!$result) { - if (function_exists('file_put_contents')) { - $result = @file_put_contents($configpath, $data); - - if ($result !== false) { - $result = true; - } - } else { $fp = @fopen($configpath, 'wt'); if ($fp !== false) { @@ -588,7 +605,6 @@ public function createUpdateFile($basename = null): bool @fclose($fp); } } - } return $result; } @@ -995,6 +1011,8 @@ public function removePackageFiles() * Gets PHP options. * @todo: Outsource, build common code base for pre install and pre update check * + * @return array Array of PHP config options + * * @since 3.10.0 */ public function getPhpOptions() @@ -1299,7 +1317,7 @@ private function getDatabaseSchemaCheck(): bool */ public function getNonCoreExtensions() { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1349,7 +1367,7 @@ public function getNonCoreExtensions() */ public function getNonCorePlugins($folderFilter = ['system', 'user', 'authentication', 'actionlog', 'multifactorauth']) { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1450,7 +1468,7 @@ public function fetchCompatibility($extensionID, $joomlaTargetVersion) private function getUpdateSitesInfo($extensionID) { $id = (int) $extensionID; - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( @@ -1635,7 +1653,7 @@ protected function translateExtensionName(&$item) */ public function isTemplateActive($template) { - $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select( From 5235f8dc74eed544577c191938450c57ca72151b Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 2 Jul 2023 10:48:38 +0200 Subject: [PATCH 072/126] cleanup branch --- .../sql/updates/mysql/4.2.0-2022-03-20.sql | 2 +- libraries/src/Table/Update.php | 4 +- .../src/Updater/Adapter/CollectionAdapter.php | 14 +- .../Libraries/Cms/TUF/HttpFileFetcherTest.php | 231 ------------------ 4 files changed, 10 insertions(+), 241 deletions(-) delete mode 100644 tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql index ab6a60c4ba6..54e2da4a7ce 100644 --- a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql +++ b/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql @@ -15,4 +15,4 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- -------------------------------------------------------- INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) -SELECT `extension_id`, '{"keytype": "ed25519", "scheme": "ed25519", "keyid": "02c3130c26fb3fe13fda279d578f3bc251f2ca3a42e5878de063e0ee345533c9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f813a2882b305389cac36a9b8ebee7576ba7a7de671d2617074b03c12fb003aa", "private": "b7cb4fab28bae035a6fc5d46736e6f2d10ea4ef943e6aace8c637c1fd141ac72"}}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; +SELECT `extension_id`, '{"_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"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; diff --git a/libraries/src/Table/Update.php b/libraries/src/Table/Update.php index 99dacfbf562..fa60a04a4dc 100644 --- a/libraries/src/Table/Update.php +++ b/libraries/src/Table/Update.php @@ -34,7 +34,7 @@ class Update extends Table /** * Constructor * - * @param DatabaseDriver $db Database driver object. + * @param DatabaseDriver $db Database driver object. * * @since 1.7.0 */ @@ -83,7 +83,7 @@ public function check() /** * Method to create and execute a SELECT WHERE query. * - * @param array $options Array of options + * @param array $options Array of options * * @return string Results of query * diff --git a/libraries/src/Updater/Adapter/CollectionAdapter.php b/libraries/src/Updater/Adapter/CollectionAdapter.php index 259d048f8e4..d52c65c6eaf 100644 --- a/libraries/src/Updater/Adapter/CollectionAdapter.php +++ b/libraries/src/Updater/Adapter/CollectionAdapter.php @@ -93,9 +93,9 @@ protected function _getParent() /** * Opening an XML element * - * @param object $parser Parser object - * @param string $name Name of element that is opened - * @param array $attrs Array of attributes for the element + * @param object $parser Parser object + * @param string $name Name of element that is opened + * @param array $attrs Array of attributes for the element * * @return void * @@ -104,7 +104,7 @@ protected function _getParent() public function _startElement($parser, $name, $attrs = []) { $this->stack[] = $name; - $tag = $this->_getStackLocation(); + $tag = $this->_getStackLocation(); // Reset the data if (isset($this->$tag)) { @@ -185,8 +185,8 @@ public function _startElement($parser, $name, $attrs = []) * Closing an XML element * Note: This is a protected function though has to be exposed externally as a callback * - * @param object $parser Parser object - * @param string $name Name of the element closing + * @param object $parser Parser object + * @param string $name Name of the element closing * * @return void * @@ -207,7 +207,7 @@ protected function _endElement($parser, $name) /** * Finds an update * - * @param array $options Options to use: update_site_id: the unique ID of the update site to look at + * @param array $options Options to use: update_site_id: the unique ID of the update site to look at * * @return array|boolean Update_sites and updates discovered. False on failure * diff --git a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php b/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php deleted file mode 100644 index 933f63a03ac..00000000000 --- a/tests/Unit/Libraries/Cms/TUF/HttpFileFetcherTest.php +++ /dev/null @@ -1,231 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Tests\Unit\Libraries\Cms\TUF; - -use Joomla\Http\Http; -use Psr\Http\Message\StreamInterface; -use Joomla\CMS\TUF\HttpFileFetcher; -use Tuf\Exception\RepoFileNotFound; -use Joomla\Tests\Unit\UnitTestCase; - -/** - * @coversDefaultClass \Joomla\CMS\TUF\HttpFileFetcher - */ -class HttpFileFetcherTest extends UnitTestCase -{ - /** - * The content of the mocked response(s). - * - * This is deliberately not readable by json_decode(), in order to prove - * that the fetcher does not try to parse or process the response content - * in any way. - * - * @var string - */ - private $testContent = 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.'; - - /** - * Returns an instance of the file fetcher under test. - * - * @return HttpFileFetcher - * An instance of the file fetcher under test. - */ - private function getFetcher($clientMock): HttpFileFetcher - { - return new HttpFileFetcher($clientMock, '/metadata/', '/targets/', ""); - } - - /** - * Data provider for testfetchFileError(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerFetchFileError(): array - { - return [ - [404, RepoFileNotFound::class, 0], - [403, 'RuntimeException'] - ]; - } - - /** - * Data provider for testFetchFileIfExistsError(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerFileIfExistsError(): array - { - return [ - [403, 'RuntimeException'] - ]; - } - - /** - * Tests various error conditions when fetching a file with fetchFile(). - * - * @param integer $statusCode - * The response status code. - * @param string $exceptionClass - * The expected exception class that will be thrown. - * @param integer|null $exceptionCode - * (optional) The expected exception code. Defaults to the status code. - * @param integer|null $maxBytes - * (optional) The maximum number of bytes to read from the response. - * Defaults to the length of $this->testContent. - * - * @return void - * - * @dataProvider providerFetchFileError - * - * @covers ::fetchFile - */ - public function testFetchFileError( - int $statusCode, - string $exceptionClass, - ?int $exceptionCode = null, - ?int $maxBytes = null - ): void { - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn($statusCode); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->expectException($exceptionClass); - $this->expectExceptionCode($exceptionCode ?? $statusCode); - $this->getFetcher($clientMock) - ->fetchMetadata('test.json', $maxBytes ?? strlen($this->testContent)) - ->wait(); - } - - /** - * Tests various error conditions when fetching a file with fetchFileIfExists(). - * - * @param integer $statusCode - * The response status code. - * @param string $exceptionClass - * The expected exception class that will be thrown. - * @param integer|null $exceptionCode - * (optional) The expected exception code. Defaults to the status code. - * @param integer|null $maxBytes - * (optional) The maximum number of bytes to read from the response. - * Defaults to the length of $this->testContent. - * - * @return void - * - * @dataProvider providerFileIfExistsError - * - * @covers ::providerFileIfExists - */ - public function testFetchFileIfExistsError( - int $statusCode, - string $exceptionClass, - ?int $exceptionCode = null, - ?int $maxBytes = null - ): void { - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn($statusCode); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->expectException($exceptionClass); - $this->expectExceptionCode($exceptionCode ?? $statusCode); - $this->getFetcher($clientMock) - ->fetchMetadataIfExists('test.json', $maxBytes ?? strlen($this->testContent)); - } - - /** - * Tests fetching a file without any errors. - * - * @return void - */ - public function testFetchMetadataReturnsCorrectResponseOnSuccessfulFetch(): void - { - $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('getContents')->willReturn($this->testContent); - - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn(200); - $clientResponseMock->method('getBody')->willReturn($clientBodyMock); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->assertSame( - $this->testContent, - $this->getFetcher($clientMock)->fetchMetadata('test.json', 256)->wait()->getContents() - ); - } - - /** - * Tests fetching a file without any errors. - * - * @return void - */ - public function testFetchMetadataIfExistsReturnsCorrectResponseOnSuccessfulFetch(): void - { - $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('rewind')->willReturnSelf(); - $clientBodyMock->method('__toString')->willReturn($this->testContent); - - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn(200); - $clientResponseMock->method('getBody')->willReturn($clientBodyMock); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->assertSame( - $this->testContent, - $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) - ); - } - - /** - * Tests fetching a file without any errors. - * - * @return void - */ - public function testFetchMetadataIfExistsReturnsCorrectResponseOnNotFoundFetch(): void - { - $clientBodyMock = $this->getMockBuilder(StreamInterface::class)->getMock(); - $clientBodyMock->method('getContents')->willReturn($this->testContent); - - $clientResponseMock = $this->getMockBuilder(\Joomla\Http\Response::class)->getMock(); - $clientResponseMock->method('getStatusCode')->willReturn(404); - $clientResponseMock->method('getBody')->willReturn($clientBodyMock); - - $clientMock = $this->getMockBuilder(Http::class)->getMock(); - $clientMock->method('get')->willReturn($clientResponseMock); - - $this->assertNull( - $this->getFetcher($clientMock)->fetchMetadataIfExists('test.json', 256) - ); - } - - /** - * Tests creating a file fetcher with a repo base URI. - * - * @return void - * - * @covers ::createFromUri - */ - public function testCreateFromUri(): void - { - $this->assertInstanceOf( - HttpFileFetcher::class, - HttpFileFetcher::createFromUri('https://example.com') - ); - } -} From 66eea9fd9cd8c1662f543bfd9e031733bce258c3 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 2 Jul 2023 11:18:54 +0200 Subject: [PATCH 073/126] further cleanup --- .../TUF/{TufValidation.php => TufFetcher.php} | 14 ++--- .../src/Updater/Adapter/CollectionAdapter.php | 2 +- libraries/src/Updater/Adapter/TufAdapter.php | 57 ++++--------------- 3 files changed, 16 insertions(+), 57 deletions(-) rename libraries/src/TUF/{TufValidation.php => TufFetcher.php} (91%) diff --git a/libraries/src/TUF/TufValidation.php b/libraries/src/TUF/TufFetcher.php similarity index 91% rename from libraries/src/TUF/TufValidation.php rename to libraries/src/TUF/TufFetcher.php index 1fa403181ed..790ac305fef 100644 --- a/libraries/src/TUF/TufValidation.php +++ b/libraries/src/TUF/TufFetcher.php @@ -27,7 +27,7 @@ /** * @since __DEPLOY_VERSION__ */ -class TufValidation +class TufFetcher { /** * The id of the extension to be updated @@ -81,16 +81,10 @@ protected function configureTufOptions(OptionsResolver $resolver) { $resolver->setDefaults( [ - 'url_prefix' => '', - 'metadata_path' => '', - 'targets_path' => '', - 'mirrors' => [], + 'location' => '' ] ) - ->setAllowedTypes('url_prefix', 'string') - ->setAllowedTypes('metadata_path', 'string') - ->setAllowedTypes('targets_path', 'string') - ->setAllowedTypes('mirrors', 'array'); + ->setAllowedTypes('location', 'string'); } /** @@ -103,7 +97,7 @@ public function getValidUpdate() { $db = Factory::getContainer()->get(DatabaseDriver::class); - $httpLoader = new HttpLoader($this->params["url_prefix"]); + $httpLoader = new HttpLoader($this->params["location"]); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); $storage = new DatabaseStorage($db, $this->extensionId); 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 index 0bcc1a62057..94d3ae0bfee 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -11,8 +11,9 @@ \defined('JPATH_PLATFORM') or die; +use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Table\Table; -use Joomla\CMS\TUF\TufValidation; +use Joomla\CMS\TUF\TufFetcher; use Joomla\CMS\Updater\UpdateAdapter; use Joomla\CMS\Updater\ConstraintChecker; use Joomla\Database\ParameterType; @@ -25,19 +26,6 @@ */ class TufAdapter extends UpdateAdapter { - /** - * The client ID mapping array - * - * @var array - */ - private $clientId = [ - 'site' => 0, - 'administrator' => 1, - 'installation' => 2, - 'api' => 3, - 'cli' => 4 - ]; - /** * Finds an update. * @@ -110,34 +98,10 @@ public function getUpdateTargets($options) ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); $db->setQuery($query); - try { - $params_string = $db->loadResult(); - $params_mirrors = explode('|', $params_string); - $params = [ - 'url_prefix' => '', - 'metadata_path' => '', - 'targets_path' => '', - 'mirrors' => [] - ]; - for ($i = 0; $i < count($params_mirrors); $i++) { - if ($i == 0) { - $params['url_prefix'] = $params_mirrors[$i]; - } else { - $mirror = [ - 'url_prefix' => $params_mirrors[$i], - 'metadata_path' => '', - 'targets_path' => '', - 'confined_target_dirs' => [] - ]; - array_push($params['mirrors'], $mirror); - } - } - } catch (\RuntimeException $e) { - // Do nothing - } + $params = ["location" => $db->loadResult()]; - $TufValidation = new TufValidation($extension_id, $params); - $metaData = $TufValidation->getValidUpdate(); + $tufFetcher = new TufFetcher($extension_id, $params); + $metaData = $tufFetcher->getValidUpdate(); $metaData = json_decode($metaData); @@ -151,11 +115,12 @@ public function getUpdateTargets($options) } } - if ( - isset($values['client']) && is_string($values['client']) - && key_exists(strtolower($values['client']), $this->clientId) - ) { - $values['client'] = $this->clientId[strtolower($values['client'])]; + 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']) && isset($values['infourl']->url)) { From 105c5a1dede86b40b9e6ed772b674ade077c1abd Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 2 Jul 2023 12:33:52 +0200 Subject: [PATCH 074/126] refactor update class and add support for update channels --- .../com_installer/src/Model/UpdateModel.php | 5 +- .../components/com_joomlaupdate/config.xml | 1 - .../src/Model/UpdateModel.php | 28 +-- libraries/src/Updater/Adapter/TufAdapter.php | 3 + .../src/Updater/Update/AbstractUpdate.php | 193 ------------------ libraries/src/Updater/Update/DataUpdate.php | 100 --------- .../Update/{XmlUpdate.php => Update.php} | 95 ++++++++- .../src/Updater/Update/UpdateInterface.php | 16 -- 8 files changed, 109 insertions(+), 332 deletions(-) delete mode 100644 libraries/src/Updater/Update/AbstractUpdate.php delete mode 100644 libraries/src/Updater/Update/DataUpdate.php rename libraries/src/Updater/Update/{XmlUpdate.php => Update.php} (83%) delete mode 100644 libraries/src/Updater/Update/UpdateInterface.php diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 03d7c38dee0..63937dd27cf 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -348,14 +348,15 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) $db->setQuery($query); $updateSiteType = (string) $db->loadObject(); + // Tuf is currently only supported for Joomla core if ($updateSiteType == 'tuf') { $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error'); return; - } else { - $update->loadFromXml($instance->detailsurl, $minimumStability); } + $update->loadFromXml($instance->detailsurl, $minimumStability); + // Find and use extra_query from update_site if available $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); $updateSiteInstance->load($instance->update_site_id); diff --git a/administrator/components/com_joomlaupdate/config.xml b/administrator/components/com_joomlaupdate/config.xml index 4aa5f5697c6..f6b75a64944 100644 --- a/administrator/components/com_joomlaupdate/config.xml +++ b/administrator/components/com_joomlaupdate/config.xml @@ -30,7 +30,6 @@ label="COM_JOOMLAUPDATE_MINIMUM_STABILITY_LABEL" description="COM_JOOMLAUPDATE_MINIMUM_STABILITY_DESC" default="4" - showon="updatesource:testing[OR]updatesource:custom" validate="options" > diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index c707b48360b..439326e3bd0 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -23,8 +23,7 @@ use Joomla\CMS\Log\Log; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Plugin\PluginHelper; -use Joomla\CMS\Updater\Update\DataUpdate; -use Joomla\CMS\Updater\Update\XmlUpdate; +use Joomla\CMS\Updater\Update\Update; use Joomla\CMS\Updater\Updater; use Joomla\CMS\User\UserHelper; use Joomla\CMS\Version; @@ -150,12 +149,8 @@ public function refreshUpdates($force = false) } $updater = Updater::getInstance(); - $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); $reflection = new \ReflectionObject($updater); $reflectionMethod = $reflection->getMethod('findUpdates'); @@ -273,20 +268,19 @@ public function getUpdateInformation() return $this->updateInformation; } - $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + $channel = $comJoomlaupdateParams->get('updatesource', 'default'); - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } + $update = new Update(); - // Fetch the full update details from the update details URL. - if (empty($updateObject->data)) { - $update = new XmlUpdate(); - $update->loadFromXml($updateObject->detailsurl, $minimumStability); + // Check if we have a local JSON string with update metadata + if (!empty($updateObject->data)) { + // Local data is available, read and parse + $update->loadFromRow($updateObject, $minimumStability, $channel); } else { - $update = new DataUpdate(); - $update->loadFromData($updateObject, $minimumStability); + // No local data, fetch the full update details from the update details URL. + $update->loadFromXml($updateObject->detailsurl, $minimumStability, $channel); } // Make sure we use the current information we got from the detailsurl diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 94d3ae0bfee..af07f0693c0 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -135,6 +135,7 @@ public function getUpdateTargets($options) $values['data'] = [ 'downloads' => $values['downloads'], + 'channel' => $values['channel'], 'targetplatform' => $values['targetplatform'], 'supported_databases' => $values['supported_databases'], 'hashes' => $target->hashes @@ -184,6 +185,7 @@ protected function configureUpdateOptions(OptionsResolver $resolver) 'downloads' => [], 'targetplatform' => new \StdClass(), 'php_minimum' => null, + 'channel' => null, 'supported_databases' => new \StdClass(), 'stability' => '' ] @@ -200,6 +202,7 @@ protected function configureUpdateOptions(OptionsResolver $resolver) ->setAllowedTypes('downloads', 'array') ->setAllowedTypes('targetplatform', 'object') ->setAllowedTypes('php_minimum', 'string') + ->setAllowedTypes('channel', 'string') ->setAllowedTypes('supported_databases', 'object') ->setAllowedTypes('stability', 'string') ->setRequired(['version']); diff --git a/libraries/src/Updater/Update/AbstractUpdate.php b/libraries/src/Updater/Update/AbstractUpdate.php deleted file mode 100644 index 0b93c1d5d19..00000000000 --- a/libraries/src/Updater/Update/AbstractUpdate.php +++ /dev/null @@ -1,193 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\CMS\Updater\Update; - -use Joomla\CMS\Object\CMSObject; -use Joomla\CMS\Updater\DownloadSource; -use Joomla\CMS\Updater\Updater; - -\defined('JPATH_PLATFORM') or die; - -abstract class AbstractUpdate extends CMSObject implements UpdateInterface -{ - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - public $name; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $description; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $element; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $type; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $version; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $infourl; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $client; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $group; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $downloads; - - /** - * Update manifest `` elements - * - * @var DownloadSource[] - * @since 3.8.3 - */ - protected $downloadSources = array(); - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $tags; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $maintainer; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $maintainerurl; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $category; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $relationships; - - /** - * Update manifest `` element - * - * @var string - * @since 1.7.0 - */ - protected $targetplatform; - - /** - * Extra query for download URLs - * - * @var string - * @since 3.2.0 - */ - protected $extra_query; - - /** - * Object containing the current update data - * - * @var \stdClass - * @since 3.0.0 - */ - protected $currentUpdate; - - /** - * Object containing the latest update data - * - * @var \stdClass - * @since 3.0.0 - */ - protected $latest; - - /** - * The minimum stability required for updates to be taken into account. The possible values are: - * 0 dev Development snapshots, nightly builds, pre-release versions and so on - * 1 alpha Alpha versions (work in progress, things are likely to be broken) - * 2 beta Beta versions (major functionality in place, show-stopper bugs are likely to be present) - * 3 rc Release Candidate versions (almost stable, minor bugs might be present) - * 4 stable Stable versions (production quality code) - * - * @var integer - * @since 14.1 - * - * @see Updater - */ - protected $minimum_stability = Updater::STABILITY_STABLE; - - /** - * Array with compatible versions used by the pre-update check - * - * @var array - * @since 3.10.2 - */ - protected $compatibleVersions = array(); -} diff --git a/libraries/src/Updater/Update/DataUpdate.php b/libraries/src/Updater/Update/DataUpdate.php deleted file mode 100644 index 788225eb5f2..00000000000 --- a/libraries/src/Updater/Update/DataUpdate.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\CMS\Updater\Update; - -\defined('JPATH_PLATFORM') or die; - -use Joomla\CMS\Factory; -use Joomla\CMS\Filter\InputFilter; -use Joomla\CMS\Http\HttpFactory; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Log\Log; -use Joomla\CMS\Updater\DownloadSource; -use Joomla\CMS\Updater\Updater; -use Joomla\CMS\Version; -use Joomla\Registry\Registry; - -/** - * Update class. It is used by Updater::update() to install an update. Use Updater::findUpdates() to find updates for - * an extension. - * - * @since 1.7.0 - */ -class DataUpdate extends AbstractUpdate -{ - /** - * Object for holding for data - * - * @var resource - * @since 3.0.0 - */ - protected $data; - - /** - * Loads an XML file from a URL. - * - * @param mixed $updateObject The object of the update containing all information. - * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} - * - * @return boolean True on success - * - * @since 1.7.0 - */ - public function loadFromData($updateObject, $minimumStability = Updater::STABILITY_STABLE) - { - foreach ($updateObject as $key => $data) { - $this->$key = $updateObject->$key; - } - - $dataJson = json_decode($updateObject->data); - $this->targetplatform = $dataJson->targetplatform; - - foreach ($dataJson->downloads as $download) { - $source = new DownloadSource; - foreach ($download as $key => $data) { - $key = strtolower($key); - $source->$key = $data; - } - $this->downloadSources[] = $source; - } - - foreach ($dataJson->hashes as $hashAlgorithm => $hashSum) { - $this->$hashAlgorithm = (object) ["_data" => $hashSum]; - } - - $this->currentUpdate = new \stdClass(); - $this->downloadurl = new \stdClass(); - $this->downloadurl->_data = $this->downloadSources[0]->url; - $this->downloadurl->format = $this->downloadSources[0]->format; - $this->downloadurl->type = $this->downloadSources[0]->type; - - 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 3.4 - */ - protected function stabilityTagToInteger($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/XmlUpdate.php b/libraries/src/Updater/Update/Update.php similarity index 83% rename from libraries/src/Updater/Update/XmlUpdate.php rename to libraries/src/Updater/Update/Update.php index 3d85a3e2e34..47c8016c1c5 100644 --- a/libraries/src/Updater/Update/XmlUpdate.php +++ b/libraries/src/Updater/Update/Update.php @@ -8,6 +8,7 @@ namespace Joomla\CMS\Updater\Update; +use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Http\HttpFactory; @@ -30,7 +31,7 @@ * * @since 1.7.0 */ -class XmlUpdate +class Update { use LegacyErrorHandlingTrait; use LegacyPropertyManagementTrait; @@ -226,6 +227,14 @@ class XmlUpdate */ 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 * @@ -354,6 +363,13 @@ public function _endElement($parser, $name) $phpMatch = true; } + $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; + } + $dbMatch = false; // Check if DB & version is supported via tag, assume supported if tag isn't present @@ -390,7 +406,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; } @@ -461,6 +477,78 @@ public function _characterData($parser, $data) } } + /** + * Loads an XML file from a URL. + * + * @param mixed $updateObject The object of the update containing all information. + * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public function loadFromRow(object $updateObject, $minimumStability = Updater::STABILITY_STABLE, $channel = null) + { + // decode data information + $data = json_decode($updateObject->data, true, 512, JSON_THROW_ON_ERROR); + + // Check channel + $channelMatch = false; + + // Check if the release channel matches, assume true if tag isn't present + if (!$channel || !isset($data["channel"]) || preg_match('/' . $channel . '/', $data["channel"])) { + $channelMatch = true; + } + + // Check minimum stability + $stabilityMatch = true; + + if (isset($data["stability"]) && ($this->stabilityTagToInteger((string) $data["stability"]) < $minimumStability)) { + $stabilityMatch = false; + } + + // Compatibility mismatch, return here before properties are written + if (!$stabilityMatch || !$channelMatch) { + return false; + } + + foreach ($updateObject as $key => $value) { + $this->$key = $updateObject->$key; + } + + if (isset($data["targetplatform"])) { + $this->targetplatform = $data["targetplatform"]; + } + + if (isset($data["downloads"])) { + foreach ($data["downloads"] as $download) { + $source = new DownloadSource; + + foreach ($download as $key => $data) { + $key = strtolower($key); + $source->$key = $data; + } + + $this->downloadSources[] = $source; + } + } + + if (isset($data["hashes"])) { + foreach ($data["hashes"] as $hashAlgorithm => $hashSum) { + $this->$hashAlgorithm = (object) ["_data" => $hashSum]; + } + } + + $this->client = ApplicationHelper::getClientInfo($updateObject->client_id); + + $this->downloadurl = new \stdClass(); + $this->downloadurl->_data = $this->downloadSources[0]->url; + $this->downloadurl->format = $this->downloadSources[0]->format; + $this->downloadurl->type = $this->downloadSources[0]->type; + + return true; + } + /** * Loads an XML file from a URL. * @@ -471,7 +559,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(); @@ -492,6 +580,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/Update/UpdateInterface.php b/libraries/src/Updater/Update/UpdateInterface.php deleted file mode 100644 index 38d654dbae8..00000000000 --- a/libraries/src/Updater/Update/UpdateInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\CMS\Updater\Update; - -\defined('JPATH_PLATFORM') or die; - -interface UpdateInterface -{ - -} From 2dbbdc534e2767315a26f74441bc697d3af66e15 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 16:27:36 +0100 Subject: [PATCH 075/126] resolve comflicts --- composer.lock | 1005 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 826 insertions(+), 179 deletions(-) diff --git a/composer.lock b/composer.lock index 881e6a8da24..0adaaa1f5bf 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": "cb54f540e3f4e28bf4163ada9e1d343c", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -193,6 +193,54 @@ ], "time": "2023-08-30T09:31:38+00:00" }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "time": "2022-12-20T22:53:13+00:00" + }, { "name": "defuse/php-encryption", "version": "v2.4.0", @@ -575,6 +623,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 +2672,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 +2765,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 +2907,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 +2967,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 +3124,93 @@ }, "time": "2023-04-30T00:54:53+00:00" }, + { + "name": "php-tuf/php-tuf", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/php-tuf/php-tuf.git", + "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/7c2c58c5275618307d6b143ae7b2f09e8bc2b378", + "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378", + "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": "2023-12-07T19:16:22+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 +3230,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 +3270,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 +3278,7 @@ "type": "github" } ], - "time": "2023-08-29T08:26:30+00:00" + "time": "2023-11-25T22:23:28+00:00" }, { "name": "phpseclib/bcmath_compat", @@ -3025,16 +3344,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.23", + "version": "3.0.34", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "866cc78fbd82462ffd880e3f65692afe928bed50" + "reference": "56c79f16a6ae17e42089c06a2144467acc35348a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/866cc78fbd82462ffd880e3f65692afe928bed50", - "reference": "866cc78fbd82462ffd880e3f65692afe928bed50", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a", + "reference": "56c79f16a6ae17e42089c06a2144467acc35348a", "shasum": "" }, "require": { @@ -3115,7 +3434,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.23" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.34" }, "funding": [ { @@ -3131,7 +3450,7 @@ "type": "tidelift" } ], - "time": "2023-09-18T17:22:01+00:00" + "time": "2023-11-27T11:13:31+00:00" }, { "name": "psr/clock", @@ -3542,6 +3861,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 +4100,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 +4117,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 +4131,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 +4174,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 +4190,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 +4241,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 +4261,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 +4316,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 +4332,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 +4392,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 +4408,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 +4459,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 +4475,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 +4890,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 +5137,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 +5160,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 +5203,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 +5297,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 +5318,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 +5355,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 +5467,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 +5493,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 +5536,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 +5552,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 +5579,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 +5619,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 +5635,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 +5660,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 +5691,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 +5707,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 +7388,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 +7429,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 +8479,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 +8520,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 +8845,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 +8928,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 +8944,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T16:55:19+00:00" }, { "name": "psr/cache", @@ -9404,16 +10027,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 +10046,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 +10065,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 +10133,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 +10167,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 +10183,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 +10243,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 +10263,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 +10306,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 +10322,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 +10370,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 +10386,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 +10431,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 +10447,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 +10493,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 +10513,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 +10551,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 +10559,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 +10627,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 +10643,5 @@ "platform-overrides": { "php": "8.1.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } From 6cb18f29de5ba430371f9b5f4efb68833327b148 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 17:04:29 +0100 Subject: [PATCH 076/126] downgrade to pinned client version --- composer.json | 2 +- composer.lock | 8 ++++---- package-lock.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 8f6d2035f70..b46f60d0692 100644 --- a/composer.json +++ b/composer.json @@ -107,7 +107,7 @@ "phpseclib/bcmath_compat": "^2.0.1", "jfcherng/php-diff": "^6.15.3", "voku/portable-utf8": "^6.0.13", - "php-tuf/php-tuf": "dev-main", + "php-tuf/php-tuf": "dev-main#d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", "cweagans/composer-patches": "^1.7" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 0adaaa1f5bf..5fb9e75dfd1 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": "cb54f540e3f4e28bf4163ada9e1d343c", + "content-hash": "09597ed1b47803155aec589f45321340", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -3130,12 +3130,12 @@ "source": { "type": "git", "url": "https://github.com/php-tuf/php-tuf.git", - "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378" + "reference": "d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/7c2c58c5275618307d6b143ae7b2f09e8bc2b378", - "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378", + "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", + "reference": "d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", "shasum": "" }, "require": { 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": { From e6cd7fa3cc908c9b7baf88f569853d612735da06 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 17:24:35 +0100 Subject: [PATCH 077/126] adjust loader to return promise --- composer.json | 2 +- composer.lock | 8 ++++---- libraries/src/TUF/HttpLoader.php | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index b46f60d0692..8f6d2035f70 100644 --- a/composer.json +++ b/composer.json @@ -107,7 +107,7 @@ "phpseclib/bcmath_compat": "^2.0.1", "jfcherng/php-diff": "^6.15.3", "voku/portable-utf8": "^6.0.13", - "php-tuf/php-tuf": "dev-main#d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", + "php-tuf/php-tuf": "dev-main", "cweagans/composer-patches": "^1.7" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 5fb9e75dfd1..0adaaa1f5bf 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": "09597ed1b47803155aec589f45321340", + "content-hash": "cb54f540e3f4e28bf4163ada9e1d343c", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -3130,12 +3130,12 @@ "source": { "type": "git", "url": "https://github.com/php-tuf/php-tuf.git", - "reference": "d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa" + "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", - "reference": "d8bd8d69bfa6da812b85c19c6fd7b0bfc8ba6daa", + "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/7c2c58c5275618307d6b143ae7b2f09e8bc2b378", + "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378", "shasum": "" }, "require": { diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php index 613fc7e62a7..68baaa56993 100644 --- a/libraries/src/TUF/HttpLoader.php +++ b/libraries/src/TUF/HttpLoader.php @@ -8,8 +8,9 @@ namespace Joomla\CMS\TUF; +use GuzzleHttp\Promise\Create; use Joomla\Http\HttpFactory; -use Psr\Http\Message\StreamInterface; +use GuzzleHttp\Promise\PromiseInterface; use Tuf\Exception\RepoFileNotFound; use Tuf\Loader\LoaderInterface; @@ -19,7 +20,7 @@ public function __construct(private readonly string $repositoryPath) { } - public function load(string $locator, int $maxBytes): StreamInterface + public function load(string $locator, int $maxBytes): PromiseInterface { $httpFactory = new HttpFactory(); @@ -34,7 +35,7 @@ public function load(string $locator, int $maxBytes): StreamInterface // Rewind to start $response->getBody()->rewind(); - // Return reponse - return $response->getBody(); + // Return response + return Create::promiseFor($response->getBody()); } } From 25e12907474410285f0388724643d4bb3ead1b6a Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 17:27:38 +0100 Subject: [PATCH 078/126] update repo path --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 9a84d8f9a00..0113515521a 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -114,7 +114,7 @@ public function applyUpdateSite() * case 'nochange': */ default: - $updateURL = 'https://raw.githubusercontent.com/joomla/updates/beta-target/repository/'; + $updateURL = 'https://raw.githubusercontent.com/joomla/updates/dec23-target/repository/'; } $updateType = (pathinfo($updateURL, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf'; From c8b7823b30eba03de0350819d89b43278e2770b4 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 17:50:09 +0100 Subject: [PATCH 079/126] cs fix --- .../components/com_installer/src/Model/UpdateModel.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 7ab9a5d042c..c0a5055f0f3 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -344,11 +344,11 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) ->from('#__update_sites') ->where($db->quoteName('id') . ' = :id') ->bind(':id', $instance->update_site_id, ParameterType::INTEGER); - $db->setQuery($query); - $updateSiteType = (string) $db->loadObject(); + + $updateSiteType = (string) $db->setQuery($query)->loadResult(); // Tuf is currently only supported for Joomla core - if ($updateSiteType == 'tuf') { + if ($updateSiteType === 'tuf') { $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error'); return; From cbcd846a6d2c300a11a26ecfa3914ef46f8490cb Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 17:50:17 +0100 Subject: [PATCH 080/126] move update class --- libraries/src/Updater/{Update => }/Update.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) rename libraries/src/Updater/{Update => }/Update.php (98%) diff --git a/libraries/src/Updater/Update/Update.php b/libraries/src/Updater/Update.php similarity index 98% rename from libraries/src/Updater/Update/Update.php rename to libraries/src/Updater/Update.php index 3c53e224475..eb7381beafa 100644 --- a/libraries/src/Updater/Update/Update.php +++ b/libraries/src/Updater/Update.php @@ -6,7 +6,7 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\CMS\Updater\Update; +namespace Joomla\CMS\Updater; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; @@ -14,8 +14,6 @@ use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; -use Joomla\CMS\Updater\DownloadSource; -use Joomla\CMS\Updater\Updater; use Joomla\CMS\Object\LegacyErrorHandlingTrait; use Joomla\CMS\Object\LegacyPropertyManagementTrait; use Joomla\CMS\Version; @@ -529,9 +527,9 @@ public function loadFromRow(object $updateObject, $minimumStability = Updater::S foreach ($data["downloads"] as $download) { $source = new DownloadSource; - foreach ($download as $key => $data) { + foreach ($download as $key => $url) { $key = strtolower($key); - $source->$key = $data; + $source->$key = $url; } $this->downloadSources[] = $source; From 2bfe797bf0d30b6f237080d264d5790dada56a17 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 17:50:24 +0100 Subject: [PATCH 081/126] restore channels --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 0113515521a..1fcdc875621 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -24,7 +24,7 @@ use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Plugin\PluginHelper; -use Joomla\CMS\Updater\Update\Update; +use Joomla\CMS\Updater\Update; use Joomla\CMS\Updater\Updater; use Joomla\CMS\User\UserHelper; use Joomla\CMS\Version; @@ -296,8 +296,9 @@ public function getUpdateInformation() $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + $channel = $comJoomlaupdateParams->get('updatesource', 'nochange'); - if (\in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) { + if (\in_array($channel, ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); } From 97d44e79db14aa344e4f88332f26b8385e0abcd7 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 18:02:52 +0100 Subject: [PATCH 082/126] Apply suggestions from code review Co-authored-by: Benjamin Trenkle --- installation/sql/mysql/base.sql | 2 +- installation/sql/postgresql/base.sql | 2 +- libraries/src/TUF/DatabaseStorage.php | 4 ++-- libraries/src/Updater/Adapter/TufAdapter.php | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 8f8d38e49e6..a53382d4cee 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -915,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', 'tuf', 'https://raw.githubusercontent.com/joomla/updates/beta-target/repository/', 1, 0), -(2, 'Accredited Joomla! Translations', 'collection', 'https://update.joomla.org/language/translationlist_4.xml', 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 b7e2242f65e..ebc34a87000 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -909,7 +909,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', 'tuf', 'https://raw.githubusercontent.com/joomla/updates/beta-target/repository/', 1, 0), -(2, 'Accredited Joomla! Translations', 'collection', 'https://update.joomla.org/language/translationlist_4.xml', 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); SELECT setval('#__update_sites_update_site_id_seq', 4, false); diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 225c31a8f19..89f28a8f738 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -20,7 +20,7 @@ */ class DatabaseStorage extends StorageBase { - const METADATA_COLUMNS = ["root", "targets", "snapshot", "timestamp", "mirrors"]; + const METADATA_COLUMNS = ['root', 'targets', 'snapshot', 'timestamp', 'mirrors']; /** * The Tuf table object @@ -29,7 +29,7 @@ class DatabaseStorage extends StorageBase */ protected $table; - protected $container = array(); + protected $container = []; /** * Initialize the DatabaseStorage class diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index af07f0693c0..38f47335fb4 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -98,7 +98,7 @@ public function getUpdateTargets($options) ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); $db->setQuery($query); - $params = ["location" => $db->loadResult()]; + $params = ['location' => $db->loadResult()]; $tufFetcher = new TufFetcher($extension_id, $params); $metaData = $tufFetcher->getValidUpdate(); @@ -151,8 +151,8 @@ public function getUpdateTargets($options) $checker = new ConstraintChecker(); foreach ($versions as $version) { - if ($checker->check((array)$version)) { - return array($version); + if ($checker->check((array) $version)) { + return [$version]; } } } From 100caed0ac29c8307f872785eeee1dbf7f813879 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 18:06:18 +0100 Subject: [PATCH 083/126] apply suggestions from code review --- .../components/com_joomlaupdate/config.xml | 1 + libraries/src/TUF/HttpLoader.php | 2 +- libraries/src/TUF/TufFetcher.php | 12 +-- libraries/src/Updater/Adapter/TufAdapter.php | 78 ++++++++++--------- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/administrator/components/com_joomlaupdate/config.xml b/administrator/components/com_joomlaupdate/config.xml index f6b75a64944..4aa5f5697c6 100644 --- a/administrator/components/com_joomlaupdate/config.xml +++ b/administrator/components/com_joomlaupdate/config.xml @@ -30,6 +30,7 @@ label="COM_JOOMLAUPDATE_MINIMUM_STABILITY_LABEL" description="COM_JOOMLAUPDATE_MINIMUM_STABILITY_DESC" default="4" + showon="updatesource:testing[OR]updatesource:custom" validate="options" > diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php index 68baaa56993..b12c2b90810 100644 --- a/libraries/src/TUF/HttpLoader.php +++ b/libraries/src/TUF/HttpLoader.php @@ -28,7 +28,7 @@ public function load(string $locator, int $maxBytes): PromiseInterface $client = $httpFactory->getHttp([], 'curl'); $response = $client->get($this->repositoryPath . $locator); - if ($response->code === 404) { + if ($response->code !== 200) { throw new RepoFileNotFound(); } diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index 790ac305fef..060feeaad60 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -107,6 +107,8 @@ public function getValidUpdate() $storage ); + $app = Factory::getApplication(); + try { try { @@ -126,23 +128,23 @@ public function getValidUpdate() } } catch (DownloadSizeException $e) { $this->rollBackTufMetadata(); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); return null; } catch (MetadataException $e) { $this->rollBackTufMetadata(); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_INVALID_METADATA'), 'error'); + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_INVALID_METADATA'), 'error'); return null; } catch (FreezeAttackException $e) { $this->rollBackTufMetadata(); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_FREEZE_ATTACK'), 'error'); + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_FREEZE_ATTACK'), 'error'); return null; } catch (RollbackAttackException $e) { $this->rollBackTufMetadata(); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_ROLLBACK_ATTACK'), 'error'); + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_ROLLBACK_ATTACK'), 'error'); return null; } catch (SignatureThresholdException $e) { $this->rollBackTufMetadata(); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD'), 'error'); + $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD'), 'error'); return null; } } diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 38f47335fb4..51a7b84f533 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -98,62 +98,64 @@ public function getUpdateTargets($options) ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); $db->setQuery($query); - $params = ['location' => $db->loadResult()]; + $params = ["location" => $db->loadResult()]; $tufFetcher = new TufFetcher($extension_id, $params); $metaData = $tufFetcher->getValidUpdate(); $metaData = json_decode($metaData); - if (isset($metaData->signed->targets)) { - foreach ($metaData->signed->targets as $filename => $target) { - $values = []; - - foreach ($keys as $key) { - if (isset($target->custom->$key)) { - $values[$key] = $target->custom->$key; - } - } + if (!isset($metaData->signed->targets) || !is_array($metaData->signed->targets)) { + return false; + } - if (isset($values['client']) && is_string($values['client'])) { - $client = ApplicationHelper::getClientInfo($values['client'], true); + foreach ($metaData->signed->targets as $filename => $target) { + $values = []; - if (is_object($client)) { - $values['client'] = $client->id; - } + foreach ($keys as $key) { + if (isset($target->custom->$key)) { + $values[$key] = $target->custom->$key; } + } - if (isset($values['infourl']) && isset($values['infourl']->url)) { - $values['infourl'] = $values['infourl']->url; - } + if (isset($values['client']) && is_string($values['client'])) { + $client = ApplicationHelper::getClientInfo($values['client'], true); - try { - $values = $resolver->resolve($values); - } catch (\Exception $e) { - continue; + if (is_object($client)) { + $values['client'] = $client->id; } + } - $values['data'] = [ - 'downloads' => $values['downloads'], - 'channel' => $values['channel'], - 'targetplatform' => $values['targetplatform'], - 'supported_databases' => $values['supported_databases'], - 'hashes' => $target->hashes - ]; + if (isset($values['infourl']) && isset($values['infourl']->url)) { + $values['infourl'] = $values['infourl']->url; + } - $versions[$values['version']] = $values; + try { + $values = $resolver->resolve($values); + } catch (\Exception $e) { + continue; } - usort($versions, function ($a, $b) { - return version_compare($b['version'], $a['version']); - }); + $values['data'] = [ + 'downloads' => $values['downloads'], + 'channel' => $values['channel'], + 'targetplatform' => $values['targetplatform'], + 'supported_databases' => $values['supported_databases'], + 'hashes' => $target->hashes + ]; - $checker = new ConstraintChecker(); + $versions[$values['version']] = $values; + } - foreach ($versions as $version) { - if ($checker->check((array) $version)) { - return [$version]; - } + usort($versions, function ($a, $b) { + return version_compare($b['version'], $a['version']); + }); + + $checker = new ConstraintChecker(); + + foreach ($versions as $version) { + if ($checker->check($version)) { + return array($version); } } From eb30306dda9b3eb2bea283991c932245bdd9ea09 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 8 Dec 2023 18:13:13 +0100 Subject: [PATCH 084/126] fix call --- libraries/src/Updater/Adapter/TufAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 51a7b84f533..d80cc68608f 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -105,7 +105,7 @@ public function getUpdateTargets($options) $metaData = json_decode($metaData); - if (!isset($metaData->signed->targets) || !is_array($metaData->signed->targets)) { + if (!isset($metaData->signed->targets)) { return false; } From 13b515ef66c7ff7a61a9d7f36a32dc79c6e0c473 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 9 Dec 2023 10:23:56 +0100 Subject: [PATCH 085/126] cleanup --- .../updates/mysql/{4.2.0-2022-03-20.sql => 5.1.0-2023-12-09.sql} | 0 libraries/src/Updater/ConstraintChecker.php | 1 - 2 files changed, 1 deletion(-) rename administrator/components/com_admin/sql/updates/mysql/{4.2.0-2022-03-20.sql => 5.1.0-2023-12-09.sql} (100%) diff --git a/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql b/administrator/components/com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql similarity index 100% rename from administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-03-20.sql rename to administrator/components/com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index fbf95eb8b5a..5fc02fc43d5 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -14,7 +14,6 @@ use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; -use Joomla\CMS\Updater\Updater; use Joomla\CMS\Version; /** From ec8262d6ed21df12fb9cc60ce9192bf2968d43b2 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 11:42:24 +0100 Subject: [PATCH 086/126] Remove "nochange" update channel --- administrator/components/com_joomlaupdate/config.xml | 1 - .../components/com_joomlaupdate/src/Model/UpdateModel.php | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) 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/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 1fcdc875621..ac22fd70da5 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -87,7 +87,7 @@ public function applyUpdateSite() // Determine the intended update URL. $params = ComponentHelper::getParams('com_joomlaupdate'); - switch ($params->get('updatesource', 'nochange')) { + switch ($params->get('updatesource', 'default')) { // "Testing" case 'testing': $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; @@ -174,7 +174,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); } @@ -296,7 +296,7 @@ public function getUpdateInformation() $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - $channel = $comJoomlaupdateParams->get('updatesource', 'nochange'); + $channel = $comJoomlaupdateParams->get('updatesource', 'default'); if (\in_array($channel, ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); From bb110c669be575c8362b6dd5344aed609a938fa6 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 12:01:07 +0100 Subject: [PATCH 087/126] Use single quotes --- libraries/src/Updater/Update.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index eb7381beafa..425a057684e 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -499,14 +499,14 @@ public function loadFromRow(object $updateObject, $minimumStability = Updater::S $channelMatch = false; // Check if the release channel matches, assume true if tag isn't present - if (!$channel || !isset($data["channel"]) || preg_match('/' . $channel . '/', $data["channel"])) { + if (!$channel || !isset($data['channel']) || preg_match('/' . $channel . '/', $data['channel'])) { $channelMatch = true; } // Check minimum stability $stabilityMatch = true; - if (isset($data["stability"]) && ($this->stabilityTagToInteger((string) $data["stability"]) < $minimumStability)) { + if (isset($data['stability']) && ($this->stabilityTagToInteger((string) $data['stability']) < $minimumStability)) { $stabilityMatch = false; } @@ -519,12 +519,12 @@ public function loadFromRow(object $updateObject, $minimumStability = Updater::S $this->$key = $updateObject->$key; } - if (isset($data["targetplatform"])) { - $this->targetplatform = $data["targetplatform"]; + if (isset($data['targetplatform'])) { + $this->targetplatform = $data['targetplatform']; } - if (isset($data["downloads"])) { - foreach ($data["downloads"] as $download) { + if (isset($data['downloads'])) { + foreach ($data['downloads'] as $download) { $source = new DownloadSource; foreach ($download as $key => $url) { @@ -536,9 +536,9 @@ public function loadFromRow(object $updateObject, $minimumStability = Updater::S } } - if (isset($data["hashes"])) { - foreach ($data["hashes"] as $hashAlgorithm => $hashSum) { - $this->$hashAlgorithm = (object) ["_data" => $hashSum]; + if (isset($data['hashes'])) { + foreach ($data['hashes'] as $hashAlgorithm => $hashSum) { + $this->$hashAlgorithm = (object) ['_data' => $hashSum]; } } From f6430110c186d414f2443b813fcffb02ac5d6e47 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 12:01:41 +0100 Subject: [PATCH 088/126] Use early return and no regex check --- libraries/src/Updater/Update.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 425a057684e..762b5b5f5a3 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -495,23 +495,12 @@ public function loadFromRow(object $updateObject, $minimumStability = Updater::S // decode data information $data = json_decode($updateObject->data, true, 512, JSON_THROW_ON_ERROR); - // Check channel - $channelMatch = false; - // Check if the release channel matches, assume true if tag isn't present - if (!$channel || !isset($data['channel']) || preg_match('/' . $channel . '/', $data['channel'])) { - $channelMatch = true; + if ($channel && isset($data['channel']) && $channel !== $data['channel']) { + return false; } - // Check minimum stability - $stabilityMatch = true; - if (isset($data['stability']) && ($this->stabilityTagToInteger((string) $data['stability']) < $minimumStability)) { - $stabilityMatch = false; - } - - // Compatibility mismatch, return here before properties are written - if (!$stabilityMatch || !$channelMatch) { return false; } From cecaae6c9f43db58bff1e3814a0ad0df6215e11c Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 13:01:51 +0100 Subject: [PATCH 089/126] Update libraries/src/Updater/Adapter/TufAdapter.php --- libraries/src/Updater/Adapter/TufAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index d80cc68608f..0847996d295 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -98,7 +98,7 @@ public function getUpdateTargets($options) ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); $db->setQuery($query); - $params = ["location" => $db->loadResult()]; + $params = ['location' => $db->loadResult()]; $tufFetcher = new TufFetcher($extension_id, $params); $metaData = $tufFetcher->getValidUpdate(); From 96c5cef5183295e1429fcb155fd65721ff730165 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 13:36:43 +0100 Subject: [PATCH 090/126] Fix TufFetcher rollback and use DI --- libraries/src/TUF/TufFetcher.php | 37 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index 060feeaad60..e3087488978 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -43,15 +43,23 @@ class TufFetcher */ private $params; + /** + * The database driver + * + * @var DatabaseDriver + */ + protected $db; + /** * Validating updates with TUF * * @param integer $extensionId The ID of the extension to be checked * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for the update */ - public function __construct(int $extensionId, $params) + public function __construct(int $extensionId, $params = [], DatabaseDriver $db = null) { $this->extensionId = $extensionId; + $this->db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); $resolver = new OptionsResolver; @@ -84,7 +92,8 @@ protected function configureTufOptions(OptionsResolver $resolver) 'location' => '' ] ) - ->setAllowedTypes('location', 'string'); + ->setAllowedTypes('location', 'string') + ->setRequired('location'); } /** @@ -95,12 +104,10 @@ protected function configureTufOptions(OptionsResolver $resolver) */ public function getValidUpdate() { - $db = Factory::getContainer()->get(DatabaseDriver::class); - - $httpLoader = new HttpLoader($this->params["location"]); + $httpLoader = new HttpLoader($this->params['location']); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); - $storage = new DatabaseStorage($db, $this->extensionId); + $storage = new DatabaseStorage($this->db, $this->extensionId); $updater = new Updater( $sizeCheckingLoader, @@ -127,26 +134,18 @@ public function getValidUpdate() throw $e; } } catch (DownloadSizeException $e) { - $this->rollBackTufMetadata(); $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_DOWNLOAD_SIZE'), 'error'); - return null; } catch (MetadataException $e) { - $this->rollBackTufMetadata(); $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_INVALID_METADATA'), 'error'); - return null; } catch (FreezeAttackException $e) { - $this->rollBackTufMetadata(); $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_FREEZE_ATTACK'), 'error'); - return null; } catch (RollbackAttackException $e) { - $this->rollBackTufMetadata(); $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_ROLLBACK_ATTACK'), 'error'); - return null; } catch (SignatureThresholdException $e) { - $this->rollBackTufMetadata(); $app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD'), 'error'); - return null; } + + $this->rollBackTufMetadata(); } /** @@ -156,10 +155,12 @@ public function getValidUpdate() */ private function rollBackTufMetadata() { - $db = Factory::getContainer()->get(DatabaseDriver::class); + $db = $this->db; + $query = $db->getQuery(true) ->delete($db->quoteName('#__tuf_metadata')) ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); - $db->setQuery($query); + + $db->setQuery($query)->execute(); } } From 591827dab5bbbe46fd03ba3a84ae39c26cc6f6df Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 13:37:08 +0100 Subject: [PATCH 091/126] Use single quotes --- libraries/src/Updater/Adapter/TufAdapter.php | 6 ++---- libraries/src/Updater/Updater.php | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index d80cc68608f..d556b9ae765 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -82,10 +82,9 @@ public function getUpdateTargets($options) ->from($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('update_site_id') . ' = :id') ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); - $db->setQuery($query); try { - $extension_id = $db->loadResult(); + $extension_id = $db->setQuery($query)->loadResult(); } catch (\RuntimeException $e) { // Do nothing } @@ -96,9 +95,8 @@ public function getUpdateTargets($options) ->from($db->quoteName('#__update_sites')) ->where($db->quoteName('update_site_id') . ' = :id') ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); - $db->setQuery($query); - $params = ["location" => $db->loadResult()]; + $params = ['location' => $db->setQuery($query)->loadResult()]; $tufFetcher = new TufFetcher($extension_id, $params); $metaData = $tufFetcher->getValidUpdate(); 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. From 2ddac3076b7d1954200ac6f4e91e05331add29f9 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 9 Dec 2023 13:41:04 +0100 Subject: [PATCH 092/126] added missing execute call --- libraries/src/TUF/TufFetcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index 060feeaad60..2dcc8e13fa0 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -160,6 +160,6 @@ private function rollBackTufMetadata() $query = $db->getQuery(true) ->delete($db->quoteName('#__tuf_metadata')) ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); - $db->setQuery($query); + $db->setQuery($query)->execute(); } } From d8dd59fa9599a643ebb67e744f6c5734c10c403f Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 13:54:30 +0100 Subject: [PATCH 093/126] Go back to the default behaviour of having a detailsUrl when updating --- libraries/src/Updater/Adapter/TufAdapter.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index c79bdf9a7b1..1ff5f564110 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -134,17 +134,12 @@ public function getUpdateTargets($options) continue; } - $values['data'] = [ - 'downloads' => $values['downloads'], - 'channel' => $values['channel'], - 'targetplatform' => $values['targetplatform'], - 'supported_databases' => $values['supported_databases'], - 'hashes' => $target->hashes - ]; + $values['detailsurl'] = rtrim($params['location'], '/') . '/targets.json'; $versions[$values['version']] = $values; } + // We only want the latest version we support usort($versions, function ($a, $b) { return version_compare($b['version'], $a['version']); }); From ae444176501958a5bff1cd80eda2e914c817169c Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 15:19:28 +0100 Subject: [PATCH 094/126] Implement TUF live parser --- .../src/Model/UpdateModel.php | 6 +- libraries/src/Updater/Update.php | 170 +++++++++++++----- 2 files changed, 127 insertions(+), 49 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index ac22fd70da5..89c7d6bc6c0 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -304,10 +304,12 @@ public function getUpdateInformation() $update = new Update(); + $updateType = (pathinfo($updateObject->detailsurl, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf'; + // Check if we have a local JSON string with update metadata - if (!empty($updateObject->data)) { + if (!empty($updateType === 'tuf')) { // Local data is available, read and parse - $update->loadFromRow($updateObject, $minimumStability, $channel); + $update->loadFromJSON($updateObject->detailsurl, $minimumStability, $channel); } else { // No local data, fetch the full update details from the update details URL. $update->loadFromXml($updateObject->detailsurl, $minimumStability, $channel); diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 762b5b5f5a3..5dd6476e80f 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -1,4 +1,5 @@ data, true, 512, JSON_THROW_ON_ERROR); + $response = $this->loadUpdateInformation($url); - // Check if the release channel matches, assume true if tag isn't present - if ($channel && isset($data['channel']) && $channel !== $data['channel']) { + if (!$response) { return false; } - if (isset($data['stability']) && ($this->stabilityTagToInteger((string) $data['stability']) < $minimumStability)) { + $data = json_decode($response->body, true); + + if (empty($data['signed']['targets'])) { return false; } - foreach ($updateObject as $key => $value) { - $this->$key = $updateObject->$key; - } + $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); - if (isset($data['targetplatform'])) { - $this->targetplatform = $data['targetplatform']; - } + foreach ($data['signed']['targets'] as $target) { + + // Check that the product matches and that the version matches (optionally a regexp) + if ( + !isset($target['custom']['targetplatform']['name']) + || $product !== $target['custom']['targetplatform']['name'] + || !preg_match('/^' . $target['custom']['targetplatform']['name'] . '/', $this->get('jversion.full', JVERSION)) + ) { + continue; + } - if (isset($data['downloads'])) { - foreach ($data['downloads'] as $download) { - $source = new DownloadSource; + // Check if PHP version supported via tag, assume true if tag isn't present + if (isset($target['custom']['php_minimum']) && !version_compare(PHP_VERSION, $target['custom']['php_minimum'], '>=')) { + continue; + } + + // Check if the release channel matches, assume true if tag isn't present + if ($channel && isset($target['custom']['channel']) && $channel !== $target['custom']['channel']) { + continue; + } - foreach ($download as $key => $url) { - $key = strtolower($key); - $source->$key = $url; + // Check if DB & version is supported via tag, assume supported if tag isn't present + if (isset($target['custom']['supported_databases'])) { + $db = Factory::getContainer()->get(DatabaseDriver::class); + $dbType = strtolower($db->getServerType()); + $dbVersion = $db->getVersion(); + $supportedDbs = $target['custom']['supported_databases']; + + // 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'; + } } - $this->downloadSources[] = $source; + // Do we have an entry for the database? + if (!isset($supportedDbs[$dbType]) || !version_compare($dbVersion, $supportedDbs[$dbType], '>=')) { + continue; + } } - } - if (isset($data['hashes'])) { - foreach ($data['hashes'] as $hashAlgorithm => $hashSum) { - $this->$hashAlgorithm = (object) ['_data' => $hashSum]; + // Check minimum stability + if (isset($target['custom']['stability']) && ($this->stabilityTagToInteger($target['custom']['stability']) < $minimumStability)) { + continue; } - } - $this->client = ApplicationHelper::getClientInfo($updateObject->client_id); + if (!empty($target['custom']['downloads'])) { + $this->compatibleVersions[] = $target['custom']['version']; + } + + if (!isset($this->latest) || version_compare($target['custom']['version'], $this->latest->version, '>')) { + $this->latest = (object) $target['custom']; + + $this->downloadSources = []; + + if (!empty($this->latest->downloads)) { + foreach ($this->latest->downloads as $download) { + $source = new DownloadSource; + + foreach ($download as $key => $url) { + $key = strtolower($key); + $source->$key = $url; + } + + $this->downloadSources[] = $source; + } + } + + $this->client = $this->latest->client; - $this->downloadurl = new \stdClass(); - $this->downloadurl->_data = $this->downloadSources[0]->url; - $this->downloadurl->format = $this->downloadSources[0]->format; - $this->downloadurl->type = $this->downloadSources[0]->type; + if (isset($target['hashes'])) { + foreach ($target['hashes'] as $hashAlgorithm => $hashSum) { + $this->$hashAlgorithm = (object) ['_data' => $hashSum]; + } + } + } + } return true; } /** - * Loads an XML file from a URL. + * 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} @@ -553,21 +610,9 @@ public function loadFromRow(object $updateObject, $minimumStability = Updater::S */ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) { - $version = new Version(); - $httpOption = new Registry(); - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); - - try { - $http = HttpFactory::getHttp($httpOption); - $response = $http->get($url); - } catch (\RuntimeException $e) { - $response = null; - } - - if ($response === null || $response->code !== 200) { - // @todo: Add a 'mark bad' setting here somehow - Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); + $response = $this->loadUpdateInformation($url); + if (!$response) { return false; } @@ -598,6 +643,37 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, return true; } + /** + * Load the update manifest file + * + * @param string $url + * + * @return Response|null + */ + protected function loadUpdateInformation($url) + { + $version = new Version(); + $httpOption = new Registry(); + + $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); + + try { + $http = HttpFactory::getHttp($httpOption); + $response = $http->get($url); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + // @todo: Add a 'mark bad' setting here somehow + Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); + + return false; + } + + return $response; + } + /** * 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. From eaff5fdc2c2cc682bda9f3d281d351650ab1d6fd Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 15:19:38 +0100 Subject: [PATCH 095/126] Fix TUF cleanup --- libraries/src/TUF/TufFetcher.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index e3087488978..86c39019e84 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -158,8 +158,10 @@ private function rollBackTufMetadata() $db = $this->db; $query = $db->getQuery(true) - ->delete($db->quoteName('#__tuf_metadata')) - ->columns(['snapshot_json', 'targets_json', 'timestamp_json']); + ->update($db->quoteName('#__tuf_metadata')) + ->set($db->quoteName('snapshot') . ' = NULL') + ->set($db->quoteName('targets') . ' = NULL') + ->set($db->quoteName('timestamp') . ' = NULL'); $db->setQuery($query)->execute(); } From 4b33268670ca98f271e4e2dce78bfc0093f7c7f6 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 15:26:21 +0100 Subject: [PATCH 096/126] Update root key --- .../components/com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql | 2 +- installation/sql/mysql/base.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 54e2da4a7ce..f5a374b6a48 100644 --- 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 @@ -15,4 +15,4 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- -------------------------------------------------------- INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) -SELECT `extension_id`, '{"_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"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; +SELECT `extension_id`, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index a53382d4cee..0d9aa86da00 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -888,7 +888,7 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- Dumping data for table `#__tuf_metadata` -- INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) -SELECT `extension_id`, '{"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"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; +SELECT `extension_id`, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; -- -------------------------------------------------------- From 05d47f17ae1b0cc46bea2c8c68cf2077334b609c Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 9 Dec 2023 15:41:13 +0100 Subject: [PATCH 097/126] improve seperation of concerncs --- libraries/src/TUF/DatabaseStorage.php | 10 ++++------ libraries/src/TUF/TufFetcher.php | 6 +++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 89f28a8f738..2f3fa312cca 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -9,6 +9,7 @@ namespace Joomla\CMS\TUF; use Joomla\CMS\Table\Table; +use Joomla\CMS\Table\TableInterface; use Joomla\CMS\Table\Tuf; use Joomla\Database\DatabaseDriver; use Tuf\Metadata\StorageBase; @@ -34,14 +35,11 @@ class DatabaseStorage extends StorageBase /** * Initialize the DatabaseStorage class * - * @param DatabaseDriver $db A database connector object - * @param integer $extensionId The extension ID where the storage should be implemented for + * @param TableInterface $table The table object that represents the metadata row */ - public function __construct(DatabaseDriver $db, int $extensionId) + public function __construct(TableInterface $table) { - $this->table = new Tuf($db); - - $this->table->load(['extension_id' => $extensionId]); + $this->table = $table; foreach (self::METADATA_COLUMNS as $column) { if ($this->table->$column === null) { diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index 6245b2bfe70..19af33d52f1 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -10,6 +10,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; +use Joomla\CMS\Table\Tuf; use Joomla\Database\DatabaseDriver; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; @@ -107,7 +108,10 @@ public function getValidUpdate() $httpLoader = new HttpLoader($this->params['location']); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); - $storage = new DatabaseStorage($this->db, $this->extensionId); + $metadataTable = new Tuf($this->db); + $metadataTable->load(['extension_id' => $this->extensionId]); + + $storage = new DatabaseStorage($metadataTable); $updater = new Updater( $sizeCheckingLoader, From 8bee44023ae7378a326655e52c076b4af2433aa0 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 15:42:06 +0100 Subject: [PATCH 098/126] Map channels zu specific versions --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 89c7d6bc6c0..74f08abc5d2 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -308,6 +308,12 @@ public function getUpdateInformation() // Check if we have a local JSON string with update metadata if (!empty($updateType === 'tuf')) { + $channel = Version::MAJOR_VERSION . '.x'; + + if ($channel === 'next') { + $channel = (Version::MAJOR_VERSION + 1) . '.x'; + } + // Local data is available, read and parse $update->loadFromJSON($updateObject->detailsurl, $minimumStability, $channel); } else { From 670ee4511985540004cd6342720bd33c2f6b159a Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 15:47:12 +0100 Subject: [PATCH 099/126] Fix update channel --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 74f08abc5d2..ebfc1449303 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -308,14 +308,15 @@ public function getUpdateInformation() // Check if we have a local JSON string with update metadata if (!empty($updateType === 'tuf')) { - $channel = Version::MAJOR_VERSION . '.x'; + // Use the correct identifier for the update channel + $updateChannel = Version::MAJOR_VERSION . '.x'; if ($channel === 'next') { - $channel = (Version::MAJOR_VERSION + 1) . '.x'; + $updateChannel = (Version::MAJOR_VERSION + 1) . '.x'; } // Local data is available, read and parse - $update->loadFromJSON($updateObject->detailsurl, $minimumStability, $channel); + $update->loadFromJSON($updateObject->detailsurl, $minimumStability, $updateChannel); } else { // No local data, fetch the full update details from the update details URL. $update->loadFromXml($updateObject->detailsurl, $minimumStability, $channel); From e370fe80458328a786fa59f0ea327aa9a4b795ac Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 16:08:26 +0100 Subject: [PATCH 100/126] Fix wrong check --- .../components/com_joomlaupdate/src/Model/UpdateModel.php | 2 +- libraries/src/Updater/Update.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index ebfc1449303..c4cc3722534 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -310,7 +310,7 @@ public function getUpdateInformation() 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'; } diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 5dd6476e80f..85c3b449ddc 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -520,7 +520,7 @@ public function loadFromJSON(string $url, $minimumStability = Updater::STABILITY if ( !isset($target['custom']['targetplatform']['name']) || $product !== $target['custom']['targetplatform']['name'] - || !preg_match('/^' . $target['custom']['targetplatform']['name'] . '/', $this->get('jversion.full', JVERSION)) + || !preg_match('/^' . $target['custom']['targetplatform']['version'] . '/', $this->get('jversion.full', JVERSION)) ) { continue; } From 038a3359159a68e56b54b902ea866035b1b8ff76 Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 16:43:14 +0100 Subject: [PATCH 101/126] Fix data format --- libraries/src/Updater/Update.php | 52 +++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 85c3b449ddc..8e5f51153ef 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -568,7 +568,11 @@ public function loadFromJSON(string $url, $minimumStability = Updater::STABILITY } if (!isset($this->latest) || version_compare($target['custom']['version'], $this->latest->version, '>')) { - $this->latest = (object) $target['custom']; + $this->latest = new \stdClass(); + + foreach ($target['custom'] as $key => $val) { + $this->latest->$key = $val; + } $this->downloadSources = []; @@ -595,6 +599,52 @@ public function loadFromJSON(string $url, $minimumStability = Updater::STABILITY } } + // If the latest item is set then we transfer it to where we want to + if (isset($this->latest)) { + // Ugly XML structure conversion + foreach (get_object_vars($this->latest) as $key => $val) { + if (in_array($key, ['infourl'])) { + $this->$key = (object) [ + '_data' => $val['url'], + ]; + + unset($val['url']); + + foreach ($val as $k => $v) { + $this->$key->$k = $v; + } + + continue; + } + elseif (in_array($key, ['targetplatform', 'supported_databases'])) { + $this->$key = (object) [ + '_data' => '', + ]; + + foreach ($val as $k => $v) { + $this->$key->$k = $v; + } + + continue; + } + elseif ($key === 'stability') { + $this->$key = $this->stabilityTagToInteger($val); + + continue; + } + + $this->$key = (object) ['_data' => $val]; + } + + $this->downloadurl = (object) [ + '_data' => $this->downloadSources[0]->url, + 'type' => $this->downloadSources[0]->type, + 'format' => $this->downloadSources[0]->format, + ]; + + unset($this->latest); + } + return true; } From 8815e21ed7932b891bba3da45d44834e3b40d4ee Mon Sep 17 00:00:00 2001 From: Benjamin Trenkle Date: Sat, 9 Dec 2023 17:04:19 +0100 Subject: [PATCH 102/126] Use correct download URL --- libraries/src/Updater/Update.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 8e5f51153ef..98eaf99addd 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -636,11 +636,15 @@ public function loadFromJSON(string $url, $minimumStability = Updater::STABILITY $this->$key = (object) ['_data' => $val]; } - $this->downloadurl = (object) [ - '_data' => $this->downloadSources[0]->url, - 'type' => $this->downloadSources[0]->type, - 'format' => $this->downloadSources[0]->format, - ]; + foreach ($this->downloadSources as $source) { + $this->downloadurl = (object) [ + '_data' => $source->url, + 'type' => $source->type, + 'format' => $source->format, + ]; + + break; + } unset($this->latest); } From 4bf7b2b78af496ca21bd01c10d945b7a6c464671 Mon Sep 17 00:00:00 2001 From: Martina Scholz <64533137+LadySolveig@users.noreply.github.com> Date: Sun, 10 Dec 2023 10:04:25 +0100 Subject: [PATCH 103/126] Update root key postgresql (#22) --- .../updates/postgresql/5.1.0-2023-12-09.sql | 19 +++++++++++++++ installation/sql/postgresql/base.sql | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 administrator/components/com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql 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..e644b4c5bca --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql @@ -0,0 +1,19 @@ +-- +-- Table structure for table "#__tuf_metadata" +-- + +CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( +"id" serial NOT NULL, +"extension_id" bigint DEFAULT 0 NOT NULL, +"root" text DEFAULT NULL, +"target" 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" ("extension_id", "root") +SELECT "extension_id", '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM "#__extensions" WHERE "type"='file' AND "element"='joomla'; diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index ebc34a87000..f5cf9738a52 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -884,6 +884,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, +"extension_id" bigint DEFAULT 0 NOT NULL, +"root" text DEFAULT NULL, +"target" 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" ("extension_id", "root") +SELECT "extension_id", '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM "#__extensions" WHERE "type"='file' AND "element"='joomla'; + -- -- Table structure for table `#__update_sites` -- From 295a96c351fc30bef90a1a4454fea846fc968f7b Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 10 Dec 2023 13:46:19 +0100 Subject: [PATCH 104/126] use tuf client to-refetch core update information, improve soc --- .../sql/updates/mysql/5.1.0-2023-12-09.sql | 8 +- .../updates/postgresql/5.1.0-2023-12-09.sql | 6 +- .../com_installer/src/Model/UpdateModel.php | 3 +- .../src/Model/UpdateModel.php | 17 +- libraries/src/TUF/TufFetcher.php | 66 ++------ libraries/src/Updater/Adapter/TufAdapter.php | 56 +++---- libraries/src/Updater/ConstraintChecker.php | 36 ++-- libraries/src/Updater/Update.php | 157 +++++------------- .../Cms/Updater/ConstraintCheckerTest.php | 34 ++-- 9 files changed, 127 insertions(+), 256 deletions(-) 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 index f5a374b6a48..40348a2631d 100644 --- 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 @@ -4,9 +4,9 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, - `extension_id` int DEFAULT 0, + `update_site_id` int DEFAULT 0, `root` text DEFAULT NULL, - `target` text DEFAULT NULL, + `targets` text DEFAULT NULL, `snapshot` text DEFAULT NULL, `timestamp` text DEFAULT NULL, `mirrors` text DEFAULT NULL, @@ -14,5 +14,5 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates'; -- -------------------------------------------------------- -INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) -SELECT `extension_id`, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; +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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); 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 index e644b4c5bca..d1ff0d98eed 100644 --- 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 @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( "id" serial NOT NULL, "extension_id" bigint DEFAULT 0 NOT NULL, "root" text DEFAULT NULL, -"target" text DEFAULT NULL, +"targets" text DEFAULT NULL, "snapshot" text DEFAULT NULL, "timestamp" text DEFAULT NULL, "mirrors" text DEFAULT NULL, @@ -15,5 +15,5 @@ PRIMARY KEY ("id") COMMENT ON TABLE "#__tuf_metadata" IS 'Secure TUF Updates'; -INSERT INTO "#__tuf_metadata" ("extension_id", "root") -SELECT "extension_id", '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM "#__extensions" WHERE "type"='file' AND "element"='joomla'; +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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index c0a5055f0f3..fd9d99a3c32 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -21,7 +21,6 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Updater\Update; use Joomla\CMS\Updater\Updater; -use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseQuery; use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\ParameterType; @@ -338,7 +337,7 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) } $app = Factory::getApplication(); - $db = Factory::getContainer()->get(DatabaseDriver::class); + $db = $this->getDatabase(); $query = $db->getQuery(true) ->select('type') ->from('#__update_sites') diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index c4cc3722534..e28b0b5040f 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; @@ -106,9 +107,10 @@ 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': @@ -134,7 +136,7 @@ public function applyUpdateSite() $db->setQuery($query); $update_site = $db->loadObject(); - if ($update_site->location != $updateURL || $update_site->type != $updateType) { + if ($update_site->location !== $updateURL || $update_site->type !== $updateType) { // Modify the database record. $update_site->last_check_timestamp = 0; $update_site->location = $updateURL; @@ -315,11 +317,14 @@ public function getUpdateInformation() $updateChannel = (Version::MAJOR_VERSION + 1) . '.x'; } - // Local data is available, read and parse - $update->loadFromJSON($updateObject->detailsurl, $minimumStability, $updateChannel); + $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 { - // No local data, fetch the full update details from the update details URL. - $update->loadFromXml($updateObject->detailsurl, $minimumStability, $channel); + // 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 diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index 19af33d52f1..8b87ba7d498 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -10,11 +10,8 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; -use Joomla\CMS\Table\Tuf; +use Joomla\CMS\Table\Tuf as MetadataTable; use Joomla\Database\DatabaseDriver; -use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; -use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; -use Symfony\Component\OptionsResolver\OptionsResolver; use Tuf\Client\Updater; use Tuf\Exception\Attack\FreezeAttackException; use Tuf\Exception\Attack\RollbackAttackException; @@ -31,71 +28,39 @@ class TufFetcher { /** - * The id of the extension to be updated + * The table object holding the metadata * - * @var integer + * @var MetadataTable */ - private $extensionId; + private MetadataTable $metadataTable; /** - * The params of the validator + * The repository base url * * @var mixed */ - private $params; + private string $repositoryUrl; /** * The database driver * * @var DatabaseDriver */ - protected $db; + protected DatabaseDriver $db; /** * Validating updates with TUF * - * @param integer $extensionId The ID of the extension to be checked - * @param mixed $params The parameters containing the Base-URI, the Metadata- and Targets-Path and mirrors for the update + * @param MetadataTable $metadataTable The table object holding the metadata + * @param string $repositoryUrl The base repo URL */ - public function __construct(int $extensionId, $params = [], DatabaseDriver $db = null) + public function __construct(MetadataTable $metadataTable, string $repositoryUrl, DatabaseDriver $db = null) { - $this->extensionId = $extensionId; + $this->metadataTable = $metadataTable; + $this->repositoryUrl = $repositoryUrl; $this->db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); - - $resolver = new OptionsResolver; - - try { - $this->configureTufOptions($resolver); - } catch (\Exception $e) { - } - - try { - $params = $resolver->resolve($params); - } catch (\Exception $e) { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { - throw $e; - } - } - - $this->params = $params; } - /** - * Configures default values or pass arguments to params - * - * @param OptionsResolver $resolver The OptionsResolver for the params - * @return void - */ - protected function configureTufOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'location' => '' - ] - ) - ->setAllowedTypes('location', 'string') - ->setRequired('location'); - } /** * Checks for updates and writes it into the database if they are valid. Then it gets the targets.json content and @@ -105,13 +70,10 @@ protected function configureTufOptions(OptionsResolver $resolver) */ public function getValidUpdate() { - $httpLoader = new HttpLoader($this->params['location']); + $httpLoader = new HttpLoader($this->repositoryUrl); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); - $metadataTable = new Tuf($this->db); - $metadataTable->load(['extension_id' => $this->extensionId]); - - $storage = new DatabaseStorage($metadataTable); + $storage = new DatabaseStorage($this->metadataTable); $updater = new Updater( $sizeCheckingLoader, diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 1ff5f564110..631efd3f517 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -12,12 +12,14 @@ \defined('JPATH_PLATFORM') or die; 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\UpdateAdapter; use Joomla\CMS\Updater\ConstraintChecker; -use Joomla\Database\ParameterType; use Symfony\Component\OptionsResolver\OptionsResolver; +use Tuf\Exception\MetadataException; /** * TUF Update Adapter Class @@ -74,45 +76,29 @@ public function getUpdateTargets($options) } catch (\Exception $e) { } - // Get extension_id for TufValidation - $db = $this->parent->getDbo(); + /** @var MetadataTable $metadataTable */ + $metadataTable = new MetadataTable(Factory::getDbo()); + $metadataTable->load(['update_site_id' => $options['update_site_id']]); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__update_sites_extensions')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); - - try { - $extension_id = $db->setQuery($query)->loadResult(); - } catch (\RuntimeException $e) { - // Do nothing - } - - // Get params for TufValidation - $query = $db->getQuery(true) - ->select($db->quoteName('location')) - ->from($db->quoteName('#__update_sites')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $options['update_site_id'], ParameterType::INTEGER); - - $params = ['location' => $db->setQuery($query)->loadResult()]; - - $tufFetcher = new TufFetcher($extension_id, $params); + $tufFetcher = new TufFetcher($metadataTable, $options['location']); $metaData = $tufFetcher->getValidUpdate(); - $metaData = json_decode($metaData); + $metaData = json_decode($metaData, true); - if (!isset($metaData->signed->targets)) { + if (!isset($metaData["signed"]["targets"])) { return false; } - foreach ($metaData->signed->targets as $filename => $target) { + foreach ($metaData["signed"]["targets"] as $filename => $target) { $values = []; + if (!isset($target["hashes"])) { + throw new MetadataException("No trusted hashes are available for '$filename'"); + } + foreach ($keys as $key) { - if (isset($target->custom->$key)) { - $values[$key] = $target->custom->$key; + if (isset($target["custom"][$key])) { + $values[$key] = $target["custom"][$key]; } } @@ -124,8 +110,8 @@ public function getUpdateTargets($options) } } - if (isset($values['infourl']) && isset($values['infourl']->url)) { - $values['infourl'] = $values['infourl']->url; + if (isset($values['infourl']) && isset($values['infourl']['url'])) { + $values['infourl'] = $values['infourl']['url']; } try { @@ -134,7 +120,7 @@ public function getUpdateTargets($options) continue; } - $values['detailsurl'] = rtrim($params['location'], '/') . '/targets.json'; + $values['detailsurl'] = $options['location']; $versions[$values['version']] = $values; } @@ -195,10 +181,10 @@ protected function configureUpdateOptions(OptionsResolver $resolver) ->setAllowedTypes('infourl', 'string') ->setAllowedTypes('client', 'int') ->setAllowedTypes('downloads', 'array') - ->setAllowedTypes('targetplatform', 'object') + ->setAllowedTypes('targetplatform', 'array') ->setAllowedTypes('php_minimum', 'string') ->setAllowedTypes('channel', 'string') - ->setAllowedTypes('supported_databases', 'object') + ->setAllowedTypes('supported_databases', 'array') ->setAllowedTypes('stability', 'string') ->setRequired(['version']); } diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index 5fc02fc43d5..b544ebd3ebc 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -26,44 +26,44 @@ class ConstraintChecker /** * Checks whether the passed constraints are matched * - * @param array $constraints The provided constraints to be checked + * @param array $candidate The provided constraints to be checked * * @return boolean * * @since __DEPLOY_VERSION__ */ - public function check(array $constraints) + public function check(array $candidate) { - if (!isset($constraints['targetplatform'])) { + if (!isset($candidate['targetplatform'])) { // targetplatform is required return false; } // Check targetplatform - if (!$this->checkTargetplatform($constraints['targetplatform'])) { + if (!$this->checkTargetplatform($candidate['targetplatform'])) { return false; } // Check php_minimumm, assume true when not set if ( - isset($constraints['php_minimum']) - && !$this->checkPhpMinimum($constraints['php_minimum']) + isset($candidate['php_minimum']) + && !$this->checkPhpMinimum($candidate['php_minimum']) ) { return false; } // Check supported databases, assume true when not set if ( - isset($constraints['supported_databases']) - && !$this->checkSupportedDatabases($constraints['supported_databases']) + isset($candidate['supported_databases']) + && !$this->checkSupportedDatabases($candidate['supported_databases']) ) { return false; } // Check stability, assume true when not set if ( - isset($constraints['stability']) - && !$this->checkStability($constraints['stability']) + isset($candidate['stability']) + && !$this->checkStability($candidate['stability']) ) { return false; } @@ -74,21 +74,21 @@ public function check(array $constraints) /** * Check the targetPlatform * - * @param object $targetPlatform + * @param array $targetPlatform * * @return boolean * * @since __DEPLOY_VERSION__ */ - protected function checkTargetplatform(\stdClass $targetPlatform) + 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) + $product === $targetPlatform["name"] + && preg_match('/^' . $targetPlatform["version"] . '/', JVERSION) ) { return true; } @@ -114,13 +114,13 @@ protected function checkPhpMinimum(string $phpMinimum) /** * Check the supported databases and versions * - * @param object $supportedDatabases stdClass of supported databases and versions + * @param array $supportedDatabases array of supported databases and versions * * @return boolean * * @since __DEPLOY_VERSION__ */ - protected function checkSupportedDatabases(\stdClass $supportedDatabases) + protected function checkSupportedDatabases(array $supportedDatabases) { $db = Factory::getDbo(); $dbType = strtolower($db->getServerType()); @@ -137,8 +137,8 @@ protected function checkSupportedDatabases(\stdClass $supportedDatabases) } // Do we have an entry for the database? - if (\property_exists($supportedDatabases, $dbType)) { - $minimumVersion = $supportedDatabases->$dbType; + if (!empty($supportedDatabases["$dbType"])) { + $minimumVersion = $supportedDatabases["$dbType"]; return version_compare($dbVersion, $minimumVersion, '>='); } diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 98eaf99addd..92d873795e2 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -9,9 +9,6 @@ namespace Joomla\CMS\Updater; -use InvalidArgumentException; -use Exception; -use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Http\HttpFactory; @@ -19,12 +16,11 @@ 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\Database\DatabaseDriver; use Joomla\Http\Response; -use Joomla\DI\Exception\KeyNotFoundException; use Joomla\Registry\Registry; -use RuntimeException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -488,154 +484,75 @@ public function _characterData($parser, $data) } /** - * Loads a JSON file from a URL. + * Loads update information from a TUF repo. * * - * @param string $url The URL. - * @param int $minimumStability The minimum stability required for updating the extension {@see Updater} + * @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 channnel * * @return boolean True on success * * @since __DEPLOY_VERSION__ */ - public function loadFromJSON(string $url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) + public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) { - $response = $this->loadUpdateInformation($url); - - if (!$response) { - return false; - } + $tufFetcher = new TufFetcher( + $metadataTable, + $url + ); - $data = json_decode($response->body, true); + $metaData = $tufFetcher->getValidUpdate(); - if (empty($data['signed']['targets'])) { - return false; - } - - $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); + $data = json_decode($metaData, true); + $constraintChecker = new ConstraintChecker(); foreach ($data['signed']['targets'] as $target) { - - // Check that the product matches and that the version matches (optionally a regexp) - if ( - !isset($target['custom']['targetplatform']['name']) - || $product !== $target['custom']['targetplatform']['name'] - || !preg_match('/^' . $target['custom']['targetplatform']['version'] . '/', $this->get('jversion.full', JVERSION)) - ) { + if (!$constraintChecker->check($target['custom'])) { continue; } - // Check if PHP version supported via tag, assume true if tag isn't present - if (isset($target['custom']['php_minimum']) && !version_compare(PHP_VERSION, $target['custom']['php_minimum'], '>=')) { - continue; + if (!empty($target['custom']['downloads'])) { + $this->compatibleVersions[] = $target['custom']['version']; } - // Check if the release channel matches, assume true if tag isn't present - if ($channel && isset($target['custom']['channel']) && $channel !== $target['custom']['channel']) { + // Check if this target is newer than the current version + if (isset($this->latest) && version_compare($target['custom']['version'], $this->latest->version, '<')) { continue; } - // Check if DB & version is supported via tag, assume supported if tag isn't present - if (isset($target['custom']['supported_databases'])) { - $db = Factory::getContainer()->get(DatabaseDriver::class); - $dbType = strtolower($db->getServerType()); - $dbVersion = $db->getVersion(); - $supportedDbs = $target['custom']['supported_databases']; - - // 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 (!isset($supportedDbs[$dbType]) || !version_compare($dbVersion, $supportedDbs[$dbType], '>=')) { - continue; - } - } - - // Check minimum stability - if (isset($target['custom']['stability']) && ($this->stabilityTagToInteger($target['custom']['stability']) < $minimumStability)) { - continue; - } + $this->latest = new \stdClass(); - if (!empty($target['custom']['downloads'])) { - $this->compatibleVersions[] = $target['custom']['version']; + foreach ($target['custom'] as $key => $val) { + $this->latest->$key = $val; } - if (!isset($this->latest) || version_compare($target['custom']['version'], $this->latest->version, '>')) { - $this->latest = new \stdClass(); + $this->downloadSources = []; - foreach ($target['custom'] as $key => $val) { - $this->latest->$key = $val; - } + if (!empty($this->latest->downloads)) { + foreach ($this->latest->downloads as $download) { + $source = new DownloadSource; - $this->downloadSources = []; - - if (!empty($this->latest->downloads)) { - foreach ($this->latest->downloads as $download) { - $source = new DownloadSource; - - foreach ($download as $key => $url) { - $key = strtolower($key); - $source->$key = $url; - } - - $this->downloadSources[] = $source; + foreach ($download as $key => $sourceUrl) { + $key = strtolower($key); + $source->$key = $sourceUrl; } + + $this->downloadSources[] = $source; } + } - $this->client = $this->latest->client; + $this->client = $this->latest->client; - if (isset($target['hashes'])) { - foreach ($target['hashes'] as $hashAlgorithm => $hashSum) { - $this->$hashAlgorithm = (object) ['_data' => $hashSum]; - } - } + 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)) { - // Ugly XML structure conversion - foreach (get_object_vars($this->latest) as $key => $val) { - if (in_array($key, ['infourl'])) { - $this->$key = (object) [ - '_data' => $val['url'], - ]; - - unset($val['url']); - - foreach ($val as $k => $v) { - $this->$key->$k = $v; - } - - continue; - } - elseif (in_array($key, ['targetplatform', 'supported_databases'])) { - $this->$key = (object) [ - '_data' => '', - ]; - - foreach ($val as $k => $v) { - $this->$key->$k = $v; - } - - continue; - } - elseif ($key === 'stability') { - $this->$key = $this->stabilityTagToInteger($val); - - continue; - } - - $this->$key = (object) ['_data' => $val]; - } - foreach ($this->downloadSources as $source) { $this->downloadurl = (object) [ '_data' => $source->url, diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php index 2316a54c7f5..c00956ed467 100644 --- a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -12,6 +12,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Updater\ConstraintChecker; +use Joomla\CMS\Version; use Joomla\Database\DatabaseDriver; use Joomla\Tests\Unit\UnitTestCase; @@ -65,7 +66,7 @@ public function testCheckMethodReturnsFalseIfPlatformIsMissing() public function testCheckMethodReturnsTrueIfPlatformIsOnlyConstraint() { - $constraint = ['targetplatform' => (object) ["name" => "joomla", "version" => "4.*"]]; + $constraint = ['targetplatform' => (array) ["name" => "joomla", "version" => JVERSION]]; $this->assertTrue($this->checker->check($constraint)); } @@ -136,37 +137,37 @@ protected function supportedDatabasesDataProvider() return [ [ ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], true ], [ ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], true ], [ ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], true ], [ ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], - (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + (array) ['mysql' => '5.8', 'mariadb' => '10.3'], false ], [ ['type' => 'pgsql', 'version' => '14.3'], - (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + (array) ['mysql' => '5.8', 'mariadb' => '10.3'], false ], [ ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], - (object) ['mysql' => '5.6', 'mariadb' => '10.4'], + (array) ['mysql' => '5.6', 'mariadb' => '10.4'], false ], [ ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], - (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + (array) ['mysql' => '5.6', 'mariadb' => '10.3'], true ], ]; @@ -182,14 +183,15 @@ protected function supportedDatabasesDataProvider() protected function targetplatformDataProvider() { return [ - [(object) ["name" => "foobar", "version" => "1.*"], false], - [(object) ["name" => "foobar", "version" => "4.*"], false], - [(object) ["name" => "joomla", "version" => "1.*"], false], - [(object) ["name" => "joomla", "version" => "3.1.2"], false], - [(object) ["name" => "joomla", "version" => ""], true], - [(object) ["name" => "joomla", "version" => ".*"], true], - [(object) ["name" => "joomla", "version" => JVERSION], true], - [(object) ["name" => "joomla", "version" => "4.*"], true], + [(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], ]; } From 9d6a481ec886031f397e9f3cf85b7f2359a754a0 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 10 Dec 2023 13:52:40 +0100 Subject: [PATCH 105/126] revert changes --- libraries/src/Table/Update.php | 6 ++-- libraries/src/Updater/Update.php | 50 ++++++++++++-------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/libraries/src/Table/Update.php b/libraries/src/Table/Update.php index 52652023bf3..c17146685b3 100644 --- a/libraries/src/Table/Update.php +++ b/libraries/src/Table/Update.php @@ -25,12 +25,12 @@ class Update extends Table { /** - * Ensure the data in json encoded in the bind method + * Ensure the params in json encoded in the bind method * * @var array - * @since __DEPLOY_VERSION__ + * @since 4.0.0 */ - protected $_jsonEncode = ['data']; + protected $_jsonEncode = ['params']; /** * Constructor diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 92d873795e2..5ad98bf6116 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -581,7 +581,24 @@ public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumSta */ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, $channel = null) { - $response = $this->loadUpdateInformation($url); + $version = new Version(); + $httpOption = new Registry(); + + $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); + + try { + $http = HttpFactory::getHttp($httpOption); + $response = $http->get($url); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + // @todo: Add a 'mark bad' setting here somehow + Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); + + return false; + } if (!$response) { return false; @@ -614,37 +631,6 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, return true; } - /** - * Load the update manifest file - * - * @param string $url - * - * @return Response|null - */ - protected function loadUpdateInformation($url) - { - $version = new Version(); - $httpOption = new Registry(); - - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); - - try { - $http = HttpFactory::getHttp($httpOption); - $response = $http->get($url); - } catch (\RuntimeException $e) { - $response = null; - } - - if ($response === null || $response->code !== 200) { - // @todo: Add a 'mark bad' setting here somehow - Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); - - return false; - } - - return $response; - } - /** * 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. From 11386a264d4b25eec5ef93b1049b700f7c5f833f Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 10 Dec 2023 13:53:55 +0100 Subject: [PATCH 106/126] cs fix --- libraries/src/Updater/Update.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 5ad98bf6116..23d3f676d6c 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -583,7 +583,6 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, { $version = new Version(); $httpOption = new Registry(); - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); try { @@ -600,10 +599,6 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, return false; } - if (!$response) { - return false; - } - $this->minimum_stability = $minimumStability; $this->channel = $channel; From c98ab26b8edda107ef8e374515f6b756a32f7d7c Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 10 Dec 2023 14:15:56 +0100 Subject: [PATCH 107/126] ensure that the shown version and the downloaded versions match --- .../src/Controller/UpdateController.php | 18 ++++++++++++++++++ .../com_joomlaupdate/src/Model/UpdateModel.php | 7 ++----- .../tmpl/joomlaupdate/update.php | 5 ++++- .../language/en-GB/com_joomlaupdate.ini | 1 + 4 files changed, 25 insertions(+), 6 deletions(-) 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 e28b0b5040f..72364cadf5c 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -390,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; } @@ -407,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; } } @@ -429,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'] = ''; + $displayData['formAppend'] = ' + + + '; endif; echo '
'; diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index cd9e5d3e373..c473c0dc190 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -171,6 +171,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 mismatch, 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" From bc481c7dfeb81ace5ccb4f186eb86903965223cc Mon Sep 17 00:00:00 2001 From: brian teeman Date: Sat, 6 Jan 2024 17:49:19 +0000 Subject: [PATCH 108/126] Comment typos code review only --- libraries/src/Updater/ConstraintChecker.php | 2 +- libraries/src/Updater/Update.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index b544ebd3ebc..662453e6f9f 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -44,7 +44,7 @@ public function check(array $candidate) return false; } - // Check php_minimumm, assume true when not set + // Check php_minimum, assume true when not set if ( isset($candidate['php_minimum']) && !$this->checkPhpMinimum($candidate['php_minimum']) diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 23d3f676d6c..3257708af2b 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -490,7 +490,7 @@ public function _characterData($parser, $data) * @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 channnel + * @param string $channel The update channel * * @return boolean True on success * From b58b7cc656655b47831e5465da5f53cb1f1a5b29 Mon Sep 17 00:00:00 2001 From: brian teeman Date: Sat, 6 Jan 2024 17:53:13 +0000 Subject: [PATCH 109/126] Lang String English grammar and style guide correction --- administrator/language/en-GB/com_joomlaupdate.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/language/en-GB/com_joomlaupdate.ini b/administrator/language/en-GB/com_joomlaupdate.ini index c473c0dc190..4f22e1d3eb4 100644 --- a/administrator/language/en-GB/com_joomlaupdate.ini +++ b/administrator/language/en-GB/com_joomlaupdate.ini @@ -171,7 +171,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 mismatch, try to refresh the update information" +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" From 9ecfe84318b7706f67b5f383b041abc14f91b4c9 Mon Sep 17 00:00:00 2001 From: brian teeman Date: Sat, 6 Jan 2024 18:01:10 +0000 Subject: [PATCH 110/126] more --- administrator/language/en-GB/lib_joomla.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index 49c06b61fc1..60a6baec3d5 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -658,13 +658,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 is expired." +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 message did not match the expected size" -JLIB_INSTALLER_TUF_ROLLBACK_ATTACK="Update not possible because the offered update version is older than the current installed version." -JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD="Update not possible because the offered update has not enough signatures." +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." From 78f2d3700193ec07c2ddbef48c72a91ce56b8fda Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 10:36:03 +0100 Subject: [PATCH 111/126] Added test case for databasestorage class --- .../Libraries/Tuf/DatabaseStorageTest.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/Unit/Libraries/Tuf/DatabaseStorageTest.php diff --git a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php new file mode 100644 index 00000000000..8f7c29e51ca --- /dev/null +++ b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php @@ -0,0 +1,106 @@ + + * @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; + +class DatabaseStorageTest extends UnitTestCase +{ + protected DatabaseStorage $object; + + public function testContructorWritesColumnMetadataToInternalStorage() + { + $table = $this->getTableMock(['root' => 'rootfoo']); + $object = new DatabaseStorage($table); + + $this->assertEquals('rootfoo', $this->getInternalStorageValue($object)['root']); + } + + public function testContructorIgnoresNonMetadataColumns() + { + $table = $this->getTableMock(['foobar' => 'aaa']); + $object = new DatabaseStorage($table); + + $this->assertArrayNotHasKey('foobar', $this->getInternalStorageValue($object)); + } + + public function testReadReturnsStorageValueForExistingColumns() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foobar'])); + $this->assertEquals('foobar', $object->read('root')); + } + + public function testReadReturnsNullForNonexistentColumns() + { + $object = new DatabaseStorage($this->getTableMock([])); + $this->assertNull($object->read('foobar')); + } + + public function testWriteUpdatesGivenInternalStorageValue() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); + $object->write('root', 'bar'); + + $this->assertEquals('bar', $this->getInternalStorageValue($object)['root']); + } + + public function testWriteCreatesNewInternalStorageValue() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); + $object->write('targets', 'bar'); + + $this->assertEquals('bar', $this->getInternalStorageValue($object)['targets']); + } + + public function testDeleteRemovesRowFromInternalStorage() + { + $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); + $object->delete('root'); + + $this->assertArrayNotHasKey('root', $this->getInternalStorageValue($object)); + } + + 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()); + } + + 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; + } + + protected function getInternalStorageValue($class) + { + $reflectionProperty = new \ReflectionProperty(DatabaseStorage::class, 'container'); + + return $reflectionProperty->getValue($class); + } +} From 4e299383b1afc837541def2c69ec957bda4b32f0 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 11:30:35 +0100 Subject: [PATCH 112/126] Added test for httploader class --- libraries/src/Factory.php | 3 +- libraries/src/Http/HttpFactory.php | 2 +- libraries/src/Http/HttpFactoryInterface.php | 15 +++ libraries/src/Service/Provider/Http.php | 48 ++++++++++ libraries/src/TUF/HttpLoader.php | 9 +- .../Libraries/Tuf/DatabaseStorageTest.php | 2 - tests/Unit/Libraries/Tuf/HttpLoaderTest.php | 95 +++++++++++++++++++ 7 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 libraries/src/Http/HttpFactoryInterface.php create mode 100644 libraries/src/Service/Provider/Http.php create mode 100644 tests/Unit/Libraries/Tuf/HttpLoaderTest.php 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/HttpLoader.php b/libraries/src/TUF/HttpLoader.php index b12c2b90810..753cc9d64b6 100644 --- a/libraries/src/TUF/HttpLoader.php +++ b/libraries/src/TUF/HttpLoader.php @@ -9,8 +9,10 @@ namespace Joomla\CMS\TUF; use GuzzleHttp\Promise\Create; -use Joomla\Http\HttpFactory; +use Joomla\CMS\Factory; use GuzzleHttp\Promise\PromiseInterface; +use Joomla\CMS\Http\Http; +use Joomla\CMS\Http\HttpFactoryInterface; use Tuf\Exception\RepoFileNotFound; use Tuf\Loader\LoaderInterface; @@ -22,9 +24,10 @@ public function __construct(private readonly string $repositoryPath) public function load(string $locator, int $maxBytes): PromiseInterface { - $httpFactory = new HttpFactory(); - // Get client instance + $httpFactory = Factory::getContainer()->get(HttpFactoryInterface::class); + + /** @var Http $client */ $client = $httpFactory->getHttp([], 'curl'); $response = $client->get($this->repositoryPath . $locator); diff --git a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php index 8f7c29e51ca..54e435ac65f 100644 --- a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php +++ b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php @@ -16,8 +16,6 @@ class DatabaseStorageTest extends UnitTestCase { - protected DatabaseStorage $object; - public function testContructorWritesColumnMetadataToInternalStorage() { $table = $this->getTableMock(['root' => 'rootfoo']); diff --git a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php new file mode 100644 index 00000000000..3937f05d922 --- /dev/null +++ b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php @@ -0,0 +1,95 @@ + + * @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\Http\Response; +use Joomla\CMS\TUF\HttpLoader; +use Joomla\Tests\Unit\UnitTestCase; +use Laminas\Diactoros\Stream; +use Tuf\Exception\RepoFileNotFound; + +class HttpLoaderTest extends UnitTestCase +{ + const REPOPATHMOCK = 'https://example.org/tuftest/'; + + protected HttpLoader $object; + + 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); + } + + 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() + ); + } + + 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); + } + + protected function getHttpFactoryMock($responseCode, $responseBody, $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; + } + + public function setUp(): void + { + $this->object = new HttpLoader(self::REPOPATHMOCK); + + parent::setUp(); + } +} From b03fd02095600e27c6d61164cff9c0d7cca29182 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 12:12:05 +0100 Subject: [PATCH 113/126] Added test cases for tuf adapter --- libraries/src/Updater/Adapter/TufAdapter.php | 83 ++++++----- .../Cms/Updater/ConstraintCheckerTest.php | 2 +- .../Libraries/Cms/Updater/TufAdapterTest.php | 132 ++++++++++++++++++ .../Libraries/Tuf/DatabaseStorageTest.php | 2 +- tests/Unit/Libraries/Tuf/HttpLoaderTest.php | 2 +- 5 files changed, 183 insertions(+), 38 deletions(-) create mode 100644 tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index 631efd3f517..86b86d3152d 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -68,13 +68,6 @@ public function findUpdate($options) public function getUpdateTargets($options) { $versions = array(); - $resolver = new OptionsResolver(); - - try { - $this->configureUpdateOptions($resolver); - $keys = $resolver->getDefinedOptions(); - } catch (\Exception $e) { - } /** @var MetadataTable $metadataTable */ $metadataTable = new MetadataTable(Factory::getDbo()); @@ -90,39 +83,15 @@ public function getUpdateTargets($options) } foreach ($metaData["signed"]["targets"] as $filename => $target) { - $values = []; - - if (!isset($target["hashes"])) { - throw new MetadataException("No trusted hashes are available for '$filename'"); - } - - foreach ($keys 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); + $version = $this->processTufTarget($filename, $target); - if (is_object($client)) { - $values['client'] = $client->id; - } - } - - if (isset($values['infourl']) && isset($values['infourl']['url'])) { - $values['infourl'] = $values['infourl']['url']; - } - - try { - $values = $resolver->resolve($values); - } catch (\Exception $e) { + if (!$version) { continue; } - $values['detailsurl'] = $options['location']; + $version['detailsurl'] = $options['location']; - $versions[$values['version']] = $values; + $versions[] = $version; } // We only want the latest version we support @@ -141,6 +110,50 @@ public function getUpdateTargets($options) 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 * diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php index c00956ed467..c8a78244aff 100644 --- a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -8,7 +8,7 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Tests\Unit\Libraries\Cms; +namespace Joomla\Tests\Unit\Libraries\Cms\Updater; use Joomla\CMS\Factory; use Joomla\CMS\Updater\ConstraintChecker; diff --git a/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php new file mode 100644 index 00000000000..497abbe36a0 --- /dev/null +++ b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php @@ -0,0 +1,132 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\Updater; + +use Joomla\CMS\Application\SiteApplication; +use Joomla\CMS\Updater\Adapter\TufAdapter; +use Joomla\Tests\Unit\UnitTestCase; +use Joomla\Utilities\ArrayHelper; +use Tuf\Exception\MetadataException; + +class TufAdapterTest extends UnitTestCase +{ + 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', []); + } + + 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']); + } + + 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']); + } + + 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; + } + + protected function getMockTarget($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 index 54e435ac65f..d5fdd706269 100644 --- a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php +++ b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php @@ -2,7 +2,7 @@ /** * @package Joomla.UnitTest - * @subpackage Access + * @subpackage Tuf * * @copyright (C) 2019 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt diff --git a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php index 3937f05d922..673612e441d 100644 --- a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php +++ b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php @@ -2,7 +2,7 @@ /** * @package Joomla.UnitTest - * @subpackage Access + * @subpackage Tuf * * @copyright (C) 2019 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt From f2dfc6e6c464d11d6e5e53b4c50a2e7d2570cae3 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 12:30:43 +0100 Subject: [PATCH 114/126] use forked TUF client and remove composer patches --- ...p-tuf_canonical-json-targetsMetadata.patch | 92 ------------------- .../5.0.0-2023-07-01_php-tuf_meta.patch | 15 --- ...23-07-01_php-tuf_sort-canonical-json.patch | 68 -------------- composer.json | 19 +--- composer.lock | 60 ++---------- 5 files changed, 11 insertions(+), 243 deletions(-) delete mode 100644 build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch delete mode 100644 build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch delete mode 100644 build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch diff --git a/build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch b/build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch deleted file mode 100644 index 254c0508ebe..00000000000 --- a/build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch +++ /dev/null @@ -1,92 +0,0 @@ -# -# ---- a/src/Metadata/MetadataBase.php -+++ b/src/Metadata/MetadataBase.php (date 1688224003871) -@@ -51,19 +51,6 @@ - { - } - -- /** -- * Returns a normalized array version of this object for JSON encoding. -- * -- * @see ::toCanonicalJson() -- * -- * @return array -- * A normalized array representation of this object. -- */ -- protected function toNormalizedArray(): array -- { -- return $this->getSigned(); -- } -- - /** - * Returns a canonical JSON representation of this metadata object. - * -@@ -72,7 +59,7 @@ - */ - public function toCanonicalJson(): string - { -- return static::encodeJson($this->toNormalizedArray()); -+ return static::encodeJson($this->getSigned()); - } - - /** - - ---- a/src/Metadata/TargetsMetadata.php -+++ b/src/Metadata/TargetsMetadata.php (date 1688224236309) -@@ -67,26 +67,32 @@ - - /** - * {@inheritDoc} -+ * Returns a canonical JSON representation of this metadata object. -+ * -+ * @return string -+ * The canonical JSON representation of this object. - */ -- protected function toNormalizedArray(): array -+ public function toCanonicalJson(): string - { -- $normalized = parent::toNormalizedArray(); -+ $metadata = $this->getSigned(); - -- foreach ($normalized['targets'] as $path => $target) { -+ // Apply sorting -+ self::sortKeys($metadata); -+ -+ foreach ($metadata['targets'] as $path => $target) { - // Custom target info should always encode to an object, even if - // it's empty. - if (array_key_exists('custom', $target)) { -- $normalized['targets'][$path]['custom'] = (object) $target['custom']; -+ $metadata['targets'][$path]['custom'] = (object) $target['custom']; - } - } - - // Ensure that these will encode as objects even if they're empty. -- $normalized['targets'] = (object) $normalized['targets']; -- if (array_key_exists('delegations', $normalized)) { -- $normalized['delegations']['keys'] = (object) $normalized['delegations']['keys']; -+ $metadata['targets'] = (object) $metadata['targets']; -+ if (array_key_exists('delegations', $metadata)) { -+ $metadata['delegations']['keys'] = (object)$metadata['delegations']['keys']; - } -- -- return $normalized; -+ return static::encodeJson($metadata); - } - - /** - - ---- a/src/CanonicalJsonTrait.php -+++ b/src/CanonicalJsonTrait.php (date 1688223879893) -@@ -53,7 +53,7 @@ - * @throws \RuntimeException - * Thrown if sorting the array fails. - */ -- private static function sortKeys(array &$data): void -+ protected static function sortKeys(array &$data): void - { - // If $data is numerically indexed, the keys are already sorted, by - // definition. diff --git a/build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch b/build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch deleted file mode 100644 index df9726f2c92..00000000000 --- a/build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch +++ /dev/null @@ -1,15 +0,0 @@ -# php-tuf/php-tuf - patch 20230701 -# TUF signed metadata should use semantic versioning 1.xx.xx while used TUF GO client drops patch level and uses -# 1.xx - therefor make patch level optional -# ---- a/src/Metadata/MetadataBase.php -+++ b/src/Metadata/MetadataBase.php (date 1688199141423) -@@ -179,7 +179,7 @@ - 'spec_version' => [ - new NotBlank(), - new Type('string'), -- new Regex('/^1\.[0-9]+\.[0-9]+$/'), -+ new Regex('/^1\.[0-9]+(\.[0-9]+)?$/'), - ], - ] + static::getVersionConstraints(), - 'allowExtraFields' => true, diff --git a/build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch b/build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch deleted file mode 100644 index 7eb5458e065..00000000000 --- a/build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch +++ /dev/null @@ -1,68 +0,0 @@ -# php-tuf/php-tuf - patch 20230701 -# Canonical json needs to be sorted in proper way -# ---- a/src/CanonicalJsonTrait.php -+++ b/src/CanonicalJsonTrait.php (date 1688219259891) -@@ -55,8 +55,14 @@ - */ - private static function sortKeys(array &$data): void - { -+ // Apply recursively on potential subarrays -+ foreach ($data as $key => $value) { -+ if (is_array($value)) { -+ static::sortKeys($data[$key]); -+ } -+ } - // If $data is numerically indexed, the keys are already sorted, by -- // definition. -+ // definition, no key sorting on this level necessary - if (array_is_list($data)) { - return; - } -@@ -65,10 +71,5 @@ - throw new \RuntimeException("Failure sorting keys. Canonicalization is not possible."); - } - -- foreach ($data as $key => $value) { -- if (is_array($value)) { -- static::sortKeys($data[$key]); -- } -- } - } - } - ---- a/tests/Unit/CanonicalJsonTraitTest.php -+++ b/tests/Unit/CanonicalJsonTraitTest.php (date 1688219312679) -@@ -55,6 +55,32 @@ - $this->assertSame([2, 3], array_keys($data['c'])); - } - -+ public function testSortForListArrays(): void -+ { -+ // Indexed arrays should not be sorted in alphabetical order, at any -+ // level. -+ $data = [ -+ // Use an associative nested array -+ 0 => [ -+ 'b' => 'Hey', -+ 'a' => 'Ho', -+ ], -+ // This should be sorted too, because PHP doesn't consider it a list. -+ 1 => [ -+ 3 => 'Hey', -+ 2 => 'Ho', -+ ], -+ ]; -+ -+ static::sortKeys($data); -+ -+ // The associative keys should be in canonical order now, and the -+ // nested, indexed array should be unchanged. -+ $this->assertSame([0,1], array_keys($data)); -+ $this->assertSame(['a', 'b'], array_keys($data['0'])); -+ $this->assertSame([2,3], array_keys($data[1])); -+ } -+ - /** - * @covers ::encodeJson - */ diff --git a/composer.json b/composer.json index 8f6d2035f70..0df62dd7bc9 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,7 @@ "vendor-dir": "libraries/vendor", "github-protocols": ["https"], "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "cweagans/composer-patches": true + "dealerdirect/phpcodesniffer-composer-installer": true } }, "support": { @@ -30,11 +29,11 @@ { "type": "vcs", "url": "https://github.com/joomla-backports/json-api-php.git", - "no-api": true + "no-api": true }, { "type": "vcs", - "url": "https://github.com/php-tuf/php-tuf.git", + "url": "https://github.com/joomla-backports/php-tuf.git", "no-api": true } ], @@ -107,8 +106,7 @@ "phpseclib/bcmath_compat": "^2.0.1", "jfcherng/php-diff": "^6.15.3", "voku/portable-utf8": "^6.0.13", - "php-tuf/php-tuf": "dev-main", - "cweagans/composer-patches": "^1.7" + "php-tuf/php-tuf": "dev-main" }, "require-dev": { "phpunit/phpunit": "^9.6.11", @@ -130,14 +128,7 @@ "symfony/polyfill-php81": "*" }, "extra": { - "composer-exit-on-patch-failure": true, - "patches": { - "php-tuf/php-tuf": { - "Patch level optional on TUF signed metadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_meta.patch", - "Sort canonical json": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_sort-canonical-json.patch", - "Fix invalid canonical JSON of TargetsMetadata": "./build/composer_patches/5.0.0-2023-07-01_php-tuf_canonical-json-targetsMetadata.patch" - } - } + "composer-exit-on-patch-failure": true }, "scripts": { "post-install-cmd": [ diff --git a/composer.lock b/composer.lock index 0adaaa1f5bf..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": "cb54f540e3f4e28bf4163ada9e1d343c", + "content-hash": "2b4e1f920883bf2cb2197de50e4bc0dd", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -193,54 +193,6 @@ ], "time": "2023-08-30T09:31:38+00:00" }, - { - "name": "cweagans/composer-patches", - "version": "1.7.3", - "source": { - "type": "git", - "url": "https://github.com/cweagans/composer-patches.git", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3.0" - }, - "require-dev": { - "composer/composer": "~1.0 || ~2.0", - "phpunit/phpunit": "~4.6" - }, - "type": "composer-plugin", - "extra": { - "class": "cweagans\\Composer\\Patches" - }, - "autoload": { - "psr-4": { - "cweagans\\Composer\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Cameron Eagans", - "email": "me@cweagans.net" - } - ], - "description": "Provides a way to patch Composer packages.", - "support": { - "issues": "https://github.com/cweagans/composer-patches/issues", - "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" - }, - "time": "2022-12-20T22:53:13+00:00" - }, { "name": "defuse/php-encryption", "version": "v2.4.0", @@ -3129,13 +3081,13 @@ "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/php-tuf/php-tuf.git", - "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378" + "url": "https://github.com/joomla-backports/php-tuf.git", + "reference": "1961502dff73cc2d2a3f19b930516a18bc0ccd22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-tuf/php-tuf/zipball/7c2c58c5275618307d6b143ae7b2f09e8bc2b378", - "reference": "7c2c58c5275618307d6b143ae7b2f09e8bc2b378", + "url": "https://api.github.com/repos/joomla-backports/php-tuf/zipball/1961502dff73cc2d2a3f19b930516a18bc0ccd22", + "reference": "1961502dff73cc2d2a3f19b930516a18bc0ccd22", "shasum": "" }, "require": { @@ -3197,7 +3149,7 @@ "MIT" ], "description": "PHP implementation of The Update Framework (TUF)", - "time": "2023-12-07T19:16:22+00:00" + "time": "2024-01-26T15:28:14+00:00" }, { "name": "phpmailer/phpmailer", From 9c41012f51307fab80007ab318e5b77a2601daf8 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 12:50:50 +0100 Subject: [PATCH 115/126] cs fixes --- libraries/src/TUF/DatabaseStorage.php | 5 +- libraries/src/TUF/HttpLoader.php | 4 ++ libraries/src/TUF/TufFetcher.php | 6 +- libraries/src/Table/Tuf.php | 4 +- libraries/src/Table/Update.php | 1 + libraries/src/Updater/Adapter/TufAdapter.php | 4 +- libraries/src/Updater/ConstraintChecker.php | 4 +- libraries/src/Updater/Update.php | 3 +- .../Cms/Updater/ConstraintCheckerTest.php | 12 +++- .../Libraries/Cms/Updater/TufAdapterTest.php | 67 ++++++++++++++----- .../Libraries/Tuf/DatabaseStorageTest.php | 64 +++++++++++++++++- tests/Unit/Libraries/Tuf/HttpLoaderTest.php | 40 ++++++++++- 12 files changed, 185 insertions(+), 29 deletions(-) diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index 2f3fa312cca..db02596efd4 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -1,4 +1,5 @@ refresh(); diff --git a/libraries/src/Table/Tuf.php b/libraries/src/Table/Tuf.php index 6c604164e9e..21b89fd2601 100644 --- a/libraries/src/Table/Tuf.php +++ b/libraries/src/Table/Tuf.php @@ -9,7 +9,9 @@ namespace Joomla\CMS\Table; -\defined('JPATH_PLATFORM') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * TUF map table diff --git a/libraries/src/Table/Update.php b/libraries/src/Table/Update.php index c17146685b3..dcf2df51b68 100644 --- a/libraries/src/Table/Update.php +++ b/libraries/src/Table/Update.php @@ -1,4 +1,5 @@ latest->downloads)) { foreach ($this->latest->downloads as $download) { - $source = new DownloadSource; + $source = new DownloadSource(); foreach ($download as $key => $sourceUrl) { $key = strtolower($key); @@ -548,7 +548,6 @@ public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumSta 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 diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php index c8a78244aff..fa18e59fb21 100644 --- a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -17,7 +17,7 @@ use Joomla\Tests\Unit\UnitTestCase; /** - * Test class for Version. + * Test class for Constraint Checker. * * @package Joomla.UnitTest * @subpackage Updater @@ -58,12 +58,22 @@ protected function tearDown(): void 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]]; diff --git a/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php index 497abbe36a0..5e17622fe2d 100644 --- a/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php +++ b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php @@ -10,14 +10,25 @@ namespace Joomla\Tests\Unit\Libraries\Cms\Updater; -use Joomla\CMS\Application\SiteApplication; 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); @@ -31,6 +42,11 @@ public function testProcessTufTargetThrowsExceptionIfHashesAreMissing() $method->invoke($object, 'nohash.json', []); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testProcesstuftargetAssignsCustomTargetKeys() { $object = $this->getMockBuilder(TufAdapter::class) @@ -40,7 +56,7 @@ public function testProcesstuftargetAssignsCustomTargetKeys() $method = $this->getPublicMethod($object, 'processTufTarget'); $result = $method->invoke($object, 'targets.json', $this->getMockTarget([ 'custom' => [ - 'name' => 'Testupdate', + 'name' => 'Testupdate', 'version' => '1.2.3', ], ])); @@ -49,6 +65,11 @@ public function testProcesstuftargetAssignsCustomTargetKeys() $this->assertSame('1.2.3', $result['version']); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testProcesstuftargetAssignsClientId() { $object = $this->getMockBuilder(TufAdapter::class) @@ -58,12 +79,17 @@ public function testProcesstuftargetAssignsClientId() $method = $this->getPublicMethod($object, 'processTufTarget'); $result = $method->invoke($object, 'targets.json', $this->getMockTarget([ - 'client' => 'site' + 'client' => 'site', ])); $this->assertSame(0, $result['client']); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testProcesstuftargetAssignsInfoUrl() { $object = $this->getMockBuilder(TufAdapter::class) @@ -75,7 +101,7 @@ public function testProcesstuftargetAssignsInfoUrl() $result = $method->invoke($object, 'targets.json', $this->getMockTarget([ 'custom' => [ 'infourl' => [ - 'url' => 'https://example.org' + 'url' => 'https://example.org', ] ] ])); @@ -97,32 +123,41 @@ public function testProcesstuftargetAssignsInfoUrl() protected function getPublicMethod($object, $method) { $reflectionClass = new \ReflectionClass($object); - $method = $reflectionClass->getMethod($method); + $method = $reflectionClass->getMethod($method); $method->setAccessible(true); return $method; } - protected function getMockTarget($overrides) + /** + * Target override data + * + * @param array $overrides + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + protected function getMockTarget(array $overrides) { return ArrayHelper::mergeRecursive( [ 'hashes' => [ - 'sha128' => '' + 'sha128' => '', ], 'custom' => [ - 'name' => 'Joomla', - 'type' => 'file', - 'version' => '1.2.3', + 'name' => 'Joomla', + 'type' => 'file', + 'version' => '1.2.3', 'targetplatform' => [ - 'name' => 'joomla', - 'version' => '(5\.[0-4])|^(4\.4)' + 'name' => 'joomla', + 'version' => '(5\.[0-4])|^(4\.4)', ], - 'php_minimum' => '8.1.0', - 'channel' => '5.x', - 'stability' => 'stable', + 'php_minimum' => '8.1.0', + 'channel' => '5.x', + 'stability' => 'stable', 'supported_databases' => [ - 'mariadb' => '10.4' + 'mariadb' => '10.4', ] ] ], diff --git a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php index d5fdd706269..679a0bbe0c1 100644 --- a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php +++ b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php @@ -14,8 +14,20 @@ 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']); @@ -24,6 +36,11 @@ public function testContructorWritesColumnMetadataToInternalStorage() $this->assertEquals('rootfoo', $this->getInternalStorageValue($object)['root']); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testContructorIgnoresNonMetadataColumns() { $table = $this->getTableMock(['foobar' => 'aaa']); @@ -32,18 +49,33 @@ public function testContructorIgnoresNonMetadataColumns() $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'])); @@ -52,6 +84,11 @@ public function testWriteUpdatesGivenInternalStorageValue() $this->assertEquals('bar', $this->getInternalStorageValue($object)['root']); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testWriteCreatesNewInternalStorageValue() { $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); @@ -60,6 +97,11 @@ public function testWriteCreatesNewInternalStorageValue() $this->assertEquals('bar', $this->getInternalStorageValue($object)['targets']); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testDeleteRemovesRowFromInternalStorage() { $object = new DatabaseStorage($this->getTableMock(['root' => 'foo'])); @@ -68,6 +110,11 @@ public function testDeleteRemovesRowFromInternalStorage() $this->assertArrayNotHasKey('root', $this->getInternalStorageValue($object)); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testPersistUpdatesTableObjectState() { $tableMock = $this->getTableMock(['root' => 'foo', 'targets' => 'Joomla', 'nonexistent' => 'value']); @@ -82,19 +129,32 @@ public function testPersistUpdatesTableObjectState() $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) - { + 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'); diff --git a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php index 673612e441d..ee5a334e1a4 100644 --- a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php +++ b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php @@ -19,12 +19,24 @@ use Laminas\Diactoros\Stream; use Tuf\Exception\RepoFileNotFound; +/** + * Test class for HttpLoader + * + * @package Joomla.UnitTest + * @subpackage Tuf + * @since __DEPLOY_VERSION__ + */ class HttpLoaderTest extends UnitTestCase { - const REPOPATHMOCK = 'https://example.org/tuftest/'; + protected const REPOPATHMOCK = 'https://example.org/tuftest/'; protected HttpLoader $object; + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testLoaderQueriesCorrectUrl() { $responseBody = $this->createMock(Stream::class); @@ -37,6 +49,11 @@ public function testLoaderQueriesCorrectUrl() $this->object->load('root.json', 2048); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testLoaderForwardsReturnedBodyFromHttpClient() { $responseBody = $this->createMock(Stream::class); @@ -52,6 +69,11 @@ public function testLoaderForwardsReturnedBodyFromHttpClient() ); } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function testLoaderThrowsExceptionForNon200Response() { $this->expectException(RepoFileNotFound::class); @@ -66,7 +88,16 @@ public function testLoaderThrowsExceptionForNon200Response() $this->object->load('root.json', 2048); } - protected function getHttpFactoryMock($responseCode, $responseBody, $expectedFile) + /** + * @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); @@ -86,6 +117,11 @@ protected function getHttpFactoryMock($responseCode, $responseBody, $expectedFil return $httpFactoryMock; } + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ public function setUp(): void { $this->object = new HttpLoader(self::REPOPATHMOCK); From e092f8d98c6f9f77bb640f96967b29fc62cadba7 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 12:58:10 +0100 Subject: [PATCH 116/126] cs fixes --- .../com_installer/src/Model/UpdateModel.php | 4 +- .../src/Model/UpdateModel.php | 2 +- libraries/src/TUF/DatabaseStorage.php | 3 +- libraries/src/TUF/HttpLoader.php | 4 +- libraries/src/TUF/TufFetcher.php | 4 +- libraries/src/Updater/Adapter/TufAdapter.php | 40 +++++++++---------- libraries/src/Updater/ConstraintChecker.php | 6 +-- libraries/src/Updater/Update.php | 11 +++-- .../Cms/Updater/ConstraintCheckerTest.php | 16 ++++---- .../Libraries/Cms/Updater/TufAdapterTest.php | 8 ++-- .../Libraries/Tuf/DatabaseStorageTest.php | 4 +- tests/Unit/Libraries/Tuf/HttpLoaderTest.php | 2 +- 12 files changed, 51 insertions(+), 53 deletions(-) diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index fd9d99a3c32..409ee61fbe8 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -336,8 +336,8 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE) continue; } - $app = Factory::getApplication(); - $db = $this->getDatabase(); + $app = Factory::getApplication(); + $db = $this->getDatabase(); $query = $db->getQuery(true) ->select('type') ->from('#__update_sites') diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 72364cadf5c..12a86ff5510 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -298,7 +298,7 @@ public function getUpdateInformation() $minimumStability = Updater::STABILITY_STABLE; $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - $channel = $comJoomlaupdateParams->get('updatesource', 'default'); + $channel = $comJoomlaupdateParams->get('updatesource', 'default'); if (\in_array($channel, ['testing', 'custom'])) { $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); diff --git a/libraries/src/TUF/DatabaseStorage.php b/libraries/src/TUF/DatabaseStorage.php index db02596efd4..34fed7d0e2e 100644 --- a/libraries/src/TUF/DatabaseStorage.php +++ b/libraries/src/TUF/DatabaseStorage.php @@ -12,7 +12,6 @@ use Joomla\CMS\Table\Table; use Joomla\CMS\Table\TableInterface; use Joomla\CMS\Table\Tuf; -use Joomla\Database\DatabaseDriver; use Tuf\Metadata\StorageBase; // phpcs:disable PSR1.Files.SideEffects @@ -74,7 +73,7 @@ public function persist(): bool $data = []; foreach (self::METADATA_COLUMNS as $column) { - if (!array_key_exists($column, $this->container)) { + if (!\array_key_exists($column, $this->container)) { continue; } diff --git a/libraries/src/TUF/HttpLoader.php b/libraries/src/TUF/HttpLoader.php index 875a3e57152..e7dd25fc488 100644 --- a/libraries/src/TUF/HttpLoader.php +++ b/libraries/src/TUF/HttpLoader.php @@ -10,8 +10,8 @@ namespace Joomla\CMS\TUF; use GuzzleHttp\Promise\Create; -use Joomla\CMS\Factory; use GuzzleHttp\Promise\PromiseInterface; +use Joomla\CMS\Factory; use Joomla\CMS\Http\Http; use Joomla\CMS\Http\HttpFactoryInterface; use Tuf\Exception\RepoFileNotFound; @@ -32,7 +32,7 @@ public function load(string $locator, int $maxBytes): PromiseInterface $httpFactory = Factory::getContainer()->get(HttpFactoryInterface::class); /** @var Http $client */ - $client = $httpFactory->getHttp([], 'curl'); + $client = $httpFactory->getHttp([], 'curl'); $response = $client->get($this->repositoryPath . $locator); if ($response->code !== 200) { diff --git a/libraries/src/TUF/TufFetcher.php b/libraries/src/TUF/TufFetcher.php index 246569e9ba5..4faaa34cae8 100644 --- a/libraries/src/TUF/TufFetcher.php +++ b/libraries/src/TUF/TufFetcher.php @@ -61,7 +61,7 @@ public function __construct(MetadataTable $metadataTable, string $repositoryUrl, { $this->metadataTable = $metadataTable; $this->repositoryUrl = $repositoryUrl; - $this->db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); + $this->db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); } @@ -73,7 +73,7 @@ public function __construct(MetadataTable $metadataTable, string $repositoryUrl, */ public function getValidUpdate() { - $httpLoader = new HttpLoader($this->repositoryUrl); + $httpLoader = new HttpLoader($this->repositoryUrl); $sizeCheckingLoader = new SizeCheckingLoader($httpLoader); $storage = new DatabaseStorage($this->metadataTable); diff --git a/libraries/src/Updater/Adapter/TufAdapter.php b/libraries/src/Updater/Adapter/TufAdapter.php index aacd9f217c0..d8def598d20 100644 --- a/libraries/src/Updater/Adapter/TufAdapter.php +++ b/libraries/src/Updater/Adapter/TufAdapter.php @@ -18,8 +18,8 @@ use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Tuf as MetadataTable; use Joomla\CMS\TUF\TufFetcher; -use Joomla\CMS\Updater\UpdateAdapter; use Joomla\CMS\Updater\ConstraintChecker; +use Joomla\CMS\Updater\UpdateAdapter; use Symfony\Component\OptionsResolver\OptionsResolver; use Tuf\Exception\MetadataException; @@ -55,7 +55,7 @@ public function findUpdate($options) } } - return array('update_sites' => array(), 'updates' => $updates); + return ['update_sites' => [], 'updates' => $updates]; } /** @@ -69,14 +69,14 @@ public function findUpdate($options) */ public function getUpdateTargets($options) { - $versions = array(); + $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 = $tufFetcher->getValidUpdate(); $metaData = json_decode($metaData, true); @@ -135,10 +135,10 @@ protected function processTufTarget(string $filename, array $target): bool|array } } - if (isset($values['client']) && is_string($values['client'])) { + if (isset($values['client']) && \is_string($values['client'])) { $client = ApplicationHelper::getClientInfo($values['client'], true); - if (is_object($client)) { + if (\is_object($client)) { $values['client'] = $client->id; } } @@ -169,21 +169,21 @@ 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, + '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' => '' + 'stability' => '', ] ) ->setAllowedTypes('version', 'string') diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php index 29395af1f53..0717a5e03e2 100644 --- a/libraries/src/Updater/ConstraintChecker.php +++ b/libraries/src/Updater/ConstraintChecker.php @@ -124,8 +124,8 @@ protected function checkPhpMinimum(string $phpMinimum) */ protected function checkSupportedDatabases(array $supportedDatabases) { - $db = Factory::getDbo(); - $dbType = strtolower($db->getServerType()); + $db = Factory::getDbo(); + $dbType = strtolower($db->getServerType()); $dbVersion = $db->getVersion(); // MySQL and MariaDB use the same database driver but not the same version numbers @@ -134,7 +134,7 @@ protected function checkSupportedDatabases(array $supportedDatabases) if (stripos($dbVersion, 'mariadb') !== false) { // MariaDB: Strip off any leading '5.5.5-', if present $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); - $dbType = 'mariadb'; + $dbType = 'mariadb'; } } diff --git a/libraries/src/Updater/Update.php b/libraries/src/Updater/Update.php index 62a777a05ba..3af945486bc 100644 --- a/libraries/src/Updater/Update.php +++ b/libraries/src/Updater/Update.php @@ -19,7 +19,6 @@ use Joomla\CMS\Table\Tuf as TufMetadata; use Joomla\CMS\TUF\TufFetcher; use Joomla\CMS\Version; -use Joomla\Http\Response; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects @@ -505,7 +504,7 @@ public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumSta $metaData = $tufFetcher->getValidUpdate(); - $data = json_decode($metaData, true); + $data = json_decode($metaData, true); $constraintChecker = new ConstraintChecker(); foreach ($data['signed']['targets'] as $target) { @@ -535,7 +534,7 @@ public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumSta $source = new DownloadSource(); foreach ($download as $key => $sourceUrl) { - $key = strtolower($key); + $key = strtolower($key); $source->$key = $sourceUrl; } @@ -554,8 +553,8 @@ public function loadFromTuf(TufMetadata $metadataTable, string $url, $minimumSta if (isset($this->latest)) { foreach ($this->downloadSources as $source) { $this->downloadurl = (object) [ - '_data' => $source->url, - 'type' => $source->type, + '_data' => $source->url, + 'type' => $source->type, 'format' => $source->format, ]; @@ -599,7 +598,7 @@ public function loadFromXml($url, $minimumStability = Updater::STABILITY_STABLE, } $this->minimum_stability = $minimumStability; - $this->channel = $channel; + $this->channel = $channel; $this->xmlParser = xml_parser_create(''); xml_set_object($this->xmlParser, $this); diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php index fa18e59fb21..7055902ebb1 100644 --- a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -148,37 +148,37 @@ protected function supportedDatabasesDataProvider() [ ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], (array) ['mysql' => '5.6', 'mariadb' => '10.3'], - true + true, ], [ ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], (array) ['mysql' => '5.6', 'mariadb' => '10.3'], - true + true, ], [ ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], (array) ['mysql' => '5.6', 'mariadb' => '10.3'], - true + true, ], [ ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], (array) ['mysql' => '5.8', 'mariadb' => '10.3'], - false + false, ], [ ['type' => 'pgsql', 'version' => '14.3'], (array) ['mysql' => '5.8', 'mariadb' => '10.3'], - false + false, ], [ ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], (array) ['mysql' => '5.6', 'mariadb' => '10.4'], - false + false, ], [ ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], (array) ['mysql' => '5.6', 'mariadb' => '10.3'], - true + true, ], ]; } @@ -218,7 +218,7 @@ protected function targetplatformDataProvider() protected function getPublicMethod($method) { $reflectionClass = new \ReflectionClass($this->checker); - $method = $reflectionClass->getMethod($method); + $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 index 5e17622fe2d..3718394a23d 100644 --- a/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php +++ b/tests/Unit/Libraries/Cms/Updater/TufAdapterTest.php @@ -102,8 +102,8 @@ public function testProcesstuftargetAssignsInfoUrl() 'custom' => [ 'infourl' => [ 'url' => 'https://example.org', - ] - ] + ], + ], ])); $this->assertSame('https://example.org', $result['infourl']); @@ -158,8 +158,8 @@ protected function getMockTarget(array $overrides) 'stability' => 'stable', 'supported_databases' => [ 'mariadb' => '10.4', - ] - ] + ], + ], ], $overrides ); diff --git a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php index 679a0bbe0c1..0f736acd89d 100644 --- a/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php +++ b/tests/Unit/Libraries/Tuf/DatabaseStorageTest.php @@ -30,7 +30,7 @@ class DatabaseStorageTest extends UnitTestCase */ public function testContructorWritesColumnMetadataToInternalStorage() { - $table = $this->getTableMock(['root' => 'rootfoo']); + $table = $this->getTableMock(['root' => 'rootfoo']); $object = new DatabaseStorage($table); $this->assertEquals('rootfoo', $this->getInternalStorageValue($object)['root']); @@ -43,7 +43,7 @@ public function testContructorWritesColumnMetadataToInternalStorage() */ public function testContructorIgnoresNonMetadataColumns() { - $table = $this->getTableMock(['foobar' => 'aaa']); + $table = $this->getTableMock(['foobar' => 'aaa']); $object = new DatabaseStorage($table); $this->assertArrayNotHasKey('foobar', $this->getInternalStorageValue($object)); diff --git a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php index ee5a334e1a4..21619dffb5a 100644 --- a/tests/Unit/Libraries/Tuf/HttpLoaderTest.php +++ b/tests/Unit/Libraries/Tuf/HttpLoaderTest.php @@ -13,8 +13,8 @@ use Joomla\CMS\Factory; use Joomla\CMS\Http\Http; use Joomla\CMS\Http\HttpFactoryInterface; -use Joomla\Http\Response; use Joomla\CMS\TUF\HttpLoader; +use Joomla\Http\Response; use Joomla\Tests\Unit\UnitTestCase; use Laminas\Diactoros\Stream; use Tuf\Exception\RepoFileNotFound; From f30c4ecd5caf5eb8b5e8eb7f74854edde4dc7e02 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 13:56:00 +0100 Subject: [PATCH 117/126] update system tests --- .../sql/updates/postgresql/5.1.0-2023-12-09.sql | 2 +- installation/sql/mysql/base.sql | 6 +++--- installation/sql/postgresql/base.sql | 6 +++--- .../components/com_joomlaupdate/Update.cy.js | 13 +++++++++++++ 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js 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 index d1ff0d98eed..4f8ffb5da8c 100644 --- 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 @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( "id" serial NOT NULL, -"extension_id" bigint DEFAULT 0 NOT NULL, +"update_site_id" bigint DEFAULT 0 NOT NULL, "root" text DEFAULT NULL, "targets" text DEFAULT NULL, "snapshot" text DEFAULT NULL, diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index ec8b4782f27..361ef117034 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -876,7 +876,7 @@ CREATE TABLE IF NOT EXISTS `#__updates` ( CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( `id` int NOT NULL AUTO_INCREMENT, - `extension_id` int DEFAULT 0, + `update_site_id` int DEFAULT 0, `root` text DEFAULT NULL, `targets` text DEFAULT NULL, `snapshot` text DEFAULT NULL, @@ -888,8 +888,8 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- -- Dumping data for table `#__tuf_metadata` -- -INSERT INTO `#__tuf_metadata` (`extension_id`, `root`) -SELECT `extension_id`, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM `#__extensions` WHERE `type`='file' AND `element`='joomla'; +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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); -- -------------------------------------------------------- diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 33830bb3db6..532abdf00a0 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -891,7 +891,7 @@ COMMENT ON TABLE "#__updates" IS 'Available Updates'; CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( "id" serial NOT NULL, -"extension_id" bigint DEFAULT 0 NOT NULL, +"update_site_id" bigint DEFAULT 0 NOT NULL, "root" text DEFAULT NULL, "target" text DEFAULT NULL, "snapshot" text DEFAULT NULL, @@ -906,8 +906,8 @@ COMMENT ON TABLE "#__tuf_metadata" IS 'Secure TUF Updates'; -- Dumping data for table "#__tuf_metadata" -- -INSERT INTO "#__tuf_metadata" ("extension_id", "root") -SELECT "extension_id", '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}' FROM "#__extensions" WHERE "type"='file' AND "element"='joomla'; +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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); -- -- Table structure for table `#__update_sites` 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..a38e2c332a0 --- /dev/null +++ b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js @@ -0,0 +1,13 @@ +describe('Test in backend that the field form', () => { + beforeEach(() => { + cy.doAdministratorLogin(); + }); + + it('Can fetch available updates', () => { + 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'); + }); +}); From 87ed8dfd74ddf5ed592ea2511cdf8fd2fd20c910 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 14:39:22 +0100 Subject: [PATCH 118/126] added system test for both cases --- .../System/fixtures/tuf/invalidMetadata.json | 117 ++++++++++++++ tests/System/fixtures/tuf/validMetadata.json | 148 ++++++++++++++++++ .../components/com_joomlaupdate/Update.cy.js | 24 ++- tests/System/support/commands/db.js | 47 ++++++ 4 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 tests/System/fixtures/tuf/invalidMetadata.json create mode 100644 tests/System/fixtures/tuf/validMetadata.json 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..b688bb5be55 --- /dev/null +++ b/tests/System/fixtures/tuf/validMetadata.json @@ -0,0 +1,148 @@ +{ + "root": { + "signed": { + "_type": "root", + "spec_version": "1.0", + "version": 5, + "expires": "2025-06-10T12:42:53Z", + "keys": { + "019b571e47e8dff8fa88f198117c9d3ddd687c0a33f0950ee0c1a2ef52317752": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "7d4c2b09775707ba7bed3725749660c6070e21d548f535ae3dc65b3700936c3f" + } + }, + "1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff" + } + }, + "696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c" + } + }, + "70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b" + } + }, + "92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7" + } + }, + "bfb5797114c9556476d27ff16a8e4335f95a8684a889e820211156652f9b7c32": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "8f719efa1d9f7c529409755cc124ac556f83df457f56373dedf3ea1b2a875d39" + } + }, + "ef9165a98b1cb074734e6d6b49252eaf3d5d3a5a194f6f8776a24aaed9b3555d": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "2d937a2581eb7b1affe79ee230a2060d6aa9248b0f9f85b8c9b58d8e91c2f662" + } + }, + "f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97" + } + } + }, + "roles": { + "root": { + "keyids": [ + "70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9", + "ID", + "ID", + "ID" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a", + "019b571e47e8dff8fa88f198117c9d3ddd687c0a33f0950ee0c1a2ef52317752", + "bfb5797114c9556476d27ff16a8e4335f95a8684a889e820211156652f9b7c32", + "ef9165a98b1cb074734e6d6b49252eaf3d5d3a5a194f6f8776a24aaed9b3555d" + ], + "threshold": 2 + }, + "timestamp": { + "keyids": [ + "1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0", + "92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b" + ], + "threshold": 1 + } + }, + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "65c2141760701c808ed5cf6efd8b6ced421591ff553a468b5fd115ac46cdf979", + "sig": "950cab2057c54315f28138ec63e9e6da73640796b2f74d1b114cb34b7b4e752b931afcb4783c5dbd28c5ad8df247d4bf84362ee89775248a8ad3fea76cf13d0e" + }, + { + "keyid": "70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750", + "sig": "0d99ff3d5b403df27f2d1333e1acdd34ee931336e3b48158543fb90c351d71b5babfcc75d3eeba3c5b7eaa7434ba1fa0842bfd381f74b8fba9050d2e49a42e07" + } + ] + } +, + "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 index a38e2c332a0..78f61bcfdfd 100644 --- a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js +++ b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js @@ -1,13 +1,31 @@ -describe('Test in backend that the field form', () => { + + +describe('Test the update retrieval logic', () => { beforeEach(() => { cy.doAdministratorLogin(); }); - it('Can fetch available updates', () => { + 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('#confirmButton').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..06dcd88a610 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,47 @@ 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: '' + } + )) +}); From 0d8c3e9559a6ddef695872c3d5b2fec14cac4875 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sat, 27 Jan 2024 17:21:02 +0100 Subject: [PATCH 119/126] fix codestyles --- .../components/com_joomlaupdate/Update.cy.js | 12 ++++----- tests/System/support/commands/db.js | 26 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js index 78f61bcfdfd..b2bb4c8831f 100644 --- a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js +++ b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js @@ -1,30 +1,28 @@ - - describe('Test the update retrieval logic', () => { beforeEach(() => { cy.doAdministratorLogin(); }); afterEach(() => { - cy.db_setValidTufRoot() + cy.db_setValidTufRoot(); }); it('Can fetch available updates with valid metadata', () => { - cy.db_setValidTufRoot() + cy.db_setValidTufRoot(); cy.visit('/administrator/index.php?option=com_joomlaupdate'); - cy.get('#confirmButton').click() + cy.get('#confirmButton').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.db_setInvalidTufRoot(); cy.visit('/administrator/index.php?option=com_joomlaupdate'); - cy.get('#confirmButton').click() + 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 06dcd88a610..bde2979a269 100644 --- a/tests/System/support/commands/db.js +++ b/tests/System/support/commands/db.js @@ -1,5 +1,5 @@ -import invalidTufMetadata from '../../fixtures/tuf/invalidMetadata.json' -import validTufMetadata from '../../fixtures/tuf/validMetadata.json' +import invalidTufMetadata from '../../fixtures/tuf/invalidMetadata.json'; +import validTufMetadata from '../../fixtures/tuf/validMetadata.json'; /** * The global cached default categories @@ -603,15 +603,14 @@ Cypress.Commands.add('db_getUserId', () => { }); }); - /** * 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', 'DELETE FROM #__tuf_metadata WHERE id = 1'); + cy.task('queryDB', 'DELETE FROM #__updates WHERE update_site_id = 1'); cy.task('queryDB', createInsertQuery( 'tuf_metadata', { @@ -620,20 +619,19 @@ Cypress.Commands.add('db_setInvalidTufRoot', () => { root: JSON.stringify(invalidTufMetadata.root), targets: '', snapshot: '', - timestamp: '' - } - )) + 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', 'DELETE FROM #__tuf_metadata WHERE id = 1'); + cy.task('queryDB', 'DELETE FROM #__updates WHERE update_site_id = 1'); cy.task('queryDB', createInsertQuery( 'tuf_metadata', { @@ -642,7 +640,7 @@ Cypress.Commands.add('db_setValidTufRoot', () => { root: JSON.stringify(validTufMetadata.root), targets: '', snapshot: '', - timestamp: '' - } - )) + timestamp: '', + }, + )); }); From 91a06765ab32552a4f143b762a3211d40b31d44f Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 28 Jan 2024 09:27:49 +0100 Subject: [PATCH 120/126] update base sql files --- installation/sql/mysql/base.sql | 2 +- installation/sql/postgresql/base.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 361ef117034..f23fbd152f9 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -889,7 +889,7 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- Dumping data for table `#__tuf_metadata` -- 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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); +VALUES (1, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); -- -------------------------------------------------------- diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 532abdf00a0..9f4ca54a8e9 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -907,7 +907,7 @@ COMMENT ON TABLE "#__tuf_metadata" IS '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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); +VALUES (1, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); -- -- Table structure for table `#__update_sites` From 4874fabc8c040eb344f3f916396a1c1268214463 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 28 Jan 2024 09:51:09 +0100 Subject: [PATCH 121/126] fix test --- .../administrator/components/com_joomlaupdate/Update.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js index b2bb4c8831f..ece4009ee02 100644 --- a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js +++ b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js @@ -12,7 +12,7 @@ describe('Test the update retrieval logic', () => { cy.visit('/administrator/index.php?option=com_joomlaupdate'); - cy.get('#confirmButton').click(); + cy.get('#toolbar joomla-toolbar-button[task="update.purge"] button').click() cy.get('#system-message-container').contains('Checked for updates.').should('exist'); }); From 0683f95c9303e1a75011630d7e615dbb0f90dbf4 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Sun, 28 Jan 2024 10:00:45 +0100 Subject: [PATCH 122/126] cs fix --- .../administrator/components/com_joomlaupdate/Update.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js index ece4009ee02..bc81d06389e 100644 --- a/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js +++ b/tests/System/integration/administrator/components/com_joomlaupdate/Update.cy.js @@ -12,7 +12,7 @@ describe('Test the update retrieval logic', () => { cy.visit('/administrator/index.php?option=com_joomlaupdate'); - cy.get('#toolbar joomla-toolbar-button[task="update.purge"] button').click() + cy.get('#toolbar joomla-toolbar-button[task="update.purge"] button').click(); cy.get('#system-message-container').contains('Checked for updates.').should('exist'); }); From 037b860286c2bc64811fa7fd25436fc144c9f8ce Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 1 Feb 2024 21:01:22 +0100 Subject: [PATCH 123/126] fix final repo config --- .../sql/updates/mysql/5.1.0-2023-12-09.sql | 2 +- .../updates/postgresql/5.1.0-2023-12-09.sql | 2 +- .../src/Model/UpdateModel.php | 2 +- installation/sql/mysql/base.sql | 4 +- installation/sql/postgresql/base.sql | 4 +- tests/System/fixtures/tuf/validMetadata.json | 87 +++++-------------- 6 files changed, 31 insertions(+), 70 deletions(-) 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 index 40348a2631d..914b434663a 100644 --- 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 @@ -15,4 +15,4 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- -------------------------------------------------------- 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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); +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"}]}'); 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 index 4f8ffb5da8c..3822307c119 100644 --- 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 @@ -16,4 +16,4 @@ PRIMARY KEY ("id") COMMENT ON TABLE "#__tuf_metadata" IS '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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); +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"}]}'); diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 12a86ff5510..df262f2e595 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -116,7 +116,7 @@ public function applyUpdateSite() * case 'nochange': */ default: - $updateURL = 'https://raw.githubusercontent.com/joomla/updates/dec23-target/repository/'; + $updateURL = 'https://update.joomla.org/cms/'; } $updateType = (pathinfo($updateURL, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf'; diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index f23fbd152f9..d100c0c629b 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -889,7 +889,7 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- Dumping data for table `#__tuf_metadata` -- INSERT INTO `#__tuf_metadata` (`update_site_id`, `root`) -VALUES (1, '{"signed":{"_type":"root","spec_version":"1.0","version":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); +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"}]}'); -- -------------------------------------------------------- @@ -915,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', 'tuf', 'https://raw.githubusercontent.com/joomla/updates/beta-target/repository/', 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 9f4ca54a8e9..eb7cf8c9b0f 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -907,7 +907,7 @@ 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":1,"expires":"2028-12-06T15:31:52Z","keys":{"1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff"}},"696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c"}},"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b"}},"92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7"}},"f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97"}}},"roles":{"root":{"keyids":["70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750"],"threshold":1},"snapshot":{"keyids":["f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9"],"threshold":1},"targets":{"keyids":["696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a"],"threshold":1},"timestamp":{"keyids":["1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0","92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750","sig":"52f8de5d8c0ac8c532a4e3c274b3e22cd2dca57a9f5d4094ccc1ded9966fb7064acc589ad564ba7ba04f7dfb42d8ccb803811b73551c60df4f9996c116967e00"}]}'); +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` @@ -933,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', 'tuf', 'https://raw.githubusercontent.com/joomla/updates/beta-target/repository/', 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/tests/System/fixtures/tuf/validMetadata.json b/tests/System/fixtures/tuf/validMetadata.json index b688bb5be55..c8b73173b5f 100644 --- a/tests/System/fixtures/tuf/validMetadata.json +++ b/tests/System/fixtures/tuf/validMetadata.json @@ -3,10 +3,10 @@ "signed": { "_type": "root", "spec_version": "1.0", - "version": 5, - "expires": "2025-06-10T12:42:53Z", + "version": 2, + "expires": "2025-03-02T11:22:17Z", "keys": { - "019b571e47e8dff8fa88f198117c9d3ddd687c0a33f0950ee0c1a2ef52317752": { + "07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f": { "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": [ @@ -14,10 +14,10 @@ "sha512" ], "keyval": { - "public": "7d4c2b09775707ba7bed3725749660c6070e21d548f535ae3dc65b3700936c3f" + "public": "9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9" } }, - "1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0": { + "1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669": { "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": [ @@ -25,10 +25,10 @@ "sha512" ], "keyval": { - "public": "71c24873013b6f21aca791f45dcd9ddb5842a97bf72ac73c211742c2659a97ff" + "public": "a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799" } }, - "696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a": { + "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e": { "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": [ @@ -36,10 +36,10 @@ "sha512" ], "keyval": { - "public": "9fac963aac4e14f948a7c2d6b3fa2232f6cb5a08bf6a8b6100bc6e68b0683c1c" + "public": "cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9" } }, - "70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750": { + "31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3": { "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": [ @@ -47,10 +47,10 @@ "sha512" ], "keyval": { - "public": "d08225342af7a8075bf210bd62154567140a8e14d824743e58b8e7e64ee8ad0b" + "public": "589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2" } }, - "92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b": { + "9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc": { "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": [ @@ -58,72 +58,34 @@ "sha512" ], "keyval": { - "public": "8d70ac7574e64f209bff3d7c1d8b8ab6e34cf4419dd09f0d222354dceee986d7" - } - }, - "bfb5797114c9556476d27ff16a8e4335f95a8684a889e820211156652f9b7c32": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "8f719efa1d9f7c529409755cc124ac556f83df457f56373dedf3ea1b2a875d39" - } - }, - "ef9165a98b1cb074734e6d6b49252eaf3d5d3a5a194f6f8776a24aaed9b3555d": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "2d937a2581eb7b1affe79ee230a2060d6aa9248b0f9f85b8c9b58d8e91c2f662" - } - }, - "f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "b7a3d08989b5885d78e93425daacf3a71b0e190759e1a8633aa41bdb3ec3cd97" + "public": "6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9" } } }, "roles": { "root": { "keyids": [ - "70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750" + "1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669", + "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e" ], "threshold": 1 }, "snapshot": { "keyids": [ - "f9854d7c61e9413f4d83678be7d50310cc9e062027746d8936ba4736e75224e9", - "ID", - "ID", - "ID" + "07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f", + "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e" ], "threshold": 1 }, "targets": { "keyids": [ - "696a7598c714e545bb8a3a4248d82bf4c66486d142e226c1e06601a14f4d939a", - "019b571e47e8dff8fa88f198117c9d3ddd687c0a33f0950ee0c1a2ef52317752", - "bfb5797114c9556476d27ff16a8e4335f95a8684a889e820211156652f9b7c32", - "ef9165a98b1cb074734e6d6b49252eaf3d5d3a5a194f6f8776a24aaed9b3555d" + "31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3" ], - "threshold": 2 + "threshold": 1 }, "timestamp": { "keyids": [ - "1689c5951cfc8a8cb4e3535c6ddc3f8d5c66e2effd4b7aae3506995f145da2a0", - "92933ea840e57ad3db67c748d1a309c4a7d8be3f70d8bbbd3cff9c4cca3bcf7b" + "9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc" ], "threshold": 1 } @@ -132,16 +94,15 @@ }, "signatures": [ { - "keyid": "65c2141760701c808ed5cf6efd8b6ced421591ff553a468b5fd115ac46cdf979", - "sig": "950cab2057c54315f28138ec63e9e6da73640796b2f74d1b114cb34b7b4e752b931afcb4783c5dbd28c5ad8df247d4bf84362ee89775248a8ad3fea76cf13d0e" + "keyid": "2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e", + "sig": "2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501" }, { - "keyid": "70c4fb4ffe87b8d75559092c75bc038d587790bf2ecb9d8d6c6c0fae6705c750", - "sig": "0d99ff3d5b403df27f2d1333e1acdd34ee931336e3b48158543fb90c351d71b5babfcc75d3eeba3c5b7eaa7434ba1fa0842bfd381f74b8fba9050d2e49a42e07" + "keyid": "1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669", + "sig": "8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005" } ] - } -, + }, "snapshot": {}, "timestamp": {}, "targets": {} From e1b7a139125e943a907e44ef41f9916560d53b85 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 2 Feb 2024 08:56:57 +0100 Subject: [PATCH 124/126] fix posgres inserts --- .../com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql | 4 ++-- installation/sql/postgresql/base.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 3822307c119..6997d9f8c8a 100644 --- 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 @@ -15,5 +15,5 @@ PRIMARY KEY ("id") COMMENT ON TABLE "#__tuf_metadata" IS '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"}]}'); +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"}]}'); diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index eb7cf8c9b0f..b27bcfe7fdf 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -906,7 +906,7 @@ COMMENT ON TABLE "#__tuf_metadata" IS 'Secure TUF Updates'; -- Dumping data for table "#__tuf_metadata" -- -INSERT INTO `#__tuf_metadata` (`update_site_id`, `root`) +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"}]}'); -- From 95edb7524079da5c21fb9c23efae9dd9515a41a8 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Fri, 2 Feb 2024 09:28:59 +0100 Subject: [PATCH 125/126] fix postgres column name --- installation/sql/postgresql/base.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index b27bcfe7fdf..5b06c905c4a 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -893,7 +893,7 @@ CREATE TABLE IF NOT EXISTS "#__tuf_metadata" ( "id" serial NOT NULL, "update_site_id" bigint DEFAULT 0 NOT NULL, "root" text DEFAULT NULL, -"target" text DEFAULT NULL, +"targets" text DEFAULT NULL, "snapshot" text DEFAULT NULL, "timestamp" text DEFAULT NULL, "mirrors" text DEFAULT NULL, From 8662e0ee62e1f1239e9380f3ce1afd9d673934cf Mon Sep 17 00:00:00 2001 From: Martina Scholz <64533137+LadySolveig@users.noreply.github.com> Date: Fri, 9 Feb 2024 13:26:28 +0100 Subject: [PATCH 126/126] fix missing query for updates_sites table --- .../com_admin/sql/updates/mysql/5.1.0-2023-12-09.sql | 5 +++++ .../com_admin/sql/updates/postgresql/5.1.0-2023-12-09.sql | 4 ++++ 2 files changed, 9 insertions(+) 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 index 914b434663a..e325600022f 100644 --- 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 @@ -16,3 +16,8 @@ CREATE TABLE IF NOT EXISTS `#__tuf_metadata` ( -- -------------------------------------------------------- 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 index 6997d9f8c8a..cda10e2c2e6 100644 --- 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 @@ -17,3 +17,7 @@ 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