'ayudawp_acs_sanitize_settings', ) ); } add_action( 'admin_init', 'ayudawp_acs_settings_init' ); /** * Sanitize settings callback * * Signal values are validated against the closed Content Signals vocabulary * ('yes', 'no' or '' for unspecified), so nothing outside it can be stored * and later emitted on any output surface. * * @param array $input Raw form input. * @return array Sanitized settings. */ function ayudawp_acs_sanitize_settings( $input ) { if ( ! is_array( $input ) ) { return array(); } // Get current settings to preserve values for fields not in the input. $current_settings = get_option( 'ayudawp_acs_settings', array() ); $sanitized = array( 'global_search' => ayudawp_acs_sanitize_signal_value( $input['global_search'] ?? $current_settings['global_search'] ?? 'yes' ), 'global_ai_input' => ayudawp_acs_sanitize_signal_value( $input['global_ai_input'] ?? $current_settings['global_ai_input'] ?? 'yes' ), 'global_ai_train' => ayudawp_acs_sanitize_signal_value( $input['global_ai_train'] ?? $current_settings['global_ai_train'] ?? 'yes' ), 'include_legal_text' => ( isset( $input['include_legal_text'] ) && 'yes' === $input['include_legal_text'] ) ? 'yes' : 'no', 'create_physical_robots' => ayudawp_acs_sanitize_physical_robots_toggle( $input, $current_settings ), 'emit_meta_tags' => ( isset( $input['emit_meta_tags'] ) && 'yes' === $input['emit_meta_tags'] ) ? 'yes' : 'no', 'emit_http_headers' => ( isset( $input['emit_http_headers'] ) && 'yes' === $input['emit_http_headers'] ) ? 'yes' : 'no', 'emit_tdmrep' => ( isset( $input['emit_tdmrep'] ) && 'yes' === $input['emit_tdmrep'] ) ? 'yes' : 'no', 'bot_settings' => array(), 'custom_bots' => array(), ); // Sanitize bot settings. if ( isset( $input['bot_settings'] ) && is_array( $input['bot_settings'] ) ) { foreach ( $input['bot_settings'] as $bot => $bot_data ) { $sanitized['bot_settings'][ sanitize_text_field( $bot ) ] = array( 'search' => ayudawp_acs_sanitize_signal_value( $bot_data['search'] ?? '' ), 'ai_input' => ayudawp_acs_sanitize_signal_value( $bot_data['ai_input'] ?? '' ), 'ai_train' => ayudawp_acs_sanitize_signal_value( $bot_data['ai_train'] ?? '' ), ); } } // Sanitize custom bots. if ( isset( $input['custom_bots'] ) && is_array( $input['custom_bots'] ) ) { foreach ( $input['custom_bots'] as $bot ) { if ( ! empty( $bot['name'] ) ) { $sanitized['custom_bots'][] = array( 'name' => sanitize_text_field( $bot['name'] ), 'search' => ayudawp_acs_sanitize_signal_value( $bot['search'] ?? '' ), 'ai_input' => ayudawp_acs_sanitize_signal_value( $bot['ai_input'] ?? '' ), 'ai_train' => ayudawp_acs_sanitize_signal_value( $bot['ai_train'] ?? '' ), ); } } } return $sanitized; } /** * Resolve the create_physical_robots setting. * * The checkbox is not rendered where the physical file is a network decision, * and an unchecked box is indistinguishable from an absent one in a POST. So on * those sites the stored value is carried over instead of being read as 'no', * which would silently flip a network administrator's choice every time a * subsite administrator saved anything else on the page. * * @since 1.3.3 * * @param array $input Raw form input. * @param array $current_settings Settings currently stored. * @return string 'yes' or 'no'. */ function ayudawp_acs_sanitize_physical_robots_toggle( $input, $current_settings ) { if ( ayudawp_acs_physical_robots_is_network_only() ) { return ( ( $current_settings['create_physical_robots'] ?? 'no' ) === 'yes' ) ? 'yes' : 'no'; } return ( isset( $input['create_physical_robots'] ) && 'yes' === $input['create_physical_robots'] ) ? 'yes' : 'no'; } /** * Enqueue admin styles and scripts * * @param string $hook Current admin page hook. */ function ayudawp_acs_admin_assets( $hook ) { if ( 'settings_page_ai-content-signals' !== $hook ) { return; } // Load Thickbox for the "Get VigIA" plugin install modal in the sibling notice. add_thickbox(); // Enqueue CSS. wp_enqueue_style( 'ayudawp-acs-admin', AYUDAWP_ACS_PLUGIN_URL . 'assets/admin.css', array(), AYUDAWP_ACS_VERSION ); // Enqueue JavaScript. wp_enqueue_script( 'ayudawp-acs-admin', AYUDAWP_ACS_PLUGIN_URL . 'assets/admin.js', array( 'jquery' ), AYUDAWP_ACS_VERSION, true ); // Pass translations to JavaScript. wp_localize_script( 'ayudawp-acs-admin', 'ayudawpAcsL10n', array( 'botUserAgentPlaceholder' => __( 'Bot User-Agent (e.g., NewAIBot)', 'ai-content-signals' ), 'searchGlobal' => __( 'Search: Global', 'ai-content-signals' ), 'searchYes' => __( 'Search: Yes', 'ai-content-signals' ), 'searchNo' => __( 'Search: No', 'ai-content-signals' ), 'aiInputGlobal' => __( 'AI Input: Global', 'ai-content-signals' ), 'aiInputYes' => __( 'AI Input: Yes', 'ai-content-signals' ), 'aiInputNo' => __( 'AI Input: No', 'ai-content-signals' ), 'aiTrainGlobal' => __( 'AI Train: Global', 'ai-content-signals' ), 'aiTrainYes' => __( 'AI Train: Yes', 'ai-content-signals' ), 'aiTrainNo' => __( 'AI Train: No', 'ai-content-signals' ), 'remove' => __( 'Remove', 'ai-content-signals' ), 'confirmRestore' => __( 'Are you sure you want to restore all settings to their default values? This cannot be undone.', 'ai-content-signals' ), ) ); } add_action( 'admin_enqueue_scripts', 'ayudawp_acs_admin_assets' ); /** * Get predefined list of common AI bots * * @return array Associative array of bot_key => bot_label. */ function ayudawp_acs_get_predefined_bots() { $bots = array( 'GPTBot' => 'OpenAI GPTBot', 'OAI-SearchBot' => 'OpenAI Search Bot', 'ChatGPT-User' => 'ChatGPT User Agent', 'ClaudeBot' => 'Anthropic ClaudeBot', 'Claude-Web' => 'Claude Web Crawler', 'anthropic-ai' => 'Anthropic AI', 'PerplexityBot' => 'Perplexity Bot', 'Perplexity-User' => 'Perplexity User Agent', 'Google-Extended' => 'Google Extended (Gemini)', 'GoogleOther' => 'Google Other Bots', 'CCBot' => 'Common Crawl Bot', 'cohere-ai' => 'Cohere AI', 'Bytespider' => 'ByteDance (TikTok)', 'Amazonbot' => 'Amazon Bot', 'Applebot-Extended' => 'Apple Extended', 'meta-externalagent' => 'Meta External Agent', 'FacebookBot' => 'Meta/Facebook Bot', 'DuckAssistBot' => 'DuckDuckGo AI', 'AI2Bot' => 'Allen Institute AI2', 'MistralAI-User' => 'Mistral AI', 'Diffbot' => 'Diffbot', 'ImagesiftBot' => 'ImagesiftBot', 'Omgilibot' => 'Omgili Bot', 'YouBot' => 'You.com Bot', 'Timpibot' => 'Timpi Bot', 'DeepSeekBot' => 'DeepSeek AI', 'GrokBot' => 'xAI Grok', 'PanguBot' => 'Huawei Pangu', ); /** * Filter the predefined bots list. * * Allows other plugins or themes to add or remove bots * from the predefined list without modifying plugin code. * * @since 1.2.0 * * @param array $bots Associative array of bot_key => bot_label. */ return apply_filters( 'ayudawp_acs_predefined_bots', $bots ); } /** * Check if file system is accessible * * @return bool True if WP_Filesystem is available. */ function ayudawp_acs_check_filesystem() { global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } return ! empty( $wp_filesystem ); } /** * Display admin notices about robots.txt status */ function ayudawp_acs_admin_notices() { $screen = get_current_screen(); if ( ! $screen || 'settings_page_ai-content-signals' !== $screen->id ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! ayudawp_acs_check_filesystem() ) { return; } global $wp_filesystem; $robots_file = ABSPATH . 'robots.txt'; $has_physical = $wp_filesystem->exists( $robots_file ); $robots_url = $has_physical ? home_url( '/robots.txt' ) : home_url( '/?robots=1' ); // On a network the physical file is one directory shared by every site, so // only a network administrator writes it. Everything the standard notices // below say about permissions and creation would be false here, so this // site gets its own message: what it can do, what it cannot, and why. if ( ayudawp_acs_physical_robots_is_network_only() ) { if ( $has_physical ) { echo '

'; echo '' . esc_html__( 'Physical robots.txt is managed by the network:', 'ai-content-signals' ) . ' '; esc_html_e( 'A physical robots.txt file exists at the root of the network and is shared by every site on it, so only a network administrator can change it. While that file exists it is what this site serves, so Content Signals cannot be added to robots.txt from here.', 'ai-content-signals' ); echo '
'; esc_html_e( 'The HTML meta tag, HTTP header and TDMRep surfaces below are per site and work normally. To change robots.txt, ask your network administrator.', 'ai-content-signals' ); echo ' ' . esc_html__( 'View robots.txt', 'ai-content-signals' ) . ' ↗'; echo '

'; } else { echo '

'; echo '' . esc_html__( 'Virtual robots.txt mode:', 'ai-content-signals' ) . ' '; esc_html_e( 'No physical robots.txt detected, so Content Signals are added to this site own virtual robots.txt and work normally.', 'ai-content-signals' ); echo '
'; esc_html_e( 'Creating a physical file is not offered here: it would sit at the root of the network and be shared by every site on it, so only a network administrator can create it.', 'ai-content-signals' ); echo ' ' . esc_html__( 'View virtual robots.txt', 'ai-content-signals' ) . ' ↗'; echo '

'; } return; } if ( $has_physical ) { $is_readable = $wp_filesystem->is_readable( $robots_file ); $is_writable = $wp_filesystem->is_writable( $robots_file ); if ( $is_readable && $is_writable ) { echo '

'; echo '' . esc_html__( 'Physical robots.txt detected:', 'ai-content-signals' ) . ' '; esc_html_e( 'File is readable and writable. Content Signals will be appended to your existing robots.txt file.', 'ai-content-signals' ); echo ' '; esc_html_e( 'If you uninstall the plugin, the file will remain but Content Signals lines will be automatically removed.', 'ai-content-signals' ); echo ' ' . esc_html__( 'View robots.txt', 'ai-content-signals' ) . ' ↗'; echo '

'; } else { echo '

'; echo '' . esc_html__( 'Warning - Cannot write to robots.txt!', 'ai-content-signals' ) . '
'; if ( ! $is_readable && ! $is_writable ) { esc_html_e( 'Physical robots.txt exists but is NOT readable or writable. Content Signals cannot be applied to the file.', 'ai-content-signals' ); echo '
'; esc_html_e( 'Please change file permissions to 644 before configuring settings.', 'ai-content-signals' ); } elseif ( ! $is_readable ) { esc_html_e( 'Physical robots.txt exists but is NOT readable. Content Signals cannot read the file.', 'ai-content-signals' ); echo '
'; esc_html_e( 'Please change file permissions to 644 before configuring settings.', 'ai-content-signals' ); } else { esc_html_e( 'Physical robots.txt exists but is NOT writable. Content Signals cannot be written to the file.', 'ai-content-signals' ); echo '
'; esc_html_e( 'Please change file permissions to 644 before configuring settings.', 'ai-content-signals' ); } echo ' ' . esc_html__( 'View robots.txt', 'ai-content-signals' ) . ' ↗'; echo '

'; } } else { $settings = get_option( 'ayudawp_acs_settings', array() ); $create_enabled = ( $settings['create_physical_robots'] ?? 'no' ) === 'yes'; if ( $create_enabled && $wp_filesystem->is_writable( ABSPATH ) ) { echo '

'; echo '' . esc_html__( 'Physical robots.txt will be created:', 'ai-content-signals' ) . ' '; esc_html_e( 'You have enabled the option to create a physical robots.txt. It will be created with basic WordPress rules when you save settings.', 'ai-content-signals' ); echo ' ' . esc_html__( 'View current virtual robots.txt', 'ai-content-signals' ) . ' ↗'; echo '

'; } elseif ( $create_enabled && ! $wp_filesystem->is_writable( ABSPATH ) ) { echo '

'; echo '' . esc_html__( 'Cannot create robots.txt:', 'ai-content-signals' ) . ' '; esc_html_e( 'The root directory is not writable. Please check directory permissions or contact your hosting provider.', 'ai-content-signals' ); echo '

'; } else { echo '

'; echo '' . esc_html__( 'Virtual robots.txt mode:', 'ai-content-signals' ) . ' '; esc_html_e( 'No physical robots.txt detected. Content Signals are being added to the WordPress virtual robots.txt. You can enable the option below to create a physical file with basic rules.', 'ai-content-signals' ); echo ' ' . esc_html__( 'View virtual robots.txt', 'ai-content-signals' ) . ' ↗'; echo '

'; } } } add_action( 'admin_notices', 'ayudawp_acs_admin_notices' ); /** * Check whether the VigIA companion plugin is active. * * @return bool True if VigIA is loaded. */ function ayudawp_acs_is_vigia_active() { return defined( 'VIGIA_VERSION' ); } /** * Render an informational notice about how AI Content Signals and VigIA complement each other. * * AI Content Signals declares the AI usage policy; VigIA observes and blocks the * crawlers. When VigIA is not installed, a Thickbox install link is offered. */ function ayudawp_acs_vigia_notice() { if ( ayudawp_acs_is_vigia_active() ) { ?>

$bot_name ) : $bot_settings = $settings['bot_settings'][ $bot_key ] ?? array(); ?>

$bot ) : ?>

render(); ?>