registry = $registry; $this->logger = $logger; $this->logs_table = $logs_table; $this->rules_provider = $rules_provider; add_action( 'admin_menu', [ $this, 'register_menu' ] ); add_action( 'admin_post_aicrg_save_settings', [ $this, 'handle_settings_save' ] ); add_action( 'admin_post_aicrg_crawler_action', [ $this, 'handle_crawler_action' ] ); add_action( 'admin_post_aicrg_clear_logs', [ $this, 'handle_clear_logs' ] ); add_action( 'admin_post_aicrg_export_logs', [ $this, 'handle_export_logs' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } public function register_menu(): void { add_menu_page( __( 'AI Crawler Guard', 'ai-crawler-guard' ), __( 'AI Crawler Guard', 'ai-crawler-guard' ), 'manage_options', 'ai-crawler-guard', [ $this, 'render_dashboard_page' ], 'dashicons-shield', 65 ); add_submenu_page( 'ai-crawler-guard', __( 'Dashboard', 'ai-crawler-guard' ), __( 'Dashboard', 'ai-crawler-guard' ), 'manage_options', 'ai-crawler-guard', [ $this, 'render_dashboard_page' ] ); add_submenu_page( 'ai-crawler-guard', __( 'Crawlers', 'ai-crawler-guard' ), __( 'Crawlers', 'ai-crawler-guard' ), 'manage_options', 'ai-crawler-guard-crawlers', [ $this, 'render_crawlers_page' ] ); add_submenu_page( 'ai-crawler-guard', __( 'Logs', 'ai-crawler-guard' ), __( 'Logs', 'ai-crawler-guard' ), 'manage_options', 'ai-crawler-guard-logs', [ $this, 'render_logs_page' ] ); add_submenu_page( 'ai-crawler-guard', __( 'Settings', 'ai-crawler-guard' ), __( 'Settings', 'ai-crawler-guard' ), 'manage_options', 'ai-crawler-guard-settings', [ $this, 'render_settings_page' ] ); add_submenu_page( 'ai-crawler-guard', __( 'Monetisation', 'ai-crawler-guard' ), __( 'Monetisation (Coming Soon)', 'ai-crawler-guard' ), 'manage_options', 'ai-crawler-guard-upgrade', [ $this, 'render_upgrade_page' ] ); } public function enqueue_assets( $hook ): void { if ( false === strpos( $hook, 'ai-crawler-guard' ) ) { return; } wp_enqueue_style( 'aicrg-admin', plugins_url( 'assets/admin.css', AICRG_PLUGIN_FILE ), [], AICRG_VERSION ); wp_enqueue_script( 'aicrg-admin', plugins_url( 'assets/admin.js', AICRG_PLUGIN_FILE ), [ 'jquery' ], AICRG_VERSION, true ); } public function render_dashboard_page(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $settings = aicrg_get_settings(); $summary = $this->registry->get_summary_stats(); $status_counts = $this->registry->get_status_counts(); $breakdown = $this->get_status_breakdown(); $trend = $this->logger->get_activity_trend( 7 ); $recent_events = $this->logger->get_logs( [ 'limit' => 10 ] ); $total_trend = max( 1, max( array_column( $trend, 'total' ) ?: [ 1 ] ) ); ?>
render_page_header(); ?> render_tabs( 'dashboard' ); ?>
render_stat_card( __( 'Total AI Hits', 'ai-crawler-guard' ), $summary['total_hits'], 'total', __( 'AI crawler requests detected', 'ai-crawler-guard' ) ); ?> render_stat_card( __( 'Blocked AI Hits', 'ai-crawler-guard' ), $summary['blocked_hits'], 'blocked', __( 'Blocked by your rules', 'ai-crawler-guard' ) ); ?> render_stat_card( __( 'Allowed AI Hits', 'ai-crawler-guard' ), $summary['allowed_hits'], 'allowed', __( 'Allowed by your rules', 'ai-crawler-guard' ) ); ?>

  • — 0 && $breakdown['allowed_by_default'] > 0 ) : ?> ( , ) 0 ) : ?> ( )
  • — 0 && $breakdown['blocked_by_default'] > 0 ) : ?> ( , ) 0 ) : ?> ( )

registry->get_all_crawlers( [ 'limit' => $per_page, 'offset' => $offset, 'search' => $search, ] ); $total = $this->registry->count_crawlers( [ 'search' => $search ] ); $total_pages = max( 1, ceil( $total / $per_page ) ); $filtered_rows = []; foreach ( $rows as $row ) { $category_info = $this->rules_provider->get_category_info( $row['bot_slug'] ); if ( 'all' !== $type_filter && $category_info['category'] !== $type_filter ) { continue; } $row['category'] = $category_info['category']; $row['category_label'] = $this->rules_provider->get_category_label( $category_info['category'] ); $filtered_rows[] = $row; } ?>
render_page_header(); ?> render_tabs( 'crawlers' ); ?>

__( 'All', 'ai-crawler-guard' ), 'ai' => __( 'AI', 'ai-crawler-guard' ), 'social' => __( 'Social', 'ai-crawler-guard' ), 'search' => __( 'Search', 'ai-crawler-guard' ), 'custom' => __( 'Custom', 'ai-crawler-guard' ), ]; foreach ( $categories as $value => $label ) { $active = $type_filter === $value ? 'acg-filter-active' : ''; $url = add_query_arg( [ 'page' => 'ai-crawler-guard-crawlers', 'aicrg_type' => $value, 's' => $search ], admin_url( 'admin.php' ) ); printf( '%3$s', esc_url( $url ), esc_attr( $active ), esc_html( $label ) ); } ?>
get_relative_time( $crawler['last_seen'] ); ?>
get_effective_status( $crawler['bot_slug'], $status, $category ); // Determine display label and class if ( 'trusted' === $status ) { $display_label = __( 'Trusted', 'ai-crawler-guard' ); $status_class = 'trusted'; } elseif ( 'blocked' === $status ) { $display_label = __( 'Blocked (User)', 'ai-crawler-guard' ); $status_class = 'blocked'; } else { // Unknown status - show policy-based label if ( 'allowed' === $effective_status ) { $display_label = __( 'Allowed (Policy)', 'ai-crawler-guard' ); $status_class = 'trusted'; } else { $display_label = __( 'Blocked (Policy)', 'ai-crawler-guard' ); $status_class = 'blocked'; } } ?>
get_effective_status( $crawler['bot_slug'], $crawler['status'], $category ); $this->render_crawler_actions( $crawler['bot_slug'], $crawler['status'], $category, $effective_status ); if ( $this->needs_blocking_warning( $crawler['bot_slug'], $category ) ) : ?>
render_pagination( $page, $total_pages, 'ai-crawler-guard-crawlers', [ 'aicrg_type' => $type_filter, 's' => $search ] ); ?>
logger->get_logs( [ 'limit' => $per_page, 'offset' => $offset ] ); $total_logs = $this->logger->count_logs(); $total_pages = max( 1, ceil( $total_logs / $per_page ) ); ?>
render_page_header(); ?> render_tabs( 'logs' ); ?>

max( 1, $page - 1 ) ], $base_url ); $next_link = add_query_arg( [ 'paged' => min( $total_pages, $page + 1 ) ], $base_url ); ?>
render_page_header(); ?> render_tabs( 'settings' ); ?>

$rule ) : ?> render_custom_rule_row( $index, $rule ); ?> render_custom_rule_row( 'new-index', [ 'name' => '', 'pattern' => '', 'category' => 'custom' ], true ); ?>

sanitize_text_field( wp_unslash( $_POST['block_mode'] ?? 'block_known_ai' ) ), 'log_enabled' => isset( $_POST['log_enabled'] ), 'log_limit' => absint( $_POST['log_limit'] ?? 500 ), 'history_enabled' => isset( $_POST['history_enabled'] ), 'core_rules_enabled' => isset( $_POST['core_rules_enabled'] ), 'custom_rules_enabled' => isset( $_POST['custom_rules_enabled'] ), 'auto_update_rules' => false, ]; $custom_rules = []; if ( isset( $_POST['custom_rules'] ) && is_array( $_POST['custom_rules'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Will be sanitized in loop below $unslashed_rules = wp_unslash( $_POST['custom_rules'] ); foreach ( $unslashed_rules as $rule ) { if ( ! is_array( $rule ) ) { continue; } $custom_rules[] = [ 'name' => sanitize_text_field( $rule['name'] ?? '' ), 'pattern' => sanitize_text_field( $rule['pattern'] ?? '' ), 'category' => sanitize_key( $rule['category'] ?? 'custom' ), 'slug' => sanitize_title( $rule['slug'] ?? '' ), ]; } } $settings['custom_rules'] = $custom_rules; aicrg_update_settings( $settings ); wp_safe_redirect( add_query_arg( [ 'page' => 'ai-crawler-guard-settings', 'updated' => 'true' ], admin_url( 'admin.php' ) ) ); exit; } public function handle_crawler_action(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'ai-crawler-guard' ) ); } $slug = sanitize_title( wp_unslash( $_POST['bot_slug'] ?? '' ) ); $status = sanitize_key( wp_unslash( $_POST['status'] ?? 'unknown' ) ); check_admin_referer( 'aicrg_crawler_action_' . $slug ); if ( $slug ) { $this->registry->set_status( $slug, $status ); } wp_safe_redirect( add_query_arg( [ 'page' => 'ai-crawler-guard-crawlers', 'updated' => 'true' ], admin_url( 'admin.php' ) ) ); exit; } public function handle_clear_logs(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'ai-crawler-guard' ) ); } check_admin_referer( 'aicrg_clear_logs' ); $this->logger->clear_logs(); wp_safe_redirect( add_query_arg( [ 'page' => 'ai-crawler-guard-logs', 'cleared' => 'true' ], admin_url( 'admin.php' ) ) ); exit; } public function handle_export_logs(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'ai-crawler-guard' ) ); } check_admin_referer( 'aicrg_export_logs' ); $filename = 'ai-crawler-logs-' . gmdate( 'Y-m-d-H-i-s' ) . '.csv'; header( 'Content-Type: text/csv' ); header( 'Content-Disposition: attachment; filename=' . $filename ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); $output = fopen( 'php://output', 'w' ); fputcsv( $output, [ 'time', 'bot_slug', 'bot_name', 'user_agent', 'url', 'action' ] ); $total = $this->logger->count_logs(); $page = 0; $batch = 200; while ( $page * $batch < $total ) { $logs = $this->logger->get_logs( [ 'limit' => $batch, 'offset' => $page * $batch ] ); foreach ( $logs as $log ) { fputcsv( $output, $log ); } $page++; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Using php://output stream for CSV export fclose( $output ); exit; } private function render_crawler_actions( string $slug, string $current_status, string $category = 'custom', string $effective_status = '' ): void { $actions = [ 'trusted' => [ 'label' => __( 'Allow', 'ai-crawler-guard' ), 'title' => __( 'Always allow this crawler', 'ai-crawler-guard' ), ], 'blocked' => [ 'label' => __( 'Block', 'ai-crawler-guard' ), 'title' => __( 'Always block this crawler', 'ai-crawler-guard' ), ], ]; $needs_warning = $this->needs_blocking_warning( $slug, $category ); // Determine which button should be active based on effective status $active_status = $current_status; if ( 'unknown' === $current_status && $effective_status ) { // For unknown status, highlight based on effective status $active_status = 'allowed' === $effective_status ? 'trusted' : 'blocked'; } foreach ( $actions as $status => $action_data ) { $label = $action_data['label']; $title = $action_data['title']; $classes = 'button acg-action-btn'; if ( $status === $active_status ) { $classes .= ' is-active'; } $data_attrs = ''; if ( 'blocked' === $status && $needs_warning ) { $data_attrs = sprintf( ' data-warning="%s" data-bot-slug="%s"', esc_attr( $this->get_warning_message( $slug ) ), esc_attr( $slug ) ); } $data_attrs .= sprintf( ' title="%s"', esc_attr( $title ) ); printf( '
' . '' . '' . '', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( $slug ), esc_attr( $status ) ); wp_nonce_field( 'aicrg_crawler_action_' . $slug ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $data_attrs is built with esc_attr() above printf( '
', esc_attr( $classes ), $data_attrs, esc_html( $label ) ); } } private function needs_blocking_warning( string $slug, string $category ): bool { $warning_slugs = [ 'facebookbot', 'meta-externalagent', 'meta-externalfetcher', 'twitterbot', 'linkedinbot' ]; return in_array( $slug, $warning_slugs, true ) || 'social' === $category; } private function get_warning_message( string $slug ): string { return __( 'Blocking this crawler may break link previews and social sharing features.', 'ai-crawler-guard' ); } private function render_pagination( int $current_page, int $total_pages, string $page_slug, array $extra_args = [] ): void { if ( $total_pages <= 1 ) { return; } $base_url = admin_url( 'admin.php?page=' . $page_slug ); $prev_link = add_query_arg( array_merge( $extra_args, [ 'paged' => max( 1, $current_page - 1 ) ] ), $base_url ); $next_link = add_query_arg( array_merge( $extra_args, [ 'paged' => min( $total_pages, $current_page + 1 ) ] ), $base_url ); ?>
> = $total_pages ); ?>>
registry->get_all_crawlers( [ 'limit' => 1000 ] ); $settings = aicrg_get_settings(); $block_mode = $settings['block_mode'] ?? 'block_known_ai'; $breakdown = [ 'allowed_total' => 0, 'allowed_by_user' => 0, 'allowed_by_default' => 0, 'blocked_total' => 0, 'blocked_by_user' => 0, 'blocked_by_default' => 0, ]; foreach ( $all_crawlers as $crawler ) { $status = $crawler['status']; $category = $this->rules_provider->get_category_info( $crawler['bot_slug'] )['category']; if ( 'trusted' === $status ) { $breakdown['allowed_total']++; $breakdown['allowed_by_user']++; } elseif ( 'blocked' === $status ) { $breakdown['blocked_total']++; $breakdown['blocked_by_user']++; } else { // Unknown status - calculate effective status $effective = $this->get_effective_status( $crawler['bot_slug'], $status, $category ); if ( 'allowed' === $effective ) { $breakdown['allowed_total']++; $breakdown['allowed_by_default']++; } else { $breakdown['blocked_total']++; $breakdown['blocked_by_default']++; } } } return $breakdown; } private function get_effective_status( string $bot_slug, string $current_status, string $category ): string { // If explicitly set, use that if ( 'trusted' === $current_status ) { return 'allowed'; } if ( 'blocked' === $current_status ) { return 'blocked'; } // For unknown status, calculate based on default policy $settings = aicrg_get_settings(); $block_mode = $settings['block_mode'] ?? 'block_known_ai'; // Social bots are trusted by default if ( 'social' === $category ) { return 'allowed'; } // Follow global mode for other bots if ( 'allow_all' === $block_mode ) { return 'allowed'; } // Default: block known AI crawlers return 'blocked'; } private function get_relative_time( string $datetime ): string { $time = strtotime( $datetime ); if ( ! $time ) { return $datetime; } $diff = time() - $time; if ( $diff < 60 ) { return __( 'Just now', 'ai-crawler-guard' ); } elseif ( $diff < 3600 ) { $mins = round( $diff / 60 ); /* translators: %d: Number of minutes */ return sprintf( _n( '%d min ago', '%d mins ago', $mins, 'ai-crawler-guard' ), $mins ); } elseif ( $diff < 86400 ) { $hours = round( $diff / 3600 ); /* translators: %d: Number of hours */ return sprintf( _n( '%d hour ago', '%d hours ago', $hours, 'ai-crawler-guard' ), $hours ); } elseif ( $diff < 604800 ) { $days = round( $diff / 86400 ); /* translators: %d: Number of days */ return sprintf( _n( '%d day ago', '%d days ago', $days, 'ai-crawler-guard' ), $days ); } else { return date_i18n( 'Y-m-d', $time ); } } private function render_stat_card( string $label, int $value, string $type, string $description = '' ): void { $icon_class = 'dashicons-reddit'; if ( 'blocked' === $type ) { $icon_class = 'dashicons-shield'; } elseif ( 'allowed' === $type ) { $icon_class = 'dashicons-yes-alt'; } ?>

[ 'label' => __( 'Dashboard', 'ai-crawler-guard' ), 'url' => admin_url( 'admin.php?page=ai-crawler-guard' ) ], 'crawlers' => [ 'label' => __( 'Crawlers', 'ai-crawler-guard' ), 'url' => admin_url( 'admin.php?page=ai-crawler-guard-crawlers' ) ], 'logs' => [ 'label' => __( 'Logs', 'ai-crawler-guard' ), 'url' => admin_url( 'admin.php?page=ai-crawler-guard-logs' ) ], 'settings' => [ 'label' => __( 'Settings', 'ai-crawler-guard' ), 'url' => admin_url( 'admin.php?page=ai-crawler-guard-settings' ) ], ]; ?> __( 'AI / LLM', 'ai-crawler-guard' ), 'search' => __( 'AI Search', 'ai-crawler-guard' ), 'social' => __( 'Social / Preview', 'ai-crawler-guard' ), 'utility' => __( 'AI Utility', 'ai-crawler-guard' ), 'custom' => __( 'Custom', 'ai-crawler-guard' ), ]; $row_class = $is_template ? 'acg-custom-rule-template' : ''; ?> data-template="1" style="display:none;">