PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-connection/src/class-heartbeat.php +231 -0 12.0.3 → 16.3 View file →
@@ -6,10 +6,16 @@
6 6 */
7 7
8 8 namespace Automattic\Jetpack;
9 9
10 +use Automattic\Jetpack\Connection\Rest_Authentication;
11 +use Automattic\Jetpack\Connection\REST_Connector;
12 +use Automattic\Jetpack\Connection\Utils;
10 13 use Jetpack_Options;
11 14 use WP_CLI;
15 +use WP_Error;
16 +use WP_REST_Request;
17 +use WP_REST_Server;
12 18
13 19 /**
14 20 * Heartbeat sends a batch of stats to wp.com once a day
15 21 */
@@ -72,8 +78,10 @@
72 78
73 79 if ( defined( 'WP_CLI' ) && WP_CLI ) {
74 80 WP_CLI::add_command( 'jetpack-heartbeat', array( $this, 'cli_callback' ) );
75 81 }
82 +
83 + add_action( 'rest_api_init', array( $this, 'initialize_rest_api' ) );
76 84 }
77 85
78 86 /**
79 87 * Method that gets executed on the wp-cron call
@@ -166,8 +174,181 @@
166 174 return $return;
167 175 }
168 176
169 177 /**
178 + * Generates the site environment stats that are reported in the heartbeat.
179 + *
180 + * These describe the host environment (WordPress/PHP versions, site configuration, etc.)
181 + * rather than the Jetpack plugin itself, so they live in the Connection package and are
182 + * reported for every connected site, including standalone-connection installs.
183 + *
184 + * @since 8.7.9
185 + *
186 + * @return array The environment stats array, keyed by unprefixed stat name.
187 + */
188 + public static function get_environment_stats() {
189 + $stats = array();
190 +
191 + $stats['wp-version'] = get_bloginfo( 'version' );
192 + $stats['php-version'] = PHP_VERSION;
193 + $stats['wp-branch'] = (float) get_bloginfo( 'version' );
194 + $stats['php-branch'] = (float) PHP_VERSION;
195 + $stats['public'] = Jetpack_Options::get_option( 'public' );
196 + $stats['ssl'] = self::permit_ssl();
197 + $stats['is-https'] = is_ssl() ? 'https' : 'http';
198 + $stats['language'] = get_bloginfo( 'language' );
199 + $stats['charset'] = get_bloginfo( 'charset' );
200 + $stats['is-multisite'] = is_multisite() ? 'multisite' : 'singlesite';
201 + $stats['plugins'] = implode( ',', self::get_active_plugins() );
202 +
203 + if ( function_exists( 'get_mu_plugins' ) ) {
204 + $stats['mu-plugins'] = implode( ',', array_keys( get_mu_plugins() ) );
205 + }
206 +
207 + if ( function_exists( 'get_space_used' ) ) { // Only available in multisite.
208 + $space_used = get_space_used();
209 + } else {
210 + // This is the same as `get_space_used`, except it does not apply the short-circuit filter.
211 + $upload_dir = wp_upload_dir();
212 + $space_used = get_dirsize( $upload_dir['basedir'] ) / MB_IN_BYTES;
213 + }
214 +
215 + $stats['space-used'] = $space_used;
216 +
217 + // is-multi-network can have three values, `single-site`, `single-network`, and `multi-network`.
218 + $stats['is-multi-network'] = 'single-site';
219 + if ( is_multisite() ) {
220 + $stats['is-multi-network'] = ( new Status() )->is_multi_network() ? 'multi-network' : 'single-network';
221 + }
222 +
223 + if ( ! empty( $_SERVER['SERVER_ADDR'] ) || ! empty( $_SERVER['LOCAL_ADDR'] ) ) {
224 + $ip = ! empty( $_SERVER['SERVER_ADDR'] ) ? wp_unslash( $_SERVER['SERVER_ADDR'] ) : wp_unslash( $_SERVER['LOCAL_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized just below.
225 + $ip_arr = array_map( 'intval', explode( '.', $ip ) );
226 + if ( 4 === count( $ip_arr ) ) {
227 + $stats['ip-2-octets'] = implode( '.', array_slice( $ip_arr, 0, 2 ) );
228 + }
229 + }
230 +
231 + return $stats;
232 + }
233 +
234 + /**
235 + * Checks whether the site can connect to WordPress.com over SSL.
236 + *
237 + * This is the canonical SSL connectivity check. It caches both the boolean result (in the
238 + * `jetpack_https_test` transient) and a structured failure reason (via
239 + * {@see self::get_ssl_test_error()}) so a single check serves both the `ssl` heartbeat stat
240 + * and consumers such as the Jetpack plugin's admin notice, which renders a localized message
241 + * from the reason code. This avoids duplicate network checks and keeps translated strings out
242 + * of the package.
243 + *
244 + * @since 8.7.9
245 + *
246 + * @param bool $force_recheck Force the SSL recheck instead of using the cached result.
247 + * @return bool Whether the site can connect to WordPress.com over SSL.
248 + */
249 + public static function permit_ssl( $force_recheck = false ) {
250 + $ssl = false;
251 + if ( ! $force_recheck ) {
252 + $ssl = get_transient( 'jetpack_https_test' );
253 + }
254 +
255 + if ( $force_recheck || false === $ssl ) {
256 + $error = array(
257 + 'code' => '',
258 + 'detail' => '',
259 + );
260 +
261 + $api_base = Constants::get_constant( 'JETPACK__API_BASE' );
262 + if ( ! $api_base ) {
263 + $api_base = Utils::DEFAULT_JETPACK__API_BASE;
264 + }
265 +
266 + if ( ! str_starts_with( $api_base, 'https' ) ) {
267 + $ssl = 0;
268 + } else {
269 + $ssl = 1;
270 +
271 + if ( ! wp_http_supports( array( 'ssl' => true ) ) ) {
272 + $ssl = 0;
273 + $error['code'] = 'no_ssl_support';
274 + } else {
275 + $response = wp_remote_get( $api_base . 'test/1/' );
276 + if ( is_wp_error( $response ) ) {
277 + $ssl = 0;
278 + $error['code'] = 'no_ssl_support';
279 + } elseif ( 'OK' !== wp_remote_retrieve_body( $response ) ) {
280 + $ssl = 0;
281 + $error['code'] = 'bad_response';
282 + $error['detail'] = wp_remote_retrieve_body( $response );
283 + }
284 + }
285 + }
286 + set_transient( 'jetpack_https_test', $ssl, DAY_IN_SECONDS );
287 + set_transient( 'jetpack_https_test_error', $error, DAY_IN_SECONDS );
288 + }
289 +
290 + return (bool) $ssl;
291 + }
292 +
293 + /**
294 + * Returns the structured reason for the last SSL connectivity failure.
295 + *
296 + * Consumers can map the returned reason code to a localized message. The `detail` value
297 + * carries any additional context (e.g. the unexpected response body for `bad_response`).
298 + *
299 + * @since 8.7.9
300 + *
301 + * @return array {
302 + * The last SSL test error.
303 + *
304 + * @type string $code Reason code: '' (no error), 'no_ssl_support', or 'bad_response'.
305 + * @type string $detail Additional context for the failure, if any.
306 + * }
307 + */
308 + public static function get_ssl_test_error() {
309 + $error = get_transient( 'jetpack_https_test_error' );
310 +
311 + if ( ! is_array( $error ) ) {
312 + $error = array();
313 + }
314 +
315 + return array(
316 + 'code' => isset( $error['code'] ) ? (string) $error['code'] : '',
317 + 'detail' => isset( $error['detail'] ) ? (string) $error['detail'] : '',
318 + );
319 + }
320 +
321 + /**
322 + * Gets all plugins currently active, regardless of whether they're traditionally
323 + * activated or network activated.
324 + *
325 + * Ported from the Jetpack plugin so the `plugins` heartbeat stat can be generated from
326 + * the Connection package. This is the canonical implementation; the Jetpack plugin's
327 + * `Jetpack::get_active_plugins()` delegates to it.
328 + *
329 + * @since 8.7.9
330 + *
331 + * @return array
332 + */
333 + public static function get_active_plugins() {
334 + $active_plugins = (array) get_option( 'active_plugins', array() );
335 +
336 + if ( is_multisite() ) {
337 + // Due to legacy code, active_sitewide_plugins stores them in the keys,
338 + // whereas active_plugins stores them in the values.
339 + $network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
340 + if ( $network_plugins ) {
341 + $active_plugins = array_merge( $active_plugins, $network_plugins );
342 + }
343 + }
344 +
345 + sort( $active_plugins );
346 +
347 + return array_unique( $active_plugins );
348 + }
349 +
350 + /**
170 351 * Registers jetpack.getHeartbeatData xmlrpc method
171 352 *
172 353 * @param array $methods The list of methods to be filtered.
173 354 * @return array $methods
@@ -249,5 +430,55 @@
249 430 WP_CLI::line( sprintf( __( 'Last heartbeat sent at: %s', 'jetpack-connection' ), $last_date ) );
250 431 }
251 432 }
252 433
434 + /**
435 + * Initialize the heartbeat REST API.
436 + *
437 + * @return void
438 + */
439 + public function initialize_rest_api() {
440 + register_rest_route(
441 + 'jetpack/v4',
442 + '/heartbeat/data',
443 + array(
444 + 'methods' => WP_REST_Server::READABLE,
445 + 'callback' => array( $this, 'rest_heartbeat_data' ),
446 + 'permission_callback' => array( $this, 'rest_heartbeat_data_permission_check' ),
447 + 'args' => array(
448 + 'prefix' => array(
449 + 'description' => __( 'Prefix to add before the stats identifiers.', 'jetpack-connection' ),
450 + 'type' => 'string',
451 + ),
452 + ),
453 + )
454 + );
455 + }
456 +
457 + /**
458 + * Endpoint to retrieve the heartbeat data.
459 + *
460 + * @param WP_REST_Request $request The request data.
461 + *
462 + * @since 2.7.0
463 + *
464 + * @return array
465 + */
466 + public function rest_heartbeat_data( WP_REST_Request $request ) {
467 + return static::generate_stats_array( $request->get_param( 'prefix' ) );
468 + }
469 +
470 + /**
471 + * Check permissions for the `get_heartbeat_data` endpoint.
472 + *
473 + * @return true|WP_Error
474 + */
475 + public function rest_heartbeat_data_permission_check() {
476 + if ( current_user_can( 'jetpack_connect' ) ) {
477 + return true;
478 + }
479 +
480 + return Rest_Authentication::is_signed_with_blog_token()
481 + ? true
482 + : new WP_Error( 'invalid_permission_heartbeat_data', REST_Connector::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
483 + }
253 484 }