resolve( $slug ); return; } NotificationManager::get()->register( $slug, sprintf( /* translators: %1$s is "Virtual" or "External", %2$s/%3$s wrap a link to the ads.txt settings tab. */ __( 'A physical ads.txt file exists in your site root, but your ads.txt source is set to %1$s - most servers serve the physical file directly, so this configuration may never be applied. %2$sReview ads.txt settings%3$s.', 'adpresso' ), $source === 'virtual' ? __( 'Virtual', 'adpresso' ) : __( 'External', 'adpresso' ), '', '' ), [ 'type' => 'warning', 'location' => 'wp_notice', ] ); } /** * Generates backup nonce for the ads.txt module. * * @param array $data * * @return array */ public static function add_backup_nonce( array $data ): array { $params = Params::get(); $page = $params->getKey( 'page' ); $tab = $params->getKey( 'tab' ); if ( $page !== 'adpresso' || $tab !== 'ads_txt' ) { return $data; } $data['adsTxtNonce'] = wp_create_nonce( 'adpresso_backup_ads_txt_action' ); return $data; } /** * Enqueue the admin scripts for the ads.txt module. * * @return void */ public static function enqueue_admin_scripts(): void { $params = Params::get(); $page = $params->getKey( 'page' ); $tab = $params->getKey( 'tab' ); if ( $page !== 'adpresso' || $tab !== 'ads_txt' ) { return; } wp_enqueue_script( 'adpresso-adstxt', ADPRESSO_BASE_URL . 'modules/ads-txt/assets/src/adstxt.js', [], ADPRESSO_VERSION, true ); } /** * Shows a notification if a physical ads.txt exists. * * @return void */ public static function show_physical_notification(): void { if ( self::physical_ads_txt_exists() ) { $file_path = ABSPATH . 'ads.txt'; $content = file_get_contents( $file_path ); echo '
'; echo '

' . esc_html__( 'AdPresso found a physical ads.txt file in the root directory of your WordPress installation. It has the following content and AdPresso will deliver it when the source "Physical file" is selected.', 'adpresso' ) . '

'; echo '
' . esc_html( $content ) . '
'; echo ''; echo '
'; } else { echo '
' . esc_html__( 'AdPresso could not find a physical ads.txt file in the root directory of your WordPress installation.', 'adpresso' ) . '
'; } } /** * Handles the different ads.txt cases. * * @return void */ public static function maybe_output_ads_txt(): void { // When ads.txt is not requested, we bail out early. // Ignore the query string, e.g. /ads.txt?ver=2 from crawlers or cache busters. $path = explode( '?', Params::server()->getString( 'REQUEST_URI' ), 2 )[0]; if ( trim( $path, '/' ) !== 'ads.txt' ) { return; } // Check if the module is active and if virtual output is enabled if ( ! class_exists( '\AdPresso\Modules\ModuleManager' ) || ! ModuleManager::is_active( 'ads_txt' ) ) { return; } // No stored source = the field's default. Older versions only rendered the source field // after the module was saved once, so switching it on stored no source and /ads.txt stayed 404. $source = Settings::get_setting( 'ads_txt_source', '' ) ?: 'virtual'; switch ( $source ) { case 'virtual': $content = self::get_virtual_ads_txt(); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'X-Content-Type-Options: nosniff' ); echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text/plain + nosniff; HTML escaping would corrupt the records. exit; case 'external': $url = Settings::get_setting( 'ads_txt_external_url', '' ); if ( wp_http_validate_url( $url ) ) { // wp_safe_redirect() would defeat the purpose here - this deliberately redirects // to the admin-configured, already-validated external ads.txt URL. // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Intentional external redirect, see comment above. wp_redirect( esc_url_raw( $url ), 302 ); exit; } break; case 'physical': default: // The physical ads.txt file is expected to be handled by the server directly. We don't do anything here. break; } } /** * The virtual ads.txt body as served: generated + filtered, control bytes stripped. * * @return string */ public static function get_virtual_ads_txt(): string { $content = ( new VirtualAdsTxtGenerator() )->generate(); $content = (string) apply_filters( 'adpresso/ads_txt/content', $content ); // Keep tab and LF only; CR (e.g. CRLF from a filter) and other control bytes are dropped. return (string) preg_replace( '/[\x00-\x08\x0B-\x1F\x7F]/', '', $content ); } /** * Backs up the real ads.txt file and switch to virtual ads.txt mode afterward. * * @return void */ public static function handle_ads_txt_backup(): void { check_ajax_referer( 'adpresso_backup_ads_txt_action', 'security' ); if ( ! current_user_can( adpresso_get_capability( 'manage_settings' ) ) ) { wp_send_json_error( [ 'message' => __( 'You are not allowed to perform that action', 'adpresso' ) ], 403 ); } $original = ABSPATH . 'ads.txt'; if ( ! file_exists( $original ) ) { wp_send_json_error( [ 'message' => __( 'No ads.txt available', 'adpresso' ) ] ); } require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); global $wp_filesystem; if ( ! $wp_filesystem->is_writable( $original ) ) { wp_send_json_error( [ 'message' => __( 'ads.txt is not writable.', 'adpresso' ) ] ); } $backup_name = 'ads.txt.backup-' . current_time( 'Ymd-His' ); $destination = ABSPATH . $backup_name; if ( ! $wp_filesystem->move( $original, $destination ) ) { wp_send_json_error( [ 'message' => __( 'Backup failed.', 'adpresso' ) ] ); } // Switch to virtual ads.txt mode after backup, keeping the other ads.txt settings. $option_name = Constants::OPTION_PREFIX . self::OPTION_NAME; $settings = get_option( $option_name, [] ); $settings = is_array( $settings ) ? $settings : []; $settings['ads_txt_source'] = 'virtual'; update_option( $option_name, $settings ); wp_send_json_success( [ /* translators: %s is the name of the backup file. */ 'message' => sprintf( __( 'File successfully saved as %s', 'adpresso' ), $backup_name ), ] ); } /** * Checks if the physical ads.txt file exists in the root directory. * * @return bool */ public static function physical_ads_txt_exists(): bool { $root_path = ABSPATH . 'ads.txt'; return file_exists( $root_path ) && is_file( $root_path ); } /** * Reads and parses the physical ads.txt file located in ABSPATH. * * @return array Array of parsed ads.txt records, or empty array if not found/readable. */ public static function parse_physical_ads_txt(): array { $file_path = ABSPATH . 'ads.txt'; // Check if file exists and is readable if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) { return []; } // Read file into an array of lines, skipping empty lines $lines = file( $file_path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); if ( ! $lines ) { return []; } $parsed_records = []; foreach ( $lines as $line ) { // Remove comments (everything from '#' onwards) $line = explode( '#', $line )[0]; $line = trim( $line ); if ( empty( $line ) ) { continue; } // Split by comma and trim each part $parts = array_map( 'trim', explode( ',', $line ) ); // A valid IAB ads.txt line must have at least 3 parts if ( count( $parts ) >= 3 ) { $parsed_records[] = [ 'domain' => strtolower( $parts[0] ), 'publisher_id' => $parts[1], // Case-sensitive or not depending on network, AdSense is usually 'pub-XXXX' 'relationship' => strtoupper( $parts[2] ), // DIRECT or RESELLER 'cert_id' => $parts[3] ?? '', ]; } } return $parsed_records; } }