scanner = $scanner; $this->openai_client = $openai_client; add_action('admin_menu', array($this, 'register_menu')); add_action('admin_bar_menu', array($this, 'register_admin_bar'), 90); add_action('admin_enqueue_scripts', array($this, 'enqueue_assets')); add_action('rest_api_init', array($this, 'register_rest_routes')); add_filter('page_row_actions', array($this, 'add_row_action'), 10, 2); add_filter('post_row_actions', array($this, 'add_row_action'), 10, 2); add_action('wp_ajax_aiseo_prepare_scan', array($this, 'ajax_prepare_scan')); add_action('wp_ajax_aiseo_test_curl_source', array($this, 'ajax_test_curl_source')); add_action('wp_ajax_aiseo_scan_post', array($this, 'ajax_scan_post')); add_action('wp_ajax_aiseo_get_report', array($this, 'ajax_get_report')); add_action('wp_ajax_aiseo_autofill_strategy', array($this, 'ajax_autofill_strategy')); add_action('wp_ajax_aiseo_save_geo_mode', array($this, 'ajax_save_geo_mode')); add_action('wp_ajax_aiseo_start_queue', array($this, 'ajax_start_queue')); add_action('wp_ajax_aiseo_queue_status', array($this, 'ajax_queue_status')); add_action('wp_ajax_aiseo_process_queue', array($this, 'ajax_process_queue')); add_action('wp_ajax_aiseo_stop_queue', array($this, 'ajax_stop_queue')); add_action('aivisseogeo_process_scan_queue', array($this, 'cron_process_queue')); add_action('admin_post_aiseo_queue_tick', array($this, 'async_queue_tick')); add_action('admin_post_nopriv_aiseo_queue_tick', array($this, 'async_queue_tick')); add_action('admin_post_aiseo_buy_requests', array($this, 'admin_post_buy_requests')); } public function register_menu() { add_menu_page( __('AiVisibility', 'aivisibility-seo-geo-agent'), __('AiVisibility', 'aivisibility-seo-geo-agent'), 'manage_options', 'aiseo-auditor', array($this, 'render_dashboard'), 'dashicons-admin-generic', 58 ); } public function register_admin_bar($wp_admin_bar) { if (!is_admin_bar_showing() || !current_user_can('manage_options')) { return; } $action_label = __('scan and report', 'aivisibility-seo-geo-agent'); $wp_admin_bar->add_node( array( 'id' => 'aiseo-auditor', 'title' => '' . esc_html(AISEO_AUDITOR_NAME) . '', 'href' => admin_url('admin.php?page=aiseo-auditor'), 'meta' => array( 'title' => sprintf(__('Start %1$s %2$s', 'aivisibility-seo-geo-agent'), AISEO_AUDITOR_NAME, $action_label), ), ) ); } public function add_row_action($actions, $post) { if (!$post instanceof WP_Post || !current_user_can('manage_options')) { return $actions; } if (!$this->scanner->is_scannable_post($post)) { return $actions; } $label = sprintf( __('%1$s %2$s', 'aivisibility-seo-geo-agent'), AISEO_AUDITOR_NAME, $this->scan_action_label() ); if ($this->row_action_scan_is_active()) { $actions['aiseo_scan'] = sprintf( '%2$s', esc_attr__('Another Scan & Report is currently running.', 'aivisibility-seo-geo-agent'), esc_html($label) ); return $actions; } $url = add_query_arg( array( 'page' => 'aiseo-auditor', 'scan_post' => (int) $post->ID, ), admin_url('admin.php') ); $actions['aiseo_scan'] = sprintf( '%2$s', esc_url($url), esc_html($label) ); return $actions; } public function enqueue_assets($hook) { if ('toplevel_page_aiseo-auditor' !== $hook) { return; } wp_enqueue_style( 'aiseo-admin', AISEO_AUDITOR_URL . 'assets/css/admin.css', array(), AISEO_AUDITOR_VERSION ); wp_enqueue_script( 'aiseo-admin', AISEO_AUDITOR_URL . 'assets/js/admin.js', array(), AISEO_AUDITOR_VERSION, true ); $this->sync_license_status_from_api(); wp_localize_script( 'aiseo-admin', 'AISEOAdmin', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'restUrl' => rest_url('aiseo/v1/'), 'restNonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('aiseo_admin_nonce'), 'autorun' => isset($_GET['autorun']) ? absint(wp_unslash($_GET['autorun'])) : 0, 'autoPostId' => isset($_GET['scan_post']) ? absint(wp_unslash($_GET['scan_post'])) : 0, 'pluginName' => AISEO_AUDITOR_NAME, 'queueStatus' => $this->queue_status_data($this->get_queue()), 'strings' => array( 'scanStarted' => __('Scan started.', 'aivisibility-seo-geo-agent'), 'scanDone' => __('Scan completed.', 'aivisibility-seo-geo-agent'), 'scanFailed' => __('Scan failed.', 'aivisibility-seo-geo-agent'), 'loadingReport' => __('Loading report...', 'aivisibility-seo-geo-agent'), 'noReport' => __('No report found yet.', 'aivisibility-seo-geo-agent'), 'scanAllLabel' => __('Scan & Report all', 'aivisibility-seo-geo-agent'), 'scanNotScannedLabel' => __('Scan & Report unscanned', 'aivisibility-seo-geo-agent'), 'queueLabel' => __('Scan & Report queue', 'aivisibility-seo-geo-agent'), 'queueStarting' => __('Starting Scan & Report queue...', 'aivisibility-seo-geo-agent'), 'rowDone' => __('Report ready', 'aivisibility-seo-geo-agent'), 'stopAllScans' => __('Stop all scans', 'aivisibility-seo-geo-agent'), 'queueStopped' => __('Scan queue stopped.', 'aivisibility-seo-geo-agent'), 'queueStopConfirm' => __('Stop the remaining scan queue?' . "\n\n" . 'The scan that is running now will continue until it finishes. AiVisibility will stop the remaining queued scans and will not start the next page.', 'aivisibility-seo-geo-agent'), 'queueStopAfterCurrent' => __('Stop requested. The current scan will finish; remaining queued scans will be stopped.', 'aivisibility-seo-geo-agent'), 'queueBackgroundNotice' => __('You can safely leave this page. AiVisibility will keep processing the scan queue in the background.', 'aivisibility-seo-geo-agent'), 'scanningNow' => __('Scanning now:', 'aivisibility-seo-geo-agent'), ), ) ); } public function register_rest_routes() { $routes = array( 'prepare-scan' => 'rest_prepare_scan', 'test-curl-source' => 'rest_test_curl_source', 'scan-post' => 'rest_scan_post', 'get-report' => 'rest_get_report', 'autofill-strategy' => 'rest_autofill_strategy', 'save-geo-mode' => 'rest_save_geo_mode', 'start-queue' => 'rest_start_queue', 'queue-status' => 'rest_queue_status', 'process-queue' => 'rest_process_queue', 'stop-queue' => 'rest_stop_queue', ); foreach ($routes as $route => $callback) { register_rest_route( 'aiseo/v1', '/' . $route, array( 'methods' => 'POST', 'callback' => array($this, $callback), 'permission_callback' => array($this, 'rest_permission'), ) ); } } public function rest_permission() { return current_user_can('manage_options'); } public function render_dashboard() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to access this page.', 'aivisibility-seo-geo-agent')); } $this->maybe_save_settings(); $business_address = (string) get_option('aiseo_business_address', ''); $business_phone = (string) get_option('aiseo_business_phone', ''); $business_email = (string) get_option('aiseo_business_email', ''); $geo_mode = (string) get_option('aiseo_geo_mode', 'mixed'); $ai_profile = 'professional'; if ('professional' !== (string) get_option('aiseo_ai_profile', 'professional')) { update_option('aiseo_ai_profile', 'professional'); } $license_key = (string) get_option('aiseo_license_key', ''); $this->sync_requests_remaining_from_api(); $requests_remaining = max(0, absint(get_option('aiseo_requests_remaining', 5))); $buy_requests_disabled = $this->buy_requests_disabled_by_license(); $seo_strategy = $this->seo_strategy(); $settings_ready = $this->settings_ready($business_address, $business_phone, $business_email, $license_key); $strategy_saved = (bool) get_option('aiseo_strategy_saved', false); $strategy_ready = $settings_ready && $strategy_saved; $all_scan_ids = array_map('absint', $this->scanner->get_scan_ids(0)); $scanned = 0; foreach ($all_scan_ids as $post_id) { $score = get_post_meta($post_id, '_aiseo_score', true); if ('' !== $score) { $scanned++; } } $not_scanned = max(0, count($all_scan_ids) - $scanned); $table_page = isset($_GET['paged']) ? max(1, absint(wp_unslash($_GET['paged']))) : 1; $per_page = max(1, min(100, (int) apply_filters('aiseo_admin_table_per_page', 20))); $excluded_scan_ids = $this->scanner->excluded_scan_post_ids(); $post_query = new WP_Query( array( 'post_type' => $this->scanner->supported_post_types(), 'post_status' => 'publish', 'posts_per_page' => $per_page, 'paged' => $table_page, 'post__not_in' => $excluded_scan_ids, 'orderby' => array( 'type' => 'ASC', 'title' => 'ASC', ), 'order' => 'ASC', 'no_found_rows' => false, ) ); $posts = $post_query->posts; $total_items = (int) $post_query->found_posts; $total_pages = max(1, (int) $post_query->max_num_pages); ?>

openai_client->has_api_key() ? esc_html(sprintf(__('On (%1$d requests = %2$d pages)', 'aivisibility-seo-geo-agent'), $requests_remaining, $requests_remaining)) : esc_html__('No site ID', 'aivisibility-seo-geo-agent'); ?>
openai_client->has_api_key()) : ?>

render_buy_requests_notice(); ?> openai_client->has_api_key()) : ?>
>

render_table_pagination($total_items, $total_pages, $table_page); ?> render_report_row($post); ?>
render_table_pagination($total_items, $total_pages, $table_page); ?>
verify_ajax_request(); $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; wp_send_json_success($this->prepare_scan_data($post_id)); } public function ajax_scan_post() { $this->verify_ajax_request(); $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; $result = $this->scan_post_data($post_id); if (is_wp_error($result)) { $this->send_ajax_error($result, $post_id); } wp_send_json_success($result); } public function ajax_test_curl_source() { $this->verify_ajax_request(); $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; $result = $this->test_curl_source_data($post_id); if (is_wp_error($result)) { $this->send_ajax_error($result, $post_id); } wp_send_json_success($result); } public function ajax_get_report() { $this->verify_ajax_request(); $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; $result = $this->get_report_data($post_id); if (is_wp_error($result)) { $this->send_ajax_error($result, $post_id); } wp_send_json_success($result); } public function ajax_autofill_strategy() { $this->verify_ajax_request(); $requested_mode = isset($_POST['seo_geo_mode']) ? sanitize_key(wp_unslash((string) $_POST['seo_geo_mode'])) : ''; $result = $this->autofill_strategy_data($requested_mode); if (is_wp_error($result)) { $this->send_ajax_error($result); } wp_send_json_success($result); } public function ajax_save_geo_mode() { $this->verify_ajax_request(); $mode = isset($_POST['geo_mode']) ? sanitize_key(wp_unslash((string) $_POST['geo_mode'])) : ''; $result = $this->save_geo_mode_data($mode); if (is_wp_error($result)) { $this->send_ajax_error($result); } wp_send_json_success($result); } public function ajax_start_queue() { $this->verify_ajax_request(); $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; $scope = isset($_POST['scope']) ? sanitize_key(wp_unslash((string) $_POST['scope'])) : 'all'; $result = $this->start_queue_data($post_id, $scope); if (is_wp_error($result)) { $this->send_ajax_error($result, $post_id); } wp_send_json_success($result); } public function ajax_stop_queue() { $this->verify_ajax_request(); $queue_id = isset($_POST['queue_id']) ? sanitize_text_field((string) wp_unslash($_POST['queue_id'])) : ''; $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; wp_send_json_success($this->stop_queue_data($queue_id, $post_id)); } public function ajax_queue_status() { $this->verify_ajax_request(); wp_send_json_success($this->queue_status_data($this->get_queue())); } public function ajax_process_queue() { $this->verify_ajax_request(); $queue_id = isset($_POST['queue_id']) ? sanitize_text_field((string) wp_unslash($_POST['queue_id'])) : ''; $result = $this->process_queue_step_data('client', $queue_id); if (is_wp_error($result)) { $this->send_ajax_error($result); } wp_send_json_success($result); } public function cron_process_queue() { $this->process_queue_step_data('cron'); } public function async_queue_tick() { $key = isset($_REQUEST['key']) ? sanitize_text_field((string) wp_unslash($_REQUEST['key'])) : ''; if (!$this->verify_queue_runner_key($key)) { status_header(403); exit; } $this->process_queue_step_data('cron'); status_header(204); exit; } public function admin_post_buy_requests() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to buy requests.', 'aivisibility-seo-geo-agent')); } if (empty($_POST['aiseo_buy_requests_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aiseo_buy_requests_nonce'])), 'aiseo_buy_requests')) { $this->set_buy_requests_notice(__('Could not verify the request. Try again.', 'aivisibility-seo-geo-agent')); wp_safe_redirect($this->checkout_return_url()); exit; } $requests = isset($_POST['aiseo_buy_requests_count']) ? absint(wp_unslash($_POST['aiseo_buy_requests_count'])) : 0; $requests = max(1, min(10000, $requests)); $license_key = $this->openai_client->license_key(); if ('' === $license_key) { $this->set_buy_requests_notice(__('Missing AiVisibility site ID.', 'aivisibility-seo-geo-agent')); wp_safe_redirect($this->checkout_return_url()); exit; } $license_status = $this->sync_license_status_from_api(); if ($this->is_wrong_license_sync_status($license_status)) { $this->set_buy_requests_notice(__('Site verification failed. Buying service credits is disabled for this domain.', 'aivisibility-seo-geo-agent')); wp_safe_redirect($this->checkout_return_url()); exit; } $endpoint = defined('AISEO_AUDITOR_STRIPE_CHECKOUT_ENDPOINT') ? AISEO_AUDITOR_STRIPE_CHECKOUT_ENDPOINT : ''; $endpoint = esc_url_raw((string) apply_filters('aiseo_stripe_checkout_endpoint', $endpoint)); if ('' === $endpoint) { $this->set_buy_requests_notice(__('Stripe checkout endpoint is not configured.', 'aivisibility-seo-geo-agent')); wp_safe_redirect($this->checkout_return_url()); exit; } $success_url = $this->checkout_return_url('success'); $cancel_url = $this->checkout_return_url('cancelled'); $body = array( 'domain' => home_url('/'), 'site_url' => home_url('/'), 'license_key' => $license_key, 'requests' => $requests, 'success_url' => $success_url, 'cancel_url' => $cancel_url, ); $response = wp_remote_post( $endpoint, array( 'timeout' => 30, 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'User-Agent' => AISEO_AUDITOR_NAME . '/' . AISEO_AUDITOR_VERSION, ), 'body' => wp_json_encode($body), ) ); if (is_wp_error($response)) { $this->set_buy_requests_notice($response->get_error_message()); wp_safe_redirect($this->checkout_return_url()); exit; } $status_code = (int) wp_remote_retrieve_response_code($response); $raw_body = (string) wp_remote_retrieve_body($response); $decoded = json_decode($raw_body, true); if ($status_code < 200 || $status_code >= 300 || !is_array($decoded) || empty($decoded['success']) || empty($decoded['checkout_url'])) { $message = is_array($decoded) && !empty($decoded['message']) ? (string) $decoded['message'] : __('Could not start Stripe checkout.', 'aivisibility-seo-geo-agent'); $this->set_buy_requests_notice($message); wp_safe_redirect($this->checkout_return_url()); exit; } $checkout_url = esc_url_raw((string) $decoded['checkout_url']); if (!$this->is_valid_stripe_checkout_url($checkout_url)) { $this->set_buy_requests_notice(__('Stripe returned an invalid checkout URL.', 'aivisibility-seo-geo-agent')); wp_safe_redirect($this->checkout_return_url()); exit; } wp_redirect($checkout_url, 303, AISEO_AUDITOR_NAME); exit; } public function rest_prepare_scan(WP_REST_Request $request) { return $this->rest_success($this->prepare_scan_data(absint($request->get_param('post_id')))); } public function rest_scan_post(WP_REST_Request $request) { $result = $this->scan_post_data(absint($request->get_param('post_id'))); if (is_wp_error($result)) { return $this->rest_error($result, absint($request->get_param('post_id'))); } return $this->rest_success($result); } public function rest_test_curl_source(WP_REST_Request $request) { $result = $this->test_curl_source_data(absint($request->get_param('post_id'))); if (is_wp_error($result)) { return $this->rest_error($result, absint($request->get_param('post_id'))); } return $this->rest_success($result); } public function rest_get_report(WP_REST_Request $request) { $result = $this->get_report_data(absint($request->get_param('post_id'))); if (is_wp_error($result)) { return $this->rest_error($result, absint($request->get_param('post_id'))); } return $this->rest_success($result); } public function rest_autofill_strategy(WP_REST_Request $request) { $result = $this->autofill_strategy_data(sanitize_key((string) $request->get_param('seo_geo_mode'))); if (is_wp_error($result)) { return $this->rest_error($result); } return $this->rest_success($result); } public function rest_save_geo_mode(WP_REST_Request $request) { $result = $this->save_geo_mode_data(sanitize_key((string) $request->get_param('geo_mode'))); if (is_wp_error($result)) { return $this->rest_error($result); } return $this->rest_success($result); } public function rest_start_queue(WP_REST_Request $request) { $post_id = absint($request->get_param('post_id')); $scope = sanitize_key((string) $request->get_param('scope')); $result = $this->start_queue_data($post_id, $scope); if (is_wp_error($result)) { return $this->rest_error($result, $post_id); } return $this->rest_success($result); } public function rest_queue_status(WP_REST_Request $request) { return $this->rest_success($this->queue_status_data($this->get_queue())); } public function rest_process_queue(WP_REST_Request $request) { $result = $this->process_queue_step_data('client', sanitize_text_field((string) $request->get_param('queue_id'))); if (is_wp_error($result)) { return $this->rest_error($result); } return $this->rest_success($result); } public function rest_stop_queue(WP_REST_Request $request) { return $this->rest_success($this->stop_queue_data( sanitize_text_field((string) $request->get_param('queue_id')), absint($request->get_param('post_id')) )); } private function start_queue_data($post_id, $scope = 'all') { $scope = sanitize_key((string) $scope); if (!in_array($scope, array('all', 'not_scanned'), true)) { $scope = 'all'; } if ($post_id) { $scope = 'single'; } $scan_data = $this->prepare_scan_data($post_id); if ('not_scanned' === $scope) { $scan_data['items'] = array_values(array_filter($scan_data['items'], function ($item) { $id = isset($item['id']) ? absint($item['id']) : 0; return $id && !$this->post_has_scan_report($id); })); $scan_data['ids'] = array_map('absint', wp_list_pluck($scan_data['items'], 'id')); $scan_data['total'] = count($scan_data['items']); } if (empty($scan_data['items'])) { if ('not_scanned' === $scope) { return new WP_Error('aiseo_queue_empty', __('No unscanned published pages, posts, or products were found for Scan & Report.', 'aivisibility-seo-geo-agent')); } return new WP_Error('aiseo_queue_empty', __('No published pages, posts, or products were found for Scan & Report.', 'aivisibility-seo-geo-agent')); } $items = array(); foreach ($scan_data['items'] as $item) { $items[] = array( 'id' => absint($item['id']), 'title' => isset($item['title']) ? sanitize_text_field((string) $item['title']) : '', 'url' => isset($item['url']) ? esc_url_raw((string) $item['url']) : '', 'run_token' => $this->new_queue_item_token(), 'status' => 'pending', 'stage' => 'scan', 'message' => '', 'error' => '', 'started_at' => '', 'completed_at' => '', ); } $queue = array( 'id' => function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : uniqid('aivis_', true), 'status' => 'running', 'scope' => $scope, 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql'), 'user_id' => get_current_user_id(), 'total' => count($items), 'current_index' => 0, 'items' => $items, 'failures' => array(), 'last_message' => __('Scan & Report queue started.', 'aivisibility-seo-geo-agent'), ); $this->clear_queue_stop_request(); $this->clear_queue_stop_pending(); $this->save_queue($queue); $this->sync_requests_remaining_from_api(); $this->continue_queue_processing($queue, 60, true); return array( 'queue' => $this->queue_status_data($queue), ); } private function stop_queue_data($requested_queue_id = '', $requested_post_id = 0) { $requested_queue_id = trim((string) $requested_queue_id); $requested_post_id = absint($requested_post_id); $queue = $this->normalize_queue($this->get_queue()); $queue_id = isset($queue['id']) ? (string) $queue['id'] : ''; if ('' !== $requested_queue_id && '' !== $queue_id && $requested_queue_id !== $queue_id) { $this->request_queue_stop($requested_queue_id, false); $this->mark_queue_stop_pending($requested_queue_id, $requested_post_id); return array( 'queue' => $this->queue_status_data($queue), ); } if (empty($queue) || 'running' !== $queue['status'] || 'single' === (isset($queue['scope']) ? sanitize_key((string) $queue['scope']) : '')) { return array( 'queue' => $this->queue_status_data($queue), ); } if (!$requested_post_id) { $requested_post_id = $this->queue_current_post_id($queue); } $this->request_queue_stop($queue_id); $this->mark_queue_stop_pending($queue_id, $requested_post_id); $queue['last_message'] = __('Stop requested. The current scan will finish; remaining queued scans will be stopped.', 'aivisibility-seo-geo-agent'); $queue['updated_at'] = current_time('mysql'); $this->save_queue_if_current($queue); return array( 'queue' => $this->queue_status_data($queue), ); } private function process_queue_step_data($source = 'client', $requested_queue_id = '') { $this->prepare_queue_worker_runtime(); $source = 'cron' === $source ? 'cron' : 'client'; $requested_queue_id = trim((string) $requested_queue_id); if ('client' === $source && '' !== $requested_queue_id) { $current_queue = $this->normalize_queue($this->get_queue()); $current_queue_id = isset($current_queue['id']) ? (string) $current_queue['id'] : ''; if ($current_queue_id !== $requested_queue_id) { return array( 'queue' => $this->queue_status_data($current_queue), 'processed' => array(), 'stale' => true, ); } } if (!$this->acquire_queue_lock()) { $this->schedule_queue_processing(30); return array( 'queue' => $this->queue_status_data($this->get_queue()), 'processed' => array(), 'locked' => true, ); } $queue = $this->normalize_queue($this->get_queue()); if (empty($queue)) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data(array()), 'processed' => array(), ); } $queue_id = isset($queue['id']) ? (string) $queue['id'] : ''; if ('client' === $source && '' !== $requested_queue_id && $queue_id !== $requested_queue_id) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($queue), 'processed' => array(), 'stale' => true, ); } if (!$this->queue_is_current($queue_id)) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($this->get_queue()), 'processed' => array(), ); } if ($this->queue_stop_requested($queue_id) && !$this->queue_stop_should_continue_current($queue, $queue_id)) { $queue = $this->stop_queue_after_current($queue); $this->save_queue_if_current($queue); $this->clear_queue_processing(); $this->clear_queue_stop_pending($queue_id); $this->clear_queue_stop_request($queue_id); $this->clear_queue_stop_request(); $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($queue), 'processed' => array(), ); } if ('running' !== $queue['status']) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($queue), 'processed' => array(), ); } $queue = $this->advance_queue_index($queue); $queue = $this->maybe_stop_queue_after_current($queue, $queue_id); if ('running' !== $queue['status']) { if (!$this->save_queue_if_current($queue)) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($this->get_queue()), 'processed' => array(), ); } $this->clear_queue_processing(); $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($queue), 'processed' => array(), ); } $index = (int) $queue['current_index']; $item = $queue['items'][$index]; $post_id = absint($item['id']); $stage = isset($item['stage']) ? sanitize_key((string) $item['stage']) : 'scan'; $run_token = isset($item['run_token']) ? sanitize_text_field((string) $item['run_token']) : ''; $this->clear_post_scan_stopped($post_id); if ('' === $run_token) { $run_token = $this->new_queue_item_token(); $queue['items'][$index]['run_token'] = $run_token; } $processed = array( 'queue_id' => $queue_id, 'run_token' => $run_token, 'post_id' => $post_id, 'stage' => $stage, 'status' => 'running', 'message' => '', ); if ('scan' === $stage) { $processed['requests_remaining'] = $this->sync_requests_remaining_from_api(); } $queue['items'][$index]['status'] = 'running'; $queue['items'][$index]['started_at'] = $queue['items'][$index]['started_at'] ? $queue['items'][$index]['started_at'] : current_time('mysql'); $queue['items'][$index]['message'] = $this->queue_stage_message($stage); $queue['updated_at'] = current_time('mysql'); if (!$this->save_queue_if_current($queue)) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($this->get_queue()), 'processed' => $this->queue_stopped_processed($post_id, $stage, $queue_id, $run_token), ); } if ('scan' === $stage) { $result = $this->test_curl_source_data($post_id); if (is_wp_error($result)) { $queue = $this->fail_queue_item($queue, $index, $result->get_error_message(), 'scan'); $processed['status'] = 'failed'; $processed['message'] = $result->get_error_message(); $processed['row'] = $this->queue_row_html($post_id); } else { $queue['items'][$index]['stage'] = 'ai'; $queue['items'][$index]['message'] = __('cURL source OK. AI Thinking...', 'aivisibility-seo-geo-agent'); $queue['updated_at'] = current_time('mysql'); $processed['status'] = 'advanced'; $processed['message'] = __('cURL source OK. AI Thinking...', 'aivisibility-seo-geo-agent'); } } elseif ('ai' === $stage) { $result = $this->scan_post_data($post_id, function () use ($queue_id, $post_id, $run_token) { return $this->queue_item_cancel_requested($queue_id, $post_id, $run_token); }); $synced_requests_remaining = !is_wp_error($result) ? $this->sync_requests_remaining_from_api() : $this->requests_remaining(); if (($this->queue_item_cancel_requested($queue_id, $post_id, $run_token)) && is_wp_error($result)) { $queue = $this->enforce_queue_stop_request($queue); $processed = $this->queue_stopped_processed($post_id, $stage, $queue_id, $run_token); } elseif (is_wp_error($result)) { $message = $this->ai_stage_error_message($result->get_error_message()); $queue = $this->fail_queue_item($queue, $index, $message, 'ai'); $queue = $this->maybe_finish_queue_for_blocking_ai_error($queue, $message); $processed['status'] = 'failed'; $processed['message'] = $message; $processed['row'] = $this->queue_row_html($post_id); $processed['requests_remaining'] = $synced_requests_remaining; } elseif ($this->queue_item_cancel_requested($queue_id, $post_id, $run_token)) { $queue = $this->enforce_queue_stop_request($queue); $processed = $this->queue_stopped_processed($post_id, $stage, $queue_id, $run_token); } elseif (empty($result['report']['ai_report'])) { $report = isset($result['report']) && is_array($result['report']) ? $result['report'] : array(); $message = !empty($report['ai_error']) ? $this->ai_stage_error_message((string) $report['ai_error']) : __('AI report is required before recommendations can be displayed.', 'aivisibility-seo-geo-agent'); $queue = $this->fail_queue_item($queue, $index, $message, 'ai'); $queue = $this->maybe_finish_queue_for_blocking_ai_error($queue, $message); $processed['status'] = 'failed'; $processed['message'] = $message; $processed['report'] = $report; $processed['row'] = isset($result['row']) ? $result['row'] : $this->queue_row_html($post_id); $processed['requests_remaining'] = $synced_requests_remaining; } else { $queue['items'][$index]['status'] = 'done'; $queue['items'][$index]['stage'] = 'done'; $queue['items'][$index]['message'] = __('Report ready.', 'aivisibility-seo-geo-agent'); $queue['items'][$index]['completed_at'] = current_time('mysql'); $queue['current_index'] = $index + 1; $queue['updated_at'] = current_time('mysql'); $processed['status'] = 'done'; $processed['message'] = __('Report ready.', 'aivisibility-seo-geo-agent'); $processed['report'] = $result['report']; $processed['row'] = isset($result['row']) ? $result['row'] : ''; $processed['requests_remaining'] = $synced_requests_remaining; } } else { $queue = $this->fail_queue_item($queue, $index, __('Unknown queue stage.', 'aivisibility-seo-geo-agent'), $stage); $processed['status'] = 'failed'; $processed['message'] = __('Unknown queue stage.', 'aivisibility-seo-geo-agent'); $processed['row'] = $this->queue_row_html($post_id); } if ($this->queue_item_cancel_requested($queue_id, $post_id, $run_token)) { $queue = $this->enforce_queue_stop_request($queue); $processed = $this->queue_stopped_processed($post_id, $stage, $queue_id, $run_token); } else { $queue = $this->advance_queue_index($queue); $queue = $this->maybe_stop_queue_after_current($queue, $queue_id); } if (!$this->save_queue_if_current($queue)) { $this->release_queue_lock(); return array( 'queue' => $this->queue_status_data($this->get_queue()), 'processed' => $this->queue_stopped_processed($post_id, $stage, $queue_id, $run_token), ); } $should_continue_queue = 'running' === $queue['status']; if (!$should_continue_queue) { $this->clear_queue_processing(); } $this->release_queue_lock(); if ($should_continue_queue) { $this->continue_queue_processing($queue, 60, true); } return array( 'queue' => $this->queue_status_data($queue), 'processed' => $processed, ); } private function schedule_queue_processing($delay = 180, $replace = false) { if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_single_event')) { return; } if ($replace) { $this->clear_queue_processing(); } if (!wp_next_scheduled('aivisseogeo_process_scan_queue')) { wp_schedule_single_event(time() + max(2, (int) $delay), 'aivisseogeo_process_scan_queue'); } } private function clear_queue_processing() { if (function_exists('wp_clear_scheduled_hook')) { wp_clear_scheduled_hook('aivisseogeo_process_scan_queue'); } } private function continue_queue_processing(array $queue, $delay = 60, $replace = false) { $queue = $this->normalize_queue($queue); if (empty($queue) || 'running' !== $queue['status']) { return; } $this->schedule_queue_processing($delay, $replace); $this->dispatch_queue_runner(); } private function dispatch_queue_runner() { if (!function_exists('wp_remote_post')) { return; } $queue = $this->normalize_queue($this->get_queue()); if (empty($queue) || 'running' !== $queue['status']) { return; } $url = add_query_arg( array( 'action' => 'aiseo_queue_tick', 'key' => $this->get_queue_runner_key(), ), admin_url('admin-post.php') ); wp_remote_post( $url, array( 'timeout' => 1, 'blocking' => false, ) ); } private function prepare_queue_worker_runtime() { if (function_exists('ignore_user_abort')) { ignore_user_abort(true); } if (function_exists('set_time_limit')) { set_time_limit((int) apply_filters('aiseo_queue_worker_time_limit', 900)); } } private function queue_runner_key_option_name() { return AISEO_AUDITOR_TABLE_PREFIX . 'queue_runner_key'; } private function get_queue_runner_key() { $key = get_option($this->queue_runner_key_option_name(), ''); $key = is_string($key) ? $key : ''; if (strlen($key) >= 32) { return $key; } if (function_exists('wp_generate_password')) { $key = wp_generate_password(48, false, false); } else { $key = md5(wp_salt('auth') . microtime(true) . wp_rand()); } update_option($this->queue_runner_key_option_name(), $key, false); return $key; } private function verify_queue_runner_key($key) { $key = (string) $key; if ('' === $key) { return false; } return hash_equals($this->get_queue_runner_key(), $key); } private function acquire_queue_lock() { $lock_name = $this->queue_lock_name(); if (get_transient($lock_name)) { return false; } $token = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : uniqid('aivis_lock_', true); set_transient($lock_name, $token, (int) apply_filters('aiseo_queue_lock_timeout', 2700)); $this->queue_lock_token = $token; return true; } private function release_queue_lock($force = false) { $lock_name = $this->queue_lock_name(); if ($force) { delete_transient($lock_name); $this->queue_lock_token = ''; return; } if ('' === $this->queue_lock_token) { return; } if (get_transient($lock_name) !== $this->queue_lock_token) { return; } delete_transient($lock_name); $this->queue_lock_token = ''; } private function queue_lock_name() { return AISEO_AUDITOR_TABLE_PREFIX . 'scan_queue_lock'; } private function get_queue() { $queue = get_option($this->queue_option_name(), array()); return is_array($queue) ? $queue : array(); } private function save_queue(array $queue) { update_option($this->queue_option_name(), $queue, false); } private function save_queue_if_current(array $queue) { $queue_id = isset($queue['id']) ? (string) $queue['id'] : ''; if ('' !== $queue_id && !$this->queue_is_current($queue_id)) { return false; } $this->save_queue($queue); return true; } private function queue_option_name() { return AISEO_AUDITOR_TABLE_PREFIX . 'scan_queue'; } private function row_action_scan_is_active() { if (null !== $this->row_action_scan_active) { return $this->row_action_scan_active; } $queue = $this->advance_queue_index($this->normalize_queue($this->get_queue())); $this->row_action_scan_active = !empty($queue) && 'running' === $queue['status']; return $this->row_action_scan_active; } private function queue_is_current($queue_id) { $queue_id = (string) $queue_id; if ('' === $queue_id) { return true; } $current_queue = $this->normalize_queue($this->get_queue()); return !empty($current_queue['id']) && (string) $current_queue['id'] === $queue_id; } private function new_queue_item_token() { return function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : uniqid('aivis_item_', true); } private function queue_item_cancel_requested($queue_id, $post_id, $run_token) { return !$this->queue_item_is_current($queue_id, $post_id, $run_token); } private function queue_item_is_current($queue_id, $post_id, $run_token) { $queue_id = (string) $queue_id; $post_id = absint($post_id); $run_token = (string) $run_token; if ('' === $queue_id || !$post_id || '' === $run_token) { return false; } $current_queue = $this->normalize_queue($this->get_queue()); if (empty($current_queue['id']) || (string) $current_queue['id'] !== $queue_id || 'running' !== $current_queue['status']) { return false; } $index = isset($current_queue['current_index']) ? max(0, (int) $current_queue['current_index']) : 0; if (empty($current_queue['items'][$index]) || !is_array($current_queue['items'][$index])) { return false; } $current_item = $current_queue['items'][$index]; $current_token = isset($current_item['run_token']) ? (string) $current_item['run_token'] : ''; return isset($current_item['id']) && absint($current_item['id']) === $post_id && hash_equals($current_token, $run_token); } private function queue_stop_flag_name($queue_id = '') { $queue_id = trim((string) $queue_id); $suffix = '' !== $queue_id ? '_' . md5($queue_id) : ''; return AISEO_AUDITOR_TABLE_PREFIX . 'scan_queue_stop_requested' . $suffix; } private function queue_stop_pending_name($queue_id = '') { $queue_id = trim((string) $queue_id); $suffix = '' !== $queue_id ? '_' . md5($queue_id) : ''; return AISEO_AUDITOR_TABLE_PREFIX . 'scan_queue_stop_pending' . $suffix; } private function request_queue_stop($queue_id = '', $set_global = true) { $ttl = defined('HOUR_IN_SECONDS') ? HOUR_IN_SECONDS : 3600; if ($set_global) { set_transient($this->queue_stop_flag_name(), 1, $ttl); } if ('' !== (string) $queue_id) { set_transient($this->queue_stop_flag_name($queue_id), 1, $ttl); } } private function mark_queue_stop_pending($queue_id = '', $post_id = 0) { $queue_id = trim((string) $queue_id); if ('' === $queue_id) { return; } $ttl = 10 * (defined('MINUTE_IN_SECONDS') ? MINUTE_IN_SECONDS : 60); $payload = array( 'queue_id' => $queue_id, 'post_id' => absint($post_id), 'until' => time() + $ttl, ); set_transient($this->queue_stop_pending_name(), $payload, $ttl); set_transient($this->queue_stop_pending_name($queue_id), $payload, $ttl); } private function clear_queue_stop_pending($queue_id = '') { $queue_id = trim((string) $queue_id); if ('' !== $queue_id) { delete_transient($this->queue_stop_pending_name($queue_id)); } delete_transient($this->queue_stop_pending_name()); } private function queue_stop_pending_data($queue_id = '') { $queue_id = trim((string) $queue_id); $payload = false; if ('' !== $queue_id) { $payload = get_transient($this->queue_stop_pending_name($queue_id)); } if (!is_array($payload)) { $payload = get_transient($this->queue_stop_pending_name()); } if (!is_array($payload)) { return array(); } $until = isset($payload['until']) ? (int) $payload['until'] : 0; if ($until <= time()) { $this->clear_queue_stop_pending($queue_id); return array(); } return array( 'queue_id' => isset($payload['queue_id']) ? (string) $payload['queue_id'] : '', 'post_id' => isset($payload['post_id']) ? absint($payload['post_id']) : 0, 'until' => $until, ); } private function clear_queue_stop_request($queue_id = '') { $queue_id = (string) $queue_id; if ('' !== $queue_id) { delete_transient($this->queue_stop_flag_name($queue_id)); return; } delete_transient($this->queue_stop_flag_name()); } private function queue_stop_requested($queue_id = '') { if ('' !== (string) $queue_id && get_transient($this->queue_stop_flag_name($queue_id))) { return true; } return (bool) get_transient($this->queue_stop_flag_name()); } private function queue_current_post_id(array $queue) { $index = isset($queue['current_index']) ? max(0, (int) $queue['current_index']) : 0; if (empty($queue['items'][$index]) || !is_array($queue['items'][$index])) { return 0; } return isset($queue['items'][$index]['id']) ? absint($queue['items'][$index]['id']) : 0; } private function queue_stop_should_continue_current(array $queue, $queue_id) { if (!$this->queue_stop_requested($queue_id)) { return false; } if ('running' !== (isset($queue['status']) ? (string) $queue['status'] : '')) { return false; } $stop_pending = $this->queue_stop_pending_data($queue_id); $pending_post_id = isset($stop_pending['post_id']) ? absint($stop_pending['post_id']) : 0; if (!$pending_post_id) { return false; } return $this->queue_current_post_id($queue) === $pending_post_id; } private function maybe_stop_queue_after_current(array $queue, $queue_id) { if (!$this->queue_stop_requested($queue_id) || $this->queue_stop_should_continue_current($queue, $queue_id)) { return $queue; } $queue = $this->stop_queue_after_current($queue); $this->clear_queue_stop_pending($queue_id); $this->clear_queue_stop_request($queue_id); $this->clear_queue_stop_request(); return $queue; } private function stop_queue_after_current(array $queue) { $queue['status'] = 'stopped'; $queue['last_message'] = __('Scan queue stopped. The current scan finished; remaining queued scans were not started.', 'aivisibility-seo-geo-agent'); $queue['updated_at'] = current_time('mysql'); unset($queue['client_active_until']); return $queue; } private function enforce_queue_stop_request(array $queue) { $queue_id = isset($queue['id']) ? (string) $queue['id'] : ''; $index = isset($queue['current_index']) ? max(0, (int) $queue['current_index']) : 0; if (!empty($queue['items'][$index]) && is_array($queue['items'][$index])) { $post_id = isset($queue['items'][$index]['id']) ? absint($queue['items'][$index]['id']) : 0; $stage = isset($queue['items'][$index]['stage']) ? sanitize_key((string) $queue['items'][$index]['stage']) : ''; $queue['items'][$index]['status'] = 'stopped'; $queue['items'][$index]['message'] = __('Scan queue stopped.', 'aivisibility-seo-geo-agent'); $this->mark_post_scan_stopped($post_id, $stage, $queue_id); } $queue['status'] = 'stopped'; $queue['last_message'] = __('Scan queue stopped.', 'aivisibility-seo-geo-agent'); $queue['updated_at'] = current_time('mysql'); unset($queue['client_active_until']); return $queue; } private function queue_stopped_processed($post_id, $stage, $queue_id = '', $run_token = '') { $this->mark_post_scan_stopped($post_id, $stage, $queue_id); $this->clear_queue_stop_pending($queue_id); return array( 'queue_id' => (string) $queue_id, 'run_token' => (string) $run_token, 'post_id' => absint($post_id), 'stage' => sanitize_key((string) $stage), 'status' => 'stopped', 'message' => __('Scan queue stopped.', 'aivisibility-seo-geo-agent'), ); } private function normalize_queue(array $queue) { if (empty($queue['items']) || !is_array($queue['items'])) { return array(); } $queue['status'] = isset($queue['status']) ? sanitize_key((string) $queue['status']) : 'running'; $queue['current_index'] = isset($queue['current_index']) ? max(0, (int) $queue['current_index']) : 0; $queue['total'] = count($queue['items']); return $queue; } private function advance_queue_index(array $queue) { $items = isset($queue['items']) && is_array($queue['items']) ? $queue['items'] : array(); $index = isset($queue['current_index']) ? max(0, (int) $queue['current_index']) : 0; $total = count($items); while ($index < $total && !empty($items[$index]['status']) && in_array($items[$index]['status'], array('done', 'failed'), true)) { $index++; } $queue['current_index'] = $index; if ($index >= $total) { $queue['status'] = 'complete'; $queue['last_message'] = __('Scan & Report queue completed.', 'aivisibility-seo-geo-agent'); $queue['updated_at'] = current_time('mysql'); } return $queue; } private function fail_queue_item(array $queue, $index, $message, $stage) { $post_id = isset($queue['items'][$index]['id']) ? absint($queue['items'][$index]['id']) : 0; $failure = array( 'post_id' => $post_id, 'title' => isset($queue['items'][$index]['title']) ? (string) $queue['items'][$index]['title'] : '', 'stage' => sanitize_key((string) $stage), 'message' => (string) $message, 'failed_at' => current_time('mysql'), ); $queue['items'][$index]['status'] = 'failed'; $queue['items'][$index]['stage'] = 'failed'; $queue['items'][$index]['error'] = (string) $message; $queue['items'][$index]['message'] = (string) $message; $queue['items'][$index]['completed_at'] = current_time('mysql'); $queue['current_index'] = (int) $index + 1; $queue['updated_at'] = current_time('mysql'); $queue['last_message'] = (string) $message; if (empty($queue['failures']) || !is_array($queue['failures'])) { $queue['failures'] = array(); } $queue['failures'][] = $failure; return $queue; } private function maybe_finish_queue_for_blocking_ai_error(array $queue, $message) { $blocked_label = $this->scan_blocked_last_scan_label($message); if ('' === $blocked_label) { return $queue; } $queue['status'] = 'complete'; $queue['last_message'] = $blocked_label; $queue['updated_at'] = current_time('mysql'); unset($queue['client_active_until']); return $queue; } private function queue_status_data(array $queue) { $queue = $this->normalize_queue($queue); $scan_totals = $this->scan_totals(); $queue_id_for_pending = isset($queue['id']) ? (string) $queue['id'] : ''; $stop_pending = $this->queue_stop_pending_data($queue_id_for_pending); if (empty($queue)) { return array( 'active' => false, 'status' => 'idle', 'scope' => '', 'total' => 0, 'completed' => 0, 'failed' => 0, 'remaining' => 0, 'progress_percent' => 0, 'current_item' => null, 'stop_pending' => !empty($stop_pending), 'stop_pending_queue_id' => isset($stop_pending['queue_id']) ? (string) $stop_pending['queue_id'] : '', 'stop_pending_post_id' => isset($stop_pending['post_id']) ? absint($stop_pending['post_id']) : 0, 'stop_pending_until' => isset($stop_pending['until']) ? (int) $stop_pending['until'] : 0, 'all_count' => (int) $scan_totals['all'], 'scanned_count' => (int) $scan_totals['scanned'], 'not_scanned_count' => (int) $scan_totals['not_scanned'], ); } $queue = $this->advance_queue_index($queue); $counts = $this->queue_counts($queue); $current_item = null; if ('running' === $queue['status'] && isset($queue['items'][$queue['current_index']])) { $current_item = $this->queue_item_response($queue['items'][$queue['current_index']]); } $progress_units = (int) $counts['completed'] + (int) $counts['failed']; if ('running' === $queue['status'] && $current_item) { $stage = isset($current_item['stage']) ? (string) $current_item['stage'] : 'scan'; $stage_fraction = array( 'scan' => 0.15, 'ai' => 0.55, ); $progress_units += isset($stage_fraction[$stage]) ? $stage_fraction[$stage] : 0; } $progress_percent = $counts['total'] ? min(100, max(0, (int) round(($progress_units / (int) $counts['total']) * 100))) : 0; return array( 'active' => 'running' === $queue['status'], 'id' => isset($queue['id']) ? (string) $queue['id'] : '', 'status' => $queue['status'], 'scope' => isset($queue['scope']) ? sanitize_key((string) $queue['scope']) : 'all', 'created_at' => isset($queue['created_at']) ? (string) $queue['created_at'] : '', 'updated_at' => isset($queue['updated_at']) ? (string) $queue['updated_at'] : '', 'total' => (int) $counts['total'], 'completed' => (int) $counts['completed'], 'failed' => (int) $counts['failed'], 'remaining' => (int) $counts['remaining'], 'progress_percent' => $progress_percent, 'current_index' => isset($queue['current_index']) ? (int) $queue['current_index'] : 0, 'current_item' => $current_item, 'last_message' => isset($queue['last_message']) ? (string) $queue['last_message'] : '', 'failures' => !empty($queue['failures']) && is_array($queue['failures']) ? array_slice($queue['failures'], -10) : array(), 'stop_pending' => !empty($stop_pending), 'stop_pending_queue_id' => isset($stop_pending['queue_id']) ? (string) $stop_pending['queue_id'] : '', 'stop_pending_post_id' => isset($stop_pending['post_id']) ? absint($stop_pending['post_id']) : 0, 'stop_pending_until' => isset($stop_pending['until']) ? (int) $stop_pending['until'] : 0, 'all_count' => (int) $scan_totals['all'], 'scanned_count' => (int) $scan_totals['scanned'], 'not_scanned_count' => (int) $scan_totals['not_scanned'], 'requests_remaining' => $this->requests_remaining(), ); } private function queue_counts(array $queue) { $items = isset($queue['items']) && is_array($queue['items']) ? $queue['items'] : array(); $completed = 0; $failed = 0; foreach ($items as $item) { $status = isset($item['status']) ? (string) $item['status'] : ''; if ('done' === $status) { $completed++; } elseif ('failed' === $status) { $failed++; } } $total = count($items); return array( 'total' => $total, 'completed' => $completed, 'failed' => $failed, 'remaining' => max(0, $total - $completed - $failed), ); } private function queue_item_response(array $item) { $stage = isset($item['stage']) ? sanitize_key((string) $item['stage']) : 'scan'; return array( 'id' => isset($item['id']) ? absint($item['id']) : 0, 'title' => isset($item['title']) ? (string) $item['title'] : '', 'url' => isset($item['url']) ? (string) $item['url'] : '', 'run_token' => isset($item['run_token']) ? (string) $item['run_token'] : '', 'status' => isset($item['status']) ? (string) $item['status'] : 'pending', 'stage' => $stage, 'message' => isset($item['message']) && '' !== $item['message'] ? (string) $item['message'] : $this->queue_stage_message($stage), 'error' => isset($item['error']) ? (string) $item['error'] : '', ); } private function queue_stage_message($stage) { if ('ai' === $stage) { return __('AI Thinking...', 'aivisibility-seo-geo-agent'); } return __('Scan...', 'aivisibility-seo-geo-agent'); } private function queue_row_html($post_id) { $post = get_post(absint($post_id)); return $post ? $this->render_report_row($post) : ''; } private function scan_totals() { $all_ids = array_map('absint', $this->scanner->get_scan_ids(0)); $scanned = 0; foreach ($all_ids as $post_id) { if ($this->post_has_scan_report($post_id)) { $scanned++; } } return array( 'all' => count($all_ids), 'scanned' => $scanned, 'not_scanned' => max(0, count($all_ids) - $scanned), ); } private function scanned_count() { $totals = $this->scan_totals(); return (int) $totals['scanned']; } private function post_has_scan_report($post_id) { $post_id = absint($post_id); if ($this->post_scan_stopped($post_id)) { return false; } $report = get_post_meta($post_id, '_aiseo_report', true); return is_array($report) && isset($report['ai_report']) && is_array($report['ai_report']); } private function mark_post_scan_stopped($post_id, $stage = '', $queue_id = '') { $post_id = absint($post_id); if (!$post_id) { return; } update_post_meta($post_id, '_aiseo_scan_stopped', array( 'at' => current_time('mysql'), 'stage' => sanitize_key((string) $stage), 'queue_id' => sanitize_text_field((string) $queue_id), )); } private function clear_post_scan_stopped($post_id) { $post_id = absint($post_id); if (!$post_id) { return; } delete_post_meta($post_id, '_aiseo_scan_stopped'); } private function post_scan_stopped($post_id, $scan_time = '') { $post_id = absint($post_id); if (!$post_id) { return array(); } $stopped = get_post_meta($post_id, '_aiseo_scan_stopped', true); if (!is_array($stopped) || empty($stopped['at'])) { return array(); } if ('' !== (string) $scan_time && strcmp((string) $scan_time, (string) $stopped['at']) > 0) { return array(); } return $stopped; } private function ai_stage_error_message($message) { if ($this->is_no_requests_message($message)) { return __('No requests', 'aivisibility-seo-geo-agent'); } if (preg_match('/HTTP\s+5\d\d|Service Unavailable|timed out|timeout/i', (string) $message)) { return sprintf( __('cURL source test completed successfully, so the public page source is readable. The failure happened during the AI analysis request or the long-running WordPress REST/PHP response. Details: %s', 'aivisibility-seo-geo-agent'), (string) $message ); } return (string) $message; } private function is_no_requests_message($message) { return (bool) preg_match('/no\s+(?:request\s+credits?|requests?|credits?)|request\s+credits?\s+remaining/i', (string) $message); } private function is_license_error_message($message) { return (bool) preg_match('/licen[cs]e|unauthori[sz]ed|forbidden|not\s+allowed|invalid\s+(?:api\s+)?key|missing\s+(?:api\s+)?key|HTTP\s+40[13]/i', (string) $message); } private function scan_blocked_last_scan_label($message) { if ($this->is_no_requests_message($message)) { return __('No requests', 'aivisibility-seo-geo-agent'); } if ($this->is_license_error_message($message)) { return __('Site verification failed', 'aivisibility-seo-geo-agent'); } return ''; } private function prepare_scan_data($post_id) { $ids = $this->scanner->get_scan_ids($post_id); $items = array(); foreach ($ids as $id) { $items[] = array( 'id' => absint($id), 'url' => get_permalink(absint($id)), 'title' => get_the_title(absint($id)), ); } return array( 'ids' => array_map('absint', $ids), 'items' => $items, 'total' => count($ids), ); } private function scan_post_data($post_id, $should_cancel = null) { $this->clear_post_scan_stopped($post_id); $report = $this->scanner->scan_post($post_id, $should_cancel); if (is_wp_error($report)) { return $report; } $post = get_post($post_id); return array( 'post_id' => $post_id, 'report' => $report, 'row' => $post ? $this->render_report_row($post) : '', 'requests_remaining' => $this->requests_remaining(), ); } private function test_curl_source_data($post_id) { return $this->scanner->test_curl_source($post_id); } private function get_report_data($post_id) { $report = $this->scanner->get_report($post_id); if (!$report) { return new WP_Error('aiseo_no_report', __('No report found for this page yet.', 'aivisibility-seo-geo-agent')); } $post = get_post($post_id); return array( 'report' => $report, 'row' => $post ? $this->render_report_row($post) : '', 'requests_remaining' => $this->requests_remaining(), ); } private function autofill_strategy_data($requested_mode = '') { if (!$this->openai_client->has_api_key()) { return new WP_Error( 'aiseo_missing_license_key', __('The AiVisibility site ID is missing. Reactivate the plugin before using AI strategy fill.', 'aivisibility-seo-geo-agent') ); } $payload = $this->scanner->build_strategy_autofill_payload($requested_mode); if (is_wp_error($payload)) { return $payload; } $result = $this->openai_client->autofill_strategy($payload); if (is_wp_error($result)) { return $result; } return $this->normalize_autofill_strategy_result($result, $requested_mode); } private function normalize_autofill_strategy_result(array $result, $requested_mode = '') { $business = isset($result['business']) && is_array($result['business']) ? $result['business'] : array(); $strategy_input = isset($result['seo_strategy']) && is_array($result['seo_strategy']) ? $result['seo_strategy'] : array(); $current_mode = sanitize_key((string) $requested_mode); if (!in_array($current_mode, array('local', 'generic', 'mixed'), true)) { $current_mode = (string) get_option('aiseo_geo_mode', 'mixed'); } $current_mode = in_array($current_mode, array('local', 'generic', 'mixed'), true) ? $current_mode : 'mixed'; $mode = $current_mode; if ('' === sanitize_key((string) $requested_mode) && isset($result['seo_geo_mode'])) { $candidate_mode = sanitize_key((string) $result['seo_geo_mode']); if (in_array($candidate_mode, array('local', 'generic', 'mixed'), true)) { $mode = $candidate_mode; } } return array( 'message' => __('AI filled the strategy fields from the homepage. Review the values, adjust anything needed, then Save settings.', 'aivisibility-seo-geo-agent'), 'homepage_url' => home_url('/'), 'business' => array( 'address' => isset($business['address']) ? sanitize_text_field((string) $business['address']) : '', 'phone' => isset($business['phone']) ? sanitize_text_field((string) $business['phone']) : '', 'email' => isset($business['email']) ? sanitize_email((string) $business['email']) : '', ), 'seo_geo_mode' => $mode, 'seo_strategy' => $this->sanitize_seo_strategy($strategy_input), 'summary' => isset($result['summary']) ? sanitize_textarea_field((string) $result['summary']) : '', 'ai_execution' => isset($result['ai_execution']) && is_array($result['ai_execution']) ? $result['ai_execution'] : array(), ); } private function save_geo_mode_data($mode) { $mode = sanitize_key((string) $mode); if (!in_array($mode, array('local', 'generic', 'mixed'), true)) { return new WP_Error( 'aiseo_invalid_geo_mode', __('Choose a valid SEO-GEO mode.', 'aivisibility-seo-geo-agent') ); } update_option('aiseo_geo_mode', $mode); return array( 'geo_mode' => $mode, 'message' => __('SEO-GEO mode saved. Reloading the strategy fields...', 'aivisibility-seo-geo-agent'), ); } private function requests_remaining() { return max(0, absint(get_option('aiseo_requests_remaining', 0))); } private function scan_action_label() { return __('Scan & Report', 'aivisibility-seo-geo-agent'); } private function sync_license_status_from_api() { if (class_exists('AISEO_Plugin') && $this->openai_client->has_api_key()) { $result = AISEO_Plugin::sync_requests_remaining(); return is_array($result) ? $result : array(); } return $this->first_requests_status(); } private function sync_requests_remaining_from_api() { $this->sync_license_status_from_api(); return $this->requests_remaining(); } private function buy_requests_disabled_by_license() { return $this->is_wrong_license_sync_status($this->first_requests_status()); } private function first_requests_status() { $status = get_option('aiseo_first_requests_status', array()); return is_array($status) ? $status : array(); } private function is_wrong_license_sync_status($status) { if (!is_array($status)) { return false; } $message = isset($status['message']) ? strtolower(trim((string) $status['message'])) : ''; return 'wrong license' === $message; } private function rest_success(array $data) { return new WP_REST_Response( array( 'success' => true, 'data' => $data, ), 200 ); } private function rest_error(WP_Error $error, $post_id = 0) { return new WP_REST_Response( array( 'success' => false, 'data' => array( 'message' => $error->get_error_message(), 'post_id' => absint($post_id), 'code' => $error->get_error_code(), 'requests_remaining' => $this->requests_remaining(), ), ), $this->error_http_status($error) ); } private function send_ajax_error(WP_Error $error, $post_id = 0) { wp_send_json_error( array( 'message' => $error->get_error_message(), 'post_id' => absint($post_id), 'code' => $error->get_error_code(), 'requests_remaining' => $this->requests_remaining(), ), $this->error_http_status($error) ); } private function error_http_status(WP_Error $error) { $code = (string) $error->get_error_code(); if ('aiseo_no_requests' === $code) { return 402; } if ('aiseo_wrong_license' === $code) { return 403; } if (false !== stripos($code, 'permission') || false !== stripos($code, 'forbidden')) { return 403; } if (false !== stripos($code, 'not_found') || 'aiseo_no_report' === $code) { return 404; } if (false !== stripos($code, 'timeout')) { return 504; } if (false !== stripos($code, 'curl') || false !== stripos($code, 'fetch')) { return 503; } return 400; } private function seo_strategy() { $saved = get_option('aiseo_seo_strategy', array()); if (!is_array($saved)) { $saved = array(); } return array_merge($this->default_seo_strategy(), array_intersect_key($saved, $this->default_seo_strategy())); } private function default_seo_strategy() { return array( 'business_description' => '', 'main_offer' => '', 'target_audience' => '', 'conversion_goal' => '', 'tone' => '', 'avoid_keywords' => '', 'local_main_area' => '', 'local_service_areas' => '', 'local_services' => '', 'local_keyword_strategy' => 'ai_decide', 'local_schema_focus' => 'ai_decide', 'generic_target_market' => '', 'generic_topic_category' => '', 'generic_intent_focus' => 'ai_decide', 'generic_primary_keywords' => '', 'generic_entity_terms' => '', 'generic_local_terms' => 'avoid', 'mixed_primary_generic_keyword' => '', 'mixed_local_modifier_strategy' => 'ai_decide', 'mixed_priority_split' => 'balanced', 'mixed_generic_pages' => '', 'mixed_local_pages' => '', 'mixed_schema_mix' => 'ai_decide', ); } private function strategy_value(array $strategy, $key) { return isset($strategy[$key]) ? (string) $strategy[$key] : ''; } private function sanitize_seo_strategy($input) { if (!is_array($input)) { $input = array(); } $strategy = $this->default_seo_strategy(); $text_fields = array( 'business_description', 'main_offer', 'target_audience', 'conversion_goal', 'tone', 'avoid_keywords', 'local_main_area', 'local_service_areas', 'local_services', 'generic_target_market', 'generic_topic_category', 'generic_primary_keywords', 'generic_entity_terms', 'mixed_primary_generic_keyword', 'mixed_generic_pages', 'mixed_local_pages', ); foreach ($text_fields as $key) { if (isset($input[$key])) { $strategy[$key] = sanitize_textarea_field((string) $input[$key]); } } $select_fields = array( 'local_keyword_strategy' => array('ai_decide', 'city_area_terms', 'service_area_terms', 'avoid_local_modifiers'), 'local_schema_focus' => array('ai_decide', 'localbusiness', 'professionalservice', 'organization'), 'generic_intent_focus' => array('ai_decide', 'commercial', 'informational', 'mixed'), 'generic_local_terms' => array('avoid', 'brand_only', 'ai_decide'), 'mixed_local_modifier_strategy' => array('ai_decide', 'light', 'strong', 'schema_only'), 'mixed_priority_split' => array('balanced', 'generic_70', 'local_70'), 'mixed_schema_mix' => array('ai_decide', 'organization', 'organization_localbusiness'), ); foreach ($select_fields as $key => $allowed_values) { if (!isset($input[$key])) { continue; } $value = sanitize_key((string) $input[$key]); if (in_array($value, $allowed_values, true)) { $strategy[$key] = $value; } } return $strategy; } private function maybe_save_settings() { if (empty($_POST['aiseo_save_settings'])) { return; } if (empty($_POST['aiseo_settings_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aiseo_settings_nonce'])), 'aiseo_save_settings')) { return; } $business_address = isset($_POST['aiseo_business_address']) ? sanitize_text_field(wp_unslash((string) $_POST['aiseo_business_address'])) : ''; $business_phone = isset($_POST['aiseo_business_phone']) ? sanitize_text_field(wp_unslash((string) $_POST['aiseo_business_phone'])) : ''; $business_email = isset($_POST['aiseo_business_email']) ? sanitize_email(wp_unslash((string) $_POST['aiseo_business_email'])) : ''; $geo_mode = isset($_POST['aiseo_geo_mode']) ? sanitize_key(wp_unslash((string) $_POST['aiseo_geo_mode'])) : 'mixed'; $ai_profile = 'professional'; $strategy_submitted = isset($_POST['aiseo_seo_strategy']); if ($strategy_submitted) { $seo_strategy_input = wp_unslash($_POST['aiseo_seo_strategy']); $seo_strategy = $this->sanitize_seo_strategy($seo_strategy_input); } else { $seo_strategy = $this->seo_strategy(); } if (!in_array($geo_mode, array('local', 'generic', 'mixed'), true)) { $geo_mode = 'mixed'; } update_option('aiseo_business_address', $business_address); update_option('aiseo_business_phone', $business_phone); update_option('aiseo_business_email', $business_email); update_option('aiseo_geo_mode', $geo_mode); update_option('aiseo_ai_profile', $ai_profile); update_option('aiseo_seo_strategy', $seo_strategy, false); if ($strategy_submitted) { update_option('aiseo_strategy_saved', '1', false); } echo '

' . esc_html(sprintf(__('%s settings saved.', 'aivisibility-seo-geo-agent'), AISEO_AUDITOR_NAME)) . '

'; } private function checkout_return_url($status = '') { $url = add_query_arg('page', 'aiseo-auditor', admin_url('admin.php')); if ('' !== $status) { $url = add_query_arg('aiseo_checkout', sanitize_key((string) $status), $url); } return $url; } private function set_buy_requests_notice($message, $type = 'error') { $type = in_array($type, array('success', 'warning', 'error', 'info'), true) ? $type : 'error'; set_transient( $this->buy_requests_notice_key(), array( 'message' => sanitize_text_field((string) $message), 'type' => $type, ), 5 * MINUTE_IN_SECONDS ); } private function render_buy_requests_notice() { $notice = get_transient($this->buy_requests_notice_key()); if ($notice) { delete_transient($this->buy_requests_notice_key()); } if (!$notice && isset($_GET['aiseo_checkout'])) { $checkout_status = sanitize_key((string) wp_unslash($_GET['aiseo_checkout'])); if ('success' === $checkout_status) { $notice = array( 'message' => __('Stripe checkout completed. Requests will update after Stripe confirms the payment.', 'aivisibility-seo-geo-agent'), 'type' => 'success', ); } elseif ('cancelled' === $checkout_status) { $notice = array( 'message' => __('Stripe checkout was cancelled.', 'aivisibility-seo-geo-agent'), 'type' => 'warning', ); } } if (!$notice || empty($notice['message'])) { return; } $type = isset($notice['type']) ? sanitize_key((string) $notice['type']) : 'info'; $type = in_array($type, array('success', 'warning', 'error', 'info'), true) ? $type : 'info'; echo '

' . esc_html((string) $notice['message']) . '

'; } private function buy_requests_notice_key() { return 'aiseo_buy_requests_notice_' . get_current_user_id(); } private function is_valid_stripe_checkout_url($url) { $parts = wp_parse_url((string) $url); $scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) : ''; $host = isset($parts['host']) ? strtolower((string) $parts['host']) : ''; return 'https' === $scheme && (bool) preg_match('/(^|\.)stripe\.com$/', $host); } private function settings_ready($business_address, $business_phone, $business_email, $license_key) { $geo_mode = get_option('aiseo_geo_mode', false); $ai_profile = get_option('aiseo_ai_profile', false); return '' !== trim((string) $business_address) && '' !== trim((string) $business_phone) && '' !== trim((string) $business_email) && '' !== trim((string) $license_key) && in_array((string) $geo_mode, array('local', 'generic', 'mixed'), true) && 'professional' === (string) $ai_profile; } private function verify_ajax_request() { check_ajax_referer('aiseo_admin_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error( array( 'message' => __('You do not have permission to run SEO scans.', 'aivisibility-seo-geo-agent'), ), 403 ); } } private function render_table_pagination($total_items, $total_pages, $current_page) { $total_items = max(0, absint($total_items)); $total_pages = max(1, absint($total_pages)); $current_page = max(1, min($total_pages, absint($current_page))); if ($total_items < 1) { return ''; } $links = array(); if ($total_pages > 1) { $base_url = add_query_arg('page', 'aiseo-auditor', admin_url('admin.php')); $links = paginate_links( array( 'base' => add_query_arg('paged', '%#%', $base_url), 'format' => '', 'current' => $current_page, 'total' => $total_pages, 'type' => 'array', 'prev_text' => '‹', 'next_text' => '›', ) ); $links = is_array($links) ? $links : array(); } ob_start(); ?>
getTimestamp(); } catch (Exception $exception) { $timestamp = strtotime($datetime); return $timestamp ? (int) $timestamp : 0; } } private function report_button($post_id, $has_report) { return sprintf( '', esc_attr((string) $post_id), $has_report ? '' : ' disabled', esc_html__('Report', 'aivisibility-seo-geo-agent') ); } private function render_report_row(WP_Post $post) { $report = $this->scanner->get_report($post->ID); $score = isset($report['score']) ? (int) $report['score'] : null; $scan_time = isset($report['scanned_at']) ? $report['scanned_at'] : ''; $stopped = $this->post_scan_stopped($post->ID, $scan_time); $ai_error = isset($report['ai_error']) ? (string) $report['ai_error'] : ''; $ai_ready = isset($report['ai_report']) && is_array($report['ai_report']); $ai_error_only = empty($stopped) && !$ai_ready && '' !== $ai_error; $last_scan_notice = $ai_error_only ? $this->scan_blocked_last_scan_label($ai_error) : ''; $has_report_action = !empty($report) && $ai_ready; $display_time = !empty($stopped['at']) ? (string) $stopped['at'] : $scan_time; $display_timestamp = $this->local_datetime_timestamp($display_time); $score_class = !empty($stopped) ? 'is-stopped' : ($ai_ready ? 'is-good' : (null === $score ? '' : $this->score_class($score))); $score_label = !empty($stopped) ? __('Stopped', 'aivisibility-seo-geo-agent') : ($ai_ready ? __('Green', 'aivisibility-seo-geo-agent') : $this->score_status_label($score)); ob_start(); ?>
|
post_type); ?> - — > report_button($post->ID, $has_report_action); ?> = 80) { return 'is-good'; } if ($score >= 55) { return 'is-mid'; } return 'is-low'; } private function score_status_label($score) { if (null === $score) { return '-'; } if ($score >= 80) { return __('Green', 'aivisibility-seo-geo-agent'); } if ($score >= 55) { return __('Orange', 'aivisibility-seo-geo-agent'); } return __('Red', 'aivisibility-seo-geo-agent'); } }