Skip to content

fix(credits): make get_default_custom_credit_html public; revert defensive coerce from #1279#1295

Merged
superdav42 merged 1 commit into
mainfrom
fix/credits-visibility-field-default
May 27, 2026
Merged

fix(credits): make get_default_custom_credit_html public; revert defensive coerce from #1279#1295
superdav42 merged 1 commit into
mainfrom
fix/credits-visibility-field-default

Conversation

@superdav42

@superdav42 superdav42 commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR #1279 added a defensive coerce in Field::validate_textarea_field() to swallow the
Setup Wizard fatal that affected fresh installs. That band-aid masked a one-character
root cause in inc/class-credits.php. This PR reverts the band-aid and fixes the source.

Root cause

inc/class-credits.php:103 registers the credits_custom_html field's default as an
array callable:

'default' => [$this, 'get_default_custom_credit_html'],

But get_default_custom_credit_html() was declared protected. The consumer that
needs to invoke it lives in a different class:

// inc/class-settings.php (save_settings):
if (is_callable($field_default)) {
    $field_default = call_user_func($field_default);
}

PHP returns false for is_callable([$instance, 'protected_method']) evaluated outside
the declaring class scope. So Settings::save_settings() saw the callable as not
callable, never invoked it, and let the literal array [$instance, 'method-name']
flow through as the field value into Field::validate_textarea_field()
addslashes(array) → fatal:

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

The same unresolved-callable array also poisoned the data-state JSON the Setup
Wizard's Vue form is initialised from, which is why on a fresh install the wizard
step rendered with no settings fields at all. Clicking Continue still POSTed
and triggered the fatal.

Both symptoms — empty form and the save fatal — disappear with protectedpublic.

Audit of related patterns

Surveyed every 'default' => [$this, '...'] and 'options' => [$this, '...']
callable registration across inc/. Credits::get_default_custom_credit_html was
the only target with restricted visibility. All other registered field-default
and field-options callables are already public:

File:line Method Visibility
inc/class-credits.php:103 get_default_custom_credit_html was protected → now public
inc/managers/class-domain-manager.php:555 default_domain_mapping_instructions public
inc/class-settings.php:715 get_default_company_country public
inc/ui/class-template-switching-element.php:193 get_template_selection_templates public
inc/checkout/signup-fields/class-signup-field-order-summary.php:182 get_templates public
inc/checkout/signup-fields/class-signup-field-period-selection.php:174 get_template_options public
inc/checkout/signup-fields/class-signup-field-steps.php:168 get_templates public
inc/checkout/signup-fields/class-signup-field-template-selection.php:253 get_template_selection_templates public
inc/checkout/signup-fields/class-signup-field-pricing-table.php:231 get_pricing_table_templates public
inc/managers/class-gateway-manager.php:388 get_gateways_as_options public

No other code changes needed.

Changes

  • inc/class-credits.php — flip get_default_custom_credit_html from protected
    to public. Adds a docblock explaining the field-default callable contract so a
    future visibility downgrade is caught at review time (and by the new regression
    test below).
  • inc/ui/class-field.php — revert the coerce_textarea_value() defensive layer
    from Fix/textarea array coercion #1279. Preserves the two unrelated PHPCS lint cleanups (alignment + blank-line)
    from Fix/textarea array coercion #1279's first commit.
  • 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 confirms the
      resolved default value is a string.
  • tests/WP_Ultimo/UI/Field_Test.php — reverts the six textarea-coerce tests
    added by Fix/textarea array coercion #1279; they exercised the removed band-aid.

Verification

vendor/bin/phpunit --filter Credits_Test
# 17 tests, 28 assertions, OK

vendor/bin/phpcs inc/class-credits.php inc/ui/class-field.php \
  tests/WP_Ultimo/Credits_Test.php tests/WP_Ultimo/UI/Field_Test.php
# 0 errors

vendor/bin/phpstan analyse inc/class-credits.php inc/ui/class-field.php \
  tests/WP_Ultimo/Credits_Test.php
# OK No errors

Live-install verification on a fresh install:

Step Before fix After fix
Setup wizard step=your-company form rendering 0 fields visible All 11 fields render
Submitting the wizard step TypeError: addslashes(): … array given in class-field.php:504 Saves cleanly, advances to next step
wp-content/debug.log 3 identical fatal traces clean

Why revert #1279 instead of keeping it as defence-in-depth

The coerce in validate_textarea_field() silently dropped any non-string value
to ''. With the source bug fixed, no non-string value reaches this validator
from this code path. Keeping a silent coercer here would hide a future
recurrence of the same bug class instead of failing loudly. The visibility
contract + regression test is the better signal.


Summary by CodeRabbit

  • Refactor

    • Improved custom footer HTML field configuration structure.
    • Simplified textarea field validation logic.
  • Tests

    • Added regression tests for custom credit HTML functionality and external accessibility.
    • Updated field validation test coverage.

Review Change Stack

…nsive coerce from #1279

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

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 235eba91-f30b-4566-83c8-1bf9c5874b2e

📥 Commits

Reviewing files that changed from the base of the PR and between bce8e38 and 89fe76b.

📒 Files selected for processing (4)
  • inc/class-credits.php
  • inc/ui/class-field.php
  • tests/WP_Ultimo/Credits_Test.php
  • tests/WP_Ultimo/UI/Field_Test.php
💤 Files with no reviewable changes (1)
  • tests/WP_Ultimo/UI/Field_Test.php

📝 Walkthrough

Walkthrough

The PR exposes Credits::get_default_custom_credit_html() as a public method so the Settings framework can invoke it as a field-default callable, updates the custom footer HTML field configuration, removes defensive input coercion from textarea validation, adds regression tests for callable behavior, and removes obsolete sanitization tests.

Changes

Callable Default Method and Textarea Validation Simplification

Layer / File(s) Summary
Public method declaration and field configuration
inc/class-credits.php
get_default_custom_credit_html() changes from protected to public with expanded docblock documenting callable resolution and the specific textarea validation failure it prevents; the credits_custom_html field configuration is reformatted to match the new visibility.
Textarea field validation simplification
inc/ui/class-field.php
validate_textarea_field() removes defensive non-string coercion and its coerce_textarea_value() helper, directly applying string operations based on allow_html flag, since the callable default now guarantees string input.
Regression tests for callable method
tests/WP_Ultimo/Credits_Test.php
Two new tests verify get_default_custom_credit_html is externally callable via is_callable() and reflection checks, and directly invokes the method to assert it returns a non-empty string containing "Powered by".
Test cleanup and field behavior verification
tests/WP_Ultimo/UI/Field_Test.php
test_title_fallback_to_name gains explicit assertion that $field->title falls back to name when title is false; six textarea/wp_editor sanitization regression tests exercising array/object/null/scalar coercion are removed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

review-feedback-scanned

Poem

🐰 The Credits class now speaks out loud,
No more hiding in the shadows, proud!
Coercion falls away like morning dew,
Clean validation for the HTML true.
Methods callable, defaults now pure,
The textarea's future, clean and sure.

🚥 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 clearly identifies the two main changes: making get_default_custom_credit_html public and reverting a defensive coerce function, directly matching the core fixes described in the PR objectives.
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/credits-visibility-field-default

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

@superdav42 superdav42 merged commit 1511f86 into main May 27, 2026
8 of 11 checks passed
@superdav42

Copy link
Copy Markdown
Collaborator Author

Summary

PR #1279 added a defensive coerce in Field::validate_textarea_field() to swallow the
Setup Wizard fatal that affected fresh installs. That band-aid masked a one-character
root cause in inc/class-credits.php. This PR reverts the band-aid and fixes the source.

Root cause

inc/class-credits.php:103 registers the credits_custom_html field's default as an
array callable:

'default' => [$this, 'get_default_custom_credit_html'],

But get_default_custom_credit_html() was declared protected. The consumer that
needs to invoke it lives in a different class:

// inc/class-settings.php (save_settings):
if (is_callable($field_default)) {
    $field_default = call_user_func($field_default);
}

PHP returns false for is_callable([$instance, 'protected_method']) evaluated outside
the declaring class scope. So Settings::save_settings() saw the callable as not
callable, never invoked it, and let the literal array [$instance, 'method-name']
flow through as the field value into Field::validate_textarea_field()
addslashes(array) → fatal:

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

The same unresolved-callable array also poisoned the data-state JSON the Setup
Wizard's Vue form is initialised from, which is why on a fresh install the wizard
step rendered with no settings fields at all. Clicking Continue still POSTed
and triggered the fatal.
Both symptoms — empty form and the save fatal — disappear with protectedpublic.

Audit of related patterns

Surveyed every 'default' => [$this, '...'] and 'options' => [$this, '...']
callable registration across inc/. Credits::get_default_custom_credit_html was
the only target with restricted visibility. All other registered field-default
and field-options callables are already public:

File:line Method Visibility
inc/class-credits.php:103 get_default_custom_credit_html was protected → now public
inc/managers/class-domain-manager.php:555 default_domain_mapping_instructions public
inc/class-settings.php:715 get_default_company_country public
inc/ui/class-template-switching-element.php:193 get_template_selection_templates public
inc/checkout/signup-fields/class-signup-field-order-summary.php:182 get_templates public
inc/checkout/signup-fields/class-signup-field-period-selection.php:174 get_template_options public
inc/checkout/signup-fields/class-signup-field-steps.php:168 get_templates public
inc/checkout/signup-fields/class-signup-field-template-selection.php:253 get_template_selection_templates public
inc/checkout/signup-fields/class-signup-field-pricing-table.php:231 get_pricing_table_templates public
inc/managers/class-gateway-manager.php:388 get_gateways_as_options public
No other code changes needed.

Changes

  • inc/class-credits.php — flip get_default_custom_credit_html from protected
    to public. Adds a docblock explaining the field-default callable contract so a
    future visibility downgrade is caught at review time (and by the new regression
    test below).
  • inc/ui/class-field.php — revert the coerce_textarea_value() defensive layer
    from Fix/textarea array coercion #1279. Preserves the two unrelated PHPCS lint cleanups (alignment + blank-line)
    from Fix/textarea array coercion #1279's first commit.
  • 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 confirms the
      resolved default value is a string.
  • tests/WP_Ultimo/UI/Field_Test.php — reverts the six textarea-coerce tests
    added by Fix/textarea array coercion #1279; they exercised the removed band-aid.

Verification

vendor/bin/phpunit --filter Credits_Test
# 17 tests, 28 assertions, OK
vendor/bin/phpcs inc/class-credits.php inc/ui/class-field.php \
  tests/WP_Ultimo/Credits_Test.php tests/WP_Ultimo/UI/Field_Test.php
# 0 errors
vendor/bin/phpstan analyse inc/class-credits.php inc/ui/class-field.php \
  tests/WP_Ultimo/Credits_Test.php
# OK No errors

Live-install verification on a fresh install:

Step Before fix After fix
Setup wizard step=your-company form rendering 0 fields visible All 11 fields render
Submitting the wizard step TypeError: addslashes(): … array given in class-field.php:504 Saves cleanly, advances to next step
wp-content/debug.log 3 identical fatal traces clean

Why revert #1279 instead of keeping it as defence-in-depth

The coerce in validate_textarea_field() silently dropped any non-string value
to ''. With the source bug fixed, no non-string value reaches this validator
from this code path. Keeping a silent coercer here would hide a future
recurrence of the same bug class instead of failing loudly. The visibility
contract + regression test is the better signal.


Merged via PR #1295 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).


aidevops.sh v3.19.5 spent 47s on this as a headless bash routine.

@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label May 28, 2026
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