From 1eedf87a4001e5f31933f3d678c130e1b09209cd Mon Sep 17 00:00:00 2001 From: diosmosis Date: Tue, 15 Sep 2026 13:47:41 -0700 Subject: [PATCH 1/2] fix php 8.5 deprecations --- .github/workflows/phpunit.yml | 10 +++- classes/WP_Piwik.php | 2 +- classes/WP_Piwik/Request/Rest.php | 19 +++++- libs/matomo-php-tracker/MatomoTracker.php | 36 ++++++++++-- proxy/proxy.php | 27 +++++++-- readme.txt | 5 +- tests/phpunit/AIBotTrackingTest.php | 6 +- tests/phpunit/MatomoTrackerTest.php | 70 +++++++++++++++++++++++ tests/phpunit/ProxyIntegrationTest.php | 17 ++++++ wp-piwik.php | 2 +- 10 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 tests/phpunit/MatomoTrackerTest.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index f04af9a..f279167 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -25,10 +25,15 @@ concurrency: jobs: phpunit: - name: PHPUnit (DDEV) + name: PHPUnit (DDEV, PHP ${{ matrix.php }}) runs-on: ubuntu-latest timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + php: [ "7.4", "8.5" ] + steps: - name: Checkout uses: actions/checkout@v4 @@ -40,6 +45,9 @@ jobs: with: autostart: false + - name: Select the PHP version + run: ddev config --php-version=${{ matrix.php }} + - name: Start the environment run: ddev start diff --git a/classes/WP_Piwik.php b/classes/WP_Piwik.php index 36fcfe9..d116b79 100644 --- a/classes/WP_Piwik.php +++ b/classes/WP_Piwik.php @@ -25,7 +25,7 @@ class WP_Piwik { const DISMISS_SHORTCODE_NOTICE_ARG = 'wp-piwik-dismiss-shortcode-notice'; private static $revision_id = 2023092201; - private static $version = '1.1.11'; + private static $version = '1.1.12'; private static $blog_id; private static $plugin_basename = null; private static $logger; diff --git a/classes/WP_Piwik/Request/Rest.php b/classes/WP_Piwik/Request/Rest.php index 9a32c8c..aee49ca 100644 --- a/classes/WP_Piwik/Request/Rest.php +++ b/classes/WP_Piwik/Request/Rest.php @@ -140,10 +140,25 @@ private function fopen( $id, $url, $params ) { } $context = stream_context_create( $context_definition ); + // $http_response_header must be assigned before the fallback read below: PHP 8.5 deprecated + // the predefined variable and reports the read at compile time, so it would otherwise emit a + // notice merely by loading this file. PHP still overwrites the value. $http_response_header = []; + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - $response = @file_get_contents( $full_url, false, $context ); - self::$last_error = $this->check_response( $response, isset( $http_response_header[0] ) ? $http_response_header[0] : '' ); + $response = @file_get_contents( $full_url, false, $context ); + + $response_headers = []; + if ( function_exists( 'http_get_last_response_headers' ) ) { + $headers = http_get_last_response_headers(); + if ( is_array( $headers ) ) { + $response_headers = $headers; + } + } else { + $response_headers = $http_response_header; + } + + self::$last_error = $this->check_response( $response, isset( $response_headers[0] ) ? $response_headers[0] : '' ); $result = $this->unserialize( $response ); if ( $GLOBALS ['wp-piwik_debug'] ) { self::$debug[ $id ] = [ get_headers( $full_url, 1 ), ( $this->should_use_post() ? 'POST ' : 'GET ' ) . $url . ' ' . $this->build_param_string( $params, true ) ]; diff --git a/libs/matomo-php-tracker/MatomoTracker.php b/libs/matomo-php-tracker/MatomoTracker.php index 2a37b17..bccae1f 100755 --- a/libs/matomo-php-tracker/MatomoTracker.php +++ b/libs/matomo-php-tracker/MatomoTracker.php @@ -2097,6 +2097,14 @@ protected function prepareStreamOptions(string $method, $data, bool $forcePostUr return $stream_options; } + /** + * @ignore + */ + protected function hasCurlSupport(): bool + { + return function_exists('curl_init') && function_exists('curl_exec'); + } + /** * @ignore */ @@ -2155,7 +2163,7 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $content = ''; - if (function_exists('curl_init') && function_exists('curl_exec')) { + if ($this->hasCurlSupport()) { $options = $this->prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded); $ch = curl_init(); @@ -2184,17 +2192,37 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $this->parseIncomingCookies(explode("\r\n", $header)); } finally { - curl_close($ch); + if (version_compare(PHP_VERSION, '8', '<')) { + // no-op since PHP 8.0 and deprecated since PHP 8.5 + curl_close($ch); + } ob_end_clean(); } } elseif (function_exists('stream_context_create')) { $stream_options = $this->prepareStreamOptions($method, $data, $forcePostUrlEncoded); $ctx = stream_context_create($stream_options); - $response = file_get_contents($url, 0, $ctx); + + // $http_response_header must be assigned before the fallback read below: PHP 8.5 + // deprecated the predefined variable and reports it when read, so the read would + // otherwise emit a notice merely by loading this file. PHP still overwrites the value. + $http_response_header = []; + + // a tracking request that cannot reach Matomo must not emit a warning into the page + $response = @file_get_contents($url, 0, $ctx); $content = $response; - $this->parseIncomingCookies($http_response_header); + $responseHeaders = []; + if (function_exists('http_get_last_response_headers')) { + $headers = http_get_last_response_headers(); + if (is_array($headers)) { + $responseHeaders = $headers; + } + } else { + $responseHeaders = $http_response_header; + } + + $this->parseIncomingCookies($responseHeaders); } return $content; diff --git a/proxy/proxy.php b/proxy/proxy.php index fa3633f..9bfa39b 100644 --- a/proxy/proxy.php +++ b/proxy/proxy.php @@ -410,16 +410,32 @@ function getHttpContentAndStatus($url, $timeout, $user_agent, $postBody = '') if ($useFopen) { $ctx = stream_context_create($stream_options); + // WP-Matomo customization: $http_response_header must be assigned before the fallback read + // below. PHP 8.5 deprecated the predefined variable and reports the read at compile time, + // so it would otherwise emit a notice merely by loading this file. PHP still overwrites + // the value. + $http_response_header = array(); + if ($DEBUG_PROXY) { $content = file_get_contents($url, 0, $ctx); } else { $content = @file_get_contents($url, 0, $ctx); } + $responseHeaders = array(); + if (function_exists('http_get_last_response_headers')) { + $headers = http_get_last_response_headers(); + if (is_array($headers)) { + $responseHeaders = $headers; + } + } else { + $responseHeaders = $http_response_header; + } + $httpStatus = ''; - if (isset($http_response_header[0])) { - $httpStatus = $http_response_header[0]; - $httpResponseHeaders = array_slice($http_response_header, 1); + if (isset($responseHeaders[0])) { + $httpStatus = $responseHeaders[0]; + $httpResponseHeaders = array_slice($responseHeaders, 1); $httpResponseHeaders = array_map('transformHeaderLine', $httpResponseHeaders); } } else { @@ -458,7 +474,10 @@ function getHttpContentAndStatus($url, $timeout, $user_agent, $postBody = '') if (!empty($httpStatus)) { $httpStatus = 'HTTP/1.1 ' . $httpStatus; } - curl_close($ch); + if (version_compare(PHP_VERSION, '8', '<')) { + // no-op since PHP 8.0 and deprecated since PHP 8.5 + curl_close($ch); + } } return array( diff --git a/readme.txt b/readme.txt index 1dac6ba..4a9beb1 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: Braekling Requires at least: 5.0 Tested up to: 7.0.0 -Stable tag: 1.1.11 +Stable tag: 1.1.12 Tags: matomo, tracking, statistics, stats, analytics Adds Matomo (former Piwik) statistics to your WordPress dashboard and is also able to add the Matomo Tracking Code to your blog. @@ -155,6 +155,9 @@ Add WP-Matomo to your /wp-content/plugins folder and enable it as [Network Plugi == Changelog == += 1.1.12 = +* Bug fix: PHP 8.5 no longer reports deprecation notices for the bundled Matomo tracker, the tracking proxy and requests to the Matomo HTTP API. + = 1.1.11 = * Deprecation: the overview and post shortcodes ([wp-piwik], [wp-piwik module="overview"] and [wp-piwik module="post"]) are deprecated and will be removed in the next major release, which will be published by November 2026 at the latest. Matomo's Widgetize feature is the supported replacement: https://matomo.org/faq/reports/embed-a-matomo-report-in-a-html-page/. The opt-out shortcode is not deprecated. * Deprecation: the "Self-hosted (PHP API)" connection method is deprecated and will be removed in the next major release, which will be published by November 2026 at the latest. It is no longer offered when connecting a new site, and sites still using it are shown a notice. Switch to "Self-hosted (HTTP API)" and enter the URL of your Matomo instance instead. diff --git a/tests/phpunit/AIBotTrackingTest.php b/tests/phpunit/AIBotTrackingTest.php index 83cfd3d..ea29fa4 100644 --- a/tests/phpunit/AIBotTrackingTest.php +++ b/tests/phpunit/AIBotTrackingTest.php @@ -20,7 +20,11 @@ public function tear_down() { private function reset_tracked_flag() { $property = new \ReflectionProperty( AIBotTracking::class, 'ai_bot_tracked' ); - $property->setAccessible( true ); + if ( PHP_VERSION_ID < 80100 ) { + // implicit since PHP 8.1 and deprecated since PHP 8.5 + // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated + $property->setAccessible( true ); + } $property->setValue( null, false ); } diff --git a/tests/phpunit/MatomoTrackerTest.php b/tests/phpunit/MatomoTrackerTest.php new file mode 100644 index 0000000..d90387c --- /dev/null +++ b/tests/phpunit/MatomoTrackerTest.php @@ -0,0 +1,70 @@ +curl_support = $curl_support; + } + + public function send_request_public( $url ) { + return $this->sendRequest( $url ); + } + + protected function hasCurlSupport(): bool { + return $this->curl_support; + } +} + +/** + * @phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + */ +class MatomoTrackerTest extends WP_Piwik_TestCase { + + public function test_sendRequest_should_return_an_empty_string_when_matomo_cannot_be_reached_without_curl() { + $tracker = new Testable_Matomo_Tracker( 1, false ); + + $this->assertSame( '', $tracker->send_request_public( $this->get_unreachable_url() ) ); + } + + public function test_sendRequest_should_leave_the_incoming_cookies_empty_when_matomo_cannot_be_reached_without_curl() { + $tracker = new Testable_Matomo_Tracker( 1, false ); + + $tracker->send_request_public( $this->get_unreachable_url() ); + + $this->assertSame( [], $tracker->incomingTrackerCookies ); + } + + public function test_sendRequest_should_report_the_transport_error_when_matomo_cannot_be_reached_with_curl() { + if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) { + self::markTestSkipped( 'the curl extension is not available' ); + } + + $tracker = new Testable_Matomo_Tracker( 1, true ); + + $this->expectException( \RuntimeException::class ); + + $tracker->send_request_public( $this->get_unreachable_url() ); + } + + private function get_unreachable_url() { + $server = stream_socket_server( 'tcp://127.0.0.1:0', $errno, $errstr ); + $this->assertNotFalse( $server, 'could not reserve a port: ' . $errstr ); + + $address = stream_socket_get_name( $server, false ); + fclose( $server ); + + return 'http://' . $address . '/matomo.php?idsite=1&rec=1'; + } +} diff --git a/tests/phpunit/ProxyIntegrationTest.php b/tests/phpunit/ProxyIntegrationTest.php index a4957b3..cbde2e3 100644 --- a/tests/phpunit/ProxyIntegrationTest.php +++ b/tests/phpunit/ProxyIntegrationTest.php @@ -207,6 +207,23 @@ public function test_proxy_forwards_the_upstream_status_code() { $this->assertSame( 204, $response->status ); } + public function test_proxy_forwards_an_upstream_error_status_and_its_headers() { + $harness = self::$harness; + $harness->set_matomo_tracker_response( + 500, + [ + 'Content-Type' => 'text/plain', + 'Access-Control-Allow-Origin' => '*', + ], + 'Internal Server Error' + ); + + $response = $harness->get( 'matomo.php', [ 'idsite' => 1 ] ); + + $this->assertSame( 500, $response->status ); + $this->assertSame( '*', $response->headers['access-control-allow-origin'] ); + } + public function test_proxy_forwards_whitelisted_headers_and_strips_secure_from_cookies() { $harness = self::$harness; $harness->set_matomo_tracker_response( diff --git a/wp-piwik.php b/wp-piwik.php index ffdd8a2..11400c7 100644 --- a/wp-piwik.php +++ b/wp-piwik.php @@ -3,7 +3,7 @@ * Plugin Name: Connect Matomo * Plugin URI: http://wordpress.org/extend/plugins/wp-piwik/ * Description: Adds Matomo statistics to your WordPress dashboard and is also able to add the Matomo Tracking Code to your blog. - * Version: 1.1.11 + * Version: 1.1.12 * Author: Matomo, André Bräkling * Author URI: https://matomo.org * Text Domain: wp-piwik From 0878b0dd76760e1374ba7dda87f3778511cbdf1a Mon Sep 17 00:00:00 2001 From: diosmosis Date: Tue, 15 Sep 2026 15:31:16 -0700 Subject: [PATCH 2/2] clear any previous response headers before making a request --- classes/WP_Piwik/Request/Rest.php | 5 ++ libs/matomo-php-tracker/MatomoTracker.php | 5 ++ proxy/proxy.php | 5 ++ tests/phpunit/MatomoTrackerTest.php | 65 +++++++++++++++++++++++ tests/phpunit/RestIntegrationTest.php | 22 +++++++- 5 files changed, 100 insertions(+), 2 deletions(-) diff --git a/classes/WP_Piwik/Request/Rest.php b/classes/WP_Piwik/Request/Rest.php index aee49ca..ed02d6c 100644 --- a/classes/WP_Piwik/Request/Rest.php +++ b/classes/WP_Piwik/Request/Rest.php @@ -145,6 +145,11 @@ private function fopen( $id, $url, $params ) { // notice merely by loading this file. PHP still overwrites the value. $http_response_header = []; + // clear any stored response headers before a request + if ( function_exists( 'http_clear_last_response_headers' ) ) { + http_clear_last_response_headers(); + } + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $response = @file_get_contents( $full_url, false, $context ); diff --git a/libs/matomo-php-tracker/MatomoTracker.php b/libs/matomo-php-tracker/MatomoTracker.php index bccae1f..4165643 100755 --- a/libs/matomo-php-tracker/MatomoTracker.php +++ b/libs/matomo-php-tracker/MatomoTracker.php @@ -2208,6 +2208,11 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null // otherwise emit a notice merely by loading this file. PHP still overwrites the value. $http_response_header = []; + // clear any stored response headers before a request + if (function_exists('http_clear_last_response_headers')) { + http_clear_last_response_headers(); + } + // a tracking request that cannot reach Matomo must not emit a warning into the page $response = @file_get_contents($url, 0, $ctx); $content = $response; diff --git a/proxy/proxy.php b/proxy/proxy.php index 9bfa39b..f9e1b23 100644 --- a/proxy/proxy.php +++ b/proxy/proxy.php @@ -416,6 +416,11 @@ function getHttpContentAndStatus($url, $timeout, $user_agent, $postBody = '') // the value. $http_response_header = array(); + // WP-Matomo customization: clear any stored response headers before a request + if (function_exists('http_clear_last_response_headers')) { + http_clear_last_response_headers(); + } + if ($DEBUG_PROXY) { $content = file_get_contents($url, 0, $ctx); } else { diff --git a/tests/phpunit/MatomoTrackerTest.php b/tests/phpunit/MatomoTrackerTest.php index d90387c..995b548 100644 --- a/tests/phpunit/MatomoTrackerTest.php +++ b/tests/phpunit/MatomoTrackerTest.php @@ -32,6 +32,16 @@ protected function hasCurlSupport(): bool { */ class MatomoTrackerTest extends WP_Piwik_TestCase { + private $mock_response_file; + + public function tear_down() { + if ( $this->mock_response_file && file_exists( $this->mock_response_file ) ) { + unlink( $this->mock_response_file ); + $this->mock_response_file = null; + } + parent::tear_down(); + } + public function test_sendRequest_should_return_an_empty_string_when_matomo_cannot_be_reached_without_curl() { $tracker = new Testable_Matomo_Tracker( 1, false ); @@ -58,6 +68,21 @@ public function test_sendRequest_should_report_the_transport_error_when_matomo_c $tracker->send_request_public( $this->get_unreachable_url() ); } + public function test_sendRequest_should_not_parse_an_earlier_responses_cookies_when_the_url_cannot_be_requested_over_http() { + $mock_url = $this->serve_mock_matomo( [ 'Set-Cookie' => 'stale_cookie=from_the_first_response; Path=/' ] ); + + $tracker = new Testable_Matomo_Tracker( 1, false ); + $tracker->send_request_public( $mock_url . '?idsite=1&rec=1' ); + + $this->assertSame( [ 'stale_cookie' => 'from_the_first_response' ], $tracker->incomingTrackerCookies ); + + // a Matomo URL saved without a scheme is handled by the file stream wrapper, which leaves + // the response headers PHP 8.4+ keeps process wide untouched + $tracker->send_request_public( 'stats.example.org/matomo.php?idsite=1&rec=1' ); + + $this->assertSame( [], $tracker->incomingTrackerCookies ); + } + private function get_unreachable_url() { $server = stream_socket_server( 'tcp://127.0.0.1:0', $errno, $errstr ); $this->assertNotFalse( $server, 'could not reserve a port: ' . $errstr ); @@ -67,4 +92,44 @@ private function get_unreachable_url() { return 'http://' . $address . '/matomo.php?idsite=1&rec=1'; } + + /** + * Points the mock Matomo endpoint at tests/phpunit/proxy/mock/ at a response with the given + * headers and returns its URL. Needs a web server serving this plugin, so it skips without one. + * + * @param array $headers response headers the mock should send + * @return string URL of the mock tracking endpoint + * @throws \Exception If the directory the mock reads its response from cannot be created. + */ + private function serve_mock_matomo( array $headers ) { + if ( ! getenv( 'WP_MATOMO_INTEGRATION_TESTS' ) ) { + self::markTestSkipped( 'WP_MATOMO_INTEGRATION_TESTS is not set, cannot run this test' ); + } + + $runtime = __DIR__ . '/proxy/mock/runtime'; + if ( ! is_dir( $runtime ) && ! mkdir( $runtime, 0777, true ) && ! is_dir( $runtime ) ) { + throw new \Exception( 'Could not create the mock matomo directory' ); + } + chmod( $runtime, 0777 ); + + $response_file = $runtime . '/response.json'; + file_put_contents( + $response_file, + wp_json_encode( + [ + 'status' => 200, + 'headers' => $headers, + 'body' => 'MOCKGIF', + ] + ) + ); + chmod( $response_file, 0666 ); + + $this->mock_response_file = $response_file; + + $plugins_path = wp_parse_url( plugins_url(), PHP_URL_PATH ); + $plugin_dir = basename( dirname( __DIR__, 2 ) ); + + return 'http://localhost' . rtrim( $plugins_path, '/' ) . '/' . $plugin_dir . '/tests/phpunit/proxy/mock/matomo.php'; + } } diff --git a/tests/phpunit/RestIntegrationTest.php b/tests/phpunit/RestIntegrationTest.php index 5f59145..e942093 100644 --- a/tests/phpunit/RestIntegrationTest.php +++ b/tests/phpunit/RestIntegrationTest.php @@ -67,7 +67,7 @@ private function mock_url( $trailing_slash = true ) { . '/tests/phpunit/rest/mock' . ( $trailing_slash ? '/' : '' ); } - private function perform_bulk_request( $url, $connection, $method ) { + private function perform_bulk_request( $url, $connection, $method, $date = '2015-01-01' ) { $settings = $this->create_settings( [ 'piwik_mode' => 'http', @@ -87,7 +87,7 @@ private function perform_bulk_request( $url, $connection, $method ) { 'VisitsSummary.get', [ 'period' => 'day', - 'date' => '2015-01-01', + 'date' => $date, ] ); @@ -252,4 +252,22 @@ public function test_a_server_error_is_reported( $connection ) { $this->assertNotSame( '', $error ); $this->assertStringContainsString( '500', $error ); } + + public function test_perform_should_not_report_an_earlier_responses_status_when_the_matomo_url_is_not_an_http_url() { + $not_matomo = $this->runtime . '/not-matomo.txt'; + $this->write_runtime_file( 'not-matomo.txt', 'this is not JSON' ); + + // a first request over HTTP, so that there is a stored 200 response to hand back + list( $result ) = $this->perform_bulk_request( $this->mock_url(), 'fopen', 'post' ); + $this->assertNotFalse( $result ); + + // results are keyed by the request id, so the second request has to ask for another date + // for the request to be made at all + list( $result, $error ) = $this->perform_bulk_request( $not_matomo, 'fopen', 'post', '2015-01-02' ); + unlink( $not_matomo ); + + $this->assertFalse( $result ); + $this->assertStringContainsString( 'valid JSON', $error ); + $this->assertStringNotContainsString( '200', $error ); + } }