id !== self::$hook_suffix ) {
return;
}
if ( ! AIDF_Multilingual::is_active() || AIDF_Multilingual::is_enabled() ) {
return;
}
$languages = wp_list_pluck( AIDF_Multilingual::get_secondary_languages(), 'name' );
if ( empty( $languages ) ) {
return;
}
printf(
'
',
esc_html__( 'AI Discovery Files — translated pages are not being published', 'ai-discovery-files' ),
sprintf(
/* translators: 1: multilingual plugin name, 2: comma-separated language names */
esc_html__( '%1$s is active and your pages in %2$s are being left out of your AI Discovery Files. AI systems reading them will see your default language only. Turn on "Include translated pages" under Advanced to publish them.', 'ai-discovery-files' ),
esc_html( AIDF_Multilingual::get_plugin_label() ),
esc_html( implode( ', ', $languages ) )
)
);
}
/**
* Add the top-level menu page.
*
* @return void
*/
public static function add_menu_page() {
self::$hook_suffix = add_menu_page(
__( 'AI Discovery Files', 'ai-discovery-files' ),
__( 'AI Discovery', 'ai-discovery-files' ),
'manage_options',
'ai-discovery-files',
array( __CLASS__, 'render_page' ),
'dashicons-visibility',
80
);
}
/**
* Enqueue admin CSS and JS on our settings page only.
*
* @param string $hook_suffix Current admin page hook suffix.
* @return void
*/
/**
* Build a cache-busting version string for a plugin asset. Uses the file's
* modification time when available, falls back to AIDF_VERSION otherwise.
*
* Why: when we ship multiple builds of the same plugin version (during
* release-candidate testing, hot fixes, etc.) the constant AIDF_VERSION
* stays the same, so browsers and CDNs see the same ?ver=X.Y.Z URL and
* serve cached CSS/JS even after the file body has changed. filemtime
* solves that: every actual file change produces a new ?ver= value.
*
* @param string $relative_path Path relative to AIDF_PLUGIN_DIR (e.g. "admin/js/admin.js").
* @return string
*/
private static function asset_ver( $relative_path ) {
$full = AIDF_PLUGIN_DIR . $relative_path;
if ( file_exists( $full ) ) {
$mtime = filemtime( $full );
if ( false !== $mtime ) {
return AIDF_VERSION . '.' . $mtime;
}
}
return AIDF_VERSION;
}
public static function enqueue_assets( $hook_suffix ) {
if ( self::$hook_suffix !== $hook_suffix ) {
return;
}
wp_enqueue_media();
wp_enqueue_style(
'aidf-admin',
AIDF_PLUGIN_URL . 'admin/css/admin.css',
array(),
self::asset_ver( 'admin/css/admin.css' )
);
wp_enqueue_script(
'aidf-admin',
AIDF_PLUGIN_URL . 'admin/js/admin.js',
array( 'jquery' ),
self::asset_ver( 'admin/js/admin.js' ),
true
);
wp_localize_script(
'aidf-admin',
'aidfAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aidf_admin_nonce' ),
'i18n' => array(
'confirmReset' => __( 'Reset all settings to defaults? This cannot be undone.', 'ai-discovery-files' ),
'generating' => __( 'Generating preview…', 'ai-discovery-files' ),
'validating' => __( 'Validating files…', 'ai-discovery-files' ),
'saved' => __( 'Settings saved.', 'ai-discovery-files' ),
'selectLogo' => __( 'Select Logo', 'ai-discovery-files' ),
'changeLogo' => __( 'Change Logo', 'ai-discovery-files' ),
'useThisImage' => __( 'Use This Image', 'ai-discovery-files' ),
'chooseLogo' => __( 'Choose a Logo Image', 'ai-discovery-files' ),
'remove' => __( 'Remove', 'ai-discovery-files' ),
'questionPlaceholder' => __( 'Question…', 'ai-discovery-files' ),
'answerPlaceholder' => __( 'Answer…', 'ai-discovery-files' ),
'faqUrlPlaceholder' => __( 'Link URL (optional) — page with more information', 'ai-discovery-files' ),
'servicePlaceholder' => __( 'Service or product name', 'ai-discovery-files' ),
'serviceUrlPlaceholder' => __( 'Page URL (optional)', 'ai-discovery-files' ),
'namePlaceholder' => __( 'Full name', 'ai-discovery-files' ),
'rolePlaceholder' => __( 'Role (e.g. Managing Director)', 'ai-discovery-files' ),
'personUrlPlaceholder' => __( 'Profile URL (LinkedIn, team page, etc.)', 'ai-discovery-files' ),
'valid' => __( 'Valid', 'ai-discovery-files' ),
'allValid' => __( 'All active files are valid and consistent.', 'ai-discovery-files' ),
'previewError' => __( 'Error: Could not load preview.', 'ai-discovery-files' ),
'errorPrefix' => __( 'Error: ', 'ai-discovery-files' ),
'unknownError' => __( 'Unknown error', 'ai-discovery-files' ),
'crossFileConsistency' => __( 'Cross-File Consistency', 'ai-discovery-files' ),
'verifyFileActive' => __( 'Verification file is live at', 'ai-discovery-files' ),
'verifyFileInactive' => __( 'No verification code set. Enter your code above to activate.', 'ai-discovery-files' ),
'searchPages' => __( 'Search pages…', 'ai-discovery-files' ),
'searching' => __( 'Searching…', 'ai-discovery-files' ),
'noResults' => __( 'No pages found', 'ai-discovery-files' ),
'typeToSearch' => __( 'Type to search…', 'ai-discovery-files' ),
'saving' => __( 'Saving...', 'ai-discovery-files' ),
'saveSettings' => __( 'Save Settings', 'ai-discovery-files' ),
'confirmClearLog' => __( 'This permanently deletes all crawler log data. This cannot be undone. Continue?', 'ai-discovery-files' ),
'noDataYet' => __( 'No data for this period.', 'ai-discovery-files' ),
'justNow' => __( 'Just now', 'ai-discovery-files' ),
/* translators: %d: number of minutes ago */
'mAgo' => __( '%dm ago', 'ai-discovery-files' ),
/* translators: %d: number of hours ago */
'hAgo' => __( '%dh ago', 'ai-discovery-files' ),
/* translators: %d: number of days ago */
'dAgo' => __( '%dd ago', 'ai-discovery-files' ),
'loading' => __( 'Loading...', 'ai-discovery-files' ),
),
)
);
// AI Import modal assets — loaded on the plugin's settings screen.
wp_enqueue_style(
'aidf-import-modal',
AIDF_PLUGIN_URL . 'admin/css/aidf-import-modal.css',
array(),
self::asset_ver( 'admin/css/aidf-import-modal.css' )
);
wp_enqueue_script(
'aidf-import-modal',
AIDF_PLUGIN_URL . 'admin/js/aidf-import-modal.js',
array(),
self::asset_ver( 'admin/js/aidf-import-modal.js' ),
true
);
wp_localize_script(
'aidf-import-modal',
'aidfImportData',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( AIDF_Import::NONCE_ACTION ),
'homeUrl' => home_url( '/' ),
// WordPress's own Tagline from Settings -> General. Used by the
// "Use WordPress tagline" one-click button on the single-line
// tagline field — no AI call needed for something WP already knows.
'wpTagline' => get_bloginfo( 'description' ),
// Hard client-side gate: if false, the import modal must NOT open
// when an AI Import button is clicked. Surfaces aiUnavailableText
// to the user instead.
'aiAvailable' => AIDF_AI_Extract::is_available(),
'aiUnavailableText' => __( 'No AI provider is configured on this site. Install an AI connector and add your API key under Settings -> Connectors, then try again.', 'ai-discovery-files' ),
'connectorsUrl' => admin_url( 'options-connectors.php' ),
)
);
$current_tab = self::get_current_tab();
if ( 'crawlers' === $current_tab ) {
wp_enqueue_style( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/css/crawlers.css', array( 'aidf-admin' ), AIDF_VERSION );
wp_enqueue_script( 'aidf-chartjs', AIDF_PLUGIN_URL . 'admin/js/chart.min.js', array(), '4.4.8', true );
wp_enqueue_script( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/js/crawlers.js', array( 'jquery', 'aidf-chartjs' ), AIDF_VERSION, true );
}
}
/**
* Redirect to settings page after activation.
*
* @return void
*/
public static function handle_activation_redirect() {
if ( ! get_transient( 'aidf_activation_redirect' ) ) {
return;
}
delete_transient( 'aidf_activation_redirect' );
if ( wp_doing_ajax() || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
wp_safe_redirect( admin_url( 'admin.php?page=ai-discovery-files' ) );
exit;
}
/**
* Add Settings and Docs links to plugin action links.
*
* @param array $links Existing action links.
* @return array
*/
public static function add_action_links( $links ) {
$settings_link = sprintf(
'%s',
esc_url( admin_url( 'admin.php?page=ai-discovery-files' ) ),
esc_html__( 'Settings', 'ai-discovery-files' )
);
$docs_link = sprintf(
'%s',
esc_url( 'https://www.ai-visibility.org.uk/specifications/' ),
esc_html__( 'Docs', 'ai-discovery-files' )
);
array_unshift( $links, $docs_link );
array_unshift( $links, $settings_link );
return $links;
}
/**
* Render the settings page.
*
* @return void
*/
public static function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
include AIDF_PLUGIN_DIR . 'admin/views/settings-page.php';
}
/**
* AJAX handler: preview a file type.
*
* @return void
*/
public static function ajax_preview_file() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$file_slug = isset( $_POST['file_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['file_slug'] ) ) : '';
$file_types = AIDF_Plugin::get_file_types();
if ( empty( $file_slug ) || ! isset( $file_types[ $file_slug ] ) ) {
wp_send_json_error( __( 'Invalid file type.', 'ai-discovery-files' ) );
}
$content = AIDF_Generator::generate( $file_slug );
if ( false === $content ) {
wp_send_json_error( __( 'Could not generate file.', 'ai-discovery-files' ) );
}
wp_send_json_success(
array(
'content' => $content,
'filename' => $file_types[ $file_slug ]['filename'],
'url' => AIDF_Server::get_file_url( $file_slug ),
)
);
}
/**
* AJAX handler: validate all active files.
*
* @return void
*/
public static function ajax_validate_files() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$results = AIDF_Validator::validate_all();
wp_send_json_success( $results );
}
/**
* AJAX handler: save domain verification code.
*
* @return void
*/
public static function ajax_save_verify_code() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$code = isset( $_POST['verify_code'] ) ? sanitize_text_field( wp_unslash( $_POST['verify_code'] ) ) : '';
// Strip the prefix if pasted — we only store the hex token.
$code = preg_replace( '/^ai-visibility-verify=/i', '', $code );
// Validate: must be hex characters only, or empty to clear.
if ( '' !== $code && ! preg_match( '/^[a-f0-9]+$/i', $code ) ) {
wp_send_json_error( __( 'Invalid verification code. Paste the code shown in your directory dashboard.', 'ai-discovery-files' ) );
}
$settings = get_option( 'aidf_settings', array() );
$settings['verify_code'] = $code;
update_option( 'aidf_settings', $settings );
// Flush rewrite rules to ensure the verify file route is active.
AIDF_Server::register_rewrite_rules();
flush_rewrite_rules();
$data = array(
'message' => empty( $code )
? __( 'Verification code removed.', 'ai-discovery-files' )
: __( 'Verification code saved.', 'ai-discovery-files' ),
'active' => ! empty( $code ),
'url' => home_url( '/ai-visibility-verify.txt' ),
);
wp_send_json_success( $data );
}
/**
* Get the current active tab.
*
* @return string
*/
public static function get_current_tab() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab display only, no state change.
$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'identity';
$valid_tabs = array( 'identity', 'permissions', 'content', 'advanced', 'preview', 'status', 'crawlers' );
if ( ! in_array( $tab, $valid_tabs, true ) ) {
$tab = 'identity';
}
return $tab;
}
/**
* Get all tabs with labels.
*
* @return array
*/
public static function get_tabs() {
return array(
'identity' => __( 'Identity', 'ai-discovery-files' ),
'permissions' => __( 'Permissions', 'ai-discovery-files' ),
'content' => __( 'Content', 'ai-discovery-files' ),
'advanced' => __( 'Technical', 'ai-discovery-files' ),
'preview' => __( 'Preview', 'ai-discovery-files' ),
'status' => __( 'Status', 'ai-discovery-files' ),
'crawlers' => __( 'AI Crawlers', 'ai-discovery-files' ),
);
}
/**
* Render an inline tooltip icon with hover popup.
*
* @param string $text Tooltip content (plain text).
* @return void
*/
public static function tooltip( $text ) {
printf(
'',
esc_attr__( 'More information', 'ai-discovery-files' ),
esc_html( $text )
);
}
/**
* Show a welcome notice if the plugin hasn't been configured.
*
* @return void
*/
public static function maybe_show_welcome_notice() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Don't show on our own settings page — they're already there.
$screen = get_current_screen();
if ( $screen && self::$hook_suffix === $screen->id ) {
return;
}
// Check if user has dismissed this notice.
if ( get_user_meta( get_current_user_id(), 'aidf_welcome_dismissed', true ) ) {
return;
}
// Only show if plugin hasn't been meaningfully configured.
$saved = get_option( 'aidf_settings', array() );
if ( ! is_array( $saved ) ) {
$saved = array();
}
$has_custom_name = ! empty( $saved['business_name'] ) && $saved['business_name'] !== get_bloginfo( 'name' );
$has_services = ! empty( $saved['services'] );
if ( $has_custom_name || $has_services ) {
return;
}
// Inline only the welcome notice styles — avoid loading the full admin.css on every page.
wp_register_style( 'aidf-welcome-notice', false, array(), AIDF_VERSION );
wp_enqueue_style( 'aidf-welcome-notice' );
wp_add_inline_style(
'aidf-welcome-notice',
'.aidf-welcome-notice{border-left-color:#e77d15!important}' .
'.aidf-welcome-notice .aidf-welcome-inner{display:flex;align-items:center;gap:12px;padding:4px 0}' .
'.aidf-welcome-notice .aidf-welcome-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#fef3e6;border-radius:6px;flex-shrink:0}' .
'.aidf-welcome-notice .aidf-welcome-icon .dashicons{color:#e77d15;font-size:18px;width:18px;height:18px}' .
'.aidf-welcome-notice .aidf-welcome-text strong{display:block;margin-bottom:2px}' .
'.aidf-welcome-notice .aidf-welcome-text a{font-weight:600}'
);
$settings_url = admin_url( 'admin.php?page=ai-discovery-files' );
$nonce = wp_create_nonce( 'aidf_dismiss_welcome' );
wp_add_inline_script(
'jquery',
'jQuery( document ).on( "click", "#aidf-welcome-notice .notice-dismiss", function() {' .
'jQuery.post( ajaxurl, {' .
'action: "aidf_dismiss_welcome",' .
'nonce: document.getElementById( "aidf-welcome-notice" ).getAttribute( "data-nonce" )' .
'} );' .
'} );'
);
?>
id, array( self::$hook_suffix, 'plugins' ), true ) ) {
return;
}
$conflicts = self::detect_file_conflicts();
if ( empty( $conflicts ) ) {
return;
}
$messages = array();
foreach ( $conflicts as $conflict ) {
$messages[] = sprintf(
/* translators: 1: plugin name, 2: feature name */
esc_html__( '%1$s has %2$s enabled, which may conflict with this plugin.', 'ai-discovery-files' ),
'' . esc_html( $conflict['plugin'] ) . '',
esc_html( $conflict['feature'] )
);
}
printf(
'%s
',
esc_html__( 'AI Discovery Files — Potential Conflict Detected', 'ai-discovery-files' )
);
foreach ( $messages as $msg ) {
echo '- ' . wp_kses( $msg, array( 'strong' => array() ) ) . '
';
}
printf(
'
%s
',
esc_html__( 'To avoid duplicate or conflicting AI discovery files, disable the other plugin\'s AI file generation feature, or deactivate those file types in AI Discovery Files settings.', 'ai-discovery-files' )
);
}
/**
* Detect other plugins that generate AI discovery files.
*
* @return array> List of conflicts with 'plugin' and 'feature' keys.
*/
private static function detect_file_conflicts() {
$conflicts = array();
// Yoast SEO — llms.txt (added in Yoast SEO 24.x).
if ( defined( 'WPSEO_VERSION' ) ) {
$yoast_options = get_option( 'wpseo', array() );
if ( ! empty( $yoast_options['enable_llms_txt'] ) ) {
$conflicts[] = array(
'plugin' => 'Yoast SEO',
'feature' => 'llms.txt generation',
);
}
}
// Rank Math — llms.txt (added in Rank Math 1.0.230+).
if ( class_exists( 'RankMath' ) ) {
$rm_general = get_option( 'rank-math-options-general', array() );
if ( ! empty( $rm_general['enable_llms_txt'] ) ) {
$conflicts[] = array(
'plugin' => 'Rank Math SEO',
'feature' => 'llms.txt generation',
);
}
}
// SEOPress — llms.txt support.
if ( defined( 'SEOPRESS_VERSION' ) ) {
$sp_options = get_option( 'seopress_pro_option_name', array() );
if ( ! empty( $sp_options['seopress_ai_llms_txt'] ) ) {
$conflicts[] = array(
'plugin' => 'SEOPress',
'feature' => 'llms.txt generation',
);
}
}
// All in One SEO.
if ( defined( 'AIOSEO_VERSION' ) ) {
$aioseo_options = get_option( 'aioseo_options', '' );
if ( is_string( $aioseo_options ) ) {
$aioseo_options = json_decode( $aioseo_options, true );
}
if ( is_array( $aioseo_options ) && ! empty( $aioseo_options['searchAppearance']['advanced']['llmsTxt']['enable'] ) ) {
$conflicts[] = array(
'plugin' => 'All in One SEO',
'feature' => 'llms.txt generation',
);
}
}
return $conflicts;
}
/**
* AJAX handler: dismiss the welcome notice permanently.
*
* @return void
*/
public static function ajax_dismiss_welcome() {
check_ajax_referer( 'aidf_dismiss_welcome', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error();
}
update_user_meta( get_current_user_id(), 'aidf_welcome_dismissed', 1 );
wp_send_json_success();
}
/**
* Check whether the review prompt banner should display.
*
* @return bool
*/
public static function should_show_review_banner() {
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
// Condition 1: settings saved at least once.
$saved_at = get_option( 'aidf_settings_saved_at' );
if ( false === $saved_at ) {
return false;
}
// Condition 2: 2+ active files.
$settings = get_option( 'aidf_settings', array() );
$active_files = isset( $settings['active_files'] ) ? (array) $settings['active_files'] : array();
if ( count( $active_files ) < 2 ) {
return false;
}
// Condition 3: 3+ days since activation.
$activated_at = get_option( 'aidf_activated_at' );
if ( false === $activated_at || ( time() - (int) $activated_at ) < ( 3 * DAY_IN_SECONDS ) ) {
return false;
}
// Condition 4: not permanently dismissed.
$user_id = get_current_user_id();
$dismissed = get_user_meta( $user_id, 'aidf_review_dismissed', true );
if ( in_array( $dismissed, array( 'done', 'low' ), true ) ) {
return false;
}
// Condition 5: not snoozed (or 90+ days elapsed).
$snoozed = get_user_meta( $user_id, 'aidf_review_snoozed', true );
if ( $snoozed && ( time() - (int) $snoozed ) < ( 90 * DAY_IN_SECONDS ) ) {
return false;
}
return true;
}
/**
* AJAX handler: dismiss the review prompt.
*
* @return void
*/
public static function ajax_dismiss_review() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error();
}
$reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : '';
$user_id = get_current_user_id();
switch ( $reason ) {
case 'done':
case 'reviewed':
case 'low':
update_user_meta( $user_id, 'aidf_review_dismissed', $reason );
delete_user_meta( $user_id, 'aidf_review_snoozed' );
break;
case 'snooze':
update_user_meta( $user_id, 'aidf_review_snoozed', time() );
break;
default:
wp_send_json_error( 'Invalid reason.' );
}
wp_send_json_success();
}
/**
* AJAX handler: search pages for the page picker autocomplete.
*
* @return void
*/
public static function ajax_search_pages() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
if ( strlen( $search ) < 2 ) {
wp_send_json_success( array() );
}
// Determine post types — defaults to page, but caller can request additional types.
$raw_types = isset( $_POST['post_types'] ) ? sanitize_text_field( wp_unslash( $_POST['post_types'] ) ) : 'page';
$allowed = array( 'page', 'product' );
$post_types = array_intersect(
array_map( 'trim', explode( ',', $raw_types ) ),
$allowed
);
if ( empty( $post_types ) ) {
$post_types = array( 'page' );
}
$query = new WP_Query(
array(
's' => $search,
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => 20,
'orderby' => 'relevance',
'no_found_rows' => true,
)
);
$results = array();
foreach ( $query->posts as $post ) {
$results[] = array(
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink( $post ),
'type' => $post->post_type,
);
}
wp_send_json_success( $results );
}
/**
* Register WordPress dashboard widgets for crawler analytics.
*
* @since 1.3.0
*/
public static function register_dashboard_widgets() {
$settings = AIDF_Plugin::get_settings();
if ( empty( $settings['crawler_logging_enabled'] ) ) {
return;
}
wp_add_dashboard_widget(
'aidf_file_access',
__( 'AI Discovery File Access', 'ai-discovery-files' ),
array( __CLASS__, 'render_file_access_widget' )
);
}
/**
* Render the File Access dashboard widget.
*
* @since 1.3.0
*/
public static function render_file_access_widget() {
include AIDF_PLUGIN_DIR . 'admin/views/partials/crawler-file-access-widget.php';
}
/**
* Output inline CSS for dashboard widgets (only on the Dashboard screen).
*
* Gated on crawler_logging_enabled so styles are not injected when the
* feature is disabled and the widgets are not registered.
*
* @since 1.3.0
*/
public static function dashboard_widget_styles() {
$settings = AIDF_Plugin::get_settings();
if ( empty( $settings['crawler_logging_enabled'] ) ) {
return;
}
?>
AIDF_Crawler_Analytics::get_summary( $days ),
'chart' => AIDF_Crawler_Analytics::get_chart_data( $days ),
'breakdown' => AIDF_Crawler_Analytics::get_bot_breakdown( $days ),
'days' => $days,
) );
}
/**
* AJAX: Crawler bot detail.
*
* Returns detailed analytics for a single bot: daily trend, status code
* breakdown, files accessed, discovery file access, and first/last seen.
*
* @since 1.3.0
*/
public static function ajax_crawler_bot_detail() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$bot_name = isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '';
if ( empty( $bot_name ) ) {
wp_send_json_error( __( 'Bot name required.', 'ai-discovery-files' ) );
}
$days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30;
if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) {
$days = 30;
}
wp_send_json_success( AIDF_Crawler_Analytics::get_bot_detail( $bot_name, $days ) );
}
/**
* AJAX: Crawler log entries.
*
* Returns a paginated, filtered slice of the raw crawler log.
* Supported filters: bot_name, date_from, date_to, status_code, url_path.
*
* @since 1.3.0
*/
public static function ajax_crawler_log_entries() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$args = array(
'bot_name' => isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '',
'date_from' => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '',
'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '',
'status_code' => isset( $_GET['status_code'] ) ? sanitize_text_field( wp_unslash( $_GET['status_code'] ) ) : '',
'url_path' => isset( $_GET['url_path'] ) ? sanitize_text_field( wp_unslash( $_GET['url_path'] ) ) : '',
'page' => isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1,
'per_page' => 50,
'orderby' => isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'created_at',
'order' => isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'DESC',
);
wp_send_json_success( AIDF_Crawler_Analytics::get_log_entries( $args ) );
}
/**
* AJAX: Export crawler log as CSV.
*
* Streams a UTF-8 CSV file with all matching log entries and exits.
* Filter parameters mirror those of ajax_crawler_log_entries.
*
* @since 1.3.0
*/
public static function ajax_crawler_export_csv() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'ai-discovery-files' ) );
}
$args = array(
'bot_name' => isset( $_GET['bot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['bot_name'] ) ) : '',
'date_from' => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '',
'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '',
'status_code' => isset( $_GET['status_code'] ) ? sanitize_text_field( wp_unslash( $_GET['status_code'] ) ) : '',
'url_path' => isset( $_GET['url_path'] ) ? sanitize_text_field( wp_unslash( $_GET['url_path'] ) ) : '',
);
AIDF_Crawler_Analytics::export_csv( $args );
}
/**
* AJAX: Clear crawler log.
*
* @since 1.3.0
*/
public static function ajax_crawler_clear_log() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
global $wpdb;
// Use DELETE instead of TRUNCATE for shared hosting compatibility
// (TRUNCATE requires DROP privilege which many hosts don't grant).
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DELETE FROM {$wpdb->prefix}aidf_crawler_log" );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DELETE FROM {$wpdb->prefix}aidf_crawler_summary" );
wp_send_json_success( array(
'message' => __( 'Log data cleared.', 'ai-discovery-files' ),
) );
}
/**
* AJAX: Rebuild the crawler summary table from the raw log.
*
* Triggers an immediate summary rebuild so the dashboard reflects the
* latest data without waiting for the daily cron job.
*
* @since 1.3.2
*/
public static function ajax_crawler_rebuild_summary() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
AIDF_Crawler_Analytics::rebuild_summary( true );
wp_send_json_success( array(
'message' => __( 'Summary rebuilt successfully.', 'ai-discovery-files' ),
) );
}
/**
* AJAX: Crawler conflicts data.
*
* @since 1.3.0
*/
public static function ajax_crawler_conflicts() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$conflicts = AIDF_Crawler_Conflicts::get_conflicts();
// Sanitize messages for safe DOM insertion in JavaScript.
foreach ( $conflicts as &$alert ) {
$alert['message'] = wp_kses_post( $alert['message'] );
}
unset( $alert );
wp_send_json_success( array(
'conflicts' => $conflicts,
) );
}
/**
* AJAX: Crawler file access data.
*
* @since 1.3.0
*/
public static function ajax_crawler_file_access() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$days = isset( $_GET['days'] ) ? absint( $_GET['days'] ) : 30;
if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) {
$days = 30;
}
wp_send_json_success( array(
'file_access' => AIDF_Crawler_Analytics::get_file_access( $days ),
'days' => $days,
) );
}
/**
* AJAX: Save crawler settings.
*
* @since 1.3.0
*/
public static function ajax_crawler_save_settings() {
check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
}
$settings = AIDF_Plugin::get_settings();
$was_enabled = ! empty( $settings['crawler_logging_enabled'] );
// Logging toggle.
$settings['crawler_logging_enabled'] = ! empty( $_POST['crawler_logging_enabled'] );
// Retention period.
$valid_retention = array( '30', '60', '90', '180', '365' );
$retention = isset( $_POST['crawler_log_retention'] )
? sanitize_text_field( wp_unslash( $_POST['crawler_log_retention'] ) )
: '90';
$settings['crawler_log_retention'] = in_array( $retention, $valid_retention, true ) ? $retention : '90';
// Enabled bots.
$enabled_bots = array();
$raw_bots = isset( $_POST['crawler_enabled_bots'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['crawler_enabled_bots'] ) ) : array();
if ( ! empty( $raw_bots ) ) {
$all_bot_keys = array_keys( AIDF_Crawler_Registry::get_bots() );
foreach ( $raw_bots as $key ) {
if ( in_array( $key, $all_bot_keys, true ) ) {
$enabled_bots[] = $key;
}
}
}
$settings['crawler_enabled_bots'] = $enabled_bots;
update_option( 'aidf_settings', $settings );
// Manage cron based on toggle change.
$is_enabled = ! empty( $settings['crawler_logging_enabled'] );
if ( $is_enabled && ! $was_enabled ) {
AIDF_Crawler_Logger::schedule_cron();
} elseif ( ! $is_enabled && $was_enabled ) {
AIDF_Crawler_Logger::clear_cron();
}
wp_send_json_success( array(
'message' => __( 'Settings saved.', 'ai-discovery-files' ),
) );
}
}