Changeset 3488445
- Timestamp:
- 03/22/2026 09:45:05 PM (10 days ago)
- Location:
- ai-discovery-files
- Files:
-
- 24 added
- 14 edited
- 1 copied
-
tags/1.3.0 (copied) (copied from ai-discovery-files/trunk)
-
tags/1.3.0/admin/class-admin.php (modified) (5 diffs)
-
tags/1.3.0/admin/class-settings.php (modified) (1 diff)
-
tags/1.3.0/admin/css/crawlers.css (added)
-
tags/1.3.0/admin/js/crawlers.js (added)
-
tags/1.3.0/admin/views/partials/crawler-bot-detail.php (added)
-
tags/1.3.0/admin/views/partials/crawler-bot-selector.php (added)
-
tags/1.3.0/admin/views/partials/crawler-dashboard-widget.php (added)
-
tags/1.3.0/admin/views/partials/crawler-file-access-widget.php (added)
-
tags/1.3.0/admin/views/partials/crawler-log-viewer.php (added)
-
tags/1.3.0/admin/views/settings-page.php (modified) (3 diffs)
-
tags/1.3.0/admin/views/tab-crawlers.php (added)
-
tags/1.3.0/ai-discovery-files.php (modified) (6 diffs)
-
tags/1.3.0/includes/class-crawler-analytics.php (added)
-
tags/1.3.0/includes/class-crawler-conflicts.php (added)
-
tags/1.3.0/includes/class-crawler-logger.php (added)
-
tags/1.3.0/includes/class-crawler-registry.php (added)
-
tags/1.3.0/includes/class-plugin.php (modified) (2 diffs)
-
tags/1.3.0/readme.txt (modified) (5 diffs)
-
tags/1.3.0/uninstall.php (modified) (1 diff)
-
trunk/admin/class-admin.php (modified) (5 diffs)
-
trunk/admin/class-settings.php (modified) (1 diff)
-
trunk/admin/css/crawlers.css (added)
-
trunk/admin/js/crawlers.js (added)
-
trunk/admin/views/partials/crawler-bot-detail.php (added)
-
trunk/admin/views/partials/crawler-bot-selector.php (added)
-
trunk/admin/views/partials/crawler-dashboard-widget.php (added)
-
trunk/admin/views/partials/crawler-file-access-widget.php (added)
-
trunk/admin/views/partials/crawler-log-viewer.php (added)
-
trunk/admin/views/settings-page.php (modified) (3 diffs)
-
trunk/admin/views/tab-crawlers.php (added)
-
trunk/ai-discovery-files.php (modified) (6 diffs)
-
trunk/includes/class-crawler-analytics.php (added)
-
trunk/includes/class-crawler-conflicts.php (added)
-
trunk/includes/class-crawler-logger.php (added)
-
trunk/includes/class-crawler-registry.php (added)
-
trunk/includes/class-plugin.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
ai-discovery-files/tags/1.3.0/admin/class-admin.php
r3482407 r3488445 39 39 add_action( 'wp_ajax_aidf_dismiss_review', array( __CLASS__, 'ajax_dismiss_review' ) ); 40 40 add_action( 'wp_ajax_aidf_search_pages', array( __CLASS__, 'ajax_search_pages' ) ); 41 add_action( 'wp_ajax_aidf_crawler_overview', array( __CLASS__, 'ajax_crawler_overview' ) ); 42 add_action( 'wp_ajax_aidf_crawler_bot_detail', array( __CLASS__, 'ajax_crawler_bot_detail' ) ); 43 add_action( 'wp_ajax_aidf_crawler_log_entries', array( __CLASS__, 'ajax_crawler_log_entries' ) ); 44 add_action( 'wp_ajax_aidf_crawler_export_csv', array( __CLASS__, 'ajax_crawler_export_csv' ) ); 45 add_action( 'wp_ajax_aidf_crawler_clear_log', array( __CLASS__, 'ajax_crawler_clear_log' ) ); 46 add_action( 'wp_ajax_aidf_crawler_conflicts', array( __CLASS__, 'ajax_crawler_conflicts' ) ); 47 add_action( 'wp_ajax_aidf_crawler_file_access', array( __CLASS__, 'ajax_crawler_file_access' ) ); 48 add_action( 'wp_ajax_aidf_crawler_save_settings', array( __CLASS__, 'ajax_crawler_save_settings' ) ); 49 add_action( 'wp_dashboard_setup', array( __CLASS__, 'register_dashboard_widgets' ) ); 50 add_action( 'admin_head-index.php', array( __CLASS__, 'dashboard_widget_styles' ) ); 41 51 add_action( 'admin_notices', array( __CLASS__, 'maybe_show_welcome_notice' ) ); 42 52 add_action( 'admin_notices', array( __CLASS__, 'maybe_show_conflict_notice' ) ); … … 123 133 'noResults' => __( 'No pages found', 'ai-discovery-files' ), 124 134 'typeToSearch' => __( 'Type to search…', 'ai-discovery-files' ), 135 'saving' => __( 'Saving...', 'ai-discovery-files' ), 136 'saveSettings' => __( 'Save Settings', 'ai-discovery-files' ), 137 'confirmClearLog' => __( 'This permanently deletes all crawler log data. This cannot be undone. Continue?', 'ai-discovery-files' ), 138 'noDataYet' => __( 'No data for this period.', 'ai-discovery-files' ), 139 'justNow' => __( 'Just now', 'ai-discovery-files' ), 140 /* translators: %d: number of minutes ago */ 141 'mAgo' => __( '%dm ago', 'ai-discovery-files' ), 142 /* translators: %d: number of hours ago */ 143 'hAgo' => __( '%dh ago', 'ai-discovery-files' ), 144 /* translators: %d: number of days ago */ 145 'dAgo' => __( '%dd ago', 'ai-discovery-files' ), 146 'loading' => __( 'Loading...', 'ai-discovery-files' ), 125 147 ), 126 148 ) 127 149 ); 150 151 $current_tab = self::get_current_tab(); 152 if ( 'crawlers' === $current_tab ) { 153 wp_enqueue_style( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/css/crawlers.css', array( 'aidf-admin' ), AIDF_VERSION ); 154 wp_enqueue_script( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/js/crawlers.js', array( 'jquery' ), AIDF_VERSION, true ); 155 } 128 156 } 129 157 … … 287 315 $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'identity'; 288 316 289 $valid_tabs = array( 'identity', 'permissions', 'content', 'advanced', 'preview', 'status' );317 $valid_tabs = array( 'identity', 'permissions', 'content', 'advanced', 'preview', 'status', 'crawlers' ); 290 318 291 319 if ( ! in_array( $tab, $valid_tabs, true ) ) { … … 309 337 'preview' => __( 'Preview', 'ai-discovery-files' ), 310 338 'status' => __( 'Status', 'ai-discovery-files' ), 339 'crawlers' => __( 'AI Crawlers', 'ai-discovery-files' ), 311 340 ); 312 341 } … … 658 687 wp_send_json_success( $results ); 659 688 } 689 690 /** 691 * Register WordPress dashboard widgets for crawler analytics. 692 * 693 * @since 1.3.0 694 */ 695 public static function register_dashboard_widgets() { 696 $settings = AIDF_Plugin::get_settings(); 697 if ( empty( $settings['crawler_logging_enabled'] ) ) { 698 return; 699 } 700 701 wp_add_dashboard_widget( 702 'aidf_crawler_activity', 703 __( 'AI Crawler Activity', 'ai-discovery-files' ), 704 array( __CLASS__, 'render_crawler_activity_widget' ) 705 ); 706 707 wp_add_dashboard_widget( 708 'aidf_file_access', 709 __( 'AI Discovery File Access', 'ai-discovery-files' ), 710 array( __CLASS__, 'render_file_access_widget' ) 711 ); 712 } 713 714 /** 715 * Render the Crawler Activity dashboard widget. 716 * 717 * @since 1.3.0 718 */ 719 public static function render_crawler_activity_widget() { 720 include AIDF_PLUGIN_DIR . 'admin/views/partials/crawler-dashboard-widget.php'; 721 } 722 723 /** 724 * Render the File Access dashboard widget. 725 * 726 * @since 1.3.0 727 */ 728 public static function render_file_access_widget() { 729 include AIDF_PLUGIN_DIR . 'admin/views/partials/crawler-file-access-widget.php'; 730 } 731 732 /** 733 * Output inline CSS for dashboard widgets (only on the Dashboard screen). 734 * 735 * Gated on crawler_logging_enabled so styles are not injected when the 736 * feature is disabled and the widgets are not registered. 737 * 738 * @since 1.3.0 739 */ 740 public static function dashboard_widget_styles() { 741 $settings = AIDF_Plugin::get_settings(); 742 if ( empty( $settings['crawler_logging_enabled'] ) ) { 743 return; 744 } 745 ?> 746 <style> 747 /* AI Discovery Files — Dashboard Widget Styles */ 748 .aidf-widget-top-bots { color: #5a5d6b; font-size: 13px; } 749 .aidf-widget-warning { color: #d97706; font-size: 13px; } 750 .aidf-widget-warning .dashicons { font-size: 16px; width: 16px; height: 16px; vertical-align: text-bottom; } 751 .aidf-widget-empty { color: #8b8fa3; font-style: italic; } 752 .aidf-widget-link { text-align: right; margin-bottom: 0; } 753 .aidf-widget-link a { color: #e77d15; text-decoration: none; font-weight: 500; } 754 .aidf-widget-link a:hover { color: #d06e0d; } 755 .aidf-widget-files { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } 756 .aidf-widget-file-row { display: flex; align-items: center; gap: 8px; font-size: 12px; } 757 .aidf-widget-file-name { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; width: 110px; flex-shrink: 0; color: #1a1a2e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 758 .aidf-widget-file-bar { flex: 1; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; } 759 .aidf-widget-file-bar-fill { display: block; height: 100%; background: #e77d15; border-radius: 4px; transition: width 0.3s ease; } 760 .aidf-widget-file-count { width: 30px; text-align: right; font-weight: 600; color: #1a1a2e; flex-shrink: 0; } 761 .aidf-widget-file-bots { width: 90px; flex-shrink: 0; color: #5a5d6b; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 762 .aidf-widget-more { color: #8b8fa3; } 763 .aidf-widget-empty-text { color: #8b8fa3; } 764 .aidf-widget-summary { color: #5a5d6b; font-size: 13px; } 765 .aidf-widget-summary strong { color: #1a1a2e; } 766 </style> 767 <?php 768 } 769 770 /** 771 * AJAX: Crawler overview data (summary + chart + breakdown). 772 * 773 * @since 1.3.0 774 */ 775 public static function ajax_crawler_overview() { 776 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 777 if ( ! current_user_can( 'manage_options' ) ) { 778 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 779 } 780 781 $days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30; 782 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 783 $days = 30; 784 } 785 786 wp_send_json_success( array( 787 'summary' => AIDF_Crawler_Analytics::get_summary( $days ), 788 'chart' => AIDF_Crawler_Analytics::get_chart_data( $days ), 789 'breakdown' => AIDF_Crawler_Analytics::get_bot_breakdown( $days ), 790 'days' => $days, 791 ) ); 792 } 793 794 /** 795 * AJAX: Crawler bot detail. 796 * 797 * Returns detailed analytics for a single bot: daily trend, status code 798 * breakdown, pages visited, discovery file access, and first/last seen. 799 * 800 * @since 1.3.0 801 */ 802 public static function ajax_crawler_bot_detail() { 803 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 804 if ( ! current_user_can( 'manage_options' ) ) { 805 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 806 } 807 808 $bot_name = isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : ''; 809 if ( empty( $bot_name ) ) { 810 wp_send_json_error( __( 'Bot name required.', 'ai-discovery-files' ) ); 811 } 812 813 $days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30; 814 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 815 $days = 30; 816 } 817 818 wp_send_json_success( AIDF_Crawler_Analytics::get_bot_detail( $bot_name, $days ) ); 819 } 820 821 /** 822 * AJAX: Crawler log entries. 823 * 824 * Returns a paginated, filtered slice of the raw crawler log. 825 * Supported filters: bot_name, date_from, date_to, status_code, url_path. 826 * 827 * @since 1.3.0 828 */ 829 public static function ajax_crawler_log_entries() { 830 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 831 if ( ! current_user_can( 'manage_options' ) ) { 832 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 833 } 834 835 $args = array( 836 'bot_name' => isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '', 837 'date_from' => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '', 838 'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '', 839 'status_code' => isset( $_GET['status_code'] ) ? sanitize_text_field( wp_unslash( $_GET['status_code'] ) ) : '', 840 'url_path' => isset( $_GET['url_path'] ) ? sanitize_text_field( wp_unslash( $_GET['url_path'] ) ) : '', 841 'page' => isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1, 842 'per_page' => 50, 843 'orderby' => isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'created_at', 844 'order' => isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'DESC', 845 ); 846 847 wp_send_json_success( AIDF_Crawler_Analytics::get_log_entries( $args ) ); 848 } 849 850 /** 851 * AJAX: Export crawler log as CSV. 852 * 853 * Streams a UTF-8 CSV file with all matching log entries and exits. 854 * Filter parameters mirror those of ajax_crawler_log_entries. 855 * 856 * @since 1.3.0 857 */ 858 public static function ajax_crawler_export_csv() { 859 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 860 if ( ! current_user_can( 'manage_options' ) ) { 861 wp_die( esc_html__( 'Permission denied.', 'ai-discovery-files' ) ); 862 } 863 864 $args = array( 865 'bot_name' => isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '', 866 'date_from' => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '', 867 'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '', 868 'status_code' => isset( $_GET['status_code'] ) ? sanitize_text_field( wp_unslash( $_GET['status_code'] ) ) : '', 869 'url_path' => isset( $_GET['url_path'] ) ? sanitize_text_field( wp_unslash( $_GET['url_path'] ) ) : '', 870 ); 871 872 AIDF_Crawler_Analytics::export_csv( $args ); 873 } 874 875 /** 876 * AJAX: Clear crawler log. 877 * 878 * @since 1.3.0 879 */ 880 public static function ajax_crawler_clear_log() { 881 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 882 if ( ! current_user_can( 'manage_options' ) ) { 883 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 884 } 885 886 global $wpdb; 887 888 // Use DELETE instead of TRUNCATE for shared hosting compatibility 889 // (TRUNCATE requires DROP privilege which many hosts don't grant). 890 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 891 $wpdb->query( "DELETE FROM {$wpdb->prefix}aidf_crawler_log" ); 892 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 893 $wpdb->query( "DELETE FROM {$wpdb->prefix}aidf_crawler_summary" ); 894 895 wp_send_json_success( array( 896 'message' => __( 'Log data cleared.', 'ai-discovery-files' ), 897 ) ); 898 } 899 900 /** 901 * AJAX: Crawler conflicts data. 902 * 903 * @since 1.3.0 904 */ 905 public static function ajax_crawler_conflicts() { 906 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 907 if ( ! current_user_can( 'manage_options' ) ) { 908 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 909 } 910 911 $conflicts = AIDF_Crawler_Conflicts::get_conflicts(); 912 913 // Sanitize messages for safe DOM insertion in JavaScript. 914 foreach ( $conflicts as &$alert ) { 915 $alert['message'] = wp_kses_post( $alert['message'] ); 916 } 917 unset( $alert ); 918 919 wp_send_json_success( array( 920 'conflicts' => $conflicts, 921 ) ); 922 } 923 924 /** 925 * AJAX: Crawler file access data. 926 * 927 * @since 1.3.0 928 */ 929 public static function ajax_crawler_file_access() { 930 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 931 if ( ! current_user_can( 'manage_options' ) ) { 932 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 933 } 934 935 $days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30; 936 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 937 $days = 30; 938 } 939 940 wp_send_json_success( array( 941 'file_access' => AIDF_Crawler_Analytics::get_file_access( $days ), 942 'days' => $days, 943 ) ); 944 } 945 946 /** 947 * AJAX: Save crawler settings. 948 * 949 * @since 1.3.0 950 */ 951 public static function ajax_crawler_save_settings() { 952 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 953 if ( ! current_user_can( 'manage_options' ) ) { 954 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 955 } 956 957 $settings = AIDF_Plugin::get_settings(); 958 $was_enabled = ! empty( $settings['crawler_logging_enabled'] ); 959 960 // Logging toggle. 961 $settings['crawler_logging_enabled'] = ! empty( $_POST['crawler_logging_enabled'] ); 962 963 // Retention period. 964 $valid_retention = array( '30', '60', '90', '180', '365' ); 965 $retention = isset( $_POST['crawler_log_retention'] ) 966 ? sanitize_text_field( wp_unslash( $_POST['crawler_log_retention'] ) ) 967 : '90'; 968 $settings['crawler_log_retention'] = in_array( $retention, $valid_retention, true ) ? $retention : '90'; 969 970 // Enabled bots. 971 $enabled_bots = array(); 972 $raw_bots = isset( $_POST['crawler_enabled_bots'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['crawler_enabled_bots'] ) ) : array(); 973 if ( ! empty( $raw_bots ) ) { 974 $all_bot_keys = array_keys( AIDF_Crawler_Registry::get_bots() ); 975 foreach ( $raw_bots as $key ) { 976 if ( in_array( $key, $all_bot_keys, true ) ) { 977 $enabled_bots[] = $key; 978 } 979 } 980 } 981 $settings['crawler_enabled_bots'] = $enabled_bots; 982 983 update_option( 'aidf_settings', $settings ); 984 985 // Manage cron based on toggle change. 986 $is_enabled = ! empty( $settings['crawler_logging_enabled'] ); 987 if ( $is_enabled && ! $was_enabled ) { 988 AIDF_Crawler_Logger::schedule_cron(); 989 } elseif ( ! $is_enabled && $was_enabled ) { 990 AIDF_Crawler_Logger::clear_cron(); 991 } 992 993 wp_send_json_success( array( 994 'message' => __( 'Settings saved.', 'ai-discovery-files' ), 995 ) ); 996 } 660 997 } -
ai-discovery-files/tags/1.3.0/admin/class-settings.php
r3475304 r3488445 278 278 } 279 279 280 // Preserve crawler analytics settings. 281 // Read from $input first (AJAX save passes crawler keys via update_option), 282 // fall back to $fallback (non-crawler tab saves don't include these keys). 283 $clean['crawler_logging_enabled'] = isset( $input['crawler_logging_enabled'] ) 284 ? (bool) $input['crawler_logging_enabled'] 285 : ( isset( $fallback['crawler_logging_enabled'] ) ? (bool) $fallback['crawler_logging_enabled'] : false ); 286 $clean['crawler_log_retention'] = isset( $input['crawler_log_retention'] ) 287 ? sanitize_text_field( $input['crawler_log_retention'] ) 288 : ( isset( $fallback['crawler_log_retention'] ) ? $fallback['crawler_log_retention'] : '90' ); 289 $clean['crawler_enabled_bots'] = isset( $input['crawler_enabled_bots'] ) && is_array( $input['crawler_enabled_bots'] ) 290 ? array_map( 'sanitize_text_field', $input['crawler_enabled_bots'] ) 291 : ( isset( $fallback['crawler_enabled_bots'] ) ? $fallback['crawler_enabled_bots'] : array() ); 292 280 293 // Flush rewrite rules since active files may have changed. 281 294 AIDF_Server::register_rewrite_rules(); -
ai-discovery-files/tags/1.3.0/admin/views/settings-page.php
r3475304 r3488445 51 51 'preview' => 'dashicons-media-code', 52 52 'status' => 'dashicons-yes-alt', 53 'crawlers' => 'dashicons-chart-bar', 53 54 ); 54 55 … … 77 78 <?php include AIDF_PLUGIN_DIR . 'admin/views/partials/directory-cta.php'; ?> 78 79 79 <?php if ( in_array( $current_tab, array( 'preview', 'status' ), true ) ) : ?>80 <?php if ( in_array( $current_tab, array( 'preview', 'status', 'crawlers' ), true ) ) : ?> 80 81 <?php include AIDF_PLUGIN_DIR . 'admin/views/tab-' . $current_tab . '.php'; ?> 81 82 <?php else : ?> … … 85 86 86 87 <!-- Persist active_files and spec_attribution across all tabs --> 87 <?php if ( 'status' !== $current_tab) : ?>88 <?php if ( ! in_array( $current_tab, array( 'status', 'crawlers' ), true ) ) : ?> 88 89 <?php 89 90 $active_files = isset( $settings['active_files'] ) ? (array) $settings['active_files'] : array(); -
ai-discovery-files/tags/1.3.0/ai-discovery-files.php
r3487801 r3488445 3 3 * Plugin Name: AI Discovery Files – llms.txt & AI Visibility 4 4 * Plugin URI: https://www.ai-visibility.org.uk/wordpress-plugin/ai-discovery-files/ 5 * Description: The only WordPress plugin built on the official AI Discovery Files Specification. Generates all 10 files. Built by 365i.6 * Version: 1. 2.15 * Description: The only WordPress plugin that generates all 10 AI Discovery Files and tracks which AI bots visit your site. Built by 365i. 6 * Version: 1.3.0 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 8.0 … … 24 24 * Plugin constants. 25 25 */ 26 define( 'AIDF_VERSION', '1. 2.1' );26 define( 'AIDF_VERSION', '1.3.0' ); 27 27 define( 'AIDF_PLUGIN_FILE', __FILE__ ); 28 28 define( 'AIDF_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 38 38 require_once AIDF_PLUGIN_DIR . 'includes/class-server.php'; 39 39 require_once AIDF_PLUGIN_DIR . 'includes/class-validator.php'; 40 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-registry.php'; 41 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-logger.php'; 42 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-analytics.php'; 43 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-conflicts.php'; 44 45 /** 46 * Create or upgrade the crawler analytics database tables. 47 * 48 * Uses dbDelta() so it is safe to call on both fresh installs and upgrades. 49 * Stores the installed DB version in the aidf_db_version option. 50 */ 51 function aidf_create_crawler_tables() { 52 global $wpdb; 53 54 $charset_collate = $wpdb->get_charset_collate(); 55 56 $sql = "CREATE TABLE {$wpdb->prefix}aidf_crawler_log ( 57 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 58 bot_name varchar(50) NOT NULL, 59 bot_ua varchar(500) NOT NULL DEFAULT '', 60 url_path varchar(500) NOT NULL DEFAULT '', 61 status_code smallint(5) unsigned NOT NULL DEFAULT 200, 62 created_at datetime NOT NULL, 63 PRIMARY KEY (id), 64 KEY bot_date (bot_name, created_at), 65 KEY date_idx (created_at) 66 ) $charset_collate; 67 68 CREATE TABLE {$wpdb->prefix}aidf_crawler_summary ( 69 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 70 bot_name varchar(50) NOT NULL, 71 log_date date NOT NULL, 72 hit_count int(10) unsigned NOT NULL DEFAULT 0, 73 top_page varchar(500) NOT NULL DEFAULT '', 74 status_200 int(10) unsigned NOT NULL DEFAULT 0, 75 status_403 int(10) unsigned NOT NULL DEFAULT 0, 76 status_other int(10) unsigned NOT NULL DEFAULT 0, 77 PRIMARY KEY (id), 78 UNIQUE KEY bot_day (bot_name, log_date), 79 KEY date_idx (log_date) 80 ) $charset_collate;"; 81 82 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 83 dbDelta( $sql ); 84 85 update_option( 'aidf_db_version', AIDF_VERSION ); 86 } 40 87 41 88 /** … … 55 102 56 103 set_transient( 'aidf_activation_redirect', true, 30 ); 104 105 aidf_create_crawler_tables(); 57 106 } 58 107 register_activation_hook( __FILE__, 'aidf_activate' ); … … 63 112 function aidf_deactivate() { 64 113 flush_rewrite_rules(); 114 AIDF_Crawler_Logger::clear_cron(); 65 115 } 66 116 register_deactivation_hook( __FILE__, 'aidf_deactivate' ); … … 73 123 } 74 124 add_action( 'plugins_loaded', 'aidf_init' ); 125 126 /** 127 * Cron: prune old crawler log entries. 128 */ 129 add_action( 'aidf_prune_crawler_log', array( 'AIDF_Crawler_Logger', 'prune_log' ) ); 130 131 /** 132 * Cron: rebuild crawler summary table from raw log data. 133 */ 134 add_action( 'aidf_rebuild_crawler_summary', array( 'AIDF_Crawler_Analytics', 'rebuild_summary' ) ); 135 136 /** 137 * Run DB migrations when the plugin version has advanced. 138 * 139 * Hooked to admin_init so it runs on every admin page load, but only 140 * performs work when the stored version is behind the current version. 141 */ 142 function aidf_maybe_upgrade_db() { 143 $installed_version = get_option( 'aidf_db_version', '0' ); 144 if ( version_compare( $installed_version, AIDF_VERSION, '<' ) ) { 145 aidf_create_crawler_tables(); 146 } 147 } 148 add_action( 'admin_init', 'aidf_maybe_upgrade_db' ); -
ai-discovery-files/tags/1.3.0/includes/class-plugin.php
r3472434 r3488445 39 39 */ 40 40 private function __construct() { 41 AIDF_Crawler_Logger::init(); 41 42 AIDF_Server::init(); 42 43 … … 126 127 // Directory verification. 127 128 'verify_code' => '', 129 130 // Crawler analytics. 131 'crawler_logging_enabled' => false, 132 'crawler_log_retention' => '90', 133 'crawler_enabled_bots' => AIDF_Crawler_Registry::get_default_bot_keys(), 128 134 ); 129 135 -
ai-discovery-files/tags/1.3.0/readme.txt
r3487801 r3488445 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.0 7 Stable tag: 1. 2.17 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 The only WordPress plugin built on the official AI Discovery Files Specification. Generates all 10 files. Built by 365i.11 The only WordPress plugin that generates all 10 AI Discovery Files and tracks which AI bots visit your site. Built by 365i. 12 12 13 13 == Description == … … 66 66 * **Live preview** — see exactly what each file contains before enabling it 67 67 * **Validation** — checks files against the specification and flags issues 68 * **AI Crawler Analytics** — see which AI bots visit your site, how often, and which pages they access 69 * **Discovery File Access tracking** — proof that AI bots are reading the files this plugin generates 70 * **robots.txt conflict detection** — warns when your robots.txt contradicts your AI visibility settings 68 71 * **Conflict detection** — warns if physical files already exist at the same URLs 69 72 * **Directory verification** — verify domain ownership for the [AI Visibility Directory](https://www.ai-visibility.org.uk/) without FTP … … 145 148 }, 10, 3 );` 146 149 150 = What is AI Crawler Analytics? = 151 152 AI Crawler Analytics shows you which AI bots are visiting your site — GPTBot, ClaudeBot, PerplexityBot, Applebot, and 40+ others. You can see how often they visit, which pages they access, and whether any are being blocked by your robots.txt. It also shows which of your AI Discovery Files are being read by AI crawlers, giving you proof that the plugin is working. 153 154 = Will the crawler analytics slow down my website? = 155 156 No. When crawler logging is enabled, the plugin checks each request's user agent against a list of known AI bots. For non-bot requests (99.9% of traffic), this check takes less than a millisecond and involves no database queries. Bot hits are buffered in memory and written to the database once per request on shutdown. When logging is disabled (the default), the check is not registered at all — zero overhead. 157 147 158 = Will this slow down my website? = 148 159 149 No. The plugin only runs when its specific URLs are requested (e.g., `/llms.txt`). It adds zero overhead to your normal page loads. Files are generated on-the-fly from cached settings data. 160 No. The plugin only runs when its specific URLs are requested (e.g., `/llms.txt`). It adds zero overhead to your normal page loads. Files are generated on-the-fly from cached settings data. The crawler analytics feature is disabled by default and adds negligible overhead when enabled. 150 161 151 162 = What happens if I deactivate the plugin? = … … 167 178 168 179 == Changelog == 180 181 = 1.3.0 = 182 * New: AI Crawler Analytics — see which AI bots visit your site 183 * New: Dashboard showing bot visits, frequency, and pages accessed 184 * New: Discovery File Access panel — see which bots read your AI Discovery Files 185 * New: robots.txt conflict detection with actionable alerts 186 * New: Categorised bot registry with 43 AI crawlers across 8 categories 187 * New: Filterable activity log viewer with CSV export 188 * New: Bot detail drill-down with visit trends and page breakdown 189 * New: Two WordPress dashboard widgets (Crawler Activity + File Access) 190 * New: User controls — enable/disable logging, data retention, bot selection 169 191 170 192 = 1.2.1 = … … 207 229 == Upgrade Notice == 208 230 231 = 1.3.0 = 232 New: AI Crawler Analytics. See which AI bots visit your site, track Discovery File access, and detect robots.txt conflicts. Includes dashboard widgets, bot detail drill-downs, filterable log viewer, and CSV export. 233 209 234 = 1.1.0 = 210 235 New: Domain verification for the AI Visibility Directory. Serve your verification file directly from WordPress — no FTP needed. -
ai-discovery-files/tags/1.3.0/uninstall.php
r3475304 r3488445 28 28 delete_option( 'aidf_settings_saved_at' ); 29 29 30 // Drop crawler analytics tables. 31 global $wpdb; 32 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}aidf_crawler_log" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 33 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}aidf_crawler_summary" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 34 35 // Remove crawler-specific options and transients. 36 delete_option( 'aidf_db_version' ); 37 delete_transient( 'aidf_robots_txt_cache' ); 38 wp_clear_scheduled_hook( 'aidf_rebuild_crawler_summary' ); 39 wp_clear_scheduled_hook( 'aidf_prune_crawler_log' ); 40 30 41 // Flush rewrite rules to remove our custom rules. 31 42 flush_rewrite_rules(); -
ai-discovery-files/trunk/admin/class-admin.php
r3482407 r3488445 39 39 add_action( 'wp_ajax_aidf_dismiss_review', array( __CLASS__, 'ajax_dismiss_review' ) ); 40 40 add_action( 'wp_ajax_aidf_search_pages', array( __CLASS__, 'ajax_search_pages' ) ); 41 add_action( 'wp_ajax_aidf_crawler_overview', array( __CLASS__, 'ajax_crawler_overview' ) ); 42 add_action( 'wp_ajax_aidf_crawler_bot_detail', array( __CLASS__, 'ajax_crawler_bot_detail' ) ); 43 add_action( 'wp_ajax_aidf_crawler_log_entries', array( __CLASS__, 'ajax_crawler_log_entries' ) ); 44 add_action( 'wp_ajax_aidf_crawler_export_csv', array( __CLASS__, 'ajax_crawler_export_csv' ) ); 45 add_action( 'wp_ajax_aidf_crawler_clear_log', array( __CLASS__, 'ajax_crawler_clear_log' ) ); 46 add_action( 'wp_ajax_aidf_crawler_conflicts', array( __CLASS__, 'ajax_crawler_conflicts' ) ); 47 add_action( 'wp_ajax_aidf_crawler_file_access', array( __CLASS__, 'ajax_crawler_file_access' ) ); 48 add_action( 'wp_ajax_aidf_crawler_save_settings', array( __CLASS__, 'ajax_crawler_save_settings' ) ); 49 add_action( 'wp_dashboard_setup', array( __CLASS__, 'register_dashboard_widgets' ) ); 50 add_action( 'admin_head-index.php', array( __CLASS__, 'dashboard_widget_styles' ) ); 41 51 add_action( 'admin_notices', array( __CLASS__, 'maybe_show_welcome_notice' ) ); 42 52 add_action( 'admin_notices', array( __CLASS__, 'maybe_show_conflict_notice' ) ); … … 123 133 'noResults' => __( 'No pages found', 'ai-discovery-files' ), 124 134 'typeToSearch' => __( 'Type to search…', 'ai-discovery-files' ), 135 'saving' => __( 'Saving...', 'ai-discovery-files' ), 136 'saveSettings' => __( 'Save Settings', 'ai-discovery-files' ), 137 'confirmClearLog' => __( 'This permanently deletes all crawler log data. This cannot be undone. Continue?', 'ai-discovery-files' ), 138 'noDataYet' => __( 'No data for this period.', 'ai-discovery-files' ), 139 'justNow' => __( 'Just now', 'ai-discovery-files' ), 140 /* translators: %d: number of minutes ago */ 141 'mAgo' => __( '%dm ago', 'ai-discovery-files' ), 142 /* translators: %d: number of hours ago */ 143 'hAgo' => __( '%dh ago', 'ai-discovery-files' ), 144 /* translators: %d: number of days ago */ 145 'dAgo' => __( '%dd ago', 'ai-discovery-files' ), 146 'loading' => __( 'Loading...', 'ai-discovery-files' ), 125 147 ), 126 148 ) 127 149 ); 150 151 $current_tab = self::get_current_tab(); 152 if ( 'crawlers' === $current_tab ) { 153 wp_enqueue_style( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/css/crawlers.css', array( 'aidf-admin' ), AIDF_VERSION ); 154 wp_enqueue_script( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/js/crawlers.js', array( 'jquery' ), AIDF_VERSION, true ); 155 } 128 156 } 129 157 … … 287 315 $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'identity'; 288 316 289 $valid_tabs = array( 'identity', 'permissions', 'content', 'advanced', 'preview', 'status' );317 $valid_tabs = array( 'identity', 'permissions', 'content', 'advanced', 'preview', 'status', 'crawlers' ); 290 318 291 319 if ( ! in_array( $tab, $valid_tabs, true ) ) { … … 309 337 'preview' => __( 'Preview', 'ai-discovery-files' ), 310 338 'status' => __( 'Status', 'ai-discovery-files' ), 339 'crawlers' => __( 'AI Crawlers', 'ai-discovery-files' ), 311 340 ); 312 341 } … … 658 687 wp_send_json_success( $results ); 659 688 } 689 690 /** 691 * Register WordPress dashboard widgets for crawler analytics. 692 * 693 * @since 1.3.0 694 */ 695 public static function register_dashboard_widgets() { 696 $settings = AIDF_Plugin::get_settings(); 697 if ( empty( $settings['crawler_logging_enabled'] ) ) { 698 return; 699 } 700 701 wp_add_dashboard_widget( 702 'aidf_crawler_activity', 703 __( 'AI Crawler Activity', 'ai-discovery-files' ), 704 array( __CLASS__, 'render_crawler_activity_widget' ) 705 ); 706 707 wp_add_dashboard_widget( 708 'aidf_file_access', 709 __( 'AI Discovery File Access', 'ai-discovery-files' ), 710 array( __CLASS__, 'render_file_access_widget' ) 711 ); 712 } 713 714 /** 715 * Render the Crawler Activity dashboard widget. 716 * 717 * @since 1.3.0 718 */ 719 public static function render_crawler_activity_widget() { 720 include AIDF_PLUGIN_DIR . 'admin/views/partials/crawler-dashboard-widget.php'; 721 } 722 723 /** 724 * Render the File Access dashboard widget. 725 * 726 * @since 1.3.0 727 */ 728 public static function render_file_access_widget() { 729 include AIDF_PLUGIN_DIR . 'admin/views/partials/crawler-file-access-widget.php'; 730 } 731 732 /** 733 * Output inline CSS for dashboard widgets (only on the Dashboard screen). 734 * 735 * Gated on crawler_logging_enabled so styles are not injected when the 736 * feature is disabled and the widgets are not registered. 737 * 738 * @since 1.3.0 739 */ 740 public static function dashboard_widget_styles() { 741 $settings = AIDF_Plugin::get_settings(); 742 if ( empty( $settings['crawler_logging_enabled'] ) ) { 743 return; 744 } 745 ?> 746 <style> 747 /* AI Discovery Files — Dashboard Widget Styles */ 748 .aidf-widget-top-bots { color: #5a5d6b; font-size: 13px; } 749 .aidf-widget-warning { color: #d97706; font-size: 13px; } 750 .aidf-widget-warning .dashicons { font-size: 16px; width: 16px; height: 16px; vertical-align: text-bottom; } 751 .aidf-widget-empty { color: #8b8fa3; font-style: italic; } 752 .aidf-widget-link { text-align: right; margin-bottom: 0; } 753 .aidf-widget-link a { color: #e77d15; text-decoration: none; font-weight: 500; } 754 .aidf-widget-link a:hover { color: #d06e0d; } 755 .aidf-widget-files { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } 756 .aidf-widget-file-row { display: flex; align-items: center; gap: 8px; font-size: 12px; } 757 .aidf-widget-file-name { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; width: 110px; flex-shrink: 0; color: #1a1a2e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 758 .aidf-widget-file-bar { flex: 1; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; } 759 .aidf-widget-file-bar-fill { display: block; height: 100%; background: #e77d15; border-radius: 4px; transition: width 0.3s ease; } 760 .aidf-widget-file-count { width: 30px; text-align: right; font-weight: 600; color: #1a1a2e; flex-shrink: 0; } 761 .aidf-widget-file-bots { width: 90px; flex-shrink: 0; color: #5a5d6b; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 762 .aidf-widget-more { color: #8b8fa3; } 763 .aidf-widget-empty-text { color: #8b8fa3; } 764 .aidf-widget-summary { color: #5a5d6b; font-size: 13px; } 765 .aidf-widget-summary strong { color: #1a1a2e; } 766 </style> 767 <?php 768 } 769 770 /** 771 * AJAX: Crawler overview data (summary + chart + breakdown). 772 * 773 * @since 1.3.0 774 */ 775 public static function ajax_crawler_overview() { 776 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 777 if ( ! current_user_can( 'manage_options' ) ) { 778 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 779 } 780 781 $days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30; 782 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 783 $days = 30; 784 } 785 786 wp_send_json_success( array( 787 'summary' => AIDF_Crawler_Analytics::get_summary( $days ), 788 'chart' => AIDF_Crawler_Analytics::get_chart_data( $days ), 789 'breakdown' => AIDF_Crawler_Analytics::get_bot_breakdown( $days ), 790 'days' => $days, 791 ) ); 792 } 793 794 /** 795 * AJAX: Crawler bot detail. 796 * 797 * Returns detailed analytics for a single bot: daily trend, status code 798 * breakdown, pages visited, discovery file access, and first/last seen. 799 * 800 * @since 1.3.0 801 */ 802 public static function ajax_crawler_bot_detail() { 803 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 804 if ( ! current_user_can( 'manage_options' ) ) { 805 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 806 } 807 808 $bot_name = isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : ''; 809 if ( empty( $bot_name ) ) { 810 wp_send_json_error( __( 'Bot name required.', 'ai-discovery-files' ) ); 811 } 812 813 $days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30; 814 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 815 $days = 30; 816 } 817 818 wp_send_json_success( AIDF_Crawler_Analytics::get_bot_detail( $bot_name, $days ) ); 819 } 820 821 /** 822 * AJAX: Crawler log entries. 823 * 824 * Returns a paginated, filtered slice of the raw crawler log. 825 * Supported filters: bot_name, date_from, date_to, status_code, url_path. 826 * 827 * @since 1.3.0 828 */ 829 public static function ajax_crawler_log_entries() { 830 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 831 if ( ! current_user_can( 'manage_options' ) ) { 832 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 833 } 834 835 $args = array( 836 'bot_name' => isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '', 837 'date_from' => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '', 838 'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '', 839 'status_code' => isset( $_GET['status_code'] ) ? sanitize_text_field( wp_unslash( $_GET['status_code'] ) ) : '', 840 'url_path' => isset( $_GET['url_path'] ) ? sanitize_text_field( wp_unslash( $_GET['url_path'] ) ) : '', 841 'page' => isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1, 842 'per_page' => 50, 843 'orderby' => isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'created_at', 844 'order' => isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'DESC', 845 ); 846 847 wp_send_json_success( AIDF_Crawler_Analytics::get_log_entries( $args ) ); 848 } 849 850 /** 851 * AJAX: Export crawler log as CSV. 852 * 853 * Streams a UTF-8 CSV file with all matching log entries and exits. 854 * Filter parameters mirror those of ajax_crawler_log_entries. 855 * 856 * @since 1.3.0 857 */ 858 public static function ajax_crawler_export_csv() { 859 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 860 if ( ! current_user_can( 'manage_options' ) ) { 861 wp_die( esc_html__( 'Permission denied.', 'ai-discovery-files' ) ); 862 } 863 864 $args = array( 865 'bot_name' => isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '', 866 'date_from' => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '', 867 'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '', 868 'status_code' => isset( $_GET['status_code'] ) ? sanitize_text_field( wp_unslash( $_GET['status_code'] ) ) : '', 869 'url_path' => isset( $_GET['url_path'] ) ? sanitize_text_field( wp_unslash( $_GET['url_path'] ) ) : '', 870 ); 871 872 AIDF_Crawler_Analytics::export_csv( $args ); 873 } 874 875 /** 876 * AJAX: Clear crawler log. 877 * 878 * @since 1.3.0 879 */ 880 public static function ajax_crawler_clear_log() { 881 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 882 if ( ! current_user_can( 'manage_options' ) ) { 883 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 884 } 885 886 global $wpdb; 887 888 // Use DELETE instead of TRUNCATE for shared hosting compatibility 889 // (TRUNCATE requires DROP privilege which many hosts don't grant). 890 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 891 $wpdb->query( "DELETE FROM {$wpdb->prefix}aidf_crawler_log" ); 892 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 893 $wpdb->query( "DELETE FROM {$wpdb->prefix}aidf_crawler_summary" ); 894 895 wp_send_json_success( array( 896 'message' => __( 'Log data cleared.', 'ai-discovery-files' ), 897 ) ); 898 } 899 900 /** 901 * AJAX: Crawler conflicts data. 902 * 903 * @since 1.3.0 904 */ 905 public static function ajax_crawler_conflicts() { 906 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 907 if ( ! current_user_can( 'manage_options' ) ) { 908 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 909 } 910 911 $conflicts = AIDF_Crawler_Conflicts::get_conflicts(); 912 913 // Sanitize messages for safe DOM insertion in JavaScript. 914 foreach ( $conflicts as &$alert ) { 915 $alert['message'] = wp_kses_post( $alert['message'] ); 916 } 917 unset( $alert ); 918 919 wp_send_json_success( array( 920 'conflicts' => $conflicts, 921 ) ); 922 } 923 924 /** 925 * AJAX: Crawler file access data. 926 * 927 * @since 1.3.0 928 */ 929 public static function ajax_crawler_file_access() { 930 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 931 if ( ! current_user_can( 'manage_options' ) ) { 932 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 933 } 934 935 $days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30; 936 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 937 $days = 30; 938 } 939 940 wp_send_json_success( array( 941 'file_access' => AIDF_Crawler_Analytics::get_file_access( $days ), 942 'days' => $days, 943 ) ); 944 } 945 946 /** 947 * AJAX: Save crawler settings. 948 * 949 * @since 1.3.0 950 */ 951 public static function ajax_crawler_save_settings() { 952 check_ajax_referer( 'aidf_admin_nonce', 'nonce' ); 953 if ( ! current_user_can( 'manage_options' ) ) { 954 wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) ); 955 } 956 957 $settings = AIDF_Plugin::get_settings(); 958 $was_enabled = ! empty( $settings['crawler_logging_enabled'] ); 959 960 // Logging toggle. 961 $settings['crawler_logging_enabled'] = ! empty( $_POST['crawler_logging_enabled'] ); 962 963 // Retention period. 964 $valid_retention = array( '30', '60', '90', '180', '365' ); 965 $retention = isset( $_POST['crawler_log_retention'] ) 966 ? sanitize_text_field( wp_unslash( $_POST['crawler_log_retention'] ) ) 967 : '90'; 968 $settings['crawler_log_retention'] = in_array( $retention, $valid_retention, true ) ? $retention : '90'; 969 970 // Enabled bots. 971 $enabled_bots = array(); 972 $raw_bots = isset( $_POST['crawler_enabled_bots'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['crawler_enabled_bots'] ) ) : array(); 973 if ( ! empty( $raw_bots ) ) { 974 $all_bot_keys = array_keys( AIDF_Crawler_Registry::get_bots() ); 975 foreach ( $raw_bots as $key ) { 976 if ( in_array( $key, $all_bot_keys, true ) ) { 977 $enabled_bots[] = $key; 978 } 979 } 980 } 981 $settings['crawler_enabled_bots'] = $enabled_bots; 982 983 update_option( 'aidf_settings', $settings ); 984 985 // Manage cron based on toggle change. 986 $is_enabled = ! empty( $settings['crawler_logging_enabled'] ); 987 if ( $is_enabled && ! $was_enabled ) { 988 AIDF_Crawler_Logger::schedule_cron(); 989 } elseif ( ! $is_enabled && $was_enabled ) { 990 AIDF_Crawler_Logger::clear_cron(); 991 } 992 993 wp_send_json_success( array( 994 'message' => __( 'Settings saved.', 'ai-discovery-files' ), 995 ) ); 996 } 660 997 } -
ai-discovery-files/trunk/admin/class-settings.php
r3475304 r3488445 278 278 } 279 279 280 // Preserve crawler analytics settings. 281 // Read from $input first (AJAX save passes crawler keys via update_option), 282 // fall back to $fallback (non-crawler tab saves don't include these keys). 283 $clean['crawler_logging_enabled'] = isset( $input['crawler_logging_enabled'] ) 284 ? (bool) $input['crawler_logging_enabled'] 285 : ( isset( $fallback['crawler_logging_enabled'] ) ? (bool) $fallback['crawler_logging_enabled'] : false ); 286 $clean['crawler_log_retention'] = isset( $input['crawler_log_retention'] ) 287 ? sanitize_text_field( $input['crawler_log_retention'] ) 288 : ( isset( $fallback['crawler_log_retention'] ) ? $fallback['crawler_log_retention'] : '90' ); 289 $clean['crawler_enabled_bots'] = isset( $input['crawler_enabled_bots'] ) && is_array( $input['crawler_enabled_bots'] ) 290 ? array_map( 'sanitize_text_field', $input['crawler_enabled_bots'] ) 291 : ( isset( $fallback['crawler_enabled_bots'] ) ? $fallback['crawler_enabled_bots'] : array() ); 292 280 293 // Flush rewrite rules since active files may have changed. 281 294 AIDF_Server::register_rewrite_rules(); -
ai-discovery-files/trunk/admin/views/settings-page.php
r3475304 r3488445 51 51 'preview' => 'dashicons-media-code', 52 52 'status' => 'dashicons-yes-alt', 53 'crawlers' => 'dashicons-chart-bar', 53 54 ); 54 55 … … 77 78 <?php include AIDF_PLUGIN_DIR . 'admin/views/partials/directory-cta.php'; ?> 78 79 79 <?php if ( in_array( $current_tab, array( 'preview', 'status' ), true ) ) : ?>80 <?php if ( in_array( $current_tab, array( 'preview', 'status', 'crawlers' ), true ) ) : ?> 80 81 <?php include AIDF_PLUGIN_DIR . 'admin/views/tab-' . $current_tab . '.php'; ?> 81 82 <?php else : ?> … … 85 86 86 87 <!-- Persist active_files and spec_attribution across all tabs --> 87 <?php if ( 'status' !== $current_tab) : ?>88 <?php if ( ! in_array( $current_tab, array( 'status', 'crawlers' ), true ) ) : ?> 88 89 <?php 89 90 $active_files = isset( $settings['active_files'] ) ? (array) $settings['active_files'] : array(); -
ai-discovery-files/trunk/ai-discovery-files.php
r3487801 r3488445 3 3 * Plugin Name: AI Discovery Files – llms.txt & AI Visibility 4 4 * Plugin URI: https://www.ai-visibility.org.uk/wordpress-plugin/ai-discovery-files/ 5 * Description: The only WordPress plugin built on the official AI Discovery Files Specification. Generates all 10 files. Built by 365i.6 * Version: 1. 2.15 * Description: The only WordPress plugin that generates all 10 AI Discovery Files and tracks which AI bots visit your site. Built by 365i. 6 * Version: 1.3.0 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 8.0 … … 24 24 * Plugin constants. 25 25 */ 26 define( 'AIDF_VERSION', '1. 2.1' );26 define( 'AIDF_VERSION', '1.3.0' ); 27 27 define( 'AIDF_PLUGIN_FILE', __FILE__ ); 28 28 define( 'AIDF_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 38 38 require_once AIDF_PLUGIN_DIR . 'includes/class-server.php'; 39 39 require_once AIDF_PLUGIN_DIR . 'includes/class-validator.php'; 40 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-registry.php'; 41 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-logger.php'; 42 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-analytics.php'; 43 require_once AIDF_PLUGIN_DIR . 'includes/class-crawler-conflicts.php'; 44 45 /** 46 * Create or upgrade the crawler analytics database tables. 47 * 48 * Uses dbDelta() so it is safe to call on both fresh installs and upgrades. 49 * Stores the installed DB version in the aidf_db_version option. 50 */ 51 function aidf_create_crawler_tables() { 52 global $wpdb; 53 54 $charset_collate = $wpdb->get_charset_collate(); 55 56 $sql = "CREATE TABLE {$wpdb->prefix}aidf_crawler_log ( 57 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 58 bot_name varchar(50) NOT NULL, 59 bot_ua varchar(500) NOT NULL DEFAULT '', 60 url_path varchar(500) NOT NULL DEFAULT '', 61 status_code smallint(5) unsigned NOT NULL DEFAULT 200, 62 created_at datetime NOT NULL, 63 PRIMARY KEY (id), 64 KEY bot_date (bot_name, created_at), 65 KEY date_idx (created_at) 66 ) $charset_collate; 67 68 CREATE TABLE {$wpdb->prefix}aidf_crawler_summary ( 69 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 70 bot_name varchar(50) NOT NULL, 71 log_date date NOT NULL, 72 hit_count int(10) unsigned NOT NULL DEFAULT 0, 73 top_page varchar(500) NOT NULL DEFAULT '', 74 status_200 int(10) unsigned NOT NULL DEFAULT 0, 75 status_403 int(10) unsigned NOT NULL DEFAULT 0, 76 status_other int(10) unsigned NOT NULL DEFAULT 0, 77 PRIMARY KEY (id), 78 UNIQUE KEY bot_day (bot_name, log_date), 79 KEY date_idx (log_date) 80 ) $charset_collate;"; 81 82 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 83 dbDelta( $sql ); 84 85 update_option( 'aidf_db_version', AIDF_VERSION ); 86 } 40 87 41 88 /** … … 55 102 56 103 set_transient( 'aidf_activation_redirect', true, 30 ); 104 105 aidf_create_crawler_tables(); 57 106 } 58 107 register_activation_hook( __FILE__, 'aidf_activate' ); … … 63 112 function aidf_deactivate() { 64 113 flush_rewrite_rules(); 114 AIDF_Crawler_Logger::clear_cron(); 65 115 } 66 116 register_deactivation_hook( __FILE__, 'aidf_deactivate' ); … … 73 123 } 74 124 add_action( 'plugins_loaded', 'aidf_init' ); 125 126 /** 127 * Cron: prune old crawler log entries. 128 */ 129 add_action( 'aidf_prune_crawler_log', array( 'AIDF_Crawler_Logger', 'prune_log' ) ); 130 131 /** 132 * Cron: rebuild crawler summary table from raw log data. 133 */ 134 add_action( 'aidf_rebuild_crawler_summary', array( 'AIDF_Crawler_Analytics', 'rebuild_summary' ) ); 135 136 /** 137 * Run DB migrations when the plugin version has advanced. 138 * 139 * Hooked to admin_init so it runs on every admin page load, but only 140 * performs work when the stored version is behind the current version. 141 */ 142 function aidf_maybe_upgrade_db() { 143 $installed_version = get_option( 'aidf_db_version', '0' ); 144 if ( version_compare( $installed_version, AIDF_VERSION, '<' ) ) { 145 aidf_create_crawler_tables(); 146 } 147 } 148 add_action( 'admin_init', 'aidf_maybe_upgrade_db' ); -
ai-discovery-files/trunk/includes/class-plugin.php
r3472434 r3488445 39 39 */ 40 40 private function __construct() { 41 AIDF_Crawler_Logger::init(); 41 42 AIDF_Server::init(); 42 43 … … 126 127 // Directory verification. 127 128 'verify_code' => '', 129 130 // Crawler analytics. 131 'crawler_logging_enabled' => false, 132 'crawler_log_retention' => '90', 133 'crawler_enabled_bots' => AIDF_Crawler_Registry::get_default_bot_keys(), 128 134 ); 129 135 -
ai-discovery-files/trunk/readme.txt
r3487801 r3488445 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.0 7 Stable tag: 1. 2.17 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 The only WordPress plugin built on the official AI Discovery Files Specification. Generates all 10 files. Built by 365i.11 The only WordPress plugin that generates all 10 AI Discovery Files and tracks which AI bots visit your site. Built by 365i. 12 12 13 13 == Description == … … 66 66 * **Live preview** — see exactly what each file contains before enabling it 67 67 * **Validation** — checks files against the specification and flags issues 68 * **AI Crawler Analytics** — see which AI bots visit your site, how often, and which pages they access 69 * **Discovery File Access tracking** — proof that AI bots are reading the files this plugin generates 70 * **robots.txt conflict detection** — warns when your robots.txt contradicts your AI visibility settings 68 71 * **Conflict detection** — warns if physical files already exist at the same URLs 69 72 * **Directory verification** — verify domain ownership for the [AI Visibility Directory](https://www.ai-visibility.org.uk/) without FTP … … 145 148 }, 10, 3 );` 146 149 150 = What is AI Crawler Analytics? = 151 152 AI Crawler Analytics shows you which AI bots are visiting your site — GPTBot, ClaudeBot, PerplexityBot, Applebot, and 40+ others. You can see how often they visit, which pages they access, and whether any are being blocked by your robots.txt. It also shows which of your AI Discovery Files are being read by AI crawlers, giving you proof that the plugin is working. 153 154 = Will the crawler analytics slow down my website? = 155 156 No. When crawler logging is enabled, the plugin checks each request's user agent against a list of known AI bots. For non-bot requests (99.9% of traffic), this check takes less than a millisecond and involves no database queries. Bot hits are buffered in memory and written to the database once per request on shutdown. When logging is disabled (the default), the check is not registered at all — zero overhead. 157 147 158 = Will this slow down my website? = 148 159 149 No. The plugin only runs when its specific URLs are requested (e.g., `/llms.txt`). It adds zero overhead to your normal page loads. Files are generated on-the-fly from cached settings data. 160 No. The plugin only runs when its specific URLs are requested (e.g., `/llms.txt`). It adds zero overhead to your normal page loads. Files are generated on-the-fly from cached settings data. The crawler analytics feature is disabled by default and adds negligible overhead when enabled. 150 161 151 162 = What happens if I deactivate the plugin? = … … 167 178 168 179 == Changelog == 180 181 = 1.3.0 = 182 * New: AI Crawler Analytics — see which AI bots visit your site 183 * New: Dashboard showing bot visits, frequency, and pages accessed 184 * New: Discovery File Access panel — see which bots read your AI Discovery Files 185 * New: robots.txt conflict detection with actionable alerts 186 * New: Categorised bot registry with 43 AI crawlers across 8 categories 187 * New: Filterable activity log viewer with CSV export 188 * New: Bot detail drill-down with visit trends and page breakdown 189 * New: Two WordPress dashboard widgets (Crawler Activity + File Access) 190 * New: User controls — enable/disable logging, data retention, bot selection 169 191 170 192 = 1.2.1 = … … 207 229 == Upgrade Notice == 208 230 231 = 1.3.0 = 232 New: AI Crawler Analytics. See which AI bots visit your site, track Discovery File access, and detect robots.txt conflicts. Includes dashboard widgets, bot detail drill-downs, filterable log viewer, and CSV export. 233 209 234 = 1.1.0 = 210 235 New: Domain verification for the AI Visibility Directory. Serve your verification file directly from WordPress — no FTP needed. -
ai-discovery-files/trunk/uninstall.php
r3475304 r3488445 28 28 delete_option( 'aidf_settings_saved_at' ); 29 29 30 // Drop crawler analytics tables. 31 global $wpdb; 32 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}aidf_crawler_log" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 33 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}aidf_crawler_summary" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 34 35 // Remove crawler-specific options and transients. 36 delete_option( 'aidf_db_version' ); 37 delete_transient( 'aidf_robots_txt_cache' ); 38 wp_clear_scheduled_hook( 'aidf_rebuild_crawler_summary' ); 39 wp_clear_scheduled_hook( 'aidf_prune_crawler_log' ); 40 30 41 // Flush rewrite rules to remove our custom rules. 31 42 flush_rewrite_rules();
Note: See TracChangeset
for help on using the changeset viewer.