Flatboard 5.7.2 "BEACON" — what changed since 5.6.0 "BASTION"
Three weeks and about a dozen point releases. This note collects everything between BASTION (5.6.0) and the current 5.7.2, grouped by what it does for you rather than which patch it shipped in.
The codename fits the work. BASTION closed the first half of a security audit; BEACON closed the second half, wired up a sitemap and feed pipeline that stays in sync with the forum, and rebuilt the installer.
🔒 Security
The audit that started in 5.6.0 ran all the way through this cycle. Here's what got fixed.
Stored XSS in the Markdown editors. EasyMDE and TUIEditor both render through Parsedown with rich HTML allowed, and the old cleanup only stripped <script> tags. Inline handlers like <img > and links slipped through and ran when someone opened the post. That's a stored XSS any author could plant. A new Sanitizer::stripXss() helper removes script tags, every on* handler, dangerous URL protocols, and non-image data URIs, while leaving legitimate formatting (bold, links, images, code) untouched. Both editors route their output through it now.
Logout was a GET route. That meant <img src="https://forum/logout"> on any external page could silently sign visitors out. Logout is POST-only with a CSRF token now; every theme already used a POST form, so nothing changes for users.
Login no longer leaks which accounts exist. When the identifier matched no user, the old code skipped password_verify() and returned early, fast enough that the timing difference told an attacker the account was valid. It now runs a verify against a dummy hash either way, so the response time is the same.
Password reset got a full sweep. Tokens are validated against a strict ^[a-f0-9]{64}$ shape before any storage lookup (closing a path-traversal window in the JSON backend) and stored only as a SHA-256 hash, so a database leak no longer hands over usable tokens. Resetting a password now invalidates any session still logged in under the old one, sends the account owner a "your password was changed" email, and a per-account throttle (10 tries / 15 min) sits on top of the per-IP limit so rotating IPs can't brute-force a single account.
State-changing API routes now require CSRF. All the /api/* POST/PUT/DELETE endpoints relied on the session and SameSite cookies alone; they now check a CSRF token. The shared apiRequest() helper injects it automatically, so existing frontend code keeps working.
Smaller hardening. Uploaded .php files are blocked at the nginx level, not just in .htaccess. The ForumImporter's db_prefix is validated before it touches a query, plugin IDs must match a strict format before resolving to a path, and the login controller stopped writing submitted identifiers into debug logs.
🧭 Installer and registration
The installer used to be one long form that ran off the bottom of the screen. It's a four-step wizard now: site configuration, admin account, SMTP, then a review screen before it commits anything. A numbered progress bar sits up top, each step validates before you can move on, and the last step shows a recap of what you entered. No JavaScript? It falls back to the old single-page form, install button and all.
The admin password field has a live strength meter: a colored bar plus a checklist that ticks off as you type (10 characters, lower, upper, digit, special). The policy behind it got stricter too, from a bare 8-character minimum to 10 characters with all four character classes required.
That meter is now a standalone component (themes/assets/css/shared/password-strength.css and its .js), and member registration uses the same one. Registration enforces the same strong policy now, so the meter on the signup form matches what the server actually accepts.
A few installer hardening bits landed alongside: security headers (X-Frame-Options, a restrictive CSP, and friends), the installer deletes itself after a successful run, and the submitted timezone and language are checked against known-good lists before they reach config.json.
One real bug, too. On an already-installed site the installer could start over and run on top of a live install. The "already installed" guard was passing absolute paths through a sanitizer that expects relative ones, which doubled the base path and made file_exists() always return false, so nothing ever tripped the guard. It's fixed, and the check is broader now: a valid config.json (what the app reads for both JSON and SQLite setups) counts as installed even when the lock file is gone, and you get a clear "Flatboard is already installed" page instead of a silent bounce to the homepage.
🔍 Sitemap and feeds in sync
Deleting a discussion used to leave it in /sitemap.xml indefinitely (the static file was served straight by nginx and never regenerated) and lingering in the RSS/Atom feeds for up to 15 minutes. Creating or editing had the same lag.
A new SeoCacheInvalidator clears all three layers (the sitemap cache, the static public/sitemap.xml file, and every RSS/Atom cache entry) the moment a discussion or post is created, edited, or deleted. The only staleness left is the browser/CDN cache, which is out of the server's hands.
Several feed bugs got sorted along the way: old discussions no longer resurface in readers when they get a reply, edited titles no longer reappear as unread (the feed ID is slug-free and stable now), and HEAD requests from feed bots match the GET routes as the spec requires.
🚫 Bans and IP tracking
Ban handling got a real overhaul this cycle.
The user record now stores registration_ip and last_ip directly, in both storage backends, with no plugin needed. SQLite databases pick up the columns automatically on the next startup.
That feeds a few things. The "Add ban" form auto-fills the member's last IP and shows both IPs as clickable badges. Banning by user ID without an IP now creates a matching IP ban automatically, with a toast so the admin knows. Each IP ban row lists every member who shares that IP (one batched query, no N+1), red for banned and grey for active.
Banned users are visibly marked everywhere now: a ban icon and a red badge on posts, profiles, member cards, and discussion lists, not just the profile page as before. And a stale-post-count bug is fixed: deleting a discussion on SQLite now removes its posts too, so the admin user list stops counting orphaned rows.
🇵🇱 Polish
Flatboard now officially speaks Polish, thanks to @nononsense and forum thread #178. The pack covers the full core, the premium theme, and the EasyMDE, Logger, and LegalNotice plugins. Polish is the sixth supported language, alongside French, English, German, Portuguese, and Chinese.
A couple of hardcoded-French strings that the contribution surfaced were fixed too: the draft-restore prompt and the discussion-form toasts now read from translation keys like everything else.
⚡ Performance
Most of the speed work landed in 5.6.6.
Static assets get long-term cache headers on Apache now (nginx already had them), so repeat visits stop revalidating every file. The session cache-limiter moved from nocache to private, which re-enables the browser back/forward cache without exposing authenticated content to shared caches. Bootstrap's JS loads with defer, and the frontend layout gained preload and preconnect hints with the logo marked high-priority since it's usually the LCP element.
Five small stylesheets were merged into one theme-ui-bundle.css, cutting five round-trips to one on every page.
The editors and several plugins now load their assets only where they're actually used. TUIEditor and EasyMDE (a quarter-megabyte of JS and CSS) only load on routes that render an editor; PrivateMessaging, ForumMonitoring, and FlatModerationExtend scope their CSS to the pages that need it. A new storage method, getUsersByGroup(), replaces the old "load every user and filter in PHP" pattern with a single indexed query. It also counts both primary and additional group membership correctly, which the old path silently missed.
🛠️ Other fixes
Visitor tracking only counts real pages now. It used to run before routing, so 404s and scanner probes (/wp-login.php, /.env, and friends) showed up as real guests in the admin panel. It runs after a route matches and its middleware passes, with a scanner blocklist as backup.
Markdown tables survive CRLF. Content saved with Windows line endings was getting a blank line injected between every table row, which broke rendering. Line endings are normalized before any other processing now.
Terminal theme got two fixes: missing logo and favicon files, and missing badge and presence-dot sizing in the post view. It also first shipped this cycle, in 5.6.2.
Admin panel dropdowns no longer clip inside cards, the sidebar logo lines up across every theme, admin JS translations stop falling back to French because the translation block now loads in the <head>, and the backup list refreshes properly after create/upload.
You can finally create tags from the admin panel. Until now a tag only came into existence when someone typed it into the Tags field of a discussion, so Admin → Tags sat empty on a fresh install with no obvious way to add one. People kept asking how to create a tag, which was the giveaway. There's a "Create tag" button there now (name, icon, color, with a live preview).
Clicking a member's profile no longer dumps you on the homepage. Whether profiles are public is up to the admin (it's a permission). On forums that keep them members-only, guests — and freshly registered members who haven't confirmed their email yet, since they sit in the guest group until they do — used to get bounced to the login page and then silently forwarded to the homepage, losing the profile they were after. The login redirect remembers the profile now and sends you back to it once you're in.
One specific member's profile would 404 while everyone else's loaded fine. That's the kind of bug that makes no sense until you find it. On SQLite installs, usernames are matched case-insensitively through a "normalized" column, but that column was only ever filled by a one-time migration; creating an account never set it, so anyone registered or imported afterwards had it blank. If a profile URL's casing didn't match exactly (say /u/nononsense for a user saved as NoNonsense), the lookup came up empty and you got a "not found" that, with its homepage banner, looked like a redirect home. New accounts fill the column in now, the lookup tolerates the old blank ones, and a migration backfills everything on the next startup.
And the same profile showing up under a different spelling. Part of the same mess: the username cache keyed its entries by exact casing and even remembered "not found" answers for fifteen minutes, so /u/nononsense and /u/Nononsense lived in separate slots that could drift apart. One would show a stale name in the header while the admin list showed the real one; another could stay stuck on "not found" well after the data was fine. The cache key is lowercased now so every spelling shares a single entry, editing a username keeps its normalized form in sync, and renaming clears the old entry. Existing sites want one cache clear after updating to flush anything already stuck in there.
SocialLogin Google OAuth works behind WAFs that block https:// in query strings; the callback uses form_post now. PHPMailer moved to 7.1.1.
Local updates are smoother. The "update available" badge now lights up for an update archive staged locally, not just after the remote version check — handy on an offline server or when a Pro package was dropped in by hand. And once you upload an update archive, you land straight on the Updates page instead of having to find your way there before clicking "Update". There's also a fix for a sneakier problem: static JS and CSS are served with a one-year immutable browser cache, but their URLs carried no version — so after an update, returning visitors kept the old files, sometimes for a long time. Asset URLs now include a ?v= based on the file's modification time, so a deployed change (JS or CSS, themes and plugins alike) is picked up immediately, with no manual cache clear.
😊 An emoji picker that doesn't make you scroll
Both Markdown editors (EasyMDE and TUIEditor) used to give you a plain grid of about a hundred emojis and that was it. They now share a real picker: a popup with category tabs (smileys, people, animals, food, travel, activities, objects, symbols, flags) and a search box, so you type "cat" or "pizza" and click. It drops the emoji at your cursor, follows light/dark mode, and closes on click-away or Escape. It's one shared component used by both editors, with the category labels translated in all six languages. No skin-tone variants yet.
🧩 For plugin developers
Two small extensibility additions, both groundwork for a TinyPNG-style image optimizer plugin.
Uploaded images now fire a hook, upload.image.saved, the moment they're written to disk — editor images and avatars alike. It's a filter: a plugin can compress the file in place, or convert it (say PNG to WebP) and rename it, and the new URL flows back to the caller, so the link the editor drops into the post points at the optimized file. Core doesn't change its own behaviour; the door is just open now.
And a plugin can finally require a system binary. plugin.json gained a requires.binaries list alongside the existing PHP-version and extension checks, so a plugin that shells out to pngquant or cwebp refuses to activate until the tool is actually installed — the same "missing requirement" gate that already blocks on an old Flatboard version. The check is hardened against command injection and falls back to a manual PATH scan when shell_exec is disabled.
A layout bug got fixed along the way: a plugin injecting HTML into an admin page through the view.admin.main.before hook saw its fragment rendered twice and the real page content wiped out, because the backend layout reused the $content variable (the page body) as its loop variable. The loop variables are renamed, so the hook works as intended — it's exactly what an in-page settings panel relies on.
📦 Packages
- Community: EasyMDE
2.3.13— emoji picker, XSS fix, conditional asset loading, table fixes - Pro (includes everything in Community): FlatModerationExtend
1.0.7— pre-moderation correctness, batched counters, optimized notifications · TUIEditor1.3.11— emoji picker, XSS fix, conditional loading
⬆️ Updating
Update via Admin > Updates. No database migration; the SQLite column additions run automatically on the next startup. JSON and SQLite both supported. If you're updating an existing install, nothing about the installer changes touches you, but if you ever re-run install.php on a live site it now stops and tells you instead of starting over.
📋 Full changelog
The complete changelog with all technical details is available in full changelog thread.
Thank you to everyone who reported bugs and contributed to this release. 🙏