Merge pull request #6544 from CHItA/ticket/15851

[ticket/15851] Automatic update downloader
This commit is contained in:
Marc Alexander 2025-02-13 21:07:50 +01:00 committed by GitHub
commit e0bcea9000
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1035 additions and 4 deletions

View file

@ -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

View file

@ -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
View file

@ -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": {

View file

@ -20,3 +20,5 @@ parameters:
- passwords.driver.bcrypt
- passwords.driver.salted_md5
- passwords.driver.phpass
packages.public_key: 'auJX0pGetfYatE7t/rX5hAkCLZv9s78TwKkLfR3YGuQ='

View file

@ -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 }

View 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%'

View file

@ -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)
{

View file

@ -221,6 +221,13 @@ $lang = array_merge($lang, array(
<p>We noticed that the last update of your phpBB installation hasnt 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
//

View file

@ -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');

View 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),
];
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}