AgentReadyScan) * * Stores everything in a single WP option: arscan_settings (associative array). */ defined( 'ABSPATH' ) || exit; class ARSCAN_Settings { private static ?ARSCAN_Settings $instance = null; public static function get_instance(): ARSCAN_Settings { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_post_arscan_save_settings', array( $this, 'handle_save' ) ); add_action( 'admin_post_arscan_verify_license', array( $this, 'handle_verify_license' ) ); add_action( 'admin_post_arscan_regenerate_files', array( $this, 'handle_regenerate_files' ) ); add_action( 'admin_post_arscan_sync_license', array( $this, 'handle_sync_license' ) ); add_action( 'admin_post_arscan_load_site_data', array( $this, 'handle_load_site_data' ) ); add_action( 'admin_post_arscan_refresh_golden_questions', array( $this, 'handle_refresh_golden_questions' ) ); add_action( 'admin_post_arscan_prepare_pro_setup', array( $this, 'handle_prepare_pro_setup' ) ); add_action( 'admin_post_arscan_manage_billing', array( $this, 'handle_manage_billing' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) ); add_action( 'wp_ajax_arscan_dismiss_upsell', array( $this, 'ajax_dismiss_upsell' ) ); add_filter( 'plugin_action_links_' . plugin_basename( ARSCAN_PLUGIN_FILE ), array( $this, 'plugin_action_links' ) ); add_action( 'admin_footer', array( $this, 'deactivation_survey_modal' ) ); add_action( 'wp_ajax_arscan_submit_deactivation_survey', array( $this, 'ajax_submit_deactivation_survey' ) ); } public function ajax_dismiss_upsell(): void { check_ajax_referer( 'arscan_dismiss_upsell' ); update_user_meta( get_current_user_id(), 'arscan_upsell_dismissed', '1' ); wp_die(); } // ----------------------------------------------------------------------- // Menu // ----------------------------------------------------------------------- public function add_menu(): void { add_options_page( __( 'AgentReadyScan', 'agentreadyscan' ), __( 'AgentReadyScan', 'agentreadyscan' ), 'manage_options', 'agentreadyscan', array( $this, 'render_page' ) ); } // ----------------------------------------------------------------------- // Settings registration (for nonce + sanitisation via Settings API) // ----------------------------------------------------------------------- public function register_settings(): void { register_setting( 'arscan_settings_group', ARSCAN_OPTION_KEY, array( 'type' => 'array', 'default' => arscan_default_settings(), 'sanitize_callback' => array( $this, 'sanitize' ), ) ); } public function sanitize( $input ): array { $clean = arscan_default_settings(); wp_cache_delete( ARSCAN_OPTION_KEY, 'options' ); $current = get_option( ARSCAN_OPTION_KEY, arscan_default_settings() ); $readonly_text_fields = array( 'license_status', 'license_message', 'license_checked_at', 'license_renewal_date', 'license_error_code', 'license_last_sync_at', 'license_last_sync_message', 'pro_health_status', 'pro_health_score', 'pro_health_checked_at', 'pro_health_last_error', 'last_written', 'last_write_hash', ); foreach ( $readonly_text_fields as $readonly_field ) { if ( isset( $current[ $readonly_field ] ) ) { $clean[ $readonly_field ] = sanitize_text_field( (string) $current[ $readonly_field ] ); } } // license_features is an array  sanitize each element if ( isset( $current['license_features'] ) && is_array( $current['license_features'] ) ) { $clean['license_features'] = array_map( 'sanitize_text_field', $current['license_features'] ); } if ( isset( $current['pro_health_top_issues'] ) && is_array( $current['pro_health_top_issues'] ) ) { $clean['pro_health_top_issues'] = array_map( 'sanitize_text_field', $current['pro_health_top_issues'] ); } if ( isset( $current['pro_health_recommendations'] ) && is_array( $current['pro_health_recommendations'] ) ) { $clean['pro_health_recommendations'] = array_map( 'sanitize_text_field', $current['pro_health_recommendations'] ); } if ( isset( $current['pro_health_issue_count'] ) ) { $clean['pro_health_issue_count'] = max( 0, absint( $current['pro_health_issue_count'] ) ); } // API key  sanitize and store as-is (validation against server is future work) $clean['api_key'] = isset( $current['api_key'] ) ? sanitize_text_field( $current['api_key'] ) : ''; if ( isset( $input['api_key'] ) ) { $clean['api_key'] = sanitize_text_field( $input['api_key'] ); } $text_fields = array( 'business_name', 'industry', 'description', 'phone', 'address', 'city', 'state', 'zip', 'country', 'services', 'hours', 'schema_type', 'legal_name', 'service_area', 'pricing_summary', 'primary_contact', 'founded_on', ); foreach ( $text_fields as $f ) { if ( isset( $input[ $f ] ) ) { $clean[ $f ] = sanitize_text_field( $input[ $f ] ); } } if ( isset( $input['business_url'] ) ) { $clean['business_url'] = esc_url_raw( trim( (string) $input['business_url'] ) ); } if ( isset( $input['contact_url'] ) ) { $clean['contact_url'] = esc_url_raw( trim( (string) $input['contact_url'] ) ); } if ( isset( $input['email'] ) ) { $clean['email'] = sanitize_email( $input['email'] ); } if ( isset( $input['explicit_exclusions'] ) ) { $lines = preg_split( '/\r\n|\r|\n/', (string) $input['explicit_exclusions'] ); $lines = array_filter( array_map( static function( $line ) { return sanitize_text_field( trim( $line ) ); }, $lines ) ); $clean['explicit_exclusions'] = implode( "\n", $lines ); } $bool_fields = array( 'inject_jsonld', 'inject_robots', 'write_llms_txt', 'write_agent_facts' ); foreach ( $bool_fields as $f ) { $clean[ $f ] = ! empty( $input[ $f ] ); } // Q&A pairs: array of {q, a} submitted from repeating rows $clean['qa_pairs'] = array(); if ( ! empty( $input['qa_pairs'] ) && is_array( $input['qa_pairs'] ) ) { foreach ( $input['qa_pairs'] as $pair ) { $q = isset( $pair['q'] ) ? sanitize_text_field( $pair['q'] ) : ''; $a = isset( $pair['a'] ) ? sanitize_textarea_field( $pair['a'] ) : ''; if ( $q !== '' ) { $clean['qa_pairs'][] = array( 'q' => $q, 'a' => $a ); } } } // If no Q&A pairs were submitted yet, seed from industry template if ( empty( $clean['qa_pairs'] ) ) { $clean['qa_pairs'] = ARSCAN_Files::seed_qa_pairs( $clean ); } return $clean; } private function persist_settings( array $settings ): void { wp_cache_delete( ARSCAN_OPTION_KEY, 'options' ); wp_cache_delete( 'alloptions', 'options' ); $updated = update_option( ARSCAN_OPTION_KEY, $settings, true ); if ( false === $updated ) { delete_option( ARSCAN_OPTION_KEY ); add_option( ARSCAN_OPTION_KEY, $settings, '', 'yes' ); } wp_cache_delete( ARSCAN_OPTION_KEY, 'options' ); wp_cache_delete( 'alloptions', 'options' ); } private function debug_log( string $message, array $context = array() ): void { if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; } $payload = array( 'time' => current_time( 'mysql' ), 'message' => $message, 'context' => $context, ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'ARS: ' . wp_json_encode( $payload ) ); } private function extract_page_links( string $html ): array { if ( empty( $html ) ) { return array(); } preg_match_all( '/href=["\']([^"\']+)["\']/i', $html, $matches ); if ( empty( $matches[1] ) ) { return array(); } $links = array(); foreach ( $matches[1] as $href ) { if ( empty( $href ) || '#' === $href[0] || 0 === strpos( $href, 'mailto:' ) || 0 === strpos( $href, 'tel:' ) ) { continue; } $absolute = wp_http_validate_url( $href ) ? $href : home_url( ltrim( $href, '/' ) ); if ( $absolute ) { $links[] = $absolute; } } return array_values( array_unique( $links ) ); } private function collect_site_pages(): array { $pages = array(); $queue = array( home_url( '/' ), home_url( '/about/' ), home_url( '/about' ), home_url( '/contact/' ), home_url( '/contact' ), home_url( '/services/' ), home_url( '/services' ), home_url( '/faq/' ), home_url( '/faq' ), ); $allowed_host = wp_parse_url( home_url(), PHP_URL_HOST ); foreach ( $queue as $url ) { $response = wp_remote_get( $url, array( 'timeout' => 12, 'redirection' => 3, ) ); if ( is_wp_error( $response ) ) { continue; } if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { continue; } $html = wp_remote_retrieve_body( $response ); if ( empty( $html ) ) { continue; } $pages[ $url ] = $html; foreach ( $this->extract_page_links( $html ) as $link ) { $host = wp_parse_url( $link, PHP_URL_HOST ); if ( $host && $allowed_host === $host ) { $path = wp_parse_url( $link, PHP_URL_PATH ) ?: ''; if ( preg_match( '#/(about|contact|services|faq|team|locations?)/?#i', $path ) ) { $queue[] = $link; } } } } return $pages; } private function extract_between_tags( string $html, string $tag ): string { if ( preg_match( '/<' . preg_quote( $tag, '/' ) . '[^>]*>(.*?)<\/' . preg_quote( $tag, '/' ) . '>/is', $html, $matches ) ) { return trim( wp_strip_all_tags( html_entity_decode( $matches[1], ENT_QUOTES | ENT_HTML5 ) ) ); } return ''; } private function extract_meta_description( string $html ): string { // name="description" (attribute order variant 1) if ( preg_match( '/]*name=["\']description["\']\s[^>]*content=["\']([^"\']{10,})["\'][^>]*>/i', $html, $m ) ) { return trim( wp_strip_all_tags( html_entity_decode( $m[1], ENT_QUOTES | ENT_HTML5 ) ) ); } // name="description" (attribute order variant 2  content before name) if ( preg_match( '/]*content=["\']([^"\']{10,})["\']\s[^>]*name=["\']description["\']/i', $html, $m ) ) { return trim( wp_strip_all_tags( html_entity_decode( $m[1], ENT_QUOTES | ENT_HTML5 ) ) ); } // og:description (variant 1) if ( preg_match( '/]*property=["\']og:description["\']\s[^>]*content=["\']([^"\']{10,})["\'][^>]*>/i', $html, $m ) ) { return trim( wp_strip_all_tags( html_entity_decode( $m[1], ENT_QUOTES | ENT_HTML5 ) ) ); } // og:description (variant 2  content before property) if ( preg_match( '/]*content=["\']([^"\']{10,})["\']\s[^>]*property=["\']og:description["\']/i', $html, $m ) ) { return trim( wp_strip_all_tags( html_entity_decode( $m[1], ENT_QUOTES | ENT_HTML5 ) ) ); } return ''; } private function extract_first_paragraph( string $html ): string { // Walk
tags and return the first one that looks like real prose (50+ chars) if ( preg_match_all( '/
]*>(.*?)<\/p>/is', $html, $matches ) ) { foreach ( $matches[1] as $raw ) { $text = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( html_entity_decode( $raw, ENT_QUOTES | ENT_HTML5 ) ) ) ); if ( strlen( $text ) >= 50 ) { return $text; } } } return ''; } private function is_valid_service_candidate( string $candidate ): bool { $candidate = trim( preg_replace( '/\s+/', ' ', $candidate ) ); if ( '' === $candidate ) { return false; } // Service names should be concise  reject very short or long strings. if ( strlen( $candidate ) < 4 || strlen( $candidate ) > 60 ) { return false; } // Reject strings that look like phone numbers. if ( preg_match( '/\(?\d{3}\)?[\s\-\.]\d{3}[\s\-\.]\d{4}/', $candidate ) ) { return false; } if ( preg_match( '/^\+?[\d\s\-\.\(\)]{8,}$/', $candidate ) ) { return false; } // Reject strings that look like times or hours. if ( preg_match( '/\d{1,2}:\d{2}\s*(?:am|pm)/i', $candidate ) ) { return false; } // Reject multi-sentence text  service names don't have periods. if ( substr_count( $candidate, '.' ) > 0 && strlen( $candidate ) > 30 ) { return false; } $blocked_patterns = array( '/^(home|about|contact|services|faq|menu|reviews?)$/i', '/^recent posts?$/i', '/^recent comments?$/i', '/^hello world!?$/i', '/^archives?$/i', '/^categories$/i', '/^search$/i', '/^uncategorized$/i', '/^powered by wordpress$/i', '/^proudly powered by wordpress$/i', '/^skip to content$/i', '/^new patient$/i', '/^get started$/i', '/^learn more$/i', '/^read more$/i', '/^click here$/i', '/^view all$/i', '/^see all$/i', '/^submit$/i', '/^sign up$/i', '/^log ?in$/i', '/^call us/i', '/^contact us/i', '/comments? (are )?closed/i', '/skip to/i', '/welcome to/i', ); foreach ( $blocked_patterns as $pattern ) { if ( preg_match( $pattern, $candidate ) ) { return false; } } return true; } private function infer_industry_from_text( string $text ): string { $text = strtolower( $text ); // Weighted terms improve resilience compared to plain keyword-hit counting. $map = array( 'dental' => array( 'dentist' => 1.2, 'dental' => 1.2, 'orthodont' => 1.0, 'hygienist' => 1.0, 'tooth' => 0.8, 'teeth' => 0.8 ), 'healthcare' => array( 'clinic' => 1.0, 'medical' => 1.0, 'physician' => 1.0, 'patient care' => 1.0, 'healthcare' => 1.0, 'urgent care' => 1.2 ), 'personal_injury' => array( 'personal injury' => 1.4, 'injury attorney' => 1.2, 'accident lawyer' => 1.2, 'car accident' => 1.0 ), 'family_law' => array( 'family law' => 1.4, 'divorce' => 1.2, 'custody' => 1.2, 'child support' => 1.0 ), 'criminal_law' => array( 'criminal defense' => 1.4, 'dui' => 1.2, 'felony' => 1.0, 'misdemeanor' => 1.0 ), 'general_law' => array( 'law firm' => 1.2, 'attorney' => 1.0, 'lawyer' => 1.0, 'legal services' => 1.0 ), 'restaurant' => array( 'restaurant' => 1.2, 'reservation' => 1.0, 'menu' => 0.6, 'dine in' => 1.0, 'takeout' => 0.8, 'catering' => 0.8 ), 'real_estate' => array( 'real estate' => 1.4, 'realtor' => 1.2, 'mls' => 1.0, 'homes for sale' => 1.0, 'open house' => 1.0 ), 'home_services' => array( 'roofing' => 1.2, 'plumbing' => 1.2, 'hvac' => 1.2, 'electrician' => 1.0, 'contractor' => 0.8 ), 'financial' => array( 'financial advisor' => 1.4, 'wealth management' => 1.2, 'retirement planning' => 1.0, 'tax preparation' => 1.0, 'bookkeeping' => 0.8, 'mortgage' => 0.8 ), 'saas' => array( 'saas' => 1.2, 'software' => 1.0, 'api' => 0.8, 'dashboard' => 0.8, 'platform' => 0.8 ), 'ecommerce' => array( 'add to cart' => 1.4, 'checkout' => 1.0, 'product catalog' => 1.0, 'shop now' => 1.0, 'free shipping' => 0.8 ), 'education' => array( 'course' => 1.0, 'academy' => 1.0, 'curriculum' => 1.0, 'enroll' => 1.0, 'student' => 0.8 ), 'professional' => array( 'consulting' => 1.0, 'professional services' => 1.0, 'advisory' => 1.0, 'strategy' => 0.8 ), ); $scores = array(); foreach ( $map as $industry => $terms ) { $score = 0.0; $max = 0.0; foreach ( $terms as $term => $weight ) { $max += $weight; $pattern = '/\b' . preg_quote( $term, '/' ) . '\b/i'; $hits = preg_match_all( $pattern, $text, $matches ); if ( $hits ) { $score += min( $hits, 3 ) * $weight; } } if ( $score > 0.0 ) { $scores[ $industry ] = array( 'score' => $score, 'confidence' => $max > 0 ? ( $score / $max ) : 0, ); } } if ( empty( $scores ) ) { return 'general'; } uasort( $scores, static function( array $a, array $b ): int { if ( $a['score'] === $b['score'] ) { return $b['confidence'] <=> $a['confidence']; } return $b['score'] <=> $a['score']; } ); $industries = array_keys( $scores ); $best_key = $industries[0]; $best = $scores[ $best_key ]; $runner_up = isset( $industries[1] ) ? $scores[ $industries[1] ] : array( 'score' => 0.0, 'confidence' => 0.0 ); // Guardrails to reduce false positives on generic websites. if ( $best['score'] < 1.5 || $best['confidence'] < 0.14 ) { return 'general'; } if ( abs( (float) $best['score'] - (float) $runner_up['score'] ) < 0.25 ) { return 'general'; } return $best_key; } private function extract_site_data(): array { $pages = $this->collect_site_pages(); $combined_text = ''; $emails = array(); $phones = array(); foreach ( $pages as $html ) { $combined_text .= "\n" . preg_replace( '/\s+/', ' ', wp_strip_all_tags( $html ) ); preg_match_all( '/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}/i', $html, $email_matches ); preg_match_all( '/(?:\+?1[\s\-.]?)?(?:\(?\d{3}\)?[\s\-.]?)\d{3}[\s\-.]?\d{4}/', $html, $phone_matches ); if ( ! empty( $email_matches[0] ) ) { $emails = array_merge( $emails, $email_matches[0] ); } if ( ! empty( $phone_matches[0] ) ) { $phones = array_merge( $phones, $phone_matches[0] ); } } $homepage = $pages[ home_url( '/' ) ] ?? reset( $pages ); $title = is_string( $homepage ) ? $this->extract_between_tags( $homepage, 'title' ) : ''; // Description: meta tag first (most reliable), then WP tagline, then first real paragraph. $description = is_string( $homepage ) ? $this->extract_meta_description( $homepage ) : ''; if ( '' === $description ) { $description = trim( (string) get_bloginfo( 'description' ) ); } if ( '' === $description && is_string( $homepage ) ) { $description = $this->extract_first_paragraph( $homepage ); } $service_candidates = array(); if ( preg_match_all( '/<(?:li|h2|h3)[^>]*>(.*?)<\/(?:li|h2|h3)>/is', (string) $homepage, $service_matches ) ) { foreach ( $service_matches[1] as $candidate ) { $candidate = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( html_entity_decode( $candidate, ENT_QUOTES | ENT_HTML5 ) ) ) ); if ( $this->is_valid_service_candidate( $candidate ) ) { $service_candidates[] = $candidate; } } } $service_candidates = array_values( array_unique( $service_candidates ) ); $service_candidates = array_slice( $service_candidates, 0, 8 ); $industry = $this->infer_industry_from_text( $combined_text ); return array( 'business_name' => get_bloginfo( 'name' ), 'business_url' => home_url(), 'description' => $description, 'email' => ! empty( $emails ) ? sanitize_email( $emails[0] ) : '', 'phone' => ! empty( $phones ) ? sanitize_text_field( $phones[0] ) : '', 'services' => ! empty( $service_candidates ) ? implode( ', ', $service_candidates ) : '', 'industry' => $industry, 'title_hint' => $title, ); } private function get_cached_site_data(): array { $cache_key = 'arscan_detected_site_data'; $cached = get_transient( $cache_key ); if ( is_array( $cached ) ) { return $cached; } $detected = $this->extract_site_data(); set_transient( $cache_key, $detected, 30 * MINUTE_IN_SECONDS ); return $detected; } private function combined_consistency_issues( array $settings, ?array $detected = null ): array { $detected = is_array( $detected ) ? $detected : $this->get_cached_site_data(); $issues = array_merge( ARSCAN_Files::consistency_issues( $settings ), ARSCAN_Files::website_conflict_issues( $settings, $detected ) ); return array_values( array_unique( array_filter( array_map( 'trim', $issues ) ) ) ); } private function maybe_send_consistency_alert( array $settings, ?array $detected = null ): void { $issues = $this->combined_consistency_issues( $settings, $detected ); if ( empty( $issues ) ) { return; } $hash = md5( (string) wp_json_encode( $issues ) ); $cache_key = 'arscan_consistency_alert_' . md5( home_url() ); $last = get_transient( $cache_key ); if ( is_array( $last ) && ( $last['hash'] ?? '' ) === $hash ) { return; } set_transient( $cache_key, array( 'hash' => $hash, 'at' => time(), ), DAY_IN_SECONDS ); $to = array(); $admin_email = sanitize_email( (string) get_option( 'admin_email', '' ) ); if ( is_email( $admin_email ) ) { $to[] = $admin_email; } $contact_email = sanitize_email( (string) ( $settings['email'] ?? '' ) ); if ( is_email( $contact_email ) ) { $to[] = $contact_email; } $to = array_values( array_unique( $to ) ); if ( empty( $to ) ) { return; } $host = wp_parse_url( home_url(), PHP_URL_HOST ); $subject = sprintf( /* translators: %s: website host */ __( '[%s] AgentReadyScan detected data conflicts', 'agentreadyscan' ), (string) $host ); $lines = array( __( 'AgentReadyScan found consistency conflicts between your saved plugin settings and website-detected content.', 'agentreadyscan' ), '', __( 'Conflicts found:', 'agentreadyscan' ), ); foreach ( $issues as $issue ) { $lines[] = '- ' . $issue; } $lines[] = ''; $lines[] = __( 'Open Settings > AgentReadyScan in wp-admin, review the conflicts, and republish your files.', 'agentreadyscan' ); $lines[] = admin_url( 'options-general.php?page=agentreadyscan' ); wp_mail( $to, $subject, implode( "\n", $lines ) ); } private function build_pro_snapshot_payload( array $settings, ?array $detected = null ): array { $detected = is_array( $detected ) ? $detected : $this->get_cached_site_data(); $qa_pairs = is_array( $settings['qa_pairs'] ?? null ) ? $settings['qa_pairs'] : array(); $issues = $this->combined_consistency_issues( $settings, $detected ); $qa_questions = array(); foreach ( $qa_pairs as $pair ) { if ( ! is_array( $pair ) ) { continue; } $question = sanitize_text_field( (string) ( $pair['q'] ?? '' ) ); if ( '' !== $question ) { $qa_questions[] = $question; } } $qa_questions = array_values( array_unique( $qa_questions ) ); $agent_facts_preview = ARSCAN_Files::build_agent_facts( $settings ); $llms_preview = ARSCAN_Files::build_llms_txt( $settings ); return array( 'captured_at' => gmdate( 'c' ), 'settings' => array( 'business_name' => sanitize_text_field( (string) ( $settings['business_name'] ?? '' ) ), 'description' => sanitize_textarea_field( (string) ( $settings['description'] ?? '' ) ), 'phone' => sanitize_text_field( (string) ( $settings['phone'] ?? '' ) ), 'email' => sanitize_email( (string) ( $settings['email'] ?? '' ) ), 'services' => sanitize_text_field( (string) ( $settings['services'] ?? '' ) ), 'industry' => sanitize_key( (string) ( $settings['industry'] ?? 'general' ) ), 'schema_type' => sanitize_text_field( (string) ( $settings['schema_type'] ?? '' ) ), 'qa_count' => count( $qa_pairs ), 'qa_questions' => $qa_questions, 'explicit_exclusions_count' => count( array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', (string) ( $settings['explicit_exclusions'] ?? '' ) ) ?: array() ) ) ), ), 'files' => array( 'llms_exists' => file_exists( ABSPATH . 'llms.txt' ), 'agent_facts_exists' => file_exists( ABSPATH . 'agent-facts.html' ), 'agent_facts_preview' => $agent_facts_preview, 'llms_preview' => $llms_preview, ), 'detected' => array( 'industry' => sanitize_key( (string) ( $detected['industry'] ?? 'general' ) ), 'description' => sanitize_textarea_field( (string) ( $detected['description'] ?? '' ) ), 'services' => sanitize_text_field( (string) ( $detected['services'] ?? '' ) ), 'phone' => sanitize_text_field( (string) ( $detected['phone'] ?? '' ) ), 'email' => sanitize_email( (string) ( $detected['email'] ?? '' ) ), ), 'consistency_issues' => array_values( array_map( 'sanitize_text_field', $issues ) ), ); } private function apply_pro_health_audit( array $settings, array $audit ): array { $settings['pro_health_status'] = sanitize_key( (string) ( $audit['status'] ?? '' ) ); $settings['pro_health_score'] = isset( $audit['score'] ) ? (string) round( (float) $audit['score'], 1 ) : ''; $settings['pro_health_checked_at'] = sanitize_text_field( (string) ( $audit['created_at'] ?? current_time( 'mysql' ) ) ); $findings = is_array( $audit['findings'] ?? null ) ? $audit['findings'] : array(); $recommendations = is_array( $audit['recommendations'] ?? null ) ? $audit['recommendations'] : array(); $top_issues = array(); foreach ( $findings as $finding ) { if ( ! is_array( $finding ) ) { continue; } $message = sanitize_text_field( (string) ( $finding['message'] ?? '' ) ); if ( '' !== $message ) { $top_issues[] = $message; } if ( count( $top_issues ) >= 5 ) { break; } } $settings['pro_health_top_issues'] = $top_issues; $settings['pro_health_issue_count'] = count( $top_issues ); $settings['pro_health_recommendations'] = array_values( array_slice( array_filter( array_map( static function( $item ) { return sanitize_text_field( (string) $item ); }, $recommendations ) ), 0, 5 ) ); $settings['pro_health_last_error'] = ''; return $settings; } private function refresh_pro_health( array $settings, ?array $detected = null ): array { if ( empty( $settings['api_key'] ) || 'active' !== ( $settings['license_status'] ?? '' ) ) { return $settings; } $snapshot = $this->build_pro_snapshot_payload( $settings, $detected ); $sync = ARSCAN_License::sync_pro_snapshot( (string) $settings['api_key'], $snapshot, (string) ARSCAN_Files::settings_hash( $settings ) ); if ( is_wp_error( $sync ) ) { $settings['pro_health_last_error'] = sanitize_text_field( $sync->get_error_message() ); return $settings; } if ( ! empty( $sync['audit'] ) && is_array( $sync['audit'] ) ) { return $this->apply_pro_health_audit( $settings, $sync['audit'] ); } $latest = ARSCAN_License::fetch_latest_pro_audit( (string) $settings['api_key'] ); if ( is_wp_error( $latest ) ) { $settings['pro_health_last_error'] = sanitize_text_field( $latest->get_error_message() ); return $settings; } if ( ! empty( $latest['audit'] ) && is_array( $latest['audit'] ) ) { return $this->apply_pro_health_audit( $settings, $latest['audit'] ); } return $settings; } private function maybe_populate_pro_exclusions( array $settings ): array { if ( 'active' !== ( $settings['license_status'] ?? '' ) ) { return $settings; } $existing = trim( (string) ( $settings['explicit_exclusions'] ?? '' ) ); if ( '' !== $existing ) { return $settings; } $seed = ARSCAN_Files::seed_exclusions( $settings ); if ( empty( $seed ) ) { return $settings; } $settings['explicit_exclusions'] = implode( "\n", array_values( array_filter( array_map( static function( $line ) { return sanitize_text_field( (string) $line ); }, $seed ) ) ) ); return $settings; } // ----------------------------------------------------------------------- // Handle save + regenerate files // ----------------------------------------------------------------------- public function handle_save(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_save_settings_action', 'arscan_nonce' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- entire array sanitized field-by-field inside $this->sanitize() $input = isset( $_POST['arscan_settings'] ) ? wp_unslash( $_POST['arscan_settings'] ) : array(); $settings = $this->sanitize( $input ); $detected = $this->extract_site_data(); set_transient( 'arscan_detected_site_data', $detected, 30 * MINUTE_IN_SECONDS ); if ( ! empty( $settings['api_key'] ) ) { $settings = ARSCAN_License::refresh_license_state( $settings ); if ( 'active' === ( $settings['license_status'] ?? '' ) ) { $seed_source = $settings; $seed_source['industry'] = $settings['industry'] ?? 'general'; $current_pairs = is_array( $settings['qa_pairs'] ?? null ) ? $settings['qa_pairs'] : array(); if ( count( $current_pairs ) < 12 ) { $settings['qa_pairs'] = ARSCAN_Files::seed_pro_qa_pairs( $seed_source ); } $settings = $this->maybe_populate_pro_exclusions( $settings ); } } else { $settings = ARSCAN_License::clear_license_state( $settings, __( 'Pro features are off until you save a valid API key.', 'agentreadyscan' ) ); } $settings['last_written'] = current_time( 'mysql' ); // Regenerate static files $result = ARSCAN_Files::write_all( $settings ); if ( $result['success'] ) { $settings['last_write_hash'] = ARSCAN_Files::settings_hash( $settings ); } $settings = $this->refresh_pro_health( $settings, $detected ); $this->persist_settings( $settings ); $this->maybe_send_consistency_alert( $settings, $detected ); $status = $result['success'] ? 'saved' : 'error'; $msg = $result['success'] ? urlencode( __( 'Settings saved and files updated.', 'agentreadyscan' ) ) : urlencode( $result['error'] ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => $status, 'arscan_msg' => $msg ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_regenerate_files(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_regenerate_files_action', 'arscan_regenerate_nonce' ); $settings = get_option( ARSCAN_OPTION_KEY, arscan_default_settings() ); $settings = ARSCAN_License::maybe_refresh_license_state( $settings ); $settings['last_written'] = current_time( 'mysql' ); $result = ARSCAN_Files::write_all( $settings ); if ( $result['success'] ) { $settings['last_write_hash'] = ARSCAN_Files::settings_hash( $settings ); } $settings = $this->refresh_pro_health( $settings ); $this->persist_settings( $settings ); $status = $result['success'] ? 'saved' : 'error'; $msg = $result['success'] ? urlencode( __( 'Files regenerated successfully.', 'agentreadyscan' ) ) : urlencode( $result['error'] ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => $status, 'arscan_msg' => $msg ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_verify_license(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_verify_license_action', 'arscan_verify_nonce' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only api_key is used; sanitized with sanitize_text_field() on the next line $submitted = isset( $_POST['arscan_settings'] ) ? wp_unslash( $_POST['arscan_settings'] ) : array(); $settings = wp_parse_args( get_option( ARSCAN_OPTION_KEY, array() ), arscan_default_settings() ); $settings['api_key'] = isset( $submitted['api_key'] ) ? sanitize_text_field( $submitted['api_key'] ) : ''; $this->debug_log( 'verify_start', array( 'api_key_suffix' => substr( $settings['api_key'], -8 ), 'existing_status' => $settings['license_status'] ?? '', ) ); if ( empty( $settings['api_key'] ) ) { $settings = ARSCAN_License::clear_license_state( $settings, __( 'Enter a Pro API key before verifying.', 'agentreadyscan' ) ); $this->persist_settings( $settings ); $this->debug_log( 'verify_missing_key', array( 'saved_status' => $settings['license_status'] ?? '' ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => 'error', 'arscan_msg' => urlencode( __( 'Enter a Pro API key before verifying.', 'agentreadyscan' ) ), ), admin_url( 'options-general.php' ) ) ); exit; } $detected = null; $settings = ARSCAN_License::refresh_license_state( $settings ); if ( 'active' === ( $settings['license_status'] ?? '' ) ) { $detected = $this->extract_site_data(); foreach ( array( 'business_name', 'business_url', 'description', 'email', 'phone', 'services' ) as $field ) { if ( ! empty( $detected[ $field ] ) && empty( $settings[ $field ] ) ) { $settings[ $field ] = $detected[ $field ]; } } if ( ! empty( $detected['industry'] ) && 'general' !== $detected['industry'] && 'general' === ( $settings['industry'] ?? 'general' ) ) { $settings['industry'] = $detected['industry']; if ( empty( $settings['schema_type'] ) || 'Organization' === ( $settings['schema_type'] ?? '' ) ) { $settings['schema_type'] = arscan_default_schema_type_for_industry( $detected['industry'] ); } } $seed_source = $settings; $seed_source['industry'] = $settings['industry'] ?? 'general'; $settings['qa_pairs'] = ARSCAN_Files::seed_pro_qa_pairs( $seed_source ); $settings = $this->maybe_populate_pro_exclusions( $settings ); } $settings = $this->refresh_pro_health( $settings, $detected ); $this->debug_log( 'verify_after_refresh', array( 'license_status' => $settings['license_status'] ?? '', 'license_message' => $settings['license_message'] ?? '', 'license_checked_at' => $settings['license_checked_at'] ?? '', 'renewal_date' => $settings['license_renewal_date'] ?? '', ) ); $this->persist_settings( $settings ); $persisted = get_option( ARSCAN_OPTION_KEY, array() ); $this->debug_log( 'verify_after_persist', array( 'persisted_status' => $persisted['license_status'] ?? '', 'persisted_checked_at' => $persisted['license_checked_at'] ?? '', 'persisted_renewal_date' => $persisted['license_renewal_date'] ?? '', 'persisted_api_key_suffix' => ! empty( $persisted['api_key'] ) ? substr( $persisted['api_key'], -8 ) : '', ) ); $status = ( 'active' === ( $settings['license_status'] ?? '' ) ) ? 'saved' : 'error'; $msg = urlencode( 'active' === ( $settings['license_status'] ?? '' ) ? __( 'Pro license active. AgentReadyScan rebuilt the Golden Questions from the current site and industry.', 'agentreadyscan' ) : ( $settings['license_message'] ?? __( 'License check complete.', 'agentreadyscan' ) ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => $status, 'arscan_msg' => $msg, ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_sync_license(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_sync_license_action', 'arscan_sync_nonce' ); $settings = get_option( ARSCAN_OPTION_KEY, arscan_default_settings() ); if ( empty( $settings['api_key'] ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => 'error', 'arscan_msg' => urlencode( __( 'Enter a Pro API key before syncing.', 'agentreadyscan' ) ), ), admin_url( 'options-general.php' ) ) ); exit; } $settings = ARSCAN_License::refresh_license_state( $settings ); if ( 'active' === ( $settings['license_status'] ?? '' ) ) { $seed_source = $settings; $seed_source['industry'] = arscan_effective_industry(); $settings['qa_pairs'] = ARSCAN_Files::seed_pro_qa_pairs( $seed_source ); $settings = $this->maybe_populate_pro_exclusions( $settings ); } $settings = $this->refresh_pro_health( $settings ); $this->persist_settings( $settings ); $status = ( 'active' === ( $settings['license_status'] ?? '' ) ) ? 'saved' : 'error'; $msg = urlencode( $settings['license_message'] ?? __( 'License sync complete.', 'agentreadyscan' ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => $status, 'arscan_msg' => $msg ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_load_site_data(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_load_site_data_action', 'arscan_load_site_data_nonce' ); $settings = wp_parse_args( get_option( ARSCAN_OPTION_KEY, array() ), arscan_default_settings() ); $settings = ARSCAN_License::maybe_refresh_license_state( $settings ); $detected = $this->extract_site_data(); set_transient( 'arscan_detected_site_data', $detected, 30 * MINUTE_IN_SECONDS ); foreach ( array( 'business_name', 'business_url', 'description', 'email', 'phone', 'services' ) as $field ) { if ( ! empty( $detected[ $field ] ) ) { $settings[ $field ] = $detected[ $field ]; } } if ( ! empty( $detected['industry'] ) && 'general' === ( $settings['industry'] ?? 'general' ) ) { $settings['industry'] = $detected['industry']; if ( empty( $settings['schema_type'] ) || 'Organization' === ( $settings['schema_type'] ?? '' ) ) { $settings['schema_type'] = arscan_default_schema_type_for_industry( $detected['industry'] ); } } if ( 'active' === ( $settings['license_status'] ?? '' ) ) { $seed_source = $settings; $seed_source['industry'] = arscan_effective_industry(); $settings['qa_pairs'] = ARSCAN_Files::seed_pro_qa_pairs( $seed_source ); $settings = $this->maybe_populate_pro_exclusions( $settings ); } else { $settings['qa_pairs'] = ARSCAN_Files::seed_qa_pairs( $settings ); } $settings = $this->refresh_pro_health( $settings, $detected ); $this->persist_settings( $settings ); $this->maybe_send_consistency_alert( $settings, $detected ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => 'saved', 'arscan_msg' => urlencode( __( 'Loaded available business data from the existing site.', 'agentreadyscan' ) ), ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_refresh_golden_questions(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_refresh_golden_questions_action', 'arscan_refresh_golden_questions_nonce' ); $settings = wp_parse_args( get_option( ARSCAN_OPTION_KEY, array() ), arscan_default_settings() ); $seed_source = $settings; $seed_source['industry'] = arscan_effective_industry(); if ( 'active' === ( $settings['license_status'] ?? '' ) ) { $settings['qa_pairs'] = ARSCAN_Files::seed_pro_qa_pairs( $seed_source ); $settings = $this->maybe_populate_pro_exclusions( $settings ); } else { $settings['qa_pairs'] = ARSCAN_Files::seed_qa_pairs( $seed_source ); } $this->persist_settings( $settings ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => 'saved', 'arscan_msg' => urlencode( __( 'Golden Questions rebuilt from the current industry template.', 'agentreadyscan' ) ), ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_prepare_pro_setup(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_prepare_pro_setup_action', 'arscan_prepare_pro_setup_nonce' ); $settings = wp_parse_args( get_option( ARSCAN_OPTION_KEY, array() ), arscan_default_settings() ); $detected = $this->extract_site_data(); set_transient( 'arscan_detected_site_data', $detected, 30 * MINUTE_IN_SECONDS ); foreach ( array( 'business_name', 'business_url', 'description', 'email', 'phone', 'services' ) as $field ) { if ( ! empty( $detected[ $field ] ) ) { $settings[ $field ] = $detected[ $field ]; } } if ( ! empty( $detected['industry'] ) && 'general' === ( $settings['industry'] ?? 'general' ) ) { $settings['industry'] = $detected['industry']; if ( empty( $settings['schema_type'] ) || 'Organization' === ( $settings['schema_type'] ?? '' ) ) { $settings['schema_type'] = arscan_default_schema_type_for_industry( $detected['industry'] ); } } if ( ! empty( $settings['api_key'] ) ) { $settings = ARSCAN_License::refresh_license_state( $settings ); } $seed_source = $settings; $seed_source['industry'] = arscan_effective_industry(); if ( 'active' === ( $settings['license_status'] ?? '' ) ) { $settings['qa_pairs'] = ARSCAN_Files::seed_pro_qa_pairs( $seed_source ); $settings = $this->maybe_populate_pro_exclusions( $settings ); } else { $settings['qa_pairs'] = ARSCAN_Files::seed_qa_pairs( $seed_source ); } $settings['last_written'] = current_time( 'mysql' ); $result = ARSCAN_Files::write_all( $settings ); if ( $result['success'] ) { $settings['last_write_hash'] = ARSCAN_Files::settings_hash( $settings ); } $settings = $this->refresh_pro_health( $settings, $detected ); $this->persist_settings( $settings ); $this->maybe_send_consistency_alert( $settings, $detected ); $status = $result['success'] ? 'saved' : 'error'; $msg = $result['success'] ? urlencode( __( 'Pro setup loaded site data, rebuilt Golden Questions, and republished your AI files.', 'agentreadyscan' ) ) : urlencode( $result['error'] ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => $status, 'arscan_msg' => $msg, ), admin_url( 'options-general.php' ) ) ); exit; } public function handle_manage_billing(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'agentreadyscan' ) ); } check_admin_referer( 'arscan_manage_billing_action', 'arscan_manage_billing_nonce' ); $settings = wp_parse_args( get_option( ARSCAN_OPTION_KEY, array() ), arscan_default_settings() ); if ( empty( $settings['api_key'] ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => 'error', 'arscan_msg' => urlencode( __( 'No Pro License key is saved for this site.', 'agentreadyscan' ) ), ), admin_url( 'options-general.php' ) ) ); exit; } $result = ARSCAN_License::create_billing_portal_session( $settings['api_key'] ); if ( is_wp_error( $result ) || empty( $result['portal_url'] ) ) { $message = is_wp_error( $result ) ? $result->get_error_message() : ( $result['error'] ?? __( 'Could not open the Stripe billing portal.', 'agentreadyscan' ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'agentreadyscan', 'arscan_status' => 'error', 'arscan_msg' => urlencode( $message ), ), admin_url( 'options-general.php' ) ) ); exit; } wp_safe_redirect( esc_url_raw( $result['portal_url'] ) ); exit; } // ----------------------------------------------------------------------- // Admin notices // ----------------------------------------------------------------------- public function admin_notices(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display of result from a prior nonce-verified POST redirect $status = isset( $_GET['arscan_status'] ) ? sanitize_key( wp_unslash( $_GET['arscan_status'] ) ) : ''; if ( empty( $status ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display of result from a prior nonce-verified POST redirect $msg = isset( $_GET['arscan_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['arscan_msg'] ) ) : ''; $class = ( 'saved' === $status ) ? 'notice-success' : 'notice-error'; printf( '
%s
Enter your Pro API key to connect this site to the optional AgentReadyScan service for license-backed sync, hosted rule updates, and ongoing verification. Each Pro License covers one WordPress site. Upgrade on the website for $99/year if you still need a license.