diff --git a/projects/packages/connection/changelog/update-heartbeat-env-stats-to-package b/projects/packages/connection/changelog/update-heartbeat-env-stats-to-package new file mode 100644 index 000000000000..b46f23646dc3 --- /dev/null +++ b/projects/packages/connection/changelog/update-heartbeat-env-stats-to-package @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Heartbeat: Report the site environment stats (WordPress/PHP versions, site configuration, etc.) for all connected sites. diff --git a/projects/packages/connection/src/class-heartbeat.php b/projects/packages/connection/src/class-heartbeat.php index cc32b5363de3..c300bd353b34 100644 --- a/projects/packages/connection/src/class-heartbeat.php +++ b/projects/packages/connection/src/class-heartbeat.php @@ -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; @@ -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 * diff --git a/projects/packages/connection/src/class-manager.php b/projects/packages/connection/src/class-manager.php index 94b53854deaa..ad2b87f76a14 100644 --- a/projects/packages/connection/src/class-manager.php +++ b/projects/packages/connection/src/class-manager.php @@ -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 @@ -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; } diff --git a/projects/packages/connection/tests/php/Heartbeat_Test.php b/projects/packages/connection/tests/php/Heartbeat_Test.php new file mode 100644 index 000000000000..a29e728b640c --- /dev/null +++ b/projects/packages/connection/tests/php/Heartbeat_Test.php @@ -0,0 +1,94 @@ +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'] ); + } +} diff --git a/projects/packages/connection/tests/php/ManagerTest.php b/projects/packages/connection/tests/php/ManagerTest.php index 45446f568250..732d75931fa0 100644 --- a/projects/packages/connection/tests/php/ManagerTest.php +++ b/projects/packages/connection/tests/php/ManagerTest.php @@ -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 ); } /** @@ -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 ) ); diff --git a/projects/plugins/jetpack/changelog/update-heartbeat-env-stats-to-package b/projects/plugins/jetpack/changelog/update-heartbeat-env-stats-to-package new file mode 100644 index 000000000000..812977e12084 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-heartbeat-env-stats-to-package @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Heartbeat: Site environment stats are now provided by the Connection package. diff --git a/projects/plugins/jetpack/class.jetpack-heartbeat.php b/projects/plugins/jetpack/class.jetpack-heartbeat.php index 4dac9853dfe3..f2ece0a569a8 100644 --- a/projects/plugins/jetpack/class.jetpack-heartbeat.php +++ b/projects/plugins/jetpack/class.jetpack-heartbeat.php @@ -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. * @@ -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'; } diff --git a/projects/plugins/jetpack/class.jetpack.php b/projects/plugins/jetpack/class.jetpack.php index 28c44d84c71a..d72546ac3bc5 100644 --- a/projects/plugins/jetpack/class.jetpack.php +++ b/projects/plugins/jetpack/class.jetpack.php @@ -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; @@ -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(); @@ -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();