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;
}
}