*/ final class AdsTxt { /** * Ads.txt constructor. * * @since 2.0.0 */ public function __construct() { if ( ACTIRISE_CRON !== 'true' ) { $this->check_adstxt_update(); } } /** * Check if hosted ads.txt is enabled * * @since 3.3.1 * @return bool */ private static function hosted_adstxt_enabled() { $hosted_enabled = Options::get( 'hosted-adstxt-enabled', false ); return $hosted_enabled === true; } /** * The loader of this plugin. * * @since 2.0.0 * @return void */ public function init() { // Check if hosted ads.txt is enabled if ( self::hosted_adstxt_enabled() ) { $request = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : false; // @phpstan-ignore-line if ( $request === '/ads.txt' ) { $this->redirect_to_hosted_adstxt(); } } // Standard ads.txt handling if ( Options::get( 'adstxt-active', false ) !== false ) { $request = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : false; // @phpstan-ignore-line if ( $request === '/ads.txt' && Options::get( 'adstxt-actirise', false ) !== false ) { $this->render_adstxt(); } } } /** * Get hosted ads.txt URL * * @since 3.3.1 * @return string|false The hosted ads.txt URL or false if not available */ private static function get_hosted_url() { $uuid = Options::get( 'website-uuid', '' ); if ( empty( $uuid ) ) { Logger::add_log( 'get_hosted_url: no website uuid', 'public/include/adstxt', 'error' ); return false; } $url = 'https://gateway.actirise.com/services/adstxt/' . $uuid; return $url; } /** * Redirect to hosted ads.txt * * @since 3.3.1 * @return void */ private function redirect_to_hosted_adstxt() { $url = self::get_hosted_url(); if ( $url === false ) { Logger::add_log( 'redirect_to_hosted_adstxt: URL not available', 'public/include/adstxt', 'error' ); // Fallback to standard ads.txt rendering if ( Options::get( 'adstxt-actirise', false ) !== false ) { $this->render_adstxt(); } return; } $allowed_host = wp_parse_url( $url, PHP_URL_HOST ); add_filter( 'allowed_redirect_hosts', static function ( $hosts ) use ( $allowed_host ) { if ( is_string( $allowed_host ) && $allowed_host !== '' ) { $hosts[] = $allowed_host; } return $hosts; } ); wp_safe_redirect( $url, 302 ); exit; } /** * Generate abd render ads.txt * * @since 2.0.0 * @return void */ private function render_adstxt() { header( 'Content-Type: text/plain' ); header( 'Actirise: true' ); header( 'Cache-Control: no-cache, must-revalidate' ); header( 'Expires: Thu, 29 Dec 1994 12:25:00 GMT' ); header( 'Pragma: no-cache' ); $adstxt = self::get_adstxt( true ); header( 'ETag: W/"' . md5( $adstxt ) . '"' ); /** @var string $adstxt_filter */ $adstxt_filter = apply_filters( 'actirise_ads_txt_content', $adstxt ); echo esc_html( $adstxt_filter ); die(); } /** * Clean duplicate line * * @since 2.4.0 * @param string $adstxt * @param string $lines * * @return string */ private static function clean_duplicate_line( $adstxt, $lines ) { $lines = explode( "\n", $lines ); $lines = array_unique( $lines ); foreach ( $lines as $key => &$line ) { $exploded_line = explode( ',', $line ); if ( count( $exploded_line ) > 0 ) { $exploded_line[0] = strtolower( $exploded_line[0] ); } if ( count( $exploded_line ) >= 2 ) { $exploded_line[2] = strtoupper( $exploded_line[2] ); } $line = implode( ',', $exploded_line ); if ( $line !== '' ) { if ( false !== strpos( $adstxt, $line ) ) { unset( $lines[ $key ] ); } } } $lines = implode( "\n", $lines ); return $lines; } /** * Get ads.txt formated * * @since 2.0.0 * @param bool $custom * @return string */ public static function get_adstxt( $custom = false ) { /** @var string $adstxt_actirise */ $adstxt_actirise = Options::get( 'adstxt-actirise', '' ); preg_match( '/OWNERDOMAIN=(.*)\n/', $adstxt_actirise, $matches ); $owner_domain = ''; if ( isset( $matches[1] ) ) { $owner_domain = $matches[1]; } preg_match( '/MANAGERDOMAIN=(.*)\n/', $adstxt_actirise, $matches ); $manager_domain = ''; if ( isset( $matches[1] ) ) { $manager_domain = $matches[1]; } /** @var string $adstxt_actirise */ $adstxt_actirise = preg_replace( '/MANAGERDOMAIN=.*\n/', '', $adstxt_actirise ); /** @var string $adstxt_actirise */ $adstxt_actirise = preg_replace( '/OWNERDOMAIN=.*\n/', '', $adstxt_actirise ); /** @var string $adstxt_actirise */ $adstxt_actirise = preg_replace( '/## START actirise.com ##\n/', '', $adstxt_actirise ); /** @var string $adstxt_actirise */ $adstxt_actirise = preg_replace( '/## END actirise.com ##/', '', $adstxt_actirise ); /** @var string $adstxt */ $adstxt = '#---------------------------- Monetized by Actirise WordPress Plugin ----------------------------'; $adstxt .= "\n"; $adstxt .= "\n"; $adstxt .= 'contact=hello@actirise.com'; $adstxt .= "\n"; $adstxt .= "\n"; if ( $owner_domain !== '' ) { $adstxt .= 'OWNERDOMAIN=' . $owner_domain; $adstxt .= "\n"; } if ( $manager_domain !== '' ) { $adstxt .= 'MANAGERDOMAIN=' . $manager_domain; $adstxt .= "\n"; } /** @var string $adstxt */ $adstxt .= $adstxt_actirise; $adstxt .= '#--------------------------------------- End Actirise.com ---------------------------------------'; if ( $custom ) { /** @var string $adstxt_custom */ $adstxt_custom = Options::get( 'adstxt-custom' ); /** @var array<\stdClass> $adstxt_custom_array */ $adstxt_custom_array = array(); if ( $adstxt_custom ) { /** @var array<\stdClass> $adstxt_custom_array */ $adstxt_custom_array = json_decode( $adstxt_custom ); } if ( count( $adstxt_custom_array ) > 0 ) { $adstxt .= "\n"; foreach ( $adstxt_custom_array as $adstxt_item ) { $cleaned_line = self::clean_duplicate_line( $adstxt, $adstxt_item->value ); // @phpstan-ignore argument.type if ( $cleaned_line !== '' ) { $adstxt .= "\n"; $adstxt .= '## START ' . $adstxt_item->title . " ##\n"; $adstxt .= $cleaned_line . "\n"; $adstxt .= '## END ' . $adstxt_item->title . " ##\n"; } } } } return $adstxt; } /** * Get ads.txt from API * * @since 2.0.0 * * @return bool|string */ public static function get_from_api() { $args = array( 'domain' => rawurlencode( Helpers::get_server_details()['host'] ), ); if ( Options::get( 'settings-uuid-type', 'boot' ) === 'universal' ) { $args['universal'] = 'true'; $args['product'] = '3'; } /** @var string $uuid */ $uuid = Options::get( 'settings-uuid' ); $api_url = 'adstxt_files/' . $uuid; $api = new Api(); $response = $api->get( 'api', $api_url, $args ); if ( is_wp_error( $response ) ) { Logger::add_log( 'get_from_api error ' . $response->get_error_code(), 'public/include/adstxt', 'error' ); return false; } if ( ! is_array( $response ) ) { Logger::add_log( 'get_from_api response is not array ', 'public/include/adstxt', 'error' ); return false; } if ( ! isset( $response['datas'] ) ) { Logger::add_log( 'get_from_api response is empty ', 'public/include/adstxt', 'error' ); return false; } $array_response = array( $response['datas'] ); if ( count( $array_response ) !== 1 ) { Logger::add_log( 'get_from_api responselength is up to 1 ', 'public/include/adstxt', 'error' ); return false; } if ( empty( $array_response[0] ) ) { Logger::add_log( 'get_from_api responselength[0] is not exist ', 'public/include/adstxt', 'error' ); return false; } /** @var array{'datas': string} $response */ return $response['datas']; } /** * Check ads.txt file using API * * @since 3.0.0 * @param string $force * @return bool|array */ public static function check_from_api( $force ) { /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $refresh = $force === 'true' ? 1 : 0; if ( $token !== '' ) { $website_id = Options::get( 'website-id' ); $api = new Api(); $response = $api->get( 'api', 'websites/' . $website_id . '/adstxt_monitorings', array( 'refresh' => $refresh ), $token ); if ( is_wp_error( $response ) ) { Logger::add_log( 'check_from_api error ' . $response->get_error_code(), 'public/include/adstxt', 'error' ); return false; } if ( ! is_array( $response ) ) { Logger::add_log( 'check_from_api response is not array ', 'public/include/adstxt', 'error' ); return false; } // Store missing and blacklist counts for health check self::store_validation_counts( $response ); return $response; } return false; } /** * Store ads.txt validation counts for health check * * @since 3.3.0 * @param array $response API response. * @return void */ private static function store_validation_counts( $response ) { $missing_count = 0; $blacklist_count = 0; if ( isset( $response['missing'] ) ) { if ( is_string( $response['missing'] ) ) { // Count lines in string (split by newline) $missing_lines = array_filter( explode( "\n", trim( $response['missing'] ) ) ); $missing_count = count( $missing_lines ); } elseif ( is_array( $response['missing'] ) ) { $missing_count = count( $response['missing'] ); } } if ( isset( $response['json']['blacklistLines'] ) && is_array( $response['json']['blacklistLines'] ) ) { $blacklist_count = count( $response['json']['blacklistLines'] ); } Options::update( 'adstxt-missing-count', $missing_count ); Options::update( 'adstxt-blacklist-count', $blacklist_count ); } /** * Update ads.txt file * * @since 2.4.0 * @return void */ public static function update_file() { // Don't update file if hosted ads.txt is enabled if ( self::hosted_adstxt_enabled() ) { Logger::add_log( 'update_file: hosted ads.txt is enabled, skipping file update', 'public/include/adstxt', 'debug' ); return; } Logger::add_log( 'update_file start', 'public/include/adstxt', 'debug' ); try { $wp_fs = Helpers::get_wp_fs(); if ( is_wp_error( $wp_fs ) ) { Logger::add_log( 'update_file wp_fs error', 'public/include/adstxt', 'error' ); Options::update( 'adstxt-file', false ); return; } $path_ads_txt = ABSPATH . 'ads.txt'; $file_exists = $wp_fs->exists( $path_ads_txt ); if ( is_multisite() ) { Options::update( 'adstxt-file', false ); if ( $file_exists ) { Options::update( 'adstxt-existing', true ); } else { Options::update( 'adstxt-existing', false ); } return; } if ( $file_exists && ! $wp_fs->is_writable( $path_ads_txt ) ) { Logger::add_log( 'update_file not writable', 'public/include/adstxt', 'error' ); Options::update( 'adstxt-file', false ); Options::update( 'adstxt-existing', true ); return; } if ( defined( 'FS_CHMOD_FILE' ) ) { $modes = FS_CHMOD_FILE; } else { $modes = false; } $success = $wp_fs->put_contents( $path_ads_txt, self::get_adstxt( true ), $modes ); if ( $success === false ) { Logger::add_log( 'update_file put_contents failed', 'public/include/adstxt', 'error' ); Options::update( 'adstxt-file', false ); Options::update( 'adstxt-existing', $file_exists ); return; } Logger::add_log( 'update_file success', 'public/include/adstxt', 'debug' ); Options::update( 'adstxt-existing', false ); Options::update( 'adstxt-file', $success ); Options::update( 'adstxt-update', false ); } catch ( \Exception $e ) { Logger::add_log( 'update_file catch: ', 'public/include/adstxt', 'error' ); Options::update( 'adstxt-file', false ); } } /** * Delete ads.txt file * * @since 3.0.0 * @return void */ public static function delete_file() { Logger::add_log( 'delete_file start', 'public/include/adstxt', 'debug' ); if ( Options::get( 'adstxt-file' ) !== false ) { $ads_txt_file = ABSPATH . 'ads.txt'; if ( file_exists( $ads_txt_file ) ) { wp_delete_file( $ads_txt_file ); } } } /** * Check if ads.txt need to be updated * * @since 2.0.0 * @return void */ private function check_adstxt_update() { $cron = new Cron(); $cron->check_scheduled_task_with_transient( 'update_adstxt', array( $cron, 'update_adstxt', ) ); } }