Skip to content

Release 0.11.0 - #152

Merged
GaryJones merged 48 commits into
mainfrom
release/0.11.0
Apr 25, 2026
Merged

Release 0.11.0#152
GaryJones merged 48 commits into
mainfrom
release/0.11.0

Conversation

@GaryJones

Copy link
Copy Markdown
Contributor

Summary

Cuts the 0.11.0 release. The work itself has already landed on develop over the past few months across more than twenty PRs; this branch simply rolls those into a release with the three standard commits — changelog, regenerated POT, version bumps.

The headline of the release is the security work that landed in PRs #148, #149, #150 and #151. Three reflected/stored XSS surfaces on the zone admin screen are now closed, the term-description deserialiser uses unserialize with allowed_classes => false so a poisoned description cannot trigger PHP object instantiation, and the zoninator_search_posts AJAX handler now requires both an authentication check and a valid nonce so a logged-in subscriber can no longer enumerate scheduled-post titles. Alongside that, a handful of latent bugs around the lock heartbeat and the Delete confirm i18n string are tidied up.

The release also raises the minimum WordPress version from 5.9 to 6.4, in line with the rest of our plugin standards. The PHP minimum stays at 7.4.

The one behaviour change worth flagging in upgrade notes is that GET /wp-json/zoninator/v1/zones now requires an authenticated user. This previously responded to anonymous callers, which leaked zone names and descriptions to anyone who knew the endpoint existed. The admin UI does not consume this endpoint (it goes through admin-ajax), so that path is unaffected. Sites that genuinely depend on anonymous zone listing — for instance, a static site generator pulling zone metadata at build time — can opt back into the historical behaviour with a one-line filter:

add_filter( 'zoninator_rest_get_zones_permissions_check', '__return_true' );

The remainder of the release is the maintenance and dev-environment work that has been accumulating since 0.10.2: migration to wp-env, the wp-test-utils swap, GitHub Actions hardening, CODEOWNERS migration, and the usual run of Dependabot updates.

Six smoke tests against a live wp-env confirmed each of the security fixes behaves as intended, and one i18n test confirmed the previously-discarded Delete confirm string is now extractable. CI is green on develop ahead of this release branch.

Test plan

  • Confirm CI checks pass on this branch.
  • Verify the changelog entry on CHANGELOG.md covers everything that landed since 0.10.2 and matches the existing format.
  • Verify the version is consistent across zoninator.php (header + ZONINATOR_VERSION constant), package.json, package-lock.json, README.md (Stable tag), and the regenerated languages/zoninator.pot (Project-Id-Version header).
  • Confirm the regenerated POT preserved Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/zoninator and now contains the new "Are you sure you want to delete this zone?" string.
  • After merging, tag 0.11.0 from main, push the tag, and confirm the GitHub Release + WordPress.org SVN deploy workflows fire.

GaryJones and others added 30 commits October 2, 2025 18:50
Add .wp-env.json to standardize local development environment with:
- PHP 7.4 (plugin minimum)
- Latest WordPress (auto-updates)
- Query Monitor auto-installed
- Debug constants enabled
- Test environment with PHP 8.4 + WordPress trunk

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update minimum supported WordPress version from 5.9 to 6.4.

Changes:
- Plugin header: Add 'Requires at least: 6.4'
- README.md: Update minimum WP to 6.4, tested up to 6.8
- .phpcs.xml.dist: Update minimum_supported_wp_version to 6.4
- Workflow: Update test matrix minimum from 5.9 to 6.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Removes unnecessary eclint settings, explicit indent_size for tabs,
and LICENSE section. This aligns with the standard WordPress core
.editorconfig configuration used across other plugins.
Replace SVN-based test setup with @wordpress/env.

Changes:
- Use wp-env to set up WordPress test environment
- Update matrix: 'latest' → '6.8', 'trunk' → 'master'
- Rename composer scripts: test/test-ms → test-integration/test-integration-ms
- Remove SVN dependency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Normalize and expand on items like sorting packages, and script descriptions.
Comprehensive standardisation to align with other Automattic plugins:

- Renames integrations.yml to integration.yml for consistent naming
- Adds Dependabot configuration for automated dependency updates
- Updates composer.json to use yoast/wp-test-utils instead of phpunit-polyfills
- Renames test-integration to test:integration for script naming consistency
- Pins all GitHub Actions to specific commit SHAs to prevent supply chain attacks
- Adds explicit permissions blocks (least privilege)
- Adds persist-credentials: false to checkout actions
- Fixes template injection vulnerabilities using environment variables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates .gitignore with section headers and adds .wp-env.override.json
exclusion. Updates .distignore with consistent patterns matching other
plugins in the organisation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Composer scripts use colons (test:integration) not hyphens.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version.
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](rectorphp/rector@1.2.0...2.2.14)

---
updated-dependencies:
- dependency-name: rector/rector
  dependency-version: 2.2.14
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Move reviewer configuration from dependabot.yml to CODEOWNERS file
per GitHub's recommended approach. This allows the same reviewers
to be automatically assigned for all PRs, not just Dependabot ones.

See: https://github.com/dependabot/codeowner-migration-action

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates CI workflow to test against WP 6.4 and master with PHP 7.4 and
latest. Removes allowed_failure flag so CI fails if tests fail on any
supported configuration.

Updates README to reflect tested versions (WP 6.9).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](lodash/lodash@4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@8e8c483...de0fac2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Aligns the test infrastructure with the a8c-plugins standard by
using yoast/wp-test-utils bootstrap pattern with conditional
WordPress loading.

The bootstrap now only loads WordPress when the integration
testsuite is requested, and tests are relocated from tests/unit/
to tests/Integration/ to reflect their true nature as integration
tests requiring a full WordPress environment.
Migrate test infrastructure to wp-test-utils
Move agent instructions from .claude/CLAUDE.md to a root-level
AGENTS.md with five standardised sections: project knowledge,
commands, conventions, architectural decisions, and common pitfalls.
The .claude/CLAUDE.md now references AGENTS.md so that Claude Code
sessions still pick up the content automatically.
Add AGENTS.md with structured agent guidance
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](isaacs/minimatch@v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
dependabot Bot and others added 18 commits March 16, 2026 12:02
Bumps the actions group with 2 updates: [shivammathur/setup-php](https://github.com/shivammathur/setup-php) and [ramsey/composer-install](https://github.com/ramsey/composer-install).


Updates `shivammathur/setup-php` from 2.36.0 to 2.37.0
- [Release notes](https://github.com/shivammathur/setup-php/releases)
- [Commits](shivammathur/setup-php@44454db...accd612)

Updates `ramsey/composer-install` from 3.1.1 to 4.0.0
- [Release notes](https://github.com/ramsey/composer-install/releases)
- [Commits](ramsey/composer-install@3cf229d...65e4f84)

---
updated-dependencies:
- dependency-name: shivammathur/setup-php
  dependency-version: 2.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: ramsey/composer-install
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](micromatch/picomatch@2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](lodash/lodash@4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Three small XSS surfaces existed on the zone admin screen, each requiring
an attacker who could already create a post or craft a URL clicked by an
editor. Closing them so the admin UI is safe regardless of what an
upstream filter, importer, or another plugin writes into post titles,
post status, or the error query parameter.

The autocomplete result renderer concatenated raw post fields into HTML;
switching to jQuery .text() ensures values are inserted as text nodes.
The recent-posts AJAX response now escapes the title/status fragment
before it is appended into a <select>, so titles introduced via paths
that bypass title sanitisation can no longer break out. The admin error
notice now resolves a sanitised error code through the existing
zone_messages whitelist, with a fallback to the generic error, instead
of echoing arbitrary URL-supplied text.
Zone "details" are stored by serialising an array into the term
description and read back via maybe_unserialize on every render of the
admin screen and on every REST or feed response that lists zones. That
makes the description column a high-traffic deserialisation sink: if
anything (a buggy companion plugin, a database compromise, a future
filter on zoninator_insert_zone) writes a crafted payload, the next
read instantiates whatever PHP objects it names, which is the standard
shape of a POP-gadget RCE.

Replace maybe_unserialize with an explicit is_serialized check followed
by unserialize() with allowed_classes=false, and reject anything that
does not deserialise to an array. Strings are still treated as plain
text via wp_parse_args, so legitimate descriptions are unchanged.

While here, require a logged-in user to list zones via the REST index.
The endpoint previously accepted unauthenticated requests and exposed
zone names and descriptions to any caller, which is unnecessary since
the admin UI is the only first-party consumer. The historical behaviour
is preserved behind the zoninator_rest_get_zones_permissions_check
filter so anyone integrating against the public listing can opt back
in deliberately.
A handful of unrelated bugs spotted during a security review of the
plugin. None are exploitable on their own, but each is the sort of
latent fault that turns into a real incident later.

ajax_recent_posts ran the zone post query before checking the nonce
and capability. The data was never written to the response without a
valid check, so this is not a disclosure today, but moving the checks
to the top closes the gap and avoids paying the DB cost for unauthorised
calls.

lock_zone referenced $user->ID outside the branch that defined $user, so
calling lock_zone( $zone, $user_id ) with an explicit user id would have
emitted an undefined-variable notice and stored the wrong value (or
nothing, on PHP 8). Use get_current_user_id() and store $user_id
directly so both call shapes work.

The Delete confirmation string was wrapped in esc_js( 'text', 'zoninator' )
— esc_js takes one argument, so the text-domain string was discarded
and the message was never marked translatable. Wrap it in __() and pass
a single argument to esc_js.

The REST update_zone description ran strip_tags after the field's
sanitize_string callback had already entity-encoded the input, so it had
nothing to strip. Drop the redundant callback to keep the layered
sanitisation honest.
The wp_ajax_zoninator_search_posts handler was reachable by any
authenticated user and returned title, ID, date, type and status for
posts in publish or future status. Because the search uses WP_Query's
's' parameter, low-privileged users could brute-force scheduled post
titles character by character via a search oracle, exposing embargoed
content.

Bring the handler into line with every other Zoninator AJAX endpoint
by calling verify_nonce and verify_access at the top of the method.
The default verify_access path checks edit_others_posts, the same cap
that gates the Zones admin UI.

The autocomplete on the Zones page now sends _wpnonce alongside the
existing search request so the legitimate editor flow keeps working.

Adds an integration regression test covering subscriber rejection,
admin-without-nonce rejection and the valid editor success path.
@GaryJones GaryJones self-assigned this Apr 25, 2026
@GaryJones GaryJones added the type: release Release-related tasks label Apr 25, 2026
@GaryJones GaryJones added this to the Next milestone Apr 25, 2026
@GaryJones
GaryJones merged commit fe963c0 into main Apr 25, 2026
7 checks passed
@GaryJones
GaryJones deleted the release/0.11.0 branch April 25, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: release Release-related tasks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant