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' ), ) ); } }