*/ final class HealthCheck { /** * Api class * * @since 4.0.0 * @access private * @var Api $api Api class. */ private $api; /** * CMP vendors cache * * @since 4.0.0 * @var array|null */ private $cmp_vendors_cache = null; /** * Initialize the class * * @since 4.0.0 */ public function __construct() { $this->api = new Api(); } /** * Get all health check issues * * @since 4.0.0 * @return array> */ public function get_issues() { $issues = array(); // Check presized divs (Core Web Vitals) $core_web_vitals_issue = $this->check_core_web_vitals(); if ( $core_web_vitals_issue ) { $issues[] = $core_web_vitals_issue; } // Check Ads.txt $adstxt_issue = $this->check_adstxt(); if ( $adstxt_issue ) { $issues[] = $adstxt_issue; } // Check Ad Formats $ad_formats_issue = $this->check_ad_formats(); if ( $ad_formats_issue ) { $issues[] = $ad_formats_issue; } // Check CMP vendors $cmp_issue = $this->check_cmp(); if ( $cmp_issue ) { $issues[] = $cmp_issue; } // Check CMP design customization $cmp_design_issue = $this->check_cmp_design(); if ( $cmp_design_issue ) { $issues[] = $cmp_design_issue; } // Check hosted ads.txt redirect $ads_txt_redirect_issue = $this->check_hosted_adstxt_redirect(); if ( $ads_txt_redirect_issue ) { $issues[] = $ads_txt_redirect_issue; } // Suggest migration to hosted ads.txt $ads_txt_migration_issue = $this->check_adstxt_migration(); if ( $ads_txt_migration_issue ) { $issues[] = $ads_txt_migration_issue; } // Check optimization checklist $optimization_issue = $this->check_optimization_checklist(); if ( $optimization_issue ) { $issues[] = $optimization_issue; } return $issues; } /** * Check Core Web Vitals / Presized Divs * * Checks if all available presized divs are activated. * Returns an issue if: * - Feature is disabled, OR * - Feature is enabled but not all presized divs are activated * * @since 4.0.0 * @return array|null */ public function check_core_web_vitals() { $presized_div_active = Options::get( 'presizeddiv-active', false ); // Get available presized divs from Actirise /** @var array> $available_divs */ $available_divs = Options::get( 'presizeddiv-actirise', array() ); // If no presized divs available, no issue if ( empty( $available_divs ) ) { return null; } // If feature is disabled, all divs are inactive if ( ! $presized_div_active ) { return array( 'type' => 'core_web_vitals', 'count' => count( $available_divs ), ); } // Feature is enabled, check selected/activated presized divs /** @var array> $selected_divs */ $selected_divs = Options::get( 'presizeddiv-selected', array() ); // Count inactive presized divs by comparing available vs selected $inactive_count = 0; foreach ( $available_divs as $available_div ) { $slot_name = isset( $available_div['slotName'] ) ? $available_div['slotName'] : ''; if ( empty( $slot_name ) ) { continue; } // Check if this div is in selected and active $is_active = false; foreach ( $selected_divs as $selected_div ) { if ( isset( $selected_div['slotName'] ) && $selected_div['slotName'] === $slot_name ) { $is_active = isset( $selected_div['active'] ) && $selected_div['active'] === true; break; } } if ( ! $is_active ) { ++$inactive_count; } } if ( $inactive_count > 0 ) { return array( 'type' => 'core_web_vitals', 'count' => $inactive_count, ); } return null; } /** * Check Ads.txt configuration * * @since 4.0.0 * @return array|null */ public function check_adstxt() { if ( Options::get( 'adstxt-active', false ) === false && Options::get( 'adstxt-update', false ) === true ) { $missing_count = Options::get( 'adstxt-missing-count', 0 ); $blacklist_count = Options::get( 'adstxt-blacklist-count', 0 ); return array( 'type' => 'adstxt', 'missing' => $missing_count, 'blacklist' => $blacklist_count, ); } return null; } /** * Check Ad Formats configuration * Premium users do not control ad placements, so this check is irrelevant for them. * * @since 4.0.0 * @return array|null */ public function check_ad_formats() { if ( Options::get( 'contract-type' ) === 'premium' ) { return null; } $xpath_failed_active_config = Options::get( 'xpath_failed_active_config', 0 ); // Handle boolean true case (should be treated as 1) if ( true === $xpath_failed_active_config ) { $xpath_failed_active_config = 1; } // Convert to int if it's numeric if ( is_numeric( $xpath_failed_active_config ) ) { $xpath_failed_active_config = (int) $xpath_failed_active_config; } if ( $xpath_failed_active_config > 0 ) { return array( 'type' => 'ad_formats', 'count' => $xpath_failed_active_config, ); } return null; } /** * Check CMP vendors configuration * * @since 4.0.0 * @return array|null */ public function check_cmp() { // Only check if using own CMP (fastCMP disabled) if ( Options::get( 'fastcmp-enabled', false ) !== false ) { return null; } // Get current vendors $vendors = $this->get_cmp_vendors(); if ( empty( $vendors ) ) { return null; } // Get saved snapshot $saved_vendors = Options::get( 'cmp_vendors_snapshot', array() ); if ( ! is_array( $saved_vendors ) || empty( $saved_vendors ) ) { // No snapshot saved, consider it as changed return array( 'type' => 'cmp', ); } // Extract vendor IDs from current vendors $current_vendor_ids = array_map( function ( $vendor ) { if ( ! is_array( $vendor ) || ! isset( $vendor['id'] ) ) { return null; } return is_string( $vendor['id'] ) || is_int( $vendor['id'] ) ? (int) $vendor['id'] : null; }, $vendors ); // Remove null values $current_vendor_ids = array_values( array_filter( $current_vendor_ids ) ); // Sort for comparison sort( $current_vendor_ids ); sort( $saved_vendors ); // Compare if ( $saved_vendors !== $current_vendor_ids ) { return array( 'type' => 'cmp', ); } return null; } /** * Default FastCMP button styles. * * Mirrors the fallbacks used by Helpers::get_fastcmp_options() and by the * front-end store, so an untouched configuration is detected identically * whether the options row exists or not. * * @since 4.3.0 * @return array> */ private function get_default_cmp_styles() { return array( 'fastcmp-acceptButtonStyle' => array( 'bg' => 'transparent', 'font' => '#6f6f6f', ), 'fastcmp-declineButtonStyle' => array( 'bg' => 'transparent', 'font' => '#6f6f6f', 'border' => '#6f6f6f', ), 'fastcmp-parametersButtonStyle' => array( 'bg' => 'transparent', 'font' => '#6f6f6f', 'border' => '#6f6f6f', ), ); } /** * Check CMP design customization * * Returns an issue when FastCMP is active but its appearance (accept, * decline and parameters button colors plus the custom CSS) is still on the * default values, inviting the publisher to match the banner to their brand. * * @since 4.3.0 * @return array|null */ public function check_cmp_design() { // Only relevant when the Actirise CMP is the one being served. if ( ! Options::get( 'fastcmp-enabled', false ) ) { return null; } // Any custom CSS counts as a customized design. $custom_style = Options::get( 'fastcmp-customStyle', '' ); if ( is_string( $custom_style ) && trim( $custom_style ) !== '' ) { return null; } foreach ( $this->get_default_cmp_styles() as $option_name => $defaults ) { /** @var array|mixed $style */ $style = Options::get( $option_name, $defaults ); if ( ! $this->is_default_style( $style, $defaults ) ) { return null; } } return array( 'type' => 'cmp_design', ); } /** * Check whether a stored button style still matches its default values. * * An empty color is treated as default: the CMP falls back to its own * rendering when no value is set. * * @since 4.3.0 * @param mixed $style Stored style, expected as an array of colors. * @param array $defaults Default colors for this style. * @return bool */ private function is_default_style( $style, $defaults ) { // Styles are persisted straight from a json_decode() of the AJAX payload, // so they read back as stdClass rather than as an array — see // Ajax::options_fast_cmp() and the json round-trip that // Helpers::get_fastcmp_options() performs for the same reason. if ( is_object( $style ) ) { $style = (array) $style; } // Unreadable value: assume something was customized rather than nagging // a publisher who already styled their banner. if ( ! is_array( $style ) ) { return false; } foreach ( $defaults as $key => $default_value ) { if ( ! isset( $style[ $key ] ) || ! is_string( $style[ $key ] ) ) { continue; } $value = strtolower( trim( $style[ $key ] ) ); if ( '' === $value ) { continue; } if ( $value !== $default_value ) { return false; } } return true; } /** * Get CMP vendors from API * * @since 4.0.0 * @return array */ private function get_cmp_vendors() { // Return cached value if already fetched if ( $this->cmp_vendors_cache !== null ) { return $this->cmp_vendors_cache; } /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $response = $this->api->get( 'api', 'cmp_vendors', array(), $token ); if ( is_wp_error( $response ) || ! is_array( $response ) || empty( $response ) ) { $this->cmp_vendors_cache = array(); return array(); } $this->cmp_vendors_cache = $response; return $response; } /** * Check hosted ads.txt redirect * * Returns an issue if hosted ads.txt is enabled but the redirect * is obstructed by a local file or a conflicting third-party rule. * * @since 3.4.2 * @return array|null */ public function check_hosted_adstxt_redirect() { $hosted_enabled = Options::get( 'hosted-adstxt-enabled', false ); if ( ! $hosted_enabled ) { return null; } $redirect_error = Options::get( 'hosted-adstxt-redirect-error', false ); if ( empty( $redirect_error ) || ! is_array( $redirect_error ) ) { return null; } $error_type = isset( $redirect_error['error_type'] ) ? $redirect_error['error_type'] : ''; if ( 'file_exists' === $error_type || 'third_party' === $error_type ) { return array( 'type' => 'ads_txt_redirect', 'error_type' => $error_type, ); } return null; } /** * Check if publisher should migrate to hosted ads.txt * * Returns an issue if ads.txt is active but not using hosted mode, * suggesting the publisher migrate for automatic seller updates. * * @since 3.4.2 * @return array|null */ public function check_adstxt_migration() { $hosted_enabled = Options::get( 'hosted-adstxt-enabled', false ); if ( $hosted_enabled ) { return null; } // Check if hosted ads.txt feature is available for this website $website_settings = Options::get( 'website-settings', array() ); $hosted_adstxt_available = is_array( $website_settings ) && isset( $website_settings['hosted_adstxt'] ) && $website_settings['hosted_adstxt']; if ( $hosted_adstxt_available ) { return array( 'type' => 'ads_txt_migration', ); } return null; } /** * Check if all optimization checklist items are completed. * * Returns an issue if any of the 7 checklist items is incomplete: * - Hosted ads.txt enabled * - Cache exclusion marked as done (manual) * - FastCMP enabled * - Corner ad format enabled * - Rewards ad format enabled * - Pre-sized divs all enabled * - Editorial best practices marked as done (manual) * * @since 4.1.0 * @return array|null */ private function check_optimization_checklist() { // Only relevant for essential contract types if ( Options::get( 'contract-type' ) !== 'essential' ) { return null; } // 1. Hosted ads.txt $hosted_adstxt = Options::get( 'hosted-adstxt-enabled', false ); if ( ! $hosted_adstxt ) { return array( 'type' => 'optimization_checklist' ); } // 2. Cache exclusion (manual) $manual_steps = Options::get( 'optimization-checklist-manual', array() ); if ( ! is_array( $manual_steps ) || empty( $manual_steps['cache_exclusion'] ) ) { return array( 'type' => 'optimization_checklist' ); } // 3. FastCMP $fastcmp_enabled = Options::get( 'fastcmp-enabled', false ); if ( ! $fastcmp_enabled ) { return array( 'type' => 'optimization_checklist' ); } // 4 & 5. Corner and Rewards ad formats (keyed by slug name with device info) $enabled_slugs = Options::get( 'ad-format-enabled-slugs', array() ); if ( ! is_array( $enabled_slugs ) ) { return array( 'type' => 'optimization_checklist' ); } foreach ( array( 'corner', 'rewards' ) as $slug ) { if ( ! isset( $enabled_slugs[ $slug ] ) || ! is_array( $enabled_slugs[ $slug ] ) ) { return array( 'type' => 'optimization_checklist' ); } if ( empty( $enabled_slugs[ $slug ]['desktop'] ) || empty( $enabled_slugs[ $slug ]['mobile'] ) ) { return array( 'type' => 'optimization_checklist' ); } } // 6. Pre-sized divs: feature enabled AND all selected slots active $presized_active = Options::get( 'presizeddiv-active', false ); if ( ! $presized_active ) { return array( 'type' => 'optimization_checklist' ); } $presized_selected = Options::get( 'presizeddiv-selected', array() ); if ( is_array( $presized_selected ) ) { foreach ( $presized_selected as $slot ) { if ( isset( $slot['active'] ) && ! $slot['active'] ) { return array( 'type' => 'optimization_checklist' ); } } } // 7. Editorial best practices (manual) if ( empty( $manual_steps['editorial_best_practices'] ) ) { return array( 'type' => 'optimization_checklist' ); } return null; } }