Web Development

WP Bones 3.0 — when your plugin says nothing

A major release: migrations run once per site, what declares no permission is for administrators, forms carry a nonce, and generated files leave the plugin folder.

•
3 min read
Giovambattista Fazioli
Senior Full-stack Engineer, Lead Developer·
Series

WordPress

Part 2 of 2Latest

Prev
Next
WP Bones 3.0 — when your plugin says nothing

WP Bones 3.0 is out. It is a major release, and the theme is a single question: what does the framework do when your plugin says nothing? In 2.x the answer was usually "the convenient thing". In 3.0 it is the safe thing, and when you want the convenient one, you say so.

Migrations run once per site

In 2.x every migration, and every seeder, ran again on each activation and on each update made from the dashboard, inside the request that installed the update, with the old code still in memory. An update by ZIP, FTP, git or Composer never ran them at all.

In 3.0 each migration runs once per site, in file-name order, and its name is recorded in an option. The pending ones run when the plugin is activated, on the first request after its Version header changes, however the update arrived, or when you run php bones migrate. A lock keeps two requests from running them at once, and a failure stops the run and tells the administrators until it runs. Seeders are gone: seed data is a migration now.

This design came out of a long thread on GitHub issue #40 with @balazsnasz, who pushed for a real record of what ran. Thank you.

What declares nothing is for administrators

A page in config/routes.php or pages/, or a menu in config/menus.php, that declares no capability now needs manage_options. A REST route without a permission_callback refuses every request with WordPress's own rest_forbidden. If a page is meant for every logged-in user, or a route is public on purpose, say so: 'capability' => 'read', 'permission_callback' => '__return_true'.

Requests that change something carry a nonce

In 2.x a form on another site could post to a WP Bones admin page with an administrator's cookies, and the page would run its controller. In 3.0 every request to an admin page that is not a GET carries the plugin's nonce: your forms print it with $plugin->csrfField(), and the check runs before any load callback or controller method. Logged-in Ajax actions need a $nonceHash, and make:ajax now sets one for you.

Generated files leave the plugin folder

Compiled Blade views and log files used to live inside the plugin folder, which the web server serves. They now go to wp-content/uploads/wpbones/<your plugin>/, behind an .htaccess, and the log files carry a name keyed with your site's salt, so they cannot be guessed on servers that ignore .htaccess.

The "What changed" table of the WP Bones 3.0 upgrade guide: migrations, seeders, pages and menus, REST routes, POST to an admin page, logged Ajax actions, useHTTPPost(), logs and compiled Blade views, each with its 2.x and 3.0 behaviour

One command to start

php bones migrate:to-v3 converts your migrations and seeders, then lists what it leaves to you: every page, menu and REST route that declares nothing, every POST form without the nonce, every Ajax provider without one. It rewrites nothing on that list: who may open a page is your decision.

Upgrading

Read the upgrade guide first, then composer require "wpbones/wpbones:^3.0" and php bones migrate:to-v3. A plugin that stays on ^2 keeps receiving 2.1.3. PHP stays at 8.1+, React stays at 18, and the build does not change. The 14 boilerplates and their Playground demos are already on 3.0, the suite has 350 tests, and the full notes are on GitHub.


WP Bones is free and open source (LGPL-3.0), runs on PHP 8.1+, and installs with Composer.

Series

WordPress

Part 2 of 2Latest

Prev
Next

Comments (2)

Join the discussion by logging into your account.

Igor Ganapolsky

Defaulting an undeclared page to manage_options, and an undeclared REST route to rest_forbidden, is the change I would keep even if nothing else landed. In 2.x a missing field meant a cookie was enough, or that the REST route was open. Requiring an explicit capability of read, or a permission_callback that returns true, when you actually want that is the same rule WordPress uses for a missing permission_callback. The migration side matches it: once per site, filename order, and only after the Version header changes, so a ZIP, git, or Composer update is not a silent skip. The sharp edge is a failed migration. If the filename is written to the option before the schema change commits, the next request treats a half-applied file as done. The lock stops two requests overlapping. It does not by itself make that write atomic with the SQL.

Giovambattista Fazioli
Giovambattista Fazioli

Senior Full-stack Engineer, Lead Developer

Italian Senior Full-stack Engineer and Lead Developer on the Cloud team at Namecheap. I build developer tools, React/Mantine UI components and native macOS apps — mostly open source. I work across TypeScript, Next.js, Go and SwiftUI, and I've been coding since the Commodore/Assembly days. Creator of WP Bones and 25+ Mantine extensions, and maintainer of the Undolog open-source studio.

Subscribe to Giovambattista Fazioli's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.