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