Skip to content

17 July 2026

Account data export, admin fixes & scaffolding polish. Self-service GDPR data export with an app-extensible hook, consistent account navigation across all three themes, the admin Tokens/Token Actions fixes, the init admin-password prompt, project:switch-ui, and assorted DevPanel/email/cache fixes.

6 changes:

  • Self-service data export (GDPR) with an app-extensible hook
  • Consistent account navigation across all three themes
  • Admin Tokens & Token Actions pages fixed (all themes)
  • init prompts for the admin password (with a safe fallback)
  • New project:switch-ui command
  • Assorted fixes: DevPanel cache flush, email audit log, cache adapter

Self-service data export (GDPR) with an app-extensible hook

Users can now download a complete copy of their data from the account UI, and applications built on the framework can contribute their own sections to that export without touching the framework.

Added

  • Account::exportdata() — GET renders a confirmation page listing exactly what the download will contain; POST (CSRF-checked) streams a pretty-printed JSON attachment and records a data_export_requested activity entry.
  • Account::buildExportData() aggregates every personal-data source: profile, authorized applications, OAuth consents, passkeys, two-factor status, active sessions, tokens, token activity, account details, privacy settings and the activity log.
  • Extensibility hookbuildExportData() fires the account.data_export event with the user id; listeners return [section => data] maps that are merged into the payload. Core sections are protected from being overwritten, so an application (e.g. one adding licensing/billing data) extends the export without forking.

Security

  • The export is metadata-only where it matters: passwords/salts, token values, TOTP secrets and backup codes, passkey public keys and credential ids, raw request bodies (tokenactions.params) and password-reset hashes are never included. Each collector is individually guarded — a missing table degrades to an empty section rather than failing the whole export.

Consistent account navigation across all three themes

The built-in account area now has one coherent navigation model in the plain-CSS, Bootstrap and Tailwind scaffolding.

Changed

  • Shared sidebar + breadcrumb partials (partials/account_sidebar, partials/account_breadcrumb), included via the framework's $this->insert() view-in-view mechanism, so every account page shares the same navigation.
  • A clear hierarchy: Security groups change-password, two-factor and passkeys; Privacy groups data export and account deletion. Options that live inside a section are no longer duplicated in the sidebar.
  • Stable back-links via a single accountBase, and the 2FA/passkey controllers render inside the same account chrome. All auth-view JavaScript is consolidated into the shared, data-attribute-driven pf-auth.js.
  • Footers no longer carry a "Powered by" line in any theme (including the scaffolder-generated footer for new/switched projects).

Admin Tokens & Token Actions pages fixed (all themes)

The built-in admin Tokens and Token Actions pages showed nothing (or wrong data), and their view/revoke links were dead. Both are now correct across the plain-CSS, Bootstrap and Tailwind themes.

Fixed

  • Admin actions read the route id from the request option, not a method argument. The dispatcher always calls controller actions with an empty $args, so …/view/5, …/revoke/5, …/show/5 etc. must read the id from Request::staticGetOption(). Tokens, Token Actions, Applications, Permissions, Emails and Queue admin controllers were corrected — fixing the dead view/revoke/edit/delete links.
  • Token Actions used the wrong columns. It now selects the real actionid primary key, joins urls to show the human-readable endpoint (the row stores an integer urlid), renders the Unix servertime as a date, and the CSV export carries the endpoint instead of the opaque id.
  • Tokens no longer hides session/API tokens. The applications join is a LEFT join, so web_session/API tokens (which have no applicationid) appear; the list shows the username and a formatted last-used time, and the user_id/app_id filters work.
  • Bootstrap & Tailwind parity. Both themes were brought in line with the corrected plain-CSS views, and the Tailwind tables got proper cell styling and pill status badges (they previously rendered unstyled).

init prompts for the admin password (with a safe fallback)

Project initialisation now lets you set the first admin account's password interactively, and hardens that input against terminal editing quirks.

Added

  • Admin password prompt in project:init — press enter to accept a strong random default, or type your own.

Fixed

  • UTF-8-aware line editing. Before prompting, init enables stty iutf8 on the terminal so backspace erases a whole multibyte character. On terminals left in byte-oriented mode (notably WSL) a backspace would otherwise delete a single byte of a multibyte character, silently corrupting the stored password.
  • Defensive sanitizePassword(). As a second layer, the entered value is cleaned of stray backspace/DEL bytes, invalid UTF-8 and control characters (a warning is shown if anything was removed), so the saved password always matches what the user can reproduce at login.
  • writeFile() creates the parent directory before writing, so installUiFramework() works standalone (e.g. project:switch-ui) against a project missing a sub-directory.

New project:switch-ui command

Switch an existing project between the bundled UI frameworks in place — no re-scaffolding — to preview the built-in account/auth UI under each.

Added

  • project:switch-ui <plain-css|bootstrap|tailwind>
  • updates scaffold_theme in app/app.php (and relaxes the CSP style-src to 'unsafe-inline' for Tailwind's runtime build, strict otherwise);
  • re-installs the theme chrome and www/assets and pulls the framework's CSS/JS vendor assets (delegating to Init::installUiFramework()).

The scaffolded account/auth views are theme-agnostic and resolve per-framework from the bundled scaffolding, so switching needs no view copying.


Assorted fixes: DevPanel cache flush, email audit log, cache adapter

A handful of smaller fixes across the DevPanel, the mailer and the file cache.

Added

  • Email audit logEmail::send() records every outbound attempt (success or failure) in the mails table, so the admin Emails viewer has a complete delivery log. On by default; suppress per send with $recordToMails = false.

Fixed

  • DevPanel "flush all cache" now returns its JSON for an in-page AJAX request instead of navigating the browser to a raw JSON response.
  • File cache adapter — flushing or cleaning a cache group that was never written (or already removed) no longer throws: listDirectoryFiles() returns no files for a missing directory instead of letting RecursiveDirectoryIterator raise UnexpectedValueException.