[3.0] Give each action subclass its own instance in ActionTrait::load() - #9321
Open
albertlast wants to merge 1 commit into
Open
[3.0] Give each action subclass its own instance in ActionTrait::load()#9321albertlast wants to merge 1 commit into
albertlast wants to merge 1 commit into
Conversation
ActionTrait declares $obj as a static property, and a static property is
shared with every descendant class that does not redeclare it. None of
the eleven action classes that extend another action redeclare it, so
they all share one slot with their parent.
Once the parent has been loaded, load() finds that slot occupied and
returns the parent's instance, which does not satisfy the "static"
return type:
SMF\Actions\Login2::load(): Return value must be of type
SMF\Actions\Logout, SMF\Actions\Login2 returned
This is reachable during login: User::enforceBans() calls Logout::call()
to kick a banned member, by which point Login2 has already been loaded,
so a banned member gets a fatal error instead of being logged out.
Checks that the cached instance is of the class being loaded, rather than
merely present.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 29, 2026
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This change was produced by an LLM. The code, the commit message and this
description were all written by Claude (Anthropic), driven by @albertlast. It has
not yet had human code review.
Everything stated below was verified by actually running it against a PostgreSQL
install of this branch, rather than only reasoned about. Even so, please review it
as untrusted work: the diagnosis may be right while the fix is not what SMF would
prefer stylistically or architecturally.
Description
A banned member who tries to log in gets a fatal error instead of being told they
are banned:
ActionTraitkeeps the cached instance in a static property:A static property is shared with every descendant class that does not redeclare it.
Eleven action classes extend another action, and none of them redeclare
$obj:So each of those pairs shares one slot. Once the parent has been loaded,
isset()is true for the child as well, and the child gets handed the parent's instance —
which does not satisfy the
staticreturn type.The reachable case today is logging in.
?action=login2loadsLogin2, andLogin2::DoLogin()reachesUser::enforceBans(), which kicks a banned member with:Logout::load()then returns theLogin2instance that is already sitting in theshared slot, and the member sees the TypeError above rather than the ban notice.
They also are not logged out, since
Logout::execute()never runs.This checks that the cached instance is of the class being loaded, rather than merely
present. Keeping
$objas-is, rather than switching to per-class storage, keeps theproperty's meaning unchanged for anything that reads it. In the rare request that
loads both a parent and a child action, the second
load()simply constructs a newinstance; the constructors are empty, so there is nothing to lose.
How this was verified
PostgreSQL 17 / PHP 8.4.23.
The baseline was
release-3.0plus the pending fixes #9310–#9317 and #9319, since anon-admin cannot complete a login without those and so cannot reach this code at all.
With a
cannot_accessban on the member:POST ?action=login2→ HTTP 500 with the TypeError above, and itrecorded in the error log.
forum!" together with the ban reason, and the member is logged out —
index.phpafterwards returns HTTP 200 as a guest.Since this touches the loader every action goes through, also swept a range of
actions as an ordinary logged-in member, deliberately including the subclassed ones:
recent,unread,unreadreplies,stats,who,profile,pm,search,help,credits,agreement,notifyboard, a board view, the post form, andfinally
logout. All HTTP 200 (logout 302), no errors logged.php -lclean.Relationship to other PRs
Sources/ActionTrait.phpis not touched by any other open PR. It sits immediatelyafter #9319 in the same login flow: #9319 gets an ordinary member through
enforceBans(), and this one handles what happens when that member turns out to bebanned. They are independent changes to different files and merge cleanly together.
While confirming this, one unrelated warning shows up on the same path —
Undefined array key "email_address"atSources/User.php:2087, when a ban matchesby member id rather than by email. Not addressed here; noted only so it is not
mistaken for a side effect.
Issues References (Fixes|Related|Closes)