Ship 1.6.0: Abilities permission fix, private data storage, HTML API content filters - #51
Merged
Conversation
…filters Closes every finding from the v1.5.0 audit. Security - Abilities API: a permission callback per ability instead of one shared callback that downgraded to edit_post whenever the input carried a post_id. Every input schema now sets additionalProperties => false, and check-content-integrity also verifies the post type. A contributor with one draft could previously toggle modules, create redirects and tasks, trigger scans, and spend AI credits. - Redirects, the 404 log, and task projects move to a private directory with a random name under wp-content/functionalities/, with Apache, IIS, and listing guards written alongside and automatic migration of existing files. A new async Site Health check confirms over HTTP whether the folder is served. - Header and footer snippets are filtered once at save time against the author's unfiltered_html, not at output time against the viewer's. Logged-out visitors were receiving mangled code: && became && and comparison operators were eaten as tags, while the administrator saw it work. - Login Security gains username throttling, an IP allowlist, an unlock action on the lockout log, and a warning when every recent lockout shares one address. - Prism.js is bundled locally instead of loaded from a third-party CDN. - The service worker no longer caches wp-admin, the login page, REST responses, cross-origin requests, or anything marked no-store; the runtime cache is capped and precaching is per URL so one stale entry cannot abort install. Correctness and performance - A JSON exception preset served from a URL is cached rather than fetched on every page load, and the cache clears whenever a post or page is edited. - Link Management, Block Cleanup, and Schema use WP_HTML_Tag_Processor. Attributes are edited in place, so Vue, Alpine, and mustache syntax survive and the skip guard added in 1.4.3 and 1.4.4 is gone. - Redirect hits and 404 aggregates are buffered and flushed in batches instead of rewriting the whole JSON file under a lock per request. - Redirects run at parse_request, guarded against WordPress's own entry points. - The bulk nofollow tool pages by ID cursor and can now finish past 100 posts. - The Content Integrity list column reads a result cached at save time. - The SVG icon library leaves the autoload set, which needed a real upgrade routine because update_option() returns early on an unchanged value. - Content Integrity and Assumption Detection gained the filters their docs promised; the docs no longer list 19 hooks that were never fired. Release hygiene - .distignore patterns anchored to the root; an unanchored vendor rule also matched assets/vendor and would have dropped Prism from the zip. build.sh now shares the release workflow's exclude list and asserts required files. - Translation template generated from source: 1,136 strings, was 1 placeholder. - CI sparse-checks out wordpress-develop so the HTML API tests run, diffs the POT file, and asserts the new runtime files ship. Verified on WordPress 7.0.2: 52 tests, 203 assertions, PHPCS clean, lint clean on PHP 8.5, and an end-to-end pass on a live install covering the permission model, the storage migration, snippet output, all three content filters, hit buffering, and the admin screens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying 1.6.0 to a production site showed the upgrade routine skipping the storage migration there. Both the Redirect Manager and Task Manager modules are switched off on that site, but a redirects.json written by an earlier version was still on disk and still served at its old public URL. Gating the migration on module state meant the exposed file would have stayed exposed. Upgrader::has_legacy_data() now checks for the files themselves, so a site carrying data from a module it no longer uses is migrated too. Also records what live deployment established: on that host no .htaccess form works at all, because AllowOverride is off for wp-content. Six variants were tested inside the plugin's own data directory and every one still served the file. The unguessable directory name is therefore the protection that holds, and the Site Health check wording already reflects that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two release-hygiene bugs that only surfaced once CI built from a clean checkout rather than from the working tree. .gitignore carried an unanchored `vendor/`, the same bug already fixed in .distignore. It matched assets/vendor/ as well as Composer's root vendor/, so the bundled Prism copy added for the local-syntax-highlighting change was never committed. build.sh checks its allowlist against the checkout and failed on the missing file, meaning a release built from GitHub would have shipped the Misc module pointing at a 404. Both patterns are now anchored to the repo root and the four Prism files are tracked. The POT freshness check compared the committed template against a command CI spelled out inline, and the two had drifted on Report-Msgid-Bugs-To: the committed file carried the GitHub issues URL, the CI command produced the wordpress.org default. Rather than duplicating the header in a second place, the whole command now lives in bin/make-pot.sh, which regenerates the template by default and diffs it under --check. CI calls that script, so a developer and the workflow run byte-identical commands and the two cannot drift again. bin/ is excluded from the zip and the script is syntax-checked in CI alongside build.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cs site Tested up to is now 7.1, which is what 1.6.0 was actually verified against on a live install rather than an aspirational bump. Three claims had gone stale and are now accurate: - Both readmes promised DOMDocument-based HTML processing. 1.6.0 replaced it with the WordPress HTML API in all three content filters, which is the reason framework markup survives, so the bullet now says so. - The redirect storage answer predated the private data folder added in 1.6.0 and described only the file-based part. - The redirect migration answer told people to add CSV rows by hand. Bulk CSV import with column aliases, dry runs, and loop/chain validation has shipped since 1.4.8. Adds a "New in 1.6.0" section and three FAQ entries covering supported WordPress versions, what the plugin sends outbound (nothing, with the two opt-in exceptions named), and where files are written. Drops a Markdown `###` heading that WordPress.org renders literally. Links now point at functionalities.dev throughout: the docs home, getting started, module and dashboard guides, hooks, API reference, FAQ, and downloads, plus a per-module link on each entry that has a page. Fonts, Components, PWA, Meta and Editor Links have no docs page yet and are deliberately left unlinked. All 19 URLs verified 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes every finding from the v1.5.0 audit, and is already running on gauravtiwari.org.
Security
Abilities API privilege escalation. One shared permission callback downgraded to
edit_postwhenever the input carried apost_id, and it guarded all nine abilities. No input schema setadditionalProperties, and core only rejects unknown properties when that flag isfalse. Any user who could edit a single post could toggle modules off, create an external redirect from any path, create tasks, trigger loopback scans, and spend AI credits through/wp-abilities/v1/abilities/functionalities/*/run.Now: a permission callback per ability,
additionalProperties => falseon every input schema, andcheck-content-integrityadditionally verifies the post type. Verified against a real contributor account on production — all eight site-wide abilities returnability_invalid_permissions, and that user can still check only their own post.Web-readable data files.
wp-content/functionalities/redirects.jsonwas served publicly at a fixed, guessable URL. Data now lives in a directory named with 20 random characters, created and migrated byStorage\Data_Directory, with.htaccess,web.config, andindex.phpwritten at both levels and an async Site Health probe that asks the server whether the folder is actually reachable.Snippets mangled for visitors. Output ran kses against the viewer's capability, so anonymous readers got
&&where the author wrote&&, and comparison operators were eaten as tags — while the logged-in administrator saw the snippet work. Filtering now happens once at save time against the author's capability, with anunfilteredflag recorded per snippet.Also: username throttling, an IP allowlist and an unlock action in Login Security; Prism bundled locally instead of pulled from a CDN; and a service worker that no longer caches wp-admin, the login page, REST responses, cross-origin requests, or anything marked
no-store, with a capped runtime cache and per-URL precaching so one stale entry cannot abort install.Correctness and performance
WP_HTML_Tag_Processor. Attributes are edited in place, so Vue, Alpine, and mustache syntax survive untouched. The skip guard added in 1.4.3 and 1.4.4 is deleted, and content that used to be skipped entirely is now processed.parse_request, before the main query, guarded against WordPress's own entry points.update_option()returns early on an unchanged value and never touches the autoload column.Release hygiene
.distignorepatterns anchored to the root. An unanchoredvendorwas also matchingassets/vendor/, which would have shipped a zip with Prism missing.build.shnow uses the same exclude list as the release workflow and asserts required runtime files.wordpress-developso the HTML API tests run rather than skip, diffs the POT file, and asserts the new runtime files ship.CLAUDE.mdrewritten to the current architecture.Testing
52 tests, 203 assertions. PHPCS clean, lint clean across PHP 7.4 to 8.5, distribution zip clean at 85 files.
Verified end to end on WordPress 7.0.2 locally and on gauravtiwari.org (WordPress 7.1, PHP 8.5.6, LiteSpeed, Cloudflare, 212 users): the permission model against a real contributor, the storage migration, snippet output for a logged-out visitor, all three content filters on real posts, hit buffering, preset invalidation, and every admin screen.
Two things live deployment caught
redirects.jsonfrom an earlier version was still on disk and still public.Upgrader::has_legacy_data()now migrates whenever legacy files exist, regardless of whether the modules are on. Without this the exposed file would have stayed exposed..htaccessis inert on that host. Six forms were tested inside the plugin's own data directory —Require all denied, bareOrder/Deny,FilesMatch,RedirectMatch 404, and amod_rewriteforbid — and every one still served the file, becauseAllowOverrideis off forwp-content. The unguessable directory name is therefore the protection that holds, which is why the Site Health check reportsrecommendedrather thancriticalwhen a folder is servable.Known follow-up
To make data files unservable even when the URL is known, each payload would need to sit inside a
.phpfile beginning with<?php exit; ?>. That means a filename change, a read/write guard inAtomic_JSON_Store, and another migration, so it belongs in 1.6.1 with its own test pass rather than being rushed onto a live site.Upgrade notes
Existing option names, hooks, admin URLs, shortcodes, and stored JSON stay compatible. Data written by earlier versions migrates on the first admin page load. Two intentional behaviour changes: disabling Heartbeat now applies to the front end only unless the new admin option is also enabled, so autosave and post locking keep working; and the revision limit is configurable rather than hard-coded to 10.
🤖 Generated with Claude Code