Flatboard 5.4.0 - Changelog

Avatar
Posts 428 1580

🚀 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 formattingDateHelper::format() and DateHelper::human() now replace the F (month long), M (month short), l (day long), and D (day short) PHP format specifiers with translated names read from the active language file, instead of relying on PHP's English-only date() output. All five language files (fr, en, de, pt, zh) gain a new datetime top-level section providing months_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 editsFlatHomeService::renderPageContent() was reading $discussion['content'] (the initial snapshot stored in discussion.json) instead of the actual first-post content. It now calls Post::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's updated_at (or created_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 abbreviationmodeles/home.php built the card date with date('M j, Y', ...) which always produced English names (e.g. "Jan 1, 2026"). Replaced with DateHelper::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 (D specifier) and the per-user activity bars (D d/m format) were produced with raw date() calls and always returned English names (Mon, Tue…). Both now go through DateHelper::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 with DateHelper::format().
    Files changed: plugins/PrivateMessaging/views/admin.php.

  • Router: /admin/plugins/{plugin_id}/admin route delivered null as the $view parameter — The route was declared with a literal /admin suffix, so PluginViewController::show() always received $view = null and 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.view was restricted for guests, UserController::show() returned a 403 for both unauthenticated visitors and logged-in members without permission. Guests are now redirected to /login with an explanatory toast (permission.profileLoginRequired) via handlePermissionDenied(..., 401), while authenticated members without the permission still receive a proper 403. Translation key profileLoginRequired added 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 functionsApp::__construct() now calls date_default_timezone_set() immediately after loading the configuration, using the admin-configured timezone value (default Europe/Paris). This ensures all date() / time() calls throughout the application use the correct timezone rather than the PHP server default. DateHelper::format() and DateHelper::human() also construct their DateTime objects with this timezone explicitly, making today/yesterday boundaries in human() 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() and atomCategory() now resolve the slug to a category ID via Category::findBySlug() before delegating to RssService, 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 sitemapFlatSEOSitemap::getDiscussionUrls() and getCategoryUrls() did not check allowed_groups on 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-empty allowed_groups. Categories are pre-loaded in a single batch to avoid N+1 queries. Additionally, FlatSEOService::invalidateSitemapCache() was not deleting the flatseo:sitemap_main cache 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 numbergetDiscussionUrls() 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 contextapp/Cli/console.php now checks PHP_SAPI !== 'cli' at the very top and exits with HTTP 403 immediately if the script is reached through a web request. Although app/ is already blocked at the web-server level (Apache .htaccess and nginx.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 handleDeleteBackup method in backups-management.js now 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 $silent parameter — Passing $silent = true suppresses the logRateLimitHit() 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/permissions rendered its Badges and Matrix views with its own inline loop that bypassed PermissionSection entirely. 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 / $isLocked logic used by all other tabs: locked cells display a dimmed <span> with fa-lock in Badges view and a perm-mx-locked cell 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 new UpdateStatsService::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 the toast.js fallback scanner and shown as toasts on every page load. data-toast="none" is now set on these elements to exclude them from the scanner. A pageshow handler 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). A shown.bs.tab listener 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: version and date not updated in plugin.json / theme.json — The smart-merge strategy used when deploying update archives preserved all existing scalar values, including metadata fields such as version, date, update_url, and description. These fields are now explicitly overwritten from the archive before the merge, while user-configured values (plugin settings, active flag, 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-info banner. 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 checksfindCACert() now queries curl.cainfo and openssl.cafile from php.ini first (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-cacert command — New CLI command that downloads the latest Mozilla CA bundle from curl.se and overwrites stockage/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 renewalUpdateStatsService::scheduleCacertRenewalIfDue() is invoked on every request via App::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_url in their plugin.json / theme.json. Each update_url endpoint must return {"version": "x.y.z", "changelog_url": "..."}. Results are cached for 1 hour. The /admin/updates page 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 an update_url pointing to https://versions.flatboard.org/api/plugins/{id} and https://versions.flatboard.org/api/themes/premium respectively. 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 across UpdateController and independently re-implemented in install.php and Shortcodes/ShortcodesRegistry.php have been extracted into a new App\Services\UpdateStatsService class 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 of UpdateStatsService::findCACert(). The writeAtomicJson() wrapper function in install.php has 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_url carrying the same statistics payload as a regular update check, plus "install": true to 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. Both handleTimeouts() and isExpired() now apply REMEMBER_ME_LIFETIME (30 days) as an absolute ceiling while still skipping the idle timeout.
    Files changed: app/Core/Session.php.

  • IP spoofing via proxy headersSession::getClientIp() and Request::getIp() previously trusted X-Forwarded-For, X-Real-IP, and CF-Connecting-IP unconditionally. A new IpHelper class reads those headers only when REMOTE_ADDR belongs to the configured security.trusted_proxies list (supports individual IPs and CIDR ranges, IPv4 and IPv6). Default is an empty list (safe by default). Configured via config.json key security.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, default 3600 — 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 debug is 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() stamps permissions_version = time() on group change; LoginController stores this value in the session at login; AuthMiddleware compares 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-inline and unsafe-eval removed from script-src — The Content-Security-Policy header previously included both 'unsafe-inline' and 'unsafe-eval' in script-src/script-src-elem, nullifying XSS protection. Two changes eliminate these directives. (1) Nonces: a new CspNonce class generates a cryptographically random nonce per request via random_bytes(16); the nonce is embedded in script-src as 'nonce-{nonce}'; an output-buffer callback in App::run() automatically injects nonce="{nonce}" into every <script> tag — no view or plugin needs modification. (2) unsafe-eval: the sole actual caller was EasyMDEPlugin, which serialised the image-upload handler as a PHP string and reconstructed it client-side via new Function(). The string is removed; easymde-init.js already contains an identical built-in handler that reads editorConfig.csrfToken directly — no behaviour change. The webpack new Function("return this") polyfill in TUI Editor is never executed on modern browsers (globalThis is 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, oninvalid inline attributes across the core and all packaged plugins violated the script-src-attr CSP directive. Each has been replaced: image fallbacks use data-fallback (captured error event); form submission, navigation, text selection, color-sync, range-display, and custom-validation patterns use delegated handlers registered once in frontend-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.) use data-* 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 five install.json language files and the block now uses t() 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-Cookie header conflict: session_start() (called at the top of every request) emits a session-only Set-Cookie; the subsequent manual setcookie() 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 manual setcookie() calls in both login() and verify2FA() with Session::regenerate(), which calls session_regenerate_id() then extendRememberMeCookie() — producing a single, clean Set-Cookie carrying 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 (views field, 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 the show_views setting. The views/page.php article 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. The injectBannerContent() method now fetches settings and passes $showViews / $pageViews to 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.items when another plugin injects an array itemFlatHomePlugin::addNavItems() iterated over $items and passed each element directly to parseNavHtml(string $html). Plugins such as Impersonate inject array items (structured data) rather than HTML strings into this hook, triggering a fatal TypeError. The fix collects non-string items in a separate list before parsing, skips them in parseNavHtml, 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.5rem with a body-background outline ring for visual separation. Only the first 3 tags are shown inline; a +N dashed pill badge (rendered as a <button>) indicates the count. Clicking the badge opens a .flatboard-tags-overflow popup card positioned absolute above 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: 10rem with ellipsis). Dark mode support via [data-bs-theme="dark"] overrides. Applied to all four discussion-item partials, all five frontend CSS files, and main.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 UXTagController::index() previously issued one countDiscussionsByTag() call per tag (N+1 pattern). A new countAllDiscussionsByTag() method in StorageInterface, JsonStorage (single glob scan), and SqliteStorage (single GROUP BY query) reduces total I/O to two operations. The update() action now accepts a color field (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 in DOMContentLoaded to avoid the bootstrap is not defined crash. Tag colours are now applied as inline background-color / color / border-color styles in all three theme discussion-item partials (premium, ClassicForum, IPB) and on the public /tags page, using the WCAG relative-luminance formula for contrast text colour. All five language files receive the panel.tags.search, panel.tags.pagination, and panel.tags.rename.color_label keys.
    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-tag dialog (reusing the existing icon-picker.php component without any JS changes). The TagController::update() action validates the submitted icon against the FontAwesome class format regex and stores it on the tag; the AJAX response includes a new_icon field 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 hardcoded fas fa-tag fallback: the three discussion-item partials (premium, ClassicForum, IPB), the default fallback partial, and both /tags page 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 from brands.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. purgeInvalidIcons removes 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 color et icon manquantes en SQLite — Les installations SQLite existantes pouvaient être dépourvues des colonnes color et icon dans la table tags car elles avaient été ajoutées à la définition CREATE TABLE sans migration ALTER TABLE. La mise à jour d'un tag échouait avec SQLSTATE[HY000]: General error: 1 no such column: icon (et potentiellement color sur 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 le ALTER TABLE correspondant si nécessaire. Les colonnes color TEXT et icon TEXT sont également présentes dans la définition CREATE TABLE IF NOT EXISTS pour les nouvelles installations.
    Files changed: app/Storage/SqliteStorage.php.

  • Admin — Tags: icon picker closing the edit modal — The icon picker trigger button inside renameTagModal used data-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 removes data-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 for hidden.bs.modal, then shows the icon picker; when the icon picker fires hidden.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 sectionimage.upload and avatar.upload were previously routed to the catch-all "Other" section because the controller only tested startsWith('attachment.'). The routing condition now also matches startsWith('image.') and startsWith('avatar.'), so all five file-related permissions appear together under "Attachments" in admin/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.success notification triggered by security.apiToken.generated was displayed simultaneously with the inline alert-warning banner 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. The Toast.success call 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 uncheckedFileSettingsValidator::validate() applied the required rule to each *_types array field. The required validator 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 from required|array to array alone, making an empty selection a valid state meaning "no file type allowed".
    Files changed: app/Helpers/FileSettingsValidator.php.

  • i18n — settings.file.types_and_sizes key 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 string Types & tailles autorisés to 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, and avatar_upload in languages/fr/admin.json had their name and desc fields 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.create being grantedDiscussionController::create(), DiscussionController::store(), and PostController::store() called requireAuth() as their very first instruction, unconditionally redirecting unauthenticated visitors to the login page before any permission check could run. As a result, granting discussion.create or post.create to the Guests group in admin/permissions had no effect whatsoever. The fix replaces requireAuth() with PermissionHelper::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 onesCategoryController::reorder() iterated over the ordered IDs and called Category::update() for each one. Category::update() delegates to JsonStorage::updateCategory(), which reads the full categories.json via getAllCategories(). That method populates a static property self::$categoriesCache on 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 own order_index change, and wrote the full array back — silently discarding the order_index values written by all previous iterations. Groups were unaffected because updateGroup() uses updateGeneric(), which reads directly from disk without a static cache. The fix introduces a dedicated reorderCategories(array $orderedIds): bool method in both JsonStorage and SqliteStorage. In JsonStorage, it acquires the file lock once, applies all order_index values in a single pass, writes the file once, then clears self::$categoriesCache and invalidates the file-based cache. In SqliteStorage, it wraps all updates in a single transaction. CategoryController::reorder() now calls $storage->reorderCategories() directly, followed by CacheInvalidator::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 extensivelyRateLimitMiddleware::handle() identified users by user_id when 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 of handle(), before the limiter is instantiated and before any counter is incremented: if Session::get('is_admin') === true, the request is unconditionally allowed through. The is_admin flag is already set in session by LoginController at authentication time, so no additional database call is required. Middlewares that only apply to unauthenticated flows (login, register, password_reset) are unaffected because is_admin is false or null in those contexts, preserving all brute-force protections.
    Files changed: app/Middleware/RateLimitMiddleware.php.

  • Permissions — Plugin permissions not visible in admin/permissions after activation — Eight plugins declared their permission keys in _permissions inside plugin.json but did not implement the registerPermissions() method required by PermissionHelper::registerPluginPermissions(). Without this method, the activation flow produced an empty $pluginPermissions array, 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 from admin/plugins.
    Files changed: plugins/FlatModerationExtend/FlatModerationExtendPlugin.php, plugins/ForumMonitoring/ForumMonitoringPlugin.php, plugins/PrivateMessaging/PrivateMessagingPlugin.php.

  • Plugin install/update — plugin.json overwritten 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 existing plugin.json. This caused the active state and all user-configured plugin settings stored under the plugin key to be reset to the archive defaults on every update. The fix introduces a mergePluginJson() helper in PluginController: for flat archives, the helper is called per-file before file_put_contents(); for folder archives, the existing plugin.json is read before ZipArchive::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 no plugin.json exists 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.php built $configToInject with array_merge($configToInject, $themeData['theme_settings']), so the hardcoded theme_settings defaults (always "journal") silently overwrote the saved settings value; the <script id="theme-config"> therefore always reported "journal" regardless of what was saved. Fixed by reversing the merge order so settings wins. (2) bootswatch-colors.php restored Bootstrap color variables overridden by frontend.css but never restored shadow variables — frontend.css redefines --bs-box-shadow, --bs-box-shadow-sm, --bs-box-shadow-lg with 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-vars block. (3) bootswatch-colors.php emitted its variables under :root (specificity 0,1,0), but theme-initializer.js synchronously sets data-theme="light" on <html> before page render, activating frontend.css's html[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 uses data-bs-theme not data-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-primary and .btn-secondary receive a background: 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:hover gets transform: translateY(-2px) and box-shadow: var(--shadow-md) (a flat Flatboard shadow unrelated to the active theme). Fixed by adding background-image: none overrides for both button variants and resetting .card:hover to box-shadow: var(--bs-box-shadow-sm); transform: none in frontend-bootswatch-minimal.css, allowing Bootswatch's native button and card styles to take effect. Additionally, .btn-outline-primary and .btn-outline-secondary had their color and border-color forced to var(--bs-primary) / var(--bs-secondary) and their hover state to color: 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-color is a dark colour (e.g. Brite: #000) and the footer uses a dark --bg-dark background: frontend.css sets color: var(--text-light) on footer elements but not on <a> descendants, so Bootstrap's link colour overrides the intended light text. Fixed by adding footer 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 SoftwareApplication JSON-LD on homepage — A SoftwareApplication structured data block is now injected into <head> on the homepage via the view.header.styles hook. Fields: name (site name from config), applicationCategory: CommunicationApplication, operatingSystem: Linux/Windows/macOS (PHP 8.1+), url, softwareVersion (from FLATBOARD_VERSION), description (from config), and a free Offer (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-LD ItemList blocks added at the end of the homepage template: one for the blog section (BlogPosting items with datePublished and description) and one for the forum section (DiscussionForumPosting items with datePublished, dateModified when a reply exists, and author). Files changed: plugins/FlatHome/modeles/home.php.

Added

  • Local update — Automatic maintenance mode during update — When the local update workflow starts (verify step), 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 the cleanup step 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 ApiTokenController was 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/generate and /settings/api-token/revoke are 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.showToast calls to window.Toast with translated strings — Nine JS/PHP files were still using the deprecated window.showToast global (a non-existent alias) with hardcoded French strings. All occurrences replaced with window.Toast.success/error/warning/info() and window.__() 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 keys rateLimit.retryAfter and validation.length.min from errors.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 stringstoast.js resolved toast type titles (success, error, warning, info) via window.Translations?.main?.success which does not exist at the root of main.json. The correct path is common.message.success/error/warning/info. Updated both occurrences (Bootstrap and fallback paths) in toast.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.copied as the toast body because (1) the PHP Translator::trans() call was resolving to the key and (2) the key path was wrong. Replaced with window.__('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/discussions loaded 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 $userId before loading discussions, calling filterByPermissions() (pre-caches Category::canView() per unique category ID) immediately after loading, and checking Category::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 $legacyKeyMap migration shim introduced in 5.2.5 to rename double-prefixed permission keys (e.g. plugin.polls.polls.viewplugin.polls.polls_view) has been removed along with the now-dead $renamedPluginKeys variable and the migrateGroupPermissionKeys() helper. All existing forums have gone through at least one initDefaults() 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-theme returned 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-theme triggered applyThemePreference() (since the URL was not matched by isAdminRequest()), which patched Config::theme.active to premium in-memory. App.php then loaded themes/premium/boot.php instead of themes/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 causes isAdminRequest() to return true, ThemeSwitcher skips the preference override, Config stays bootswatch, 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() called Plugin::getData() (which already returns the plugin section) 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 with isset(). The same two bugs were found and fixed in TUIEditorPlugin.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.php output $tag['id'] raw in a data-tag-id attribute; theme-config.php output $groupData['icon'] (a Font Awesome class string) raw in a class attribute; webhooks-history.php output $item['http_code'] raw inside a <code> element. Fixed with htmlspecialchars() (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 posts table and the discussion view reads content via Post::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 after Discussion::create(), matching the behaviour of the normal discussion creation flow.
    Files changed: plugins/FlatModerationExtend/FlatModerationExtendController.php.

  • Security — HTTP security header improvementsX-XSS-Protection changed from 1; mode=block to 0: the reflective-XSS filter it controls was removed from Chrome/Firefox/Safari and its block mode was known to cause false-positive page blanking on IE11, making 0 the current OWASP recommendation. upgrade-insecure-requests added to the Content-Security-Policy: instructs browsers to silently rewrite any http:// sub-resource request to https:// before sending it, eliminating mixed-content warnings without a server-side redirect.
    Files changed: app/Core/App.php.

  • FlatHome — Recent discussions sorted by last activitygetRecentForumDiscussions() now sorts by last_post_at (falling back to updated_at then created_at) instead of created_at, so discussions with recent replies rise to the top. The display timestamp in the home and flatboard templates 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ésolu regardless of the site language. The name now uses Translator::trans('discussion.solved', [], 'main'), which resolves to the correct translation for the active site language. Existing tags named Résolu must be renamed manually from the tag admin panel.
    Files changed: app/Controllers/Discussion/DiscussionController.php.

  • Security — Upload MIME type spoofing in UploadController::upload() — The legacy upload() 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 via finfo_file() (falling back to mime_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 to UploadService and were unaffected.
    Files changed: app/Controllers/UploadController.php.

  • FlatHome — Recent discussions respect category permissionsgetRecentForumDiscussions() now loads the full discussion dataset (Discussion::all(null, 0)) and filters each entry through Category::canView(), so discussions from restricted categories are never shown to users who lack access. Previously the function fetched a fixed batch of $limit × 4 entries, which could be exhausted by blog-category posts before reaching enough visible forum discussions.
    Files changed: plugins/FlatHome/FlatHomeService.php.

  • changelog
Avatar
Posts 428 1580

🚀 Changelog — Flatboard 5.4.1

Release date: April 2, 2026


Fixed

  • SQLite: updating a user's group failed with "no such column: permissions_version" — The permissions_version column (used to invalidate active sessions when a user's group changes) was not added to existing databases by the schema migration routine. A new migration block now adds this INTEGER DEFAULT 0 column automatically when the schema version is bumped. SCHEMA_VERSION is incremented to 16 to trigger this migration on first boot.
    Files changed: app/Storage/SqliteStorage.php.

  • Update: "Array to string conversion" warning during plugin.json deep mergedeepMergePreserveExisting() used array_unique() with the default SORT_STRING flag when merging indexed lists (hooks, styles, etc.). If any element was itself an array (e.g. a hook entry ['hook.name', 'callable']), PHP would attempt string conversion and emit a warning. Fixed by passing SORT_REGULAR, which compares elements without coercing them to strings.
    Files changed: app/Controllers/Admin/UpdateController.php.

  • SQLite: deleting a user with dependent records crashed with a FOREIGN KEY constraint errorSqliteStorage::deleteUser() issued a bare DELETE FROM users without first removing dependent rows, which SQLite rejects when foreign keys are enabled (NO ACTION=RESTRICT). The method now runs all cleanup inside a single transaction: tokens (email_verification_tokens, email_change_tokens, password_reset_tokens), activity data (discussion_views, read_status, subscriptions, drafts, mentions, visitors), reactions and badges (post_reactions, user_badges, user_groups), notifications, and audit/moderation records. Nullable FK columns (bans.banned_by, reports.moderator_id) are set to NULL to preserve the historical record; NOT NULL columns (bans.user_id, reports.reporter_id, edit_history.edited_by, audit_logs.user_id) are deleted. The whole operation is atomic — a failure rolls back every change.
    Files changed: app/Storage/SqliteStorage.php.

  • Email verification: user stuck in guest group when member group resolution fails — After a user clicked the email verification link, the group promotion from "guest" to "member" was silently skipped if GroupHelper::getMemberGroupId() returned null (e.g. when the group type field was not set and the permission heuristic yielded no result). The failure is now logged as an error so it is detectable. Additionally, the getMemberGroupId() heuristic now explicitly skips the guest group to prevent it from being returned as the member group. A sanity check in RegisterController also blocks registration if getMemberGroupId() returns the guest group ID, which previously caused newly registered users to silently land in the wrong group. Finally, when an admin manually sets email_verified = true for a user in the guest group via the admin panel, the user is now automatically promoted to the member group (matching the behaviour of the normal verification flow).
    Files changed: app/Helpers/GroupHelper.php, app/Controllers/Auth/EmailVerificationController.php, app/Controllers/Auth/RegisterController.php, app/Controllers/Admin/UserManagementController.php.


Visitor, reply to this topic…
Log in to reply
Navigation
2 Posts
post #1
01 Apr 2026
By Utilisateur
Statistics
166
Discussions
911
Replies
21
Flatboarders
4
Contributors
New member : Emmanuel
Online
29 Guests online