detector = $detector; $this->llms = $llms; } /** * Admin hooks. */ public function hooks() { add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_post_adg_aim_save', array( $this, 'handle_save' ) ); add_action( 'admin_post_adg_aim_regen', array( $this, 'handle_regen' ) ); add_action( 'admin_post_adg_aim_llms_selftest', array( $this, 'handle_llms_selftest' ) ); add_action( 'admin_post_adg_aim_htaccess_fix', array( $this, 'handle_htaccess_fix' ) ); add_filter( 'plugin_action_links_' . ADG_AIM_BASENAME, array( $this, 'action_links' ) ); // Pro: analytics + referral export/reset actions, self-test, digest. if ( ADG_AIM_Plugin::is_pro() ) { add_action( 'admin_post_adg_aim_reset_stats', array( $this, 'handle_reset_stats' ) ); add_action( 'admin_post_adg_aim_export_stats', array( $this, 'handle_export_stats' ) ); add_action( 'admin_post_adg_aim_reset_referrals', array( $this, 'handle_reset_referrals' ) ); add_action( 'admin_post_adg_aim_export_referrals', array( $this, 'handle_export_referrals' ) ); add_action( 'admin_post_adg_aim_selftest', array( $this, 'handle_selftest' ) ); add_action( 'admin_post_adg_aim_refresh_savings', array( $this, 'handle_refresh_savings' ) ); add_action( 'admin_post_adg_aim_send_digest', array( $this, 'handle_send_digest' ) ); } add_action( 'add_meta_boxes', array( $this, 'add_metabox' ) ); add_action( 'save_post', array( $this, 'save_metabox' ) ); add_filter( 'post_row_actions', array( $this, 'row_actions' ), 10, 2 ); add_filter( 'page_row_actions', array( $this, 'row_actions' ), 10, 2 ); } /** * Build the "View Markdown" preview URL for a post. * * @param WP_Post $post Post. * @return string */ private function preview_url( $post ) { return add_query_arg( 'adg_preview_md', '1', get_permalink( $post ) ); } /** * Add a "View Markdown" row action for enabled post types. * * @param array $actions Row actions. * @param WP_Post $post Post. * @return array */ public function row_actions( $actions, $post ) { $types = (array) ADG_AIM_Plugin::get_setting( 'post_types', array( 'post', 'page' ) ); if ( in_array( $post->post_type, $types, true ) && 'publish' === $post->post_status ) { $actions['adg_view_md'] = '' . esc_html__( 'View Markdown', 'adg-markdown-for-ai-crawlers' ) . ''; } return $actions; } /** * "Settings" link on the plugins list. * * @param array $links Links. * @return array */ public function action_links( $links ) { $url = admin_url( 'options-general.php?page=' . self::PAGE_SLUG ); array_unshift( $links, '' . esc_html__( 'Settings', 'adg-markdown-for-ai-crawlers' ) . '' ); return $links; } /** * Menu item. */ public function add_menu() { add_options_page( __( 'AI Bots & Markdown', 'adg-markdown-for-ai-crawlers' ), __( 'AI Bots & Markdown', 'adg-markdown-for-ai-crawlers' ), 'manage_options', self::PAGE_SLUG, array( $this, 'render_page' ) ); } /* --------------------------------------------------------------------- * Saving * ------------------------------------------------------------------- */ /** * Settings save handler. */ public function handle_save() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); $settings = ADG_AIM_Plugin::default_settings(); $settings['negotiation_enabled'] = isset( $_POST['negotiation_enabled'] ); $settings['schema_enabled'] = isset( $_POST['schema_enabled'] ); $settings['markdown_cache'] = isset( $_POST['markdown_cache'] ); $settings['tax_archives'] = isset( $_POST['tax_archives'] ); $settings['llms_enabled'] = isset( $_POST['llms_enabled'] ); // Pro: training/citation access policy + robots.txt management. Only the // Pro UI renders these fields, so when unlicensed we preserve the stored // values instead of resetting them from an absent form. if ( ADG_AIM_Plugin::is_pro() ) { $settings['allow_training'] = isset( $_POST['allow_training'] ); $settings['allow_citation'] = isset( $_POST['allow_citation'] ); $settings['robots_manage'] = isset( $_POST['robots_manage'] ); $settings['ip_verification'] = isset( $_POST['ip_verification'] ); $settings['weekly_digest'] = isset( $_POST['weekly_digest'] ); $settings['digest_email'] = isset( $_POST['digest_email'] ) ? sanitize_email( wp_unslash( $_POST['digest_email'] ) ) : ''; } else { $prev = get_option( ADG_AIM_OPT_SETTINGS, array() ); foreach ( array( 'allow_training', 'allow_citation', 'robots_manage', 'ip_verification', 'weekly_digest', 'digest_email' ) as $pro_key ) { if ( is_array( $prev ) && isset( $prev[ $pro_key ] ) ) { $settings[ $pro_key ] = $prev[ $pro_key ]; } } } // Front-matter field toggles. foreach ( array( 'fm_title', 'fm_url', 'fm_date', 'fm_author', 'fm_tags', 'fm_canonical' ) as $fm ) { $settings[ $fm ] = isset( $_POST[ $fm ] ); } $settings['meta_keys'] = isset( $_POST['meta_keys'] ) ? sanitize_textarea_field( wp_unslash( $_POST['meta_keys'] ) ) : ''; $settings['excluded_terms'] = isset( $_POST['excluded_terms'] ) ? sanitize_text_field( wp_unslash( $_POST['excluded_terms'] ) ) : ''; $settings['llms_site_desc'] = isset( $_POST['llms_site_desc'] ) ? sanitize_textarea_field( wp_unslash( $_POST['llms_site_desc'] ) ) : ''; $settings['post_types'] = isset( $_POST['post_types'] ) && is_array( $_POST['post_types'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['post_types'] ) ) : array(); $settings['llms_full_types'] = isset( $_POST['llms_full_types'] ) && is_array( $_POST['llms_full_types'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['llms_full_types'] ) ) : array(); update_option( ADG_AIM_OPT_SETTINGS, $settings ); $this->save_bots(); delete_transient( ADG_AIM_LLMS_Generator::TRANSIENT_LLMS ); // The physical llms.txt fallback bypasses the virtual route (and its // llms_enabled check) entirely once a file exists on disk, so turning // the feature off must also remove any file the plugin wrote. if ( empty( $settings['llms_enabled'] ) ) { ADG_AIM_LLMS_Generator::cleanup_physical_files(); } // Pro: sampled post types may have changed → drop the savings estimate; // match the weekly-digest schedule to the (possibly changed) setting. if ( class_exists( 'ADG_AIM_Token_Savings' ) ) { ADG_AIM_Token_Savings::clear(); } if ( class_exists( 'ADG_AIM_Digest' ) ) { ADG_AIM_Digest::ensure_schedule(); } $this->redirect_back( 'saved' ); } /** * Save the bot list from the form. */ private function save_bots() { // Nonce is verified by handle_save() before this method runs. // phpcs:disable WordPress.Security.NonceVerification.Missing $bots = array(); // Stored bots by id — so a non-Pro save (Category/token columns not // rendered) preserves those fields instead of resetting them. $stored = array(); foreach ( (array) get_option( ADG_AIM_OPT_BOTS, array() ) as $b ) { if ( isset( $b['id'] ) ) { $stored[ $b['id'] ] = $b; } } if ( isset( $_POST['bot'] ) && is_array( $_POST['bot'] ) ) { $raw_rows = map_deep( wp_unslash( $_POST['bot'] ), 'sanitize_text_field' ); foreach ( $raw_rows as $row ) { $label = isset( $row['label'] ) ? $row['label'] : ''; $ua = isset( $row['ua_pattern'] ) ? $row['ua_pattern'] : ''; if ( '' === $label || '' === $ua ) { continue; } $id = isset( $row['id'] ) && '' !== $row['id'] ? sanitize_key( $row['id'] ) : sanitize_key( $label ); $prev = isset( $stored[ $id ] ) ? $stored[ $id ] : array(); $bots[] = array( 'id' => $id, 'label' => $label, 'ua_pattern' => $ua, 'category' => isset( $row['category'] ) ? $this->sanitize_category( $row['category'] ) : ADG_AIM_Bot_Detector::bot_category( $prev ), 'robots_token' => isset( $row['robots_token'] ) ? $this->sanitize_token( $row['robots_token'] ) : ( isset( $prev['robots_token'] ) ? (string) $prev['robots_token'] : '' ), 'enabled' => ! empty( $row['enabled'] ), ); } } if ( isset( $_POST['new_bot'] ) && is_array( $_POST['new_bot'] ) ) { $new = map_deep( wp_unslash( $_POST['new_bot'] ), 'sanitize_text_field' ); $label = isset( $new['label'] ) ? $new['label'] : ''; $ua = isset( $new['ua_pattern'] ) ? $new['ua_pattern'] : ''; if ( '' !== $label && '' !== $ua ) { $bots[] = array( 'id' => sanitize_key( $label . '-' . wp_rand( 100, 999 ) ), 'label' => $label, 'ua_pattern' => $ua, 'category' => $this->sanitize_category( isset( $new['category'] ) ? $new['category'] : '' ), 'robots_token' => $this->sanitize_token( isset( $new['robots_token'] ) ? $new['robots_token'] : '' ), 'enabled' => true, ); } } if ( ! empty( $bots ) ) { update_option( ADG_AIM_OPT_BOTS, $bots ); } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Sanitize a bot access category from the form. * * @param string $raw Raw value. * @return string 'training'|'citation'. */ private function sanitize_category( $raw ) { $raw = strtolower( sanitize_key( (string) $raw ) ); return in_array( $raw, ADG_AIM_Bot_Detector::categories(), true ) ? $raw : 'citation'; } /** * Sanitize a robots.txt product token from the form (letters, digits, . _ -). * * @param string $raw Raw value. * @return string Clean token, or '' if unusable. */ private function sanitize_token( $raw ) { $raw = trim( sanitize_text_field( (string) $raw ) ); if ( '' === $raw ) { return ''; } return (string) preg_replace( '/[^A-Za-z0-9._-]/', '', $raw ); } /** * "Regenerate now" handler. */ public function handle_regen() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); ADG_AIM_Markdown_Cache::clear_all(); $this->llms->force_regenerate(); $this->redirect_back( 'regenerated' ); } /** * Re-check llms.txt availability handler. */ public function handle_llms_selftest() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); ADG_AIM_LLMS_Generator::run_selftest(); $this->redirect_back( 'llms_tested' ); } /** * "Try automatic fix" handler: prepends a self-contained .htaccess * exception for /llms.txt and /llms-full.txt, ahead of any earlier block * (e.g. a host's edge-cache rule) that would otherwise intercept them * first. Backs up the previous file before writing. */ public function handle_htaccess_fix() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); if ( ! function_exists( 'get_home_path' ) ) { // Core has moved this function before (misc.php → file.php); requiring // both is harmless (require_once) and resilient to either location. require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/misc.php'; } $result = ADG_AIM_LLMS_Htaccess_Fixer::apply( get_home_path() . '.htaccess' ); if ( $result['applied'] ) { ADG_AIM_LLMS_Generator::run_selftest(); $this->redirect_back( 'htaccess_fixed' ); } $this->redirect_back( 'htaccess_fix_failed' ); } /** * Redirect back to the settings page with a status flag. * * @param string $status Status. */ private function redirect_back( $status ) { $url = add_query_arg( array( 'page' => self::PAGE_SLUG, 'adg_status' => $status, ), admin_url( 'options-general.php' ) ); wp_safe_redirect( $url ); exit; } /* --------------------------------------------------------------------- * Page rendering * ------------------------------------------------------------------- */ /** * Render the settings page. */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $s = get_option( ADG_AIM_OPT_SETTINGS, ADG_AIM_Plugin::default_settings() ); $bots = $this->detector->get_bots(); $ptypes = get_post_types( array( 'public' => true ), 'objects' ); unset( $ptypes['attachment'] ); $is_pro = ADG_AIM_Plugin::is_pro(); $status = isset( $_GET['adg_status'] ) ? sanitize_key( wp_unslash( $_GET['adg_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only status flag for a notice. ?>

render_llms_notice(); ?> render_readiness(); $this->render_selftest(); } ?>

$bot ) : ?>
/>
+

render_token_savings(); $this->render_referrals(); $this->render_stats(); } ?> render_pro(); ?>


redirect_back( 'stats_reset' ); } /** * "Reset AI referrals" handler (Pro). */ public function handle_reset_referrals() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); if ( class_exists( 'ADG_AIM_Referrals' ) ) { ADG_AIM_Referrals::reset(); } $this->redirect_back( 'referrals_reset' ); } /** * Export AI-referral statistics as CSV. */ public function handle_export_referrals() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); $data = ADG_AIM_Referrals::get(); nocache_headers(); header( 'Content-Type: text/csv; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename=adg-ai-referrals-' . gmdate( 'Ymd-His' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); fputcsv( $out, array( 'source', 'referrals' ) ); foreach ( ADG_AIM_Referrals::by_source( $data ) as $row ) { fputcsv( $out, array( $row['label'], (int) $row['count'] ) ); } fputcsv( $out, array() ); fputcsv( $out, array( 'date', 'referrals' ) ); $by_day = isset( $data['by_day'] ) && is_array( $data['by_day'] ) ? $data['by_day'] : array(); ksort( $by_day ); foreach ( $by_day as $day => $count ) { fputcsv( $out, array( $day, (int) $count ) ); } fputcsv( $out, array() ); fputcsv( $out, array( 'landing_page', 'referrals' ) ); foreach ( ADG_AIM_Referrals::top_pages( $data, 100 ) as $path => $count ) { fputcsv( $out, array( $path, (int) $count ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions exit; } /** * Export statistics as CSV. */ public function handle_export_stats() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); $stats = ADG_AIM_Analytics::get(); nocache_headers(); header( 'Content-Type: text/csv; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename=adg-ai-markdown-stats-' . gmdate( 'Ymd-His' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); // "By bot" section. fputcsv( $out, array( 'bot', 'verified', 'unverified', 'suspicious', 'total' ) ); $by_bot = isset( $stats['by_bot'] ) && is_array( $stats['by_bot'] ) ? $stats['by_bot'] : array(); foreach ( $by_bot as $label => $c ) { $v = isset( $c['verified'] ) ? (int) $c['verified'] : 0; $u = isset( $c['unverified'] ) ? (int) $c['unverified'] : 0; $s = isset( $c['suspicious'] ) ? (int) $c['suspicious'] : 0; fputcsv( $out, array( $label, $v, $u, $s, $v + $u + $s ) ); } // Blank separator + "by day" section. fputcsv( $out, array() ); fputcsv( $out, array( 'date', 'visits' ) ); $by_day = isset( $stats['by_day'] ) && is_array( $stats['by_day'] ) ? $stats['by_day'] : array(); ksort( $by_day ); foreach ( $by_day as $day => $count ) { fputcsv( $out, array( $day, (int) $count ) ); } // "Top pages" section. fputcsv( $out, array() ); fputcsv( $out, array( 'url', 'visits' ) ); foreach ( ADG_AIM_Analytics::top_pages( $stats, 100 ) as $url => $count ) { fputcsv( $out, array( $url, (int) $count ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions exit; } /** * AI referrals dashboard (human visits from AI assistants) — Pro. */ private function render_referrals() { $data = ADG_AIM_Referrals::get(); $total = ADG_AIM_Referrals::total( $data ); ?>

render_referral_chart( $data ); ?> render_referral_sources_chart( $data ); ?>

$count ) : ?>

$count ) : ?>
'#10a37f', 'perplexity' => '#20808d', 'gemini' => '#4285f4', 'copilot' => '#8661c5', 'claude' => '#d97757', 'other' => '#8c8f94', ); $max = 1; foreach ( $rows as $row ) { $max = max( $max, (int) $row['count'] ); } ?>

0 ? ( $count / $total ) * 100 : 0; $color = isset( $colors[ $row['key'] ] ) ? $colors[ $row['key'] ] : '#8c8f94'; ?>
(%)

render_daily_chart( $stats ); ?> render_bot_table( $stats ); ?> render_top_pages( $stats ); ?>

$count ) : ?>

$counts ) { $v = isset( $counts['verified'] ) ? (int) $counts['verified'] : 0; $u = isset( $counts['unverified'] ) ? (int) $counts['unverified'] : 0; $s = isset( $counts['suspicious'] ) ? (int) $counts['suspicious'] : 0; $rows[] = array( 'label' => $label, 'v' => $v, 'u' => $u, 's' => $s, 'total' => $v + $u + $s, ); } usort( $rows, static function ( $a, $b ) { return $b['total'] <=> $a['total']; } ); ?>

$count ) : ?>
redirect_back( 'selftest' ); } /** * "Refresh token-savings estimate" handler (Pro). */ public function handle_refresh_savings() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); if ( class_exists( 'ADG_AIM_Token_Savings' ) ) { ADG_AIM_Token_Savings::get( true ); } $this->redirect_back( 'savings_refreshed' ); } /** * "Send digest now" handler (Pro). */ public function handle_send_digest() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'adg-markdown-for-ai-crawlers' ) ); } check_admin_referer( self::NONCE ); $ok = class_exists( 'ADG_AIM_Digest' ) ? ADG_AIM_Digest::send() : false; $this->redirect_back( $ok ? 'digest_sent' : 'digest_failed' ); } /** * AI Readiness Score banner (score + config checklist). */ private function render_readiness() { $r = ADG_AIM_Readiness::compute(); ?>
/100

array( '✓', '#00a32a', __( 'OK', 'adg-markdown-for-ai-crawlers' ) ), 'warn' => array( '!', '#996800', __( 'Warning', 'adg-markdown-for-ai-crawlers' ) ), 'fail' => array( '✕', '#d63638', __( 'Problem', 'adg-markdown-for-ai-crawlers' ) ), ); $fails = 0; $warns = 0; foreach ( $report as $row ) { if ( 'fail' === $row['status'] ) { ++$fails; } elseif ( 'warn' === $row['status'] ) { ++$warns; } } ?>

↳

pro_teaser_open( __( 'Access policy: training vs citation', 'adg-markdown-for-ai-crawlers' ), __( 'Decide separately whether to allow model-training crawlers (GPTBot, ClaudeBot, Google-Extended) and citation crawlers (OAI-SearchBot, Perplexity, Claude-User). Each bot is tagged by purpose and gated independently.', 'adg-markdown-for-ai-crawlers' ) ); ?> pro_teaser_close(); ?> pro_teaser_open( __( 'robots.txt management', 'adg-markdown-for-ai-crawlers' ), __( 'Write per-crawler Allow / Disallow rules straight into your robots.txt, following the same training vs citation policy — no manual editing.', 'adg-markdown-for-ai-crawlers' ) ); ?>
User-agent: GPTBot
Allow: /

User-agent: PerplexityBot
Disallow: /
pro_teaser_close(); ?> pro_teaser_open( __( 'AI analytics & citation dashboard', 'adg-markdown-for-ai-crawlers' ), __( 'See which AI crawlers read your site, the human visitors each assistant sends back (ChatGPT, Perplexity, Gemini, Copilot, Claude), top pages, token savings and a weekly email digest.', 'adg-markdown-for-ai-crawlers' ) ); ?>

pro_teaser_close(); ?>

ID, '_adg_hide_from_ai_bots', true ); ?> post_status ) : ?>