From 4c7d33ddee0a0df5b3660c7cf46d2a3da36ad8da Mon Sep 17 00:00:00 2001 From: rumblefrog Date: Mon, 25 May 2026 17:02:14 -0400 Subject: [PATCH] fix(1441): kick the player from their server when the ban is added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-clicking a player → Ban → submitting the form would land the ban row in the database (and SourceMod's on-connect check would correctly reject subsequent reconnects), but the player's existing session stayed alive until they voluntarily disconnected — often 20-40 minutes later on a low-turnover server. The user-visible symptom from #1441: "ban is added to database and marked as active but player is not kicked from server". Root cause was a v1.x → v2.0 cutover oversight. The post-`Actions.BansAdd` `.then()` handler in `page_admin_bans_add.tpl` carried: if (r.data && r.data.kickit && typeof window.ShowKickBox === 'function') { window.ShowKickBox(r.data.kickit.check, r.data.kickit.type, ...); } `window.ShowKickBox` was defined in `web/scripts/sourcebans.js`, deleted with the rest of the v1.x bulk JS at #1123 D1 (v2.0.0). The `typeof ... === 'function'` guard silently resolves to `false`, the body of the `if` never runs, AND no console error fires — so the surrounding success branch falls through to a generic "Ban added" toast while the load-bearing rcon side-effect (kick the player off every server they're currently on) silently doesn't happen. This is the worst class of UI regression: chrome reads as success while the action never fires. `api_bans_add` has been returning the kickit envelope shape (`{kickit: {check, type}, reload: true}`) the whole time — only the client-side branch that consumes it was dead. SourceMod's on-connect hook DOES catch the next time the player tries to reconnect, which masked the bug to anyone testing the flow quickly (the "ban then click Kick" two-step the reporter describes is what muscle-memory has been compensating with). Fix --- Mirror the existing comms.add → blockit.php iframe pattern that has been working continuously since v2.0. In the `.then()` handler: if (r && r.ok && r.data && r.data.kickit) { var iframe = document.createElement('iframe'); iframe.id = 'srvkicker'; iframe.style.display = 'none'; iframe.src = 'pages/admin.kickit.php?check=' + encodeURIComponent(r.data.kickit.check) + '&type=' + encodeURIComponent(r.data.kickit.type); document.body.appendChild(iframe); ... } The iframe's `page_kickit.tpl` script (untouched server-side) auto-fires `Actions.KickitLoadServers` on the iframe's load event, enumerates enabled servers, and fans out per-server `Actions.KickitKickPlayer` rcon calls. The operator never sees the iframe (`display: none`) — they just see the "Ban Added" toast followed by a 2s reload of the parent page (matching comms.add's shape exactly). Also standardised the fallback branch's toast title from 'Ban added' (sentence case) to 'Ban Added' (Title Case) so both branches are visually symmetric AND match comms.add's 'Block Added'. The mixed casing was pre-existing but invisible because only the fallback branch ever ran on the live panel; with both branches now running on different installs (kickit-enabled vs kickit-disabled), the inconsistency would have become operator-visible. Coverage -------- - `web/tests/e2e/specs/flows/ban-kickit-iframe.spec.ts` (NEW) pins both branches end-to-end. Stubs `Actions.BansAdd` AND `pages/admin.kickit.php` at the network layer so the spec stays robust under multi-worker DB pressure against the shared `sourcebans_e2e` DB; asserts the iframe is spawned EXACTLY ONCE (`toBe(1)` per reviewer Finding #1 — a future double-mount regression from a `.then()` chain that resolves twice or a `dispatchEvent` re-firing the submit handler must surface here, `toBeGreaterThanOrEqual(1)` would silently pass). Sister test asserts the kickit-DISABLED response (no `r.data.kickit`) does NOT spawn the iframe. - `web/tests/e2e/specs/flows/admin-ban-lifecycle.spec.ts` + `web/tests/e2e/pages/SubmitBanFlow.ts` widen the post-add toast matcher to `/ban added/i` so both casings round-trip; comments document the new contract. - `web/themes/default/page_kickit.tpl` docblock updated to describe the new caller chain (the kickit iframe is now reached from `page_admin_bans_add.tpl`'s success branch as well as the existing context-menu top-level navigation added at #1433). AGENTS.md --------- - New anti-pattern entry banning `typeof window. === 'function'` guards against deleted v1.x helpers (`ShowKickBox`, `TabToReload`) with canonical file paths, the JS field-name asymmetry callout (`r.data.kickit.check` vs `r.data.block.steam` — the URL parameter `check=` is symmetric, the envelope field names are not, because `api_bans_add` and `api_comms_add` chose different envelope shapes), and the regression-guard pointer. Search anchor for future sweeps: `rg "typeof window\.\w+ === 'function'" web/themes/`. - New "Where to find what" row documenting the bans.add → kickit.php iframe fan-out contract alongside the comms.add → blockit.php sibling, the field-name asymmetry reminder, and the regression-guard pointers (`ban-kickit-iframe.spec.ts` + `kickit-iframe.spec.ts` + `KickitTest.php`). Adversarial-reviewer follow-up ------------------------------ A spawned reviewer subagent enumerated 11 findings; #1-#5 (this PR's correctness + docs polish) are addressed: 1. Iframe-request count assertion tightened to `toBe(1)`. 2. AGENTS.md anti-pattern reworded — pre-fix it claimed the new branch was "byte-for-byte symmetric to comms.add"; it isn't (different envelope field names). Now spells out the JS field-name asymmetry as documented structural API drift. 3. Fallback toast title standardised to Title Case. 4. New "Where to find what" row added (this commit). 5. Anti-pattern entry now cites canonical file paths (`page_admin_bans_add.tpl`, `page_admin_comms_add.tpl`, `ban-kickit-iframe.spec.ts`, `kickit-iframe.spec.ts`, `KickitTest.php`). #6-#9 (reload-on-success asymmetry, iframe-load-error UX, parent-reload vs iframe-rcon-fan-out race, iframe-never-removed) are pre-existing comms.add behaviour the PR inherits unchanged. Reviewer's recommendation is to file them as follow-up issues; they're architectural observations, not blockers for #1441's scope. #10 (duplicate `srvkicker` id) and #11 (theoretical XSS via `k.check`) were analysed and confirmed safe by design — three defence-in-depth gates: server-side `SteamID::HANDLER_STRICT_REGEX`, client-side `encodeURIComponent`, Smarty auto-escape. Fixes: #1441 CLA: I have read the CLA Document and I hereby sign the CLA Co-authored-by: Fishy --- AGENTS.md | 55 +++ web/tests/e2e/pages/SubmitBanFlow.ts | 21 +- .../specs/flows/admin-ban-lifecycle.spec.ts | 24 +- .../e2e/specs/flows/ban-kickit-iframe.spec.ts | 401 ++++++++++++++++++ web/themes/default/page_admin_bans_add.tpl | 60 ++- web/themes/default/page_kickit.tpl | 26 +- 6 files changed, 553 insertions(+), 34 deletions(-) create mode 100644 web/tests/e2e/specs/flows/ban-kickit-iframe.spec.ts diff --git a/AGENTS.md b/AGENTS.md index b1adc21d8..32d6a659b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2512,6 +2512,60 @@ contacting every contributor individually. via `data-action` + a page-tail vanilla-JS dispatcher per the canonical confirm-modal shape under "Add a confirm + reason modal" in "Where to find what". +- `typeof window. === 'function'` guards on a v1.x + helper inside a v2.0 page-tail handler (e.g. + `typeof window.ShowKickBox === 'function'` for the kick-on-ban + surface; `typeof window.TabToReload === 'function'` for the + post-action page-reload trigger) → both were defined in + `web/scripts/sourcebans.js`, deleted at #1123 D1 (v2.0.0). The + `typeof` test silently resolves to `false`, the body of the + `if` never runs, AND no console error fires — so the surrounding + success branch falls through to a generic toast while the + actual side-effect (kick a player, reload the tab) silently + doesn't happen. This is the worst class of UI regression: the + chrome reads as success while the load-bearing action never + fired. The kick-on-ban path was the canonical case (#1441 — + operators reported "banning doesn't kick" on the v2.0 panel; the + DB row landed and SourceMod's on-connect check did reject + subsequent connects, but the player's existing session stayed + alive until they voluntarily disconnected, often 20-40min later + on a low-turnover server). The fix shape is the existing + comms.add → blockit.php iframe pattern (canonical: + `web/themes/default/page_admin_comms_add.tpl`'s + `Actions.CommsAdd .then()` handler; mirrored at + `web/themes/default/page_admin_bans_add.tpl`'s + `Actions.BansAdd .then()` handler post-#1441): create a hidden + `