Skip to content

Pattern directory: apply the editor block allowlist during validation - #765

Merged
obenland merged 4 commits into
trunkfrom
pattern-directory/server-block-allowlist
Aug 31, 2026
Merged

Pattern directory: apply the editor block allowlist during validation#765
obenland merged 4 commits into
trunkfrom
pattern-directory/server-block-allowlist

Conversation

@obenland

@obenland obenland commented Aug 31, 2026

Copy link
Copy Markdown
Member

What

The Pattern Directory editor already keeps wporg/* blocks and a small set of core blocks out of the inserter via the allowed_block_types_all filter, but the REST content validator only checked that each submitted block was registered — so those same block types were still accepted on the server (its error message even promises "core blocks" only). This makes the server enforce the same policy as the editor, from one shared place, and closes a few adjacent gaps found in review.

Changes

Block allowlist (server-side)

  • Extracts the disallowed set and the wporg/* rule into a shared DISALLOWED_BLOCK_TYPES constant and is_block_allowed_in_pattern() predicate.
  • remove_disallowed_blocks() (the editor filter) now uses the predicate instead of its own inline list.
  • validate_content() runs every submitted block — flattened, so nested blocks are covered — through the same predicate and returns a rest_pattern_disallowed_blocks error for anything the editor wouldn't offer.
  • Adds core/pattern to the disallowed set: it splices another registered pattern in by slug on render, the same indirection core/block and core/template-part are already blocked for.

Validate the exact stored content

  • Stops normalising \n\n out of the content before parsing. That normalisation could hide a block from the validator (e.g. a <!-- wp:shortcode\n\n--> delimiter) while it still parsed as a real block once stored. Whitespace-only separator "blocks" are skipped during flattening instead.

Interactivity directives

  • Rejects data-wp-* directives carried in a block's inner HTML (rest_pattern_interactivity_directive). KSES preserves them and they sit in inner HTML, out of reach of the block-attribute check. Shared as content_has_block_directives(), which skips tokenising entirely when no data-wp- marker is present.

Translation import path

  • Translator-supplied strings are assembled into stored markup without passing through the REST validators, so the same block-allowlist and directive checks now run on the translation import too; create_or_update_translated_pattern() returns a WP_Error when content is refused, and the cron logs the refusal (pattern name + locale) to the server log.

pattern_content REST field

  • Guards the field so it only returns content for an actual wporg-pattern, returning empty when the id resolves to a different post type.

Tests

  • New test_wporg_blocks_are_disallowed covering the wporg/* path, plus data-provider cases for core/nextpage, core/shortcode (flat and nested), core/pattern, the \n\n-in-delimiter bypass, and interactivity directives (data-wp-interactive/data-wp-init, data-wp-bind--src). The existing core/nextpage "valid" case moved to "disallowed" to match the editor.
  • Full plugin suite green (139 tests); phpcs clean.

Copilot AI lite review requested due to automatic review settings August 31, 2026 13:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

obenland and others added 2 commits August 31, 2026 09:20
The editor's `allowed_block_types_all` filter already hides `wporg/*` and a
handful of core blocks from the pattern inserter, but the REST content
validator only checked that each block was registered. Share that policy
between the two via a single `is_block_allowed_in_pattern()` predicate so a
submission can't include a block type the editor never offers, and reject any
that slip through with a `rest_pattern_disallowed_blocks` error.

Adds coverage for the shared predicate and updates the content-validation
tests accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two defense-in-depth measures alongside the block allowlist:

- Reject Interactivity API `data-wp-*` directives in submitted block HTML.
  Core blocks emit these at render time and never store them, so their
  presence in a submission is markup trying to drive a store; KSES keeps
  them and they live in inner HTML, out of reach of the attribute check.
- Guard the `pattern_content` REST field so it only returns content for an
  actual pattern, returning empty when the id resolves to another post type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@obenland
obenland force-pushed the pattern-directory/server-block-allowlist branch from 487cbbd to 0f9e39d Compare August 31, 2026 14:20
obenland and others added 2 commits August 31, 2026 09:24
Follow-ups from review of the allowlist and directive validators:

- Validate the exact content that will be stored. Stripping `\n\n` before
  parsing let a delimiter like `<!-- wp:shortcode\n\n-->` vanish from the
  validator's view while still parsing as a real block once saved. Skip the
  editor's whitespace-only separator "blocks" during flattening instead.
- Disallow `core/pattern`, which splices another registered pattern in by
  slug on render - the same indirection `core/block` and
  `core/template-part` are blocked for.
- Run the block allowlist and directive checks on the translation import
  too: translator-supplied strings are assembled into stored markup without
  passing through the REST validators. The cron now logs a refused import.
- Extract `content_has_block_directives()` so both paths share the scan,
  and skip tokenizing entirely when no `data-wp-` marker is present.
- Cast the block name once in `is_block_allowed_in_pattern()` so the
  prefix test and the strict `in_array()` judge the same value.
- Test hygiene: unregister the test block in a `finally`, dedupe the
  paragraph fixture, and cover the two new rejections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ract

A translation refused by the content guard only surfaced in echoed cron
output, which isn't reliably captured, so also send it to the server log
with the pattern name and locale. Document that
`create_or_update_translated_pattern()` can return a `WP_Error`, and
correct the guard's docblock to state which checks it actually runs
rather than claiming full parity with the REST validators.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@obenland
obenland merged commit 440342b into trunk Aug 31, 2026
3 checks passed
@obenland
obenland deleted the pattern-directory/server-block-allowlist branch August 31, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants