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] [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; + } +}