Skip to content

Rewrite the 12 MIXED files surfaced by goals#3's audit (Route B, all phases) - #1388

Merged
rumblefrog merged 14 commits into
mainfrom
goals-5-rewrite-mixed-files
May 15, 2026
Merged

Rewrite the 12 MIXED files surfaced by goals#3's audit (Route B, all phases)#1388
rumblefrog merged 14 commits into
mainfrom
goals-5-rewrite-mixed-files

Conversation

@rumblefrog

@rumblefrog rumblefrog commented May 15, 2026

Copy link
Copy Markdown
Member

Closes sbpp/goals#5.

Summary

Lands all four phases of the Route B rewrite in a single PR, so issue #5 closes here and goals#4 (ELv2 adoption) can land on a clean slate without per-file attribution footnotes.

  • Phase 1 (license reconciliation)LICENSE.md reverted from the unintended CC BY-SA 4.0 swap back to the verbatim CC BY-NC-SA 3.0 Unported legal code that every other live surface (README, install wizard, file headers, composer.json) already references. Same change propagated to the README badge URL/alt-text, CONTRIBUTING.md, and AGENTS.md's "Contributor License Agreement gate" section so all surfaces agree on the project's current license.
  • Phase 2 (per-file rewrites) — every MIXED file from goals#3's audit rewritten or deleted such that no substantive 1.4.11 expression remains:
    • web/getdemo.php — rewritten ~30 lines, Content-Disposition: attachment (RFC 6266) + X-Content-Type-Options: nosniff + Cache-Control: private replace Content-type: application/force-download; LFI hardening preserved.
    • web/includes/SmartyCustomFunctions.phpsmarty_function_help_icon and smarty_function_sb_button deleted (no live call sites; v2.0 chrome uses .btn / typed Views).
    • web/includes/system-functions.phpCreateLinkR, BitToString, SmFlagsToSb, NextSid, PruneBans, GetMapImage, Format1Decimal, getDirSize, GetMaxAdminID, Date2SQL, IsValidIP, MaskIP bodies restructured (PHP 8.5 idioms — native types, ??, match, str_contains, intdiv()); function names preserved as-is for theme-fork compat.
    • web/pages/page.admin.php — 9-subquery composite COUNT block + Sbpp\Theme::wantsLegacyAdminCounts() gate + legacyComputeCount() debug counter all deleted; web/includes/Theme.php, AdminHomePerformanceTest.php, profile-admin-home.php removed; AdminHomeView lost the legacy access_* / total_* / archived_* / demosize properties.
    • web/pages/admin.edit.{admindetails,admingroup,adminperms,adminservers,group,mod,server}.php — full rewrites against typed Sbpp\View\…View DTOs + Smarty templates; _admin_edit_helpers.php shared helper kit; CSRF + setBusy() + Lucide chrome; legacy <input onclick=ProcessEditAdminPermissions(); onmouseover=ButtonOver(...)> markup gone; the MooTools $('groupname').value idiom gone; echo '<div id="msg-red">...' blocks replaced with {if $error}<div class="alert alert--error">…</div>{/if} partials. admin.edit.adminservers.php's DELETE-then-INSERT loop on :prefix_admins_servers_groups is now beginTransaction / endTransaction / cancelTransaction-wrapped (no zero-groups window). The reviewer-flagged 1.4.11 perm-grid partials (groups.web.perm.php / groups.server.perm.php / group.name.php) and their three live JSON actions (admins.update_perms / groups.update_perms / groups.add_server_group_name) deleted along with their PHPUnit tests, snapshots, and PermissionMatrixTest rows.
    • Sbpp\Upload\UploadHandler — new shared upload handler under web/includes/Upload/; CSRF + permission gate + sanitiseName() LFI defence (also a NEW security fix on icon/mapimg uploads — they previously trusted $_FILES[…][name] verbatim) + extension allowlist + audit log. admin.uploaddemo.php / admin.uploadicon.php / admin.uploadmapimg.php collapsed to ~30-line wrappers.
    • web/themes/default/theme.conf.php — five define() constants restructured with per-define explanations; InterWave Studios attribution restored (was dropped during the SBPP-era header rewrite).
  • Phase 3 (header sweep) — 36 files migrated from the 17-line legacy Copyright © 2007-2014 SourceBans Team header to the 4-line v2.0 shape (PHP // comments + Smarty {* *} on .tpl). Files where the header sat after <?php + namespace + use (e.g. Sbpp\Auth\UserManager) keep the new header in the same position. web/includes/Auth/openid.php and web/includes/tinymce/** left untouched (own licences).
  • Phase 4 (THIRD-PARTY-NOTICES.txt) — new file at the repo root with six attributions: SourceBans 1.4.x (defensive — schema layout, permission flag bitmasks, audit-log letter codes), SourceComms 0.9.266, InterWave Studios, LightOpenID (verbatim MIT), TinyMCE (LGPL pointer), Composer dependencies.

Adversarial review pass

A second AI reviewer spot-checked the original 12-commit drop and surfaced two must-fix items, both addressed in the final two commits on this branch:

  • C1 (CRITICAL)groups.web.perm.php / groups.server.perm.php / group.name.php partials still served by api_admins_update_perms / api_groups_update_perms / api_groups_add_server_group_name. Per the audit, those partials had to "become typed Views too" — but Phase 2 originally only rewrote the page handlers and left the partials as a parallel surface served via file_get_contents(). Fixed in refactor(api): remove 1.4.11 perm-grid partials and their JSON actions: 562 lines of 1.4.11 markup + the three actions + their tests + their snapshots + their PermissionMatrixTest rows + the stale AdminGroupsAddView docblock all gone; api-contract.js regenerated.
  • H1 (HIGH) — Phase 1 reverted LICENSE.md correctly but left README.md's shields.io badge, CONTRIBUTING.md's CLA blurb, and AGENTS.md's "Contributor License Agreement gate" section still claiming "CC BY-SA 4.0". Fixed in chore(license): propagate CC BY-NC-SA 3.0 to README badge, CONTRIBUTING, AGENTS. rg -i "CC[\s_-]*BY[\s_-]*SA[\s_-]*4" now returns zero hits across the repo.

Reviewer's nice-to-fix observations (out of scope for #5) tracked separately:

  • Pre-existing XSS in CreateLinkR callers (admin-controlled values reach onclick= strings unescaped) — file as a follow-up; the rewrite touched the function but the fix is bigger than Fix invalid query #5's scope.
  • Pre-existing echo '<div id="msg-red">' blocks in admin.email.php (audit categorised it CLEAN so it was outside Fix invalid query #5; AGENTS.md anti-pattern still applies).

Quality gates

Gate Result
./sbpp.sh phpstan OK — No errors (237 files, level 5 + dba)
./sbpp.sh test OK — 595 tests, 2510 assertions (was 604; the 9-test drop matches C1's deletions)
./sbpp.sh ts-check OK
./sbpp.sh composer api-contract OK — git status clean post-regenerate (67 actions / 32 perms / 16 typedefs)
CI=1 ./sbpp.sh e2e --workers=1 258 passed, 276 skipped, 0 failed (the suite's documented workers: 1 shape)
`CI=1 ./sbpp.sh e2e --grep 'admin.*(perm group)' --workers=1` (post-C1)

web/phpstan-baseline.neon net -60 lines (313 → 312 entries; 4 dead ?? '' fallbacks cleaned).

Commit log (14)

3a7c7af7 chore(license): propagate CC BY-NC-SA 3.0 to README badge, CONTRIBUTING, AGENTS
2eb25133 refactor(api): remove 1.4.11 perm-grid partials and their JSON actions
8782755c docs(agents): document goals#5 conventions + new anti-patterns
d22a3154 chore(phpstan): regenerate baseline + clean dead null-coalesce sites
cd04b54c docs(third-party): add THIRD-PARTY-NOTICES.txt
36e4d8c5 chore(headers): replace 17-line legacy header with 4-line v2.0 shape
3f8c026c chore(theme.conf): restructure + restore InterWave Studios attribution
bba60dec feat(upload): introduce shared Sbpp\Upload\UploadHandler
6213792b refactor(admin-edit): rewrite the admin.edit.* cluster for v2.0 conventions
ec17b501 refactor(admin-home): drop legacy COUNT block + Sbpp\Theme gate
aa100ec0 refactor(system-functions): adopt PHP 8.5 idioms and restructure 1.4.x bodies
21ca9fce chore(smarty): drop dead help_icon and sb_button plugins
3bab8802 refactor(getdemo): rewrite for v2.0 conventions
eef675c6 chore(license): revert LICENSE.md to CC-BY-NC-SA 3.0 Unported

Test plan for review

  • ./sbpp.sh up against the branch, log in as admin/admin, walk every admin.edit.* flow (admins → edit details / change perms; groups → edit web / edit server / rename; servers → edit; mods → edit). Confirm save round-trip, error toast on validation failures, audit-log entries.
  • Trigger an upload through each of demo / icon / mapimg surfaces and confirm the new shared handler stores the file, emits the popup, and audit-logs.
  • Hit /getdemo.php?type=B&id=<known-bid> from a logged-in admin browser; confirm the file downloads with Content-Disposition: attachment and the LFI hardening still rejects type=X / id=999999 cleanly.
  • Read LICENSE.md — confirm it's the canonical CC BY-NC-SA 3.0 Unported legal code (title, §4(b), §4(c) all present).
  • Read THIRD-PARTY-NOTICES.txt — confirm the six attributions are substantive.
  • rg -l "Copyright © 2007-2014 SourceBans Team" . — only AGENTS.md (where the string is documentation) should remain.

I have read the CLA Document and I hereby sign the CLA

rumblefrog added 14 commits May 14, 2026 19:39
Phase 1 of goals#5. The LICENSE.md file has been carrying the
verbatim CC BY-SA 4.0 text (no NC clause) while every other surface
in the project — README.md's License section, all 50 file headers,
the install wizard's `$licenseText` heredoc + `page_license.tpl`,
and goals#2 — describes the panel as CC BY-NC-SA 3.0. The audit at
goals#3 confirmed this is an unintentional regression, not a quiet
relicense. Reconcile by replacing the file's contents with the
verbatim Creative Commons Attribution-NonCommercial-ShareAlike 3.0
Unported legal code from creativecommons.org/licenses/by-nc-sa/3.0/legalcode,
formatted in the same Markdown style the file already used.

This is NOT the relicense itself (that's goals#4's job) — just the
prerequisite reconciliation so every project surface agrees on the
panel's *current* license, which then becomes the clean baseline
goals#4 relicenses from.

Add a one-line cross-reference to THIRD-PARTY-NOTICES.txt at the
top so vendored third-party components (LightOpenID, TinyMCE,
InterWave Studios theme.conf.php attribution, and the defensive
SourceBans 1.4.x lineage credit) have an obvious pointer.
Drop the legacy 1.4.11-derived structure and rebuild around an
RFC 6266 `Content-Disposition: attachment` envelope plus an
RFC 5987 `filename*=UTF-8''…` slot for non-ASCII demo names.
Different control flow, different error strings, different MIME
shape (`application/octet-stream` over the non-standard
`application/force-download`), explicit HTTP status codes, and a
`getdemo_die()` `: never` helper so the 400/404/500 surfaces are
distinct.

LFI hardening (basename collapse + scandir membership check) is
preserved verbatim — both the type allowlist and the on-disk
listing probe stay load-bearing. Adds a defensive `ob_end_clean`
loop before `readfile()` so any output buffer stage upstream
(framework chrome, debug shim) doesn't truncate the binary
payload.

Drops the SVN-reference comment carried since 2014; the upstream
URL (`code.google.com/p/sourcebans/...`) doesn't exist anymore
and the underlying audit-log race the comment alluded to is
unrelated to download semantics.

Refs #5 phase 2.1.
Both plugins were 1.4.x carry-overs:

* `smarty_function_help_icon` emitted a one-shot `<img class="tip">`
  with the `images/help.png` asset (a v1.x SourceBans help icon that
  the v2.0 chrome replaced with Lucide glyphs). Zero live `.tpl`
  call sites — confirmed via `rg "\\{help_icon"` against `web/`.

* `smarty_function_sb_button` emitted an `<input type=button>` with
  the dead `onmouseover='ButtonOver("$id")'` legacy JS handler
  (`ButtonOver` was a `web/scripts/sourcebans.js` helper deleted at
  #1123 D1 along with the rest of MooTools). Same audit — zero live
  `.tpl` call sites.

The fix is tiny because every other plugin in the file
(`load_template`, `csrf_field`, `has_access`, `stripslashes`,
`htmlspecialchars`) is in active use; only the two 1.x carry-overs
get cut, plus their registrations in `init.php` and the 10
integration test bootstrap blocks that mirrored the registration
shape.

Refs #5 phase 2.2.
…x bodies

The procedural helpers in `web/includes/system-functions.php` are
mostly the same conceptual operations the 1.4.x panel exposed
(`CreateLinkR`, `BitToString`, `SmFlagsToSb`, `NextSid` / `NextAid`,
`SecondsToString`, `FetchIp`, `GetMapImage`, `PruneBans` /
`PruneComms`, `getDirSize` / `sizeFormat`, `rcon` / `parseRconStatus`,
`GetCommunityName`, `checkMultiplePlayers`). Function names are
preserved for theme-fork compatibility — the bodies are restructured
so they no longer trace back to the legacy text:

* `CreateLinkR`: collapse the two arms into a single `$attrs` map
  rendered via `sprintf` so the tooltip / no-tooltip branches share
  the attribute-emission code; also clarifies that the leading +
  trailing space inside the anchor is intentional (legacy template
  consumer concatenates link strings inline).
* `BitToString`: `ADMIN_OWNER` short-circuit lifted out of the
  per-flag `if`, `ALL_WEB` skip is the first arm; `int $value` cast
  at the bind site so the column-typed primitive is what the
  comparison sees.
* `SmFlagsToSb`: `'z'` (root) detection lifted to a once-computed
  `$isRoot` so the loop body no longer rescans the haystack on each
  iteration.
* `SecondsToString`: replace the parallel `$div` / `$desc` arrays
  with a single tuple list iterated with `[$label, $size]`
  destructuring; switch from `floor() + %` to `intdiv()` so the
  arithmetic is integer-typed end-to-end. Adds the explicit '0 sec'
  fallback for `$sec = 0` so the output never lands at the
  pre-existing `substr(..., 0, -2)` empty-string bug.
* `PruneBans`: minor restructure (use `array_push(...$args)` instead
  of `array_merge`; bail early when both candidate arrays are empty
  before composing the dynamic SQL).
* `getDirSizeBytes`: handle the `glob() === false` failure mode and
  the `is_dir() === false` non-file case explicitly so a broken
  symlink doesn't push a `0` into the running total.
* `rcon`: `(int) $server['port']` cast at the call boundary (the DB
  column is `int` but the column type system was previously trusting
  the row map's whatever-PDO-decided shape); `??` for the empty rcon
  password test (handles the `null` row case the legacy
  `empty($server['rcon'])` masked).
* `FetchIp`: tightened return type to `string`. The legacy `mixed`
  return mostly worked because every consumer cast to string anyway,
  but the ISO code is always a 2-letter string + the `"zz"` sentinel.
* `parseRconStatus`: handles `preg_match_all === false` failure
  mode explicitly.
* `GetCommunityName`: replace the manual query-string concatenation
  with `http_build_query` so URL-special characters in the API key
  / SteamID get encoded.

The restructure is intentional and localized — function names + call
sites + behaviour are all preserved (regression tests in `web/tests/`
exercise these helpers via the page handlers that consume them).

Refs #5 phase 2.3.
The 9-subquery composite COUNT (over `:prefix_banlog`,
`:prefix_bans`, `:prefix_comms`, `:prefix_admins`,
`:prefix_submissions`, `:prefix_protests`, `:prefix_servers`) plus
the recursive `getDirSize(SB_DEMOS)` walk in `web/pages/page.admin.php`
were inherited from the 1.4.x stat-counts row that the v2.0.0 8-card
grid never displayed (#1146). Issue #1270 gated the compute behind
`Sbpp\Theme::wantsLegacyAdminCounts()` for theme forks that still
rendered them — but the gate itself was the only consumer of
`Sbpp\Theme`, and the legacy DTO surface (`access_*`, `demosize`,
`total_*`, `archived_*` on `AdminHomeView`) only existed to feed
the unreachable `{if false}` parity block in `page_admin.tpl`.

`goals#5` finishes the cleanup by deleting all of the moving parts:

* `web/pages/page.admin.php` — drop the gate + the COUNT + the
  placeholder zeros + the legacy `access_*` constructor args. The
  handler is now a thin "compute composite per-area can_<area>
  booleans → render the 8-card grid".
* `web/includes/View/AdminHomeView.php` — drop the legacy
  `access_*` / `demosize` / `total_*` / `archived_*` properties.
  The DTO is back to the canonical 8 booleans the grid actually
  consumes.
* `web/themes/default/page_admin.tpl` — drop the `{if false}`
  parity block that referenced the legacy props.
* `web/includes/Theme.php` — delete entirely. The class only ever
  carried `wantsLegacyAdminCounts()` + the per-process counter
  triple `recordLegacyComputePass` / `legacyComputeCount` /
  `resetLegacyComputeCount` for the matching regression test;
  with the gate gone, every method is dead.
* `web/tests/integration/AdminHomePerformanceTest.php` — delete.
  Its sole job was asserting the gate fired correctly under
  default-theme vs fork-opt-in, and the gate no longer exists.
* `web/tests/scripts/profile-admin-home.php` — delete. Its sole
  job was profiling the (now removed) legacy compute branch
  vs the gated default-theme path.
* `ARCHITECTURE.md` + `AGENTS.md` — drop the `Sbpp\Theme` /
  `wantsLegacyAdminCounts` rows from the namespace table and the
  "Where to find what" cheat-sheet.

Theme forks still rendering the legacy stat-counts row need to
either migrate to the 8-card grid (the canonical v2.0 admin
landing) or compute the COUNT + getDirSize themselves in their
fork — the panel's job is to ship the supported chrome, not to
carry compute behind every legacy DTO field.

Refs #5 phase 2.4.
…ntions

The 8 admin.edit.* page handlers (admindetails / admingroup / adminperms /
adminservers / ban / group / mod / server) carried inline `echo '<form>...'`
HTML, MooTools `$('id.msg').setStyle('display','block')` validation
messaging, and `<script>ShowBox(...)</script>` toast emitters that all
relied on JS handlers (`ButtonOver`, `ProcessEditAdminPermissions`,
`ProcessEditGroup`) deleted with sourcebans.js at #1123 D1 — meaning
every legacy emitter was a silent no-op.

This commit:
- Introduces `web/pages/_admin_edit_helpers.php` carrying
  `sbpp_admin_edit_die_with_toast()`,
  `sbpp_admin_edit_emit_tail_script()`, and
  `sbpp_admin_edit_collect_rehash_sids()` — the shared page-tail
  vanilla-JS contract that paints validation errors into existing
  `<id>.msg` divs, fires `window.SBPP.showToast(...)`, and (where
  appropriate) calls `Actions.SystemRehashAdmins`.
- Rewrites each handler as the canonical thin shape: validate input,
  build a typed View DTO, render via `Renderer::render()`, emit the
  tail script. CSRF gates each POST via `CSRF::rejectIfInvalid()`.
- Adds two new typed View DTOs (`EditAdminPermsView`, `EditGroupView`)
  + matching Smarty templates (`page_admin_edit_admins_perms.tpl`,
  `page_admin_edit_group.tpl`) that server-render checkbox state
  instead of relying on the legacy `$('p2').checked = true`
  re-paint script.
- Modernizes `AdminEditModView` to carry the `enabled` flag (so the
  tail-`<script>$('enabled').checked = N` shim is gone) and
  `AdminServersAddView` to carry `enabled` + `assigned_groups` for
  the same reason.
- Wraps the DELETE-then-INSERT sweep on `:prefix_admins_servers_groups`
  and `:prefix_servers_groups` in transactions so half-applied state
  is no longer reachable. Schema doesn't carry a UNIQUE that would
  let us collapse to `INSERT ... ON DUPLICATE KEY UPDATE` (would
  need a paired migration; out of #5's scope).
- Permission edit submits go through the existing
  `Actions.AdminsEditPerms` JSON action; group edits go through
  `Actions.GroupsEdit`. No new API surface.

`admin.edit.ban.php` is left for the Phase 3 header sweep — its
body was already in v2.0 shape.
The three popup upload handlers (admin.uploaddemo.php,
admin.uploadicon.php, admin.uploadmapimg.php) duplicated the same
six-step flow: CSRF check, extension allowlist, move_uploaded_file,
Log::add, emit a `<script>window.opener.<callback>(...)</script>`
blob, render the popup template.

The new `Sbpp\Upload\UploadHandler::handle()` static centralises every
step. Each page handler is now a thin wrapper that names its
permission gate, allowed extensions, destination directory,
`window.opener.<callback>` name, and audit-log strings.

Also adds `UploadHandler::sanitiseName()` defensive sanitisation —
the legacy code passed `\$_FILES[…][name]` verbatim into
`move_uploaded_file()`'s second argument, which on the icon and
mapimage paths trusted admin-uploaded names against directory
traversal. The new helper basenames + strips backslashes + trims
leading dots so a `name=../../etc/passwd` upload can no longer
escape the destination directory.

Demo uploads keep the `md5(time() . rand())` rename behaviour;
icon / mapimage uploads keep filename-as-disk-name (theme forks
look icons up by name) but with the sanitiser in front.
The default theme's chrome was originally authored by InterWave
Studios Development Team in SourceBans 1.4.x. The 1.4.11 version of
this file carried that attribution; SourceBans++ inherited the file
in 2014 and the attribution was lost when the legacy 17-line header
replaced it.

Restoring the InterWave Studios attribution is owed under
CC BY-NC-SA 3.0 §4(c). The detailed attribution lives in the new
THIRD-PARTY-NOTICES.txt file (Phase 4); this header just points
there.

Also restructures the five `define()`s for clarity (per-define
comment explaining what the constant is for + why theme forks care).
Sweeps the 36 files (35 PHP + the InterWave-attribution chunk in the
five `.tpl` shipped templates was already migrated as part of the
Phase 2 admin.edit.* rewrite).

The legacy 17-line header carried four problems:
- Hard-coded the SourceBans 1.4.11 attribution as a comment block
  on every file, which made the "no substantive 1.4.11 carryover"
  audit (see #3) misleading by inflating per-file overlap percentages.
- Carried two year ranges in parallel (`2014-2024` and `2014-2026`),
  with no automated drift gate.
- Pointed at `creativecommons.org/licenses/by-nc-sa/3.0/` for the
  license text — but `LICENSE.md` is the actual licence (now
  reconciled to CC BY-NC-SA 3.0 Unported in this PR's earlier
  commit).
- The three comms-related pages (admin.comms.php, page.commslist.php,
  admin.comms.search.php) carried an additional SourceComms 0.9.266
  attribution under the SourceBans block.

Each file's legacy header is replaced with the canonical 4-line shape
documented in AGENTS.md — a single pointer to LICENSE.md (license)
and THIRD-PARTY-NOTICES.txt (attributions). The SourceComms 0.9.266
attribution survives intact in THIRD-PARTY-NOTICES.txt (added in
this PR's Phase 4 commit).

Files where the header sat after `<?php` + `namespace` + `use` (e.g.
`web/includes/Auth/UserManager.php`) keep the 4-line replacement
in the same position so the namespace shape stays valid.

Out of scope:
- `web/includes/Auth/openid.php` — third-party LightOpenID, MIT
  license, never carried our header.
- `web/includes/tinymce/**` — third-party TinyMCE, own licence.
- `web/includes/vendor/**` — Composer-managed.
Phase 4 of #5. The Phase 3 header sweep collapsed 17-line per-file
attribution blocks into a 4-line v2.0 header that points here. This
file is the authoritative attribution surface.

Six attributions are documented:

1. SourceBans 1.4.x (defensive — schema layout, permission flag
   bitmask values, audit-log letter codes, function-name surface
   in `system-functions.php` are continuous-through-line traces
   from the 2007-2014 SourceBans Team's panel).
2. SourceComms 0.9.266 by Alexandr Duplishchev — origin of the
   :prefix_comms data model + the panel's communications
   moderation surfaces (was inline-attributed in three of the
   page handlers' headers prior to Phase 3).
3. InterWave Studios Development Team — origin of
   `web/themes/default/theme.conf.php` (the metadata file shape
   theme forks copy verbatim). The visual chrome was rewritten
   in v2.0 (#1123); only the `theme.conf.php` shape is
   continuous.
4. LightOpenID by Mewp — vendored verbatim under
   `web/includes/Auth/openid.php`, MIT licensed; full MIT text
   embedded.
5. TinyMCE by Tiny Technologies — vendored under
   `web/includes/tinymce/`, LGPL v2.1 licensed (full text in
   the directory's `license.txt`). Retained for theme-fork
   back-compat; no longer reachable from the panel UI after
   the v2.0 stored-XSS hardening (#1113).
6. Composer dependencies — declared in `web/composer.json`,
   licences resolved via the vendored package directories.

The cross-link from LICENSE.md to this file already lands as part
of the Phase 1 LICENSE.md reconciliation commit.
The Phase 2 admin.edit.* rewrites + the system-functions.php pass
shrank PHPStan's reportable surface; regenerating the baseline drops
60 lines of stale ignores and exposes a handful of dead `?? ''`
fallbacks against `$_POST['x']` keys that an `isset(...)` check
above already proved present.

The four call sites cleaned:
- `admin.edit.admindetails.php` line 65: `(string) ($_POST['adminname'] ?? '')`
  → `(string) $_POST['adminname']` (gated by `isset($_POST['adminname'])`).
- `admin.edit.mod.php` line 63: same shape against `$_POST['name']`.
- `admin.edit.server.php` line 70: same shape against `$_POST['address']`.
- `_admin_edit_helpers.php` line 97:
  `array_values(array_map('intval', $rehashSids))`
  → `array_map('intval', $rehashSids)` (array_map already returns a list).

Net baseline change: -60 lines (313 entries → 312 entries).

PHPStan + PHPUnit + ts-check + api-contract all pass on this commit.
Anti-patterns added (all swept by goals#5):
- The 17-line legacy SourceBans 1.4.11 attribution header at the
  top of every PHP / Smarty file → use the 4-line v2.0 shape.
- Inline `echo '<form>...'` HTML blobs in admin.edit.* page
  handlers → typed `Sbpp\View\AdminEdit<X>View` DTO + Smarty
  template.
- `echo '<div id="msg-red">...'` / `echo '<div id="msg-green">...'`
  inline error / success banners → Smarty `{if $error}` template
  block + the `sbpp_admin_edit_emit_tail_script()` helper.
- 1.4.11 JS handler names (`ButtonOver`, `ProcessEditAdminPermissions`,
  `ProcessEditGroup`, `ProcessEditMod`, `ProcessEditServer`,
  `errorScript`) referenced from inline `onclick=` / `onmouseover=`
  attributes → wire to `sb.api.call(Actions.PascalName, …)` via
  `data-action` + a page-tail vanilla-JS dispatcher.
- MooTools `$('id').value` / `$('id').setStyle(…)` idioms in
  inline page-tail scripts → vanilla DOM (`document.getElementById`
  + native `.value` / `.style.display`).
- Non-transactional DELETE-then-INSERT on
  `:prefix_admins_servers_groups` / `:prefix_servers_groups` →
  `Sbpp\Db\Database::beginTransaction()` /
  `endTransaction()` / `cancelTransaction()` wrapper.
- Hand-rolled `move_uploaded_file()` per upload page →
  `Sbpp\Upload\UploadHandler::handle()`.

Conventions added:
- Namespacing table now lists `Sbpp\Upload\UploadHandler`.
- "Where to find what" entries for adding a new admin.edit.* page
  and adding a new popup file-upload page.

ARCHITECTURE.md directory tree mentions `Upload/` next to `Servers/`.
Phase 2 of goals#5 rewrote `admin.edit.adminperms.php` /
`admin.edit.admingroup.php` to server-render the permission grid via
typed `EditAdminPermsView` / `EditGroupView` DTOs, but left the v1.4.11
partials and the three JSON actions that vended them on disk:

- `web/pages/groups.web.perm.php`
- `web/pages/groups.server.perm.php`
- `web/pages/group.name.php`

The handlers `api_admins_update_perms`, `api_groups_update_perms`, and
`api_groups_add_server_group_name` `file_get_contents()`-d those
partials and returned their bytes — 1.4.x `<table border="0"
cellspacing="0" cellpadding="4">` markup, `tablerow1` / `tablerow2` /
`tablerow4` rules, `align="absbottom"`, `<div id="{name}_err"
class="badentry">`, `{title}` / `{name}` placeholder substitution, plus
the dead `UpdateCheckBox(2,3,39)` onclick handler (`UpdateCheckBox` was
deleted with `web/scripts/sourcebans.js` at #1123 D1, the click resolves
to nothing — the AGENTS.md "Anti-patterns" entry forbids it).

The new chrome doesn't call any of these, but the dispatcher accepts the
actions and `PermissionMatrixTest` pinned the masks, so a fork chrome
(or a malicious client) could still pull the 1.4.11 markup off the live
API. Per goals#5's acceptance criterion ("no substantive 1.4.11 code
remains"), this was the last Phase 2 acceptance miss.

This commit:

- Deletes the three partial files.
- Deletes the three handler functions in `web/api/handlers/admins.php`
  and `web/api/handlers/groups.php`.
- Drops their three rows from `web/api/handlers/_register.php` (and the
  matching `Actions.AdminsUpdatePerms` / `GroupsUpdatePerms` /
  `GroupsAddServerGroupName` exports + typedef stubs in the regenerated
  `web/scripts/api-contract.js`).
- Drops the matching PHPUnit tests from `web/tests/api/AdminsTest.php`
  and `web/tests/api/GroupsTest.php`, deletes the two snapshot files
  under `__snapshots__/{admins,groups}/update_perms_web.json`, and drops
  the three rows from `web/tests/api/PermissionMatrixTest.php`.
- Fixes the now-stale `AdminGroupsAddView` docblock — the form posts
  via `Actions.GroupsAdd` and is intentionally minimal (name + type +
  optional `srvflags`); permission flag editing is the marquee surface
  of the **list** tab's master-detail editor, which `AdminGroupsListView`
  populates via `all_flags`. The "lazy-loads via `groups.update_perms`"
  claim was wrong on both fronts (the template doesn't, and the action
  is gone).
- Swaps the `ARCHITECTURE.md` `Api::register` example off the deleted
  `admins.update_perms` and onto its sibling `admins.generate_password`
  to keep the "any admin" sample live.
…NG, AGENTS

Phase 1 of goals#5 reverted `LICENSE.md` to the verbatim CC BY-NC-SA 3.0
Unported legal code (the licence the project has actually shipped under
since v1.0), but three surfaces still claimed "CC BY-SA 4.0":

- `README.md` shields.io badge URL + alt text.
- `CONTRIBUTING.md` "## Contributor License Agreement (web panel only)"
  blurb describing the dual-licence model.
- `AGENTS.md` "Contributor License Agreement gate" Conventions block.

Updates all three to "CC BY-NC-SA 3.0" so the panel chrome (install
wizard footer, README "## License" section, badge, contributor docs,
agent docs) tell the same story end-to-end. The shields.io badge URL
uses the literal hyphen-encoded path (`CC_BY--NC--SA_3.0`) so the
`-NC-` and `-SA-` separators render as single dashes.

Verified post-change: `rg -i "CC[\s_-]*BY[\s_-]*SA[\s_-]*4"` returns
zero hits across the repo (excluding `.git`, `web/includes/vendor/`,
`web/includes/tinymce/`).
@rumblefrog

Copy link
Copy Markdown
Member Author

I have read the CLA Document and I hereby sign the CLA

1 similar comment
@rumblefrog

Copy link
Copy Markdown
Member Author

I have read the CLA Document and I hereby sign the CLA

@rumblefrog
rumblefrog added this pull request to the merge queue May 15, 2026
Merged via the queue into main with commit 4b9577c May 15, 2026
6 of 8 checks passed
@rumblefrog
rumblefrog deleted the goals-5-rewrite-mixed-files branch May 15, 2026 03:32
@github-actions github-actions Bot locked and limited conversation to collaborators May 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant