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' ); ?>
|
|
|
|
|
|
|
|
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' ); ?>
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_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 );
?>
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;">
|
|
|
|