HEX
Server: Apache/2.4.65 (Debian)
System: Linux 88f31f35b0b8 6.1.0-38-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.147-1 (2025-08-02) x86_64
User: www-data (33)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/optimization-detective/site-health.php
<?php
/**
 * Site Health checks.
 *
 * @package optimization-detective
 * @since 1.0.0
 */

// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd

/**
 * Adds the Optimization Detective REST API check to site health tests.
 *
 * @since 1.0.0
 * @access private
 *
 * @param array{direct: array<string, array{label: string, test: string}>}|mixed $tests Site Health Tests.
 * @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
 */
function od_add_rest_api_availability_test( $tests ): array {
	if ( ! is_array( $tests ) ) {
		$tests = array();
	}
	$tests['direct']['optimization_detective_rest_api'] = array(
		'label' => __( 'Optimization Detective REST API Endpoint Availability', 'optimization-detective' ),
		'test'  => static function () {
			// Note: A closure is used here to improve symbol discovery for the sake of potential refactoring.
			return od_test_rest_api_availability();
		},
	);

	return $tests;
}

/**
 * Tests availability of the Optimization Detective REST API endpoint.
 *
 * @since 1.0.0
 * @access private
 *
 * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
 */
function od_test_rest_api_availability(): array {
	$response       = od_get_rest_api_health_check_response( false );
	$result         = od_compose_site_health_result( $response );
	$is_unavailable = 'good' !== $result['status'];
	update_option(
		'od_rest_api_unavailable',
		$is_unavailable ? '1' : '0',
		true // Intentionally autoloaded since used on every frontend request.
	);
	return $result;
}

/**
 * Checks whether the Optimization Detective REST API endpoint is unavailable.
 *
 * This merely checks the database option what was previously computed in the Site Health test as done in {@see od_test_rest_api_availability()}.
 * This is to avoid checking for REST API availability during a frontend request. Note that when the plugin is first
 * installed, the 'od_rest_api_unavailable' option will not be in the database, as the check has not been performed
 * yet. Once Site Health's weekly check happens or when a user accesses the admin so that the admin_init action fires,
 * then at this point the check will be performed at {@see od_maybe_run_rest_api_health_check()}. In practice, this will
 * happen immediately after the user activates a plugin since the user is redirected back to the plugin list table in
 * the admin. The reason for storing the negative unavailable state as opposed to the positive available state is that
 * when an option does not exist, then `get_option()` returns `false` which is the same falsy value as the stored `'0'`.
 *
 * @since 1.0.0
 * @access private
 *
 * @return bool Whether unavailable.
 */
function od_is_rest_api_unavailable(): bool {
	return 1 === (int) get_option( 'od_rest_api_unavailable', '0' );
}

/**
 * Tests availability of the Optimization Detective REST API endpoint.
 *
 * @since 1.0.0
 * @access private
 *
 * @param array<string, mixed>|WP_Error $response REST API response.
 * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
 */
function od_compose_site_health_result( $response ): array {
	$common_description_html = '<p>' . wp_kses(
		sprintf(
			/* translators: %s is the REST API endpoint */
			__( 'To collect URL Metrics from visitors, the REST API must be available to unauthenticated users. Specifically, visitors must be able to perform a <code>POST</code> request to the <code>%s</code> endpoint.', 'optimization-detective' ),
			'/' . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE
		),
		array( 'code' => array() )
	) . '</p>';

	$result = array(
		'label'       => __( 'The Optimization Detective REST API endpoint is available', 'optimization-detective' ),
		'status'      => 'good',
		'badge'       => array(
			'label' => __( 'Optimization Detective', 'optimization-detective' ),
			'color' => 'blue',
		),
		'description' => $common_description_html . '<p><strong>' . esc_html__( 'This appears to be working properly.', 'optimization-detective' ) . '</strong></p>',
		'actions'     => '',
		'test'        => 'optimization_detective_rest_api',
	);

	$error_label            = __( 'The Optimization Detective REST API endpoint is unavailable', 'optimization-detective' );
	$error_description_html = '<p>' . esc_html__( 'You may have a plugin active or server configuration which restricts access to logged-in users. Unauthenticated access must be restored in order for Optimization Detective to work.', 'optimization-detective' ) . '</p>';

	if ( is_wp_error( $response ) ) {
		$result['status']      = 'recommended';
		$result['label']       = $error_label;
		$result['description'] = $common_description_html . $error_description_html . '<p>' . wp_kses(
			sprintf(
				/* translators: %s is the error code */
				__( 'The REST API responded with the error code <code>%s</code> and the following error message:', 'optimization-detective' ),
				esc_html( (string) $response->get_error_code() )
			),
			array( 'code' => array() )
		) . '</p><blockquote>' . esc_html( $response->get_error_message() ) . '</blockquote>';
	} else {
		$code    = wp_remote_retrieve_response_code( $response );
		$message = wp_remote_retrieve_response_message( $response );
		$body    = wp_remote_retrieve_body( $response );
		$data    = json_decode( $body, true );
		$header  = wp_remote_retrieve_header( $response, 'content-type' );
		if ( is_array( $header ) ) {
			$header = array_pop( $header );
		}

		$is_expected = (
			400 === $code &&
			isset( $data['code'], $data['data']['params'] ) &&
			'rest_missing_callback_param' === $data['code'] &&
			is_array( $data['data']['params'] ) &&
			count( $data['data']['params'] ) > 0
		);
		if ( ! $is_expected ) {
			$result['status'] = 'recommended';
			if ( 401 === $code ) {
				$result['label'] = __( 'The Optimization Detective REST API endpoint is unavailable to logged-out users', 'optimization-detective' );
			} else {
				$result['label'] = $error_label;
			}
			$result['description'] = $common_description_html . $error_description_html . '<p>' . wp_kses(
				sprintf(
					/* translators: %d is the HTTP status code, %s is the status header description */
					__( 'The REST API returned with an HTTP status of <code>%1$d %2$s</code>.', 'optimization-detective' ),
					$code,
					esc_html( $message )
				),
				array( 'code' => array() )
			) . '</p>';

			if ( isset( $data['message'] ) && is_string( $data['message'] ) ) {
				$result['description'] .= '<blockquote>' . esc_html( $data['message'] ) . '</blockquote>';
			}

			if ( '' !== $body ) {
				$result['description'] .= '<details>';
				$result['description'] .= '<summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary>';

				if ( is_string( $header ) && str_contains( $header, 'html' ) ) {
					$escaped_content        = htmlspecialchars( $body, ENT_QUOTES, 'UTF-8' );
					$result['description'] .= '<iframe srcdoc="' . $escaped_content . '" sandbox width="100%" height="300"></iframe>';
				} else {
					$result['description'] .= '<pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre>';
				}
				$result['description'] .= '</details>';
			}
		}
	}
	return $result;
}

/**
 * Gets the response to an Optimization Detective REST API store request to confirm it is available to unauthenticated requests.
 *
 * @since 1.0.0
 * @access private
 *
 * @param bool $use_cached Whether to use a previous response cached in a transient.
 * @return array{ response: array{ code: int, message: string }, body: string }|WP_Error Response.
 */
function od_get_rest_api_health_check_response( bool $use_cached ) {
	$transient_key = 'od_rest_api_health_check_response';
	$response      = $use_cached ? get_transient( $transient_key ) : false;
	if ( false !== $response ) {
		return $response;
	}
	$rest_url = get_rest_url( null, OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE );
	$response = wp_remote_post(
		$rest_url,
		array(
			'headers'   => array( 'Content-Type' => 'application/json' ),
			'sslverify' => false,
		)
	);

	// This transient will be used when showing the admin notice with the plugin on the plugins screen.
	// The 1-day expiration allows for fresher content than the weekly check initiated by Site Health.
	set_transient( $transient_key, $response, DAY_IN_SECONDS );
	return $response;
}

/**
 * Renders an admin notice if the REST API health check fails.
 *
 * @since 1.0.0
 * @access private
 *
 * @param bool $in_plugin_row Whether the notice is to be printed in the plugin row.
 */
function od_maybe_render_rest_api_health_check_admin_notice( bool $in_plugin_row ): void {
	if ( ! od_is_rest_api_unavailable() ) {
		return;
	}

	$response = od_get_rest_api_health_check_response( true );
	$result   = od_compose_site_health_result( $response );
	if ( 'good' === $result['status'] ) {
		// There's a slight chance the DB option is stale in the initial if statement.
		return;
	}

	$message = sprintf(
		$in_plugin_row
			? '<summary style="margin: 0.5em 0">%s %s</summary>'
			: '<p><strong>%s %s</strong></p>',
		esc_html__( 'Warning:', 'optimization-detective' ),
		esc_html( $result['label'] )
	);

	$message .= $result['description']; // This has already gone through Kses.

	if ( current_user_can( 'view_site_health_checks' ) ) {
		$site_health_message = wp_kses(
			sprintf(
				/* translators: %s is the URL to the Site Health admin screen */
				__( 'Please visit <a href="%s">Site Health</a> to re-check this once you believe you have resolved the issue.', 'optimization-detective' ),
				esc_url( admin_url( 'site-health.php' ) )
			),
			array( 'a' => array( 'href' => array() ) )
		);
		$message .= "<p><em>$site_health_message</em></p>";
	}

	if ( $in_plugin_row ) {
		$message = "<details>$message</details>";
	}

	$notice = wp_get_admin_notice(
		$message,
		array(
			'type'               => 'warning',
			'additional_classes' => $in_plugin_row ? array( 'inline', 'notice-alt' ) : array(),
			'paragraph_wrap'     => false,
		)
	);

	echo wp_kses(
		$notice,
		array_merge(
			wp_kses_allowed_html( 'post' ),
			array(
				'iframe' => array_fill_keys( array( 'srcdoc', 'sandbox', 'width', 'height' ), true ),
			)
		)
	);
}

/**
 * Displays an admin notice on the plugin row if the REST API health check fails.
 *
 * @since 1.0.0
 * @access private
 *
 * @param non-empty-string $plugin_file Plugin file.
 */
function od_render_rest_api_health_check_admin_notice_in_plugin_row( string $plugin_file ): void {
	if ( 'optimization-detective/load.php' !== $plugin_file ) { // TODO: What if a different plugin slug is used?
		return;
	}
	od_maybe_render_rest_api_health_check_admin_notice( true );
}

/**
 * Runs the REST API health check if it hasn't been run yet.
 *
 * This happens at the `admin_init` action to avoid running the check on the frontend. This will run on the first admin
 * page load after the plugin has been activated. This allows for this function to add an action at `admin_notices` so
 * that an error message can be displayed after performing that plugin activation request. Note that a plugin activation
 * hook cannot be used for this purpose due to not being compatible with multisite. While the site health notice is
 * shown at the `admin_notices` action once, the notice will only be displayed inline with the plugin row thereafter
 * via {@see od_render_rest_api_health_check_admin_notice_in_plugin_row()}.
 *
 * @since 1.0.0
 * @access private
 */
function od_maybe_run_rest_api_health_check(): void {
	// If the option already exists, then the REST API health check has already been performed.
	if ( false !== get_option( 'od_rest_api_unavailable' ) ) {
		return;
	}

	// This will populate the od_rest_api_unavailable option so that the function won't execute on the next page load.
	if ( 'good' !== od_test_rest_api_availability()['status'] ) {
		// Show any notice in the main admin notices area for the first page load (e.g. after plugin activation).
		add_action(
			'admin_notices',
			static function (): void {
				od_maybe_render_rest_api_health_check_admin_notice( false );
			}
		);
	}
}