mirror of
https://github.com/phpbb/phpbb.git
synced 2025-06-07 20:08:53 +00:00
Merge pull request #6544 from CHItA/ticket/15851
[ticket/15851] Automatic update downloader
This commit is contained in:
commit
e0bcea9000
13 changed files with 1035 additions and 4 deletions
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -28,7 +28,9 @@
|
|||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"ext-sodium": "*",
|
||||
"bantu/ini-get-wrapper": "~1.0",
|
||||
"carlos-mg89/oauth": "^0.8.15",
|
||||
"chita/topological_sort": "^3.0",
|
||||
|
|
6
phpBB/composer.lock
generated
6
phpBB/composer.lock
generated
|
@ -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",
|
||||
|
@ -10176,7 +10176,9 @@
|
|||
"platform": {
|
||||
"php": "^8.1",
|
||||
"ext-pdo": "*",
|
||||
"ext-zlib": "*"
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"ext-sodium": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
|
|
|
@ -20,3 +20,5 @@ parameters:
|
|||
- passwords.driver.bcrypt
|
||||
- passwords.driver.salted_md5
|
||||
- passwords.driver.phpass
|
||||
|
||||
packages.public_key: 'auJX0pGetfYatE7t/rX5hAkCLZv9s78TwKkLfR3YGuQ='
|
||||
|
|
|
@ -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 }
|
||||
|
|
14
phpBB/config/default/container/services_updater.yml
Normal file
14
phpBB/config/default/container/services_updater.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
services:
|
||||
updater.get_updates:
|
||||
class: phpbb\update\get_updates
|
||||
arguments:
|
||||
- '@filesystem'
|
||||
- '%packages.public_key%'
|
||||
- '%core.root_path%'
|
||||
|
||||
updater.controller:
|
||||
class: phpbb\update\controller
|
||||
arguments:
|
||||
- '@filesystem'
|
||||
- '@updater.get_updates'
|
||||
- '%core.root_path%'
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -221,6 +221,13 @@ $lang = array_merge($lang, array(
|
|||
|
||||
<p>We noticed that the last update of your phpBB installation hasn’t been completed. Visit the <a href="%1$s" title="%1$s">database updater</a>, ensure <em>Update database only</em> is selected and click on <strong>Submit</strong>. Don\'t forget to delete the "install"-directory after you have updated the database successfully.</p>',
|
||||
|
||||
// Auto update
|
||||
'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.',
|
||||
'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
|
||||
//
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
137
phpBB/phpbb/update/controller.php
Normal file
137
phpBB/phpbb/update/controller.php
Normal file
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of the phpBB Forum Software package.
|
||||
*
|
||||
* @copyright (c) phpBB Limited <https://www.phpbb.com>
|
||||
* @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 (!$this->filesystem->exists($update_path))
|
||||
{
|
||||
$result = $this->updater->download($download, $update_path);
|
||||
if (!$result)
|
||||
{
|
||||
return $this->error_response('UPDATE_PACKAGE_DOWNLOAD_FAILURE');
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
if (!$this->filesystem->exists($update_path . '.sig'))
|
||||
{
|
||||
$result = $this->updater->download($download . '.sig', $update_path . '.sig');
|
||||
if (!$result)
|
||||
{
|
||||
return $this->error_response('UPDATE_SIGNATURE_DOWNLOAD_FAILURE');
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
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 $this->error_response('UPDATE_SIGNATURE_INVALID');
|
||||
}
|
||||
|
||||
$result = $this->updater->extract($update_path, $this->phpbb_root_path . 'store/update');
|
||||
if (!$result)
|
||||
{
|
||||
return $this->error_response('UPDATE_PACKAGE_EXTRACT_FAILURE');
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
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 $this->error_response('UPDATE_FILES_COPY_FAILURE');
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
$this->filesystem->remove([
|
||||
$this->phpbb_root_path . 'store/update',
|
||||
$update_path,
|
||||
$update_path . '.sig'
|
||||
]);
|
||||
|
||||
$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),
|
||||
];
|
||||
}
|
||||
}
|
175
phpBB/phpbb/update/get_updates.php
Normal file
175
phpBB/phpbb/update/get_updates.php
Normal file
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of the phpBB Forum Software package.
|
||||
*
|
||||
* @copyright (c) phpBB Limited <https://www.phpbb.com>
|
||||
* @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 manager */
|
||||
protected filesystem_interface $filesystem;
|
||||
|
||||
/** @var Client HTTP client */
|
||||
protected Client $http_client;
|
||||
|
||||
/** @var ZipArchive Zip extractor */
|
||||
protected ZipArchive $zipper;
|
||||
|
||||
/** @var string Public key to verify package */
|
||||
protected string $public_key;
|
||||
|
||||
/** @var string phpBB root path */
|
||||
private string $phpbb_root_path;
|
||||
|
||||
/**
|
||||
* 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->zipper = new ZipArchive();
|
||||
$this->public_key = base64_decode($public_key);
|
||||
$this->phpbb_root_path = $phpbb_root_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || !is_readable($file_path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($signature_path) === false || !is_readable($signature_path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$signature = file_get_contents($signature_path);
|
||||
|
||||
$hash = hash_file('sha384', $file_path, true);
|
||||
if ($hash === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$raw_signature = base64_decode($signature, true);
|
||||
if ($raw_signature === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$raw_public_key = base64_decode($this->public_key, true);
|
||||
if ($raw_public_key === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return sodium_crypto_sign_verify_detached($raw_signature, $hash, $raw_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;
|
||||
}
|
||||
}
|
351
tests/update/controller_test.php
Normal file
351
tests/update/controller_test.php
Normal file
|
@ -0,0 +1,351 @@
|
|||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of the phpBB Forum Software package.
|
||||
*
|
||||
* @copyright (c) phpBB Limited <https://www.phpbb.com>
|
||||
* @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);
|
||||
}
|
||||
}
|
320
tests/update/get_updates_test.php
Normal file
320
tests/update/get_updates_test.php
Normal file
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of the phpBB Forum Software package.
|
||||
*
|
||||
* @copyright (c) phpBB Limited <https://www.phpbb.com>
|
||||
* @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()
|
||||
{
|
||||
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));
|
||||
|
||||
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()
|
||||
{
|
||||
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);
|
||||
$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);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue