Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion classes/WP_Piwik.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 22 additions & 2 deletions classes/WP_Piwik/Request/Rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,30 @@ 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 = [];

// 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 );
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 ) ];
Expand Down
41 changes: 37 additions & 4 deletions libs/matomo-php-tracker/MatomoTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -2184,17 +2192,42 @@ 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 = [];

// 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;

$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;
Expand Down
32 changes: 28 additions & 4 deletions proxy/proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,16 +410,37 @@ 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();

// 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 {
$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 {
Expand Down Expand Up @@ -458,7 +479,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(
Expand Down
5 changes: 4 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion tests/phpunit/AIBotTrackingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

Expand Down
135 changes: 135 additions & 0 deletions tests/phpunit/MatomoTrackerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace WP_Piwik\Tests;

if ( ! class_exists( '\WP_Piwik\MatomoTracker' ) ) {
require_once dirname( __DIR__, 2 ) . '/libs/matomo-php-tracker/MatomoTracker.php';
}

/**
* @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.boolFound
*/
class Testable_Matomo_Tracker extends \WP_Piwik\MatomoTracker {

private $curl_support;

public function __construct( $id_site, $curl_support ) {
parent::__construct( $id_site );
$this->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 {

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 );

$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() );
}

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 );

$address = stream_socket_get_name( $server, false );
fclose( $server );

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';
}
}
17 changes: 17 additions & 0 deletions tests/phpunit/ProxyIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading