Skip to content

[3.0][Testing] Install the forum from the command line - #9344

Open
albertlast wants to merge 12 commits into
SimpleMachines:release-3.0from
albertlast:tests/install-cli
Open

[3.0][Testing] Install the forum from the command line#9344
albertlast wants to merge 12 commits into
SimpleMachines:release-3.0from
albertlast:tests/install-cli

Conversation

@albertlast

@albertlast albertlast commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Description

The dev environment from #9317 stops at a generated Settings.php and a staged
install.php, leaving the install itself to a human clicking through a browser.
That is the one step between a fresh clone and a running forum that could not be
scripted, and everything that wants to test against a real install has to start
by doing it.

This makes it one command:

.docker/install-forum.sh --engine mysql
.docker/install-forum.sh --engine both

Four scripts under .docker/: install-forum.sh, use-engine.sh, reset.sh,
and a sourced lib.sh. Nothing ships with the forum — .docker/ is already
skipped by check-smf-index.php and check-smf-license.php.

The installer turned out to be almost there already: parseCliArguments() turns
--name=value into $_POST and execute() runs every step in one process. So
this needs two passes rather than the five curl requests 2.1 needs. The second
carries pop_done, which is the short-circuit past the population report;
passing it on the first pass would skip building the schema entirely.

--engine both installs MySQL and then PostgreSQL, sequentially — Settings.php
pins one $db_type and Db::load() hands back the connection it already made,
so only one engine is ever live in a process. Both installs are kept, and
use-engine.sh switches between them without reinstalling.

The five installer fixes this needed

Each of these only bites without a browser, so none could be worked around from
outside. Four are CLI-only; the last one is wrong in the browser too.

  1. Nothing was ever reported. Maintenance::exit() renders the tool's
    templates, and those are the only place errors are shown. On the command line
    it takes the fallthrough path straight to die(), so a scripted install that
    died on step three looked exactly like one that finished — no message, exit
    status 0. ToolsBase::updateSettingsFile() made the same assumption more
    directly, calling die() outright rather than recording the error.
  2. forumSettings() was fatal. It built a suggested board URL with
    substr($self, 0, strrpos($self, '/')). getSelf() is $_SERVER['PHP_SELF'],
    which on the command line is usually a bare install.php with no directory in
    it, so strrpos() returns false and substr() throws on PHP 8.
  3. defaultHost() read $_SERVER['SERVER_NAME'] and ['SERVER_PORT']
    unguarded, so every run opened with an undefined index warning.
  4. finalize() signed a browser in, setting a login cookie and recording a
    session against the user agent that asked for it. With no browser that is four
    warnings — headers sent after output started, a session that could not start,
    an id that could not be regenerated — and a sessions row built from an
    undefined HTTP_USER_AGENT.
  5. An unrecognised database type reported nothing. It used
    Lang::getTxt('upgrade_unknown_error'), which is not a string that exists, so
    the fatal error was blank in the browser as well. Now names the type that was
    rejected and the ones that would have been accepted — which matters most on
    the command line, where the type is typed by hand rather than picked from a
    list of exactly those keys. (They are capitalised: MySQL, PostgreSQL.)
Verified

Installed from scratch on both engines, from the tree in this branch:

  • .docker/install-forum.sh --engine both --pin-secrets --force completes clean,
    no warnings, exit 0.
  • Both forums serve 200 on the board index, login, help, recent, stats and
    credits.
  • check-signed-off.php, check-smf-index.php, check-smf-license.php and
    check-smf-languages.php all pass; shellcheck is clean on all four scripts.

Not addressed here, because it is not CLI-specific: a fresh install logs five
undefined_vars errors for latestMember / latestRealName, because
finalize() loads the theme before Logging::updateStats('member') runs. The
values are correct once installed, and browsing afterwards is clean.

Merge order

Merge #9317 and #9316 before this one. Both are contained in this branch, so the
diff shown here is theirs as well as its own; once they land and this is rebased on
release-3.0, what is left is the install script and the installer fixes it needed.

Issues References (Fixes|Related|Closes)

  1. Depends on [3.0][Testing] Add a Docker development environment for MySQL and PostgreSQL #9317 — this builds on .docker/ and compose.yaml.
  2. Depends on [3.0] Load the current user before finalize() records dates #9316 — merged in here, or the install dies on its last step.

albertlast and others added 8 commits August 2, 2026 07:33
Maintenance::exit() renders the tool's templates, and those are the only
place errors are ever shown. On the command line it takes the fallthrough
path instead and goes straight to die(), so nothing was reported and the
exit status was always 0: a scripted install that died on step three
looked exactly like one that had finished.

ToolsBase::updateSettingsFile() made the same assumption more directly,
calling die() outright when Settings.php could not be written rather than
recording the error the way the web path does.

Writes the warnings and errors to stderr and exits non-zero when the tool
actually failed. A step that merely wants input it was not given sets
neither, so pausing part way through is still a success - the installer
is meant to be called more than once - and that case now says which step
it stopped on instead of nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Two things in the installer only hold when a browser is on the other end,
and both are reached before the forum exists, so neither could be worked
around from outside.

defaultHost() reads $_SERVER['SERVER_NAME'] and ['SERVER_PORT'] whenever
HTTP_HOST is absent. On the command line none of the three is set, so
every run began with an undefined index warning. Falls back to localhost:
the value only seeds the suggested board URL on the form, and a scripted
install passes its own boardurl in.

forumSettings() then built the same suggestion with
substr($self, 0, strrpos($self, '/')). getSelf() is $_SERVER['PHP_SELF'],
which in a request is a rooted path but on the command line is whatever
was typed - usually a bare 'install.php' with no directory in it. strrpos()
returns false, and substr() with a false length is fatal on PHP 8, so the
installer died here on every CLI run.

While in there: an unrecognised database type reported
Lang::getTxt('upgrade_unknown_error'), which is not a string that exists.
The fatal error was therefore blank in the browser too. Names the type
that was rejected and the ones that would have been accepted, which
matters most on the command line where the type is typed by hand rather
than picked from a list of exactly those keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
finalize() ends by signing the new administrator in, so the browser that
just ran the installer lands on an admin session instead of a login form.
It sets a login cookie, then records the session against the user agent
that asked for it.

None of that has any meaning on the command line. There is no browser to
hold the cookie and no user agent to key the session on, so every CLI
install ended with four warnings - headers sent after output had already
started, a session that could not be started, and an id that could not be
regenerated - and then wrote a sessions row built from an undefined
HTTP_USER_AGENT.

Runs the whole block only when there is a request behind it. The stats
that follow it are untouched, so an install still records latestMember,
totalMessages and totalTopics either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Two things were wrong with the note the command line prints when a tool
stops part way. It indexed the step list to get the number, which counts
from zero, while every other line of output uses the step's own id, which
counts from one - so it disagreed with the "Step 3: Database Settings"
lines immediately above it.

It also fired on a successful run. Tools deliberately return false from
their last step so the web flow stops and renders its "all done" template,
which means reaching that step is success rather than a pause, and a
completed install claimed to have stopped at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The dev environment stopped at a Settings.php and a staged install.php,
leaving the actual install to a human clicking through a browser. That is
the one step between a fresh clone and a running forum that could not be
scripted, and everything that wants to test against a real install has to
start by doing it.

Adds four scripts under .docker/:

  install-forum.sh   installs a forum, no browser involved
  use-engine.sh      switches which installed forum is live
  reset.sh           empties one engine's database and restages
  lib.sh             shared settings and engine name normalisation

The installer is already CLI-native - parseCliArguments() turns
--name=value into $_POST and execute() runs every step in one process -
so this is two passes rather than 2.1's five curl requests. The second
pass carries pop_done, which is the short-circuit past the population
report; passing it on the first pass would skip building the schema.

--engine both installs MySQL and then PostgreSQL. It has to be sequential:
Settings.php pins a single db_type and Db::load() hands back the
connection it already made, so only one engine is ever live in a process.
Both installs are kept, and use-engine.sh swaps between them by putting
the saved Settings.php back - no restart, because the entrypoint only
writes one when there is not one already.

--pin-secrets fixes auth_secret and image_proxy_secret, which are
generated with random_bytes() and stored nowhere but Settings.php. Without
it the two installs differ by more than their database and a login cookie
does not survive the switch. The cookie name needs no such help:
createCookieName() is a crc32 of the database name and prefix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The README invokes them as .docker/install-forum.sh rather than through
bash, which only works with the bit set. Windows checkouts do not carry
it, so it has to be recorded in the index.

lib.sh is left alone: it is sourced, never run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The installer tells you to delete it and cannot do it itself: the ?delete
link it offers is a GET, and command line arguments only ever reach
$_POST, so nothing on the CLI path ever gets there.

Leaving it behind is not cosmetic. Settings.php redirects every request
back into the installer while the file exists, so the forum the script
just built is unreachable, and SMF puts a "MAJOR SECURITY RISK: you have
not removed install.php" box on every page it shows an administrator -
which also lands in front of anything else a test or a person is trying
to read on that page.

Deleting it is safe for a reinstall because install_one() calls reset.sh
first, and reset.sh clears Settings.php and then blocks until the
entrypoint has staged a fresh copy. Adds a check in front of the two
installer passes to say so out loud when it has not: without one, php
reports "Could not open input file: install.php", which reads like a
broken script rather than a stack that was never made installable.

Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Two forums side by side, each with its own administrator, and a password
chosen at install time is a combination that ends in hand written SQL
sooner or later - which is a poor way to answer a question as ordinary as
"is this the password?".

user.sh answers it. list shows the accounts, check says whether SMF would
accept a password and exits 0 or 1 so it can be used in a conditional,
and reset sets a new one. --engine reads the settings use-engine.sh saved
for the other engine, so the forum that is not currently live can be
looked at without switching to it and back.

Two details that stop it being a thin wrapper around an UPDATE:

  - The hashing goes through Security::hashPassword() rather than being
    written here, so what lands in the table is by construction what
    Login2 reads back out. A script that hashes passwords its own way is
    a script that eventually disagrees with the forum.
  - reset clears passwd_flood too. SMF locks an account out for a while
    after enough wrong guesses, and a new password behind a live lockout
    behaves exactly like a password that did not take.

check also points out an account that is not activated, which fails to
log in with an entirely correct password.

The password is passed to the container through the environment rather
than in the argument list, which anything able to read the process table
can see. Also completes the file list in the README, which still only
described the image and had none of the scripts in it.

Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
@github-actions github-actions Bot added Installer Localization Language & internationalization labels Aug 16, 2026
@jdarwood007 jdarwood007 added this to the 3.0 Alpha 6 milestone Aug 23, 2026
The Docker environment this branch was built on has landed upstream in
SimpleMachines#9317, so the copies of compose.yaml and most of .docker/ that were
riding along in this diff are now the same files release-3.0 already
has. Merging drops them from the diff and leaves the install tooling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Installer Localization Language & internationalization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants