Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions core/tests/Unit/Install/CliInstallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@

use Tests\TestCase;

final class PartialInstallConfigWriteStream
{
public mixed $context;

private int $writeCount = 0;

public function stream_open(): bool
{
return true;
}

public function stream_write(string $data): int
{
$this->writeCount++;

return $this->writeCount === 1 ? min(2, strlen($data)) : 0;
}

public function stream_flush(): bool
{
return true;
}

public function stream_close(): void
{
}

public function url_stat(): false
{
return false;
}
}

require_once dirname(__DIR__, 4) . '/install/cli-install.php';

final class CliInstallTest extends TestCase
Expand Down Expand Up @@ -64,4 +97,68 @@ public function getAttribute($attribute): string
self::assertStringNotContainsString('[+database_name+]', $config);
self::assertStringContainsString("'username' => env('DB_USERNAME', 'db_user')", $config);
}

public function testConfigWriterReturnsFalseWhenTheParentDirectoryDoesNotExist(): void
{
$path = sys_get_temp_dir() . '/evo-missing-' . uniqid('', true) . '/default.php';

self::assertFalse(hasInstallConfigPermissions($path));
self::assertFalse(writeInstallConfigFile($path, '<?php return [];'));
self::assertFileDoesNotExist($path);
}

public function testConfigWriterWritesTheCompleteContents(): void
{
$path = tempnam(sys_get_temp_dir(), 'evo-config-');
self::assertIsString($path);
$contents = "<?php\nreturn ['driver' => 'sqlite'];\n";

try {
self::assertTrue(hasInstallConfigPermissions(dirname($path) . '/evo-new-config-' . uniqid() . '.php'));
self::assertTrue(hasInstallConfigPermissions($path));
self::assertTrue(writeInstallConfigFile($path, $contents));
self::assertSame($contents, file_get_contents($path));
self::assertTrue(is_readable($path));
self::assertTrue(is_writable($path));
} finally {
@chmod($path, 0600);
@unlink($path);
}
}

public function testConfigWriterRejectsAPartialWrite(): void
{
$scheme = 'evopartial' . bin2hex(random_bytes(4));
self::assertTrue(stream_wrapper_register($scheme, PartialInstallConfigWriteStream::class));

try {
self::assertFalse(writeInstallConfigFile($scheme . '://default.php', 'generated config'));
} finally {
stream_wrapper_unregister($scheme);
}
}

public function testCliWriteConfigThrowsWhenTheTargetCannotBeOpened(): void
{
$missingPath = sys_get_temp_dir() . '/evo-missing-' . uniqid('', true) . '/default.php';
$installer = new class([], $missingPath) extends \InstallEvo {
public function __construct(array $arguments, private readonly string $path)
{
parent::__construct($arguments);
}

protected function configFilePath(): string
{
return $this->path;
}
};
$installer->databaseType = 'sqlite';
$installer->database = 'evolution';
$installer->tablePrefix = 'evo_';

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Unable to write the database configuration file');

$installer->writeConfig();
}
}
117 changes: 117 additions & 0 deletions core/tests/Unit/Install/WebInstallConfigFailureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Tests\Unit\Install;

use PHPUnit\Framework\TestCase;

final class WebInstallConfigFailureTest extends TestCase
{
public function testWebInstallerStopsBeforeBootstrappingAfterConfigWriteFailure(): void
{
$source = file_get_contents(dirname(__DIR__, 4) . '/install/src/controllers/install.php');

self::assertIsString($source);
self::assertStringContainsString(
'$configFileFailed = !writeInstallConfigFile($filename, $configString);',
$source
);
self::assertMatchesRegularExpression(
'/if \(\$configFileFailed === true\).*?include .*?template\/actions\/install\.php.*?return;/s',
$source
);
}

public function testGeneratedConfigIsEscapedInTheFailureResponse(): void
{
$source = file_get_contents(dirname(__DIR__, 4) . '/install/src/template/actions/install.php');

self::assertIsString($source);
self::assertStringContainsString(
"htmlspecialchars(\$configString, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')",
$source
);
}

public function testInstallerLocalesRetainTheEnglishFallback(): void
{
$source = file_get_contents(dirname(__DIR__, 4) . '/install/src/lang.php');

self::assertIsString($source);
self::assertStringContainsString('$fallbackLang = $_lang;', $source);
self::assertStringContainsString('$_lang += $fallbackLang;', $source);
}

public function testEveryInstallerLocaleDefinesTheConfigWriteMessages(): void
{
$languageFiles = glob(dirname(__DIR__, 4) . '/install/src/lang/*.inc.php');
self::assertIsArray($languageFiles);
self::assertNotEmpty($languageFiles);

foreach ($languageFiles as $languageFile) {
$source = file_get_contents($languageFile);
self::assertIsString($source);
self::assertStringContainsString(
"'cant_write_config_file_retry'",
$source,
basename($languageFile) . ' must translate the config write failure instructions.'
);
self::assertStringContainsString(
"'checking_if_database_config_writable'",
$source,
basename($languageFile) . ' must translate the config writability check.'
);
foreach (['0644', 'rw-r--r--', '0755', 'rwxr-xr-x', '0777'] as $permission) {
self::assertStringContainsString(
$permission,
$source,
basename($languageFile) . " must include the {$permission} permission guidance."
);
}
}
}

public function testConfigFailureLayoutHandlesLongTranslatedText(): void
{
$template = file_get_contents(dirname(__DIR__, 4) . '/install/src/template/actions/install.php');
$styles = file_get_contents(dirname(__DIR__, 4) . '/install/style.css');
$layout = file_get_contents(dirname(__DIR__, 4) . '/install/src/template/install.tpl');

self::assertIsString($template);
self::assertIsString($styles);
self::assertIsString($layout);
self::assertStringContainsString('class="config-write-failure"', $template);
self::assertStringContainsString('class="config-write-failure__content"', $template);
self::assertStringContainsString('overflow-wrap: anywhere;', $styles);
self::assertStringContainsString('word-break: break-all;', $styles);
self::assertStringContainsString('max-width: 40rem;', $styles);
self::assertStringContainsString('direction: ltr;', $styles);
self::assertStringContainsString('text-align: left;', $styles);
self::assertStringContainsString('box-sizing: border-box;', $styles);
self::assertStringContainsString('@media (max-width: 600px)', $styles);
self::assertStringContainsString('name="viewport"', $layout);
}

public function testUpgradeModeBlocksAnUnreadableDatabaseConfig(): void
{
$controller = file_get_contents(dirname(__DIR__, 4) . '/install/src/controllers/mode.php');
$template = file_get_contents(dirname(__DIR__, 4) . '/install/src/template/actions/mode.tpl');

self::assertIsString($controller);
self::assertIsString($template);
self::assertStringContainsString('!is_readable($databaseConfigFile)', $controller);
self::assertStringContainsString("\$ph['disabledAdvUpg']", $controller);
self::assertStringContainsString("\$ph['configPermissionError']", $controller);
self::assertStringContainsString('[+configPermissionError+]', $template);
self::assertStringContainsString('[+disabledNext+]', $template);
}

public function testSummaryCacheWriterDoesNotUseUncheckedStreamHandles(): void
{
$source = file_get_contents(dirname(__DIR__, 4) . '/install/src/controllers/summary.php');

self::assertIsString($source);
self::assertStringContainsString("\$_lang['cant_write_config_file_retry']", $source);
self::assertStringContainsString('file_put_contents($path, $data, LOCK_EX)', $source);
self::assertStringNotContainsString('fwrite($hnd, $data)', $source);
}
}
43 changes: 22 additions & 21 deletions install/cli-install.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@
* Use --skipComposer=y to keep the already installed dependencies untouched.
**/

function runCliInstall(array $argv): void
function runCliInstall(array $argv): int
{
$install = new InstallEvo($argv);
$install->start();
try {
$install = new InstallEvo($argv);
$install->start();
} catch (RuntimeException $exception) {
error($exception->getMessage());

return 1;
}

return 0;
}

if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
runCliInstall($argv);
exit(runCliInstall($argv));
}

class InstallEvo
Expand Down Expand Up @@ -544,25 +552,18 @@ public function writeConfig()
$configString = file_get_contents(__DIR__ . '/stubs/files/config/database/connections/default.tpl');
$configString = parse($configString, $confph);

$filename = EVO_CORE_PATH . 'config/database/connections/default.php';
$configFileFailed = false;

if (file_exists($filename)) {
@chmod($filename, 0777);
}

if (!$handle = fopen($filename, 'w')) {
$configFileFailed = true;
}
// write $somecontent to our opened file.
if (@ fwrite($handle, $configString) === false) {
$configFileFailed = true;
$filename = $this->configFilePath();
if (!writeInstallConfigFile($filename, $configString)) {
throw new RuntimeException(
"Unable to write the database configuration file: {$filename}. "
. 'Check the file and directory permissions, then run the installer again.'
);
}
@ fclose($handle);

// try to chmod the config file go-rwx (for suexeced php)
@chmod($filename, 0404);
}

protected function configFilePath(): string
{
return EVO_CORE_PATH . 'config/database/connections/default.php';
}

public function migrationAndSeed()
Expand Down
20 changes: 3 additions & 17 deletions install/src/controllers/install.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,12 @@
$configString = parse($configString, $confph);

$filename = EVO_CORE_PATH . 'config/database/connections/default.php';
$configFileFailed = false;
$configFileFailed = !writeInstallConfigFile($filename, $configString);

if (file_exists($filename)) {
@chmod($filename, 0777);
}

if (@!$handle = fopen($filename, 'w')) {
$configFileFailed = true;
}

// write $somecontent to our opened file.
if (@fwrite($handle, $configString) === false) {
$configFileFailed = true;
}
@fclose($handle);

// try to chmod the config file go-rwx (for suexeced php)
@chmod($filename, 0404);
if ($configFileFailed === true) {
$errors += 1;
include dirname(__DIR__) . '/template/actions/install.php';
return;
} else {
$installLevel = 3;
}
Expand Down
17 changes: 12 additions & 5 deletions install/src/controllers/mode.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
// Determine upgradeability
$isConnectable = false;
$installMode = isset($_POST['installmode']) ? (int)$_POST['installmode'] : 0;
$databaseConfigFile = EVO_CORE_PATH . 'config/database/connections/default.php';
$databaseConfigUnreadable = is_file($databaseConfigFile) && !is_readable($databaseConfigFile);

if (!is_file(EVO_CORE_PATH . 'config/database/connections/default.php')) {
if (!is_file($databaseConfigFile)) {
$isNew = true;
} elseif ($databaseConfigUnreadable) {
$isNew = false;
} else {
$isNew = false;
$db_config = include_once EVO_CORE_PATH . 'config/database/connections/default.php';
$db_config = include_once $databaseConfigFile;
if (isset($db_config['database'])) {
try {
$pdoOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
Expand All @@ -32,11 +36,14 @@
$ph['displayUpg'] = $isNew ? 'hidden' : '';
$ph['displayAdvUpg'] = $ph['displayUpg'];
$ph['checkedNew'] = $isNew ? 'checked' : '';
$ph['checkedUpg'] = ((!$isNew && $isConnectable) || ($installMode === 1)) ? 'checked' : '';
$ph['checkedAdvUpg'] = ((!$isNew && !$isConnectable) || ($installMode === 2)) ? 'checked' : '';
$ph['checkedUpg'] = (!$databaseConfigUnreadable && ((!$isNew && $isConnectable) || ($installMode === 1))) ? 'checked' : '';
$ph['checkedAdvUpg'] = (!$databaseConfigUnreadable && ((!$isNew && !$isConnectable) || ($installMode === 2))) ? 'checked' : '';
$ph['install_language'] = $install_language;
$ph['disabledUpg'] = !$isConnectable ? 'disabled' : '';
$ph['disabledAdvUpg'] = '';
$ph['disabledAdvUpg'] = $databaseConfigUnreadable ? 'disabled' : '';
$ph['configPermissionError'] = $databaseConfigUnreadable ? $_lang['cant_write_config_file_retry'] : '';
$ph['configPermissionErrorHidden'] = $databaseConfigUnreadable ? '' : 'hidden';
$ph['disabledNext'] = $databaseConfigUnreadable ? 'disabled' : '';
$ph['csrf_nonce'] = csrfNonce();

$tpl = file_get_contents(dirname(__DIR__) . '/template/actions/mode.tpl');
Expand Down
Loading