Skip to content

Release 0.10.4#949

Merged
GaryJones merged 98 commits intomainfrom
release/0.10.4
Apr 24, 2026
Merged

Release 0.10.4#949
GaryJones merged 98 commits intomainfrom
release/0.10.4

Conversation

@GaryJones
Copy link
Copy Markdown
Contributor

Summary

Release 0.10.4 is dominated by defence-in-depth hardening following a security review of the plugin's authenticated code paths. None of the issues are known to be exploited in the wild, but all users are encouraged to update. See the full changelog for details.

Security

  • Require manage_options on the Add Custom Status form handler (#940)
  • Correct ICS text escaping per RFC 5545 (#941)
  • Stop double-escaping editorial comment author fields (#942)
  • Use correct wp_kses arguments in inline-save error paths (#943)
  • Harden calendar trashed-message Undo URL construction against query-arg injection (#944)
  • Strip HTML from filter-supplied editorial metadata CSS to prevent </style> breakout (#945)
  • Validate metadata term in calendar AJAX update handler (#946)
  • Require edit_post access on notification subscription AJAX handlers (#931)

Fixed

  • Show "Immediately" for custom status posts in the block editor (#938)
  • Stop passing null to wp_kses_post in list-table single_row — removes five PHP 8.1+ deprecations per row (#947)
  • Stop passing null to wp_kses_post on inline-save success — removes deprecations on Quick Edit (#948)

Documentation

  • Add AGENTS.md with structured agent guidance (#906)
  • Add missing license fields to readme (#889)
  • Update WordPress forums link in README (#929)

Maintenance

  • Exclude eslint from the Dependabot dev-dependencies group (#919)
  • Routine dependency updates for npm packages and GitHub Actions

Release commits

  1. Version 0.10.4 changelog — CHANGELOG entry + link reference
  2. Version 0.10.4 i18n — regenerated languages/edit-flow.pot
  3. Version 0.10.4 — version bumps in edit_flow.php (header + EDIT_FLOW_VERSION), README.md stable tag, package.json, package-lock.json, and AGENTS.md

GaryJones and others added 30 commits January 12, 2026 16:01
WordPress.org flagged the readme as missing license information, even though the plugin header in edit_flow.php already includes these fields. The readme is parsed separately for the plugin directory page, so both locations need the license declaration to satisfy repository requirements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bumps the actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 6.1.0 to 6.2.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@395ad32...6044e13)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 16.3.1 to 16.3.2.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](testing-library/react-testing-library@v16.3.1...v16.3.2)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [lodash-es](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-es
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.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 dev-dependencies group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@wordpress/components](https://github.com/WordPress/gutenberg/tree/HEAD/packages/components) | `31.0.0` | `32.0.0` |
| [@wordpress/data](https://github.com/WordPress/gutenberg/tree/HEAD/packages/data) | `10.37.0` | `10.38.0` |
| [@wordpress/e2e-test-utils-playwright](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright) | `1.37.0` | `1.38.0` |
| [@wordpress/env](https://github.com/WordPress/gutenberg/tree/HEAD/packages/env) | `10.37.0` | `10.38.0` |
| [@wordpress/i18n](https://github.com/WordPress/gutenberg/tree/HEAD/packages/i18n) | `6.10.0` | `6.11.0` |
| [@wordpress/jest-console](https://github.com/WordPress/gutenberg/tree/HEAD/packages/jest-console) | `8.37.0` | `8.38.0` |
| [@wordpress/jest-preset-default](https://github.com/WordPress/gutenberg/tree/HEAD/packages/jest-preset-default) | `12.37.0` | `12.38.0` |
| [@wordpress/plugins](https://github.com/WordPress/gutenberg/tree/HEAD/packages/plugins) | `7.37.0` | `7.38.0` |
| [@wordpress/scripts](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts) | `31.2.0` | `31.3.0` |
| [@wordpress/url](https://github.com/WordPress/gutenberg/tree/HEAD/packages/url) | `4.37.0` | `4.38.0` |


Updates `@wordpress/components` from 31.0.0 to 32.0.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/components@32.0.0/packages/components)

Updates `@wordpress/data` from 10.37.0 to 10.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/data/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/data@10.38.0/packages/data)

Updates `@wordpress/e2e-test-utils-playwright` from 1.37.0 to 1.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/e2e-test-utils-playwright/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/e2e-test-utils-playwright@1.38.0/packages/e2e-test-utils-playwright)

Updates `@wordpress/env` from 10.37.0 to 10.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/env/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/env@10.38.0/packages/env)

Updates `@wordpress/i18n` from 6.10.0 to 6.11.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/i18n/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/i18n@6.11.0/packages/i18n)

Updates `@wordpress/jest-console` from 8.37.0 to 8.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/jest-console/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/jest-console@8.38.0/packages/jest-console)

Updates `@wordpress/jest-preset-default` from 12.37.0 to 12.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/jest-preset-default/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/jest-preset-default@12.38.0/packages/jest-preset-default)

Updates `@wordpress/plugins` from 7.37.0 to 7.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/plugins/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/plugins@7.38.0/packages/plugins)

Updates `@wordpress/scripts` from 31.2.0 to 31.3.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/scripts@31.3.0/packages/scripts)

Updates `@wordpress/url` from 4.37.0 to 4.38.0
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/url/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/url@4.38.0/packages/url)

---
updated-dependencies:
- dependency-name: "@wordpress/components"
  dependency-version: 32.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/data"
  dependency-version: 10.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/e2e-test-utils-playwright"
  dependency-version: 1.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/env"
  dependency-version: 10.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/i18n"
  dependency-version: 6.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/jest-console"
  dependency-version: 8.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/jest-preset-default"
  dependency-version: 12.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/plugins"
  dependency-version: 7.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/scripts"
  dependency-version: 31.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@wordpress/url"
  dependency-version: 4.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

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>
Bumps [webpack](https://github.com/webpack/webpack) from 5.104.0 to 5.105.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](webpack/webpack@v5.104.0...v5.105.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](ljharb/qs@v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
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 [basic-ftp](https://github.com/patrickjuchli/basic-ftp) from 5.0.5 to 5.2.0.
- [Release notes](https://github.com/patrickjuchli/basic-ftp/releases)
- [Changelog](https://github.com/patrickjuchli/basic-ftp/blob/master/CHANGELOG.md)
- [Commits](patrickjuchli/basic-ftp@v5.0.5...v5.2.0)

---
updated-dependencies:
- dependency-name: basic-ftp
  dependency-version: 5.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.4 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](immutable-js/immutable-js@v5.1.4...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [svgo](https://github.com/svg/svgo) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](svg/svgo@v3.3.2...v3.3.3)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 3.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [simple-git](https://github.com/steveukx/git-js/tree/HEAD/simple-git) from 3.30.0 to 3.33.0.
- [Release notes](https://github.com/steveukx/git-js/releases)
- [Changelog](https://github.com/steveukx/git-js/blob/main/simple-git/CHANGELOG.md)
- [Commits](https://github.com/steveukx/git-js/commits/simple-git@3.33.0/simple-git)

---
updated-dependencies:
- dependency-name: simple-git
  dependency-version: 3.33.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the actions group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.2.0` | `6.3.0` |
| [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `2.5.0` | `2.6.1` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.0` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` |
| [shivammathur/setup-php](https://github.com/shivammathur/setup-php) | `2.36.0` | `2.37.0` |
| [ramsey/composer-install](https://github.com/ramsey/composer-install) | `3.1.1` | `4.0.0` |



Updates `actions/setup-node` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@6044e13...53b8394)

Updates `softprops/action-gh-release` from 2.5.0 to 2.6.1
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@a06a81a...153bb8e)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@b7c566a...bbbca2d)

Updates `actions/download-artifact` from 7.0.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@37930b1...3e5f45b)

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: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: softprops/action-gh-release
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- 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 [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](WebReflection/flatted@v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](micromatch/picomatch@4.0.3...4.0.4)

Updates `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@4.0.3...4.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.3 to 1.4.0.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](digitalbazaar/forge@v1.3.3...v1.4.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
The handle_add_custom_status admin handler verified the nonce but did
not re-check the user's capability, leaving the action relying solely
on the admin page's render-time cap check. A user with a valid nonce
(e.g. a lower-privileged contributor who obtains one via XSS or a
shared form) could therefore create custom statuses. Layering an
explicit manage_options check restores defence in depth and matches
the pattern used elsewhere in the module.
The calendar's ICS subscription feed had three related issues with text
escaping. do_ics_escaping replaced semicolons with backslash-colon
instead of backslash-semicolon, so any event text containing a
semicolon produced an invalid escape sequence. The same function
escaped backslashes last, meaning the backslashes it had just inserted
when escaping commas and semicolons were themselves doubled, producing
corrupt output for any string containing those characters. Newlines
were not escaped at all, so a stray LF in a post title or taxonomy
term would terminate the line and break every calendar client's parse
of the remainder of the feed.

The DESCRIPTION field also concatenated field labels and values
directly into the feed without passing them through the escaper, and
the SUMMARY field did the same for the post status label. Taxonomy
term names and custom status labels are user-editable, so a comma,
semicolon, backslash or newline in either would corrupt the event.

Fixing the escaper to follow RFC 5545 section 3.3.11 (escape
backslashes first, then newlines, then the separator characters) and
routing every variable field through it closes all three paths with
the minimum number of changes. New tests pin the escaper's behaviour
across each escapable character and their combination.
The ajax_insert_comment handler passed five of the comment-data fields
through esc_sql() before handing the array to wp_insert_comment(). That
function already escapes every value through $wpdb->prepare(), so the
pre-escape was applied twice: the literal backslash-quote sequences
from the first pass were passed through as data to the second, and
stored verbatim in the comments table. A user whose display name
contained an apostrophe would appear on their editorial comment as
"O\'Brien", and the same corruption affected comment author email,
URL, IP address and user agent.

Removing esc_sql() from all five fields fixes the storage bug. The
$_SERVER values retain a light sanitisation via sanitize_text_field()
and wp_unslash() so we still satisfy phpcs without reintroducing the
double escape.
Three inline-save AJAX handlers (custom-status, editorial-metadata and
user-groups) built their "could not update" error message with sprintf
and then passed it through wp_kses() with the wrong second argument.
Two sites passed the string 'strong', which wp_kses treats as a
context name via wp_kses_allowed_html(); the third passed nothing at
all, which produces a TypeError on PHP 8 because $allowed_html is a
required parameter. The latter turns a benign error path into a
fatal.

Each call now passes an explicit array( 'strong' => array() ), so only
the intended bold wrapper survives. The user-supplied term name that
is interpolated into the message is run through esc_html() first, so
a name containing HTML cannot break out of the wrapper even if the
translated string is ever tampered with. This is the same belt-and-
braces shape used for the adjacent wp_die error paths in the same
handlers.
The calendar's trashed/untrashed notice interpolated the raw $_GET
['ids'] query parameter into the Undo link's URL and derived the post
type from the first value of that list without checking that it was a
registered type. esc_url() on the resulting link only makes the URL
safe to emit into HTML; it does not stop a crafted ids value from
injecting additional query arguments (e.g. ids=1%26action%3Dmalicious)
or from pointing the undo to an arbitrary post type.

Each id is now cast through absint() before the list is reassembled,
the post type is rejected unless post_type_exists() confirms it, and
add_query_arg() builds the URL so the ampersand separator cannot come
from user input. The cast also flows into the translated message and
into number_format_i18n() so both receive a known-integer value.

While in the file, the stray unset( $_GET['undeleted'] ) is corrected
to the parameter the branch actually guards on (untrashed), and the
ValidatedSanitizedInput phpcs suppression is dropped because the
input is now sanitised in code rather than by comment.
The admin list-table CSS for editorial metadata columns is assembled
into a $css_rules array, passed through an
ef_editorial_metadata_manage_posts_css_rules filter, then echoed
inside a <style> block with no escaping. The default selector and
declarations are safe, but a filter callback from another plugin or
theme could smuggle a literal </style> sequence into the keys or
values and break out of the style block into arbitrary HTML.

Each property and declaration is now passed through
wp_strip_all_tags() before being echoed. That removes any HTML tag
the filter may have introduced (including the </style> breakout)
whilst leaving valid CSS syntax untouched — esc_html() would encode
quotes and ampersands, which would corrupt legitimate rules such as
url("…") declarations a plugin might legitimately add.
The handle_ajax_update_metadata handler accepted an unvalidated
metadata_term from the request and used it either as part of a
post meta key or as the taxonomy argument to wp_set_post_terms.
A user with edit_post capability could therefore write arbitrary
post meta keys on a post, or assign terms from any registered
taxonomy — including taxonomies not intended to be edited via
this interface.

Constrain the term before use: for editorial metadata updates
it must match the slug of an existing editorial metadata term,
and for taxonomy updates it must be a taxonomy registered on
the post's post type. Both inputs are also now sanitised up
front so the phpcs suppressions can be tightened to the single
value that still passes through wp_set_post_terms.
PHPCS flagged $_GET['ids'] as unsanitised input because the sniff
does not recognise the subsequent absint/explode chain as a
sanitisation path. Wrap the initial read in sanitize_text_field
so the intent is explicit and CI stays clean; the resulting value
is still fed through absint per id and post_type_exists on the
inferred post type, so the security properties are unchanged.
…shed-message

Harden calendar trashed-message Undo URL construction
Replace the manual echo of a <style> block with wp_add_inline_style,
attached to the edit_flow-editorial_metadata-styles handle that is
already enqueued on the same gate immediately above. This keeps the
output in the style pipeline WordPress manages rather than injecting
markup mid-admin_enqueue_scripts, and it also drops the remaining
phpcs EscapeOutput suppression.

The wp_strip_all_tags defences on the filter-supplied selectors and
rules are retained: wp_add_inline_style does not escape the string
it is handed, so an injected closing </style> tag would still need
to be stripped to prevent breakout.
WP_List_Table::single_row_columns() echoes its output and returns
void, so wrapping the call in wp_kses_post means the row HTML is
already out the door before we pass null into the kses chain. On
PHP 8.1+ that null propagates through wp_kses_post, wp_kses,
wp_kses_no_null and two preg_replace calls, emitting five
"passing null to non-nullable parameter" deprecations per row on
every Custom Statuses, Editorial Metadata and User Groups settings
screen.

Drop the misapplied wrapper and let single_row_columns() echo as
WP_List_Table intends; the column_* callbacks in each list table
already return pre-escaped strings, which is the documented
contract. The <tr> open tag is rebuilt with printf, casting the
term id through (int) and running the alternate-class marker
through esc_attr, which is cleaner than wrapping a static string
template in wp_kses_post.
The inline-save AJAX handlers in Custom Statuses, Editorial Metadata
and User Groups all echo a refreshed table row after a successful
update by way of:

    echo wp_kses_post( $wp_list_table->single_row( $return ) );

Because WP_List_Table::single_row() echoes internally and returns
void, the row HTML is already out the door before wp_kses_post
receives null. On PHP 8.1+ that null propagates through the kses
chain (wp_kses_post → wp_kses → wp_kses_no_null → preg_replace x 2)
and emits five "passing null to non-nullable parameter" deprecations
on every successful inline save.

This is the same bug PR 947 fixed on the list-rendering path; three
further call sites were missed because they referenced single_row
via a $wp_list_table variable rather than $this. Remove the
wrappers and let single_row() do its own output. The column_*
callbacks in each list table already return pre-escaped strings,
which is the documented contract.
@GaryJones GaryJones requested a review from a team as a code owner April 24, 2026 01:22
@GaryJones GaryJones added this to the Next milestone Apr 24, 2026
@GaryJones GaryJones self-assigned this Apr 24, 2026
@GaryJones GaryJones added the type: release Release-related tasks label Apr 24, 2026
@GaryJones GaryJones merged commit bd14595 into main Apr 24, 2026
12 checks passed
@GaryJones GaryJones deleted the release/0.10.4 branch April 24, 2026 01:28
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.

4 participants