Skip to content
Merged
5 changes: 5 additions & 0 deletions src/Admin/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ private function is_ssl() {
* @since 1.3.0
*/
private function test_proxy( $run = true ) {
// Always succeed if this is a CI environment.
if ( defined( 'PLAUSIBLE_CI' ) ) {
return true;
}

// Should we run the test?
if ( ! apply_filters( 'plausible_analytics_module_run_test_proxy', $run ) ) {
return false; // @codeCoverageIgnore
Expand Down
76 changes: 41 additions & 35 deletions src/Ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ public function fetch_messages() {
public function quit_wizard() {
$request_data = $this->clean( $_REQUEST );

if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data[ '_nonce' ], 'plausible_analytics_quit_wizard' ) < 1 ) {
if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_quit_wizard' ) < 1 ) {
Messages::set_error( __( 'Not allowed', 'plausible-analytics' ) );

wp_send_json_error( null, 403 );
}

update_option( 'plausible_analytics_wizard_done', true );

$this->maybe_handle_redirect( $request_data[ 'redirect' ] );
$this->maybe_handle_redirect( $request_data['redirect'] );

wp_send_json_success();
}
Expand All @@ -93,12 +93,12 @@ public function quit_wizard() {
* Clean variables using `sanitize_text_field`.
* Arrays are cleaned recursively. Non-scalar values are ignored.
*
* @since 1.3.0
* @access public
*
* @param string|array $var Sanitize the variable.
*
* @return string|array
* @since 1.3.0
* @access public
*
*/
private function clean( $var ) {
// If the variable is an array, recursively apply the function to each element of the array.
Expand All @@ -111,8 +111,8 @@ private function clean( $var ) {
// Parse the variable using the wp_parse_url function.
$parsed = wp_parse_url( $var );
// If the variable has a scheme (e.g. http:// or https://), sanitize the variable using the esc_url_raw function.
if ( isset( $parsed[ 'scheme' ] ) ) {
return esc_url_raw( wp_unslash( $var ), [ $parsed[ 'scheme' ] ] );
if ( isset( $parsed['scheme'] ) ) {
return esc_url_raw( wp_unslash( $var ), [ $parsed['scheme'] ] );
}

// If the variable does not have a scheme, sanitize the variable using the sanitize_text_field function.
Expand Down Expand Up @@ -157,54 +157,54 @@ private function maybe_handle_redirect( $direction ) {
public function show_wizard() {
$request_data = $this->clean( $_REQUEST );

if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data[ '_nonce' ], 'plausible_analytics_show_wizard' ) < 1 ) {
if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_show_wizard' ) < 1 ) {
Messages::set_error( __( 'Not allowed.', 'plausible-analytics' ) );

wp_send_json_error( null, 403 );
}

delete_option( 'plausible_analytics_wizard_done' );

$this->maybe_handle_redirect( $request_data[ 'redirect' ] );
$this->maybe_handle_redirect( $request_data['redirect'] );

wp_send_json_success();
}

/**
* Save Admin Settings
*
* @since 1.0.0
* @return void
* @since 1.0.0
*/
public function toggle_option() {
// Sanitize all the post data before using.
$post_data = $this->clean( $_POST );
$settings = Helpers::get_settings();

if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data[ '_nonce' ], 'plausible_analytics_toggle_option' ) < 1 ) {
if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data['_nonce'], 'plausible_analytics_toggle_option' ) < 1 ) {
wp_send_json_error( __( 'Not allowed.', 'plausible-analytics' ), 403 );
}

if ( $post_data[ 'is_list' ] ) {
if ( $post_data['is_list'] ) {
/**
* Toggle lists.
*/
if ( $post_data[ 'toggle_status' ] === 'on' ) {
if ( $post_data['toggle_status'] === 'on' ) {
// If toggle is on, store the value under a new key.
if ( ! in_array( $post_data[ 'option_value' ], $settings[ $post_data[ 'option_name' ] ] ) ) {
$settings[ $post_data[ 'option_name' ] ][] = $post_data[ 'option_value' ];
if ( ! in_array( $post_data['option_value'], $settings[ $post_data['option_name'] ] ) ) {
$settings[ $post_data['option_name'] ][] = $post_data['option_value'];
}
} else {
// If toggle is off, find the key by its value and unset it.
if ( ( $key = array_search( $post_data[ 'option_value' ], $settings[ $post_data[ 'option_name' ] ] ) ) !== false ) {
unset( $settings[ $post_data[ 'option_name' ] ][ $key ] );
if ( ( $key = array_search( $post_data['option_value'], $settings[ $post_data['option_name'] ] ) ) !== false ) {
unset( $settings[ $post_data['option_name'] ][ $key ] );
}
}
} else {
/**
* Single toggles.
*/
$settings[ $post_data[ 'option_name' ] ] = $post_data[ 'toggle_status' ];
$settings[ $post_data['option_name'] ] = $post_data['toggle_status'];
}

// Update all the options to plausible settings.
Expand All @@ -213,22 +213,22 @@ public function toggle_option() {
/**
* Allow devs to perform additional actions.
*/
do_action( 'plausible_analytics_settings_saved', $settings, $post_data[ 'option_name' ], $post_data[ 'toggle_status' ] );
do_action( 'plausible_analytics_settings_saved', $settings, $post_data['option_name'], $post_data['toggle_status'] );

$option_label = $post_data[ 'option_label' ];
$toggle_status = $post_data[ 'toggle_status' ] === 'on' ? __( 'enabled', 'plausible-analytics' ) : __( 'disabled', 'plausible-analytics' );
$option_label = $post_data['option_label'];
$toggle_status = $post_data['toggle_status'] === 'on' ? __( 'enabled', 'plausible-analytics' ) : __( 'disabled', 'plausible-analytics' );
$message = apply_filters(
'plausible_analytics_toggle_option_success_message',
sprintf( '%s %s.', $option_label, $toggle_status ),
$post_data[ 'option_name' ],
$post_data[ 'toggle_status' ]
$post_data['option_name'],
$post_data['toggle_status']
);

Messages::set_success( $message );

$additional = $this->maybe_render_additional_message( $post_data[ 'option_name' ], $post_data[ 'toggle_status' ] );
$additional = $this->maybe_render_additional_message( $post_data['option_name'], $post_data['toggle_status'] );

Messages::set_additional( $additional, $post_data[ 'option_name' ] );
Messages::set_additional( $additional, $post_data['option_name'] );

wp_send_json_success( null, 200 );
}
Expand Down Expand Up @@ -271,17 +271,21 @@ public function save_options() {
$post_data = $this->clean( $_POST );
$settings = Helpers::get_settings();

if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data[ '_nonce' ], 'plausible_analytics_toggle_option' ) < 1 ) {
if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data['_nonce'], 'plausible_analytics_toggle_option' ) < 1 ) {
Messages::set_error( __( 'Not allowed.', 'plausible-analytics' ) );

wp_send_json_error( null, 403 );
}

$options = json_decode( $post_data[ 'options' ] );
$options = json_decode( stripslashes( $post_data['options'] ) );

if ( empty( $options ) ) {
Messages::set_error( __( 'No options found to save.', 'plausible-analytics' ) );

if ( defined( 'PLAUSIBLE_CI' ) ) {
return;
}

wp_send_json_error( null, 400 );
}

Expand All @@ -298,14 +302,14 @@ function ( $option ) {
);

if ( count( $input_array_elements ) > 0 ) {
$options = [];
$array_name = preg_replace( '/\[[0-9]+]/', '', $input_array_elements[ 0 ]->name );
$options[ 0 ] = (object) [];
$options[ 0 ]->name = $array_name;
$options = [];
$array_name = preg_replace( '/\[[0-9]+]/', '', $input_array_elements[0]->name );
$options[0] = (object) [];
$options[0]->name = $array_name;

foreach ( $input_array_elements as $input_array_element ) {
if ( $input_array_element->value ) {
$options[ 0 ]->value[] = $input_array_element->value;
$options[0]->value[] = $input_array_element->value;
}
}
}
Expand All @@ -328,16 +332,18 @@ function ( $option ) {
}

// Refresh Tracker ID if Domain Name has changed (e.g. after migration from staging to production)
if ($option->name === 'domain_name') {
delete_option('plausible_analytics_tracker_id');
if ( $option->name === 'domain_name' ) {
delete_option( 'plausible_analytics_tracker_id' );
}
}

update_option( 'plausible_analytics_settings', $settings );

Messages::set_success( __( 'Settings saved.', 'plausible-analytics' ) );

wp_send_json_success( null, 200 );
if ( ! defined( 'PLAUSIBLE_CI' ) ) {
wp_send_json_success( null, 200 );
}
}

/**
Expand Down
11 changes: 0 additions & 11 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,6 @@ public static function get_settings() {

$settings = get_option( 'plausible_analytics_settings', [] );

/**
* If this is an AJAX request, make sure the latest settings are used.
*/
if ( isset( $_POST['action'] ) && $_POST['action'] === 'plausible_analytics_save_options' ) {
$options = json_decode( str_replace( '\\', '', $_POST['options'] ) );

foreach ( $options as $option ) {
$settings[ $option->name ] = $option->value;
}
}

return apply_filters( 'plausible_analytics_settings', wp_parse_args( $settings, $defaults ) );
}

Expand Down
4 changes: 4 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public function __construct() {
define( 'PLAUSIBLE_TESTS_ROOT', __DIR__ . '/' );
}

if ( ! defined( 'PLAUSIBLE_CI' ) ) {
define( 'PLAUSIBLE_CI', true );
}

parent::__construct();
}

Expand Down
3 changes: 2 additions & 1 deletion tests/TestableHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

namespace Plausible\Analytics\Tests;

use Plausible\Analytics\WP\Client;
use Plausible\Analytics\WP\Helpers;

/**
Expand All @@ -15,7 +16,7 @@ class TestableHelpers extends Helpers {
* @return
*/
protected static function get_client() {
return new class {
return new class extends Client {
public function get_tracker_id() {
return 'pa-test-tracker-id';
}
Expand Down
111 changes: 111 additions & 0 deletions tests/integration/AjaxTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
/**
* @package Plausible Analytics Integration Tests - Ajax
*/

namespace Plausible\Analytics\Tests\Integration;

use Plausible\Analytics\Tests\TestCase;
use Plausible\Analytics\WP\Ajax;
use Plausible\Analytics\WP\Helpers;

class AjaxTest extends TestCase {
/**
* @var Ajax
*/
private $ajax;

/**
* Set up a test environment.
*/
public function setUp(): void {
parent::setUp();
$this->ajax = new Ajax();

// Ensure we are an admin for these tests.
$this->addUserCap( 'manage_options' );

// Mock nonce verification
add_filter( 'nonce_user_logged_out', '__return_true' );
}

/**
* Clean up after each test.
*
* @return void
*/
public function tearDown(): void {
parent::tearDown();

$_POST = [];

remove_filter( 'nonce_user_logged_out', '__return_true' );
}

/**
* Test save_options with normal JSON data.
*/
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
}

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

/**
* Test save_options with escaped JSON data (simulating WordPress's $_POST behavior).
* This specifically tests the fix with stripslashes().
*/
public function testSaveOptionsWithEscapedJson() {
$options = [
[ 'name' => 'domain_name', 'value' => 'escaped.com' ],
];

$json = wp_json_encode( $options );
$escaped_json = addslashes( $json );

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

try {
$this->ajax->save_options();
} catch ( \Exception $e ) {
}

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

/**
* Test save_options with invalid JSON.
*/
public function testSaveOptionsInvalidJson() {
$_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 ) {
}

// Verify that settings were NOT updated to something weird.
$settings = Helpers::get_settings();
$this->assertNotEquals( 'invalid-json', $settings['domain_name'] );
}
}
2 changes: 1 addition & 1 deletion tests/integration/HelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public function testGetPostSettings() {

$settings = Helpers::get_settings();

$this->assertArrayHasKey( 'post_test', $settings );
$this->assertArrayNotHasKey( 'post_test', $settings );
}

/**
Expand Down
Loading