diff --git a/src/admin/class-settings-page.php b/src/admin/class-settings-page.php index 34015e9..bd4a0af 100644 --- a/src/admin/class-settings-page.php +++ b/src/admin/class-settings-page.php @@ -171,6 +171,7 @@ public function handle_form_submission(): void { 'website_urn' => isset( $_POST['website_urn'] ) ? sanitize_text_field( wp_unslash( $_POST['website_urn'] ) ) : '', 'merchant_api_key' => isset( $_POST['merchant_api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['merchant_api_key'] ) ) : null, 'bot_protection_enabled' => isset( $_POST['bot_protection_enabled'] ), + 'analytics_enabled' => isset( $_POST['analytics_enabled'] ), 'active_paths' => isset( $_POST['active_paths'] ) && is_array( $_POST['active_paths'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['active_paths'] ) ) : array(), @@ -202,11 +203,13 @@ private function process_save_settings( array $form_data ): void { } elseif ( '' === $merchant_api_key ) { // API key field was shown but submitted empty — disable bot protection. $this->settings->set_bot_protection_enabled( false ); + $this->settings->set_analytics_enabled( false ); } // Bot protection settings are only present when API key is already saved. if ( null === $merchant_api_key ) { $this->settings->set_bot_protection_enabled( $form_data['bot_protection_enabled'] ); + $this->settings->set_analytics_enabled( $form_data['analytics_enabled'] ); $raw_paths = $form_data['active_paths']; @@ -296,6 +299,7 @@ public function render_page(): void { 'website_urn' => $this->settings->get_website_urn(), 'license_url' => home_url( '/license.xml' ), 'bot_protection_enabled' => $this->settings->is_bot_protection_enabled(), + 'analytics_enabled' => $this->settings->is_analytics_enabled(), 'active_paths' => $this->settings->get_active_paths(), 'site_url' => home_url( '/' ), 'resource_links' => array( diff --git a/src/class-plugin.php b/src/class-plugin.php index efc09d0..9dd3f28 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -82,7 +82,8 @@ public function init(): void { $status_handler = new Status_Handler( $settings, SUPERTAB_CONNECT_API_BASE_URL, $http_client ); $status_handler->register(); - $analytics_enabled = $settings->has_merchant_api_key() && $settings->is_bot_protection_enabled(); + $bot_protection_active = $settings->has_merchant_api_key() && $settings->is_bot_protection_enabled(); + $analytics_enabled = $bot_protection_active && $settings->is_analytics_enabled(); $dispatcher = null; if ( $analytics_enabled && self::should_use_wp_queue() ) { @@ -95,8 +96,8 @@ public function init(): void { return; } - if ( $analytics_enabled && ! defined( 'REST_REQUEST' ) && ! wp_doing_cron() ) { - $this->init_bot_protection( $settings, $http_client, $dispatcher ); + if ( $bot_protection_active && ! defined( 'REST_REQUEST' ) && ! wp_doing_cron() ) { + $this->init_bot_protection( $settings, $http_client, $dispatcher, $analytics_enabled ); } } @@ -148,13 +149,14 @@ public function add_privacy_policy_content(): void { /** * Initialize bot protection for front-end requests. * - * @param Settings $settings Settings manager. - * @param HttpClientInterface $http_client HTTP client for SDK requests. - * @param ?Analytics_Dispatcher $dispatcher When set, analytics events are queued via this - * dispatcher; when null, the SDK's default transport is used. + * @param Settings $settings Settings manager. + * @param HttpClientInterface $http_client HTTP client for SDK requests. + * @param ?Analytics_Dispatcher $dispatcher When set, analytics events are queued via this + * dispatcher; when null, the SDK's default transport is used. + * @param bool $analytics_enabled Whether the merchant opted in to analytics. * @return void */ - private function init_bot_protection( Settings $settings, HttpClientInterface $http_client, ?Analytics_Dispatcher $dispatcher ): void { + private function init_bot_protection( Settings $settings, HttpClientInterface $http_client, ?Analytics_Dispatcher $dispatcher, bool $analytics_enabled ): void { $enforcement = self::get_enforcement_mode(); if ( null !== $dispatcher ) { @@ -174,7 +176,7 @@ private function init_bot_protection( Settings $settings, HttpClientInterface $h httpClient: $http_client, baseUrl: SUPERTAB_CONNECT_API_BASE_URL, cache: new WP_Transient_Cache(), - analyticsEnabled: true, + analyticsEnabled: $analytics_enabled, analyticsTransport: $analytics_transport, ); $bot_protection = new Bot_Protection( $supertab_connect, $settings ); diff --git a/src/class-settings.php b/src/class-settings.php index 778c15c..4951220 100644 --- a/src/class-settings.php +++ b/src/class-settings.php @@ -35,6 +35,13 @@ class Settings { */ private const OPTION_BOT_PROTECTION_ENABLED = 'supertab_connect_bot_protection_enabled'; + /** + * Option name for the analytics enabled flag. + * + * @var string + */ + private const OPTION_ANALYTICS_ENABLED = 'supertab_connect_analytics_enabled'; + /** * Option name for active paths. * @@ -106,6 +113,27 @@ public function set_bot_protection_enabled( bool $enabled ): void { update_option( self::OPTION_BOT_PROTECTION_ENABLED, $enabled ); } + /** + * Check if analytics is enabled. + * + * Opt-in: absent or falsy means disabled. + * + * @return bool True if analytics is enabled. + */ + public function is_analytics_enabled(): bool { + return (bool) get_option( self::OPTION_ANALYTICS_ENABLED, false ); + } + + /** + * Set the analytics enabled flag. + * + * @param bool $enabled Whether analytics should be enabled. + * @return void + */ + public function set_analytics_enabled( bool $enabled ): void { + update_option( self::OPTION_ANALYTICS_ENABLED, $enabled ); + } + /** * Get the active paths for bot protection. * @@ -173,6 +201,7 @@ public function delete(): void { delete_option( self::OPTION_MERCHANT_API_KEY ); delete_option( self::OPTION_WEBSITE_URN ); delete_option( self::OPTION_BOT_PROTECTION_ENABLED ); + delete_option( self::OPTION_ANALYTICS_ENABLED ); delete_option( self::OPTION_ACTIVE_PATHS ); // Invalidate cached license XML. diff --git a/src/class-status-handler.php b/src/class-status-handler.php index fc3711b..0ac96b5 100644 --- a/src/class-status-handler.php +++ b/src/class-status-handler.php @@ -153,11 +153,13 @@ public function build_response( string $authorization_header, string $expected_a ); } - // Mirrors the gate in Plugin::init(): bot protection (and with it, - // event reporting) only runs when both conditions hold. + // Bot protection gate: requires both merchant API key and enabled flag. + // Event reporting additionally requires opt-in analytics. $protection_active = $this->settings->has_merchant_api_key() && $this->settings->is_bot_protection_enabled(); + $event_reporting = $protection_active && $this->settings->is_analytics_enabled(); + return array( 'status' => 200, 'body' => (string) wp_json_encode( @@ -169,7 +171,7 @@ public function build_response( string $authorization_header, string $expected_a 'version' => SUPERTAB_CONNECT_VERSION, ), 'enforcement' => $protection_active ? Plugin::get_enforcement_mode()->value : 'disabled', - 'eventReporting' => $protection_active, + 'eventReporting' => $event_reporting, ) ), ); diff --git a/templates/settings.php b/templates/settings.php index d3f46d2..8ee6e19 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -19,6 +19,7 @@ $supertab_connect_website_urn = $template_data['website_urn']; $supertab_connect_license_url = $template_data['license_url']; $supertab_connect_bot_protection_enabled = $template_data['bot_protection_enabled']; +$supertab_connect_analytics_enabled = $template_data['analytics_enabled']; $supertab_connect_active_paths = $template_data['active_paths']; $supertab_connect_site_url = $template_data['site_url']; $supertab_connect_resource_links = $template_data['resource_links']; @@ -184,6 +185,24 @@ class="wp-hide-pw hide-if-no-js"
> + +
+ +
+ +

+ +

+

diff --git a/tests/SettingsTest.php b/tests/SettingsTest.php index 0e26325..a275592 100644 --- a/tests/SettingsTest.php +++ b/tests/SettingsTest.php @@ -125,6 +125,34 @@ public function test_set_bot_protection_enabled_can_disable(): void { $this->assertFalse( $settings->is_bot_protection_enabled() ); } + public function test_is_analytics_enabled_returns_false_by_default(): void { + $settings = new Settings(); + $this->assertFalse( $settings->is_analytics_enabled() ); + } + + public function test_set_analytics_enabled_stores_value(): void { + $settings = new Settings(); + $settings->set_analytics_enabled( true ); + + $this->assertTrue( $settings->is_analytics_enabled() ); + } + + public function test_set_analytics_enabled_can_disable(): void { + $settings = new Settings(); + $settings->set_analytics_enabled( true ); + $settings->set_analytics_enabled( false ); + + $this->assertFalse( $settings->is_analytics_enabled() ); + } + + public function test_delete_removes_analytics_setting(): void { + $settings = new Settings(); + $settings->set_analytics_enabled( true ); + $settings->delete(); + + $this->assertFalse( $settings->is_analytics_enabled() ); + } + public function test_delete_removes_bot_protection_setting(): void { $settings = new Settings(); $settings->set_bot_protection_enabled( true ); diff --git a/tests/StatusHandlerTest.php b/tests/StatusHandlerTest.php index 8056aae..023ae9f 100644 --- a/tests/StatusHandlerTest.php +++ b/tests/StatusHandlerTest.php @@ -142,6 +142,7 @@ static function ( string $token, string $audience ) use ( &$seen ): bool { public function test_valid_challenge_returns_status_payload(): void { $this->enable_bot_protection(); + $this->settings->set_analytics_enabled( true ); $handler = $this->create_handler( static fn (): bool => true ); $response = $handler->build_response( 'Bearer good-token', 'https://example.com' ); @@ -178,6 +179,20 @@ public function test_disabled_bot_protection_reports_disabled(): void { $this->assertFalse( $payload['eventReporting'] ); } + public function test_analytics_opt_in_required_for_event_reporting(): void { + $this->enable_bot_protection(); + // Analytics enabled flag left off (opt-in default: disabled). + + $handler = $this->create_handler( static fn (): bool => true ); + $response = $handler->build_response( 'Bearer good-token', 'https://example.com' ); + + $payload = json_decode( $response['body'], true ); + + $this->assertSame( 200, $response['status'] ); + $this->assertSame( 'observe', $payload['enforcement'] ); + $this->assertFalse( $payload['eventReporting'], 'eventReporting must be false when analytics is not opt-in enabled.' ); + } + public function test_missing_credentials_reports_disabled(): void { $this->settings->set_bot_protection_enabled( true ); // No merchant API key saved. diff --git a/uninstall.php b/uninstall.php index bd315e9..eda388f 100644 --- a/uninstall.php +++ b/uninstall.php @@ -18,6 +18,7 @@ delete_option( 'supertab_connect_merchant_api_key' ); delete_option( 'supertab_connect_website_urn' ); delete_option( 'supertab_connect_bot_protection_enabled' ); +delete_option( 'supertab_connect_analytics_enabled' ); delete_option( 'supertab_connect_active_paths' ); // Remove transients.