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_daily_chart( $stats ); ?> render_bot_table( $stats ); ?> render_top_pages( $stats ); ?>
$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']; } ); ?>
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' ) ); ?>