Skip to content

Fix/textarea array coercion#1279

Merged
superdav42 merged 3 commits into
mainfrom
fix/textarea-array-coercion
May 26, 2026
Merged

Fix/textarea array coercion#1279
superdav42 merged 3 commits into
mainfrom
fix/textarea-array-coercion

Conversation

@superdav42

@superdav42 superdav42 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes

    • Improved textarea field input handling to safely process non-standard values and prevent errors.
  • Documentation

    • Updated task management and execution plan document templates.
  • Tests

    • Added comprehensive test coverage for textarea field sanitization.

Review Change Stack

Two unrelated nits flagged by PHPCS but never addressed:

- Line 301: align equals sign with surrounding assignment (auto-fixable
  Generic.Formatting.MultipleStatementAlignment warning).
- Line 466: remove stray blank line before block comment in
  validate_number_field (Squiz.Commenting.BlockComment.HasEmptyLineBefore
  error).

Lint-only cleanup; no behaviour change.
Setup Wizard / Settings save fataled with:

    Uncaught TypeError: addslashes(): Argument #1 ($string) must be of
    type string, array given in inc/ui/class-field.php:504

This happened when a textarea or wp_editor field reached
Field::validate_textarea_field() with a non-string value. Two real
sources observed:

1. Settings::save_settings() iterates ALL registered sections, not just
   the fields posted in the current Setup Wizard step. For fields not
   in $settings_to_save it falls back to the saved DB value as
   $existing_value. If a previous corruption stored an array / object /
   Closure (see #1148), that value flows straight back into
   set_value() -> sanitize() -> validate_textarea_field() and fatals
   under addslashes()/stripslashes() on PHP 8+.

2. A form posting name="field[]" syntax (intentional or otherwise)
   delivers an array in $_POST.

Fix: harden validate_textarea_field() with a coerce_textarea_value()
helper that:

  - returns the value unchanged when already a string,
  - returns '' for null / array / object (preventing the TypeError and
    preserving the historical 'drop malformed values' behaviour rather
    than propagating corruption), and
  - casts other scalars (int / float / bool) to their string form.

Add 6 regression tests covering string round-trip, array, object,
null, and scalar coercion for both textarea and wp_editor types.

Verification:
  - vendor/bin/phpunit --filter 'WP_Ultimo\\UI\\Field_Test' -> 25/25 pass
  - vendor/bin/phpstan analyse on both files -> clean
  - vendor/bin/phpcs on both files -> only a pre-existing json_encode
    warning in test code, none in modified ranges
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds defensive input coercion to textarea and wp_editor field sanitization to prevent TypeErrors on non-string values, backed by six regression tests, and simultaneously restructures TODO.md and PLANS.md into standardized TOON-compatible template formats with defined metadata sections and empty placeholders for tooling integration.

Changes

Textarea field input coercion

Layer / File(s) Summary
Field value normalization and coercion logic
inc/ui/class-field.php
wrapper_classes attribute resolution now coerces non-string values to empty string. validate_textarea_field() calls a new private coerce_textarea_value() helper that safely converts arrays, objects, and null to empty string and casts other scalar types to string before the existing allow_html/strip-tags sanitization flows.
Textarea sanitization regression tests
tests/WP_Ultimo/UI/Field_Test.php
Six test methods verify textarea coercion handles plain strings, arrays, wp_editor with allow_html, objects, null, and scalar non-string values (int/float) without fataling, each coercing to an appropriate safe representation.

Documentation template restructuring

Layer / File(s) Summary
TODO.md TOON template structure
TODO.md
Replaces ad-hoc task backlog with a TOON-compatible template: SPDX header, format documentation with HTML-comment examples, task ID/dependency/time/risk field definitions, meta/version block, and empty state placeholders for Ready, Backlog, In Progress, In Review, Done, Declined sections plus dependencies, subtasks, and summary blocks.
PLANS.md execution plans template structure
todo/PLANS.md
Restructures from a simple "Plans" list into a formal Execution Plans format with required field definitions, Active/Completed/Archived plan sections, a comprehensive Plan Template with linkage and decision log patterns, and an Analytics section with TOON directives for dependency tracking and aggregated stats.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A curious field now coerces with care,
No more shall arrays crash—they're handled fair!
And docs find structure in templates so neat,
Where TOON and markdown in order do meet.
This fuzzy-eared coder hops through the test case by case! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/textarea array coercion' directly and specifically describes the main change in the changeset: the addition of array coercion logic to textarea field sanitization to prevent TypeError fatals when non-string values are passed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/textarea-array-coercion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
todo/PLANS.md (1)

42-147: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Inconsistency between TOON metadata and actual plan content.

The TOON directive at lines 46-47 declares active_plans[0] (zero active plans), but lines 49-147 contain a substantial active PayPal integration plan with execution phases, context, and file references. This inconsistency will cause TOON parsing tools to misreport the plan count and break automated analytics.

Additionally, the preserved plan content doesn't follow the standardized template structure defined at lines 164-214 (which includes sections for Purpose, Development Environment, Linkage, Progress checkboxes, Decision Log, and Surprises & Discoveries).

Consider one of the following approaches:

  1. Reformat the PayPal plan to match the new template structure, and update the TOON metadata count to [1]
  2. Move the plan content to a separate file (e.g., todo/tasks/p001-paypal-review.md) and link it from a minimal Active Plans entry
  3. Complete the migration in a follow-up if this restructuring is intentionally incremental
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@todo/PLANS.md` around lines 42 - 147, The TOON metadata declares
active_plans[0] but the file actually contains a full PayPal plan (references
like t523a–t523e, Phase 1/2/3 and the Plan Template), causing a mismatch; either
(A) update the TOON directive to active_plans[1] and refactor the PayPal content
to conform exactly to the Plan Template sections (Purpose, Development
Environment, Linkage, Progress checkboxes, Decision Log, Surprises &
Discoveries), or (B) move the detailed PayPal plan out to a new file (e.g.,
todo/tasks/p001-paypal-review.md), replace the Active Plans entry with a minimal
TOON pointer/summary, and ensure the TOON metadata and plan body are consistent
(adjust TOON index and any tags) so parsers reading the TOON directive
(active_plans[0]) match the actual plan count and structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@todo/PLANS.md`:
- Around line 42-147: The TOON metadata declares active_plans[0] but the file
actually contains a full PayPal plan (references like t523a–t523e, Phase 1/2/3
and the Plan Template), causing a mismatch; either (A) update the TOON directive
to active_plans[1] and refactor the PayPal content to conform exactly to the
Plan Template sections (Purpose, Development Environment, Linkage, Progress
checkboxes, Decision Log, Surprises & Discoveries), or (B) move the detailed
PayPal plan out to a new file (e.g., todo/tasks/p001-paypal-review.md), replace
the Active Plans entry with a minimal TOON pointer/summary, and ensure the TOON
metadata and plan body are consistent (adjust TOON index and any tags) so
parsers reading the TOON directive (active_plans[0]) match the actual plan count
and structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: edc8c2c5-ab24-41ec-af72-ff8d7f828d43

📥 Commits

Reviewing files that changed from the base of the PR and between 5064889 and d900d7a.

📒 Files selected for processing (4)
  • TODO.md
  • inc/ui/class-field.php
  • tests/WP_Ultimo/UI/Field_Test.php
  • todo/PLANS.md

@github-actions

Copy link
Copy Markdown

Performance Test Results

Performance test results for e8f2640 are in 🛎️!

Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown.

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 39 (-2 / -4% ) 37.72 MB 825.50 ms 166.50 ms 1011.00 ms (-43.00 ms / -4% ) 1936.00 ms (-70.00 ms / -4% ) 1858.60 ms (-63.05 ms / -3% ) 81.00 ms (-3.25 ms / -4% )
1 56 49.13 MB 929.50 ms (+53.50 ms / +6% ) 146.50 ms 1077.50 ms (+55.50 ms / +5% ) 2070.00 ms 1998.40 ms (+42.20 ms / +2% ) 73.70 ms (-2.00 ms / -3% )

@superdav42 superdav42 merged commit 211c1f1 into main May 26, 2026
11 checks passed
@superdav42

Copy link
Copy Markdown
Collaborator Author

Completed via PR #1279, merged to main.

Merged by deterministic merge pass (pulse-wrapper.sh). Neither MERGE_SUMMARY comment nor PR body text was available.


aidevops.sh v3.19.5 spent 2m on this as a headless bash routine.

@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label May 27, 2026
superdav42 added a commit that referenced this pull request May 27, 2026
…nsive coerce from #1279 (#1295)

The Setup Wizard fatal that #1279 worked around had a single-character
root cause, not a Field-layer defect: Credits::get_default_custom_credit_html
was declared `protected`, but inc/class-credits.php:103 registers the field
default as the array callable `[$this, 'get_default_custom_credit_html']`.

`WP_Ultimo\Settings::save_settings()` resolves field defaults with
`is_callable($field_default)` from a different class scope. PHP returns
`false` for `is_callable([$instance, 'protected_method'])` evaluated outside
the declaring class. With the callable unresolvable, the literal array
`[$instance, 'get_default_custom_credit_html']` survived as the field value,
landed in `Field::validate_textarea_field()`, hit `addslashes()`, and raised:

    Uncaught TypeError: addslashes(): Argument #1 ($string) must be of
    type string, array given in inc/ui/class-field.php:504

The same broken JSON also poisoned the wizard data-state JSON used to
initialise the Vue form, which is why the form rendered with no settings
fields on a fresh install. Both symptoms — empty form and the save fatal —
disappear once the method is public.

Changes:

- inc/class-credits.php: visibility flipped from `protected` to `public`,
  with a docblock explaining the field-default callable contract so this
  bug class cannot silently regress.

- inc/ui/class-field.php: reverts the defensive `coerce_textarea_value()`
  added by #1279. The function was a downstream band-aid; with the source
  fixed, no non-string value can reach this validator from this code path.
  Preserves the two unrelated PHPCS lint cleanups from #1279's first commit
  (alignment at line 301 and blank line before block comment at line 466).

- tests/WP_Ultimo/Credits_Test.php: adds two regression tests:
    * `test_get_default_custom_credit_html_is_callable_externally` asserts
      both `is_callable([$instance, 'method'])` and ReflectionMethod::isPublic
      so a future visibility downgrade fails CI.
    * `test_get_default_custom_credit_html_returns_non_empty_string` asserts
      the resolved default value is a string suitable for textarea validation.

- tests/WP_Ultimo/UI/Field_Test.php: reverts the six textarea-coerce tests
  added by #1279; they covered the removed band-aid.

Audit: surveyed every `'default' => [$this, '...']` and `'options' => [$this,
'...']` callable across inc/. `Credits::get_default_custom_credit_html` was
the only target with restricted visibility. The two other registered field
defaults (`Domain_Manager::default_domain_mapping_instructions`,
`Settings::get_default_company_country`) and all seven options callables
are already public — no further code changes needed.

Verification:

  vendor/bin/phpunit --filter 'Credits_Test'            # 17/17 pass
  vendor/bin/phpcs inc/class-credits.php inc/ui/class-field.php \
    tests/WP_Ultimo/Credits_Test.php                    # 0 errors
  vendor/bin/phpstan analyse inc/class-credits.php \
    inc/ui/class-field.php tests/WP_Ultimo/Credits_Test.php   # No errors

Live-install repro on a fresh install before/after the change:
- Before: wizard step 2 renders with no fields; submitting fatals at
  class-field.php:504 with the addslashes TypeError above.
- After: all 11 fields render and save without error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant