Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ matching its directory. PSR-4 autoloads from `web/includes/` →
| `Sbpp\Api\Api` | JSON API dispatcher |
| `Sbpp\Api\ApiError` | structured API error |
| `Sbpp\View\AdminTabs` | admin sub-route sidebar mounter |
| `Sbpp\View\BrandLogo` | `template.logo` resolver with `is_file()` fallback to `images/favicon.svg` — single source for the navbar + login chrome brand-mark renders; rejects path-traversal + null-bytes + the v1.x default (case-insensitive); fail-closed on missing `SB_THEMES` |
| `Sbpp\View\Toast` | server-side toast emitter (`emit(kind, title, body, ?redirect)` — replaces v1.x `ShowBox(...)`) |
| `Sbpp\View\*` | view DTOs (page-level + partials) |
| `Sbpp\Servers\SourceQueryCache` | per-`(ip, port)` on-disk cache around the xPaw A2S probe (#1311) |
Expand Down Expand Up @@ -4818,6 +4819,7 @@ contributions without contacting every contributor individually.
| Generate a presigned S3 PUT URL for the panel's "Full data export" S3 mode | Operator-side workflow — the panel never generates the URL itself, only consumes it. AWS: `aws s3 presign s3://bucket/key --http-method PUT --expires-in 3600` (the `--http-method PUT` flag is load-bearing — defaults to GET). Cloudflare R2: same `aws` CLI configured against an R2 token + `--endpoint-url https://<account-id>.r2.cloudflarestorage.com`; or `wrangler r2 object put` with `--presign`; or the R2 dashboard's "Generate URL" tool. MinIO: `mc share upload --expire 1h myminio/bucket/key` (note `share upload`, NOT `share download` — distinct presign flavours). The presigned URL is a single-use write credential; use a short expiry (≤1 hour) and never paste the URL into chat / public logs / issue trackers (anyone who sees it can PUT arbitrary content to your bucket until it expires). Operator-facing docs at `docs/src/content/docs/configuring/data-export.mdx` carry the worked examples per provider. |
| Add a cross-repo JSON contract (vendored schema lock + reader + extractor parity test) | `web/includes/Telemetry/Schema1.php` is the reference shape (`payloadFieldNames(): list<string>` over a Draft-7 JSON Schema lock file). Pair with one PHPUnit extractor parity test (collect() vs. lock file in both directions). The schema lock file is the single source of truth — don't mirror the field list into a markdown doc paired with a separate parity test, that pattern was tried for telemetry and removed because the duplication paid for the drift risk it created. Sync via a manual `make sync-<subsystem>-schema` target — no scheduled auto-PR. See "Cross-repo JSON contracts" under Conventions. |
| Display a user's own permission flags grouped by category | `Sbpp\View\PermissionCatalog::groupedDisplayFromMask($mask)` (`web/includes/View/PermissionCatalog.php`). Adding a new flag to `web/configs/permissions/web.json` requires a paired entry in `WEB_CATEGORIES`; `PermissionCatalogTest` enforces it. |
| Resolve the operator-configurable `template.logo` setting to a brand-mark path for the chrome (`core/navbar.tpl` sidebar + `page_login.tpl` sign-in card) | `Sbpp\View\BrandLogo::resolve()` (theme-relative path) and `Sbpp\View\BrandLogo::resolveUrl()` (public URL) in `web/includes/View/BrandLogo.php`. Single source for the fallback ladder: trim → empty? → defaults to `BrandLogo::DEFAULT_PATH` (`images/favicon.svg`, the SourceBans++ shield from the favicon set); leading-`/` strip; reject `..` / `\` / `\0` (path-traversal + null-byte injection); reject the v1.x default `logos/sb-large.png` case-insensitively (defense-in-depth against installs that missed migration 809 — case-insensitive because Windows + macOS filesystems would otherwise let `Logos/SB-Large.PNG` slip through and still resolve to the missing file on Linux-hosted panels); `is_file()` against `SB_THEMES.<config.theme>/<value>` (theme tree on disk) — fall back on miss; otherwise pass through. **Fail-closed on missing `SB_THEMES`**: `themesRoot()` raises `LogicException` if the constant isn't defined (init.php sets it; anything calling `BrandLogo` without `init.php` is a programmer error and must surface loudly rather than silently returning the fallback). The admin Settings page (`page_admin_settings_settings.tpl`) deliberately renders the RAW configured value so operators can see + fix a broken pointer; only the render paths (`core/header.php` + `page.login.php`) consume the resolver. **Silent-fallback indicator** (#1480 review finding 5): the admin Settings page also surfaces a red warning chip (`[data-testid="setting-warning-template.logo"]`) next to the input when the configured value is non-empty AND the resolver is falling back — without this an operator who customised the path has no signal that their customisation is inactive. The indicator is computed in `web/pages/admin.settings.php` as `trim($rawLogo) !== '' && trim($rawLogo) !== BrandLogo::DEFAULT_PATH && BrandLogo::resolve() === BrandLogo::DEFAULT_PATH` and passed to the template via `AdminSettingsView::$config_logo_using_fallback`. Pre-fix the chrome read `Config::get('template.logo')` directly and shipped a broken `<img>` on three reachable input shapes (the v1.x default that never shipped in the v2.0 default theme, empty / null / missing row, custom paths to deleted files). **Paired surfaces (every site that hardcodes a SourceBans++ brand-mark image path)**: (1) `web/install/includes/sql/data.sql` (fresh-install seed for `template.logo`); (2) `web/updater/data/809.php` (upgrade-path forward-conversion from the v1.x default; pinned to `BrandLogo::V1_DEFAULT_PATH` / `BrandLogo::DEFAULT_PATH` literals at the call site); (3) `web/themes/default/core/header.tpl` (favicon `<link rel="icon">` tags — `{$theme_url}/images/favicon.svg` for the panel chrome); (4) `web/themes/default/install/_chrome.tpl` (favicon `<link rel="icon">` + the install wizard's brand `<img>` in the header — `../themes/default/images/favicon.svg`, hardcoded because the wizard runs against an unconfigured panel with no `Config::get`); (5) `web/themes/default/updater.tpl` (the updater wizard's brand `<img>`, hardcoded for the same reason as the install wizard); (6) `web/themes/default/page_admin_settings_settings.tpl` (the inline `<code>` reference in the help-text under the input — informational only); (7) `web/includes/View/LoginView.php` (docblock reference describing the resolver's fallback). If you move the shield to a new path, update (1) + (2) + (3) + (4) + (5) + (6) + (7) AND `BrandLogo::DEFAULT_PATH` in the same PR — and bump (2)'s migration number if the change ships in a release (per "Updater migrations" rules). The install wizard + updater hardcodes (4) + (5) are intentional and stay hardcoded — those surfaces have no `Config::get` available. Regression guards: `web/tests/integration/BrandLogoTest.php` covers every fallback branch + the v1.x-default rejection + leading-slash strip + path-traversal rejection + null-byte rejection + case-insensitive v1.x default rejection + migration 809 literal-pinning + fresh-install seed literal-pinning + valid round-trip. `web/tests/integration/BrandLogoChromeWiringTest.php` pins the wiring of `BrandLogo::resolve()` into `web/pages/core/header.php` (`$theme->assign('logo', …)` ships the resolved value, not the raw `Config::get`) AND `BrandLogo::resolveUrl()` into `web/pages/page.login.php` (the `LoginView::$brand_logo_url` property carries the resolved URL, not the pre-fix `themes/<theme>/<raw>` concatenation), AND covers the admin Settings page's `$config_logo_using_fallback` indicator matrix (6 cases: v1.x default flips warning, deleted file flips warning, valid custom path does not, empty does not, default itself does not, traversal indicator flips warning). |
| Live-preview Markdown in a settings textarea | `system.preview_intro_text` JSON action + `web/themes/default/page_admin_settings_settings.tpl` (`.dash-intro-editor` / `.dash-intro-preview`) |
| Regex-read `theme.conf.php` metadata for the admin Settings → Themes picker (without executing the manifest) | `Sbpp\Theme\ThemeConf` (`web/includes/Theme/ThemeConf.php`) — `parseDefine()` + `sanitizeLink()` / `sanitizeScreenshotFilename()`; wired from `web/pages/admin.settings.php` discovery loop (#1466). JSON theme preview (`api_system_sel_theme`) still `include`s the manifest. Regression: `web/tests/integration/ThemeConfParseTest.php`. |
| Build an empty-state surface (first-run vs filtered, primary/secondary CTAs) | `.empty-state` rules in `web/themes/default/css/theme.css` + reference shapes in `page_servers.tpl`, `page_dashboard.tpl`, `page_bans.tpl`, `page_comms.tpl`, `page_admin_audit.tpl`, `page_admin_bans_protests.tpl`, `page_admin_bans_submissions.tpl` |
Expand Down
22 changes: 22 additions & 0 deletions web/includes/View/AdminSettingsView.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ final class AdminSettingsView extends View
public function __construct(
public readonly string $config_title,
public readonly string $config_logo,
/**
* True when `template.logo` is non-empty but the chrome is
* currently rendering the fallback shield (i.e.
* {@see \Sbpp\View\BrandLogo::resolve()} returned
* {@see \Sbpp\View\BrandLogo::DEFAULT_PATH} for a non-default
* configured value — the file doesn't exist on disk, the
* value points at the v1.x default, or the value contains a
* traversal indicator). The template surfaces a warning chip
* next to the input so an operator who customised the path
* gets a visible signal that their customisation is
* inactive — without this the silent fallback would leave the
* operator believing their saved value is what the chrome
* renders (#1480 review finding 5).
*
* Stays false when the configured value EQUALS the resolved
* value (the happy path) AND when the configured value is
* empty / unset (no customisation in flight to flag as
* broken). The "configured is empty + fallback is being
* used" case isn't worth flagging because the empty input
* itself reads as "no customisation".
*/
public readonly bool $config_logo_using_fallback,
public readonly int $config_min_password,
public readonly string $config_dateformat,
public readonly string $config_dash_title,
Expand Down
Loading
Loading