From 4ecca7ed9a57d4ae2ef35ee53d2d4095de8f110a Mon Sep 17 00:00:00 2001 From: Christoffer Jakob Woldbye Romild Date: Wed, 4 Feb 2026 13:41:03 +0100 Subject: [PATCH 1/2] Resolve script-injection problem --- .distignore | 9 +- betterlytics.php | 2 +- includes/class-betterlytics.php | 4 +- public/class-betterlytics-public.php | 110 +++++++++++------ readme.txt | 2 +- tests/test-public.php | 173 +++++++++++++++++++-------- 6 files changed, 202 insertions(+), 98 deletions(-) diff --git a/.distignore b/.distignore index 85b14e6..8def903 100644 --- a/.distignore +++ b/.distignore @@ -1,24 +1,23 @@ .git .github -.agent -.claude .editorconfig -.env -.env.example .gitignore .distignore +.dockerignore Dockerfile.test docker-compose.yml phpcs.xml.dist phpunit.xml.dist composer.json -composer.lock vendor/ tests/ bin/ build/ demo/ node_modules/ +package.sh +package.json +pnpm-lock.yaml CONTRIBUTING.md CONFIGURATION.md .wordpress-org/ diff --git a/betterlytics.php b/betterlytics.php index 5e98a30..f4d891c 100644 --- a/betterlytics.php +++ b/betterlytics.php @@ -12,7 +12,7 @@ * Plugin URI: https://github.com/betterlytics/betterlytics-wordpress * Description: Privacy-first analytics for WordPress. Automatically adds the Betterlytics tracking script and provides easy WordPress action hooks integration. * Version: 1.0.0 - * Requires at least: 5.0 + * Requires at least: 6.3 * Requires PHP: 7.4 * Author: Betterlytics * Author URI: https://betterlytics.io diff --git a/includes/class-betterlytics.php b/includes/class-betterlytics.php index e60c4bc..4e37c99 100644 --- a/includes/class-betterlytics.php +++ b/includes/class-betterlytics.php @@ -125,8 +125,8 @@ private function define_admin_hooks() { private function define_public_hooks() { $plugin_public = new Betterlytics_Public( $this->get_plugin_name(), $this->get_version() ); - $this->loader->add_action( 'wp_head', $plugin_public, 'inject_tracking_script', 1 ); - $this->loader->add_action( 'wp_footer', $plugin_public, 'output_queued_events', 99 ); + $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'inject_tracking_script', 1 ); + $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'output_queued_events', 99 ); $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_event_scripts' ); // Initialize custom hooks integration. diff --git a/public/class-betterlytics-public.php b/public/class-betterlytics-public.php index 0e7fbd0..5f0f204 100644 --- a/public/class-betterlytics-public.php +++ b/public/class-betterlytics-public.php @@ -7,6 +7,10 @@ * @since 1.0.0 */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Public-facing functionality. */ @@ -39,7 +43,7 @@ public function __construct( $plugin_name, $version ) { } /** - * Inject the Betterlytics tracking script. + * Enqueue the Betterlytics tracking script. * * @since 1.0.0 */ @@ -58,41 +62,69 @@ public function inject_tracking_script() { return; } - $options = Betterlytics_Options::get_options(); - $site_id = esc_attr( $options['site_id'] ); - $server_url = esc_url( $options['server_url'] ); - $script_url = esc_url( $options['script_url'] ); - $track_outbound = esc_attr( $options['track_outbound']['mode'] ); - $web_vitals = ! empty( $options['track_web_vitals'] ) ? 'true' : 'false'; + $options = Betterlytics_Options::get_options(); + $script_url = $options['script_url']; + + // Register and enqueue the external tracking script with async strategy (WP 6.3+). + wp_enqueue_script( + 'betterlytics-tracker', + $script_url, + [], + null, + [ + 'strategy' => 'async', + 'in_footer' => false, + ] + ); - // Debug: Output configuration as HTML comment. + // Add filter for data-* attributes. + add_filter( 'script_loader_tag', [ $this, 'add_tracker_script_attributes' ], 10, 2 ); + + // Build inline script for event queue buffer. + $inline_script = 'window.betterlytics = window.betterlytics || { event: function() { (window.betterlytics.q = window.betterlytics.q || []).push(arguments); } };'; + + // Add debug logging if WP_DEBUG is enabled. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - echo "\n"; - echo '\n"; - echo '\n"; - echo '\n"; + $site_id = esc_js( $options['site_id'] ); + $server_url = esc_js( $options['server_url'] ); + $script_url_js = esc_js( $script_url ); + $track_outbound = esc_js( $options['track_outbound']['mode'] ); + + $inline_script .= "\nconsole.log('[Betterlytics] Script injected', { siteId: '{$site_id}', serverUrl: '{$server_url}', scriptUrl: '{$script_url_js}', trackOutbound: '{$track_outbound}' });"; } - // Output async event queue for reliable tracking before script loads. - ?> - - - - . + $tag = str_replace( '>', $data_attrs . '>', $tag ); + + return $tag; } /** @@ -131,23 +163,25 @@ public function enqueue_event_scripts() { } /** - * Output queued events as JavaScript. + * Output queued events as inline JavaScript. * * @since 1.0.0 */ public function output_queued_events() { global $betterlytics_queued_events; - if ( empty( $betterlytics_queued_events ) ) { + if ( empty( $betterlytics_queued_events ) || ! wp_script_is( 'betterlytics-tracker', 'enqueued' ) ) { return; } - echo "\n"; + + // Add queued events as inline script AFTER the main tracking script. + wp_add_inline_script( 'betterlytics-tracker', implode( "\n", $script_lines ), 'after' ); } } diff --git a/readme.txt b/readme.txt index 90935c6..95bf5b0 100644 --- a/readme.txt +++ b/readme.txt @@ -1,7 +1,7 @@ === Betterlytics === Contributors: betterlytics Tags: analytics, privacy, gdpr, cookieless, statistics -Requires at least: 5.0 +Requires at least: 6.3 Tested up to: 6.9 Stable tag: 1.0.0 Requires PHP: 7.4 diff --git a/tests/test-public.php b/tests/test-public.php index 709f276..21c10f3 100644 --- a/tests/test-public.php +++ b/tests/test-public.php @@ -25,10 +25,14 @@ class Test_Betterlytics_Public extends Betterlytics_Test_Case { public function set_up() { parent::set_up(); $this->public = new Betterlytics_Public( 'betterlytics', '1.0.0' ); + + // Reset WordPress scripts for each test. + global $wp_scripts; + $wp_scripts = null; } /** - * Test that tracking script is not output when disabled. + * Test that tracking script is not enqueued when disabled. */ public function test_script_not_output_when_disabled() { $this->set_options( @@ -38,15 +42,13 @@ public function test_script_not_output_when_disabled() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringNotContainsString( 'assertFalse( wp_script_is( 'betterlytics-tracker', 'enqueued' ) ); } /** - * Test that tracking script is not output without site_id. + * Test that tracking script is not enqueued without site_id. */ public function test_script_not_output_without_site_id() { $this->set_options( @@ -56,15 +58,13 @@ public function test_script_not_output_without_site_id() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringNotContainsString( 'assertFalse( wp_script_is( 'betterlytics-tracker', 'enqueued' ) ); } /** - * Test that tracking script is output when properly configured. + * Test that tracking script is enqueued when properly configured. */ public function test_script_output_when_configured() { $this->set_options( @@ -76,16 +76,22 @@ public function test_script_output_when_configured() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - - // Check that the script contains expected elements. - $this->assertStringContainsString( 'window.betterlytics', $output ); - $this->assertStringContainsString( 'data-site-id="my-test-site"', $output ); - $this->assertStringContainsString( 'data-server-url="https://analytics.example.com/event"', $output ); - $this->assertStringContainsString( 'src="https://analytics.example.com/script.js"', $output ); - $this->assertStringContainsString( 'async', $output ); + + // Check that the script is enqueued. + $this->assertTrue( wp_script_is( 'betterlytics-tracker', 'enqueued' ) ); + + // Check the script URL. + global $wp_scripts; + $this->assertEquals( + 'https://analytics.example.com/script.js', + $wp_scripts->registered['betterlytics-tracker']->src + ); + + // Check inline script before. + $inline_before = $wp_scripts->get_data( 'betterlytics-tracker', 'before' ); + $this->assertNotEmpty( $inline_before ); + $this->assertStringContainsString( 'window.betterlytics', implode( '', $inline_before ) ); } /** @@ -99,17 +105,19 @@ public function test_event_queue_initialized() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); + + global $wp_scripts; + $inline_before = $wp_scripts->get_data( 'betterlytics-tracker', 'before' ); + $inline_script = implode( '', $inline_before ); // Check for the queue initialization code. - $this->assertStringContainsString( 'window.betterlytics.q', $output ); - $this->assertStringContainsString( 'push(arguments)', $output ); + $this->assertStringContainsString( 'window.betterlytics.q', $inline_script ); + $this->assertStringContainsString( 'push(arguments)', $inline_script ); } /** - * Test queued events are output correctly. + * Test queued events are added as inline script. */ public function test_queued_events_output() { global $betterlytics_queued_events; @@ -123,27 +131,50 @@ public function test_queued_events_output() { ), ); - ob_start(); + // First enqueue the tracker script. + $this->set_options( + array( + 'enabled' => true, + 'site_id' => 'test-site', + ) + ); + $this->public->inject_tracking_script(); + + // Now output queued events. $this->public->output_queued_events(); - $output = ob_get_clean(); - $this->assertStringContainsString( "betterlytics.event('test-event'", $output ); - $this->assertStringContainsString( '"wp_hook":"test_hook"', $output ); - $this->assertStringContainsString( '"value":123', $output ); + global $wp_scripts; + $inline_after = $wp_scripts->get_data( 'betterlytics-tracker', 'after' ); + $inline_script = implode( '', $inline_after ); + + $this->assertStringContainsString( "betterlytics.event('test-event'", $inline_script ); + $this->assertStringContainsString( '"wp_hook":"test_hook"', $inline_script ); + $this->assertStringContainsString( '"value":123', $inline_script ); } /** - * Test no output when no queued events. + * Test no inline script added when no queued events. */ public function test_no_output_when_no_queued_events() { global $betterlytics_queued_events; $betterlytics_queued_events = array(); - ob_start(); + // First enqueue the tracker script. + $this->set_options( + array( + 'enabled' => true, + 'site_id' => 'test-site', + ) + ); + $this->public->inject_tracking_script(); + + // Now output queued events (should do nothing). $this->public->output_queued_events(); - $output = ob_get_clean(); - $this->assertEmpty( $output ); + global $wp_scripts; + $inline_after = $wp_scripts->get_data( 'betterlytics-tracker', 'after' ); + + $this->assertEmpty( $inline_after ); } /** @@ -162,12 +193,24 @@ public function test_multiple_queued_events_output() { ), ); - ob_start(); + // First enqueue the tracker script. + $this->set_options( + array( + 'enabled' => true, + 'site_id' => 'test-site', + ) + ); + $this->public->inject_tracking_script(); + + // Now output queued events. $this->public->output_queued_events(); - $output = ob_get_clean(); - $this->assertStringContainsString( "betterlytics.event('event-one'", $output ); - $this->assertStringContainsString( "betterlytics.event('event-two'", $output ); + global $wp_scripts; + $inline_after = $wp_scripts->get_data( 'betterlytics-tracker', 'after' ); + $inline_script = implode( '', $inline_after ); + + $this->assertStringContainsString( "betterlytics.event('event-one'", $inline_script ); + $this->assertStringContainsString( "betterlytics.event('event-two'", $inline_script ); } /** @@ -182,11 +225,13 @@ public function test_web_vitals_enabled() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringContainsString( 'data-web-vitals="true"', $output ); + // Check the data attributes via the filter method. + $test_tag = ''; + $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' ); + + $this->assertStringContainsString( 'data-web-vitals="true"', $result ); } /** @@ -201,11 +246,13 @@ public function test_web_vitals_disabled() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringContainsString( 'data-web-vitals="false"', $output ); + // Check the data attributes via the filter method. + $test_tag = ''; + $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' ); + + $this->assertStringContainsString( 'data-web-vitals="false"', $result ); } /** @@ -220,11 +267,13 @@ public function test_outbound_links_domain_mode() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringContainsString( 'data-outbound-links="domain"', $output ); + // Check the data attributes via the filter method. + $test_tag = ''; + $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' ); + + $this->assertStringContainsString( 'data-outbound-links="domain"', $result ); } /** @@ -239,11 +288,13 @@ public function test_outbound_links_full_mode() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringContainsString( 'data-outbound-links="full"', $output ); + // Check the data attributes via the filter method. + $test_tag = ''; + $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' ); + + $this->assertStringContainsString( 'data-outbound-links="full"', $result ); } /** @@ -258,10 +309,30 @@ public function test_outbound_links_off_mode() { ) ); - ob_start(); $this->public->inject_tracking_script(); - $output = ob_get_clean(); - $this->assertStringContainsString( 'data-outbound-links="off"', $output ); + // Check the data attributes via the filter method. + $test_tag = ''; + $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' ); + + $this->assertStringContainsString( 'data-outbound-links="off"', $result ); + } + + /** + * Test that filter doesn't modify other script handles. + */ + public function test_filter_ignores_other_handles() { + $this->set_options( + array( + 'enabled' => true, + 'site_id' => 'test-site', + ) + ); + + $test_tag = ''; + $result = $this->public->add_tracker_script_attributes( $test_tag, 'other-script' ); + + // Should return unchanged. + $this->assertEquals( $test_tag, $result ); } } From dffebfe85059e80072935ec2f30269cd5cba397e Mon Sep 17 00:00:00 2001 From: Christoffer Jakob Woldbye Romild Date: Wed, 4 Feb 2026 13:51:09 +0100 Subject: [PATCH 2/2] - Resolve indentation issues - Add validation to script URL - Add PHPCS ignore for null version --- public/class-betterlytics-public.php | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/public/class-betterlytics-public.php b/public/class-betterlytics-public.php index 5f0f204..68d0681 100644 --- a/public/class-betterlytics-public.php +++ b/public/class-betterlytics-public.php @@ -48,9 +48,10 @@ public function __construct( $plugin_name, $version ) { * @since 1.0.0 */ public function inject_tracking_script() { + $options = Betterlytics_Options::get_options(); + // Debug: Log tracking check. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - $options = Betterlytics_Options::get_options(); $tracking_status = Betterlytics_Options::is_tracking_enabled() ? 'ENABLED' : 'DISABLED'; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug only. error_log( '[Betterlytics] Tracking status: ' . $tracking_status ); @@ -62,14 +63,19 @@ public function inject_tracking_script() { return; } - $options = Betterlytics_Options::get_options(); $script_url = $options['script_url']; + // Validate script URL before enqueueing. + if ( empty( $script_url ) || ! filter_var( $script_url, FILTER_VALIDATE_URL ) ) { + return; + } + // Register and enqueue the external tracking script with async strategy (WP 6.3+). wp_enqueue_script( 'betterlytics-tracker', $script_url, [], + // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- External script URL; version managed by remote server. null, [ 'strategy' => 'async', @@ -144,22 +150,22 @@ public function enqueue_event_scripts() { return; } - wp_enqueue_script( - 'betterlytics-events', - BETTERLYTICS_PLUGIN_URL . 'public/js/betterlytics-events.js', - [], - $this->version, - true - ); - - wp_localize_script( + wp_enqueue_script( 'betterlytics-events', - 'betterlyticsEvents', - [ - 'trackDownloads' => ! empty( $options['track_downloads']['enabled'] ), - 'trackCustomHtmlAttr' => ! empty( $options['track_custom_html_attribute']['enabled'] ), - ] + BETTERLYTICS_PLUGIN_URL . 'public/js/betterlytics-events.js', + [], + $this->version, + true ); + + wp_localize_script( + 'betterlytics-events', + 'betterlyticsEvents', + [ + 'trackDownloads' => ! empty( $options['track_downloads']['enabled'] ), + 'trackCustomHtmlAttr' => ! empty( $options['track_custom_html_attribute']['enabled'] ), + ] + ); } /**