Skip to content

Fix block editor rendering: query decoding and StrictMode#406

Open
adrianmomorales wants to merge 4 commits into
WordPress:trunkfrom
adrianmomorales:fix/block-editor-rendering
Open

Fix block editor rendering: query decoding and StrictMode#406
adrianmomorales wants to merge 4 commits into
WordPress:trunkfrom
adrianmomorales:fix/block-editor-rendering

Conversation

@adrianmomorales

Copy link
Copy Markdown

Summary

Two fixes for ACF block rendering in the Gutenberg editor:

1. Decode $query when sent as JSON string (acf_ajax_fetch_block)

The block editor JS may serialize the query parameter as a JSON string, but acf_ajax_fetch_block() assumes it is already a PHP array. On PHP 8.4 this causes a fatal TypeError at line 1099:

Cannot access offset of type string on string

The $block and $context parameters are already decoded with json_decode() (lines 1040-1046), but $query is not. This adds the same treatment.

2. Enable StrictMode flag in localized block editor data (gated on WP 6.9+)

The block editor JS already has code to handle React 18 StrictMode (mount → unmount → remount), gated behind acf.get('StrictMode'):

// acf-pro-blocks.js — V.setState()
(this.subscribed || acf.get('StrictMode')) && super.setState(e);

But the flag is never setacf.get('StrictMode') always returns null. Without it, when React StrictMode remounts a block component, this.subscribed is false (set during unmount) and super.setState() is skipped. The block form HTML is fetched successfully but never rendered — the block shows an infinite spinner.

This adds 'StrictMode' => version_compare( $GLOBALS['wp_version'], '6.9', '>=' ) to the localized data so the flag is only active where React StrictMode is actually used.

PHPUnit tests

Added tests covering the query decoding logic: valid JSON string, invalid JSON fallback, array passthrough, non-array JSON values, and empty strings.

Steps to reproduce

  1. Use WordPress 6.9+ (React 18 StrictMode in the block editor)
  2. Register any ACF block with acf_block_version: 2 and mode: edit
  3. Insert the block in the editor
  4. Block shows infinite spinner (fix 2), or on PHP 8.4 a 500 error (fix 1)

Test plan

  • Insert an ACF block in the editor on PHP 8.4 — should render form without errors
  • Insert an ACF block in the editor on WordPress 6.9+ — should render form without infinite spinner
  • Verify block preview mode works correctly
  • Verify block frontend rendering is unaffected

Note: This replaces #391 which was closed when the fork was accidentally deleted. All review feedback from that PR has been incorporated.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Apr 10, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adrianmomorales, cbravobernal.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@adrianmomorales

Copy link
Copy Markdown
Author

Hi! I've already linked my GitHub and WordPress.org accounts. Could a maintainer please add the props-bot label to refresh the contributor list? Thanks!

@cbravobernal cbravobernal added the props-bot Label to trigger the Props Bot GitHub action. label Apr 24, 2026
@github-actions github-actions Bot removed the props-bot Label to trigger the Props Bot GitHub action. label Apr 24, 2026
adrianmomorales and others added 3 commits April 24, 2026 22:09
Two fixes for block rendering in the Gutenberg editor:

1. Decode $query when sent as JSON string in acf_ajax_fetch_block().
   The block editor JS may serialize the query parameter as a JSON string,
   but the PHP code assumes it is already an array. On PHP 8.4 this causes
   a fatal TypeError: "Cannot access offset of type string on string".

2. Enable StrictMode flag in localized block editor data.
   The block JS already has code to handle React 18 StrictMode
   (mount → unmount → remount), gated behind acf.get('StrictMode'),
   but the flag was never set. Without it, blocks inserted in the editor
   show an infinite spinner because setState is skipped when the component
   is remounted after StrictMode cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tests

Gate StrictMode on WP 6.9+ using version_compare so older WP versions
keep existing behavior. Add PHPUnit tests covering JSON string query
decoding, invalid JSON fallback, and array passthrough.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cbravobernal cbravobernal force-pushed the fix/block-editor-rendering branch from 855f2c8 to 7555b72 Compare April 24, 2026 20:09

@cbravobernal cbravobernal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Approve direction — fixes are small, targeted, correct. Low risk. Main gap: tests duplicate the decode inline instead of exercising acf_ajax_fetch_block(), so they pass even if the production fix is reverted. A few stylistic nits. No security blockers.

Non-blocking suggestions

Tests don't actually exercise the fix

tests/php/includes/test-blocks-query-decoding.php — every test copies the 4-line decode snippet into the test body. The test file would still pass if someone deleted the fix from blocks.php. Options:

  • Extract the decode into a helper (e.g. acf_decode_block_query_arg()) and test that.
  • Or invoke acf_ajax_fetch_block() by priming $_REQUEST / nonce / caps and assert no TypeError on a string query.

$GLOBALS['wp_version'] vs global $wp_version;

includes/blocks.php:917 — rest of codebase uses global $wp_version; (see includes/assets.php:609, includes/class-acf-site-health.php:210). Match that pattern.

StrictMode PascalCase

includes/blocks.php:917 — key name is PascalCase while neighbors (blockTypes, postType) are camelCase. This is correct because the compiled JS already reads acf.get('StrictMode') (PascalCase hardcoded), but worth an inline comment so a future reader doesn't "normalize" it to strictMode and break the feature.

Sanitization asymmetry on $query

includes/blocks.php:1035-1042$query flows through acf_request_args()acf_sanitize_request_args() which applies wp_kses( $v, 'acf' ) to string inputs, so the JSON string is HTML-sanitized before json_decode sees it. $block and $context deliberately bypass this by reading from $_REQUEST with a phpcs:ignore (lines 1025-1026). Not exploitable today because $query keys are only boolean-tested (preview, form, validate), but a latent footgun if anyone later adds a string-valued key. Consider reading $query the same way $block is read, or add a comment.

Docblock

The inline comment in the decode block is great. The function docblock above acf_ajax_fetch_block() doesn't mention $query may arrive as a JSON string — minor.

Test coverage gaps

No test for: JSON-encoded true / null / number (falls to array() — good to assert), nested object matching real {preview,form,validate} keys, array-passthrough via the handler path.

Security review (20-skill framework)

Risk: low / none. No new trust boundary.

  • Auth/AuthZ (03): clean — acf_verify_ajax() + acf_current_user_can_edit_post / edit_theme_options gate the decode (lines 997-1023).
  • Error handling (07): clean — json_decode → null / non-array falls back to array(). Fails closed.
  • Type safety (09): clean — is_string() gate + is_array() post-check catches JSON scalars. wp_unslash before json_decode is correct WP idiom.
  • API contracts (15): implicit contract change — $query now accepts array OR JSON string. Not in docblock.
  • Web app security (16): clean — $query consumed only as flag bag (! empty( $query['preview'] ) etc.). Never echoed, never passed to WP_Query / get_posts / SQL. Verified across includes/blocks.php.
  • Data validation (17): see sanitization-asymmetry note above.
  • DoS (20): clean in practice — json_decode default depth 512, but attacker must already pass nonce + cap (authenticated block-editor user has higher-privileged ways to DoS). Belt-and-suspenders: add depth arg + try/catch with JSON_THROW_ON_ERROR. Not required.

StrictMode info disclosure: none — enqueue_block_editor_assets fires only in the authenticated block editor, which already exposes wp_version directly in the same localized payload (includes/assets.php:622). Flag only reveals WP ≥ 6.9 indirectly.

Questions

  1. Can the tests exercise acf_ajax_fetch_block() (or a new extracted helper)? As written they'd let a regression ship.
  2. Reason for $GLOBALS['wp_version'] over global $wp_version;?
  3. Is StrictMode PascalCase deliberate to match acf.get('StrictMode') in the compiled JS? Worth a comment if so.

- Extract query decoding into acf_decode_block_query_arg() so tests
  exercise the production code path instead of duplicating it inline.
- Use `global $wp_version;` to match the codebase convention
  (includes/assets.php, includes/class-acf-site-health.php).
- Add inline comment explaining StrictMode key is intentionally PascalCase
  to match acf.get('StrictMode') in the compiled acf-pro-blocks.js.
- Add comment noting $query passes through acf_sanitize_request_args()
  (wp_kses) — safe today since callers only boolean-test keys.
- Update acf_ajax_fetch_block() docblock to mention the JSON-string case.
- Expand tests to cover JSON scalars (true/false/null/number/string),
  nested objects, slashed input, and non-string/non-array values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@adrianmomorales

Copy link
Copy Markdown
Author

Thanks for the thorough review @cbravobernal! Addressed in 6e03724:

1. Tests now exercise the production code path. Extracted the decode logic into acf_decode_block_query_arg() and rewrote the test file to call it directly — reverting the production fix would now break the tests. Also expanded coverage to JSON scalars (true/false/null/number/string), nested objects, slashed input, and non-string/non-array values (15 tests, 21 assertions).

2. $GLOBALS['wp_version']global $wp_version;. No specific reason — switched to match the convention in includes/assets.php and includes/class-acf-site-health.php.

3. StrictMode PascalCase is deliberate to match acf.get('StrictMode') in the compiled acf-pro-blocks.js. Added an inline comment so a future reader doesn't normalize it.

Other items:

  • Updated acf_ajax_fetch_block() docblock to mention the JSON-string case.
  • Added an inline comment on the sanitization asymmetry: $query passes through acf_sanitize_request_args() (wp_kses) — safe today since callers only boolean-test keys, but flagged for the next person who touches it.

Let me know if you'd like me to also harden the json_decode call (depth arg + JSON_THROW_ON_ERROR) — left out as the review marked it not required, but happy to add it.

@cbravobernal cbravobernal added the [Type] Bug Something isn't working label May 4, 2026
@cbravobernal

Copy link
Copy Markdown
Contributor

@adrianmomorales I tried to reproduce the errors by adding a v2 block and could not replicate it. Would you mind sharing a a video without / with the fix?

I'm trying to avoid AI slop 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants