Fix block editor rendering: query decoding and StrictMode#406
Fix block editor rendering: query decoding and StrictMode#406adrianmomorales wants to merge 4 commits into
Conversation
|
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 Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Hi! I've already linked my GitHub and WordPress.org accounts. Could a maintainer please add the |
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>
855f2c8 to
7555b72
Compare
cbravobernal
left a comment
There was a problem hiding this comment.
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 stringquery.
$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_optionsgate the decode (lines 997-1023). - Error handling (07): clean —
json_decode→ null / non-array falls back toarray(). Fails closed. - Type safety (09): clean —
is_string()gate +is_array()post-check catches JSON scalars.wp_unslashbeforejson_decodeis correct WP idiom. - API contracts (15): implicit contract change —
$querynow accepts array OR JSON string. Not in docblock. - Web app security (16): clean —
$queryconsumed only as flag bag (! empty( $query['preview'] )etc.). Never echoed, never passed toWP_Query/get_posts/ SQL. Verified acrossincludes/blocks.php. - Data validation (17): see sanitization-asymmetry note above.
- DoS (20): clean in practice —
json_decodedefault 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 withJSON_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
- Can the tests exercise
acf_ajax_fetch_block()(or a new extracted helper)? As written they'd let a regression ship. - Reason for
$GLOBALS['wp_version']overglobal $wp_version;? - Is
StrictModePascalCase deliberate to matchacf.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>
|
Thanks for the thorough review @cbravobernal! Addressed in 6e03724: 1. Tests now exercise the production code path. Extracted the decode logic into 2. 3. Other items:
Let me know if you'd like me to also harden the |
|
@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 😅 |
Summary
Two fixes for ACF block rendering in the Gutenberg editor:
1. Decode
$querywhen sent as JSON string (acf_ajax_fetch_block)The block editor JS may serialize the
queryparameter as a JSON string, butacf_ajax_fetch_block()assumes it is already a PHP array. On PHP 8.4 this causes a fatal TypeError at line 1099:The
$blockand$contextparameters are already decoded withjson_decode()(lines 1040-1046), but$queryis not. This adds the same treatment.2. Enable
StrictModeflag 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'):But the flag is never set —
acf.get('StrictMode')always returnsnull. Without it, when React StrictMode remounts a block component,this.subscribedisfalse(set during unmount) andsuper.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
acf_block_version: 2andmode: editTest plan
🤖 Generated with Claude Code