From d2c402f038904df52e663ca22fd11d2bc4cdefb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Bartus?= Date: Fri, 22 Sep 2023 09:35:54 +0100 Subject: [PATCH 01/11] [ticket/15851] Automatic update downloader PHPBB3-15851 --- phpBB/composer.json | 1 + phpBB/config/default/container/parameters.yml | 2 + phpBB/config/default/container/services.yml | 1 + .../default/container/services_updater.yml | 14 ++ phpBB/includes/acp/acp_update.php | 20 +++ phpBB/language/en/install.php | 7 + .../task/check_server_environment.php | 2 +- phpBB/phpbb/update/controller.php | 137 ++++++++++++++ phpBB/phpbb/update/get_updates.php | 169 ++++++++++++++++++ 9 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 phpBB/config/default/container/services_updater.yml create mode 100644 phpBB/phpbb/update/controller.php create mode 100644 phpBB/phpbb/update/get_updates.php diff --git a/phpBB/composer.json b/phpBB/composer.json index c77ab77819..7430017299 100644 --- a/phpBB/composer.json +++ b/phpBB/composer.json @@ -29,6 +29,7 @@ "php": "^8.1", "ext-pdo": "*", "ext-zlib": "*", + "ext-sodium": "*", "bantu/ini-get-wrapper": "~1.0", "carlos-mg89/oauth": "^0.8.15", "chita/topological_sort": "^3.0", diff --git a/phpBB/config/default/container/parameters.yml b/phpBB/config/default/container/parameters.yml index 8fcb401914..ba9b7aff18 100644 --- a/phpBB/config/default/container/parameters.yml +++ b/phpBB/config/default/container/parameters.yml @@ -20,3 +20,5 @@ parameters: - passwords.driver.bcrypt - passwords.driver.salted_md5 - passwords.driver.phpass + + public_key: 'auJX0pGetfYatE7t/rX5hAkCLZv9s78TwKkLfR3YGuQ=' diff --git a/phpBB/config/default/container/services.yml b/phpBB/config/default/container/services.yml index c09f21b5c6..ecf8f3109e 100644 --- a/phpBB/config/default/container/services.yml +++ b/phpBB/config/default/container/services.yml @@ -37,6 +37,7 @@ imports: - { resource: services_twig.yml } - { resource: services_twig_extensions.yml } - { resource: services_ucp.yml } + - { resource: services_updater.yml } - { resource: services_user.yml } - { resource: tables.yml } diff --git a/phpBB/config/default/container/services_updater.yml b/phpBB/config/default/container/services_updater.yml new file mode 100644 index 0000000000..0db8edf014 --- /dev/null +++ b/phpBB/config/default/container/services_updater.yml @@ -0,0 +1,14 @@ +services: + updater.get_updates: + class: phpbb\update\get_updates + arguments: + - '@filesystem' + - '%public_key%' + - '%core.root_path%' + + updater.controller: + class: phpbb\update\controller + arguments: + - '@filesystem' + - '@updater.get_updates' + - '%core.root_path%' diff --git a/phpBB/includes/acp/acp_update.php b/phpBB/includes/acp/acp_update.php index fa3afa6ce3..0c542cad06 100644 --- a/phpBB/includes/acp/acp_update.php +++ b/phpBB/includes/acp/acp_update.php @@ -38,12 +38,32 @@ class acp_update try { $recheck = $request->variable('versioncheck_force', false); + $do_update = $request->variable('do_update', false); + $updates_available = $version_helper->get_update_on_branch($recheck); $upgrades_available = $version_helper->get_suggested_updates(); + $branch = ''; if (!empty($upgrades_available)) { + $branch = array_key_last($upgrades_available); $upgrades_available = array_pop($upgrades_available); } + + if ($do_update && !empty($updates_available)) + { + $updater = $phpbb_container->get('updater.controller'); + $current_version = $config['version']; + $new_version = $upgrades_available['current']; + $download_url = 'https://download.phpbb.com/pub/release/'; + $download_url .= $branch . '/' . $new_version . '/'; + $download_url .= 'phpBB-' . $current_version . '_to_' . $new_version . '.zip'; + $data = $updater->handle( + $download_url + ); + + $response = new \phpbb\json_response(); + $response->send($data); + } } catch (\RuntimeException $e) { diff --git a/phpBB/language/en/install.php b/phpBB/language/en/install.php index 50ec74f933..f355d1427a 100644 --- a/phpBB/language/en/install.php +++ b/phpBB/language/en/install.php @@ -221,6 +221,13 @@ $lang = array_merge($lang, array(

We noticed that the last update of your phpBB installation hasn’t been completed. Visit the database updater, ensure Update database only is selected and click on Submit. Don\'t forget to delete the "install"-directory after you have updated the database successfully.

', + // Auto update + 'COULD_NOT_DOWNLOAD_UPDATE_PACKAGE' => 'Failed to download the update package.', + 'COULD_NOT_DOWNLOAD_UPDATE_SIGNATURE' => 'Failed to download the update package signature.', + 'UPDATE_SIGNATURE_INVALID' => 'The update package is corrupted.', + 'COULD_NOT_EXTRACT_UPDATE' => 'Could not extract files from the update package.', + 'COULD_NOT_WRITE_UPDATE_FILES' => 'Could not copy files from the update package.', + // // Server data // diff --git a/phpBB/phpbb/install/module/requirements/task/check_server_environment.php b/phpBB/phpbb/install/module/requirements/task/check_server_environment.php index bb43ed4b2f..9ae953ee8e 100644 --- a/phpBB/phpbb/install/module/requirements/task/check_server_environment.php +++ b/phpBB/phpbb/install/module/requirements/task/check_server_environment.php @@ -96,7 +96,7 @@ class check_server_environment extends \phpbb\install\task_base */ protected function check_php_version() { - if (version_compare(PHP_VERSION, '7.2.0', '<')) + if (version_compare(PHP_VERSION, '8.1.0', '<')) { $this->response_helper->add_error_message('PHP_VERSION_REQD', 'PHP_VERSION_REQD_EXPLAIN'); diff --git a/phpBB/phpbb/update/controller.php b/phpBB/phpbb/update/controller.php new file mode 100644 index 0000000000..5253f2b3d3 --- /dev/null +++ b/phpBB/phpbb/update/controller.php @@ -0,0 +1,137 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\update; + +use phpbb\filesystem\filesystem_interface; +use phpbb\language\language; + +class controller +{ + /** @var filesystem_interface Filesystem manager */ + private filesystem_interface $filesystem; + + /** @var get_updates Updater class */ + private get_updates $updater; + + /** @var language Translation handler */ + private language $language; + + /** @var string phpBB root path */ + private string $phpbb_root_path; + + /** + * Constructor. + * + * @param filesystem_interface $filesystem + * @param get_updates $updater + * @param language $language + * @param string $phpbb_root_path + */ + public function __construct( + filesystem_interface $filesystem, + get_updates $updater, + language $language, + string $phpbb_root_path) + { + $this->filesystem = $filesystem; + $this->language = $language; + $this->updater = $updater; + $this->phpbb_root_path = $phpbb_root_path; + } + + /** + * Handle requests. + * + * @param string $download The download URL. + * + * @return string[] Unencoded json response. + */ + public function handle(string $download): array + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + $status = ['status' => 'continue']; + if (!file_exists($update_path)) + { + $result = $this->updater->download($download, $update_path); + if (!$result) + { + return [ + 'status' => 'error', + 'error' => $this->language->lang('COULD_NOT_DOWNLOAD_UPDATE_PACKAGE') + ]; + } + + return $status; + } + + if (!file_exists($update_path . '.sig')) + { + $result = $this->updater->download($download . '.sig', $update_path . '.sig'); + if (!$result) + { + return [ + 'status' => 'error', + 'error' => $this->language->lang('COULD_NOT_DOWNLOAD_UPDATE_SIGNATURE') + ]; + } + return $status; + } + + if (!is_dir($this->phpbb_root_path . 'store/update')) + { + $result = $this->updater->validate($update_path, $update_path . '.sig'); + if (!$result) + { + return [ + 'status' => 'error', + 'error' => $this->language->lang('UPDATE_SIGNATURE_INVALID') + ]; + } + + $result = $this->updater->extract($update_path, $this->phpbb_root_path . 'store/update'); + if (!$result) + { + return [ + 'status' => 'error', + 'error' => $this->language->lang('COULD_NOT_EXTRACT_UPDATE') + ]; + } + + return $status; + } + + if (!is_dir($this->phpbb_root_path . 'install')) + { + $result = $this->updater->copy($this->phpbb_root_path . 'store/update'); + if (!$result) + { + return [ + 'status' => 'error', + 'error' => $this->language->lang('COULD_NOT_WRITE_UPDATE_FILES') + ]; + } + + return $status; + } + + $this->filesystem->remove([ + $this->phpbb_root_path . 'store/update', + $update_path, + $update_path . '.sig' + ]); + + $status['status'] = 'done'; + return $status; + } +} diff --git a/phpBB/phpbb/update/get_updates.php b/phpBB/phpbb/update/get_updates.php new file mode 100644 index 0000000000..1b65cec2a7 --- /dev/null +++ b/phpBB/phpbb/update/get_updates.php @@ -0,0 +1,169 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\update; + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use phpbb\filesystem\exception\filesystem_exception; +use phpbb\filesystem\filesystem_interface; +use SodiumException; +use ZipArchive; + +class get_updates +{ + /** @var filesystem_interface Filesystem managerr */ + private filesystem_interface $filesystem; + + /** @var Client HTTP client */ + private Client $http_client; + + /** @var string Public key to verify package */ + private string $public_key; + + /** @var string phpBB root path */ + private string $phpbb_root_path; + + /** @var ZipArchive Zip extractor */ + private ZipArchive $zipper; + + /** + * Constructor + * + * @param filesystem_interface $filesystem + * @param string $public_key + * @param string $phpbb_root_path + */ + public function __construct( + filesystem_interface $filesystem, + string $public_key, + string $phpbb_root_path) + { + $this->filesystem = $filesystem; + $this->http_client = new Client(); + $this->public_key = base64_decode($public_key); + $this->phpbb_root_path = $phpbb_root_path; + $this->zipper = new ZipArchive(); + } + + /** + * Download the update package. + * + * @param string $url Download link to the update. + * @param string $storage_path Location for the download. + * + * @return bool Whether the download completed successfully. + */ + public function download(string $url, string $storage_path): bool + { + try + { + $this->http_client->request('GET', $url, [ + 'sink' => $storage_path, + 'allow_redirects' => false + ]); + } + catch (GuzzleException) + { + return false; + } + + return true; + } + + /** + * Validate the downloaded file. + * + * @param string $file_path Path to the download. + * @param string $signature_path The signature file. + * + * @return bool Whether the signature is correct or not. + */ + public function validate(string $file_path, string $signature_path): bool + { + if (file_exists($file_path) === false) + { + return false; + } + + if (file_exists($signature_path) === false) + { + return false; + } + + $raw_signature = file_get_contents($signature_path); + + $hash = hash_file('sha384', $file_path, true); + if ($hash === false) + { + return false; + } + + $signature = base64_decode($raw_signature); + if ($signature === false) + { + return false; + } + + try + { + return sodium_crypto_sign_verify_detached($signature, $hash, $this->public_key); + } + catch (SodiumException) + { + return false; + } + } + + /** + * Extract the downloaded archive. + * + * @param string $zip_file Path to the archive. + * @param string $to Path to where to extract the archive to. + * + * @return bool Whether the extraction completed successfully. + */ + public function extract(string $zip_file, string $to): bool + { + if ($this->zipper->open($zip_file) === false) + { + return false; + } + + $result = $this->zipper->extractTo($to); + $this->zipper->close(); + + return $result; + } + + /** + * Copy the update package to the root folder. + * + * @param string $src_dir Where to copy from. + * + * @return bool Whether the files were copied successfully. + */ + public function copy(string $src_dir): bool + { + try + { + $this->filesystem->mirror($src_dir, $this->phpbb_root_path); + } + catch (filesystem_exception) + { + return false; + } + + return true; + } +} From 1ba58768f36471cb99d0aebeb8444e866ec89ef9 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Mon, 28 Oct 2024 21:38:37 +0100 Subject: [PATCH 02/11] [ticket/15851] Add zip extension to required extensions PHPBB-15851 --- phpBB/composer.json | 1 + phpBB/composer.lock | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/phpBB/composer.json b/phpBB/composer.json index 7430017299..321365c140 100644 --- a/phpBB/composer.json +++ b/phpBB/composer.json @@ -28,6 +28,7 @@ "require": { "php": "^8.1", "ext-pdo": "*", + "ext-zip": "*", "ext-zlib": "*", "ext-sodium": "*", "bantu/ini-get-wrapper": "~1.0", diff --git a/phpBB/composer.lock b/phpBB/composer.lock index bf9ac175e9..754690b1e4 100644 --- a/phpBB/composer.lock +++ b/phpBB/composer.lock @@ -10176,7 +10176,9 @@ "platform": { "php": "^8.1", "ext-pdo": "*", - "ext-zlib": "*" + "ext-zip": "*", + "ext-zlib": "*", + "ext-sodium": "*" }, "platform-dev": [], "platform-overrides": { From 573c6c87b20c1c80d5334e7741cc29014db5c7c2 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Mon, 28 Oct 2024 21:42:29 +0100 Subject: [PATCH 03/11] [ticket/15851] Add prefix to parameter for public key PHPBB-15851 --- phpBB/config/default/container/parameters.yml | 2 +- phpBB/config/default/container/services_updater.yml | 2 +- phpBB/phpbb/update/get_updates.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/phpBB/config/default/container/parameters.yml b/phpBB/config/default/container/parameters.yml index ba9b7aff18..29502123ed 100644 --- a/phpBB/config/default/container/parameters.yml +++ b/phpBB/config/default/container/parameters.yml @@ -21,4 +21,4 @@ parameters: - passwords.driver.salted_md5 - passwords.driver.phpass - public_key: 'auJX0pGetfYatE7t/rX5hAkCLZv9s78TwKkLfR3YGuQ=' + packages.public_key: 'auJX0pGetfYatE7t/rX5hAkCLZv9s78TwKkLfR3YGuQ=' diff --git a/phpBB/config/default/container/services_updater.yml b/phpBB/config/default/container/services_updater.yml index 0db8edf014..918c4de407 100644 --- a/phpBB/config/default/container/services_updater.yml +++ b/phpBB/config/default/container/services_updater.yml @@ -3,7 +3,7 @@ services: class: phpbb\update\get_updates arguments: - '@filesystem' - - '%public_key%' + - '%packages.public_key%' - '%core.root_path%' updater.controller: diff --git a/phpBB/phpbb/update/get_updates.php b/phpBB/phpbb/update/get_updates.php index 1b65cec2a7..730fe9e903 100644 --- a/phpBB/phpbb/update/get_updates.php +++ b/phpBB/phpbb/update/get_updates.php @@ -22,7 +22,7 @@ use ZipArchive; class get_updates { - /** @var filesystem_interface Filesystem managerr */ + /** @var filesystem_interface Filesystem manager */ private filesystem_interface $filesystem; /** @var Client HTTP client */ From dacabf05377800f5f1598cf526a5e1be264e2292 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Mon, 28 Oct 2024 22:22:13 +0100 Subject: [PATCH 04/11] [ticket/15851] Add sodium extension to windows runner on github actions PHPBB-15851 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3795b00a7e..d42bcd12f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -503,7 +503,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, intl, gd, exif, iconv, pgsql, pdo_pgsql + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, intl, gd, exif, iconv, pgsql, pdo_pgsql, sodium ini-values: upload_tmp_dir=${{ runner.temp }}, sys_temp_dir=${{ runner.temp }} coverage: none From 3506883c75eea6edc505d8a985d58ec5ec781285 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Tue, 29 Oct 2024 21:23:08 +0100 Subject: [PATCH 05/11] [ticket/15851] Use raw values for verifying signature PHPBB-15851 --- phpBB/phpbb/update/get_updates.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/phpBB/phpbb/update/get_updates.php b/phpBB/phpbb/update/get_updates.php index 730fe9e903..d9957971bc 100644 --- a/phpBB/phpbb/update/get_updates.php +++ b/phpBB/phpbb/update/get_updates.php @@ -101,7 +101,7 @@ class get_updates return false; } - $raw_signature = file_get_contents($signature_path); + $signature = file_get_contents($signature_path); $hash = hash_file('sha384', $file_path, true); if ($hash === false) @@ -109,15 +109,21 @@ class get_updates return false; } - $signature = base64_decode($raw_signature); - if ($signature === false) + $raw_signature = base64_decode($signature); + if ($raw_signature === false) + { + return false; + } + + $raw_public_key = base64_decode($this->public_key); + if ($raw_public_key === false) { return false; } try { - return sodium_crypto_sign_verify_detached($signature, $hash, $this->public_key); + return sodium_crypto_sign_verify_detached($raw_signature, $hash, $raw_public_key); } catch (SodiumException) { From 525924b2fc6c756bcf16a10199f9abfea2e36e41 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Wed, 30 Oct 2024 20:57:34 +0100 Subject: [PATCH 06/11] [ticket/15851] Improve validate checks and change visibility for unit testing PHPBB-15851 --- phpBB/phpbb/update/get_updates.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/phpBB/phpbb/update/get_updates.php b/phpBB/phpbb/update/get_updates.php index d9957971bc..7b79000c6d 100644 --- a/phpBB/phpbb/update/get_updates.php +++ b/phpBB/phpbb/update/get_updates.php @@ -23,20 +23,20 @@ use ZipArchive; class get_updates { /** @var filesystem_interface Filesystem manager */ - private filesystem_interface $filesystem; + protected filesystem_interface $filesystem; /** @var Client HTTP client */ - private Client $http_client; + protected Client $http_client; + + /** @var ZipArchive Zip extractor */ + protected ZipArchive $zipper; /** @var string Public key to verify package */ - private string $public_key; + protected string $public_key; /** @var string phpBB root path */ private string $phpbb_root_path; - /** @var ZipArchive Zip extractor */ - private ZipArchive $zipper; - /** * Constructor * @@ -51,9 +51,9 @@ class get_updates { $this->filesystem = $filesystem; $this->http_client = new Client(); + $this->zipper = new ZipArchive(); $this->public_key = base64_decode($public_key); $this->phpbb_root_path = $phpbb_root_path; - $this->zipper = new ZipArchive(); } /** @@ -91,12 +91,12 @@ class get_updates */ public function validate(string $file_path, string $signature_path): bool { - if (file_exists($file_path) === false) + if (file_exists($file_path) === false || !is_readable($file_path)) { return false; } - if (file_exists($signature_path) === false) + if (file_exists($signature_path) === false || !is_readable($signature_path)) { return false; } @@ -109,13 +109,13 @@ class get_updates return false; } - $raw_signature = base64_decode($signature); + $raw_signature = base64_decode($signature, true); if ($raw_signature === false) { return false; } - $raw_public_key = base64_decode($this->public_key); + $raw_public_key = base64_decode($this->public_key, true); if ($raw_public_key === false) { return false; From feb7d6f34ef46ca2de807ce3433f28fe4eeba02e Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Wed, 30 Oct 2024 20:58:05 +0100 Subject: [PATCH 07/11] [ticket/15851] Add unit tests for get_updates PHPBB-15851 --- tests/update/get_updates_test.php | 310 ++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 tests/update/get_updates_test.php diff --git a/tests/update/get_updates_test.php b/tests/update/get_updates_test.php new file mode 100644 index 0000000000..01b76aa5f4 --- /dev/null +++ b/tests/update/get_updates_test.php @@ -0,0 +1,310 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; +use phpbb\filesystem\exception\filesystem_exception; +use phpbb\filesystem\filesystem_interface; +use phpbb\update\get_updates; + +class phpbb_update_get_updates_test extends phpbb_test_case +{ + private $filesystem; + private $http_client; + private $zipper; + private $update; + private $public_key = 'atest_public_keyatest_public_keyatest_public_keyatest_public_key'; + + private $file_path = __DIR__ . '/../tmp/download.zip'; + + private $signature_path = __DIR__ . '/../tmp/signature.sig'; + + private $phpbb_root_path; + + public function setUp(): void + { + global $phpbb_root_path; + + parent::setUp(); + + $this->filesystem = $this->createMock(filesystem_interface::class); + $this->http_client = $this->createMock(Client::class); + $this->zipper = $this->createMock(ZipArchive::class); + $this->phpbb_root_path = $phpbb_root_path; + + // Set up the `get_updates` instance with injected mocks. + $this->update = new get_updates($this->filesystem, base64_encode($this->public_key), $this->phpbb_root_path); + } + + public function tearDown(): void + { + if (file_exists($this->file_path)) + { + unlink($this->file_path); + } + + if (file_exists($this->signature_path)) + { + unlink($this->signature_path); + } + + parent::tearDown(); + } + + public function test_download_success() + { + $this->http_client->expects($this->once()) + ->method('request') + ->with('GET', 'http://example.com/update.zip', [ + 'sink' => '/path/to/storage', + 'allow_redirects' => false + ]) + ->willReturn(true); + + $client_reflection = new \ReflectionProperty($this->update, 'http_client'); + $client_reflection->setValue($this->update, $this->http_client); + + $result = $this->update->download('http://example.com/update.zip', '/path/to/storage'); + $this->assertTrue($result); + } + + public function test_download_failure() + { + $this->http_client->expects($this->once()) + ->method('request') + ->willReturnCallback(function ($method, $url, $options) + { + throw new ClientException('bad client', new \GuzzleHttp\Psr7\Request($method, $url)); + }); + $client_reflection = new \ReflectionProperty($this->update, 'http_client'); + $client_reflection->setValue($this->update, $this->http_client); + + $result = $this->update->download('http://example.com/update.zip', '/path/to/storage'); + $this->assertFalse($result); + } + + public function test_validate_success() + { + $keypair = sodium_crypto_sign_keypair(); + + $secret_key = sodium_crypto_sign_secretkey($keypair); + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + file_put_contents($this->file_path, 'test file content'); + + $hash = hash_file('sha384', $this->file_path, true); + file_put_contents($this->signature_path, base64_encode(sodium_crypto_sign_detached($hash, $secret_key))); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertTrue($this->update->validate($this->file_path, $this->signature_path)); + } + + public function test_validate_file_not_exist() + { + $file_path = __DIR__ . '/../tmp/download.zip'; + $signature_path = __DIR__ . '/../tmp/signature.sig'; + + $keypair = sodium_crypto_sign_keypair(); + + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($file_path, $signature_path)); + } + + public function test_validate_sig_not_exist() + { + $keypair = sodium_crypto_sign_keypair(); + + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + file_put_contents($this->file_path, 'test file content'); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + } + + public function test_validate_file_not_accessible() + { + $keypair = sodium_crypto_sign_keypair(); + + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + file_put_contents($this->file_path, 'test file content'); + + chmod($this->file_path, 0000); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + + chmod($this->file_path, 0666); + } + + public function test_validate_sig_not_accessible() + { + $keypair = sodium_crypto_sign_keypair(); + + $secret_key = sodium_crypto_sign_secretkey($keypair); + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + file_put_contents($this->file_path, 'test file content'); + + $hash = hash_file('sha384', $this->file_path, true); + file_put_contents($this->signature_path, base64_encode(sodium_crypto_sign_detached($hash, $secret_key))); + + chmod($this->signature_path, 0000); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + + chmod($this->signature_path, 0666); + } + + public function test_validate_sig_not_base64() + { + $keypair = sodium_crypto_sign_keypair(); + + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + file_put_contents($this->file_path, 'test file content'); + + file_put_contents($this->signature_path, 'SGVsbG8gV29ybGQ==='); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + } + + public function test_validate_invalid_pub_key() + { + $keypair = sodium_crypto_sign_keypair(); + + $secret_key = sodium_crypto_sign_secretkey($keypair); + + file_put_contents($this->file_path, 'test file content'); + + $hash = hash_file('sha384', $this->file_path, true); + file_put_contents($this->signature_path, base64_encode(sodium_crypto_sign_detached($hash, $secret_key))); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, '!not!base64'); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + } + + public function test_validate_fail() + { + $keypair = sodium_crypto_sign_keypair(); + + $secret_key = sodium_crypto_sign_secretkey($keypair); + + // Recreate keypair for different public key + $keypair = sodium_crypto_sign_keypair(); + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); + + file_put_contents($this->file_path, 'test file content'); + + $hash = hash_file('sha384', $this->file_path, true); + file_put_contents($this->signature_path, base64_encode(sodium_crypto_sign_detached($hash, $secret_key))); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + } + + public function test_validate_invalid_pub_key_length() + { + $keypair = sodium_crypto_sign_keypair(); + + $secret_key = sodium_crypto_sign_secretkey($keypair); + $public_key = base64_encode(sodium_crypto_sign_publickey($keypair) . 'Foo='); + + file_put_contents($this->file_path, 'test file content'); + + $hash = hash_file('sha384', $this->file_path, true); + file_put_contents($this->signature_path, base64_encode(sodium_crypto_sign_detached($hash, $secret_key))); + + $client_reflection = new \ReflectionProperty($this->update, 'public_key'); + $client_reflection->setValue($this->update, $public_key); + + $this->assertFalse($this->update->validate($this->file_path, $this->signature_path)); + } + + public function test_extract_success() + { + $this->zipper->expects($this->once()) + ->method('open') + ->with('/path/to/zipfile.zip') + ->willReturn(true); + + $this->zipper->expects($this->once()) + ->method('extractTo') + ->with('/path/to/extract') + ->willReturn(true); + + $this->zipper->expects($this->once()) + ->method('close'); + + $zipperReflection = new \ReflectionProperty($this->update, 'zipper'); + $zipperReflection->setValue($this->update, $this->zipper); + + $result = $this->update->extract('/path/to/zipfile.zip', '/path/to/extract'); + $this->assertTrue($result); + } + + public function test_extract_failure() + { + $this->zipper->expects($this->once()) + ->method('open') + ->with('/path/to/zipfile.zip') + ->willReturn(false); + + $zipperReflection = new \ReflectionProperty($this->update, 'zipper'); + $zipperReflection->setValue($this->update, $this->zipper); + + $result = $this->update->extract('/path/to/zipfile.zip', '/path/to/extract'); + $this->assertFalse($result); + } + + public function test_copy_success() + { + $this->filesystem->expects($this->once()) + ->method('mirror') + ->with('/source/dir', $this->phpbb_root_path); + + $result = $this->update->copy('/source/dir'); + $this->assertTrue($result); + } + + public function test_copy_failure() + { + $this->filesystem->expects($this->once()) + ->method('mirror') + ->willThrowException(new filesystem_exception()); + + $result = $this->update->copy('/source/dir'); + $this->assertFalse($result); + } +} From 1157c414109c4e462ba5dcfb5bd5ec5580ba7c34 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Wed, 30 Oct 2024 21:30:47 +0100 Subject: [PATCH 08/11] [ticket/15851] Skip unreadable file tests on windows PHPBB-15851 --- tests/update/get_updates_test.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/update/get_updates_test.php b/tests/update/get_updates_test.php index 01b76aa5f4..b21a53659d 100644 --- a/tests/update/get_updates_test.php +++ b/tests/update/get_updates_test.php @@ -142,6 +142,11 @@ class phpbb_update_get_updates_test extends phpbb_test_case public function test_validate_file_not_accessible() { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') + { + $this->markTestSkipped('Unable to test unreadable files on Windows'); + } + $keypair = sodium_crypto_sign_keypair(); $public_key = base64_encode(sodium_crypto_sign_publickey($keypair)); @@ -160,6 +165,11 @@ class phpbb_update_get_updates_test extends phpbb_test_case public function test_validate_sig_not_accessible() { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') + { + $this->markTestSkipped('Unable to test unreadable files on Windows'); + } + $keypair = sodium_crypto_sign_keypair(); $secret_key = sodium_crypto_sign_secretkey($keypair); From cc488c8b7d99e504a14d982ca8fa96b8f61af5b8 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Mon, 2 Dec 2024 20:12:20 +0100 Subject: [PATCH 09/11] [ticket/15851] Modify language variables PHPBB-15851 --- phpBB/language/en/install.php | 8 ++++---- phpBB/phpbb/update/controller.php | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/phpBB/language/en/install.php b/phpBB/language/en/install.php index f355d1427a..9a73b76c42 100644 --- a/phpBB/language/en/install.php +++ b/phpBB/language/en/install.php @@ -222,11 +222,11 @@ $lang = array_merge($lang, array(

We noticed that the last update of your phpBB installation hasn’t been completed. Visit the database updater, ensure Update database only is selected and click on Submit. Don\'t forget to delete the "install"-directory after you have updated the database successfully.

', // Auto update - 'COULD_NOT_DOWNLOAD_UPDATE_PACKAGE' => 'Failed to download the update package.', - 'COULD_NOT_DOWNLOAD_UPDATE_SIGNATURE' => 'Failed to download the update package signature.', + 'UPDATE_PACKAGE_DOWNLOAD_FAILURE' => 'Failed to download the update package.', + 'UPDATE_SIGNATURE_DOWNLOAD_FAILURE' => 'Failed to download the update package signature.', 'UPDATE_SIGNATURE_INVALID' => 'The update package is corrupted.', - 'COULD_NOT_EXTRACT_UPDATE' => 'Could not extract files from the update package.', - 'COULD_NOT_WRITE_UPDATE_FILES' => 'Could not copy files from the update package.', + 'UPDATE_PACKAGE_EXTRACT_FAILURE' => 'Could not extract files from the update package.', + 'UPDATE_FILES_COPY_FAILURE' => 'Could not copy files from the update package.', // // Server data diff --git a/phpBB/phpbb/update/controller.php b/phpBB/phpbb/update/controller.php index 5253f2b3d3..45d11d2f0c 100644 --- a/phpBB/phpbb/update/controller.php +++ b/phpBB/phpbb/update/controller.php @@ -68,7 +68,7 @@ class controller { return [ 'status' => 'error', - 'error' => $this->language->lang('COULD_NOT_DOWNLOAD_UPDATE_PACKAGE') + 'error' => $this->language->lang('UPDATE_PACKAGE_DOWNLOAD_FAILURE') ]; } @@ -82,7 +82,7 @@ class controller { return [ 'status' => 'error', - 'error' => $this->language->lang('COULD_NOT_DOWNLOAD_UPDATE_SIGNATURE') + 'error' => $this->language->lang('UPDATE_SIGNATURE_DOWNLOAD_FAILURE') ]; } return $status; @@ -104,7 +104,7 @@ class controller { return [ 'status' => 'error', - 'error' => $this->language->lang('COULD_NOT_EXTRACT_UPDATE') + 'error' => $this->language->lang('UPDATE_PACKAGE_EXTRACT_FAILURE') ]; } @@ -118,7 +118,7 @@ class controller { return [ 'status' => 'error', - 'error' => $this->language->lang('COULD_NOT_WRITE_UPDATE_FILES') + 'error' => $this->language->lang('UPDATE_FILES_COPY_FAILURE') ]; } From 6eacf68047d24b7cf12a8a50112158e19b318e49 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Wed, 4 Dec 2024 21:05:30 +0100 Subject: [PATCH 10/11] [ticket/15851] Add controller error_response and tests PHPBB-15851 --- phpBB/phpbb/update/controller.php | 50 ++--- tests/update/controller_test.php | 351 ++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+), 25 deletions(-) create mode 100644 tests/update/controller_test.php diff --git a/phpBB/phpbb/update/controller.php b/phpBB/phpbb/update/controller.php index 45d11d2f0c..cdbc53495b 100644 --- a/phpBB/phpbb/update/controller.php +++ b/phpBB/phpbb/update/controller.php @@ -53,7 +53,7 @@ class controller /** * Handle requests. * - * @param string $download The download URL. + * @param string $download The download URL. * * @return string[] Unencoded json response. */ @@ -61,65 +61,51 @@ class controller { $update_path = $this->phpbb_root_path . 'store/update.zip'; $status = ['status' => 'continue']; - if (!file_exists($update_path)) + if (!$this->filesystem->exists($update_path)) { $result = $this->updater->download($download, $update_path); if (!$result) { - return [ - 'status' => 'error', - 'error' => $this->language->lang('UPDATE_PACKAGE_DOWNLOAD_FAILURE') - ]; + return $this->error_response('UPDATE_PACKAGE_DOWNLOAD_FAILURE'); } return $status; } - if (!file_exists($update_path . '.sig')) + if (!$this->filesystem->exists($update_path . '.sig')) { $result = $this->updater->download($download . '.sig', $update_path . '.sig'); if (!$result) { - return [ - 'status' => 'error', - 'error' => $this->language->lang('UPDATE_SIGNATURE_DOWNLOAD_FAILURE') - ]; + return $this->error_response('UPDATE_SIGNATURE_DOWNLOAD_FAILURE'); } + return $status; } - if (!is_dir($this->phpbb_root_path . 'store/update')) + if (!$this->filesystem->exists($this->phpbb_root_path . 'store/update') || !is_dir($this->phpbb_root_path . 'store/update')) { $result = $this->updater->validate($update_path, $update_path . '.sig'); if (!$result) { - return [ - 'status' => 'error', - 'error' => $this->language->lang('UPDATE_SIGNATURE_INVALID') - ]; + return $this->error_response('UPDATE_SIGNATURE_INVALID'); } $result = $this->updater->extract($update_path, $this->phpbb_root_path . 'store/update'); if (!$result) { - return [ - 'status' => 'error', - 'error' => $this->language->lang('UPDATE_PACKAGE_EXTRACT_FAILURE') - ]; + return $this->error_response('UPDATE_PACKAGE_EXTRACT_FAILURE'); } return $status; } - if (!is_dir($this->phpbb_root_path . 'install')) + if (!$this->filesystem->exists($this->phpbb_root_path . 'install') || !is_dir($this->phpbb_root_path . 'install')) { $result = $this->updater->copy($this->phpbb_root_path . 'store/update'); if (!$result) { - return [ - 'status' => 'error', - 'error' => $this->language->lang('UPDATE_FILES_COPY_FAILURE') - ]; + return $this->error_response('UPDATE_FILES_COPY_FAILURE'); } return $status; @@ -134,4 +120,18 @@ class controller $status['status'] = 'done'; return $status; } + + /** + * Create error response + * + * @param string $error_key + * @return array Error response + */ + protected function error_response(string $error_key): array + { + return [ + 'status' => 'error', + 'error' => $this->language->lang($error_key), + ]; + } } diff --git a/tests/update/controller_test.php b/tests/update/controller_test.php new file mode 100644 index 0000000000..bba0186b7f --- /dev/null +++ b/tests/update/controller_test.php @@ -0,0 +1,351 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +use phpbb\update\controller; +use phpbb\update\get_updates; +use phpbb\filesystem\filesystem; +use phpbb\language\language; + +class phpbb_update_controller_test extends \phpbb_test_case +{ + private $filesystem; + private $filesystem_mock; + private $updater_mock; + private $language_mock; + private $phpbb_root_path; + + protected function setUp(): void + { + global $phpbb_root_path; + + $this->filesystem = new filesystem(); + $this->filesystem_mock = $this->createMock(filesystem::class); + $this->updater_mock = $this->createMock(get_updates::class); + $this->language_mock = $this->createMock(language::class); + $this->phpbb_root_path = $phpbb_root_path; + } + + protected function tearDown(): void + { + $this->filesystem->remove([ + $this->phpbb_root_path . 'store/update.zip', + $this->phpbb_root_path . 'store/update.zip.sig', + $this->phpbb_root_path . 'store/update', + ]); + } + + public function test_download_fails(): void + { + $this->updater_mock->expects($this->once()) + ->method('download') + ->willReturn(false); + + $this->language_mock->expects($this->once()) + ->method('lang') + ->with('UPDATE_PACKAGE_DOWNLOAD_FAILURE') + ->willReturnArgument(0); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'error', 'error' => 'UPDATE_PACKAGE_DOWNLOAD_FAILURE'], $response); + } + + public function test_download_success(): void + { + $this->updater_mock->expects($this->once()) + ->method('download') + ->willReturn(true); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'continue'], $response); + } + + public function test_download_signature_fails(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', false], + [$this->phpbb_root_path . 'store/update', false], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('download') + ->with('https://example.com/update.zip.sig', $update_path . '.sig') + ->willReturn(false); + + $this->language_mock->expects($this->once()) + ->method('lang') + ->with('UPDATE_SIGNATURE_DOWNLOAD_FAILURE') + ->willReturnArgument(0); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'error', 'error' => 'UPDATE_SIGNATURE_DOWNLOAD_FAILURE'], $response); + } + + public function test_download_signature_success(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', false], + [$this->phpbb_root_path . 'store/update', false], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('download') + ->with('https://example.com/update.zip.sig', $update_path . '.sig') + ->willReturn(true); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'continue'], $response); + } + + public function test_signature_validation_fails(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', true], + [$this->phpbb_root_path . 'store/update', false], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('validate') + ->willReturn(false); + + $this->language_mock->expects($this->once()) + ->method('lang') + ->with('UPDATE_SIGNATURE_INVALID') + ->willReturnArgument(0); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'error', 'error' => 'UPDATE_SIGNATURE_INVALID'], $response); + } + + public function test_extract_fails(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', true], + [$this->phpbb_root_path . 'store/update', false], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('validate') + ->willReturn(true); + + $this->updater_mock->expects($this->once()) + ->method('extract') + ->willReturn(false); + + $this->language_mock->expects($this->once()) + ->method('lang') + ->with('UPDATE_PACKAGE_EXTRACT_FAILURE') + ->willReturnArgument(0); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'error', 'error' => 'UPDATE_PACKAGE_EXTRACT_FAILURE'], $response); + } + + public function test_extract_success(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', true], + [$this->phpbb_root_path . 'store/update', false], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('validate') + ->willReturn(true); + + $this->updater_mock->expects($this->once()) + ->method('extract') + ->willReturn(true); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'continue'], $response); + } + + public function test_copy_fails(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + $this->filesystem->touch($update_path); // Simulate existing update file + $this->filesystem->touch($update_path . '.sig'); // Simulate existing signature file + $this->filesystem->mkdir($this->phpbb_root_path . 'store/update'); + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', true], + [$this->phpbb_root_path . 'store/update', true], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('copy') + ->willReturn(false); + + $this->language_mock->expects($this->once()) + ->method('lang') + ->with('UPDATE_FILES_COPY_FAILURE') + ->willReturnArgument(0); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'error', 'error' => 'UPDATE_FILES_COPY_FAILURE'], $response); + } + + public function test_copy_success(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + $this->filesystem->touch($update_path); // Simulate existing update file + $this->filesystem->touch($update_path . '.sig'); // Simulate existing signature file + $this->filesystem->mkdir($this->phpbb_root_path . 'store/update'); + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', true], + [$this->phpbb_root_path . 'store/update', true], + [$this->phpbb_root_path . 'install', false], + ]); + + $this->updater_mock->expects($this->once()) + ->method('copy') + ->willReturn(true); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'continue'], $response); + } + + public function test_successful_update_process(): void + { + $update_path = $this->phpbb_root_path . 'store/update.zip'; + $signature_path = $update_path . '.sig'; + $update_dir = $this->phpbb_root_path . 'store/update'; + + $this->filesystem->touch($update_path); + $this->filesystem->touch($signature_path); + $this->filesystem->mkdir($update_dir); + + $this->filesystem_mock->expects($this->any()) + ->method('exists') + ->willReturnMap([ + [$update_path, true], + [$update_path . '.sig', true], + [$this->phpbb_root_path . 'store/update', true], + [$this->phpbb_root_path . 'install', true], + ]); + + $this->filesystem_mock->expects($this->once()) + ->method('remove') + ->with([$update_dir, $update_path, $signature_path]); + + $controller = new controller( + $this->filesystem_mock, + $this->updater_mock, + $this->language_mock, + $this->phpbb_root_path + ); + + $response = $controller->handle('https://example.com/update.zip'); + $this->assertEquals(['status' => 'done'], $response); + } +} \ No newline at end of file From 8b1ee790d68f7f40395fcba4558cdad6a31e93c8 Mon Sep 17 00:00:00 2001 From: Marc Alexander Date: Thu, 13 Feb 2025 19:58:26 +0100 Subject: [PATCH 11/11] [ticket/15851] Fix lock file hash PHPBB-15851 --- phpBB/composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpBB/composer.lock b/phpBB/composer.lock index 754690b1e4..8be1f6e409 100644 --- a/phpBB/composer.lock +++ b/phpBB/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": "2ce7bc4e8f61d065ff471afa38f12394", + "content-hash": "96d8bdaa91db532b0a0bf5e1b6c0ec31", "packages": [ { "name": "bantu/ini-get-wrapper",