diff --git a/AGENTS.md b/AGENTS.md index 06dae69f7..32a95fcab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2186,10 +2186,55 @@ contacting every contributor individually. reason field, and a page-tail script that dispatches to `sb.api.call(Actions.PascalName, …)`. The `Actions.PascalName` shape (NOT a string literal) catches typos at api-contract - regen time. Search anchors for the cleanup sweep: + regen time. **#1402** swept the rest of the post-#1397 cluster + (`ProcessMod`, `ProcessAddAdmin`, `LoadGeneratePassword`, + `update_server` / `update_web`, `LoadGroupBan` / + `ProcessGroupBan` / `CheckGroupBan` / `TickSelectAll`, + `RemoveComment`, `window.opener.icon(...)`, + `window.addEvent('domready', …)`). Constructive form submits + (`ProcessMod` / `ProcessAddAdmin`) intercept the form's `submit` + event, validate, then `sb.api.call(Actions.ModsAdd / .AdminsAdd)` + per the `page_admin_groups_add.tpl` reference. Multi-step + chains (`LoadGroupBan` → `Actions.BansGroupBan` → + `Actions.BansBanMemberOfGroup`) live in a page-tail dispatcher + next to the surface in `web/pages/admin.bans.php`. The trash- + can-on-a-comment trigger (banlist / commslist / admin.bans + protests + submissions) is the rare case where the same + affordance ships on three sibling pages — that one rides a + **shared `web/scripts/comment-actions.js` dispatcher** loaded + from `core/footer.tpl` so the four call sites (page.banlist.php, + page.commslist.php, admin.bans.php protests, admin.bans.php + submissions) share one source of truth instead of four inlined + page-tail blocks. The icon-upload callback (`window.opener.icon(...)` + emitted by `UploadHandler::handle()`) is wired via a per-page + `window.icon = function (filename) { … }` block in the parent + template (`page_admin_mods_add.tpl` + `page_admin_edit_mod.tpl`); + this is the same shape the demo upload uses (`window.demo` on + `admin.bans.php` / `admin.edit.ban.php`). Search anchors for the + cleanup sweep: `rg "typeof \w+ === ['\"]function['\"]" web/themes/` for the guarded shape, and `rg "onclick=\"[A-Z]\w+\(" web/themes/` for the unguarded sister shape. +- `window.addEvent('domready', …)` MooTools DOMready idiom in + inline page-tail blocks (or any `Element.prototype` / + `$$('css selector')` / `el.addEvent(...)` MooTools method call) + → use vanilla `document.addEventListener('DOMContentLoaded', + function () { … })` or — better, since panel templates render + the page-tail script AFTER the elements it targets — drop the + wrapper entirely and run synchronously. MooTools was removed + with `sourcebans.js` at #1123 D1; every `window.addEvent` + callsite that survived was a silent no-op (`window.addEvent` + is undefined, the event listener never registered, the body + never ran). Pre-#1402 `web/pages/admin.edit.comms.php` shipped + a `$errorScript` blob wrapping its DOM operations in + `window.addEvent('domready', ...)`; the validation-error toast + never painted because the wrapper itself threw. Vanilla DOM + access (`document.getElementById('id').value` / + `el.style.display = 'block'`) replaces the `$('id').value` / + `$('id').setStyle(…)` MooTools idioms in the same sweep — both + go in one PR per file because the body of the + `window.addEvent('domready', …)` callback almost always uses + MooTools `$()` too. - `web/scripts/contextMenoo.js` / `sb.contextMenu` / global `AddContextMenu` → removed at #1306. The vanilla shims were back-compat scaffolding for the MooTools-era right-click menu the @@ -2787,10 +2832,12 @@ contacting every contributor individually. | Add or rename a permission | `web/configs/permissions/web.json`, then regen contract | | Render a page | `web/pages/.php` + `web/includes/View/*View.php` | | Add a new edit page in the admin.edit.* cluster (e.g. `admin.edit..php`) | `web/pages/admin.edit..php` (the page handler — thin "validate input, build View, render" shape) + `web/includes/View/AdminEditView.php` (typed View DTO) + `web/themes/default/page_admin_edit_.tpl` (template). Shared helpers live in `web/pages/_admin_edit_helpers.php` (`sbpp_admin_edit_die_with_toast()` for permission / not-found guards, `sbpp_admin_edit_emit_tail_script()` for form-success / validation-error feedback that fires `window.SBPP.showToast()` and writes errors into `.msg` divs, `sbpp_admin_edit_collect_rehash_sids()` for the post-save Rehash Admins step). Anti-patterns to avoid: inline `echo '
...'` blocks, `echo '
…'` banners, MooTools `$('id').value` reads, legacy JS handler names (`ButtonOver`, `ProcessEditAdminPermissions`, etc.) — all swept as part of `goals#5`. CSRF gate every POST via `\CSRF::rejectIfInvalid();` after the `isset($_POST[''])` arm. | +| Wire a `window.opener.(...)` slot on the parent template of a popup file-upload page (e.g. `window.icon`, `window.demo`, `window.mapimg`) | The parent template defines `window. = function (filename) { … }` inside an inline `` and the call throws `TypeError: window.opener.icon is not a function` — popup never closes, the parent form's hidden input never updates, the uploaded asset is orphaned on disk. The `UploadHandler` emits the call unconditionally; the parent's job is to be ready for it. | | Add a popup file-upload page (demo / icon / mapimage / new asset type) | `Sbpp\Upload\UploadHandler::handle()` (`web/includes/Upload/UploadHandler.php`). The page handler at `web/pages/admin.upload.php` is a thin wrapper passing the per-page knobs (`permission` mask, `field` `$_FILES` key, `allowed` extensions, `destDir`, `callback` JS function name on `window.opener`, `auditOk` / `auditFmt` / `errorMsg` / `title` / `formName` / `formats` strings, optional `renameToHash` for demo-style randomised filenames). The handler runs CSRF + permission check, sanitises `$_FILES[…]['name']` via `sanitiseName()` (basename + strip backslashes + trim leading dots — defends LFI on the icon / mapimage paths where the filename hits disk), `move_uploaded_file()`s to the destination, calls `Log::add(LogType::Message, …)`, and on success emits the `` blob the parent page picks up. The three reference call sites (`admin.uploaddemo.php`, `admin.uploadicon.php`, `admin.uploadmapimg.php`) are 30-line wrappers; new asset types should match that line budget. Anti-pattern: hand-rolling the move / log / popup-emission sequence per page (the pre-`goals#5` shape). | | Edit a template | `web/themes/default/*.tpl` | | Reuse the moderation-queue card layout (admin submissions / protests, mobile-stacked summary rows) | `web/themes/default/css/theme.css` (`.queue-row`, `.queue-row__body`, `.queue-row__date` — #1207 PUB-2). Apply by adding `class="queue-row …"` to the outer `
` and dropping the inline `flex` / `flex-shrink:0` styles from the summary children. | | Add visible row actions to a table-rendered admin list (Edit / Unmute / Remove buttons + responsive mobile-card mirror) | `web/themes/default/page_comms.tpl` (#1207 ADM-5) is the canonical reference: `
'; @@ -964,7 +1148,16 @@ function bansBuildComments(array $commentres, $userbank, int $rowId, string $typ if ($crow['aid'] == $userbank->GetAid() || $userbank->HasAccess(WebPermission::Owner)) { $cdata['editcomlink'] = CreateLinkR('', 'index.php?p=banlist&comment=' . $rowId . '&ctype=' . $type . '&cid=' . $crow['cid'], 'Edit Comment'); if ($userbank->HasAccess(WebPermission::Owner)) { - $cdata['delcomlink'] = ""; + // #1402: see web/scripts/comment-actions.js for the dispatcher. + // $type is the literal letter 'P' (protests) or 'S' (submissions); + // the api handler's `ctype` arm consumes both. No paginator on the + // moderation queues, so data-page is the sentinel -1. + $cdata['delcomlink'] = ''; } } else { $cdata['editcomlink'] = ""; diff --git a/web/pages/admin.edit.comms.php b/web/pages/admin.edit.comms.php index c4dbc8499..a5c4c4b29 100644 --- a/web/pages/admin.edit.comms.php +++ b/web/pages/admin.edit.comms.php @@ -36,7 +36,19 @@ isset($_GET["page"]) ? $pagelink = "&page=" . urlencode($_GET["page"]) : $pagelink = ""; -$errorScript = ""; +// #1402: per-field inline-error setters now emit vanilla DOM calls +// instead of the MooTools `$('id').setStyle('display', 'block')` shape +// that died with sourcebans.js at #1123 D1. Each error tuple becomes +// `document.getElementById(id).textContent = msg; …style.display='block'` +// in the page-tail ` +{* + #1402: comment-actions.js — single document-level click delegate + for `data-action="comment-delete"` triggers (admin moderation + queues, banlist / commslist comment editor on themes that render + delcomlink). Loaded globally because the dispatcher is feature- + detected (no-op when no triggers exist) and the four surfaces it + serves render from different page handlers; per-page includes + would mean tracking four mount points instead of one. Pre-#1402 + every trash-can click on a comment threw + `ReferenceError: RemoveComment is not defined` (the helper lived + in the deleted sourcebans.js at #1123 D1). +*} +{* `defer` would be a no-op here since the script lives at the body + tail and the parser is already past the body. Drop it so the + markup matches the runtime behaviour (#1402 adversarial review + LOW 8). *} + diff --git a/web/themes/default/page_admin_admins_add.tpl b/web/themes/default/page_admin_admins_add.tpl index 607008e38..d35fc08cc 100644 --- a/web/themes/default/page_admin_admins_add.tpl +++ b/web/themes/default/page_admin_admins_add.tpl @@ -1,14 +1,31 @@ {* - SourceBans++ 2026 — admin/admins add + SourceBans++ (c) 2014-2026 SourceBans++ Dev Team + Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0. + See LICENSE.md for the full license text and THIRD-PARTY-NOTICES.txt for attributions. Pair: web/pages/admin.admins.php (renders this OR the list OR the overrides editor based on ?section=) and web/includes/View/AdminAdminsAddView.php. - Form submission stays on the legacy ProcessAddAdmin() helper to keep - the JSON-API contract identical to the default theme. The CSRF - protection comes from {csrf_field}; xajax/sb-callback are NOT - reintroduced. + #1402: Wires the four dead-on-v2.0 JS handlers + (`ProcessAddAdmin`, `LoadGeneratePassword`, `update_server`, + `update_web`) directly to the existing JSON API actions + (`Actions.AdminsAdd`, `Actions.AdminsGeneratePassword`) via a + page-tail vanilla-JS dispatcher. The pre-fix shape relied on + helpers from `web/scripts/sourcebans.js` (deleted at #1123 D1): + the form's `onsubmit="event.preventDefault(); if (typeof + ProcessAddAdmin === 'function') ProcessAddAdmin();"` always took + the `event.preventDefault()` path (silent no-op — the guard + swallowed the missing helper and the form never POSTed), the + "Generate password" button's onclick was the same shape, and the + ` + {* #1402: data-action="admin-add-generate-password" replaces the + dead `onclick="if (typeof LoadGeneratePassword === 'function') + LoadGeneratePassword(); return false;"` guard. The page-tail + dispatcher below calls Actions.AdminsGeneratePassword and + writes the result into #password / #password2. *} @@ -175,10 +197,13 @@
+ {* #1402: replaces the dead `onchange="if (typeof update_server === 'function') + update_server();"` guard. The page-tail dispatcher reacts to `change` + on this element via `data-action="admin-add-update-server"`. *}
-
+ {* #1402: pre-#1402 this was a hollow `
` that the + legacy `update_server()` helper was supposed to mount the new-group + name field + SourceMod flag input into. The helper was deleted with + sourcebans.js (#1123 D1); the new dispatcher reveals these inline + inputs on the right ` +
+
+ +
-
+
+ + +
@@ -228,5 +339,448 @@ bare `{$server.ip}:{$server.port}` per row above. Hydration to the live hostname is the next step — see the follow-up ticket tracked off #1404. *} + + {* ============================================================ + #1402 — Add-admin constructive form wiring. + + Replaces four dead JS helpers from sourcebans.js (#1123 D1): + - `ProcessAddAdmin()` → submit handler that collects fields, + builds the web-flag bitmask + server-flag string, fires + sb.api.call(Actions.AdminsAdd, …) and dispatches errors + into the per-field `.msg` slots. + - `LoadGeneratePassword()` → click handler that calls + Actions.AdminsGeneratePassword and writes the result + into #password / #password2. + - `update_server()` / `update_web()` → change handlers that + reveal the conditional inputs on "Custom permissions" / + "New admin group". + + All four were silent no-ops on v2.0 because the helpers + lived in the deleted sourcebans.js: the form's + `event.preventDefault()` swallowed every submit; the + "Generate password" button did nothing; picking "Custom + permissions" left the flag picker hidden, so an operator + who tried to ride the form ended up POSTing nothing useful + anyway. + + Constructive-form pattern mirrors `SbppGroupsAdd` in + page_admin_groups_add.tpl (canonical reference from + AGENTS.md "Add a confirm + reason modal …"). + ============================================================ *} + {literal} + + {/literal} {/if} diff --git a/web/themes/default/page_admin_bans_groups.tpl b/web/themes/default/page_admin_bans_groups.tpl index 8b103d4d4..ff2a09b54 100644 --- a/web/themes/default/page_admin_bans_groups.tpl +++ b/web/themes/default/page_admin_bans_groups.tpl @@ -4,22 +4,32 @@ "Group ban" tab on the admin bans page. Two modes share this surface: - Default: a small form to ban a Steam community group by URL. - Submission goes through the legacy ProcessGroupBan() helper in - admin.bans.php's tail script (which dispatches to - Actions.GroupbanCheck via sb.api.call). - - "From player" mode (?fid=STEAMID): the legacy LoadGetGroups() - helper enumerates the player's group memberships into the - #steamGroupsTable list; ticking groups + clicking "Add Group Ban" - runs CheckGroupBan() to issue Actions.GroupbanCheck for each - selected group. + Submission goes through the page-tail JS in admin.bans.php + (event-delegated `data-action="groupban-*"` dispatcher → chains + `Actions.BansGroupBan` + `Actions.BansBanMemberOfGroup`). + - "From player" mode (?fid=STEAMID): the inline LoadGetGroups + helper below enumerates the player's group memberships into + the #steamGroupsTable list; ticking groups + clicking + "Add Group Ban" runs the bulk dispatch (one BansGroupBan + + BansBanMemberOfGroup pair per selected group). + + #1402 — Migrated `onclick="ProcessGroupBan();"` / + `onclick="CheckGroupBan();"` / `onclick="TickSelectAll();"` + bindings to `data-action="…"` attributes per AGENTS.md + "Add a confirm + reason modal …" (the canonical-shape rule for + rewiring dead sourcebans.js helpers). The three globals lived + in web/scripts/sourcebans.js (deleted at #1123 D1); every click + was a `ReferenceError: ProcessGroupBan is not defined` (loud + sister-shape of the #1397 / #1352 trash-can bug). The new + dispatcher in admin.bans.php's tail script binds against the + data-attributes and uses sb.api.call → window.SBPP.setBusy / + showToast for the loading state + final feedback. - Both helpers (LoadGetGroups, TickSelectAll, CheckGroupBan) live in - web/scripts/sourcebans.js and aren't loaded by the sbpp2026 chrome; - that flow remains a default-theme feature for the rollout window. DOM ids (groupurl, groupreason, *.msg, agban, aback, gban, tickswitch, tickswitchlink, steamGroups, steamGroupsText, - steamGroupsTable, steamGroupStatus) are preserved so legacy callers - continue to find them on default. + steamGroupsTable, steamGroupStatus) are preserved so existing + LoadGetGroups inline script + any third-party theme that wired + extra behaviour on top still finds them. `$player_name` is rendered above the group list when reaching this tab from a banlist row (?fid=STEAMID&player=…). admin.bans.php @@ -59,7 +69,7 @@ + onsubmit="event.preventDefault(); return false;"> {csrf_field}
@@ -88,11 +98,16 @@ id="aback" data-testid="groupban-back" onclick="history.go(-1);">Back + {* #1402: was `onclick="ProcessGroupBan();"` which threw + ReferenceError post-#1123 D1 (the helper lived in the + deleted sourcebans.js). The page-tail dispatcher in + admin.bans.php picks up data-action="groupban-submit" + and chains Actions.BansGroupBan → BansBanMemberOfGroup. *}
@@ -122,20 +137,22 @@ + {* #1402: was `onclick="TickSelectAll();"` (dead since #1123 D1). *} Group + {* #1402: was `onclick="TickSelectAll();return false;"`. *} Select all @@ -151,12 +168,16 @@
+ {* #1402: was `onclick="CheckGroupBan();"` which threw + ReferenceError. The page-tail dispatcher in + admin.bans.php picks up data-action="groupban-bulk-submit" + and iterates ticked rows through the same chain. *}
diff --git a/web/themes/default/page_admin_edit_mod.tpl b/web/themes/default/page_admin_edit_mod.tpl index 78d8057eb..639c1b67a 100644 --- a/web/themes/default/page_admin_edit_mod.tpl +++ b/web/themes/default/page_admin_edit_mod.tpl @@ -16,6 +16,14 @@ emit a CSP-friendly inline script that calls `window.opener.icon()` via the modernised UploadHandler chrome. + #1402: the page-tail script below wires `window.opener.icon` so + the popup's success callback (UploadHandler.php line 187, calling + `window.opener.icon()`) actually has somewhere + to land — pre-fix the parent window had no `icon` function + defined, so the upload popup threw `TypeError: window.opener.icon + is not a function`, stayed open, and the chosen icon never + reached the form's hidden `#icon_hid` input. + Initial checkbox state is server-rendered via the new `$enabled` template variable — no MooTools-era `$('enabled').checked = …` re-paint script. @@ -139,4 +147,36 @@ + +{* #1402: wire `window.opener.icon` so admin.uploadicon.php's success + blob (emitted by Sbpp\Upload\UploadHandler::handle with + `callback: 'icon'`) can write the filename back into this form. The + pre-fix shape relied on a `window.icon` definition that lived in the + pre-v2.0.0 sourcebans.js bulk file (#1123 D1 deleted it), so the + popup's `window.opener.icon()` call threw `TypeError: + window.opener.icon is not a function`, the popup stayed open, and + the chosen icon never propagated. The handler also patches the + visible "Current: …" preview chip so the operator sees what's been + picked. *} +{literal} + +{/literal} diff --git a/web/themes/default/page_admin_mods_add.tpl b/web/themes/default/page_admin_mods_add.tpl index 37c671af2..0592bc922 100644 --- a/web/themes/default/page_admin_mods_add.tpl +++ b/web/themes/default/page_admin_mods_add.tpl @@ -1,23 +1,37 @@ {* - SourceBans++ 2026 — page / page_admin_mods_add.tpl + SourceBans++ (c) 2014-2026 SourceBans++ Dev Team + Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0. + See LICENSE.md for the full license text and THIRD-PARTY-NOTICES.txt for attributions. Second tab of the admin "Mods" page (add a new mod). Pair: Sbpp\View\AdminModsAddView + web/pages/admin.mods.php. - Submission flow (preserved end-to-end from the legacy theme): - 1. +
+ + +

16x16 GIF, PNG or JPG. Opens a popup uploader.

@@ -143,5 +166,176 @@ + + {* ============================================================ + #1402 — Add-mod constructive form wiring (inline page-tail JS). + + Replaces the v1.x `ProcessMod()` helper (deleted with + sourcebans.js at #1123 D1) — pre-fix the form's + `onsubmit="ProcessMod(); return false;"` swallowed the native + submit but never dispatched to anything, so the Add-mod button + was a silent no-op (no console error, no toast, no API call, + no row created). Also wires `window.opener.icon()` (called by + admin.uploadicon.php's success blob via + Sbpp\Upload\UploadHandler::handle's `callback: 'icon'`) into + the hidden `#icon_hid` input so the chosen icon filename + actually rides the form submission. + + Constructive-form pattern mirrors `SbppGroupsAdd` in + page_admin_groups_add.tpl: intercept submit, client-side + validate, busy-flip the submit button via SBPP.setBusy, fire + sb.api.call(Actions.ModsAdd, …), branch on the envelope. + + No `// @ts-check` here because the file is rendered by Smarty; + ts-check only runs against `.js` sources in `web/scripts`. + ============================================================ *} + {literal} + + {/literal} {/if} diff --git a/web/themes/default/page_bans.tpl b/web/themes/default/page_bans.tpl index 2f4eff3ff..9e7aaf594 100644 --- a/web/themes/default/page_bans.tpl +++ b/web/themes/default/page_bans.tpl @@ -891,6 +891,11 @@ the listing branch silently broke comment save (no submit handler attached, native form submission to action-less URL no-ops). *} +{* #1402: trash-can-on-a-comment triggers (`data-action="comment-delete"`) + on the comment-edit branch are handled by the global comment-actions.js + dispatcher loaded from core/footer.tpl — single mount point shared + with the admin moderation queues (protests / submissions) and the + commslist comment-edit branch. *} {* ============================================================ #1301 — banlist row-action wiring (inline page-tail JS).