* * @phpstan-import-type PresizedDivSlot from PresizedDiv */ final class Cron { /** * Transient key tracking the ads.txt follow-up polling attempt count. */ const ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT = 'actirise_adstxt_followup_attempt'; /** * Delay before the next ads.txt follow-up poll, in seconds. */ const ADSTXT_FOLLOWUP_DELAY = 10; /** * Maximum number of ads.txt follow-up polls before giving up. */ const ADSTXT_FOLLOWUP_MAX_ATTEMPTS = 10; /** * Lifetime of the ads.txt follow-up attempt transient, in seconds. */ const ADSTXT_FOLLOWUP_TRANSIENT_TTL = 1800; /** * Schedule cron jobs * * @since 2.0.0 * @return void */ public function schedule() { if ( ACTIRISE_CRON === 'true' ) { add_filter( 'cron_schedules', function ( $schedules ) { $schedules['five_minutes'] = array( 'interval' => 300, 'display' => esc_html__( 'Every Five Minutes', 'actirise' ), ); return $schedules; } ); if ( ! wp_next_scheduled( 'actirise_cron_update_adstxt' ) ) { wp_schedule_event( time(), 'hourly', 'actirise_cron_update_adstxt' ); $this->update_adstxt(); } if ( ! wp_next_scheduled( 'actirise_cron_update_presized_div' ) ) { wp_schedule_event( time(), 'hourly', 'actirise_cron_update_presized_div' ); $this->check_presized_div(); } if ( ! wp_next_scheduled( 'actirise_cron_update_fast_cmp' ) ) { wp_schedule_event( time(), 'hourly', 'actirise_cron_update_fast_cmp' ); $this->get_fast_cmp(); } if ( ! wp_next_scheduled( 'actirise_cron_check_adstxt' ) ) { wp_schedule_event( time(), 'hourly', 'actirise_cron_check_adstxt' ); $this->check_adstxt(); } if ( ! wp_next_scheduled( 'actirise_cron_check_slots' ) ) { wp_schedule_event( time(), 'hourly', 'actirise_cron_check_slots' ); $this->check_slots(); } if ( ! wp_next_scheduled( 'actirise_cron_event_annotation' ) ) { wp_schedule_event( time(), 'five_minutes', 'actirise_cron_event_annotation' ); $this->upload_event_annotation(); } if ( Options::get( 'contract-type' ) === 'essential' && ! wp_next_scheduled( 'actirise_cron_check_optimization' ) ) { wp_schedule_event( time(), 'five_minutes', 'actirise_cron_check_optimization' ); } } } /** * Update ads.txt file * * @since 2.0.0 * @return void */ public function update_adstxt() { if ( Options::get( 'settings-uuid' ) ) { $ads_txt = AdsTxt::get_from_api(); if ( $ads_txt !== false ) { Options::update( 'adstxt-actirise', $ads_txt ); $this->check_adstxt(); if ( Options::get( 'adstxt-active', false ) === true ) { AdsTxt::update_file(); } Options::update( 'adstxt-lastupdate', time() ); } if ( $ads_txt === false ) { Logger::add_log( 'adstxt get_from_api is false', 'include/cron', 'error' ); } } } /** * Check lines in ads.txt file * * @since 3.0.0 * @return bool|array */ public function check_adstxt() { if ( ! Helpers::is_user_authenticated() ) { return false; } // Hosted mode: ads.txt is served by Actirise, no monitoring API call needed. if ( Options::get( 'hosted-adstxt-enabled', false ) === true ) { $this->check_hosted_adstxt_redirect(); return false; } $adstxt_status = AdsTxt::check_from_api( 'true' ); if ( is_array( $adstxt_status ) && isset( $adstxt_status['state'] ) && $adstxt_status['state'] === 'pending' ) { $this->schedule_adstxt_followup( 1 ); return $adstxt_status; } $this->finalize_adstxt_check( $adstxt_status ); return $adstxt_status; } /** * Follow-up cron event for ads.txt monitoring polling. * * Fired once the initial refresh request returned `pending`. Polls the * adstxt monitoring endpoint without the refresh flag and either * reschedules itself (until ADSTXT_FOLLOWUP_MAX_ATTEMPTS is reached) or * finalizes the check when a definitive state is returned. * * @since 4.2.0 * @return void */ public function check_adstxt_followup() { if ( ! Helpers::is_user_authenticated() ) { delete_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT ); return; } // Hosted mode may have been enabled mid-chain; abort the API polling. if ( Options::get( 'hosted-adstxt-enabled', false ) === true ) { delete_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT ); return; } /** @var mixed $stored_attempt */ $stored_attempt = get_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT ); $attempt = is_numeric( $stored_attempt ) ? (int) $stored_attempt : 0; $adstxt_status = AdsTxt::check_from_api( 'false' ); $still_pending = is_array( $adstxt_status ) && isset( $adstxt_status['state'] ) && $adstxt_status['state'] === 'pending'; if ( $still_pending && $attempt < self::ADSTXT_FOLLOWUP_MAX_ATTEMPTS ) { $this->schedule_adstxt_followup( $attempt + 1 ); return; } delete_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT ); if ( $still_pending ) { Logger::add_log( 'check_adstxt_followup max attempts reached, state still pending', 'include/cron', 'error' ); return; } $this->finalize_adstxt_check( $adstxt_status ); } /** * Schedule a single follow-up event to re-poll the ads.txt monitoring * endpoint without the refresh flag. * * @since 4.2.0 * @param int $attempt The 1-based attempt counter to persist for the next run. * @return void */ private function schedule_adstxt_followup( $attempt ) { set_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT, $attempt, self::ADSTXT_FOLLOWUP_TRANSIENT_TTL ); wp_schedule_single_event( time() + self::ADSTXT_FOLLOWUP_DELAY, 'actirise_cron_check_adstxt_followup' ); } /** * Persist the ads.txt check result and run the hosted redirect check. * * @since 4.2.0 * @param bool|array $adstxt_status Final status returned by the API. * @return void */ private function finalize_adstxt_check( $adstxt_status ) { if ( is_array( $adstxt_status ) && isset( $adstxt_status['state'] ) ) { if ( $adstxt_status['state'] === 'ok' ) { Options::update( 'adstxt-update', false ); } else { Options::update( 'adstxt-update', true ); } } } /** * Check hosted ads.txt redirect * * @since 3.3.5 * @return void */ public function check_hosted_adstxt_redirect() { $is_hosted = Options::get( 'hosted-adstxt-enabled', false ) === true; if ( ! $is_hosted ) { // Not in hosted mode, nothing to check Options::delete( 'hosted-adstxt-redirect-error' ); return; } $uuid = Options::get( 'website-uuid' ); if ( ! $uuid ) { return; } $site_ads_txt_url = get_site_url() . '/ads.txt'; $flashbid_url = 'https://gateway.actirise.com/services/adstxt/' . $uuid; $response = wp_remote_get( $site_ads_txt_url, array( 'timeout' => 10, 'redirection' => 0, // Don't follow redirects 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { // Network error, mark as error Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'manual_required', 'flashbid_url' => $flashbid_url, ) ); return; } $status_code = wp_remote_retrieve_response_code( $response ); if ( $status_code === 302 || $status_code === 301 ) { $redirect_location = wp_remote_retrieve_header( $response, 'location' ); // Ignore WordPress trailing slash redirects (ads.txt -> ads.txt/) $is_trailing_slash_redirect = ( $redirect_location === trailingslashit( $site_ads_txt_url ) ); if ( $redirect_location === $flashbid_url ) { // Redirect is correct (don't check destination status), clear error Options::delete( 'hosted-adstxt-redirect-error' ); } elseif ( $is_trailing_slash_redirect ) { // WordPress trailing slash redirect - not an error, clear any existing error Options::delete( 'hosted-adstxt-redirect-error' ); } else { // Wrong redirect URL Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'wrong_url', 'flashbid_url' => $flashbid_url, ) ); } } elseif ( $status_code === 200 ) { // File is being served directly instead of redirected // Check if it's a physical file or generated by another plugin/system $path_ads_txt = ABSPATH . 'ads.txt'; $wp_fs = Helpers::get_wp_fs(); $file_exists = ! is_wp_error( $wp_fs ) && $wp_fs->exists( $path_ads_txt ); if ( $file_exists ) { // Physical file exists Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'file_exists', 'flashbid_url' => $flashbid_url, ) ); } else { // File is generated by third-party plugin/system Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'third_party', 'flashbid_url' => $flashbid_url, ) ); } } else { // Other error Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'manual_required', 'flashbid_url' => $flashbid_url, ) ); } } /** * Check for presized div * * @since 2.0.0 * @return void */ public function check_presized_div() { if ( ! Helpers::is_user_authenticated() ) { return; } if ( Options::get( 'settings-uuid' ) ) { /** @var array | bool $presized_div */ $presized_div = PresizedDiv::get_from_api(); if ( $presized_div !== false ) { /** @var mixed $presized_div_active */ $presized_div_active = Options::get( 'presizeddiv-selected', array() ); /** @var mixed $presized_div_notif */ $presized_div_notif = Options::get( 'presizeddiv-notif', array() ); if ( ! is_array( $presized_div_active ) ) { $presized_div_active = array(); } /** @var array $presized_div_active */ if ( ! is_array( $presized_div_notif ) ) { $presized_div_notif = array(); } /** @var array $presized_div */ foreach ( $presized_div as $presized_div_item ) { /** @var string $slot_name */ $slot_name = $presized_div_item['slotName']; if ( ! in_array( $slot_name, array_column( $presized_div_active, 'slotName' ), true ) ) { $presized_div_active[] = array( 'slotName' => $slot_name, 'active' => false, ); /** @var array<\stdclass> $presized_div_notif */ $presized_div_notif[] = $slot_name; } } /** @var array{slotName: string, active: bool} $presized_div_active_item */ foreach ( $presized_div_active as $key => $presized_div_active_item ) { $slot_name = $presized_div_active_item['slotName']; if ( ! in_array( $slot_name, array_column( $presized_div, 'slotName' ), true ) ) { unset( $presized_div_active[ $key ] ); } } Options::update( 'presizeddiv-selected', $presized_div_active ); Options::update( 'presizeddiv-actirise', $presized_div ); Options::update( 'presizeddiv-notif', $presized_div_notif ); Options::update( 'presizeddiv-lastupdate', time() ); } else { Options::update( 'presizeddiv-actirise', array() ); } } } /** * Get FastCmp stub light * * @since 2.3.0 * @return void */ public function get_fast_cmp() { $this->get_fastcmp_stublight(); $this->get_fastcmp_vendors(); if ( Options::get( 'fastcmp-uuid', '' ) === '' ) { $this->get_fastcmp_uuid(); } Options::update( 'fastcmp-lastupdate', time() ); } /** * Set auto update * * @since 2.4.0 * @param bool $enabled * @return void */ public function set_auto_update( $enabled ) { if ( ACTIRISE_CRON !== 'true' ) { return; } if ( $enabled === Helpers::has_auto_update() ) { return; } /** @var string $plugin_file */ $plugin_file = plugin_basename( ACTIRISE_FILE ); /** @var array $auto_updates */ $auto_updates = get_site_option( 'auto_update_plugins', array() ); if ( $enabled ) { $plugins = array_unique( array_merge( $auto_updates, array( plugin_basename( ACTIRISE_FILE ) ) ) ); } else { $plugins = array_values( array_diff( $auto_updates, array( plugin_basename( ACTIRISE_FILE ) ) ) ); } update_site_option( 'auto_update_plugins', $plugins ); } /** * Check if the slots XPath are present and valid in website pages * * @since 3.2.0 * @return void */ public function check_slots() { if ( ! Helpers::is_user_authenticated() ) { return; } if ( Options::get( 'contract-type' ) !== 'essential' ) { return; } $ad_format = new AdFormat(); $ad_format->validate_xpath_and_notify(); } /** * Uploads event annotations via cron task. * * This method is called by the cron system to trigger the event * annotation upload process. It delegates the actual work to the * EventAnnotation class. * * @return void * * @since 3.2.0 */ public function upload_event_annotation() { EventAnnotation::send_events(); } /** * Get FastCmp stub light * * @since 2.2.0 * @return void */ private function get_fastcmp_stublight() { $stub_light_url = 'https://static.fastcmp.com/fast-cmp-stub-light.js'; $response = wp_remote_request( $stub_light_url, array( 'method' => 'GET', 'timeout' => 10, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { Logger::add_log( 'get_fastcmp_stublight error ' . $response->get_error_code(), 'include/cron', 'error' ); return; } /** @var string|boolean $stub_light */ $stub_light = wp_remote_retrieve_body( $response ); if ( $stub_light === false ) { return; } Options::update( 'fastcmp-stub-light', $stub_light ); } /** * Get FastCmp vendors * * @since 2.2.0 * @return void */ private function get_fastcmp_vendors() { $vendors_api_url = 'https://eu.fastcmp.com/wp/vendor-list'; $response = wp_remote_request( $vendors_api_url, array( 'method' => 'GET', 'timeout' => 10, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { return; } /** @var string|boolean $vendors */ $vendors = wp_remote_retrieve_body( $response ); if ( $vendors === false ) { return; } /** @var string $vendors */ $vendors_json = json_decode( $vendors, true ); if ( $vendors_json === null ) { return; } Options::update( 'fastcmp-vendors', $vendors_json ); } /** * Get FastCmp uuid * * @since 2.2.0 * @return void */ private function get_fastcmp_uuid() { $fast_cmp_url = 'https://eu.fastcmp.com/wp/domain-uid?domain='; $domain = rawurlencode( Helpers::get_server_details()['host'] ); $domain = str_replace( 'www.', '', $domain ); $response = wp_remote_request( $fast_cmp_url . $domain, array( 'method' => 'GET', 'timeout' => 10, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { return; } /** @var string|boolean $uuid */ $uuid = wp_remote_retrieve_body( $response ); if ( $uuid === false ) { return; } /** @var string $uuid */ $uuid_json = json_decode( $uuid, true ); /** @var null|array{domain?: string, domainUid?: string, error?: string} $uuid_json */ if ( $uuid_json === null ) { return; } /** @var array{domain?: string, domainUid?: string, error?: string} $uuid_json */ if ( isset( $uuid_json['error'] ) || ! isset( $uuid_json['domainUid'] ) ) { return; } Options::update( 'fastcmp-uuid', $uuid_json['domainUid'] ); } /** * Check optimization checklist and cache health issues. * * @since 4.1.0 * @return void */ public function check_optimization() { $health_check = new HealthCheck(); $issues = $health_check->get_issues(); Options::update( 'health-check-cache', $issues ); } /** * Checks if a scheduled task needs to be executed based on transient expiration. * * This method uses WordPress transients to determine if a task should run. * If the transient doesn't exist (expired or never set), it creates a new one * with the specified duration and executes the callback function. * * @param string $transient_name The name of the transient to check. * @param callable $callback The function to execute if transient has expired. * @param int $duration Duration in seconds for the transient (default: 3600). * * @return bool True if the task was executed, false if it was skipped. * * @since 3.2.0 */ public function check_scheduled_task_with_transient( $transient_name, callable $callback, $duration = 3600 ) { if ( false === get_transient( $transient_name ) ) { set_transient( $transient_name, true, $duration ); call_user_func( $callback ); return true; } return false; } }