From 143294c6cb7093c2f25be74ae35c2d4737ce32e7 Mon Sep 17 00:00:00 2001 From: Artur Kyryliuk Date: Wed, 4 Nov 2026 18:08:36 +0100 Subject: [PATCH 01/11] fix(installer): make "remove the install folder" work Co-Authored-By: Claude Opus 5.5 --- .../Feature/InstallerRemovalFlowTest.php | 307 ++++++++++++++++++ .../Security/ApacheConfigHardeningTest.php | 8 + install/src/controllers/install.php | 5 +- ng.inx | 4 +- 4 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 core/tests/Feature/InstallerRemovalFlowTest.php diff --git a/core/tests/Feature/InstallerRemovalFlowTest.php b/core/tests/Feature/InstallerRemovalFlowTest.php new file mode 100644 index 0000000000..ec40a0f8e4 --- /dev/null +++ b/core/tests/Feature/InstallerRemovalFlowTest.php @@ -0,0 +1,307 @@ +stop(1); + installerFlowDelete($site['root']); + } + return $site = null; + } + if ($site !== null) { + return $site; + } + + $root = str_replace('\\', '/', sys_get_temp_dir()) . '/evo-installer-flow-' . bin2hex(random_bytes(8)); + installerFlowCopy(dirname(__DIR__, 3), $root); + foreach (['logs', 'cache', 'sessions'] as $folder) { + mkdir("$root/core/storage/$folder", 0777, true); + } + + $socket = stream_socket_server('tcp://127.0.0.1:0'); + $address = stream_socket_get_name($socket, false); + fclose($socket); + + // The settings of the test run must not reach the site: DB_* would win over its own connection. + $environment = array_map(static fn () => false, array_filter( + getenv(), + static fn (string $name) => preg_match('/^(DB_|EVO_|APP_)/', $name) === 1, + ARRAY_FILTER_USE_KEY + )); + $server = new Process([PHP_BINARY, '-d', 'opcache.enable=0', '-S', $address, '-t', $root], $root, $environment, null, null); + $server->start(); + + $url = "http://$address"; + for ($attempt = 0; $attempt < 100; $attempt++) { + $connection = @fsockopen('127.0.0.1', (int) substr(strrchr($address, ':'), 1), $code, $message, 0.1); + if ($connection !== false) { + fclose($connection); + break; + } + usleep(50000); + } + + return $site = ['root' => $root, 'url' => $url, 'server' => $server]; +} + +/** Back to a tree that was never installed, with the installer of the repository. */ +function installerFlowReset(): array +{ + $site = installerFlowSite(); + $root = $site['root']; + foreach ([ + 'core/.install', 'core/config/database/connections/default.php', 'core/database/flow.sqlite', + 'install.session.php', 'install', 'core/storage/bootstrap', 'assets/cache/installProc.inc.php', + ] as $path) { + installerFlowDelete("$root/$path"); + } + mkdir("$root/core/storage/bootstrap", 0777, true); + installerFlowCopy(dirname(__DIR__, 3) . '/install', "$root/install"); + + return $site; +} + +function installerFlowBrowser(array $site): Client +{ + return new Client([ + 'base_uri' => $site['url'], + 'cookies' => new CookieJar(), + 'http_errors' => false, + 'timeout' => 300, + 'headers' => ['Accept-Language' => 'en', 'Referer' => $site['url'] . '/install/'], + ]); +} + +/** + * The fields form#install_form submits as it stands, and where it posts them. + * + * @return array{action: string, fields: array} + */ +function installerFlowForm(string $html): array +{ + $document = new DOMDocument(); + @$document->loadHTML($html); + $form = $document->getElementById('install_form'); + expect($form)->toBeInstanceOf(DOMElement::class, 'no installer form in: ' . substr(strip_tags($html), 0, 500)); + + $fields = []; + foreach ((new DOMXPath($document))->query('.//input|.//select|.//textarea', $form) as $field) { + $name = $field->getAttribute('name'); + $type = strtolower($field->getAttribute('type')); + if ($name === '' || $field->hasAttribute('disabled') + || (in_array($type, ['checkbox', 'radio'], true) && !$field->hasAttribute('checked'))) { + continue; + } + if ($field->nodeName === 'select') { + $value = ''; + foreach ($field->getElementsByTagName('option') as $index => $option) { + if ($index === 0 || $option->hasAttribute('selected')) { + $value = $option->getAttribute('value'); + } + } + } else { + $value = $field->nodeName === 'textarea' ? $field->textContent : $field->getAttribute('value'); + } + if (str_ends_with($name, '[]')) { + $fields[substr($name, 0, -2)][] = $value; + } else { + $fields[$name] = $value; + } + } + + return ['action' => $form->getAttribute('action'), 'fields' => $fields]; +} + +function installerFlowPost(Client $browser, string $path, array $fields): string +{ + $response = $browser->post($path, ['form_params' => $fields]); + expect($response->getStatusCode())->toBe(200, "$path answered " . $response->getStatusCode()); + + return (string) $response->getBody(); +} + +/** Submits the forms from $page on, each with $choices, up to the page of results. */ +function installerFlowFinish(Client $browser, string $page, array $choices): string +{ + for ($step = 0; $step < 8 && !str_contains($page, 'remove_installer.processor.php'); $step++) { + $form = installerFlowForm($page); + $page = installerFlowPost( + $browser, + '/install/' . $form['action'], + array_replace($form['fields'], $choices) + ); + } + expect($page)->toContain('remove_installer.processor.php')->not->toContain('class="notok"'); + + return $page; +} + +/** @return string the page of results */ +function installerFlowInstall(Client $browser): string +{ + $page = (string) $browser->get('/install/index.php')->getBody(); + $form = installerFlowForm($page); + $page = installerFlowPost($browser, '/install/' . $form['action'], ['language' => 'en'] + $form['fields']); + $form = installerFlowForm($page); + $page = installerFlowPost($browser, '/install/' . $form['action'], ['installmode' => '0'] + $form['fields']); + + $connection = [ + 'database_type' => 'sqlite', 'method' => 'sqlite', 'host' => '', 'uid' => '', 'pwd' => '', + 'database_name' => 'flow', 'tableprefix' => 'evo_', 'installMode' => '0', + ]; + $collations = installerFlowPost($browser, '/install/index.php?s=1&action=connection/collation', $connection); + preg_match('/