Skip to content

Conversation

@superdav42
Copy link
Collaborator

@superdav42 superdav42 commented Jan 25, 2026

and a few other tweaks
closes #330

Summary by CodeRabbit

  • New Features

    • Added a Settings API for programmatic settings access and management
  • Bug Fixes

    • Fixed country/state selection in checkout
    • Prevented duplicate form attributes in checkout fields
  • Improvements

    • Added browser autocomplete to login forms
    • Improved add-on error handling to fail gracefully and clear tokens on errors
    • Moved error reporting setting to General for easier access
    • Better error pages for customers and admins

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 25, 2026

📝 Walkthrough

Walkthrough

Adds a new Settings REST API endpoint with CRUD operations and sensitive-key protections, relocates the error-reporting setting to General, improves Vue compatibility for form fields, enhances addon error logging, and exposes a filter for allowed HTML tags.

Changes

Cohort / File(s) Summary
Settings REST API Endpoint
inc/apis/class-settings-endpoint.php
New class WP_Ultimo\API\Settings_Endpoint (singleton) registering REST routes: GET /settings, GET /settings/{key}, POST /settings (batch), PUT/PATCH /settings/{key}. Implements auth checks, validation, sensitive-key filtering, batch update schema, WP_Error responses, and optional API call logging.
API Initialization
inc/class-wp-ultimo.php
Calls WP_Ultimo\API\Settings_Endpoint::get_instance() in extra component loading to initialize the new endpoint.
Settings Configuration
inc/class-settings.php
Moves enable_error_reporting field from "Other Options" into the "General" section; retains metadata and default.
Addon Repository Error Handling
inc/class-addon-repository.php
Changes get_user_data to log API request errors via wu_log_add (LogLevel::ERROR) and call delete_tokens() on failure instead of throwing exceptions.
HTML Sanitization Helpers
inc/functions/helper.php
wu_kses_allowed_html now builds $allowed_tags and returns it through apply_filters('wu_kses_allowed_html', $allowed_tags), exposing a filter for allowed HTML.
Login & Checkout Form Fields
inc/ui/class-login-form-element.php, views/checkout/fields/field-text.php, views/checkout/fields/field-select.php
Login form: add autocomplete attributes for username/current-password. Text/select fields: detect v-bind:name and omit server-rendered name attribute when Vue binds it, preventing duplicate attributes.
Checkout Pages Filter Registration
inc/checkout/class-checkout-pages.php
Moves registration of login_url/lostpassword_url filters out of is_main_site() block and guards them with enable_custom_login_page, changing when filters are added across sites.
Changelog / Docs
readme.txt
Adds 2.4.10 changelog entries: "Settings API", country/state fix, and improved error pages.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as REST Client
    participant Endpoint as Settings_Endpoint
    participant API as API (Auth)
    participant Settings as Settings Store
    participant Filters as Filters/Hooks

    Client->>Endpoint: Request (GET/POST/PUT /settings...)
    Endpoint->>API: authorize_request()
    API-->>Endpoint: auth result
    alt authorized
        Endpoint->>Filters: apply filters (e.g., wu_api_sensitive_settings)
        Filters-->>Endpoint: filtered rules
        Endpoint->>Settings: read/update settings
        Settings-->>Endpoint: result (data / success / failures)
        Endpoint->>Filters: maybe_log_api_call()
        Endpoint-->>Client: HTTP response (200 / 400 / 403 / 404 / 500)
    else unauthorized
        Endpoint-->>Client: 403 Forbidden
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped in swift with a settings API bright,
Secrets guarded close in the soft evening light,
Vue and forms now dance without a name fight,
Logs keep the tale should a call go uptight,
A tiny rabbit cheers this tidy new sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several changes appear outside the core Settings API objective: login form autocomplete additions, checkout field Vue binding logic, error reporting field relocation, and login URL filter refactoring. Review whether login form autocomplete, checkout Vue bindings, settings relocation, and checkout filter refactoring are part of issue #330 or should be in separate PRs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Settings API' directly describes the main change—a new Settings API endpoint class and related infrastructure for managing WP Ultimo settings via REST API.
Linked Issues check ✅ Passed The PR implements a Settings API endpoint (#330) with GET/POST/PUT/PATCH routes for settings management, validation, and security filters as required.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

🔨 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
inc/functions/helper.php (1)

498-625: Fix array union operator with numeric keys to include all intended tags.

The array union operator (+) preserves the left-hand array's values for overlapping numeric keys. Since array_keys($allowed_html) returns numeric keys [0, 1, ...] and the extra tags array also has numeric keys [0, 1, 2, ...], the + operator will drop extra tags that occupy indices already present in array_keys() (specifically 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'a', 'img', 'input'). Use array_merge() instead to concatenate the arrays and ensure all tags are included.

🔧 Suggested fix
-	] + array_merge_recursive($allowed_html, array_fill_keys(array_keys($allowed_html) + ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'a', 'img', 'input', 'textarea'], $vue_and_data_attributes));
+	] + array_merge_recursive(
+		$allowed_html,
+		array_fill_keys(
+			array_unique(array_merge(array_keys($allowed_html), ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'a', 'img', 'input', 'textarea'])),
+			$vue_and_data_attributes
+		)
+	);
🤖 Fix all issues with AI agents
In `@inc/apis/class-settings-endpoint.php`:
- Around line 258-270: The loop that calls wu_save_setting() with raw $value
should sanitize/validate via the Field API first: implement a helper (e.g.
sanitize_setting_value(string $key, $value)) that iterates
Settings::get_instance()->get_sections(), finds the field definition for $key,
constructs a \WP_Ultimo\UI\Field($key, $field_def), calls
$field->set_value($value) and returns $field->get_value(); then replace the
direct calls to wu_save_setting($key, $value) in the foreach over
$filtered_settings (and the similar persistence block later) to call $sanitized
= sanitize_setting_value($key, $value) and either pass $sanitized to
wu_save_setting or skip/reject unknown keys (returning null) as appropriate.
- Around line 426-436: The maybe_log_api_call method currently logs raw request
bodies via $request->get_body() which can leak sensitive data; before building
the $payload (or before calling wp_json_encode) sanitize the body by removing or
replacing sensitive keys (e.g., "password", "token", "authorization", "api_key",
"credit_card", "ssn") or omit the body entirely, e.g., call a helper like
redact_request_body($request->get_body()) and use that redacted result for the
'body_params' value so wp_json_encode(wu_log_add(...)) never receives plaintext
secrets; update maybe_log_api_call to perform this redaction and ensure
get_body()/body_params references the sanitized output.

In `@inc/class-settings.php`:
- Around line 711-720: The privacy-policy link in the description passed to
add_field for 'enable_error_reporting' lacks rel="noopener noreferrer" and the
URL isn't escaped; update the sprintf call building the description so the <a
href="%s" ...> uses esc_url( 'https://ultimatemultisite.com/privacy-policy/' )
and add rel="noopener noreferrer" to the anchor (i.e., modify the description
string for add_field('general','enable_error_reporting', ...) to include
rel="noopener noreferrer" and wrap the URL with esc_url()).

In `@readme.txt`:
- Around line 243-249: The changelog contains duplicate "Version [2.4.10]"
entries (the headers "Version [2.4.10]" and the block dated "2026-XX-XX"); merge
the two blocks into a single 2.4.10 entry combining both bullet points, and
replace the placeholder date "2026-XX-XX" with a concrete release date (e.g.,
"2026-01-25") or label the entry "Unreleased" if it isn't finalized; update the
single "Version [2.4.10]" header and remove the redundant header/block so only
one consolidated 2.4.10 section remains.
🧹 Nitpick comments (1)
inc/apis/class-settings-endpoint.php (1)

409-412: Drop unused $value to satisfy PHPMD

Line 409 declares $value but never uses it. Using foreach (array_keys(...)) or $key => $_ avoids the PHPMD warning.

🧹 Suggested tidy‑up
-		foreach ($settings as $key => $value) {
+		foreach ($settings as $key => $_) {
 			if ($this->is_sensitive_setting($key)) {
 				unset($settings[ $key ]);
 			}
 		}

Comment on lines +258 to +270
// Save each setting
$updated = [];
$failed = [];

foreach ($filtered_settings as $key => $value) {
$result = wu_save_setting($key, $value);

if ($result) {
$updated[] = $key;
} else {
$failed[] = $key;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate/sanitize values before wu_save_setting

Lines 263 and 327 persist raw values, bypassing the Field-based sanitization used by the UI flow. This can store invalid or unsafe values (especially for text/HTML fields). Consider resolving the field definition and running Field::set_value() before saving, or rejecting unknown keys.

🛠️ Proposed fix (sanitize via Field)
-			$result = wu_save_setting($key, $value);
+			$sanitized_value = $this->sanitize_setting_value($key, $value);
+			$result = wu_save_setting($key, $sanitized_value);
-		$result = wu_save_setting($setting_key, $value);
+		$sanitized_value = $this->sanitize_setting_value($setting_key, $value);
+		$result = wu_save_setting($setting_key, $sanitized_value);

You’d need a helper such as:

protected function sanitize_setting_value(string $key, $value) {
	$sections = \WP_Ultimo\Settings::get_instance()->get_sections();
	foreach ($sections as $section) {
		if (isset($section['fields'][$key])) {
			$field = new \WP_Ultimo\UI\Field($key, $section['fields'][$key]);
			$field->set_value($value);
			return $field->get_value();
		}
	}
	return $value; // or return null to reject unknown keys
}

Also applies to: 327-338

🤖 Prompt for AI Agents
In `@inc/apis/class-settings-endpoint.php` around lines 258 - 270, The loop that
calls wu_save_setting() with raw $value should sanitize/validate via the Field
API first: implement a helper (e.g. sanitize_setting_value(string $key, $value))
that iterates Settings::get_instance()->get_sections(), finds the field
definition for $key, constructs a \WP_Ultimo\UI\Field($key, $field_def), calls
$field->set_value($value) and returns $field->get_value(); then replace the
direct calls to wu_save_setting($key, $value) in the foreach over
$filtered_settings (and the similar persistence block later) to call $sanitized
= sanitize_setting_value($key, $value) and either pass $sanitized to
wu_save_setting or skip/reject unknown keys (returning null) as appropriate.

Comment on lines +426 to +436
protected function maybe_log_api_call($request): void {

if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) {
$payload = [
'route' => $request->get_route(),
'method' => $request->get_method(),
'url_params' => $request->get_url_params(),
'body_params' => $request->get_body(),
];

wu_log_add('api-calls', wp_json_encode($payload, JSON_PRETTY_PRINT));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redact request bodies before logging

Line 433–434 logs the raw request body. Even if sensitive keys are rejected later, their values can still be written to logs. Please redact sensitive keys (or omit body logging entirely) before logging.

🛠️ Proposed fix (redact body params)
 	protected function maybe_log_api_call($request): void {

 		if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) {
+			$body_params = $request->get_json_params();
+			if (empty($body_params)) {
+				$body_params = $request->get_body_params();
+			}
+			if (isset($body_params['settings']) && is_array($body_params['settings'])) {
+				$body_params['settings'] = $this->filter_sensitive_settings($body_params['settings']);
+			}
+			$setting_key = $request->get_param('setting_key');
+			if ($setting_key && $this->is_sensitive_setting($setting_key)) {
+				$body_params = ['redacted' => true];
+			}
 			$payload = [
 				'route'       => $request->get_route(),
 				'method'      => $request->get_method(),
 				'url_params'  => $request->get_url_params(),
-				'body_params' => $request->get_body(),
+				'body_params' => $body_params,
 			];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected function maybe_log_api_call($request): void {
if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) {
$payload = [
'route' => $request->get_route(),
'method' => $request->get_method(),
'url_params' => $request->get_url_params(),
'body_params' => $request->get_body(),
];
wu_log_add('api-calls', wp_json_encode($payload, JSON_PRETTY_PRINT));
protected function maybe_log_api_call($request): void {
if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) {
$body_params = $request->get_json_params();
if (empty($body_params)) {
$body_params = $request->get_body_params();
}
if (isset($body_params['settings']) && is_array($body_params['settings'])) {
$body_params['settings'] = $this->filter_sensitive_settings($body_params['settings']);
}
$setting_key = $request->get_param('setting_key');
if ($setting_key && $this->is_sensitive_setting($setting_key)) {
$body_params = ['redacted' => true];
}
$payload = [
'route' => $request->get_route(),
'method' => $request->get_method(),
'url_params' => $request->get_url_params(),
'body_params' => $body_params,
];
wu_log_add('api-calls', wp_json_encode($payload, JSON_PRETTY_PRINT));
🤖 Prompt for AI Agents
In `@inc/apis/class-settings-endpoint.php` around lines 426 - 436, The
maybe_log_api_call method currently logs raw request bodies via
$request->get_body() which can leak sensitive data; before building the $payload
(or before calling wp_json_encode) sanitize the body by removing or replacing
sensitive keys (e.g., "password", "token", "authorization", "api_key",
"credit_card", "ssn") or omit the body entirely, e.g., call a helper like
redact_request_body($request->get_body()) and use that redacted result for the
'body_params' value so wp_json_encode(wu_log_add(...)) never receives plaintext
secrets; update maybe_log_api_call to perform this redaction and ensure
get_body()/body_params references the sanitized output.

Comment on lines +711 to +720
$this->add_field(
'general',
'enable_error_reporting',
[
'title' => __('Help Improve Ultimate Multisite', 'ultimate-multisite'),
'desc' => sprintf(
/* translators: %s is a link to the privacy policy */
__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank">Learn more</a>.', 'ultimate-multisite'),
'https://ultimatemultisite.com/privacy-policy/'
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" to the privacy‑policy link

Line 718 opens a new tab without rel, which exposes a tabnabbing risk. Consider also wrapping the URL with esc_url() for safety.

🛠️ Proposed fix
-				'desc'    => sprintf(
+				'desc'    => sprintf(
 				/* translators: %s is a link to the privacy policy */
-					__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank">Learn more</a>.', 'ultimate-multisite'),
-					'https://ultimatemultisite.com/privacy-policy/'
+					__('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. <a href="%s" target="_blank" rel="noopener noreferrer">Learn more</a>.', 'ultimate-multisite'),
+					esc_url('https://ultimatemultisite.com/privacy-policy/')
 				),
🤖 Prompt for AI Agents
In `@inc/class-settings.php` around lines 711 - 720, The privacy-policy link in
the description passed to add_field for 'enable_error_reporting' lacks
rel="noopener noreferrer" and the URL isn't escaped; update the sprintf call
building the description so the <a href="%s" ...> uses esc_url(
'https://ultimatemultisite.com/privacy-policy/' ) and add rel="noopener
noreferrer" to the anchor (i.e., modify the description string for
add_field('general','enable_error_reporting', ...) to include rel="noopener
noreferrer" and wrap the URL with esc_url()).

Comment on lines +243 to 249
Version [2.4.10] - Released on 2026-XX-XX
- New: Settings API
- Fix: Problems with choosing country and state


Version [2.4.10] - Released on 2026-01-23
- New: Configurable minimum password strength setting with Medium, Strong, and Super Strong options.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Changelog duplicates 2.4.10 and uses a placeholder date

Lines 243–248 introduce a second 2.4.10 header with “2026‑XX‑XX”. Please merge into a single 2.4.10 entry and use a concrete release date (e.g., 2026‑01‑25 if releasing today) or mark as “Unreleased”.

🤖 Prompt for AI Agents
In `@readme.txt` around lines 243 - 249, The changelog contains duplicate "Version
[2.4.10]" entries (the headers "Version [2.4.10]" and the block dated
"2026-XX-XX"); merge the two blocks into a single 2.4.10 entry combining both
bullet points, and replace the placeholder date "2026-XX-XX" with a concrete
release date (e.g., "2026-01-25") or label the entry "Unreleased" if it isn't
finalized; update the single "Version [2.4.10]" header and remove the redundant
header/block so only one consolidated 2.4.10 section remains.

@github-actions
Copy link

🔨 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

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.

WP Ultimo API Settings

2 participants