🚀 Changelog — Flatboard 5.4.0
Release date: April 1, 2026
Added
[Pro] ForumMonitoring: "Active members today" card with 7-day sparkline — A new card appears in the full monitoring page (and as a compact counter in the dashboard widget) showing the number of unique members active today (based on
last_activity), a 7-day bar sparkline with today highlighted, a delta badge vs yesterday, and the full list of today's active members as pills with their last-seen time.ForumMonitoringService::getDailyActiveUsers(int $days = 7)is the new service method powering this feature. Translation keys added to all five language files.
Files changed:plugins/ForumMonitoring/ForumMonitoringService.php,plugins/ForumMonitoring/ForumMonitoringController.php,plugins/ForumMonitoring/ForumMonitoringPlugin.php,plugins/ForumMonitoring/views/full.php,plugins/ForumMonitoring/views/widget.php,plugins/ForumMonitoring/langs/fr.json,plugins/ForumMonitoring/langs/en.json,plugins/ForumMonitoring/langs/de.json,plugins/ForumMonitoring/langs/pt.json,plugins/ForumMonitoring/langs/zh.json.[Pro] FlatModerationExtend: dashboard widget — New admin dashboard widget showing pending content count (discussions + replies), active shadow bans count, and premod/shadowban ON/OFF status. Enabled by default; togglable via the plugin settings (
enable_dashboard_widget).
Files changed:plugins/FlatModerationExtend/FlatModerationExtendPlugin.php,plugins/FlatModerationExtend/plugin.json,plugins/FlatModerationExtend/langs/{fr,en,de,pt,zh}.json.[Pro] PrivateMessaging: dashboard widget — New admin dashboard widget displaying the current admin's unread private message count with a link to the inbox. Enabled by default; togglable via the plugin settings.
Files changed:plugins/PrivateMessaging/PrivateMessagingPlugin.php,plugins/PrivateMessaging/plugin.json,plugins/PrivateMessaging/langs/{fr,en,de,pt,zh}.json.[Pro] Private RSS/Atom feeds via API token — On Flatboard Pro, all feed endpoints (
/feed/rss,/feed/atom,/feed/rss/category/{slug},/feed/atom/category/{slug},/rss/user/{id},/atom/user/{id},/rss/tag/{id},/atom/tag/{id}) now accept an optional?token=query parameter. The token is the per-user API key generated from the profile security settings. When a valid token is supplied, the feed includes discussions from categories the user is authorised to access based on their group membership, in addition to all public content. Invalid or missing tokens fall back silently to public-only content. On Flatboard Community the parameter is ignored entirely.Tag editing — Tags can now be fully edited after creation. The admin panel exposes name, slug, color, and icon fields for existing tags. Previously only creation and deletion were supported.
Files changed:app/Controllers/Admin/TagController.php.Updates section: plugin and theme update status — The "Updates" entry in the admin menu now lists all installed plugins and themes with their current version and available update, if any. Admins can see at a glance what needs updating without visiting each plugin page individually. The locale update flow has also been clarified: when a language file update is available, the interface now describes what changed rather than showing a bare version number.
Locale-aware date formatting —
DateHelper::format()andDateHelper::human()now replace theF(month long),M(month short),l(day long), andD(day short) PHP format specifiers with translated names read from the active language file, instead of relying on PHP's English-onlydate()output. All five language files (fr,en,de,pt,zh) gain a newdatetimetop-level section providingmonths_long,months_short,days_long,days_short, and default format strings (format_date,format_datetime,format_long). Escaped characters (\F,\l, etc.) in format strings are handled correctly.
Files changed:app/Helpers/DateHelper.php,languages/fr/main.json,languages/en/main.json,languages/de/main.json,languages/pt/main.json,languages/zh/main.json.
Fixed
[Pro] FlatHome: static page linked to a discussion showed stale content after post edits —
FlatHomeService::renderPageContent()was reading$discussion['content'](the initial snapshot stored indiscussion.json) instead of the actual first-post content. It now callsPost::getFirstPost()and falls back to the snapshot only if no post is found, matching the behaviour already used by the blog article renderer.
Files changed:plugins/FlatHome/FlatHomeService.php.[Pro] FlatHome: banner date on discussion-backed pages now shows the discussion date — When a CMS page is linked to a discussion, the banner date badge was falling back to the page record's own creation date when the discussion had no
updated_at. The fallback to the page date has been removed; for discussion-type pages the date is now always taken from the discussion'supdated_at(orcreated_at), and is omitted if the linked discussion cannot be resolved.
Files changed:plugins/FlatHome/FlatHomePlugin.php.[Pro] FlatHome: blog widget card date showed English month abbreviation —
modeles/home.phpbuilt the card date withdate('M j, Y', ...)which always produced English names (e.g. "Jan 1, 2026"). Replaced withDateHelper::format($ts, 'j M Y')so the month abbreviation is drawn from the active language file.
Files changed:plugins/FlatHome/modeles/home.php.[Pro] ForumMonitoring: activity timeline and bars showed English day abbreviations — The activity timeline (
Dspecifier) and the per-user activity bars (D d/mformat) were produced with rawdate()calls and always returned English names (Mon, Tue…). Both now go throughDateHelper::format().
Files changed:plugins/ForumMonitoring/views/full.php.[Pro] PrivateMessaging: admin activity chart used English day abbreviation — The per-user activity bar chart produced day labels with
date('D', ...). Replaced withDateHelper::format().
Files changed:plugins/PrivateMessaging/views/admin.php.Router:
/admin/plugins/{plugin_id}/adminroute deliverednullas the$viewparameter — The route was declared with a literal/adminsuffix, soPluginViewController::show()always received$view = nulland returned 404. The route is now declared as/admin/plugins/{plugin_id}/{view}, correctly passing the view name as a captured parameter.
Files changed:app/Core/App.php.User profile: guests received a 403 instead of being redirected to login — When
profile.viewwas restricted for guests,UserController::show()returned a 403 for both unauthenticated visitors and logged-in members without permission. Guests are now redirected to/loginwith an explanatory toast (permission.profileLoginRequired) viahandlePermissionDenied(..., 401), while authenticated members without the permission still receive a proper 403. Translation keyprofileLoginRequiredadded to all five language files.
Files changed:app/Controllers/User/UserController.php,languages/fr/errors.json,languages/en/errors.json,languages/de/errors.json,languages/pt/errors.json,languages/zh/errors.json.Timezone not applied to PHP native date functions —
App::__construct()now callsdate_default_timezone_set()immediately after loading the configuration, using the admin-configuredtimezonevalue (defaultEurope/Paris). This ensures alldate()/time()calls throughout the application use the correct timezone rather than the PHP server default.DateHelper::format()andDateHelper::human()also construct theirDateTimeobjects with this timezone explicitly, making today/yesterday boundaries inhuman()correct regardless of server locale.
Files changed:app/Core/App.php,app/Helpers/DateHelper.php.
Changed
- RSS/Atom category feeds now use slug instead of ID in URL — Category feed URLs are now in the form
/feed/rss/category/{slug}and/feed/atom/category/{slug}instead of the opaque UUID-based/feed/rss/category/{id}. The route parameter was renamed from{id}to{slug};RssController::rssCategory()andatomCategory()now resolve the slug to a category ID viaCategory::findBySlug()before delegating toRssService, returning 404 if the slug is unknown. All views and plugins that generate category feed links have been updated accordingly.
Files changed:app/Core/App.php,app/Controllers/Seo/RssController.php,app/Views/discussions/index.php,app/Views/categories/index.php,themes/premium/views/discussions/index.php,themes/premium/views/categories/index.php,plugins/FlatBlog/FlatBlogController.php,plugins/FlatBlog/FlatBlogPlugin.php,plugins/FlatBlog/views/index.php,plugins/FlatHome/FlatHomeBlogController.php,plugins/FlatHome/FlatHomePlugin.php,plugins/FlatHome/views/blog/index.php,plugins/FlatHome/views/blog/article.php.
🚀 Changelog — Flatboard 5.3.9
Release date: March 29, 2026
Security
FlatSEO: private-category discussions and categories exposed in sitemap —
FlatSEOSitemap::getDiscussionUrls()andgetCategoryUrls()did not checkallowed_groupson the category, causing discussions and categories with restricted access to appear in the XML sitemap and be indexed by search engines. Both methods now skip entries whose category has a non-emptyallowed_groups. Categories are pre-loaded in a single batch to avoid N+1 queries. Additionally,FlatSEOService::invalidateSitemapCache()was not deleting theflatseo:sitemap_maincache key, so the main sitemap was never invalidated on content changes; this key is now included in the cleanup.
Files changed:plugins/FlatSEO/FlatSEOSitemap.php,plugins/FlatSEO/FlatSEOService.php.FlatSEO: sitemap discussion URLs used internal ID instead of global number —
getDiscussionUrls()was building URLs as/d/{internal_id}-{slug}instead of the correct/d/{number}-{slug}format. Global numbers are now resolved in a single batch call (getDiscussionGlobalNumbersBatch) before building URLs, avoiding N+1 queries.
Files changed:plugins/FlatSEO/FlatSEOSitemap.php.CLI: block execution outside of command-line context —
app/Cli/console.phpnow checksPHP_SAPI !== 'cli'at the very top and exits with HTTP 403 immediately if the script is reached through a web request. Althoughapp/is already blocked at the web-server level (Apache.htaccessandnginx.conf), this code-level guard acts as a second line of defence against server misconfiguration.
Files changed:app/Cli/console.php.
Fixed
- Backups: deleted backup not removed from the list — After confirming deletion, the backup row now disappears immediately from the list without a page reload. The
handleDeleteBackupmethod inbackups-management.jsnow locates the<tr>containing the delete button via[data-delete]and removes it directly from the DOM on success, falling back to a page reload only if the row cannot be found.
Files changed:themes/assets/js/admin/modules/backups-management.js.
Changed
RateLimiter::check()— new$silentparameter — Passing$silent = truesuppresses thelogRateLimitHit()call when a rate limit is reached. Useful for high-frequency endpoints (e.g. typing indicators) where hitting the limit is expected behaviour rather than an anomaly worth logging.
Files changed:app/Core/RateLimiter.php.Permissions — Guest group: extended lock to all actions requiring authentication — Building on the visual lock indicator introduced for the Guest group (
discussion.edit,discussion.delete,post.edit,post.delete,attachment.upload,image.upload,avatar.upload), the set of locked permissions has been expanded to cover every action that is logically impossible without a persistent identity. The rule applied: any permission that (a) creates content attributed to an author, (b) implies per-user tracking (vote, reaction, rating), (c) requires a personal account (private messaging, bot history) or (d) is an administrative/moderation role is locked. Locked permissions display a padlock icon instead of a toggle in both Badges and Matrix views, and are silently stripped from the Guest group on every save — even if somehow submitted. Newly locked core permissions:discussion.create,post.create,reaction.react,tag.create. Newly locked plugin permissions:flatpolls.polls_create,flatpolls.polls_vote,flatpolls.polls_manage;privatemessaging.pm.send,pm.receive,pm.delete_own,pm.moderate;resourcemanager.resource.submit,resource.rate,resource.manage,resource.moderate;flatletter.newsletter_manage,newsletter_send;flatmoderationextend.access;flatbot.flatbot.use,flatbot.history;forummonitoring.monitoring.view;impersonate.impersonate;inactiveusermanager.inactiveusers.view,inactiveusers.delete;translationmanager.edit,create_language,import_export,manage_members,settings. Permissions intentionally left configurable for guests (read-only, no identity required):attachment.view,attachment.download,profile.view,presence.view,tag.view,flatpolls.polls_view,resourcemanager.resource.view,resource.download.
Files changed:app/Controllers/Admin/PermissionController.php.Permissions — Guest lock: Plugins tab now honours the padlock — The Plugins tab in
admin/permissionsrendered its Badges and Matrix views with its own inline loop that bypassedPermissionSectionentirely. As a result, the guest lock indicator and the server-side strip were never applied to plugin permissions. Both loops have been updated to apply the same$isGuestLockedPerm/$isLockedlogic used by all other tabs: locked cells display a dimmed<span>withfa-lockin Badges view and aperm-mx-lockedcell in Matrix view.
Files changed:app/Views/admin/permissions.php.
🚀 Changelog — Flatboard 5.3.8
Release date: March 28, 2026
Fixed
Admin sidebar: update badge showing incorrect count or not displaying — The badge counter for available updates (core + plugins/themes) was unreliable across themes.
UpdateController::hasUpdateAvailable()was incorrectly including resource updates in its return value, causing double-counting. A newUpdateStatsService::getCachedResourceUpdates()reads from the file/memory cache without performing HTTP requests, making it safe to call from layout headers. All theme header files now use this method to compute the correct total and hide the badge when there are no updates.
Files changed:app/Services/UpdateStatsService.php,app/Controllers/Admin/UpdateController.php,app/Views/layouts/backend/header.php,themes/premium/views/layouts/backend/header.php,themes/ClassicForum/views/layouts/backend/header.php,themes/IPB/views/layouts/backend/header.php,themes/bootswatch/views/layouts/backend/header.php.Settings: persistent toast on Security tab page load — Static
.alert-*elements in the Security tab (2FA info, API token status, token warning) were picked up by thetoast.jsfallback scanner and shown as toasts on every page load.data-toast="none"is now set on these elements to exclude them from the scanner. Apageshowhandler also clears the one-shot token display div when the page is restored from the browser back/forward cache (bfcache).
Files changed:app/Views/users/settings.php.EasyMDE: editor content not visible when initialised in a hidden Bootstrap tab — CodeMirror cannot measure its dimensions when its container has
display:none, so content was invisible until the user clicked inside the editor (e.g. the signature field on the Profile tab). Ashown.bs.tablistener is now registered at editor initialisation time; when the containing tab is revealed,editor.codemirror.refresh()is called to trigger a full re-render.
Files changed:plugins/EasyMDE/dist/easymde-init.js.
Fixed
- Local update:
versionanddatenot updated inplugin.json/theme.json— The smart-merge strategy used when deploying update archives preserved all existing scalar values, including metadata fields such asversion,date,update_url, anddescription. These fields are now explicitly overwritten from the archive before the merge, while user-configured values (pluginsettings,activeflag, custom variables) are still preserved.
Files changed:app/Controllers/Admin/UpdateController.php.
Changed
Backups: informational cards explaining each action — Three info cards now appear below the action bar, describing what "Create backup", "Download" and "Upload backup" each do. The upload modal now shows two use-case cards ("Restore a backup" / "Apply an update") before the file picker, making the dual purpose of the button immediately clear. Translation keys added to all five language files.
Files changed:app/Views/admin/backups.php,languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.Settings: Security tab UX improvements — The Two-Factor Authentication section now has a card header with its title and a status badge (Enabled / Disabled). The description is displayed as plain body text instead of an
alert-infobanner. The action button is now contextual: "Enable Two-Factor Authentication" (primary) when 2FA is off, "Disable Two-Factor Authentication" (danger outline) when on, each with a matching description. The form Save/Cancel bar is automatically hidden while the Security tab is active, since all security actions are independent AJAX or navigation calls unrelated to the main profile form.
Files changed:app/Views/users/settings.php.
🚀 Changelog — Flatboard 5.3.7
Release date: March 27, 2026
Changed
UpdateStatsService: robust CA bundle resolution for SSL update checks —findCACert()now queriescurl.cainfoandopenssl.cafilefromphp.inifirst (covers Windows, cPanel, and exotic hosting environments), then falls back to common system paths, and finally to a Mozilla CA bundle (stockage/certs/cacert.pem) shipped with Flatboard. This guarantees that the update checker can always establish a verified SSL connection regardless of the host environment.
Files changed:app/Services/UpdateStatsService.php,stockage/certs/cacert.pem(new).CLI:
update:renew-cacertcommand — New CLI command that downloads the latest Mozilla CA bundle fromcurl.seand overwritesstockage/certs/cacert.pem. Run to force an immediate renewal:php app/Cli/console.php update:renew-cacert.
Files changed:app/Cli/Commands/UpdateCommand.php(new),app/Cli/console.php.Automatic monthly CA bundle renewal —
UpdateStatsService::scheduleCacertRenewalIfDue()is invoked on every request viaApp::run(). Using a flag file (stockage/certs/.last_renewal), it triggers a renewal at most once every 30 days — after the response is sent to the client when FastCGI is available, inline otherwise. No cron daemon or manual intervention required.
Files changed:app/Services/UpdateStatsService.php,app/Core/App.php.Plugin & theme update notifications — Flatboard now checks for available updates on all plugins and themes that declare an
update_urlin theirplugin.json/theme.json. Eachupdate_urlendpoint must return{"version": "x.y.z", "changelog_url": "..."}. Results are cached for 1 hour. The/admin/updatespage shows a dedicated section listing outdated resources with their current and latest version. Resources from unofficial (third-party) sources display a prominent warning. Official plugins and the Premium theme now include anupdate_urlpointing tohttps://versions.flatboard.org/api/plugins/{id}andhttps://versions.flatboard.org/api/themes/premiumrespectively. The admin sidebar badge now shows the total count of available updates (core + plugins + themes).
Files changed:app/Services/UpdateStatsService.php,app/Controllers/Admin/UpdateController.php,app/Views/admin/updates.php,app/Views/layouts/backend/header.php,plugins/EasyMDE/plugin.json,plugins/Logger/plugin.json,plugins/FlatHome/plugin.json,plugins/FlatModerationExtend/plugin.json,plugins/FlatSEO/plugin.json,plugins/ForumMonitoring/plugin.json,plugins/PrivateMessaging/plugin.json,plugins/StorageMigrator/plugin.json,plugins/TUIEditor/plugin.json,themes/premium/theme.json,languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.
🚀 Changelog — Flatboard 5.3.6
Release date: March 26, 2026
Changed
Installer: default category created with icon and colour — The category created during installation (
general) is now initialised with a default FontAwesome icon (fa-comments) and colour (#6c757d) instead of leaving those fields null.
Files changed:install.php.UpdateStatsService: centralised update/stats logic — The private methods previously duplicated acrossUpdateControllerand independently re-implemented ininstall.phpandShortcodes/ShortcodesRegistry.phphave been extracted into a newApp\Services\UpdateStatsServiceclass with static methods:detectSiteUrl(),getInstalledPlugins(),countStat(),findCACert(),buildStatsPayload(),sendStats().UpdateController::getLatestVersionInfo()and the installer both delegate to this service.ShortcodesRegistry::findCACert()has been removed in favour ofUpdateStatsService::findCACert(). ThewriteAtomicJson()wrapper function ininstall.phphas been removed; all callers now use\App\Core\AtomicFileHelper::writeAtomic()directly.
Files changed:app/Services/UpdateStatsService.php(new),app/Controllers/Admin/UpdateController.php,plugins/Shortcodes/ShortcodesRegistry.php,install.php.Installer: anonymous statistics ping on fresh install — After a successful installation, a POST request is sent to the configured
update_check_urlcarrying the same statistics payload as a regular update check, plus"install": trueto distinguish new installations from version checks. The request is silent — any failure has no effect on the installation.
Files changed:install.php,app/Services/UpdateStatsService.php.
Security
Session: Remember Me now enforces a 30-day absolute timeout — Previously
handleTimeouts()bypassed all timeout checks for remember-me sessions, making them potentially immortal. BothhandleTimeouts()andisExpired()now applyREMEMBER_ME_LIFETIME(30 days) as an absolute ceiling while still skipping the idle timeout.
Files changed:app/Core/Session.php.IP spoofing via proxy headers —
Session::getClientIp()andRequest::getIp()previously trustedX-Forwarded-For,X-Real-IP, andCF-Connecting-IPunconditionally. A newIpHelperclass reads those headers only whenREMOTE_ADDRbelongs to the configuredsecurity.trusted_proxieslist (supports individual IPs and CIDR ranges, IPv4 and IPv6). Default is an empty list (safe by default). Configured viaconfig.jsonkeysecurity.trusted_proxies.
Files changed:app/Core/IpHelper.php(new),app/Core/Config.php,app/Core/Session.php,app/Core/Request.php.Password reset token TTL now configurable — The 24-hour hardcoded expiry has been replaced by
security.password_reset_token_ttl(seconds, default3600— 1 hour, more appropriate than 24 h).
Files changed:app/Controllers/Auth/PasswordResetController.php,app/Core/Config.php.Admin dashboard: security alert when debug mode is active — A prominent red banner is displayed at the top of the admin dashboard whenever
debugis enabled, with a direct link to the settings page to disable it. Translation keys added to all five language files.
Files changed:app/Views/admin/dashboard.php,languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.Group change invalidates active sessions — When an admin changes a user's group, the user's next authenticated request now detects the mismatch and destroys their session, forcing re-login with the new permissions.
User::update()stampspermissions_version = time()on group change;LoginControllerstores this value in the session at login;AuthMiddlewarecompares the session value against the DB on every protected request.
Files changed:app/Models/User.php,app/Controllers/Auth/LoginController.php,app/Middleware/AuthMiddleware.php.CSP:
unsafe-inlineandunsafe-evalremoved fromscript-src— TheContent-Security-Policyheader previously included both'unsafe-inline'and'unsafe-eval'inscript-src/script-src-elem, nullifying XSS protection. Two changes eliminate these directives. (1) Nonces: a newCspNonceclass generates a cryptographically random nonce per request viarandom_bytes(16); the nonce is embedded inscript-srcas'nonce-{nonce}'; an output-buffer callback inApp::run()automatically injectsnonce="{nonce}"into every<script>tag — no view or plugin needs modification. (2)unsafe-eval: the sole actual caller wasEasyMDEPlugin, which serialised the image-upload handler as a PHP string and reconstructed it client-side vianew Function(). The string is removed;easymde-init.jsalready contains an identical built-in handler that readseditorConfig.csrfTokendirectly — no behaviour change. The webpacknew Function("return this")polyfill in TUI Editor is never executed on modern browsers (globalThisis natively available) and is caught by CSP gracefully on legacy ones.
Files changed:app/Core/CspNonce.php(new),app/Core/App.php,plugins/EasyMDE/EasyMDEPlugin.php,plugins/EasyMDE/dist/easymde-init.js.
Fixed
CSP: all inline event handlers replaced with event delegation — All
onerror,onclick,onchange,onsubmit,onmouseover,onmouseout,oninput,onkeyup,oninvalidinline attributes across the core and all packaged plugins violated thescript-src-attrCSP directive. Each has been replaced: image fallbacks usedata-fallback(capturederrorevent); form submission, navigation, text selection, color-sync, range-display, and custom-validation patterns use delegated handlers registered once infrontend-bundle.js(data-confirm,js-auto-submit,js-nav-select,js-select-on-click,data-color-target,data-range-display,data-sync-to,data-required-msg); plugin-specific actions (FlatPolls, ResourceManager, FlatLetter, PolicyGuard, etc.) usedata-*attributes with delegated listeners in their own existing<script>blocks.
Files changed:themes/assets/js/frontend/frontend-bundle.js,app/Views/discussions/search.php,themes/premium/views/discussions/search.php,app/Views/discussions/_discussion_item.php,themes/premium/views/discussions/_discussion_item.php,themes/IPB/views/discussions/_discussion_item.php,themes/ClassicForum/views/discussions/_discussion_item.php,app/Views/discussions/show.php,themes/premium/views/discussions/show.php,app/Views/posts/edit.php,app/Views/admin/users.php,app/Views/admin/analytics.php,app/Helpers/FormFieldHelper.php,themes/assets/js/frontend/modules/mention-manager.js,themes/assets/js/presence-lists.js,plugins/FlatSEO/views/admin.php,plugins/FlatLetter/views/admin.php,plugins/FlatLetter/views/compose.php,plugins/FlatLetter/views/subscribers.php,plugins/PrivateMessaging/views/compose.php,plugins/PolicyGuard/PolicyGuardPlugin.php,plugins/PolicyGuard/views/admin.php,plugins/FlatPolls/views/_poll_inline_edit.php,plugins/FlatPolls/views/discussion_poll_edit_form.php,plugins/FlatPolls/views/show.php,plugins/FlatPolls/views/edit.php,plugins/FlatPolls/views/admin.php,plugins/FlatHome/views/blog/article.php,plugins/FlatBlog/views/article.php,plugins/TranslationManager/views/edit_frontend.php,plugins/TranslationManager/views/admin/edit.php,plugins/LegalNotice/LegalNoticeService.php,plugins/Flatbot/views/admin.php,plugins/ResourceManager/views/by-tag.php,plugins/ResourceManager/views/index.php,plugins/ResourceManager/views/edit.php,plugins/ResourceManager/views/submit.php,plugins/ResourceManager/views/admin/cleanup.php,plugins/ResourceManager/views/admin/purchases.php,plugins/ResourceManager/views/admin/subscriptions.php,plugins/ResourceManager/views/admin/index.php,app/Services/PostEditFormGenerator.php.Installer: "already installed" page hardcoded in French — The page shown when Flatboard is already installed (
die()block) contained hardcoded French strings with no translation. Three new keys (error.alreadyInstalled.title,error.alreadyInstalled.message,error.alreadyInstalled.hint) have been added to all fiveinstall.jsonlanguage files and the block now usest()consistently.
Files changed:install.php,languages/fr/install.json,languages/en/install.json,languages/de/install.json,languages/pt/install.json,languages/zh/install.json.Login with "Remember me" + 2FA: session not persistent — When "Remember me" was checked and 2FA was active, the resulting session was treated as session-only (cleared on browser close). The root cause was a duplicate
Set-Cookieheader conflict:session_start()(called at the top of every request) emits a session-onlySet-Cookie; the subsequent manualsetcookie()call with a 30-day expiry produced a second header for the same cookie name, and some browsers honoured the first header rather than the last. The fix replaces the manualsetcookie()calls in bothlogin()andverify2FA()withSession::regenerate(), which callssession_regenerate_id()thenextendRememberMeCookie()— producing a single, cleanSet-Cookiecarrying the new session ID with the correct 30-day expiry. As a side effect, both login paths now regenerate the session ID on successful authentication, closing a session-fixation window that previously existed.
Files changed:app/Controllers/Auth/LoginController.php.
🚀 Changelog — Flatboard 5.3.5
Release date: March 25, 2026
Changed
- FlatHome: page view counter displayed in the banner — The view count (
viewsfield, tracked atomically on each CMS page visit) is now rendered in the page banner alongside the "Last updated" date, as a translucent badge (bg-white bg-opacity-25). Display is conditional on theshow_viewssetting. Theviews/page.phparticle header has been cleaned up: the date and view count are no longer duplicated in the article body; only the author avatar/name remains there if an author is set. TheinjectBannerContent()method now fetches settings and passes$showViews/$pageViewsto the banner view.
Files changed:plugins/FlatHome/FlatHomePlugin.php,plugins/FlatHome/views/banner.php,plugins/FlatHome/views/page.php.
Fixed
- FlatHome: TypeError in
view.navbar.itemswhen another plugin injects an array item —FlatHomePlugin::addNavItems()iterated over$itemsand passed each element directly toparseNavHtml(string $html). Plugins such asImpersonateinject array items (structured data) rather than HTML strings into this hook, triggering a fatalTypeError. The fix collects non-string items in a separate list before parsing, skips them inparseNavHtml, and appends them verbatim at the end of the rebuilt array — the header templates already render both formats correctly. FlatHome plugin bumped to 1.0.1.
Files changed:plugins/FlatHome/FlatHomePlugin.php,plugins/FlatHome/plugin.json.
🚀 Changelog — Flatboard 5.3.4
Release date: March 25, 2026
Changed
Discussion list: grouped tags with +N badge and click-to-reveal popup — Tags in the discussion list are displayed in a compact stacked group: each tag overlaps the previous by
0.5remwith a body-background outline ring for visual separation. Only the first 3 tags are shown inline; a+Ndashed pill badge (rendered as a<button>) indicates the count. Clicking the badge opens a.flatboard-tags-overflowpopup card positionedabsoluteabove the row with a CSS arrow and box-shadow — fully out of normal flow so the discussion layout never shifts. All overflow tags inside the popup are fully clickable links. Clicking outside the popup or pressing Escape closes it; a second click on the badge toggles it off. Active state is indicated by a darker badge background. Tags are slightly smaller (font-size: 0.65rem,max-width: 10remwith ellipsis). Dark mode support via[data-bs-theme="dark"]overrides. Applied to all four discussion-item partials, all five frontend CSS files, andmain.js(delegated click handler). ClassicForum/IPB tags container changed from<span>to<div>for valid HTML nesting.
Files changed:themes/premium/views/discussions/_discussion_item.php,app/Views/discussions/_discussion_item.php,themes/ClassicForum/views/discussions/_discussion_item.php,themes/IPB/views/discussions/_discussion_item.php,themes/default/assets/css/frontend.dev.css,themes/default/assets/css/frontend.css,themes/premium/assets/css/frontend.css,themes/ClassicForum/assets/css/frontend.css,themes/IPB/assets/css/frontend.css,themes/NordTheme/assets/css/frontend.css.Admin — Tags page: optimised loading, colour editing, and improved UX —
TagController::index()previously issued onecountDiscussionsByTag()call per tag (N+1 pattern). A newcountAllDiscussionsByTag()method inStorageInterface,JsonStorage(single glob scan), andSqliteStorage(singleGROUP BYquery) reduces total I/O to two operations. Theupdate()action now accepts acolorfield (validated hex) and returns JSON on AJAX requests. The admin/tags edit modal gains a colour picker with a live preview badge. Delete and rename both operate via AJAX — the row updates in place without page reload. The view gains a live search input (name/slug), a per-page selector (25/50/100/all), and client-side pagination. The script is now wrapped inDOMContentLoadedto avoid thebootstrap is not definedcrash. Tag colours are now applied as inlinebackground-color/color/border-colorstyles in all three theme discussion-item partials (premium, ClassicForum, IPB) and on the public/tagspage, using the WCAG relative-luminance formula for contrast text colour. All five language files receive thepanel.tags.search,panel.tags.pagination, andpanel.tags.rename.color_labelkeys.
Files changed:app/Storage/StorageInterface.php,app/Storage/JsonStorage.php,app/Storage/SqliteStorage.php,app/Controllers/Admin/TagController.php,app/Views/admin/tags.php,app/Views/discussions/tags.php,app/Views/discussions/_discussion_item.php,themes/premium/views/discussions/tags.php,themes/premium/views/discussions/_discussion_item.php,themes/ClassicForum/views/discussions/_discussion_item.php,themes/IPB/views/discussions/_discussion_item.php,languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.Admin — Tags: icon customisation via icon-picker — The tag edit modal now includes an icon field with a live preview and an icon-picker button that opens the shared
iconPickerModal-tagdialog (reusing the existingicon-picker.phpcomponent without any JS changes). TheTagController::update()action validates the submitted icon against the FontAwesome class format regex and stores it on the tag; the AJAX response includes anew_iconfield so the admin table row updates immediately without a page reload. All six frontend tag-rendering locations now display the stored icon instead of the hardcodedfas fa-tagfallback: the three discussion-item partials (premium, ClassicForum, IPB), the default fallback partial, and both/tagspage views (premium theme and app fallback). Three new language keys (panel.tags.rename.icon_label,panel.tags.rename.icon_placeholder,panel.tags.rename.icon_help) added to all five language files.
Files changed:app/Controllers/Admin/TagController.php,app/Views/admin/tags.php,app/Views/discussions/tags.php,app/Views/discussions/_discussion_item.php,themes/premium/views/discussions/tags.php,themes/premium/views/discussions/_discussion_item.php,themes/ClassicForum/views/discussions/_discussion_item.php,themes/IPB/views/discussions/_discussion_item.php,languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.Icon picker: FA 7 Free style filter (Solid / Regular / Brands) — all 548 brand icons — The shared icon-picker component now exposes three style toggle buttons above the search bar. Solid (default) and Regular each load only the icons of the matching prefix (
fa-solid/fa-regular) from the existing 20 category grids, making each tab significantly less cluttered. Brands hides the category tabs entirely and shows a dedicated flat grid with all 548 FA 7.2 Free brand icons (sourced directly frombrands.min.js; the CSS webfont already contains all glyphs). Style state is tracked per modal via_pickerState, resets to Solid on close. Live search is style-aware: it filters within the active style and loads previously un-rendered matches on the fly.purgeInvalidIconsremoves any icon whose glyph doesn't render. Style button clicks are handled via a document-level delegated listener, ensuring they work regardless of modal initialisation order. Three new translation keys (icons.style.solid,icons.style.regular,icons.style.brands) added to all five language files.
Files changed:app/Views/components/icon-picker.php,themes/assets/js/admin/components/icon-picker.js,languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.
Fixed
Admin — Tags: colonnes
coloreticonmanquantes en SQLite — Les installations SQLite existantes pouvaient être dépourvues des colonnescoloreticondans la tabletagscar elles avaient été ajoutées à la définitionCREATE TABLEsans migrationALTER TABLE. La mise à jour d'un tag échouait avecSQLSTATE[HY000]: General error: 1 no such column: icon(et potentiellementcolorsur les installations les plus anciennes). Le bloc de migration au démarrage vérifie désormais les trois colonnes ajoutées après la création initiale de la table (updated_at,color,icon) et exécute leALTER TABLEcorrespondant si nécessaire. Les colonnescolor TEXTeticon TEXTsont également présentes dans la définitionCREATE TABLE IF NOT EXISTSpour les nouvelles installations.
Files changed:app/Storage/SqliteStorage.php.Admin — Tags: icon picker closing the edit modal — The icon picker trigger button inside
renameTagModaluseddata-bs-toggle="modal", which caused Bootstrap 5's data-api click handler to close all currently open modals before showing the icon picker — the edit modal was dismissed silently before the icon picker appeared. The fix removesdata-bs-toggle="modal"from the trigger button so Bootstrap's delegated handler is never involved, and replaces the nested-modal approach with a swap pattern in JS: clicking the button hides the edit modal, waits forhidden.bs.modal, then shows the icon picker; when the icon picker fireshidden.bs.modal, the edit modal is re-shown with form values (name, colour, icon preview) intact.
Files changed:app/Views/admin/tags.php.
🚀 Changelog — Flatboard 5.3.3
Release date: March 24, 2026
Changed
Permissions — File-type permissions (
attachment.upload,attachment.view,attachment.download,image.upload,avatar.upload) centralised in the Attachments section —image.uploadandavatar.uploadwere previously routed to the catch-all "Other" section because the controller only testedstartsWith('attachment.'). The routing condition now also matchesstartsWith('image.')andstartsWith('avatar.'), so all five file-related permissions appear together under "Attachments" inadmin/permissions. The standalone "File settings" tab has been removed; its type/size grid is now embedded directly in the same "Media & Attachments" tab, immediately below the permission table, giving a single place to manage both who can upload and what formats are accepted.
Files changed:app/Controllers/Admin/PermissionController.php,app/Views/admin/permissions.php,themes/assets/js/admin/modules/permissions-management.js.Permissions — Tab layout reorganised — The "Attachments, Social & Presence" tab has been split into two dedicated tabs. "Social & Presence" (inserted before "Media & Attachments") groups the Social, Presence, Users, and Other permission sections. "Media & Attachments" contains only file-upload permissions and the type/size settings grid. Tab labels and icons updated accordingly.
Files changed:app/Views/admin/permissions.php,languages/fr/admin.json,languages/en/admin.json.
Fixed
Permissions — API token "copy" popup firing repeatedly without user action — After generating an API token in user settings, the
Toast.successnotification triggered bysecurity.apiToken.generatedwas displayed simultaneously with the inlinealert-warningbanner already visible in the form. Both messages carried the same "copy this token, it will never be shown again" wording, making it appear as though the popup was firing multiple times unprompted. TheToast.successcall after token generation has been removed; the persistent inline banner is sufficient and remains visible until the user navigates away.
Files changed:app/Views/users/settings.php.Permissions — File settings save returning 400 error when all checkboxes unchecked —
FileSettingsValidator::validate()applied therequiredrule to each*_typesarray field. Therequiredvalidator rejects an empty array, so saving the file-settings form with zero types selected in any section (the intended way to block all uploads for a given format) always triggered a validation error. The rule has been changed fromrequired|arraytoarrayalone, making an empty selection a valid state meaning "no file type allowed".
Files changed:app/Helpers/FileSettingsValidator.php.i18n —
settings.file.types_and_sizeskey missing from all language files — The translation key introduced in the file-settings section header was never added to the JSON language files, causing the raw fallback stringTypes & tailles autorisésto be displayed in all locales. The key is now present in all five supported languages (fr,en,de,pt,zh).
Files changed:languages/fr/admin.json,languages/en/admin.json,languages/de/admin.json,languages/pt/admin.json,languages/zh/admin.json.i18n — French permission labels for upload actions used "Télécharger" instead of "Uploader" —
attachment_upload,image_upload, andavatar_uploadinlanguages/fr/admin.jsonhad theirnameanddescfields translated with the verb "Télécharger" (download), causing confusion with the distinct "download" permissions that legitimately use the same word. Labels corrected to "Uploader".
Files changed:languages/fr/admin.json.Guest permissions — Guests always redirected to login despite
discussion.create/post.createbeing granted —DiscussionController::create(),DiscussionController::store(), andPostController::store()calledrequireAuth()as their very first instruction, unconditionally redirecting unauthenticated visitors to the login page before any permission check could run. As a result, grantingdiscussion.createorpost.createto the Guests group inadmin/permissionshad no effect whatsoever. The fix replacesrequireAuth()withPermissionHelper::canGuest(), which already handles both connected users (via their group) and guests (via the Guest group). If the guest permission is not granted, unauthenticated visitors are redirected to/login; authenticated users without the permission receive a 403. The Guest group default permissions are unchanged — opening posting to guests still requires an explicit admin action.
Files changed:app/Controllers/Discussion/DiscussionController.php,app/Controllers/Discussion/PostController.php.Category drag-and-drop reorder broken — static in-memory cache caused each update to overwrite previous ones —
CategoryController::reorder()iterated over the ordered IDs and calledCategory::update()for each one.Category::update()delegates toJsonStorage::updateCategory(), which reads the fullcategories.jsonviagetAllCategories(). That method populates a static propertyself::$categoriesCacheon first call and returns it on subsequent calls without re-reading the file. As a result, every iteration of the loop read the original pre-reorder state from memory, merged only its ownorder_indexchange, and wrote the full array back — silently discarding theorder_indexvalues written by all previous iterations. Groups were unaffected becauseupdateGroup()usesupdateGeneric(), which reads directly from disk without a static cache. The fix introduces a dedicatedreorderCategories(array $orderedIds): boolmethod in bothJsonStorageandSqliteStorage. InJsonStorage, it acquires the file lock once, applies allorder_indexvalues in a single pass, writes the file once, then clearsself::$categoriesCacheand invalidates the file-based cache. InSqliteStorage, it wraps all updates in a single transaction.CategoryController::reorder()now calls$storage->reorderCategories()directly, followed byCacheInvalidator::invalidateCategories().
Files changed:app/Controllers/Admin/CategoryController.php,app/Storage/JsonStorage.php,app/Storage/SqliteStorage.php.Rate limiter — Administrators blocked by their own forum when testing extensively —
RateLimitMiddleware::handle()identified users byuser_idwhen logged in, or by IP when anonymous. Because the rate limiter incremented the counter on every request regardless of role, an admin navigating intensively (or switching to a private browsing window where the session is lost and the IP is the sole identifier) could exhaust the configured thresholds and be blocked. The fix adds an early-return guard at the top ofhandle(), before the limiter is instantiated and before any counter is incremented: ifSession::get('is_admin') === true, the request is unconditionally allowed through. Theis_adminflag is already set in session byLoginControllerat authentication time, so no additional database call is required. Middlewares that only apply to unauthenticated flows (login,register,password_reset) are unaffected becauseis_adminisfalseornullin those contexts, preserving all brute-force protections.
Files changed:app/Middleware/RateLimitMiddleware.php.Permissions — Plugin permissions not visible in
admin/permissionsafter activation — Eight plugins declared their permission keys in_permissionsinsideplugin.jsonbut did not implement theregisterPermissions()method required byPermissionHelper::registerPluginPermissions(). Without this method, the activation flow produced an empty$pluginPermissionsarray, nothing was written to the permission storage, and the permissions never appeared in the admin panel.registerPermissions()has been added to all affected plugins with sensible default group assignments (admin/moderator/member where appropriate): Flatbot (flatbot.use,flatbot.history), FlatModerationExtend (access), ForumMonitoring (monitoring.view), Impersonate (impersonate), InactiveUserManager (inactiveusers.view,inactiveusers.delete), PrivateMessaging (pm.send,pm.receive,pm.moderate,pm.delete_own), ResourceManager (resource.view,resource.download,resource.rate,resource.submit,resource.manage,resource.moderate), TranslationManager (edit,create_language,import_export,manage_members,settings). To apply the fix on an existing installation, deactivate and reactivate each affected plugin fromadmin/plugins.
Files changed:plugins/FlatModerationExtend/FlatModerationExtendPlugin.php,plugins/ForumMonitoring/ForumMonitoringPlugin.php,plugins/PrivateMessaging/PrivateMessagingPlugin.php.Plugin install/update —
plugin.jsonoverwritten instead of merged on manual ZIP upload — When installing or updating a plugin via the admin ZIP upload interface, both extraction paths (flat archive and archive with a root folder) unconditionally overwrote the existingplugin.json. This caused theactivestate and all user-configured plugin settings stored under thepluginkey to be reset to the archive defaults on every update. The fix introduces amergePluginJson()helper inPluginController: for flat archives, the helper is called per-file beforefile_put_contents(); for folder archives, the existingplugin.jsonis read beforeZipArchive::extractTo()and the merge is applied immediately after. Merge strategy: structural keys (version,hooks,form_config,_permissions, etc.) are always taken from the incoming archive; user-state keys (active,plugin) are preserved from the installed version. On a fresh install where noplugin.jsonexists yet, the behaviour is unchanged.
Files changed:app/Controllers/Admin/PluginController.php.
🚀 Changelog — Flatboard 5.3.2
Release date: March 22, 2026
Fixed
- Bootswatch theme — Saved sub-theme not reflected in frontend
theme-config, neumorphic shadows lost, CSS variable overrides ignored due to specificity, and button/card element styles overridden by default theme CSS — Four independent bugs prevented the admin-saved Bootswatch sub-theme (e.g. Morph) from fully applying on the frontend. (1)header.phpbuilt$configToInjectwitharray_merge($configToInject, $themeData['theme_settings']), so the hardcodedtheme_settingsdefaults (always "journal") silently overwrote the savedsettingsvalue; the<script id="theme-config">therefore always reported "journal" regardless of what was saved. Fixed by reversing the merge order sosettingswins. (2)bootswatch-colors.phprestored Bootstrap color variables overridden byfrontend.cssbut never restored shadow variables —frontend.cssredefines--bs-box-shadow,--bs-box-shadow-sm,--bs-box-shadow-lgwith standard Bootstrap values after Morph's CSS loads, wiping its neumorphic double-shadow. Fixed by parsing--bs-box-shadow*from the active Bootswatch CSS and emitting them inside the#bootswatch-varsblock. (3)bootswatch-colors.phpemitted its variables under:root(specificity 0,1,0), buttheme-initializer.jssynchronously setsdata-theme="light"on<html>before page render, activatingfrontend.css'shtml[data-theme=light]rules (specificity 0,1,1) which silently overrode the restored Bootswatch variables — making the sub-theme appear identical to the default Bootstrap theme in the frontend (while the backend, which usesdata-bs-themenotdata-theme, was unaffected). Fixed by emitting variables under:root, html[data-theme=light], html:not([data-theme])(light themes) or:root, html[data-theme=dark], html:not([data-theme])(dark themes like Cyborg), giving the inline style equal specificity (0,1,1) and winning via document order. (4)frontend.css(default theme) applies element-level rules that conflict with Bootswatch visual styles:.btn-primaryand.btn-secondaryreceive abackground: linear-gradient()shorthand that discards Bootstrap's component CSS variables (--bs-btn-bg,--bs-btn-hover-bg) which Bootswatch themes set for their own button colours;.card:hovergetstransform: translateY(-2px)andbox-shadow: var(--shadow-md)(a flat Flatboard shadow unrelated to the active theme). Fixed by addingbackground-image: noneoverrides for both button variants and resetting.card:hovertobox-shadow: var(--bs-box-shadow-sm); transform: noneinfrontend-bootswatch-minimal.css, allowing Bootswatch's native button and card styles to take effect. Additionally,.btn-outline-primaryand.btn-outline-secondaryhad theircolorandborder-colorforced tovar(--bs-primary)/var(--bs-secondary)and their hover state tocolor: white— invisible for Bootswatch themes whose secondary is a light or brand colour. Fixed with the same component-variable approach (--bs-btn-color,--bs-btn-border-color,--bs-btn-hover-color, etc.). Footer<a>links were also invisible on themes where--bs-link-coloris a dark colour (e.g. Brite:#000) and the footer uses a dark--bg-darkbackground:frontend.csssetscolor: var(--text-light)onfooterelements but not on<a>descendants, so Bootstrap's link colour overrides the intended light text. Fixed by addingfooter a { color: inherit }so links inherit the light footer text colour.
Files changed:themes/bootswatch/views/layouts/frontend/header.php,themes/bootswatch/views/components/bootswatch-colors.php,themes/bootswatch/assets/css/frontend-bootswatch-minimal.css.
Added (Pro)
FlatHome — Schema.org
SoftwareApplicationJSON-LD on homepage — ASoftwareApplicationstructured data block is now injected into<head>on the homepage via theview.header.styleshook. Fields:name(site name from config),applicationCategory: CommunicationApplication,operatingSystem: Linux/Windows/macOS (PHP 8.1+),url,softwareVersion(fromFLATBOARD_VERSION),description(from config), and a freeOffer(price: 0). Enables Google to display software metadata (category, price, version) directly in search results and qualifies the page for rich results in the Software Application Rich Results Test.
Files changed:plugins/FlatHome/FlatHomePlugin.php.FlatHome — Blog RSS/Atom feed auto-discovery on homepage — When FlatHome is in CMS or blog mode and a blog category is configured, the frontend
<head>now includes<link rel="alternate">tags for the blog RSS and Atom feeds on the homepage (/). Previously the homepage only got the global forum feed from SeoHelper; blog content was undiscoverable by feed readers and search engines. Files changed:plugins/FlatHome/FlatHomePlugin.php.FlatHome — Homepage SEO improvements: semantic headings, machine-readable dates, last-reply author, and JSON-LD structured data — (1) Blog card titles changed from plain
<a>to<h3><a>for semantic heading hierarchy. (2) All dates (blog posts and forum discussions) now use<time datetime="ISO8601">for machine-readable markup. (3) Forum discussion rows now show both the creation date and the last-reply date (including last-reply author username), matching the parity of the forum index page. (4) JSON-LDItemListblocks added at the end of the homepage template: one for the blog section (BlogPostingitems withdatePublishedanddescription) and one for the forum section (DiscussionForumPostingitems withdatePublished,dateModifiedwhen a reply exists, andauthor). Files changed:plugins/FlatHome/modeles/home.php.
Added
Local update — Automatic maintenance mode during update — When the local update workflow starts (
verifystep), maintenance mode is now enabled automatically so visitors cannot access the forum while files are being replaced. The previous state of maintenance mode is saved in the update session; once thecleanupstep completes (archive deleted), maintenance mode is restored to its previous state — meaning it is only disabled if it was not already active before the update began. If any step fails, maintenance mode is also restored immediately via the error path. Files changed:app/Controllers/Admin/UpdateController.php.API Token UI — Generate and revoke Bearer tokens from the user profile — The
ApiTokenControllerwas fully implemented but not connected to any route or interface. Users can now generate and revoke their API token from Profile → Settings → Security → API Token. The plain token is displayed once upon generation with a copy button; only its SHA-256 hash is stored. Routes/settings/api-token/generateand/settings/api-token/revokeare now registered with CSRF middleware. Translation keys added in all 5 languages (fr, en, de, pt, zh).
Files changed:app/Core/App.php,app/Views/users/settings.php,languages/fr/auth.json,languages/en/auth.json,languages/de/auth.json,languages/pt/auth.json,languages/zh/auth.json.
Fixed
JS — Migrated all
window.showToastcalls towindow.Toastwith translated strings — Nine JS/PHP files were still using the deprecatedwindow.showToastglobal (a non-existent alias) with hardcoded French strings. All occurrences replaced withwindow.Toast.success/error/warning/info()andwindow.__()for i18n. 8 missing translation keys added to all 5 languages (discussion.view.postUpdated,discussion.view.postDeleted,discussion.view.contentNotFound,discussion.draft.savedShort,discussion.draft.restored,discussion.create.validationError,discussion.create.form.maxTagsReached,discussion.search.minQueryLength). Dynamic strings with variables ({seconds},{min},{max}) use existing keysrateLimit.retryAfterandvalidation.length.minfromerrors.json.
Files changed:themes/assets/js/frontend/modules/load-more-manager.js,discussion-show-manager.js,post-edit-manager.js,discussion-form-manager.js,tag-manager.js,search-form-manager.js,themes/assets/js/frontend/components/infinite-scroll.js,app/Views/discussions/edit.php,themes/premium/views/discussions/edit.php,languages/{fr,en,de,pt,zh}/main.json.Toast — Titles were hardcoded fallbacks instead of translated strings —
toast.jsresolved toast type titles (success,error,warning,info) viawindow.Translations?.main?.successwhich does not exist at the root ofmain.json. The correct path iscommon.message.success/error/warning/info. Updated both occurrences (Bootstrap and fallback paths) intoast.js.
Files changed:themes/assets/js/shared/toast.js.Toast — Copy token notification displayed the translation key — The API token copy button showed
common.share.copiedas the toast body because (1) the PHPTranslator::trans()call was resolving to the key and (2) the key path was wrong. Replaced withwindow.__('discussion.share.copied'), the correct runtime JS resolver and path.
Files changed:app/Views/users/settings.php.Security — All-discussions view exposed posts from restricted categories —
/api/discussionsloaded the full discussion list and passed it to the enrichment/sort/paginate pipeline without ever checking category permissions. Any user (including unauthenticated guests) could retrieve discussions from categories restricted to specific groups. Additionally, requesting/api/discussions?category_id=…for a restricted category returned its discussions without verifying access. Fixed by resolving$userIdbefore loading discussions, callingfilterByPermissions()(pre-cachesCategory::canView()per unique category ID) immediately after loading, and checkingCategory::canView()upfront for the single-category code path.
Files changed:app/Controllers/Api/DiscussionApiController.php.
🚀 Changelog — Flatboard 5.3.1 — Lighthouse
Release date: March 20, 2026
Changed
- Permissions — Legacy double-prefix key mapping removed — The
$legacyKeyMapmigration shim introduced in 5.2.5 to rename double-prefixed permission keys (e.g.plugin.polls.polls.view→plugin.polls.polls_view) has been removed along with the now-dead$renamedPluginKeysvariable and themigrateGroupPermissionKeys()helper. All existing forums have gone through at least oneinitDefaults()cycle since 5.2.5 and carry the corrected keys; the shim is no longer needed.
Files changed:app/Helpers/PermissionHelper.php.
Fixed
Bootswatch theme —
POST /bootswatch/change-themereturned 404 when admin has a ThemeSwitcher personal preference — When an admin user had a ThemeSwitcher personal theme preference (e.g.premium), posting to/bootswatch/change-themetriggeredapplyThemePreference()(since the URL was not matched byisAdminRequest()), which patchedConfig::theme.activetopremiumin-memory. App.php then loadedthemes/premium/boot.phpinstead ofthemes/bootswatch/boot.php, so the route was never registered and the request returned 404. The route is now registered under/admin/bootswatch/change-theme; the/admin/prefix causesisAdminRequest()to return true, ThemeSwitcher skips the preference override, Config staysbootswatch, and the route resolves correctly.
Files changed:themes/bootswatch/boot.php,themes/bootswatch/views/admin/bootswatch.php,themes/bootswatch/views/components/theme-selector.php.EasyMDE / TUIEditor — Custom toolbar configuration overwritten by plugin defaults — Two bugs caused saved toolbar configurations to be silently lost. First,
prepareAdminViewVars()calledPlugin::getData()(which already returns thepluginsection) and then accessed["plugin"]on the result, always obtaining an empty array; the admin settings page therefore always rendered the plugin.json default toolbars instead of the saved ones, overwriting any custom configuration on the next save. Second,getToolbarForContext()guarded saved toolbar entries with!empty(), causing an explicitly-cleared toolbar ([]) to fall through to the plugin.json defaults rather than being honoured. Both conditions are fixed: the double-nesting is removed and!empty()is replaced withisset(). The same two bugs were found and fixed inTUIEditorPlugin.php.
Files changed:plugins/EasyMDE/EasyMDEPlugin.php,plugins/TUIEditor/TUIEditorPlugin.php.Security — XSS escaping gaps in three views — Three output points were missing
htmlspecialchars():tag-input.phpoutput$tag['id']raw in adata-tag-idattribute;theme-config.phpoutput$groupData['icon'](a Font Awesome class string) raw in a class attribute;webhooks-history.phpoutput$item['http_code']raw inside a<code>element. Fixed withhtmlspecialchars()(string values) and an(int)cast (numeric HTTP code).
Files changed:app/Views/components/tag-input.php,app/Views/admin/theme-config.php,app/Views/admin/webhooks-history.php.FlatModerationExtend — Approved discussion shows only title (SQLite) — When pre-moderation was enabled and the storage backend was SQLite, approving a pending discussion created the discussion record but not its first post. Since SQLite stores post content in a separate
poststable and the discussion view reads content viaPost::getFirstPost(), the approved discussion appeared with only its title and an empty body.premoderationApprove()now creates the first post (is_first_post: true) with pre-rendered Markdown immediately afterDiscussion::create(), matching the behaviour of the normal discussion creation flow.
Files changed:plugins/FlatModerationExtend/FlatModerationExtendController.php.Security — HTTP security header improvements —
X-XSS-Protectionchanged from1; mode=blockto0: the reflective-XSS filter it controls was removed from Chrome/Firefox/Safari and itsblockmode was known to cause false-positive page blanking on IE11, making0the current OWASP recommendation.upgrade-insecure-requestsadded to theContent-Security-Policy: instructs browsers to silently rewrite anyhttp://sub-resource request tohttps://before sending it, eliminating mixed-content warnings without a server-side redirect.
Files changed:app/Core/App.php.FlatHome — Recent discussions sorted by last activity —
getRecentForumDiscussions()now sorts bylast_post_at(falling back toupdated_atthencreated_at) instead ofcreated_at, so discussions with recent replies rise to the top. The display timestamp in thehomeandflatboardtemplates is updated accordingly.
Files changed:plugins/FlatHome/FlatHomeService.php,plugins/FlatHome/modeles/home.php,plugins/FlatHome/modeles/flatboard.php.Solved tag name hardcoded in French — When marking a discussion as solved, the auto-created "solved" tag was always named
Résoluregardless of the site language. The name now usesTranslator::trans('discussion.solved', [], 'main'), which resolves to the correct translation for the active site language. Existing tags namedRésolumust be renamed manually from the tag admin panel.
Files changed:app/Controllers/Discussion/DiscussionController.php.Security — Upload MIME type spoofing in
UploadController::upload()— The legacyupload()endpoint validated file type using$_FILES['type'], which is supplied by the browser and can be trivially forged. The check is replaced with server-side detection viafinfo_file()(falling back tomime_content_type()), and a second guard ensures the file's declared extension matches the detected MIME type. The three other upload endpoints (uploadAvatar,uploadImage,uploadAttachment) already delegated toUploadServiceand were unaffected.
Files changed:app/Controllers/UploadController.php.FlatHome — Recent discussions respect category permissions —
getRecentForumDiscussions()now loads the full discussion dataset (Discussion::all(null, 0)) and filters each entry throughCategory::canView(), so discussions from restricted categories are never shown to users who lack access. Previously the function fetched a fixed batch of$limit × 4entries, which could be exhausted by blog-category posts before reaching enough visible forum discussions.
Files changed:plugins/FlatHome/FlatHomeService.php.