Skip to content

Fixed: prevent data corruption when options are saved during an AJAX request.#287

Merged
Dan0sz merged 11 commits intodevelopfrom
fix_invalid_api_token
Feb 16, 2026
Merged

Fixed: prevent data corruption when options are saved during an AJAX request.#287
Dan0sz merged 11 commits intodevelopfrom
fix_invalid_api_token

Conversation

@Dan0sz
Copy link
Collaborator

@Dan0sz Dan0sz commented Feb 16, 2026

Summary by CodeRabbit

  • Tests

    • Added integration tests covering settings save (normal, escaped, invalid JSON) and updated test helpers and flows.
  • Bug Fixes

    • More robust settings-save handling: accepts escaped JSON, rejects invalid payloads, avoids merging arbitrary POST data into runtime settings, and guards tests from CI side effects.
  • New Features

    • Enqueues search-query script when the search feature is enabled; admin-bar and admin-tracking test flows adjusted.
  • Chores

    • Formatting consistency and CI-safe guards to prevent side effects during automated runs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds PLAUSIBLE_CI guards to short‑circuit CI paths, standardizes AJAX array access and JSON decoding, removes AJAX-driven settings merge from Helpers::get_settings, introduces integration tests for Ajax save_options, and adjusts Assets enqueue timing and several test helpers.

Changes

Cohort / File(s) Summary
CI guards & test helpers
src/Admin/Module.php, tests/TestCase.php
Add PLAUSIBLE_CI short-circuit in proxy test flow; define PLAUSIBLE_CI in test constructor and add enableAdministratorTracking($settings) helper.
AJAX handler & validation
src/Ajax.php
Normalize bracketed array access, extend clean() with optional key-specific behavior, handle escaped JSON decoding, and add PLAUSIBLE_CI guards to bypass/suppress responses in CI.
Settings retrieval
src/Helpers.php
Remove runtime merge of AJAX-posted options inside Helpers::get_settings()—settings are now returned from stored options only.
Assets enqueue behavior
src/Assets.php
Register dummy main script earlier (priority 1), add maybe_enqueue_search_queries_script() and an init hook for search queries; adjust where inline script is attached.
New integration tests
tests/integration/AjaxTest.php
Add tests for save_options() covering valid JSON, escaped JSON, and invalid JSON; mock nonce and ensure admin capability.
Test updates & minor test fixes
tests/integration/HelpersTest.php, tests/TestableHelpers.php, tests/integration/AssetsTest.php, tests/integration/AdminBarTest.php
Invert one assertion and add option cleanup; change test client anonymous class to extend Client; enable administrator tracking in assets test, adjust setup/teardown and assertions; ensure admin role for admin bar test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nudge the CI to skip a hop,
brackets snug, the JSON stops.
Tests in burrows, carrots in rows,
I tweak the scripts where the rabbit goes —
then tiptoe off to softer hops.

🚥 Pre-merge checks | ✅ 4
✅ 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 accurately describes the main objective of the changeset: preventing data corruption during AJAX-driven option saves. The changes across Ajax.php, Helpers.php, Module.php, Assets.php, and test files directly support this goal.
Docstring Coverage ✅ Passed Docstring coverage is 88.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix_invalid_api_token

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

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.

Caution

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

⚠️ Outside diff range comments (1)
tests/integration/HelpersTest.php (1)

17-21: ⚠️ Potential issue | 🟠 Major

Fix TestableHelpers mock to properly implement the Client type check.

The testGetJsUrl and testGetJsPath tests fail because TestableHelpers::get_client() returns an anonymous class that doesn't extend Client. When Helpers::get_filename() evaluates the instanceof Client check on line 51 of src/Helpers.php, it fails and returns an empty string, resulting in malformed URLs like https://plausible.io/js/.js instead of https://plausible.io/js/pa-test-tracker-id.js.

Override get_filename() in TestableHelpers to return the test tracker ID directly, similar to the pattern used in other tests like Admin/ModuleTest.php.

🧹 Nitpick comments (3)
src/Ajax.php (1)

285-289: CI guard causes inconsistent behavior on empty options.

When PLAUSIBLE_CI is defined and $options is empty, the function returns early (line 286) without calling wp_send_json_error(). However, this means in CI the function returns void silently, while in production it sends a 400 error response. This asymmetry could mask issues in tests.

Consider returning a value or throwing an exception to make test assertions more explicit.

tests/integration/AjaxTest.php (2)

35-54: Test lacks cleanup and proper assertions.

  1. Missing $_POST cleanup: Tests modify $_POST but don't clean up in tearDown(), potentially polluting subsequent tests.

  2. Empty catch block: With PLAUSIBLE_CI defined, save_options() returns void instead of calling die(), so no exception is thrown. The try-catch is unnecessary.

  3. No assertion on return behavior: Consider asserting that Messages::set_success() was called or checking transients.

Suggested improvements
+	public function tearDown(): void {
+		unset( $_POST['_nonce'], $_POST['options'] );
+		parent::tearDown();
+	}

 	public function testSaveOptionsSuccess() {
 		$options = [
 			[ 'name' => 'domain_name', 'value' => 'example.com' ],
 			[ 'name' => 'proxy_enabled', 'value' => 'on' ],
 		];

 		$_POST['_nonce']  = wp_create_nonce( 'plausible_analytics_toggle_option' );
 		$_POST['options'] = wp_json_encode( $options );

-		// We use catch because wp_send_json_success calls die()
-		try {
-			$this->ajax->save_options();
-		} catch ( \Exception $e ) {
-			// Catching any unexpected exceptions
-		}
+		$this->ajax->save_options();

 		$settings = Helpers::get_settings();
 		$this->assertEquals( 'example.com', $settings['domain_name'] );
 		$this->assertEquals( 'on', $settings['proxy_enabled'] );
 	}

83-97: Invalid JSON test could be more explicit.

The test verifies settings weren't updated to 'invalid-json', but this assertion is weak since 'invalid-json' wouldn't be a valid value anyway. Consider:

  1. Asserting that the original/default domain_name value is preserved
  2. Verifying that Messages::set_error() was called with the expected message
More robust assertion
 	public function testSaveOptionsInvalidJson() {
+		$original_settings = Helpers::get_settings();
+		$original_domain = $original_settings['domain_name'];
+
 		$_POST['_nonce']  = wp_create_nonce( 'plausible_analytics_toggle_option' );
 		$_POST['options'] = 'invalid-json';

-		// wp_send_json_error will be called, which we expect.
-		// In a real WP environment it would exit.
-		try {
-			$this->ajax->save_options();
-		} catch ( \Exception $e ) {
-		}
+		$this->ajax->save_options();

 		// Verify that settings were NOT updated to something weird.
 		$settings = Helpers::get_settings();
-		$this->assertNotEquals( 'invalid-json', $settings['domain_name'] );
+		$this->assertEquals( $original_domain, $settings['domain_name'] );
 	}

@Dan0sz
Copy link
Collaborator Author

Dan0sz commented Feb 16, 2026

@coderabbitai Could you expand on this comment? "Fix TestableHelpers mock to properly implement the Client type check."

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find TestableHelpers class
fd -t f "TestableHelpers" --exec cat {}

# Also search for any references to TestableHelpers
rg -n "TestableHelpers" -C 5

# Find Client class/interface references
rg -n "class Client|interface Client" -C 3

# Search for type checks involving Client
rg -n "instanceof Client|Client \$" -C 3

Length of output: 12245


🏁 Script executed:

#!/bin/bash
# Get the full context of the get_client and get_filename methods in Helpers
rg -n "get_client|get_filename" src/Helpers.php -A 10 -B 2

# Also check the Client class structure to understand what needs to be implemented
cat src/Client.php | head -100

Length of output: 4733


@Dan0sz, I'd be happy to expand on that comment. Here's the issue:

The Problem

In src/Helpers.php, the get_filename() method includes a type check before calling get_tracker_id():

public static function get_filename() {
    $client = static::get_client();
    
    if ( $client instanceof Client ) {
        return $client->get_tracker_id();
    }
    
    return '';
}

However, in tests/TestableHelpers.php, the mock returns an anonymous class that doesn't extend Client:

protected static function get_client() {
    return new class {
        public function get_tracker_id() {
            return 'pa-test-tracker-id';
        }
    };
}

The Issue

When tests call TestableHelpers::get_filename():

  1. It calls the mocked get_client() which returns the anonymous class
  2. The instanceof Client check fails (anonymous class is not a Client)
  3. The method returns an empty string '' instead of 'pa-test-tracker-id'

This means your tests might not be actually testing what they appear to test, or they're passing for the wrong reasons.

The Solution

The mock should return an object that passes the type check. Here are two approaches:

Option 1: Extend Client (recommended)

protected static function get_client() {
    return new class('mock-token') extends Client {
        public function get_tracker_id() {
            return 'pa-test-tracker-id';
        }
    };
}

Option 2: Create a dedicated mock class

class MockClient extends Client {
    public function __construct() {
        // Skip parent constructor
    }
    
    public function get_tracker_id() {
        return 'pa-test-tracker-id';
    }
}

Would you like me to check if this is causing any test failures in the current implementation?

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.

Caution

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

⚠️ Outside diff range comments (1)
tests/TestableHelpers.php (1)

18-23: ⚠️ Potential issue | 🟡 Minor

Override __construct() to avoid test side effects from the parent Client class.

The anonymous class inherits Client's constructor, which performs I/O operations (Helpers::get_hosted_domain_url()) and instantiates DefaultApi and GuzzleClient. To keep the test helper lightweight and isolated, add a no-op constructor override:

Suggested fix
 return new class extends Client {
+    public function __construct() {
+        // Override to prevent parent initialization side effects in tests
+    }
     public function get_tracker_id() {
         return 'pa-test-tracker-id';
     }
 };

@codecov
Copy link

codecov bot commented Feb 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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: 1

🤖 Fix all issues with AI agents
In `@tests/integration/AssetsTest.php`:
- Around line 18-46: The finally block in the AssetsTest removes actions but
doesn't restore them, causing hook leakage; update the finally block that
currently calls remove_filter( 'plausible_analytics_settings', [ $this,
'enableAdministratorTracking' ] ) and wp_set_current_user( null ) to also re-add
the removed hooks by calling add_action( 'wp_enqueue_scripts', 'maybe_enqueue' )
and add_action( 'wp_enqueue_scripts', 'maybe_enqueue', 11 ); alternatively
implement a tearDown() method on AssetsTest that restores these actions (and any
other altered hooks) so removeAction/remove_action calls from the test are
always undone.

Comment on lines +18 to +46
try {
add_filter( 'plausible_analytics_settings', [ $this, 'enableAdministratorTracking' ] );

$this->removeAction( 'wp_enqueue_scripts', 'maybe_enqueue' );
$this->removeAction( 'wp_enqueue_scripts', 'maybe_enqueue', 11 );
$class = $this->getMockBuilder( Assets::class )
->disableOriginalConstructor()
->onlyMethods( [ 'get_js_url' ] )
->getMock();

$class->method( 'get_js_url' )
->willReturn( 'https://plausible.test/js/plausible.js' );
$this->removeAction( 'wp_enqueue_scripts', 'maybe_enqueue' );
$this->removeAction( 'wp_enqueue_scripts', 'maybe_enqueue', 11 );

ob_start();
$class->method( 'get_js_url' )
->willReturn( 'https://plausible.test/js/plausible.js' );

$class->maybe_enqueue_main_script();
wp_set_current_user( 1 );
$user = wp_get_current_user();
$user->add_role( 'administrator' );

do_action( 'wp_head' );
$class->maybe_enqueue_main_script();

$output = ob_get_clean();
global $wp_scripts;
$data = $wp_scripts->get_data( 'plausible-analytics', 'after' );

$this->assertStringContainsString( 'window.plausible', $output );
$this->assertStringContainsString( 'plausible.init', $output );
$this->assertStringContainsString( 'window.plausible', implode( '', $data ) );
$this->assertStringContainsString( 'plausible.init', implode( '', $data ) );
} finally {
remove_filter( 'plausible_analytics_settings', [ $this, 'enableAdministratorTracking' ] );
wp_set_current_user( null );
}
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

🧩 Analysis chain

🏁 Script executed:

find . -name "TestCase.php" -type f

Repository: plausible/wordpress

Length of output: 83


🏁 Script executed:

rg -n "function (setUp|tearDown)" tests/ -A 10

Repository: plausible/wordpress

Length of output: 1267


🏁 Script executed:

rg -n "remove_all_filters|remove_all_actions|wp_filter" tests/ -C 2

Repository: plausible/wordpress

Length of output: 866


🏁 Script executed:

cat tests/TestCase.php

Repository: plausible/wordpress

Length of output: 5644


🏁 Script executed:

cat tests/integration/AssetsTest.php

Repository: plausible/wordpress

Length of output: 5969


🏁 Script executed:

rg -n "class AssetsTest|function setUp|function tearDown" tests/integration/AssetsTest.php -A 5

Repository: plausible/wordpress

Length of output: 211


🏁 Script executed:

web_search
What does Yoast WPTestUtils BrainMonkey TestCase do with WordPress hooks between tests tearDown

Repository: plausible/wordpress

Length of output: 156


🏁 Script executed:

rg -n "brain_monkey|reset|tearDown" tests/ --type php -i

Repository: plausible/wordpress

Length of output: 188


🏁 Script executed:

rg -n "BrainMonkey|wp_filter|wp_actions" . -type php --max-count=20

Repository: plausible/wordpress

Length of output: 90


🏁 Script executed:

find . -name "composer.json" -o -name "composer.lock" | head -5

Repository: plausible/wordpress

Length of output: 148


🏁 Script executed:

cat composer.json

Repository: plausible/wordpress

Length of output: 2531


🏁 Script executed:

find vendor -name "TestCase.php" -path "*WPTestUtils*" 2>/dev/null | head -1

Repository: plausible/wordpress

Length of output: 45


🏁 Script executed:

find vendor -name "*.php" -path "*wp-test-utils*" | grep -i testcase | head -3

Repository: plausible/wordpress

Length of output: 104


🏁 Script executed:

rg -n "remove_all_filters|remove_all_actions|reset_all" vendor/ --max-count=10 2>/dev/null || echo "Search complete"

Repository: plausible/wordpress

Length of output: 78


Restore removed actions to prevent hook state leakage across tests.

The removeAction calls on lines 26-27 should be restored in the finally block. Without restoration, subsequent tests may miss these hooks. Add the following to the finally block:

Add to finally block after line 45:
// Restore removed actions
add_action( 'wp_enqueue_scripts', 'maybe_enqueue' );
add_action( 'wp_enqueue_scripts', 'maybe_enqueue', 11 );

Alternatively, implement a tearDown() method in AssetsTest that restores all removed actions for all test methods.

🤖 Prompt for AI Agents
In `@tests/integration/AssetsTest.php` around lines 18 - 46, The finally block in
the AssetsTest removes actions but doesn't restore them, causing hook leakage;
update the finally block that currently calls remove_filter(
'plausible_analytics_settings', [ $this, 'enableAdministratorTracking' ] ) and
wp_set_current_user( null ) to also re-add the removed hooks by calling
add_action( 'wp_enqueue_scripts', 'maybe_enqueue' ) and add_action(
'wp_enqueue_scripts', 'maybe_enqueue', 11 ); alternatively implement a
tearDown() method on AssetsTest that restores these actions (and any other
altered hooks) so removeAction/remove_action calls from the test are always
undone.

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.

Caution

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

⚠️ Outside diff range comments (1)
src/Ajax.php (1)

284-365: ⚠️ Potential issue | 🟡 Minor

Initialize list values and preserve falsy inputs when collapsing array options.
In the array normalization block, $options[0]->value can remain unset (notice risk later), and values like "0" are currently dropped. Initialize the array and use a non-empty-string check.

🛠️ Suggested fix
 			$options          = [];
 			$array_name       = preg_replace( '/\[[0-9]+]/', '', $input_array_elements[0]->name );
 			$options[0]       = (object) [];
 			$options[0]->name = $array_name;
+			$options[0]->value = [];
 
 			foreach ( $input_array_elements as $input_array_element ) {
-				if ( $input_array_element->value ) {
+				if ( isset( $input_array_element->value ) && $input_array_element->value !== '' ) {
 					$options[0]->value[] = $input_array_element->value;
 				}
 			}
🧹 Nitpick comments (1)
src/Ajax.php (1)

103-135: Use WordPress's standard protocol whitelist instead of user-controlled schemes.
Passing [ $parsed['scheme'] ] as the allowed protocols effectively whitelists any scheme present in the user input—including dangerous ones like javascript:. This bypasses the security protections that esc_url_raw() is designed to provide. Use wp_allowed_protocols() to leverage WordPress's curated list of safe schemes.

♻️ Suggested adjustment
-			if ( isset( $parsed['scheme'] ) ) {
-				return esc_url_raw( wp_unslash( $var ), [ $parsed['scheme'] ] );
-			}
+			if ( isset( $parsed['scheme'] ) ) {
+				return esc_url_raw( wp_unslash( $var ), wp_allowed_protocols() );
+			}

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: 1

🤖 Fix all issues with AI agents
In `@src/Admin/Module.php`:
- Around line 202-203: The ternary initializing $settings_proxy can access
$settings['proxy_enabled'] when $settings is not an array; update the expression
to match the safe pattern used in maybe_install_module and for $old_proxy by
making it: if is_array($settings) and isset($settings['proxy_enabled']) use
$settings['proxy_enabled'], otherwise fallback to an empty string—replace the
current mixed fallback with this consistent safe check for $settings_proxy.
🧹 Nitpick comments (1)
src/Admin/Module.php (1)

268-271: Consider checking the constant's value, not just its existence.

The current check defined( 'PLAUSIBLE_CI' ) will bypass the test if the constant is defined with any value, including false or 0. If this is intentional (any definition indicates CI), the code is fine. If you want to allow disabling the bypass by setting PLAUSIBLE_CI to a falsy value, consider:

Optional: Check constant value
-		if ( defined( 'PLAUSIBLE_CI' ) ) {
+		if ( defined( 'PLAUSIBLE_CI' ) && PLAUSIBLE_CI ) {

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/plausible/wordpress/issues/comments/3908419403","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: review paused by coderabbit.ai -->\n\n> [!NOTE]\n> ## Reviews paused\n> \n> It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the `reviews.auto_review.auto_pause_after_reviewed_commits` setting.\n> \n> Use the following commands to manage reviews:\n> - `@coderabbitai resume` to resume automatic reviews.\n> - `@coderabbitai review` to trigger a single review.\n> \n> Use the checkboxes below for quick actions:\n> - [ ] <!-- {\"checkboxId\": \"7f6cc2e2-2e4e-497a-8c31-c9e4573e93d1\"} --> ▶️ Resume reviews\n> - [ ] <!-- {\"checkboxId\": \"e9bb8d72-00e8-4f67-9cb2-caf3b22574fe\"} --> 🔍 Trigger review\n\n<!-- end of auto-generated comment: review paused by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nAdds PLAUSIBLE_CI guards to short‑circuit CI paths, standardizes AJAX array access and JSON decoding, removes AJAX-driven settings merge from Helpers::get_settings, introduces integration tests for Ajax save_options, and adjusts Assets enqueue timing and several test helpers.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**CI guards & test helpers** <br> `src/Admin/Module.php`, `tests/TestCase.php`|Add PLAUSIBLE_CI short-circuit in proxy test flow; define `PLAUSIBLE_CI` in test constructor and add `enableAdministratorTracking($settings)` helper.|\n|**AJAX handler & validation** <br> `src/Ajax.php`|Normalize bracketed array access, extend `clean()` with optional key-specific behavior, handle escaped JSON decoding, and add PLAUSIBLE_CI guards to bypass/suppress responses in CI.|\n|**Settings retrieval** <br> `src/Helpers.php`|Remove runtime merge of AJAX-posted `options` inside `Helpers::get_settings()`—settings are now returned from stored options only.|\n|**Assets enqueue behavior** <br> `src/Assets.php`|Register dummy main script earlier (priority 1), add `maybe_enqueue_search_queries_script()` and an init hook for search queries; adjust where inline script is attached.|\n|**New integration tests** <br> `tests/integration/AjaxTest.php`|Add tests for `save_options()` covering valid JSON, escaped JSON, and invalid JSON; mock nonce and ensure admin capability.|\n|**Test updates & minor test fixes** <br> `tests/integration/HelpersTest.php`, `tests/TestableHelpers.php`, `tests/integration/AssetsTest.php`, `tests/integration/AdminBarTest.php`|Invert one assertion and add option cleanup; change test client anonymous class to extend `Client`; enable administrator tracking in assets test, adjust setup/teardown and assertions; ensure admin role for admin bar test.|\n\n## Estimated code review effort\n\n🎯 3 (Moderate) | ⏱️ ~25 minutes\n\n## Poem\n\n> 🐇 I nudge the CI to skip a hop,  \n> brackets snug, the JSON stops.  \n> Tests in burrows, carrots in rows,  \n> I tweak the scripts where the rabbit goes —  \n> then tiptoe off to softer hops.\n\n</details>\n\n<!-- walkthrough_end -->\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4</summary>\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|        Check name        | Status   | Explanation                                                                                                                                                                                                                                  |\n| :----------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|     Description Check    | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                                                                  |\n|        Title check       | ✅ Passed | The title accurately describes the main objective of the changeset: preventing data corruption during AJAX-driven option saves. The changes across Ajax.php, Helpers.php, Module.php, Assets.php, and test files directly support this goal. |\n|    Docstring Coverage    | ✅ Passed | Docstring coverage is 88.00% which is sufficient. The required threshold is 80.00%.                                                                                                                                                          |\n| Merge Conflict Detection | ✅ Passed | ✅ No merge conflicts detected when merging into `develop`                                                                                                                                                                                    |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `fix_invalid_api_token`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/plausible/wordpress/issues/comments/3908419403","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Mon, 16 Feb 2026 17:30:10 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"300C:FDD2C:3516D4:DF625A:69935420","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

@Dan0sz Dan0sz merged commit 174f01a into develop Feb 16, 2026
7 checks passed
@Dan0sz Dan0sz deleted the fix_invalid_api_token branch February 16, 2026 17:40
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.

1 participant