Skip to content

fix(installer): make "remove the install folder" work - #2551

Open
elcreator wants to merge 10 commits into
evolution-cms:3.5.xfrom
elcreator:fix-installer-removal-3.5
Open

elcreator wants to merge 10 commits into
evolution-cms:3.5.xfrom
elcreator:fix-installer-removal-3.5

Conversation

@elcreator

@elcreator elcreator commented Nov 4, 2026 •

Copy link
Copy Markdown

Problem

The box "Remove the install folder" on the last page of the web installer does not work, seen on 3.5.9, 3.5.x and 3.6.x: the folder stays and the browser gets Not found. If you were installing Evolution CMS, the installer session has expired. It happens on a new site and on an update, on every web server.

install/src/controllers/install.php requires core/includes/define.inc.php, which ends with unset($base_path, $base_url) at the global scope. The controller then looks for $base_path . 'install.session.php', with $base_path now null, finds nothing, and skips writeLock(). The page still prints a token, but the lock never holds it, so remove_installer.processor.php refuses it. Seen on a real site by logging the values at that line: {"base":null,"exists":false}.

InstallerWebPathTest did not notice because it writes the lock by hand and runs only the processor.

On nginx the box could not have worked anyway: ng.inx runs PHP only from an allowlist, and the processor is not on it.

Change

  • install.php uses EVO_BASE_PATH, which define.inc.php leaves alone.
  • ng.inx allows manager/processors/remove_installer.processor.php. It stays guarded by the one-time token.
  • New core/tests/Feature/InstallerRemovalFlowTest.php: a real site on SQLite behind php -S, driven through the real forms like a browser. On a new site and on an update (an administrator with the installer grant from the Manager) it checks that the ticked box removes the folder and the lock, and that an unticked box keeps the folder, shut to other browsers. It also checks that a forged token removes nothing.
  • The test cannot reach a real database, whatever installation it runs from. Local configuration stays out of the copy (.env files, core/custom, connection files, the env cache), the server starts with an allowlisted environment, the site gets its own core/custom/.env, and before the first write each test asks the site what it resolves and stops on anything else. Every flow runs next to conflicting settings that lead to a separate SQLite file, whose hash must not change.
  • ApacheConfigHardeningTest pins the ng.inx entry, and that other processors stay refused.

Other fixes in this PR

One commit each, found while running sites through EasyPanel:

  • ng.inx lets /update.php through, behind the same access limit as the manager. The classic updater of the manager runs it.
  • CustomComposerPackages no longer treats dev-main and dev-main 18f1a03 as a change. Two different commits still count as a change.
  • The installer accepts database hosts with underscores, such as Docker service names.
  • The installer accepts a SQLite database in a folder below core/database. Dots stay refused, so the path cannot leave it.
  • The installer lock keeps its IP binding and tells visitors behind a proxy apart: client@remote when a valid forwarded address is present, REMOTE_ADDR as before otherwise. The cost: a forwarded header a local proxy passes on changes the identity, so a developer behind one waits for the lock to expire instead of taking it over.
  • The connection form says which settings come from DB_* in the environment.
  • The dashboard and make:site warn when .htaccess lacks the rules of the release (HtaccessRules, Apache only, can be hidden). The file itself is not changed.
  • Translations of the new strings. Not reviewed by native speakers; ky, tk, tg, ka and hy deserve a look first.

Verification

In evo-ci-php:8.4, PHP 8.4.26:

  • The new test passes with the fix, and without it all 5 removal cases fail, on both 3.5.x and 3.6.x.
  • Isolation, broken on purpose: with the copy filter off, all 10 flow cases stop before writing. With the filter, the check and the fixture's own config all off, the installer's migrations did go to the conflicting database (Connection: default, Database: .../original.sqlite), so the risk is real.
  • composer test -- --compact --fail-on-warning --fail-on-risky --fail-on-deprecation: on 3.5.x 2309 passed, 9 skipped, 0 failed.
  • Real nginx with ng.inx: /update.php reaches FastCGI, /other.php and /update.php/evil.php stay refused.
  • Real Apache site: no warning with the current ht.access, a warning naming the rule once the rule against PHP under assets is removed.
  • PHPStan: no errors.
  • Not checked on a live site, tests only: the make:site warning during a real update, the lock behind a real proxy, the notice on the form. A SQLite subfolder was not tried in a browser; the web installer needs the folder to exist.

Notes

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Seiger commented Nov 4, 2026

Copy link
Copy Markdown

Could we isolate the database configuration of InstallerRemovalFlowTest.php before merging?

installerFlowCopy() copies the working tree, including .env files and custom configuration. The exclusion list removes the generated default connection, but does not exclude these other configuration sources. Clearing inherited DB_*, EVO_*, and APP_* variables before starting PHP does not prevent the copied application from loading them again from its configuration files.

If the test runs from an existing configured installation, those settings can override the intended temporary SQLite connection. Because the test drives real install/update flows, migrations or seeding could then reach a real database.

Please create a clean, test-owned configuration, exclude local environment/custom configuration from the copy, and verify that the effective database connection points to the fixture’s temporary SQLite file before any database writes. A regression test with deliberately conflicting local configuration would help ensure that neither the fresh-install nor update scenario can touch the original database.

SokilAI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@elcreator

Copy link
Copy Markdown
Author

Thanks, the risk was real. With the isolation removed on purpose, the installer's migrations did reach the conflicting database (Connection: default, Database: .../original.sqlite).

Fixed in the latest commit:

  • Local configuration stays out of the copy: .env files, core/custom apart from the samples, connection files, the env cache and config.inc.php.
  • The server starts with an allowlisted environment, because Symfony Process also merges $_ENV.
  • The fixture writes its own core/custom/.env.
  • Before the first write each test asks the site what it resolves and stops on anything else.
  • Every fresh and update flow runs next to conflicting settings that lead to a separate SQLite file, and its hash is checked after each test. A dataset test plants settings in five places and checks that the flow is refused.

With the copy filter disabled, all 10 flow cases stop before writing.

elcreator and others added 8 commits November 5, 2026 04:24
The update widget writes update.php to the site root and opens it, and the
catch-all of ng.inx answered 404, so the update could not start on nginx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames its commit

The lock gives "dev-main 18f1a03" and Composer prints "dev-main", so every
update of a site with a package on a branch stopped and asked for consent.
Two different references still count as a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docker Swarm names services project_service, and the hostname filter refused
them, so the installer could not reach a database in such a stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…base

The name check refused "/", so a database on a mounted folder could not be
chosen in the browser. It stays a SQLite thing, and dots stay refused, so the
name cannot leave core/database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REMOTE_ADDR behind a proxy is the proxy, so the address check of the lock
compared one value for everyone, and on the same host that value is loopback,
which may take over a lock: a stranger could take a running install. The lock
now keeps the forwarded client next to REMOTE_ADDR. Without a proxy nothing
changes, and the header cannot be used to pass for another address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment overrides

The connection config asks env('DB_*') first, so what the form said about
those settings was dropped without a word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s release

An update keeps the site's .htaccess, so a site installed before a rule was
added stays without it: on 3.5.8 updated to 3.5.9 dumps under assets could be
read and PHP under assets ran. The file stays the operator's: make:site and
the dashboard name what is missing, and the warning can be hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants