fix(1433): kick via panel + kick-on-ban + add server by hostname#1434
Merged
Conversation
…mes in servers.add Three bugs from #1433 collapse to two root causes: - Bugs 1 + 2 (kick via panel / kick-on-ban): the iframe-routed kickit and blockit surfaces load /scripts/api.js with a hardcoded `endpoint: './api.php'`. Document-relative resolution against the iframe URL `/pages/admin.kickit.php` lands on /pages/api.php (404), the silent early-return in the iframe template's load handler keeps every row at the initial "Waiting..." text, and the player is never kicked. Resolve the endpoint against `document.currentScript.src` so panel-root + iframe + subdir installs all converge on the same panel-root /api.php. - Bug 3 (hostnames rejected on "Add a server"): api_servers_add ran FILTER_VALIDATE_IP alone despite the form claiming hostname support. Accept FILTER_VALIDATE_DOMAIN+FILTER_FLAG_HOSTNAME as the alternative; the sibling page handler in admin.edit.server.php already accepts hostnames. Form help text updated to match. Adds a static regression guard for api.js's endpoint contract (ApiJsEndpointResolutionTest — asserts document.currentScript + new URL('../api.php' are present, and the bare construction-time literal is gone), a Playwright E2E spec asserting the fetch lands on /api.php (not /pages/api.php), and per-handler ServersTest cases for the hostname / fqdn / garbage-address paths. AGENTS.md gets a one-liner anti-pattern entry + a Where-to-find-what row for web/scripts/api.js.
…tighter tests Adversarial-review follow-up on the prior #1433 fix commit. Pinned the five gaps the reviewer flagged: 1. **Schema-width gate** (BLOCKING). `:prefix_servers.ip` is `VARCHAR(64) NOT NULL` (see `install/includes/sql/struc.sql`), well below RFC 1035's 253-char hostname max. With the prior commit the handler happily accepted a 200-char FQDN, then `INSERT` raised `SQLSTATE[22001] 1406 Data too long` under MariaDB strict mode — the dispatcher's `Throwable` fallback wrapped it as a generic `server_error` 500 with no audit-log entry. Both `api_servers_add` and `admin.edit.server.php` now reject `strlen($ip) > 64` upstream with a structured `validation` envelope / `$validationErrors` entry. Bumping the column to `VARCHAR(255)` is a paired schema-migration follow-up; the gate matches the live column exactly. 2. **Edit-server validator parity**. `admin.edit.server.php` ran a hand-rolled `^[a-zA-Z0-9.\-]+$` regex pre-fix — looser than `FILTER_FLAG_HOSTNAME` (accepted leading hyphens, double dots, IDN-mangled UTF-8) so the same value would round-trip through Edit but get rejected by Add (and vice versa for valid hostnames the JSON handler accepted post-prior-commit). Both surfaces now share the IP || HOSTNAME filter pair so the contract is byte-symmetric. 3. **Test coverage gaps**. Added: - `testAddAcceptsBareIPv6` — `:` characters fail `FILTER_FLAG_HOSTNAME`, so the bare-IPv6 input can ONLY pass via the `FILTER_VALIDATE_IP` arm. Pins both that the IP arm is wired AND that bare-v6 inputs (cloud / dual-stack hosts) work. - `testAddRefusesDuplicateHostnamePort` — pre-fix the duplicate detection ran on `(ip, port)` when only IPs landed there. Hostnames travel through the same code path and must collide on identical `(hostname, port)` too. - `testAddRejectsAddressExceedingSchemaWidth` — pins the 64-char gate's wire-format (validation envelope + `'address'` field). - Renamed the prior `testAddValidatesIpFormat` to `testAddRejectsWhitespaceInAddress` (the misleading name implied IP-format-specific assertions; the test was actually about "fail both filters" garbage input). - Pinned the `port` column round-trip on `testAddAcceptsFqdn` — #1433 is the first hostname-shaped row through the writer, so a future refactor that mis-types-coerces `port` on the hostname-bearing arm would slip past the IP-bearing tests. 4. **api.js docs + static-load contract**. The prior commit's source comment said the `<script>` tag lives in `core/footer.tpl` — wrong; it's in `core/header.tpl` (and `page_kickit.tpl` / `page_blockit.tpl` for the iframe surfaces). Corrected, and inlined the load-bearing static-load requirement (`document.currentScript` is null when the script is appended programmatically — that's the exact pre-#1433 bug shape, so a future refactor that "modernises" to dynamic-injection silently re-opens the regression). 5. **AGENTS.md anti-pattern entry**. Added a paired entry under Anti-patterns for the `FILTER_VALIDATE_IP`-alone shape and the sibling too-loose hand-rolled hostname regex, plus the schema-width gate; updated the existing api.js row to document the static-load contract; added a "Where to find what" row for the shared server-address validator pattern. 6. **Kickit E2E spec tightened**. The post-load assertion now pins that BOTH `kickit.load_servers` AND `kickit.kick_player` POST to `/api.php` (previously just "at least one POST landed on /api.php" — would silently pass on a regression that broke only one of the two actions). Gates: PHPStan ✓ · PHPUnit 881 tests / 3321 assertions ✓ · ServersTest 41 tests / 181 assertions ✓ · ApiJsEndpointResolutionTest 4/4 ✓ · ts-check ✓ · api-contract clean ✓ · kickit-iframe E2E 3/3 on workers=1 ✓ · smoke suite 62/62 ✓. Pre-existing flakes (server-map-thumbnail, server-player-context-menu, server-refresh-debounce, admin-groups-server-cards-hydration) reproduce on `main` and are unrelated to this PR — they fail against SourceQuery / live-socket paths under local-stack `workers > 1`.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Fixes #1433. The reporter hit three bugs against the v2.0.0 panel:
the player drawer / ban-detail surface popped the iframe but
every row stayed at "Waiting…" forever.
fan-out iframe (same
page_kickit.tpltemplate, justdifferent entry point) silently no-op'd for identical reasons.
advertises "IPv4 / IPv6 / hostname" in its help text but the
JSON handler rejected every hostname-shaped input with
"You must type a valid IP."
Bugs 1 + 2 share a root cause:
web/scripts/api.jsshippedendpoint: './api.php'as the load-bearing default. Document-relativeURL resolution lands the fetch on
/api.phpfor top-level panelrenders (the chrome's pages all live at
/index.php?p=…so./api.phpresolves against/), but the iframe-routed surfaces(
pages/admin.kickit.php/pages/admin.blockit.php) sit onedirectory deep — the iframe's document URL is
/pages/admin.kickit.php, so./api.phpresolved against that landson
/pages/api.php, which Apache does not rewrite. Every fetch fromthe iframe got 404, api.js's fetch resolved to a
bad_response-shapedenvelope, and the iframe's load handler short-circuits on
!r.okwith
return— the silent early-return path leaving every row at theinitial "Waiting…" text forever. Same code path on every iframe-routed
surface that loads api.js.
Fix: api.js now resolves the endpoint against
document.currentScript.srccaptured at script-load time.
new URL('../api.php', SCRIPT_SRC).hreflands on the panel-root
/api.phpfor both top-level page rendersAND iframe contexts AND subdir installs (
https://host/sourcebans/→script at
…/scripts/api.js→ endpoint at…/api.php).Bug 3:
api_servers_addranFILTER_VALIDATE_IPalone. Now acceptsEITHER a valid IP OR a valid hostname (
FILTER_VALIDATE_DOMAIN + FILTER_FLAG_HOSTNAME); paired with a 64-char schema-width gatebecause
:prefix_servers.ipisVARCHAR(64) NOT NULLand MariaDBstrict mode would otherwise raise
SQLSTATE[22001]mid-INSERT,surfacing as a generic 500 with no audit-log entry. The sibling
admin.edit.server.phpvalidator (which ran a too-loose hand-rolled^[a-zA-Z0-9.\-]+$regex) is now byte-for-byte symmetric so thesame value either round-trips through Add AND Edit or fails on both.
Changes
web/scripts/api.js—resolveEndpoint()computesnew URL('../api.php', document.currentScript.src).href; bare-relative fallback preservedfor hostile-loader contexts. Source comment documents the static-load
contract (
document.currentScriptisnullunder dynamic injection,which is the exact pre-fix bug shape).
web/api/handlers/servers.php—FILTER_VALIDATE_IP || FILTER_VALIDATE_DOMAIN(
FILTER_FLAG_HOSTNAME) +strlen() > 64schema-width gate.web/pages/admin.edit.server.php— same validator pair, same gate;pre-fix hand-rolled regex replaced.
web/themes/default/page_admin_servers_add.tpl— help text updatedto reflect that hostnames are now actually accepted.
AGENTS.md— paired Anti-pattern entries forFILTER_VALIDATE_IP-alone hostname validation + the too-loosehand-rolled hostname regex sibling
and the shared server-address validator pattern.
Tests
web/tests/api/ServersTest.php— six new / refined tests:testAddAcceptsHostname— happy path for simple hostnames.testAddAcceptsFqdn— multi-label FQDN (the reporter's marqueeshape); pins the
portcolumn round-trip too.testAddAcceptsBareIPv6— the:chars in2606:4700:4700::1111fail
FILTER_FLAG_HOSTNAME, so this pins theFILTER_VALIDATE_IParm of the OR is wired and exercised.testAddRefusesDuplicateHostnamePort— hostnames travel throughthe same
(ip, port)duplicate-detection code path as IPs.testAddRejectsAddressExceedingSchemaWidth— pins the 64-chargate's wire-format (validation envelope +
'address'field).testAddRejectsWhitespaceInAddress— renamed from the oldmisleading
testAddValidatesIpFormat; pins the "fail bothfilters" garbage class.
web/tests/integration/ApiJsEndpointResolutionTest.php— staticshape gate (4 assertions): api.js references
document.currentScript, computesnew URL('../api.php', ...),does NOT bind the bare literal to
sb.api.endpointatconstruction time.
web/tests/e2e/specs/flows/kickit-iframe.spec.ts— runtime gate;loads
/pages/admin.kickit.php?check=…&type=0directly, interceptsthe
Actions.KickitLoadServers+Actions.KickitKickPlayerPOSTs,asserts BOTH actions POST to
/api.php(NOT/pages/api.php),and the iframe row transitions past "Waiting…" to "Player not
found." (the well-formed terminal-envelope branch).
Test plan
Pre-existing local flakes (server-map-thumbnail, server-player-context-menu,
server-refresh-debounce, admin-groups-server-cards-hydration) reproduce
on
mainand are unrelated to this PR — they fail against SourceQuery /live-socket paths under local-stack
workers > 1.Fixes #1433