Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Heartbeat: Report the site environment stats (WordPress/PHP versions, site configuration, etc.) for all connected sites.
130 changes: 130 additions & 0 deletions projects/packages/connection/src/class-heartbeat.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Automattic\Jetpack\Connection\Rest_Authentication;
use Automattic\Jetpack\Connection\REST_Connector;
use Automattic\Jetpack\Connection\Utils;
use Jetpack_Options;
use WP_CLI;
use WP_Error;
Expand Down Expand Up @@ -173,6 +174,135 @@ public static function generate_stats_array( $prefix = '' ) {
return $return;
}

/**
* Generates the site environment stats that are reported in the heartbeat.
*
* These describe the host environment (WordPress/PHP versions, site configuration, etc.)
* rather than the Jetpack plugin itself, so they live in the Connection package and are
* reported for every connected site, including standalone-connection installs.
*
* @since $$next-version$$
*
* @return array The environment stats array, keyed by unprefixed stat name.
*/
public static function get_environment_stats() {
$stats = array();

$stats['wp-version'] = get_bloginfo( 'version' );
$stats['php-version'] = PHP_VERSION;
$stats['wp-branch'] = (float) get_bloginfo( 'version' );
$stats['php-branch'] = (float) PHP_VERSION;
$stats['public'] = Jetpack_Options::get_option( 'public' );
$stats['ssl'] = self::permit_ssl();
$stats['is-https'] = is_ssl() ? 'https' : 'http';
$stats['language'] = get_bloginfo( 'language' );
$stats['charset'] = get_bloginfo( 'charset' );
$stats['is-multisite'] = is_multisite() ? 'multisite' : 'singlesite';
$stats['plugins'] = implode( ',', self::get_active_plugins() );

if ( function_exists( 'get_mu_plugins' ) ) {
$stats['mu-plugins'] = implode( ',', array_keys( get_mu_plugins() ) );
}

if ( function_exists( 'get_space_used' ) ) { // Only available in multisite.
$space_used = get_space_used();
} else {
// This is the same as `get_space_used`, except it does not apply the short-circuit filter.
$upload_dir = wp_upload_dir();
$space_used = get_dirsize( $upload_dir['basedir'] ) / MB_IN_BYTES;
}

$stats['space-used'] = $space_used;

// is-multi-network can have three values, `single-site`, `single-network`, and `multi-network`.
$stats['is-multi-network'] = 'single-site';
if ( is_multisite() ) {
$stats['is-multi-network'] = ( new Status() )->is_multi_network() ? 'multi-network' : 'single-network';
}

if ( ! empty( $_SERVER['SERVER_ADDR'] ) || ! empty( $_SERVER['LOCAL_ADDR'] ) ) {
$ip = ! empty( $_SERVER['SERVER_ADDR'] ) ? wp_unslash( $_SERVER['SERVER_ADDR'] ) : wp_unslash( $_SERVER['LOCAL_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized just below.
$ip_arr = array_map( 'intval', explode( '.', $ip ) );
if ( 4 === count( $ip_arr ) ) {
$stats['ip-2-octets'] = implode( '.', array_slice( $ip_arr, 0, 2 ) );
}
}

return $stats;
}

/**
* Checks whether the site can connect to WordPress.com over SSL.
*
* Ported from the Jetpack plugin so the `ssl` heartbeat stat can be generated from the
* Connection package. Shares the `jetpack_https_test` transient with the plugin's copy
* to avoid duplicate network checks.
*
* @since $$next-version$$
*
* @param bool $force_recheck Force the SSL recheck instead of using the cached result.
* @return bool
*/
private static function permit_ssl( $force_recheck = false ) {
$ssl = false;
if ( ! $force_recheck ) {
$ssl = get_transient( 'jetpack_https_test' );
}

if ( $force_recheck || false === $ssl ) {
$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
if ( ! $api_base ) {
$api_base = Utils::DEFAULT_JETPACK__API_BASE;
}

if ( ! str_starts_with( $api_base, 'https' ) ) {
$ssl = 0;
} else {
$ssl = 1;

if ( ! wp_http_supports( array( 'ssl' => true ) ) ) {
$ssl = 0;
} else {
$response = wp_remote_get( $api_base . 'test/1/' );
if ( is_wp_error( $response ) || 'OK' !== wp_remote_retrieve_body( $response ) ) {
$ssl = 0;
}
}
}
set_transient( 'jetpack_https_test', $ssl, DAY_IN_SECONDS );
}

return (bool) $ssl;
}

/**
* Gets all plugins currently active, regardless of whether they're traditionally
* activated or network activated.
*
* Ported from the Jetpack plugin so the `plugins` heartbeat stat can be generated from
* the Connection package.
*
* @since $$next-version$$
*
* @return array
*/
private static function get_active_plugins() {
$active_plugins = (array) get_option( 'active_plugins', array() );

if ( is_multisite() ) {
// Due to legacy code, active_sitewide_plugins stores them in the keys,
// whereas active_plugins stores them in the values.
$network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
if ( $network_plugins ) {
$active_plugins = array_merge( $active_plugins, $network_plugins );
}
}

sort( $active_plugins );

return array_unique( $active_plugins );
}

/**
* Registers jetpack.getHeartbeatData xmlrpc method
*
Expand Down
4 changes: 4 additions & 0 deletions projects/packages/connection/src/class-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,7 @@ public function get_signed_token( $token ) {
*
* @since 6.11.0 Add the list of Jetpack package versions to the heartbeat.
* @since 8.7.4 Add the missing connection owner and XML-RPC error stats to the heartbeat.
* @since $$next-version$$ Add the site environment stats (WordPress/PHP versions, etc.) to the heartbeat.
*
* @param array $stats The Heartbeat stats array.
* @return array $stats
Expand Down Expand Up @@ -2810,6 +2811,9 @@ public function add_stats_to_heartbeat( $stats ) {
\Jetpack_Options::delete_option( 'xmlrpc_errors' );
}

// Site environment stats (WordPress/PHP versions, site configuration, etc.).
$stats = array_merge( $stats, Heartbeat::get_environment_stats() );

return $stats;
}

Expand Down
94 changes: 94 additions & 0 deletions projects/packages/connection/tests/php/Heartbeat_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* Tests for the Heartbeat class.
*
* @package automattic/jetpack-connection
*/

namespace Automattic\Jetpack;

use PHPUnit\Framework\Attributes\CoversClass;
use WorDBless\BaseTestCase;

/**
* Class Heartbeat_Test
*
* @covers Automattic\Jetpack\Heartbeat
*/
#[CoversClass( Heartbeat::class )]
class Heartbeat_Test extends BaseTestCase {

/**
* Pre-seed the SSL test transient so `get_environment_stats()` does not make a network request.
*/
public function set_up() {
set_transient( 'jetpack_https_test', 1 );
}

/**
* Clean up after each test.
*/
public function tear_down() {
delete_transient( 'jetpack_https_test' );
}

/**
* `get_environment_stats()` returns the expected environment stat keys.
*/
public function test_get_environment_stats_returns_expected_keys() {
$stats = Heartbeat::get_environment_stats();

$expected_keys = array(
'wp-version',
'php-version',
'wp-branch',
'php-branch',
'public',
'ssl',
'is-https',
'language',
'charset',
'is-multisite',
'plugins',
'space-used',
'is-multi-network',
);

foreach ( $expected_keys as $key ) {
$this->assertArrayHasKey( $key, $stats );
}
}

/**
* `get_environment_stats()` reports the current WordPress and PHP versions.
*/
public function test_get_environment_stats_reports_versions() {
$stats = Heartbeat::get_environment_stats();

$this->assertSame( get_bloginfo( 'version' ), $stats['wp-version'] );
$this->assertSame( PHP_VERSION, $stats['php-version'] );
}

/**
* `get_environment_stats()` uses the cached SSL test result instead of making a request.
*/
public function test_get_environment_stats_uses_cached_ssl_result() {
set_transient( 'jetpack_https_test', 1 );
$stats = Heartbeat::get_environment_stats();
$this->assertTrue( $stats['ssl'] );

set_transient( 'jetpack_https_test', 0 );
$stats = Heartbeat::get_environment_stats();
$this->assertFalse( $stats['ssl'] );
}

/**
* `get_environment_stats()` reports site/network topology for a single-site install.
*/
public function test_get_environment_stats_reports_single_site_topology() {
$stats = Heartbeat::get_environment_stats();

$this->assertSame( 'singlesite', $stats['is-multisite'] );
$this->assertSame( 'single-site', $stats['is-multi-network'] );
}
}
6 changes: 6 additions & 0 deletions projects/packages/connection/tests/php/ManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,15 @@ public function test_add_stats_to_heartbeat_reports_missing_owner() {

// `add_stats_to_heartbeat()` reads the connected plugins list, which requires Plugin_Storage to be configured.
Plugin_Storage::configure();
// Avoid a network request for the `ssl` environment stat.
set_transient( 'jetpack_https_test', 1 );

$stats = $manager->add_stats_to_heartbeat( array() );

$this->assertArrayHasKey( 'missing-owner', $stats );
$this->assertTrue( $stats['missing-owner'] );
// Site environment stats are merged in from the Connection Heartbeat.
$this->assertArrayHasKey( 'wp-version', $stats );
}

/**
Expand All @@ -238,6 +242,8 @@ public function test_add_stats_to_heartbeat_reports_and_clears_xmlrpc_errors() {

// `add_stats_to_heartbeat()` reads the connected plugins list, which requires Plugin_Storage to be configured.
Plugin_Storage::configure();
// Avoid a network request for the `ssl` environment stat.
set_transient( 'jetpack_https_test', 1 );

Jetpack_Options::update_option( 'xmlrpc_errors', array( 'malformed_token' => true ) );

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: other

Heartbeat: Site environment stats are now provided by the Connection package.
48 changes: 7 additions & 41 deletions projects/plugins/jetpack/class.jetpack-heartbeat.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ private function __construct() {
}

/**
* Generates heartbeat stats data.
* Generates Jetpack plugin heartbeat stats data.
*
* Site environment stats (WordPress/PHP versions, site configuration, etc.) are generated by
* the Connection package via `Automattic\Jetpack\Heartbeat::get_environment_stats()` so they are
* reported for every connected site. This method only returns Jetpack-plugin-specific stats.
*
* @param string $prefix Prefix to add before stats identifier.
*
Expand All @@ -63,48 +67,10 @@ private function __construct() {
public static function generate_stats_array( $prefix = '' ) {
$return = array();

$return[ "{$prefix}version" ] = JETPACK__VERSION;
$return[ "{$prefix}wp-version" ] = get_bloginfo( 'version' );
$return[ "{$prefix}php-version" ] = PHP_VERSION;
$return[ "{$prefix}branch" ] = (float) JETPACK__VERSION;
$return[ "{$prefix}wp-branch" ] = (float) get_bloginfo( 'version' );
$return[ "{$prefix}php-branch" ] = (float) PHP_VERSION;
$return[ "{$prefix}public" ] = Jetpack_Options::get_option( 'public' );
$return[ "{$prefix}ssl" ] = Jetpack::permit_ssl();
$return[ "{$prefix}is-https" ] = is_ssl() ? 'https' : 'http';
$return[ "{$prefix}language" ] = get_bloginfo( 'language' );
$return[ "{$prefix}charset" ] = get_bloginfo( 'charset' );
$return[ "{$prefix}is-multisite" ] = is_multisite() ? 'multisite' : 'singlesite';
$return[ "{$prefix}plugins" ] = implode( ',', Jetpack::get_active_plugins() );
if ( function_exists( 'get_mu_plugins' ) ) {
$return[ "{$prefix}mu-plugins" ] = implode( ',', array_keys( get_mu_plugins() ) );
}
$return[ "{$prefix}version" ] = JETPACK__VERSION;
$return[ "{$prefix}branch" ] = (float) JETPACK__VERSION;
$return[ "{$prefix}manage-enabled" ] = true;

if ( function_exists( 'get_space_used' ) ) { // Only available in multisite.
$space_used = get_space_used();
} else {
// This is the same as `get_space_used`, except it does not apply the short-circuit filter.
$upload_dir = wp_upload_dir();
$space_used = get_dirsize( $upload_dir['basedir'] ) / MB_IN_BYTES;
}

$return[ "{$prefix}space-used" ] = $space_used;

// is-multi-network can have three values, `single-site`, `single-network`, and `multi-network`.
$return[ "{$prefix}is-multi-network" ] = 'single-site';
if ( is_multisite() ) {
$return[ "{$prefix}is-multi-network" ] = Jetpack::is_multi_network() ? 'multi-network' : 'single-network';
}

if ( ! empty( $_SERVER['SERVER_ADDR'] ) || ! empty( $_SERVER['LOCAL_ADDR'] ) ) {
$ip = ! empty( $_SERVER['SERVER_ADDR'] ) ? wp_unslash( $_SERVER['SERVER_ADDR'] ) : wp_unslash( $_SERVER['LOCAL_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized just below.
$ip_arr = array_map( 'intval', explode( '.', $ip ) );
if ( 4 === count( $ip_arr ) ) {
$return[ "{$prefix}ip-2-octets" ] = implode( '.', array_slice( $ip_arr, 0, 2 ) );
}
}

foreach ( Jetpack::get_available_modules() as $slug ) {
$return[ "{$prefix}module-{$slug}" ] = Jetpack::is_module_active( $slug ) ? 'on' : 'off';
}
Expand Down
7 changes: 5 additions & 2 deletions projects/plugins/jetpack/class.jetpack.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Automattic\Jetpack\Device_Detection\User_Agent_Info;
use Automattic\Jetpack\Errors;
use Automattic\Jetpack\Files;
use Automattic\Jetpack\Heartbeat;
use Automattic\Jetpack\Identity_Crisis;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Modules;
Expand Down Expand Up @@ -3253,7 +3254,8 @@ public static function log_settings_change( $option, $old_value, $value ) {
* @return array|string Stats data. Array if $encode is false. JSON-encoded string is $encode is true.
*/
public static function get_stat_data( $encode = true, $extended = true ) {
$data = Jetpack_Heartbeat::generate_stats_array();
// Site environment stats now live in the Connection package; merge them with the Jetpack-specific stats.
$data = array_merge( Jetpack_Heartbeat::generate_stats_array(), Heartbeat::get_environment_stats() );

if ( $extended ) {
$additional_data = self::get_additional_stat_data();
Expand Down Expand Up @@ -5761,7 +5763,8 @@ public static function absolutize_css_urls( $css, $css_file_url ) {
* $return array $filtered_data
*/
public static function jetpack_check_heartbeat_data() {
$raw_data = Jetpack_Heartbeat::generate_stats_array();
// Site environment stats (incl. wp-version/php-version checked below) now live in the Connection package.
$raw_data = array_merge( Jetpack_Heartbeat::generate_stats_array(), Heartbeat::get_environment_stats() );

$good = array();
$caution = array();
Expand Down
Loading