[ __CLASS__, 'sanitize_settings' ], ] ); } public static function sanitize_settings( array $input ): array { $clean = []; // v1.5.2: strict comparison to defend against PHP 7 loose-comparison quirks (0 == 'free' bug) $clean['mode'] = in_array( $input['mode'] ?? '', [ 'free', 'pro' ], true ) ? $input['mode'] : 'free'; $clean['pro_key'] = sanitize_text_field( $input['pro_key'] ?? '' ); $clean['auto_update'] = ! empty( $input['auto_update'] ); $clean['include_products'] = ! empty( $input['include_products'] ); $clean['include_posts'] = ! empty( $input['include_posts'] ); $clean['include_pages'] = ! empty( $input['include_pages'] ); $clean['max_products'] = min( 500, max( 5, (int) ( $input['max_products'] ?? 50 ) ) ); $clean['max_posts'] = min( 100, max( 5, (int) ( $input['max_posts'] ?? 20 ) ) ); $clean['custom_sections'] = sanitize_textarea_field( $input['custom_sections'] ?? '' ); $clean['webhook_secret'] = sanitize_text_field( $input['webhook_secret'] ?? '' ); $clean['exclude_urls'] = sanitize_textarea_field( $input['exclude_urls'] ?? '' ); $clean['respect_robots_txt'] = ! empty( $input['respect_robots_txt'] ); // v1.5.2 CRITICAL BUGFIX: WP 4.7+ calls this sanitize_callback also when update_option() // is invoked directly from ajax_save_key. The previous code read pro_* values from // get_option() (= the OLD DB state BEFORE this update), which wiped the freshly-set // tier metadata and left pro_verified_at empty \u2014 the plugin then sent a stale/empty // pro_key on Generate and the API returned "Pro or Agency licence required". // Fix: accept pro_* from $input first (= the values ajax_save_key just wrote), // fall back to $existing only when $input is missing them (= normal Settings-form save). $existing = get_option( 'llmstxt_settings', [] ); $clean['pro_plan'] = sanitize_text_field( $input['pro_plan'] ?? $existing['pro_plan'] ?? '' ); $clean['pro_tier'] = in_array( $input['pro_tier'] ?? '', [ 'solo', 'pro', 'business', 'agency' ], true ) ? $input['pro_tier'] : ( $existing['pro_tier'] ?? '' ); $clean['pro_max_urls'] = (int) ( $input['pro_max_urls'] ?? $existing['pro_max_urls'] ?? 0 ); $clean['pro_verified_at'] = sanitize_text_field( $input['pro_verified_at'] ?? $existing['pro_verified_at'] ?? '' ); $clean['pro_email_mask'] = sanitize_text_field( $input['pro_email_mask'] ?? $existing['pro_email_mask'] ?? '' ); return $clean; } public static function enqueue( string $hook ): void { if ( ! str_contains( $hook, 'aiready-llms-txt-generator' ) ) return; wp_enqueue_style( 'llmstxt-admin', LLMSTXT_URL . 'admin/admin.css', [], LLMSTXT_VERSION ); wp_enqueue_style( 'llmstxt-vscore', LLMSTXT_URL . 'admin/visibility-score.css', [ 'llmstxt-admin' ], LLMSTXT_VERSION ); wp_enqueue_script( 'llmstxt-admin', LLMSTXT_URL . 'admin/admin.js', [ 'jquery' ], LLMSTXT_VERSION, true ); wp_enqueue_script( 'llmstxt-vscore', LLMSTXT_URL . 'admin/visibility-score.js', [ 'jquery', 'llmstxt-admin' ], LLMSTXT_VERSION, true ); // Score detail page assets (NEW in 1.5.0) — load only on the detail page if ( str_contains( $hook, 'aiready-llms-txt-generator-score' ) ) { wp_enqueue_style( 'llmstxt-score-detail', LLMSTXT_URL . 'admin/score-detail.css', [ 'llmstxt-admin', 'llmstxt-vscore' ], LLMSTXT_VERSION ); wp_enqueue_script( 'llmstxt-score-detail', LLMSTXT_URL . 'admin/score-detail.js', [ 'jquery', 'llmstxt-admin' ], LLMSTXT_VERSION, true ); } wp_localize_script( 'llmstxt-admin', 'llmstxtAdmin', [ 'nonce' => wp_create_nonce( 'llmstxt_nonce' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'strings' => [ 'generateBtn' => __( '⚡ Generate Now', 'aiready-llms-txt-generator' ), 'generating' => __( 'Generating…', 'aiready-llms-txt-generator' ), 'previewBtn' => __( '👁 Preview', 'aiready-llms-txt-generator' ), 'previewLoading' => __( 'Loading…', 'aiready-llms-txt-generator' ), 'verifyBtn' => __( 'Verify', 'aiready-llms-txt-generator' ), 'verifying' => __( 'Verifying…', 'aiready-llms-txt-generator' ), 'success' => __( 'Done!', 'aiready-llms-txt-generator' ), 'error' => __( 'Error', 'aiready-llms-txt-generator' ), 'networkError' => __( 'Network error', 'aiready-llms-txt-generator' ), 'isLive' => __( 'llms.txt is live', 'aiready-llms-txt-generator' ), 'lastUpdated' => __( 'Last updated:', 'aiready-llms-txt-generator' ), 'lines' => __( 'lines', 'aiready-llms-txt-generator' ), 'keyValid' => __( '✅ Pro key valid', 'aiready-llms-txt-generator' ), 'keyInvalid' => __( '❌ Invalid key', 'aiready-llms-txt-generator' ), 'enterKey' => __( 'Please enter a key first.', 'aiready-llms-txt-generator' ), 'tierSolo' => __( 'Pro Solo', 'aiready-llms-txt-generator' ), 'tierBusiness' => __( 'Pro Business', 'aiready-llms-txt-generator' ), 'tierAgency' => __( 'Agency', 'aiready-llms-txt-generator' ), 'urlLimit' => __( 'URL limit', 'aiready-llms-txt-generator' ), 'saving' => __( 'Saving…', 'aiready-llms-txt-generator' ), // v1.5.0 detail-page strings 'recrawling' => __( 'Re-crawling…', 'aiready-llms-txt-generator' ), 'recrawlReady' => __( 'Re-Crawl now', 'aiready-llms-txt-generator' ), /* translators: %s: number of minutes until re-crawl is available */ 'cooldownLeft' => __( 'Available in %s min', 'aiready-llms-txt-generator' ), 'copied' => __( 'Copied!', 'aiready-llms-txt-generator' ), 'exporting' => __( 'Preparing export…', 'aiready-llms-txt-generator' ), // v1.6.0 loading-screen step messages (cycled in score-detail.js while AJAX runs) 'loadingSteps' => [ __( 'Checking Schema.org markup on sample products…', 'aiready-llms-txt-generator' ), __( 'Reading llms.txt and llms-full.txt…', 'aiready-llms-txt-generator' ), __( 'Probing AI crawler access (GPTBot, Claude, Perplexity)…','aiready-llms-txt-generator' ), __( 'Auditing content quality (Pro)…', 'aiready-llms-txt-generator' ), __( 'Compiling noindex compliance report…', 'aiready-llms-txt-generator' ), __( 'Almost done…', 'aiready-llms-txt-generator' ), ], ], ] ); } public static function render_page(): void { // v1.5.1: Cleanup stale 'llmstxt_last_error' option from previous versions. // The plugin never sets this option — but old installs may have it lingering. if ( get_option( 'llmstxt_last_error', null ) !== null ) { delete_option( 'llmstxt_last_error' ); } $opts = get_option( 'llmstxt_settings', [] ); $file_info = get_option( 'llmstxt_file_info', [] ); $last_err = ''; // Kept for template compat; never populated since v1.5.1 $mode = $opts['mode'] ?? 'free'; $site_url = home_url( '/llms.txt' ); include LLMSTXT_DIR . 'admin/templates/page-main.php'; } /** * Render the AI Visibility Score detail page (NEW in 1.5.0). * * v1.5.5: When the details cache is empty/stale, render a loading skeleton * instead of blocking the page render for many seconds. The skeleton triggers * an AJAX call (ajax_refresh_score_details), then reloads the page once data * is ready. Users now always see immediate visual feedback. */ public static function render_score_page(): void { $opts = get_option( 'llmstxt_settings', [] ); $mode = $opts['mode'] ?? 'free'; $main_page_url = admin_url( 'admin.php?page=aiready-llms-txt-generator' ); // v1.5.5: Cache-aware rendering. Render details synchronously ONLY when // we have a fresh cached entry; otherwise show the loading skeleton and let // JS fetch the data asynchronously. compute_details() makes up to 10 HTTP // round-trips to the local site (Schema sampling) plus one Pro-API crawl, // which can take 10-60 seconds on the first run. $cached = get_option( 'llmstxt_visibility_score_details' ); $cache_ttl = 15 * MINUTE_IN_SECONDS; $has_fresh_cache = ( is_array( $cached ) && isset( $cached['computed_at'] ) && ( time() - (int) $cached['computed_at'] ) < $cache_ttl ); if ( ! $has_fresh_cache ) { // Cache miss/stale: render loading skeleton. JS will fire the AJAX call. include LLMSTXT_DIR . 'admin/templates/score-detail-loading.php'; return; } // Cache hit: render full page synchronously (fast — pure data formatting). $details = LLMSTXT_Visibility_Score::get_details(); $history = LLMSTXT_Visibility_Score::get_history(); $recrawl = LLMSTXT_Visibility_Score::can_recrawl(); $focus = sanitize_key( $_GET['focus'] ?? '' ); $export_json_url = wp_nonce_url( admin_url( 'admin.php?page=aiready-llms-txt-generator-score&action=export&format=json' ), 'llmstxt_score_export' ); $export_csv_url = wp_nonce_url( admin_url( 'admin.php?page=aiready-llms-txt-generator-score&action=export&format=csv' ), 'llmstxt_score_export' ); include LLMSTXT_DIR . 'admin/templates/score-detail-page.php'; } public static function ajax_generate(): void { check_ajax_referer( 'llmstxt_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Forbidden', 403 ); $generator = new LLMSTXT_Generator(); $ok = $generator->generate_and_save(); $err = get_option( 'llmstxt_last_error', '' ); if ( $ok ) { $info = get_option( 'llmstxt_file_info', [] ); wp_send_json_success( [ 'message' => __( 'llms.txt generated successfully.', 'aiready-llms-txt-generator' ), 'updated' => $info['updated'] ?? '', 'lines' => $info['lines'] ?? 0, 'size' => $info['size'] ?? 0, ] ); } else { wp_send_json_error( $err ?: __( 'Generation failed.', 'aiready-llms-txt-generator' ) ); } } public static function ajax_verify_key(): void { check_ajax_referer( 'llmstxt_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Forbidden', 403 ); $key = sanitize_text_field( wp_unslash( $_POST['key'] ?? '' ) ); if ( ! $key ) wp_send_json_error( __( 'No key provided.', 'aiready-llms-txt-generator' ) ); $client = new LLMSTXT_API_Client( $key ); $result = $client->verify_key(); if ( is_wp_error( $result ) ) { wp_send_json_error( $result->get_error_message() ); } if ( empty( $result['valid'] ) ) { wp_send_json_error( $result['error'] ?? __( 'Invalid key.', 'aiready-llms-txt-generator' ) ); } wp_send_json_success( $result ); } public static function ajax_preview(): void { check_ajax_referer( 'llmstxt_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Forbidden', 403 ); $content = ( new LLMSTXT_Generator() )->build_content( 'standard' ); wp_send_json_success( [ 'content' => $content ] ); } public static function ajax_dismiss_notice(): void { check_ajax_referer( 'llmstxt_nonce', 'nonce' ); update_option( 'llmstxt_notice_dismissed', true ); wp_send_json_success(); } /** * v1.7.2: state of the one-time review request. 'later' => shown again after 30 days, * 'done' / 'never' => never shown again. Never nags: only on the plugin's own pages, * only after the first successful generation is at least 7 days old. */ public static function ajax_review_notice(): void { check_ajax_referer( 'llmstxt_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error(); $state = sanitize_key( wp_unslash( $_POST['state'] ?? 'later' ) ); if ( ! in_array( $state, [ 'later', 'done', 'never' ], true ) ) $state = 'later'; update_option( 'llmstxt_review_notice', [ 'state' => $state, 'at' => time() ], false ); wp_send_json_success(); } public static function maybe_show_review_notice(): void { $screen = get_current_screen(); if ( ! $screen || ! str_contains( $screen->id, 'aiready-llms-txt-generator' ) ) return; // only on our own pages if ( ! current_user_can( 'manage_options' ) ) return; $first = (int) get_option( 'llmstxt_first_generated', 0 ); if ( ! $first || ( time() - $first ) < 7 * DAY_IN_SECONDS ) return; // at least 7 days of real use $st = get_option( 'llmstxt_review_notice', [] ); if ( is_array( $st ) ) { if ( in_array( $st['state'] ?? '', [ 'done', 'never' ], true ) ) return; if ( ( $st['state'] ?? '' ) === 'later' && ( time() - (int) ( $st['at'] ?? 0 ) ) < 30 * DAY_IN_SECONDS ) return; } $review_url = 'https://wordpress.org/support/plugin/aiready-llms-txt-generator/reviews/#new-post'; echo '
' . esc_html__( 'Is the LLMs.txt Generator working well for you?', 'aiready-llms-txt-generator' ) . ' '; echo esc_html__( 'An honest review on WordPress.org helps other site owners decide – and helps us improve. Criticism is welcome too.', 'aiready-llms-txt-generator' ) . '
'; echo ''; echo '' . esc_html__( '★ Rate the plugin', 'aiready-llms-txt-generator' ) . ' '; echo ' '; echo ''; echo '
'; printf( // translators: %s: link to generate page esc_html__( '🤖 LLMs.txt Generator: Your llms.txt hasn\'t been generated yet. %s', 'aiready-llms-txt-generator' ), '' . esc_html__( 'Generate now →', 'aiready-llms-txt-generator' ) . '' ); echo '